mirror of
https://github.com/google/blockly.git
synced 2026-04-26 07:00:23 +02:00
Merge pull request #9671 from grega/docs
chore(docs): migrate to Docusaurus and GitHub Pages
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# .github/workflows/deploy-docusaurus.yml
|
||||
# This workflow deploys the Blockly documentation to GitHub Pages.
|
||||
# Run this manually after a release to publish updated documentation.
|
||||
|
||||
name: Deploy Docusaurus to GitHub Pages
|
||||
|
||||
on:
|
||||
# To run: GitHub -> Actions -> "Deploy Docusaurus to GitHub Pages" -> Run workflow
|
||||
# Optionally set `ref` to the release branch/tag
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Branch, tag, or commit SHA to deploy (defaults to main)"
|
||||
required: false
|
||||
default: "main"
|
||||
type: string
|
||||
|
||||
# Sets the permissions for the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued
|
||||
# However, do not cancel in-progress runs as we want to allow these production deployments to complete
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout your repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || 'main' }}
|
||||
# Allow Docusaurus to view the full commit history (required for "last edited at <date> by <person>" functionality)
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: "package-lock.json" # root level, since we're using npm workspaces
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate reference docs
|
||||
working-directory: ./packages/blockly
|
||||
run: |
|
||||
npx gulp typings
|
||||
npm run docs
|
||||
|
||||
- name: Build the Docusaurus site
|
||||
working-directory: ./packages/docs
|
||||
run: npm run build
|
||||
env:
|
||||
# When deploying to a subdirectory of your <org|name>.github.io domain, the BASE_URL
|
||||
# must be set to the name of the repo, go to your repo → Settings → Environments:
|
||||
# Open the github-pages environment, under Environment variables, add: PAGES_BASE_URL
|
||||
BASE_URL: ${{ vars.PAGES_BASE_URL || '/docs/' }}
|
||||
|
||||
- name: Setup GitHub Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./packages/docs/build
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
Generated
+25697
-6288
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,10 @@
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
"@commitlint/config-conventional": "^20.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"eslint": "9.36.0",
|
||||
"prettier": "3.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run test --ws --if-present",
|
||||
"lint": "npm run lint --ws --if-present",
|
||||
|
||||
@@ -34,7 +34,11 @@ import {
|
||||
} from './scripts/gulpfiles/build_tasks.mjs';
|
||||
import {docs} from './scripts/gulpfiles/docs_tasks.mjs';
|
||||
import {updateGithubPages} from './scripts/gulpfiles/git_tasks.mjs';
|
||||
import {cleanReleaseDir, pack} from './scripts/gulpfiles/package_tasks.mjs';
|
||||
import {
|
||||
cleanReleaseDir,
|
||||
pack,
|
||||
typings,
|
||||
} from './scripts/gulpfiles/package_tasks.mjs';
|
||||
import {publish, publishBeta} from './scripts/gulpfiles/release_tasks.mjs';
|
||||
import {
|
||||
generators,
|
||||
@@ -78,4 +82,5 @@ export {
|
||||
interactiveMocha,
|
||||
buildAdvancedCompilationTest,
|
||||
docs,
|
||||
typings,
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"concurrently": "^9.0.1",
|
||||
"conventional-changelog-conventionalcommits": "^7.0.2",
|
||||
"conventional-recommended-bump": "^9.0.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint": "9.36.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-jsdoc": "^52.0.2",
|
||||
@@ -135,7 +135,7 @@
|
||||
"markdown-tables-to-json": "^0.1.7",
|
||||
"mocha": "^11.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"puppeteer-core": "^24.17.0",
|
||||
"readline-sync": "^1.4.10",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {execSync} from 'child_process';
|
||||
import {Extractor} from 'markdown-tables-to-json';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as gulp from 'gulp';
|
||||
import header from 'gulp-header';
|
||||
import replace from 'gulp-replace';
|
||||
import rename from 'gulp-rename';
|
||||
|
||||
const DOCS_DIR = 'docs';
|
||||
const DOCS_DIR = '../docs/docs/reference';
|
||||
const REFERENCE_SIDEBAR_DIR = DOCS_DIR;
|
||||
|
||||
/**
|
||||
* Run API Extractor to generate the intermediate json file.
|
||||
@@ -30,9 +31,7 @@ const removeRenames = function() {
|
||||
*/
|
||||
const generateDocs = function(done) {
|
||||
if (!fs.existsSync(DOCS_DIR)) {
|
||||
// Create the directory if it doesn't exist.
|
||||
// If it already exists, the contents will be deleted by api-documenter.
|
||||
fs.mkdirSync(DOCS_DIR);
|
||||
fs.mkdirSync(DOCS_DIR, {recursive: true});
|
||||
}
|
||||
execSync(
|
||||
`api-documenter markdown --input-folder temp --output-folder ${DOCS_DIR}`,
|
||||
@@ -41,15 +40,282 @@ const generateDocs = function(done) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends the project and book metadata that devsite requires.
|
||||
* Extracts the title from the H2 heading in the content.
|
||||
* Falls back to filename-based title if H2 not found.
|
||||
*/
|
||||
const prependBook = function() {
|
||||
return gulp.src('docs/*.md')
|
||||
.pipe(header(
|
||||
'Project: /blockly/_project.yaml\nBook: /blockly/_book.yaml\n\n'))
|
||||
const extractTitleFromContent = function(content, filename) {
|
||||
// Remove frontmatter if exists
|
||||
let cleanContent = content.replace(/^---[\s\S]*?---\n\n/, '');
|
||||
|
||||
// Remove MDX comments
|
||||
cleanContent = cleanContent.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
|
||||
|
||||
// Find the first ## heading
|
||||
const headingMatch = cleanContent.match(/##\s+(.+)/);
|
||||
if (headingMatch) {
|
||||
// Get the full H2 heading text
|
||||
let fullTitle = headingMatch[1].trim();
|
||||
// Remove markdown links
|
||||
fullTitle = fullTitle.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
||||
// Remove inline code backticks
|
||||
fullTitle = fullTitle.replace(/`([^`]+)`/g, '$1');
|
||||
|
||||
// Simplify title: "BlocklyOptions.comments property" -> "Comments property"
|
||||
// Extract the last part after the last dot
|
||||
const parts = fullTitle.split('.');
|
||||
if (parts.length > 1) {
|
||||
// Get everything after the last dot
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
return fullTitle;
|
||||
}
|
||||
|
||||
// Fallback to filename-based title
|
||||
return extractTitle(filename);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts a clean title from the filename.
|
||||
* Example: "blockly.block_class" -> "Block class"
|
||||
* Example: "blockly.block_class.addicon_1_method" -> "Addicon method"
|
||||
*/
|
||||
const extractTitle = function(filename) {
|
||||
const nameWithoutExt = filename.replace('.mdx', '').replace('.md', '');
|
||||
const parts = nameWithoutExt.split('.');
|
||||
|
||||
if (parts.length === 2) {
|
||||
// Top-level page: blockly.block_class -> "Block class"
|
||||
let name = parts[1];
|
||||
// Remove suffixes like _class, _namespace, etc.
|
||||
const suffix = name.match(/_(class|namespace|interface|enum|type|variable)$/);
|
||||
name = name.replace(/_(class|namespace|interface|enum|type|variable)$/, '');
|
||||
|
||||
// Split by underscores and capitalize each word
|
||||
const words = name.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
);
|
||||
|
||||
// Add back the suffix with proper spacing
|
||||
if (suffix) {
|
||||
words.push(suffix[1]);
|
||||
}
|
||||
|
||||
return words.join(' ');
|
||||
} else if (parts.length > 2) {
|
||||
// Sub-page: blockly.block_class.addicon_1_method -> "Addicon method"
|
||||
let name = parts[parts.length - 1];
|
||||
// Remove number suffixes and type suffixes
|
||||
name = name.replace(/_\d+_(method|property|constructor|function|variable)$/, ' $1');
|
||||
name = name.replace(/^_constructor__\d+_constructor$/, 'Constructor');
|
||||
// Replace double underscores with space, but keep single underscores
|
||||
name = name.replace(/__/g, ' ');
|
||||
name = name.trim();
|
||||
// Capitalize first letter only
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
}
|
||||
|
||||
// Fallback: capitalize first letter
|
||||
return nameWithoutExt.charAt(0).toUpperCase() + nameWithoutExt.slice(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts description from the content.
|
||||
* Gets the first paragraph after the heading, up to the first code block or newline.
|
||||
* If no paragraph is found, generates a generic fallback description.
|
||||
*/
|
||||
const extractDescription = function(content, filename) {
|
||||
// Remove frontmatter if exists
|
||||
content = content.replace(/^---[\s\S]*?---\n\n/, '');
|
||||
|
||||
// Remove MDX comments
|
||||
content = content.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
|
||||
|
||||
// Find the first ## heading (usually the main title)
|
||||
const headingMatch = content.match(/##\s+(.+)/);
|
||||
if (!headingMatch) {
|
||||
const title = extractTitle(filename);
|
||||
return `Blockly - usage reference for the ${title}`;
|
||||
}
|
||||
|
||||
// Get the full H2 heading for fallback description
|
||||
let fullTitle = headingMatch[1].trim();
|
||||
fullTitle = fullTitle.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
||||
fullTitle = fullTitle.replace(/`([^`]+)`/g, '$1');
|
||||
|
||||
// Get content after the heading
|
||||
const afterHeading = content.substring(content.indexOf(headingMatch[0]) + headingMatch[0].length);
|
||||
|
||||
// Look for the first non-empty text after the heading
|
||||
// It might have 1 or 2 newlines before the description paragraph
|
||||
const paragraphMatch = afterHeading.match(/\n+([^\n]+(?:\n(?!\n|\*\*|```|##|<table>)[^\n]+)*)/);
|
||||
|
||||
if (paragraphMatch) {
|
||||
// Clean up the description
|
||||
let description = paragraphMatch[1].trim();
|
||||
|
||||
// Remove markdown links but keep the text
|
||||
description = description.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
||||
|
||||
// Remove inline code backticks
|
||||
description = description.replace(/`([^`]+)`/g, '$1');
|
||||
|
||||
// Remove extra whitespace and newlines
|
||||
description = description.replace(/\s+/g, ' ');
|
||||
|
||||
// Skip if it's empty after cleaning
|
||||
if (!description) {
|
||||
return `Blockly - usage reference for the ${fullTitle}`;
|
||||
}
|
||||
|
||||
// Limit to first sentence or 160 characters
|
||||
const firstSentence = description.match(/^[^.!?]+[.!?]/);
|
||||
if (firstSentence) {
|
||||
description = firstSentence[0];
|
||||
}
|
||||
|
||||
if (description.length > 160) {
|
||||
description = description.substring(0, 157) + '...';
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
// Fallback: Generate generic description using full H2 heading title
|
||||
return `Blockly - usage reference for the ${fullTitle}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepends frontmatter to MDX files with title, description, and sidebar config.
|
||||
*/
|
||||
const prependFrontmatter = function(done) {
|
||||
const files = fs.readdirSync(DOCS_DIR);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.mdx')) continue;
|
||||
|
||||
const filePath = path.join(DOCS_DIR, file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Remove existing frontmatter if present
|
||||
if (content.startsWith('---\n')) {
|
||||
const endOfFrontmatter = content.indexOf('---\n', 4);
|
||||
if (endOfFrontmatter !== -1) {
|
||||
content = content.substring(endOfFrontmatter + 4).trim() + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
const title = extractTitleFromContent(content, file);
|
||||
const description = extractDescription(content, file);
|
||||
|
||||
let frontmatter = '---\n';
|
||||
frontmatter += 'displayed_sidebar: referenceSidebar\n';
|
||||
frontmatter += 'hide_title: true\n';
|
||||
frontmatter += `title: "${title}"\n`;
|
||||
frontmatter += `description: ${JSON.stringify(description)}\n`;
|
||||
frontmatter += '---\n\n';
|
||||
|
||||
// Write the file with frontmatter
|
||||
fs.writeFileSync(filePath, frontmatter + content);
|
||||
}
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts .md files to .mdx for Docusaurus.
|
||||
*/
|
||||
/**
|
||||
* Post-process MDX files to fix problematic patterns
|
||||
*/
|
||||
const fixMdxIssues = function(done) {
|
||||
const files = fs.readdirSync(DOCS_DIR).filter(f => f.endsWith('.mdx'));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(DOCS_DIR, file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const lines = content.split('\n');
|
||||
let inCodeBlock = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim().startsWith('```')) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
if (inCodeBlock) continue;
|
||||
|
||||
// Remove all MDX comments (artifacts from HTML comment conversion)
|
||||
lines[i] = lines[i].replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
|
||||
|
||||
// Remove unnecessary markdown escapes for underscores and brackets
|
||||
lines[i] = lines[i].replace(/\\_/g, '_');
|
||||
lines[i] = lines[i].replace(/\\\[/g, '[');
|
||||
lines[i] = lines[i].replace(/\\\]/g, ']');
|
||||
|
||||
// Escape HTML tags (with or without attributes) outside of table markup
|
||||
const isTableMarkup = /^<\/?(table|thead|tbody|tr|th|td)[\s>]/.test(lines[i].trim());
|
||||
if (!isTableMarkup) {
|
||||
lines[i] = lines[i].replace(/<([a-z]+)(\s[^>]*)?>/g, '`$&`');
|
||||
lines[i] = lines[i].replace(/<\/([a-z]+)>/g, '`$&`');
|
||||
}
|
||||
|
||||
// Escape curly braces so MDX doesn't parse them as JSX expressions.
|
||||
// First undo any backslash-escaping from api-documenter, then re-escape.
|
||||
lines[i] = lines[i].replace(/\\\{/g, '{').replace(/\\\}/g, '}');
|
||||
lines[i] = lines[i].replace(/\{/g, '\\{').replace(/\}/g, '\\}');
|
||||
}
|
||||
|
||||
content = lines.join('\n');
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
}
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
const convertToMdx = function() {
|
||||
return gulp.src(`${DOCS_DIR}/*.md`)
|
||||
// Convert HTML comments to MDX comments
|
||||
.pipe(replace(/<!--\s*([\s\S]*?)\s*-->/g, '{/* $1 */}'))
|
||||
// Fix malformed markdown links: [text][/path](https://developers.google.com/path) -> [text](/path)
|
||||
.pipe(replace(/\[([^\]]+)\]\[([^\]]+)\]\(https:\/\/developers\.google\.com([^)]+)\)/g, '[$1]($2)'))
|
||||
// Fix all internal links: remove .md extension and convert ./filename to /reference/filename
|
||||
.pipe(replace(/\]\(\.\/([^)]+)\.md\)/g, '](/reference/$1)'))
|
||||
// Replace developers.google.com links with relative paths
|
||||
.pipe(replace(/https:\/\/developers\.google\.com(\/blockly\/[^)\s"']+)/g, '$1'))
|
||||
// Replace developers.devsite.google.com links with relative paths
|
||||
.pipe(replace(/https:\/\/developers\.devsite\.google\.com(\/blockly\/[^)\s"']+)/g, '$1'))
|
||||
|
||||
// Fix underscore to hyphen in URL fragments
|
||||
.pipe(replace(/(\/blockly\/[^)\s"'#]*#[^)\s"']*)_([^)\s"']*)/g, function(match) {
|
||||
return match.replace(/_/g, '-');
|
||||
}))
|
||||
// Remove %5C (URL-encoded backslash) and literal backslash before anchor tags
|
||||
.pipe(replace(/(%5C|\\)(#[^)\s"']*)/g, '$2'))
|
||||
// Fix breadcrumb "Home" link to point to the overview page
|
||||
.pipe(replace(/\[Home\]\(\/reference\/index\)/g, '[Home](/reference/blockly)'))
|
||||
// Convert <code>text</code> to markdown backtick code
|
||||
.pipe(replace(/<code>([^<]*)<\/code>/g, '`$1`'))
|
||||
// Convert paragraph breaks to spaces (for table cells) and remove remaining p tags
|
||||
.pipe(replace(/<\/p><p>/g, ' '))
|
||||
.pipe(replace(/<\/?p>/g, ''))
|
||||
.pipe(rename({ extname: '.mdx' }))
|
||||
.pipe(gulp.dest(DOCS_DIR));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete original .md files after conversion to .mdx
|
||||
*/
|
||||
const cleanMdFiles = function(done) {
|
||||
const files = fs.readdirSync(DOCS_DIR);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.md')) {
|
||||
fs.unlinkSync(path.join(DOCS_DIR, file));
|
||||
}
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a map of top-level pages to sub-pages, e.g. a mapping
|
||||
* of `block_class` to every page associated with that class.
|
||||
@@ -60,87 +326,176 @@ const prependBook = function() {
|
||||
const buildAlternatePathsMap = function(allFiles) {
|
||||
let map = new Map();
|
||||
for (let file of allFiles) {
|
||||
// Get the name of the class/namespaces/variable/etc., i.e. the top-level
|
||||
// page.
|
||||
let filePieces = file.split('.');
|
||||
let name = filePieces[1];
|
||||
if (!map.has(name)) {
|
||||
map.set(name, []);
|
||||
if (!file.endsWith('.mdx') || file === 'blockly.mdx' || file === '_reference.js') continue;
|
||||
|
||||
// Remove extension
|
||||
const nameWithoutExt = file.replace('.mdx', '');
|
||||
|
||||
// Get the name of the class/namespace/etc., i.e. the top-level page
|
||||
// Example: blockly.block_class._constructor__1.mdx -> block_class
|
||||
// Example: blockly.block_class.mdx -> block_class
|
||||
const parts = nameWithoutExt.split('.');
|
||||
|
||||
if (parts.length === 2) {
|
||||
// This is a top-level page (e.g., blockly.block_class)
|
||||
const topLevelName = parts[1];
|
||||
if (!map.has(topLevelName)) {
|
||||
map.set(topLevelName, []);
|
||||
}
|
||||
} else if (parts.length > 2) {
|
||||
// This is a sub-page (e.g., blockly.block_class._constructor__1_constructor)
|
||||
const topLevelName = parts[1];
|
||||
if (!map.has(topLevelName)) {
|
||||
map.set(topLevelName, []);
|
||||
}
|
||||
// Add the full name without extension
|
||||
map.get(topLevelName).push(nameWithoutExt);
|
||||
}
|
||||
if (filePieces[2] === 'md') {
|
||||
// Don't add the top-level page to the map.
|
||||
continue;
|
||||
}
|
||||
// Add all sub-pages to the array for the corresponding top-level page.
|
||||
map.get(name).push(file);
|
||||
}
|
||||
|
||||
// Sort sub-pages: constructors first, then alphabetically
|
||||
for (const [key, value] of map.entries()) {
|
||||
value.sort((a, b) => {
|
||||
const aIsConstructor = a.includes('._constructor');
|
||||
const bIsConstructor = b.includes('._constructor');
|
||||
if (aIsConstructor && !bIsConstructor) return -1;
|
||||
if (!aIsConstructor && bIsConstructor) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the _toc.yaml file used by devsite to create the leftnav.
|
||||
* This file is generated from the contents of `blockly.md` which contains links
|
||||
* to the other top-level API pages (each class, namespace, etc.).
|
||||
*
|
||||
* The `alternate_paths` for each top-level page contains the path for
|
||||
* each associated sub-page. All subpages must be linked to their top-level page
|
||||
* in the TOC for the left nav bar to remain correct after drilling down into a
|
||||
* sub-page.
|
||||
* Parse HTML tables from the blockly.md file to extract classes, interfaces, etc.
|
||||
* @param {string} fileContent The content of blockly.md
|
||||
* @returns {Object} Object with sections as keys and arrays of {name, path} as values
|
||||
*/
|
||||
const createToc = function(done) {
|
||||
const fileContent = fs.readFileSync(`${DOCS_DIR}/blockly.md`, 'utf8');
|
||||
// Create the TOC file. The file should not yet exist; if it does, this
|
||||
// operation will fail.
|
||||
const toc = fs.openSync(`${DOCS_DIR}/_toc.yaml`, 'ax');
|
||||
const files = fs.readdirSync(DOCS_DIR);
|
||||
const map = buildAlternatePathsMap(files);
|
||||
const referencePath = '/blockly/reference/js';
|
||||
|
||||
const tocHeader = `toc:
|
||||
- title: Overview
|
||||
path: /blockly/reference/js/blockly.md\n`;
|
||||
fs.writeSync(toc, tocHeader);
|
||||
|
||||
// Generate a section of TOC for each section/heading in the overview file.
|
||||
const parseHtmlTables = function(fileContent) {
|
||||
const result = {};
|
||||
|
||||
// Split by ## headings
|
||||
const sections = fileContent.split('##');
|
||||
|
||||
for (let section of sections) {
|
||||
// This converts the md table in each section to a JS object
|
||||
const table = Extractor.extractObject(section, 'rows', false);
|
||||
if (!table) {
|
||||
continue;
|
||||
const lines = section.split('\n');
|
||||
const sectionName = lines[0].trim();
|
||||
|
||||
if (!sectionName || sectionName === 'blockly package') continue;
|
||||
|
||||
// Match links in markdown pipe tables: | [Name](/reference/path) | ...
|
||||
const tableRowRegex = /\|\s*\[([^\]]+)\]\(\/reference\/([^\)]+)\)/g;
|
||||
const items = [];
|
||||
|
||||
let match;
|
||||
while ((match = tableRowRegex.exec(section)) !== null) {
|
||||
const name = match[1];
|
||||
const href = match[2];
|
||||
items.push({ name, path: href });
|
||||
}
|
||||
// Get the name of the section, i.e. the text immediately after the `##` in
|
||||
// the source doc
|
||||
const sectionName = section.split('\n')[0].trim();
|
||||
fs.writeSync(toc, `- heading: ${sectionName}\n`);
|
||||
for (let row in table) {
|
||||
// After going through the Extractor, the markdown is now HTML.
|
||||
// Each row in the table is now a link (anchor tag).
|
||||
// Get the target of the link, excluding the first `.` since we don't want
|
||||
// a relative path.
|
||||
const path = /href="\.(.*?)"/.exec(row)?.[1];
|
||||
// Get the name of the link (text in between the <a> and </a>)
|
||||
const name = /">(.*?)</.exec(row)?.[1];
|
||||
if (!path || !name) {
|
||||
continue;
|
||||
}
|
||||
fs.writeSync(toc, `- title: ${name}\n path: ${referencePath}${path}\n`);
|
||||
// Get the list of sub-pages for this page.
|
||||
// Add each sub-page to the `alternate_paths` property.
|
||||
let pages = map.get(path.split('.')[1]);
|
||||
if (pages?.length) {
|
||||
fs.writeSync(toc, ` alternate_paths:\n`);
|
||||
for (let page of pages) {
|
||||
fs.writeSync(toc, ` - ${referencePath}/${page}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
result[sectionName] = items;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the _reference.js file for Docusaurus sidebar.
|
||||
* This file is generated from the contents of `blockly.mdx` which contains links
|
||||
* to the other top-level API pages (each class, namespace, etc.).
|
||||
*/
|
||||
const createReferenceSidebar = function(done) {
|
||||
const fileContent = fs.readFileSync(`${DOCS_DIR}/blockly.mdx`, 'utf8');
|
||||
const files = fs.readdirSync(DOCS_DIR);
|
||||
const map = buildAlternatePathsMap(files);
|
||||
|
||||
// Parse HTML tables from the file
|
||||
const sections = parseHtmlTables(fileContent);
|
||||
|
||||
let sidebarContent = 'export const referenceSidebar = [\n';
|
||||
|
||||
// Add overview
|
||||
sidebarContent += ' {\n';
|
||||
sidebarContent += ' "type": "doc",\n';
|
||||
sidebarContent += ' "label": "Overview",\n';
|
||||
sidebarContent += ' "id": "reference/blockly"\n';
|
||||
sidebarContent += ' },\n';
|
||||
|
||||
// Process each section (Classes, Interfaces, Functions, etc.)
|
||||
for (const [sectionName, items] of Object.entries(sections)) {
|
||||
sidebarContent += ' {\n';
|
||||
sidebarContent += ' "type": "category",\n';
|
||||
sidebarContent += ` "label": "${sectionName}",\n`;
|
||||
sidebarContent += ' "collapsible": true,\n';
|
||||
sidebarContent += ' "className": "hide-level-3",\n';
|
||||
|
||||
sidebarContent += ' "items": [\n';
|
||||
|
||||
// Add items for this section
|
||||
for (const item of items) {
|
||||
const itemName = item.name;
|
||||
const itemPath = item.path.replace('.md', '').replace('.mdx', '');
|
||||
const baseName = itemPath.replace('blockly.', '');
|
||||
|
||||
// Check if this item has sub-pages
|
||||
const subPages = map.get(baseName);
|
||||
|
||||
if (subPages && subPages.length > 0) {
|
||||
// Item with sub-pages - create a category
|
||||
sidebarContent += ' {\n';
|
||||
sidebarContent += ' "type": "category",\n';
|
||||
sidebarContent += ` "label": "${itemName}",\n`;
|
||||
sidebarContent += ' "link": {\n';
|
||||
sidebarContent += ' "type": "doc",\n';
|
||||
sidebarContent += ` "id": "reference/${itemPath}"\n`;
|
||||
sidebarContent += ' },\n';
|
||||
sidebarContent += ' "items": [\n';
|
||||
|
||||
// Add sub-pages
|
||||
for (const subPage of subPages) {
|
||||
const subPageId = subPage.replace('blockly.', '');
|
||||
sidebarContent += ' {\n';
|
||||
sidebarContent += ' "type": "doc",\n';
|
||||
sidebarContent += ` "label": "${subPage}",\n`;
|
||||
sidebarContent += ` "id": "reference/${subPage}"\n`;
|
||||
sidebarContent += ' },\n';
|
||||
}
|
||||
|
||||
sidebarContent += ' ],\n';
|
||||
|
||||
if (sectionName === 'Classes' || sectionName === 'Abstract Classes') {
|
||||
sidebarContent += ' "className": "hide-from-sidebar"\n';
|
||||
}
|
||||
|
||||
sidebarContent += ' },\n';
|
||||
} else {
|
||||
// Simple item without sub-pages
|
||||
sidebarContent += ' {\n';
|
||||
sidebarContent += ' "type": "doc",\n';
|
||||
sidebarContent += ` "label": "${itemName}",\n`;
|
||||
sidebarContent += ` "id": "reference/${itemPath}"\n`;
|
||||
sidebarContent += ' },\n';
|
||||
}
|
||||
}
|
||||
|
||||
sidebarContent += ' ]\n';
|
||||
sidebarContent += ' },\n';
|
||||
}
|
||||
|
||||
sidebarContent += '];\n';
|
||||
|
||||
// Write the file to the reference directory
|
||||
if (!fs.existsSync(REFERENCE_SIDEBAR_DIR)) {
|
||||
fs.mkdirSync(REFERENCE_SIDEBAR_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(`${REFERENCE_SIDEBAR_DIR}/_reference.js`, sidebarContent);
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
export const docs = gulp.series(
|
||||
generateApiJson, removeRenames, generateDocs,
|
||||
gulp.parallel(prependBook, createToc));
|
||||
|
||||
generateApiJson, removeRenames, generateDocs, convertToMdx, cleanMdFiles, fixMdxIssues, prependFrontmatter, createReferenceSidebar);
|
||||
|
||||
@@ -240,6 +240,11 @@ export function cleanReleaseDir() {
|
||||
*
|
||||
* Prerequisite: build.
|
||||
*/
|
||||
export const typings = gulp.series(
|
||||
gulp.parallel(build.cleanBuildDir, cleanReleaseDir),
|
||||
build.tsc,
|
||||
packageDTS);
|
||||
|
||||
export const pack = gulp.series(
|
||||
gulp.parallel(
|
||||
build.cleanBuildDir,
|
||||
|
||||
@@ -40,14 +40,19 @@ async function runCompileCheckInBrowser() {
|
||||
// Run in headless mode on Github Actions.
|
||||
if (process.env.CI) {
|
||||
options.capabilities['goog:chromeOptions'] = {
|
||||
args: ['--headless', '--no-sandbox', '--disable-dev-shm-usage']
|
||||
args: [
|
||||
'--headless',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--allow-file-access-from-files',
|
||||
]
|
||||
};
|
||||
} else {
|
||||
// --disable-gpu is needed to prevent Chrome from hanging on Linux with
|
||||
// NVIDIA drivers older than v295.20. See
|
||||
// https://github.com/google/blockly/issues/5345 for details.
|
||||
options.capabilities['goog:chromeOptions'] = {
|
||||
args: ['--disable-gpu']
|
||||
args: ['--allow-file-access-from-files', '--disable-gpu']
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,32 @@
|
||||
* @fileoverview Node.js script to run Mocha tests in Chrome, via webdriver.
|
||||
*/
|
||||
const webdriverio = require('webdriverio');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {posixPath} = require('../../scripts/helpers');
|
||||
|
||||
/**
|
||||
* Ensure browser test imports that use ../../node_modules/* continue to work
|
||||
* when npm hoists dependencies to the repository root node_modules dir
|
||||
*/
|
||||
function ensureWorkspaceNodeModulesLinks() {
|
||||
const workspaceNodeModules = path.resolve(__dirname, '../../node_modules');
|
||||
const rootNodeModules = path.resolve(__dirname, '../../../../node_modules');
|
||||
const packages = ['mocha', 'sinon', 'chai', '@blockly/dev-tools'];
|
||||
|
||||
for (const pkg of packages) {
|
||||
const workspacePkgPath = path.join(workspaceNodeModules, pkg);
|
||||
const rootPkgPath = path.join(rootNodeModules, pkg);
|
||||
|
||||
if (fs.existsSync(workspacePkgPath) || !fs.existsSync(rootPkgPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(workspacePkgPath), {recursive: true});
|
||||
fs.symlinkSync(rootPkgPath, workspacePkgPath, 'dir');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runs the Mocha tests in this directory in Chrome. It uses webdriverio to
|
||||
@@ -21,6 +45,8 @@ const {posixPath} = require('../../scripts/helpers');
|
||||
* @return {number} 0 on success, 1 on failure.
|
||||
*/
|
||||
async function runMochaTestsInBrowser(exitOnCompletion = true) {
|
||||
ensureWorkspaceNodeModulesLinks();
|
||||
|
||||
const options = {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"allowJs": false,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"types": [],
|
||||
"paths": {
|
||||
"blockly-test/*": ["../../dist/*"]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Autogenerated reference docs, do not check in
|
||||
docs/reference/
|
||||
.docusaurus
|
||||
build/
|
||||
@@ -0,0 +1,11 @@
|
||||
# Markdown/MDX (linted by ESLint + eslint-plugin-mdx instead)
|
||||
*.md
|
||||
*.mdx
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
.docusaurus/
|
||||
node_modules/
|
||||
|
||||
# Auto-generated
|
||||
CHANGELOG.md
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"bracketSameLine": true,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Blockly Documentation Website
|
||||
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
## Installation
|
||||
|
||||
Run `npm install` at the root of the blockly repo, then all other commands from the `packages/docs` directory.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cd packages/docs
|
||||
```
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
## Test your build locally
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
The build folder is now served at http://localhost:3000/
|
||||
|
||||
## Formatting and linting
|
||||
|
||||
```bash
|
||||
# check formatting:
|
||||
npm run format:check
|
||||
# fix formatting:
|
||||
npm run format
|
||||
# check linting:
|
||||
npm run lint
|
||||
# fix linting:
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
Prettier is used for formatting JavaScript files (the `format` script).
|
||||
|
||||
ESlint is used for linting `.md` and `.mdx` files due to poor support for these in Prettier (the `lint` script).
|
||||
|
||||
## Generating reference docs
|
||||
|
||||
The API reference pages are auto-generated from the Blockly TypeScript source using `@microsoft/api-extractor` and `@microsoft/api-documenter`. This is a separate step from the Docusaurus build and must be run from the `packages/blockly` directory:
|
||||
|
||||
```bash
|
||||
cd packages/blockly
|
||||
npm run build && npm run package
|
||||
npm run docs
|
||||
```
|
||||
|
||||
This generates MDX files into `packages/docs/docs/reference/`. These files are gitignored, so this needs to be run locally (and / or in CI).
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/add-a-context-menu-item
|
||||
description: How to add a context menu item to the registry.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 3. Add a context menu item
|
||||
|
||||
In this section you will create a very basic `Blockly.ContextMenuRegistry.RegistryItem`, then register it to display when you open a context menu on the workspace, a block, or a comment.
|
||||
|
||||
### The RegistryItem
|
||||
|
||||
A context menu consists of one or more menu options that a user can select. Blockly stores information about menu option as items in a registry. You can think of the _registry items_ as templates for constructing _menu options_. When the user opens a context menu, Blockly retrieves all of the registry items that apply to the current context and uses them to construct a list of menu options.
|
||||
|
||||
Each item in the registry has several properties:
|
||||
|
||||
- `displayText`: The text to show in the menu. Either a string, or HTML, or a function that returns either of the former.
|
||||
- `preconditionFn`: Function that returns one of `'enabled'`, `'disabled'`, or `'hidden'` to determine whether and how the menu option should be rendered.
|
||||
- `callback`: A function called when the menu option is selected.
|
||||
- `id`: A unique string id for the item.
|
||||
- `weight`: A number that determines the sort order of the option. Options with higher weights appear later in the context menu.
|
||||
|
||||
We will discuss these in detail in later sections of the codelab.
|
||||
|
||||
### Make a RegistryItem
|
||||
|
||||
Add a function to `index.js` named `registerHelloWorldItem`. Create a new registry item in your function:
|
||||
|
||||
```js
|
||||
function registerHelloWorldItem() {
|
||||
const helloWorldItem = {
|
||||
displayText: 'Hello World',
|
||||
preconditionFn: function (scope) {
|
||||
return 'enabled';
|
||||
},
|
||||
callback: function (scope) {},
|
||||
id: 'hello_world',
|
||||
weight: 100,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Call your function from `start`:
|
||||
|
||||
```js
|
||||
function start() {
|
||||
registerHelloWorldItem();
|
||||
|
||||
Blockly.ContextMenuItems.registerCommentOptions();
|
||||
// Create main workspace.
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: toolboxSimple,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Register it
|
||||
|
||||
Next, register your item with Blockly:
|
||||
|
||||
```js
|
||||
function registerHelloWorldItem() {
|
||||
const helloWorldItem = {
|
||||
// ...
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(helloWorldItem);
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
you will never need to make a new `ContextMenuRegistry`. Always use the singleton `Blockly.ContextMenuRegistry.registry`.
|
||||
:::
|
||||
|
||||
### Test it
|
||||
|
||||
Reload your web page and open a context menu on the workspace (right-click with a mouse, or press `Ctrl+Enter` (Windows) or `Command+Enter` (Mac) if you are navigating Blockly with the keyboard). You should see a new option labeled "Hello World" at the bottom of the context menu.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/context-menu-option/hello_world.png"
|
||||
alt='A context menu. The last option says "Hello World".'
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
Next, drag a block onto the workspace and open a context menu on the block. You'll see "Hello World" at the bottom of the block's context menu. Finally, open a context menu on the workspace and create a comment, then open a context menu on the comment's header. "Hello World" should be at the bottom of the context menu.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/callback
|
||||
description: How to add a callback to a context menu item.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 7. Callback
|
||||
|
||||
The callback function determines what happens when you select the context menu option. Like the precondition, it can use the `scope` argument to access the Blockly component on which the context menu was invoked.
|
||||
|
||||
It is also passed a `PointerEvent` which is the original event that triggered opening the context menu (not the event that selected the current option). This lets you, for example, figure out where on the workspace the context menu was opened so you can create a new element there.
|
||||
|
||||
As an example, update the help item's `callback` to add a block to the workspace when selected:
|
||||
|
||||
```js
|
||||
callback: function(scope) {
|
||||
Blockly.serialization.blocks.append({
|
||||
'type': 'text',
|
||||
'fields': {
|
||||
'TEXT': 'Now there is a block'
|
||||
}
|
||||
}, scope.focusedNode);
|
||||
},
|
||||
```
|
||||
|
||||
### Test it
|
||||
|
||||
- Reload the page and open a context menu on the workspace.
|
||||
- Select the **Help** option.
|
||||
- A text block should appear in the top left of the workspace.
|
||||
|
||||

|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
pagination_prev: null
|
||||
slug: /codelabs/context-menu-option/codelab-overview
|
||||
description: Overview of the "Customizing context menus" codelab.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 1. Codelab overview
|
||||
|
||||
### What you'll learn
|
||||
|
||||
In this codelab you will learn how to:
|
||||
|
||||
- Add a context menu option to the workspace.
|
||||
- Add a context menu option to all blocks.
|
||||
- Use precondition functions to hide or disable context menu options.
|
||||
- Take an action when a menu option is selected.
|
||||
- Customize ordering and display text for context menu options.
|
||||
|
||||
### What you'll build
|
||||
|
||||
A very simple Blockly workspace with a few new context menu options.
|
||||
|
||||
### What you'll need
|
||||
|
||||
- A browser.
|
||||
- A text editor.
|
||||
- Basic knowledge of HTML, CSS, and JavaScript.
|
||||
|
||||
This codelab is focused on Blockly's context menus. Non-relevant concepts and code are glossed over and are provided for you to simply copy and paste.
|
||||
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Context Menu Codelab</title>
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="https://unpkg.com/@blockly/dev-tools"></script>
|
||||
<script src="./index.js"></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
float: bottom;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="start()">
|
||||
<h1>Context Menu Codelab</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
let workspace = null;
|
||||
|
||||
function start() {
|
||||
registerHelloWorldItem();
|
||||
registerHelpItem();
|
||||
registerDisplayItem();
|
||||
Blockly.ContextMenuRegistry.registry.unregister('workspaceDelete');
|
||||
registerSeparators();
|
||||
|
||||
Blockly.ContextMenuItems.registerCommentOptions();
|
||||
// Create main workspace.
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: toolboxSimple,
|
||||
});
|
||||
}
|
||||
|
||||
function registerHelloWorldItem() {
|
||||
const helloWorldItem = {
|
||||
displayText: 'Hello World',
|
||||
preconditionFn: function (scope) {
|
||||
// Only display this option for workspaces and blocks.
|
||||
if (
|
||||
scope.focusedNode instanceof Blockly.WorkspaceSvg ||
|
||||
scope.focusedNode instanceof Blockly.BlockSvg
|
||||
) {
|
||||
// Enable for the first 30 seconds of every minute; disable for the next 30 seconds.
|
||||
const now = new Date(Date.now());
|
||||
if (now.getSeconds() < 30) {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'disabled';
|
||||
}
|
||||
return 'hidden';
|
||||
},
|
||||
callback: function (scope) {},
|
||||
id: 'hello_world',
|
||||
weight: 100,
|
||||
};
|
||||
// Register.
|
||||
Blockly.ContextMenuRegistry.registry.register(helloWorldItem);
|
||||
}
|
||||
|
||||
function registerHelpItem() {
|
||||
const helpItem = {
|
||||
displayText: 'Help! There are no blocks',
|
||||
preconditionFn: function (scope) {
|
||||
// Only display this option on workspace context menus.
|
||||
if (!(scope.focusedNode instanceof Blockly.WorkspaceSvg)) return 'hidden';
|
||||
// Use the focused node, which is a WorkspaceSvg, to check for blocks on the workspace.
|
||||
if (!scope.focusedNode.getTopBlocks().length) {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'hidden';
|
||||
},
|
||||
// Use the focused node in the callback function to add a block to the workspace.
|
||||
callback: function (scope) {
|
||||
Blockly.serialization.blocks.append(
|
||||
{
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'Now there is a block',
|
||||
},
|
||||
},
|
||||
scope.focusedNode,
|
||||
);
|
||||
},
|
||||
id: 'help_no_blocks',
|
||||
weight: 100,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(helpItem);
|
||||
}
|
||||
|
||||
function registerDisplayItem() {
|
||||
const displayItem = {
|
||||
// Use the focused node (a BlockSvg) to set display text dynamically based on the type of the block.
|
||||
displayText: function (scope) {
|
||||
if (scope.focusedNode.type.startsWith('text')) {
|
||||
return 'Text block';
|
||||
} else if (scope.focusedNode.type.startsWith('controls')) {
|
||||
return 'Controls block';
|
||||
} else {
|
||||
return 'Some other block';
|
||||
}
|
||||
},
|
||||
preconditionFn: function (scope) {
|
||||
return scope.focusedNode instanceof Blockly.BlockSvg
|
||||
? 'enabled'
|
||||
: 'hidden';
|
||||
},
|
||||
callback: function (scope) {},
|
||||
id: 'display_text_example',
|
||||
weight: 100,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(displayItem);
|
||||
}
|
||||
|
||||
function registerSeparators() {
|
||||
const workspaceSeparator = {
|
||||
id: 'workspace_separator',
|
||||
scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE,
|
||||
weight: 99,
|
||||
separator: true,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(workspaceSeparator);
|
||||
|
||||
const blockSeparator = {
|
||||
id: 'block_separator',
|
||||
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
|
||||
weight: 99,
|
||||
separator: true,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(blockSeparator);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/display-text
|
||||
description: How to set the display text of a context menu item.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 8. Display text
|
||||
|
||||
So far the `displayText` has always been a simple string, but it can also be HTML, or a function that returns either of the former. Using a function can be useful when you want a context-dependent message.
|
||||
|
||||
When defined as a function `displayText` accepts a `scope` argument, just like `callback` and `preconditionFn`.
|
||||
|
||||
As an example, add this registry item. The display text depends on the block type.
|
||||
|
||||
```js
|
||||
function registerDisplayItem() {
|
||||
const displayItem = {
|
||||
displayText: function (scope) {
|
||||
if (scope.focusedNode.type.startsWith('text')) {
|
||||
return 'Text block';
|
||||
} else if (scope.focusedNode.type.startsWith('controls')) {
|
||||
return 'Controls block';
|
||||
} else {
|
||||
return 'Some other block';
|
||||
}
|
||||
},
|
||||
preconditionFn: function (scope) {
|
||||
return scope.focusedNode instanceof Blockly.BlockSvg
|
||||
? 'enabled'
|
||||
: 'hidden';
|
||||
},
|
||||
callback: function (scope) {},
|
||||
id: 'display_text_example',
|
||||
weight: 100,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(displayItem);
|
||||
}
|
||||
```
|
||||
|
||||
As usual, remember to call `registerDisplayItem()` from your `start` function.
|
||||
|
||||
### Test it
|
||||
|
||||
- Reload the workspace and open context menus on various blocks.
|
||||
- The last context menu option's text should vary based on the block type.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/precondition-blockly-state
|
||||
description: How to include a context menu item based on Blockly's state.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 6. Precondition: Blockly state
|
||||
|
||||
Disabling your context menu options half of the time is not useful, but you may want to show or hide an option based on what the user is doing. For example, let's show a **Help** option in the context menu if the user doesn't have any blocks on the workspace. Add this code in `index.js`:
|
||||
|
||||
```js
|
||||
function registerHelpItem() {
|
||||
const helpItem = {
|
||||
displayText: 'Help! There are no blocks',
|
||||
preconditionFn: function (scope) {
|
||||
if (!(scope.focusedNode instanceof Blockly.WorkspaceSvg)) return 'hidden';
|
||||
if (!scope.focusedNode.getTopBlocks().length) {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'hidden';
|
||||
},
|
||||
callback: function (scope) {},
|
||||
id: 'help_no_blocks',
|
||||
weight: 100,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(helpItem);
|
||||
}
|
||||
```
|
||||
|
||||
Don't forget to call `registerHelpItem` from your `start` function.
|
||||
|
||||
### Test it
|
||||
|
||||
- Reload your page and open a context menu on the workspace. You should see an option labeled "Help! There are no blocks".
|
||||
- Add a block to the workspace and open a context menu on the workspace again. The **Help** option should be gone.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/precondition-external-state
|
||||
description: How to include a context menu item based on an external condition.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 5. Precondition: External state
|
||||
|
||||
Use of the `preconditionFn` is not limited to checking the type of the Blockly component that the context menu was invoked on. You can use it to check for conditions entirely outside of Blockly. For instance, let's disable `helloWorldItem` for the second half of every minute:
|
||||
|
||||
```js
|
||||
preconditionFn: function (scope) {
|
||||
if (
|
||||
scope.focusedNode instanceof Blockly.WorkspaceSvg ||
|
||||
scope.focusedNode instanceof Blockly.BlockSvg
|
||||
) {
|
||||
const now = new Date(Date.now());
|
||||
if (now.getSeconds() < 30) {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'disabled';
|
||||
}
|
||||
return 'hidden';
|
||||
},
|
||||
```
|
||||
|
||||
### Test it
|
||||
|
||||
Reload your workspace, check your watch, and open a context menu on the workspace to confirm the timing. The option will always be in the menu, but will sometimes be greyed out.
|
||||
|
||||

|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/precondition-node-type
|
||||
description: How to include a context menu item based on the node type.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 4 . Precondition: Node type
|
||||
|
||||
Each registry item has a `preconditionFn`. It is called by Blockly to decide whether and how to display an option on a context menu. You'll use it to display the "Hello, World" option on workspace and block context menus, but not on comment context menus.
|
||||
|
||||
### The scope argument
|
||||
|
||||
The `scope` argument is an object that is passed to `preconditionFn`. You'll use the `scope.focusedNode` property to determine which object the context menu was invoked on. Why a focused node? Because Blockly keeps track of where the user is -- that is, what node (component) the user is focused on -- and opens the context menu on that node.
|
||||
|
||||
### Return value
|
||||
|
||||
The return value of `preconditionFn` is `'enabled'`, `'disabled'`, or `'hidden'`. An **enabled** option is shown with black text and is selectable. A **disabled** option is shown with grey text and is not selectable. A **hidden** option is not included in the context menu at all.
|
||||
|
||||
### Write the function
|
||||
|
||||
You can now test `scope.focusedNode` to display the "Hello World" option in workspace and block context menus, but not on any others. Change `preconditionFn` to:
|
||||
|
||||
```js
|
||||
const helloWorldItem = {
|
||||
...
|
||||
preconditionFn: function (scope) {
|
||||
if (
|
||||
scope.focusedNode instanceof Blockly.WorkspaceSvg ||
|
||||
scope.focusedNode instanceof Blockly.BlockSvg
|
||||
) {
|
||||
return 'enabled';
|
||||
}
|
||||
return 'hidden';
|
||||
},
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
Notice that the code tests for where context menus are allowed, rather than where they are not allowed. This is because custom code (such as a plugin) can add context menus to any Blockly component that can be focused. Thus, testing for specific types rather than allowing all (or all but certain types) ensures that context menus are not shown on more components than you anticipated.
|
||||
|
||||
### Test it
|
||||
|
||||
Open a context menu on the workspace, a block, and a comment. You should see a "Hello World" option on the workspace and block context menus, but not on the comment context menu.
|
||||
|
||||

|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/separators
|
||||
description: How to add a separator to a context menu.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 10. Separators
|
||||
|
||||
You can use separators to break your context menu into different sections.
|
||||
|
||||
Separators differ from other items in two ways: They cannot have `displayText`, `preconditionFn`, or `callback` properties and they can only be scoped with the `scopeType` property. The latter accepts an enum value of `Blockly.ContextMenuRegistry.ScopeType.WORKSPACE`, `Blockly.ContextMenuRegistry.ScopeType.BLOCK`, or `Blockly.ContextMenuRegistry.ScopeType.COMMENT`.
|
||||
|
||||
Use the `weight` property to position the separator. You'll use a weight of `99` to position the separator just above the other options you added, all of which have a weight of `100`.
|
||||
|
||||
You need to add a separate item for each separator:
|
||||
|
||||
```js
|
||||
function registerSeparators() {
|
||||
const workspaceSeparator = {
|
||||
id: 'workspace_separator',
|
||||
scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE,
|
||||
weight: 99,
|
||||
separator: true,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(workspaceSeparator);
|
||||
|
||||
const blockSeparator = {
|
||||
id: 'block_separator',
|
||||
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
|
||||
weight: 99,
|
||||
separator: true,
|
||||
};
|
||||
Blockly.ContextMenuRegistry.registry.register(blockSeparator);
|
||||
}
|
||||
```
|
||||
|
||||
As usual, remember to call `registerSeparators()` from your `start` function.
|
||||
|
||||
### Test it
|
||||
|
||||
Open a context menu on the workspace and a block and check that the separator line is there.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/setup
|
||||
description: Setting up the "Customizing context menus" codelab.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 2. Setup
|
||||
|
||||
### Download the sample code
|
||||
|
||||
You can get the sample code for this code by either downloading the zip here:
|
||||
|
||||
[Download zip](https://github.com/RaspberryPiFoundation/blockly/archive/main.zip)
|
||||
|
||||
or by cloning this git repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RaspberryPiFoundation/blockly.git
|
||||
```
|
||||
|
||||
If you downloaded the source as a zip, unpacking it should give you a root folder named `blockly-main`.
|
||||
|
||||
The relevant files are in `docs/docs/codelabs/context-menu-option`. There are two versions of the app:
|
||||
|
||||
- `starter-code/`: The starter code that you'll build upon in this codelab.
|
||||
- `complete-code/`: The code after completing the codelab, in case you get lost or want to compare to your version.
|
||||
|
||||
Each folder contains:
|
||||
|
||||
- `index.js` - The codelab's logic. To start, it just injects a simple workspace.
|
||||
- `index.html` - A web page containing a simple blockly workspace.
|
||||
|
||||
To run the code, simple open `starter-code/index.html` in a browser. You should see a Blockly workspace with an always-open flyout.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/context-menu-option/starter_workspace.png"
|
||||
alt='A web page with the text "Context Menu Codelab" and a simple Blockly workspace.'
|
||||
className="codelabImage"
|
||||
/>
|
||||
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Context Menu Codelab</title>
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="https://unpkg.com/@blockly/dev-tools"></script>
|
||||
<script src="./index.js"></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
float: bottom;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="start()">
|
||||
<h1>Context Menu Codelab</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
let workspace = null;
|
||||
|
||||
function start() {
|
||||
Blockly.ContextMenuItems.registerCommentOptions();
|
||||
// Create main workspace.
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: toolboxSimple,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
pagination_next: null
|
||||
slug: /codelabs/context-menu-option/summary
|
||||
description: Summary of the "Customizing context menus" codelab.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 11. Summary
|
||||
|
||||
In this codelab you have learned how to create and modify context menu options. You have learned about scope, preconditions, callbacks, and display text.
|
||||
|
||||
### Additional information
|
||||
|
||||
- [Context menu documentation](/guides/configure/web/context-menus)
|
||||
|
||||
- You can also define [block context menus](/guides/configure/web/context-menus#customize-per-block) directly on a block definition, which is equivalent to adding a precondition based on the type of the block.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
slug: /codelabs/context-menu-option/weight-and-id
|
||||
description: How to set the weight and ID of a context menu item.
|
||||
---
|
||||
|
||||
# Customizing context menus
|
||||
|
||||
## 9. Weight and id
|
||||
|
||||
The last two properties of a registry item are `weight` and `id`.
|
||||
|
||||
### Weight
|
||||
|
||||
The `weight` property is a number that determines the order of the options in the context menu. A higher number means your option will be lower in the list.
|
||||
|
||||
Test this by updating the `weight` property on one of your new registry items and confirming that the corresponding option moves to the top or bottom of the list.
|
||||
|
||||
Note that weight does not have to be positive or integer-valued.
|
||||
|
||||
### Id
|
||||
|
||||
Every registry item has an `id` that can be used to unregister it. You can use this to get rid of registry items that you don't want.
|
||||
|
||||
For instance, you can remove the item that deletes all blocks on the workspace:
|
||||
|
||||
```js
|
||||
Blockly.ContextMenuRegistry.registry.unregister('workspaceDelete');
|
||||
```
|
||||
|
||||
### Default items
|
||||
|
||||
For a list of the default registry items that Blockly provides, look at [contextmenu_items.ts](https://github.com/RaspberryPiFoundation/blockly/blob/main/packages/blockly/core/contextmenu_items.ts). Each entry contains both the `id` and the `weight`.
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
title: Use CSS in Blockly - Blocks
|
||||
slug: /codelabs/css/blocks
|
||||
description: Styling blocks with CSS
|
||||
---
|
||||
|
||||
# Use CSS in Blockly
|
||||
|
||||
## 6. Blocks
|
||||
|
||||
In this section, you will create CSS rules to assign the colours used by the [blocks section](/codelabs/theme-extension-identifier/customize-block-styles) of the themes codelab to the logic, loops, text, and lists blocks. This is a bit more complex than setting component or category colours and you'll do it in several steps.
|
||||
|
||||
### Block fill and stroke
|
||||
|
||||
Your first step is to set the `fill` and `stroke` of the logic blocks.
|
||||
|
||||
Note that setting the `fill` and `stroke` is specific to the
|
||||
[renderer](/guides/create-custom-blocks/renderers/overview)
|
||||
you are using. (In this codelab, you are using the Thrasos renderer.) An
|
||||
important consequence of this is that you need different CSS for different
|
||||
renderers.
|
||||
|
||||
#### Identify the block element
|
||||
|
||||
Drag an `if do` block onto the workspace and find it with the element inspector:
|
||||
|
||||
```
|
||||
<body>
|
||||
<div class="blocklyDiv">
|
||||
<div class="injectionDiv">
|
||||
<svg class="blocklySvg">
|
||||
<g class="blocklyWorkspace">
|
||||
<g class="blocklyBlockCanvas">
|
||||
<g class="controls_if blocklyBlock logic_blocks">
|
||||
```
|
||||
|
||||
Notice that the block's `<g>` element has classes for the block's type (`controls_if`) and style (`logic_blocks`). These are the values of the [`type` and `style` properties in the block's definition](https://github.com/RaspberryPiFoundation/blockly/blob/1c280d10cc1dcad7d50a1678211871058d4e9cfb/blocks/logic.ts#L50). You will use the style class to assign the same colour to all of the logic blocks.
|
||||
|
||||
(If you were building blocks from scratch and wanted to avoid themes, you would assign this class with the `classes` property in the block definition. However, because these are standard blocks and they were built with themes in mind, using the style class works just as well.)
|
||||
|
||||
#### Choose an element to use
|
||||
|
||||
Next, you need to decide what element to use in your colour rule. The `<g>` element identifies the block but doesn't draw it. Instead, you can use the `<g>` element's first child. This is a `<path>` element with `fill` and `stroke` presentation attributes, which are easily overridden.
|
||||
|
||||
Note that different renderers use different numbers of `<path>` elements to
|
||||
draw a block: Thrasos uses a single `<path>` element, Geras uses three `<path>`
|
||||
elements, and Zelos uses one `<path>` for the outside of the block and one
|
||||
`<path>` for each inline input.
|
||||
|
||||
#### Choose colours
|
||||
|
||||
The last step before writing your colour rules is to decide what colours to use. The Halloween theme in the themes codelab sets three colours:
|
||||
|
||||
```
|
||||
'logic_blocks': {
|
||||
'colourPrimary': "#8b4513",
|
||||
'colourSecondary':"#ff0000",
|
||||
'colourTertiary':"#c5eaff"
|
||||
},
|
||||
```
|
||||
|
||||
How these colours are used depends on the renderer. The Thrasos renderer uses
|
||||
the primary colour as the `fill` of the block, the tertiary colour as the
|
||||
`stroke`, and the secondary colour as the `fill` when the block is a
|
||||
[shadow block](/guides/configure/web/toolboxes/preset#shadow-blocks).
|
||||
|
||||
#### Add your rules
|
||||
|
||||
You're now ready to add your rules to set the `fill` and `stroke` of the logic blocks:
|
||||
|
||||
```css
|
||||
/**********/
|
||||
/* BLOCKS */
|
||||
/**********/
|
||||
|
||||
/* LOGIC BLOCKS */
|
||||
|
||||
.logic_blocks > .blocklyPath {
|
||||
fill: #8b4513;
|
||||
stroke: #c5eaff;
|
||||
}
|
||||
|
||||
.logic_blocks.blocklyShadow > .blocklyPath {
|
||||
fill: #ff0000;
|
||||
stroke: none;
|
||||
}
|
||||
```
|
||||
|
||||
Refresh your web page and open the **Logic** category. You should see that the
|
||||
logic blocks are now rendered in autumnal brown instead of blue:
|
||||

|
||||
|
||||
### Disabled blocks
|
||||
|
||||
Your next step is to handle disabled blocks. Drag an `if do` block and any block from the **Loops** category onto the workspace. Right-click on each block and disable it using the context menu. Notice that the loop block has a cross-hatch pattern while the `if do` block does not:
|
||||
|
||||

|
||||
|
||||
This is because the rules you just added have the same specificity as the Blockly rules that set the cross-hatch pattern. (You can see this if you click on the `if do` block's `<path>` element and inspect its styles.) Because your rules occur later in the document, they take precedence. To use the standard CSS for disabled blocks, add a `:not(.blocklyDisabledPattern)` to your rules:
|
||||
|
||||
```css
|
||||
/* LOGIC BLOCKS */
|
||||
|
||||
.logic_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #8b4513;
|
||||
stroke: #c5eaff;
|
||||
}
|
||||
|
||||
.logic_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ff0000;
|
||||
stroke: none;
|
||||
}
|
||||
```
|
||||
|
||||
Refresh your page, drag the `if do` block onto the workspace, and disable it. It should now use the disabled pattern:
|
||||
|
||||

|
||||
|
||||
### Dropdown arrows
|
||||
|
||||
You now need to handle dropdown arrows. Drag a logic comparison block onto the workspace and look closely at the inverted triangle in the dropdown field -- it's blue even though the rest of the block is brown:
|
||||
|
||||

|
||||
|
||||
If you look at the triangle with the element inspector, you'll see that it's a character in an SVG `<tspan>` element:
|
||||
|
||||
```
|
||||
<body>
|
||||
<div class="blocklyDiv">
|
||||
<div class="injectionDiv">
|
||||
<svg class="blocklySvg">
|
||||
<g class="blocklyWorkspace">
|
||||
<g class="blocklyBlockCanvas">
|
||||
<g class="logic_compare">
|
||||
<g class="blocklyDropdownField">
|
||||
<text class="blocklyDropdownText">
|
||||
<tspan style="fill: rgb(91, 128, 165);"> ▾</tspan>
|
||||
```
|
||||
|
||||
You can also see that its colour is set with a `style` attribute, which can only be overridden with an `!important` declaration. To do this, add the following rules:
|
||||
|
||||
```css
|
||||
.logic_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #8b4513 !important;
|
||||
}
|
||||
|
||||
.logic_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ff0000 !important;
|
||||
}
|
||||
```
|
||||
|
||||
Reload your page and drag the comparison block out again. The arrow should be the same colour as the rest of the block:
|
||||
|
||||

|
||||
|
||||
### Loop, text, and list blocks
|
||||
|
||||
Your last step is to add similar rules for the loop, text, and list blocks:
|
||||
|
||||
```css
|
||||
/* LOOP BLOCKS */
|
||||
|
||||
.loop_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #85e21f;
|
||||
stroke: #c5eaff;
|
||||
}
|
||||
|
||||
.loop_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ff0000;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.loop_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #85e21f !important;
|
||||
}
|
||||
|
||||
.loop_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ff0000 !important;
|
||||
}
|
||||
|
||||
/* TEXT BLOCKS */
|
||||
|
||||
.text_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #fe9b13;
|
||||
stroke: #c5eaff;
|
||||
}
|
||||
|
||||
.text_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ff0000;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.text_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #fe9b13 !important;
|
||||
}
|
||||
|
||||
.text_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ff0000 !important;
|
||||
}
|
||||
|
||||
/* LIST BLOCKS */
|
||||
|
||||
.list_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #4a148c;
|
||||
stroke: #cdb6e9;
|
||||
}
|
||||
|
||||
.list_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ad7be9;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.list_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #4a148c !important;
|
||||
}
|
||||
|
||||
.list_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ad7be9 !important;
|
||||
}
|
||||
```
|
||||
|
||||
And that's it! Reload your page and explore the blocks in your Halloween-themed editor:
|
||||
|
||||

|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
title: Use CSS in Blockly - Toolbox Categories
|
||||
slug: /codelabs/css/categories
|
||||
description: Styling toolbox categories with CSS
|
||||
---
|
||||
|
||||
# Use CSS in Blockly
|
||||
|
||||
## 5. Toolbox categories
|
||||
|
||||
In this section, you will create CSS rules to assign the colours used by the [categories section](/codelabs/theme-extension-identifier/customize-category-styles) of the themes codelab to the toolbox's categories.
|
||||
|
||||
### Identify the category element
|
||||
|
||||
Your first rule will set the colour of the **Logic** category. This rule needs to uniquely identify the element used by the **Logic** category, so open the developer tools and find the `blocklyToolboxCategory` `<div>` for the **Logic** category:
|
||||
|
||||
```
|
||||
<body>
|
||||
<div class="blocklyDiv">
|
||||
<div class="injectionDiv">
|
||||
<div class="blocklyToolbox">
|
||||
<div class="blocklyToolboxCategoryGroup">
|
||||
<div class="blocklyToolboxCategoryContainer">
|
||||
<div class="blocklyToolboxCategory" id="blockly-1">
|
||||
```
|
||||
|
||||
Unfortunately, the only thing that distinguishes this `<div>` from other category `<div>`s is a generated `id` attribute (`blockly-1`). This isn't stable enough to use in a CSS rule -- for example, if you switched the order of two categories you'd also have to switch the selectors in their rules.
|
||||
|
||||
To solve this problem, you'll need to add a class to the
|
||||
`blocklyToolboxCategory` `<div>` for the **Logic** category. Open the
|
||||
`toolbox.js` file and find the definition of the **Logic** category:
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Logic',
|
||||
categorystyle: 'logic_category',
|
||||
contents: [...],
|
||||
},
|
||||
```
|
||||
|
||||
The `categorystyle` property assigns a style that is used by a theme. Because
|
||||
you're not using themes to assign category colours, you don't need the
|
||||
`categorystyle` property. Delete it and add a `cssConfig` property that adds two
|
||||
classes to the **Logic** category's `<div>`: `logic_category` uniquely
|
||||
identifies the `<div>` and `blocklyToolboxCategory` is used by Blockly's CSS to
|
||||
define rules that apply to all categories.
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Logic',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory logic_category',
|
||||
},
|
||||
contents: [...],
|
||||
},
|
||||
```
|
||||
|
||||
For a complete explanation of how `cssConfig` works, see [Custom CSS classes](/guides/configure/web/toolboxes/appearance#custom-css-classes) in the toolbox documentation.
|
||||
|
||||
### Add your rules
|
||||
|
||||
Next, add the following rules, which set the row colour and its colour when selected:
|
||||
|
||||
```css
|
||||
/**************/
|
||||
/* CATEGORIES */
|
||||
/**************/
|
||||
|
||||
.logic_category {
|
||||
border-left: 8px solid #8b4513;
|
||||
}
|
||||
|
||||
.logic_category.blocklyToolboxSelected {
|
||||
background-color: #8b4513 !important;
|
||||
}
|
||||
```
|
||||
|
||||
Refresh your web page and click the **Logic** category. The row is highlighted with your new colour:
|
||||
|
||||

|
||||
|
||||
### Update the other categories
|
||||
|
||||
Before you can write rules for the remaining categories, you need to replace `categorystyle` with `cssConfig` in each of their definitions:
|
||||
|
||||
```js
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Loops',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory loop_category',
|
||||
},
|
||||
contents: [...],
|
||||
},
|
||||
|
||||
// Repeat for remaining categories
|
||||
```
|
||||
|
||||
Next, add the following rules to `halloween.css`. These rules use the colours from themes codelab for the **Loops**, **Text**, and **Lists** categories and the colours from the Classic theme (the default theme) for the **Math**, **Variables**, and **Functions** categories.
|
||||
|
||||
```css
|
||||
.loop_category {
|
||||
border-left: 8px solid #85e21f;
|
||||
}
|
||||
|
||||
.loop_category.blocklyToolboxSelected {
|
||||
background-color: #85e21f !important;
|
||||
}
|
||||
|
||||
.math_category {
|
||||
border-left: 8px solid #5b67a5;
|
||||
}
|
||||
|
||||
.math_category.blocklyToolboxSelected {
|
||||
background-color: #5b67a5 !important;
|
||||
}
|
||||
|
||||
.text_category {
|
||||
border-left: 8px solid #fe9b13;
|
||||
}
|
||||
|
||||
.text_category.blocklyToolboxSelected {
|
||||
background-color: #fe9b13 !important;
|
||||
}
|
||||
|
||||
.list_category {
|
||||
border-left: 8px solid #4a148c;
|
||||
}
|
||||
|
||||
.list_category.blocklyToolboxSelected {
|
||||
background-color: #4a148c !important;
|
||||
}
|
||||
|
||||
.variable_category {
|
||||
border-left: 8px solid #a55b80;
|
||||
}
|
||||
|
||||
.variable_category.blocklyToolboxSelected {
|
||||
background-color: #a55b80 !important;
|
||||
}
|
||||
|
||||
.procedure_category {
|
||||
border-left: 8px solid #b88cc0;
|
||||
}
|
||||
|
||||
.procedure_category.blocklyToolboxSelected {
|
||||
background-color: #b88cc0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
Refresh your web page. You should see the new colours beside each category:
|
||||
|
||||

|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
pagination_prev: null
|
||||
title: Use CSS in Blockly - Codelab Overview
|
||||
slug: /codelabs/css/codelab-overview
|
||||
description: Overview of the CSS in Blockly codelab
|
||||
---
|
||||
|
||||
# Use CSS in Blockly
|
||||
|
||||
## 1. Codelab overview
|
||||
|
||||
### What you'll learn
|
||||
|
||||
In this codelab you will learn how to use CSS to customize the colours of:
|
||||
|
||||
- Components
|
||||
- Categories
|
||||
- Blocks
|
||||
|
||||
If you don't need the fine-grained control provided by CSS, consider using
|
||||
themes instead. For more information, see the
|
||||
[Customizing your themes](/codelabs/theme-extension-identifier/codelab-overview)
|
||||
codelab.
|
||||
|
||||
### What you'll build
|
||||
|
||||
A simple Blockly workspace that uses the same Halloween colours as the [Customizing your themes](/codelabs/theme-extension-identifier/codelab-overview) codelab.
|
||||
|
||||
### What you'll need
|
||||
|
||||
- A browser
|
||||
- Basic knowledge of HTML, CSS, SVG, and JavaScript.
|
||||
- Basic knowledge of your browser's developer tools.
|
||||
- Basic understanding of Blockly, including workspace components, category toolboxes, block definitions, and themes.
|
||||
|
||||
This codelab is focused on using CSS with Blockly. Non-relevant concepts are glossed over and provided for you to simply copy and paste.
|
||||
@@ -0,0 +1,184 @@
|
||||
/**************/
|
||||
/* COMPONENTS */
|
||||
/**************/
|
||||
|
||||
.blocklySvg {
|
||||
background-color: #ff7518;
|
||||
}
|
||||
|
||||
.blocklyMutatorBackground {
|
||||
fill: #ff7518;
|
||||
}
|
||||
|
||||
.blocklyToolbox {
|
||||
background-color: #f9c10e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.blocklyFlyoutBackground {
|
||||
fill: #252526;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.blocklyFlyoutLabel > .blocklyFlyoutLabelText {
|
||||
fill: #ccc !important;
|
||||
}
|
||||
|
||||
.blocklyFlyoutButton > .blocklyText {
|
||||
fill: #ccc !important;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle {
|
||||
fill: #ff0000;
|
||||
fill-opacity: 0.4;
|
||||
}
|
||||
|
||||
.blocklyInsertionMarker > .blocklyPath {
|
||||
fill: #fff !important;
|
||||
fill-opacity: 0.3 !important;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
/**************/
|
||||
/* CATEGORIES */
|
||||
/**************/
|
||||
|
||||
.logic_category {
|
||||
border-left: 8px solid #8b4513;
|
||||
}
|
||||
|
||||
.logic_category.blocklyToolboxSelected {
|
||||
background-color: #8b4513 !important;
|
||||
}
|
||||
|
||||
.loop_category {
|
||||
border-left: 8px solid #85e21f;
|
||||
}
|
||||
|
||||
.loop_category.blocklyToolboxSelected {
|
||||
background-color: #85e21f !important;
|
||||
}
|
||||
|
||||
.math_category {
|
||||
border-left: 8px solid #5b67a5;
|
||||
}
|
||||
|
||||
.math_category.blocklyToolboxSelected {
|
||||
background-color: #5b67a5 !important;
|
||||
}
|
||||
|
||||
.text_category {
|
||||
border-left: 8px solid #fe9b13;
|
||||
}
|
||||
|
||||
.text_category.blocklyToolboxSelected {
|
||||
background-color: #fe9b13 !important;
|
||||
}
|
||||
|
||||
.list_category {
|
||||
border-left: 8px solid #4a148c;
|
||||
}
|
||||
|
||||
.list_category.blocklyToolboxSelected {
|
||||
background-color: #4a148c !important;
|
||||
}
|
||||
|
||||
.variable_category {
|
||||
border-left: 8px solid #a55b80;
|
||||
}
|
||||
|
||||
.variable_category.blocklyToolboxSelected {
|
||||
background-color: #a55b80 !important;
|
||||
}
|
||||
|
||||
.procedure_category {
|
||||
border-left: 8px solid #b88cc0;
|
||||
}
|
||||
|
||||
.procedure_category.blocklyToolboxSelected {
|
||||
background-color: #b88cc0 !important;
|
||||
}
|
||||
|
||||
/**********/
|
||||
/* BLOCKS */
|
||||
/**********/
|
||||
|
||||
/* LOGIC BLOCKS */
|
||||
|
||||
.logic_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #8b4513;
|
||||
stroke: #c5eaff;
|
||||
}
|
||||
|
||||
.logic_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ff0000;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.logic_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #8b4513 !important;
|
||||
}
|
||||
|
||||
.logic_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ff0000 !important;
|
||||
}
|
||||
|
||||
/* LOOP BLOCKS */
|
||||
|
||||
.loop_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #85e21f;
|
||||
stroke: #c5eaff;
|
||||
}
|
||||
|
||||
.loop_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ff0000;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.loop_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #85e21f !important;
|
||||
}
|
||||
|
||||
.loop_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ff0000 !important;
|
||||
}
|
||||
|
||||
/* TEXT BLOCKS */
|
||||
|
||||
.text_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #fe9b13;
|
||||
stroke: #c5eaff;
|
||||
}
|
||||
|
||||
.text_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ff0000;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.text_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #fe9b13 !important;
|
||||
}
|
||||
|
||||
.text_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ff0000 !important;
|
||||
}
|
||||
|
||||
/* LIST BLOCKS */
|
||||
|
||||
.list_blocks:not(.blocklyDisabledPattern) > .blocklyPath {
|
||||
fill: #4a148c;
|
||||
stroke: #cdb6e9;
|
||||
}
|
||||
|
||||
.list_blocks:not(.blocklyDisabledPattern).blocklyShadow > .blocklyPath {
|
||||
fill: #ad7be9;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.list_blocks > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #4a148c !important;
|
||||
}
|
||||
|
||||
.list_blocks.blocklyShadow > .blocklyDropdownField .blocklyDropdownText tspan {
|
||||
fill: #ad7be9 !important;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>CSS Codelab</title>
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="./toolbox.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<link rel="stylesheet" href="halloween.css" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
float: bottom;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="start()">
|
||||
<h1>CSS Codelab</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
let workspace = null;
|
||||
|
||||
function start() {
|
||||
// Create main workspace.
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: toolboxCategories,
|
||||
renderer: 'thrasos',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,800 @@
|
||||
const toolboxCategories = {
|
||||
kind: 'categoryToolbox',
|
||||
contents: [
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Logic',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory logic_category',
|
||||
},
|
||||
contents: [
|
||||
{
|
||||
type: 'controls_if',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'logic_compare',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'EQ',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'logic_operation',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'AND',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'logic_negate',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'logic_boolean',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
BOOL: 'TRUE',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'logic_null',
|
||||
kind: 'block',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
type: 'logic_ternary',
|
||||
kind: 'block',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Loops',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory loop_category',
|
||||
},
|
||||
contents: [
|
||||
{
|
||||
type: 'controls_repeat_ext',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
TIMES: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_repeat',
|
||||
kind: 'block',
|
||||
enabled: false,
|
||||
fields: {
|
||||
TIMES: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_whileUntil',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'WHILE',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_for',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'i',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
BY: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_forEach',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'j',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_flow_statements',
|
||||
kind: 'block',
|
||||
enabled: false,
|
||||
fields: {
|
||||
FLOW: 'BREAK',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Math',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory math_category',
|
||||
},
|
||||
contents: [
|
||||
{
|
||||
type: 'math_number',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
NUM: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_arithmetic',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'ADD',
|
||||
},
|
||||
inputs: {
|
||||
A: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
B: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_single',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'ROOT',
|
||||
},
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_trig',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'SIN',
|
||||
},
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_constant',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
CONSTANT: 'PI',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_number_property',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
PROPERTY: 'EVEN',
|
||||
},
|
||||
inputs: {
|
||||
NUMBER_TO_CHECK: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_round',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'ROUND',
|
||||
},
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 3.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_on_list',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'SUM',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_modulo',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
DIVIDEND: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 64,
|
||||
},
|
||||
},
|
||||
},
|
||||
DIVISOR: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_constrain',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
LOW: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
HIGH: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_random_int',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_random_float',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'math_atan2',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
X: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
Y: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Text',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory text_category',
|
||||
},
|
||||
contents: [
|
||||
{
|
||||
type: 'text',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_join',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'text_append',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
name: 'item',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_length',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_isEmpty',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_indexOf',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
END: 'FIRST',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
FIND: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_charAt',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
WHERE: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_getSubstring',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
WHERE1: 'FROM_START',
|
||||
WHERE2: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
STRING: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_changeCase',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
CASE: 'UPPERCASE',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_trim',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'BOTH',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_count',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
SUB: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_replace',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_reverse',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
type: 'text_print',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_prompt_ext',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
TYPE: 'TEXT',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Lists',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory list_category',
|
||||
},
|
||||
contents: [
|
||||
{
|
||||
type: 'lists_create_with',
|
||||
kind: 'block',
|
||||
extraState: {
|
||||
itemCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_create_with',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'lists_repeat',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_length',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'lists_isEmpty',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'lists_indexOf',
|
||||
kind: 'block',
|
||||
|
||||
fields: {
|
||||
END: 'FIRST',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_getIndex',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'GET',
|
||||
WHERE: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_setIndex',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'SET',
|
||||
WHERE: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
LIST: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_getSublist',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
WHERE1: 'FROM_START',
|
||||
WHERE2: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
LIST: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_split',
|
||||
kind: 'block',
|
||||
|
||||
fields: {
|
||||
MODE: 'SPLIT',
|
||||
},
|
||||
inputs: {
|
||||
DELIM: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: ',',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_sort',
|
||||
kind: 'block',
|
||||
|
||||
fields: {
|
||||
TYPE: 'NUMERIC',
|
||||
DIRECTION: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_reverse',
|
||||
kind: 'block',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'sep',
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Variables',
|
||||
custom: 'VARIABLE',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory variable_category',
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Functions',
|
||||
custom: 'PROCEDURE',
|
||||
cssConfig: {
|
||||
row: 'blocklyToolboxCategory procedure_category',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: Use CSS in Blockly - Components
|
||||
slug: /codelabs/css/components
|
||||
description: Styling Blockly components with CSS
|
||||
---
|
||||
|
||||
# Use CSS in Blockly
|
||||
|
||||
## 4. Components
|
||||
|
||||
In this section, you will create CSS rules to assign the colours used by the [components section](/codelabs/theme-extension-identifier/customize-components) of the themes codelab to various components.
|
||||
|
||||
To start, create a file named `halloween.css` and add it to your `index.html` file:
|
||||
|
||||
```html
|
||||
...
|
||||
<script src="./index.js"></script>
|
||||
<link rel="stylesheet" href="halloween.css" />
|
||||
```
|
||||
|
||||
### Main workspace colour
|
||||
|
||||
Your first rule will set the background colour of the main workspace. In your Blockly editor, find the `<rect>` element with the `blocklyMainBackground` class:
|
||||
|
||||
```
|
||||
<body>
|
||||
<div class="blocklyDiv">
|
||||
<div class="injectionDiv">
|
||||
<svg class="blocklySvg">
|
||||
<g class="blocklyWorkspace">
|
||||
<rect class="blocklyMainBackground">
|
||||
```
|
||||
|
||||
This seems like a good target for your rule, except that the `fill` property is already used to set the grid pattern. Instead, we'll set the `background-color` property of the `blocklySvg` element. To do this, add the following rule to `halloween.css`:
|
||||
|
||||
```css
|
||||
/**************/
|
||||
/* COMPONENTS */
|
||||
/**************/
|
||||
|
||||
.blocklySvg {
|
||||
background-color: #ff7518;
|
||||
}
|
||||
```
|
||||
|
||||
Refresh your page and notice that the workspace background is now orange:
|
||||
|
||||

|
||||
|
||||
### Mutator workspace colour
|
||||
|
||||
Now drag an `if do` block onto the workspace and click the mutator (gear) icon. Notice that the mutator workspace's background colour is unchanged. This is because it's a different workspace. See if you can find the `<rect>` that draws the mutator workspace. (Here's a hint: It's on the bubble canvas, which is where the bubbles used by mutators, comments, and warnings are drawn.)
|
||||
|
||||
Don't worry if you didn't find it right away -- Blockly's DOM tree is fairly complex:
|
||||
|
||||
```
|
||||
<body>
|
||||
<div class="blocklyDiv">
|
||||
<div class="injectionDiv">
|
||||
<svg class="blocklySvg">
|
||||
<g class="blocklyWorkspace">
|
||||
<g class="blocklyBubbleCanvas">
|
||||
<g class="blocklyMiniWorkspaceBubble">
|
||||
<g>
|
||||
<svg>
|
||||
<g class="blocklyWorkspace">
|
||||
<rect class="blocklyMutatorBackground">
|
||||
```
|
||||
|
||||
Next, add a CSS rule to set the workspace's background colour:
|
||||
|
||||
```css
|
||||
.blocklyMutatorBackground {
|
||||
fill: #ff7518;
|
||||
}
|
||||
```
|
||||
|
||||
To see your new colours, reload the web page, drag the `if do` block out again,
|
||||
and reopen the mutator. You should see that the mutator workspace background
|
||||
colour is orange, matching the main workspace background:
|
||||

|
||||
|
||||
### Other component colours
|
||||
|
||||
Themes allow you to define the colours of many (but not all) components. The following table shows what classes and properties to use to set the same colours as the component styles in themes:
|
||||
|
||||
| Component style | Selectors (properties) |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `workspaceBackgroundColour` | `.blocklySvg (background-color)`, `.blocklyMutatorBackground (fill)` |
|
||||
| `toolboxBackgroundColour` | `.blocklyToolbox (background-color)` |
|
||||
| `toolboxForegroundColour` | `.blocklyToolbox (color)` |
|
||||
| `flyoutBackgroundColour` | `.blocklyFlyoutBackground (fill)` |
|
||||
| `flyoutForegroundColour` | `.blocklyFlyoutLabel > .blocklyFlyoutLabelText (fill)`, `.blocklyFlyoutButton > .blocklyText (fill)` |
|
||||
| `flyoutOpacity` | `.blocklyFlyoutBackground (fill-opacity)` |
|
||||
| `scrollbarColour` | `.blocklyScrollbarHandle (fill)` |
|
||||
| `scrollbarOpacity` | `.blocklyScrollbarHandle (fill-opacity)` |
|
||||
| `insertionMarkerColour` | `.blocklyInsertionMarker > .blocklyPath (fill)` |
|
||||
| `insertionMarkerOpacity` | `.blocklyInsertionMarker > .blocklyPath (fill-opacity)` |
|
||||
|
||||
Add the following rules to `halloween.css`:
|
||||
|
||||
```css
|
||||
.blocklyToolbox {
|
||||
background-color: #f9c10e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.blocklyFlyoutBackground {
|
||||
fill: #252526;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.blocklyFlyoutLabel > .blocklyFlyoutLabelText {
|
||||
fill: #ccc !important;
|
||||
}
|
||||
|
||||
.blocklyFlyoutButton > .blocklyText {
|
||||
fill: #ccc !important;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle {
|
||||
fill: #ff0000;
|
||||
fill-opacity: 0.4;
|
||||
}
|
||||
|
||||
.blocklyInsertionMarker > .blocklyPath {
|
||||
fill: #fff !important;
|
||||
fill-opacity: 0.3 !important;
|
||||
stroke: none;
|
||||
}
|
||||
```
|
||||
|
||||
Now reload your page. You should see a Blockly editor with a yellow toolbox and
|
||||
red scrollbars:
|
||||

|
||||
|
||||
### fill vs background-color
|
||||
|
||||
You might have noticed that some rules use `background-color` and `color` and
|
||||
others use `fill` and `stroke`. This is because `background-color` and `color`
|
||||
apply to HTML elements, like the `<div>` used by the toolbox, and `fill` and
|
||||
`stroke` apply to most SVG elements, like the `<path>` used by the flyout
|
||||
background. (An exception to this is the top-level `<svg>` element that contains
|
||||
Blockly, which uses `background-color` and `color`.)
|
||||
|
||||
### The !important declaration
|
||||
|
||||
You might have also noticed that some rules use an `!important` declaration while others don't. This is because Blockly sets colours in several different ways, some of which are easily overridden and some of which aren't.
|
||||
|
||||
- **Presentation attributes:** These are attributes on SVG elements, such as `fill` and `stroke`. They have a specificity of 0 and are overridden by any rules you write. As you will see later, block colours use presentation attributes.
|
||||
|
||||
- **`<style>` tags:** Many of Blockly's CSS rules are included via two `<style>` tags at the start of the `<head>` tag. Your rules override these if they have the same or higher specificity. For example, the rule for `.blocklyScrollbarHandle` has the same specificity as Blockly's rule for this class, but overrides Blockly's rule because it occurs later in the document. On the other hand, the rule for `.blocklyFlyoutLabel > .blocklyFlyoutLabelText` has a lower specificity than Blockly's rule and must override it with an `!important` declaration.
|
||||
|
||||
- **Inline styles:** These rules are included via a `style` attribute and can only be overridden by an `!important` declaration. As you will see later, the colour of the arrow in a dropdown field is set with an inline style and must be overridden with `!important`.
|
||||
|
||||
The easiest way to determine how a rule is set is to highlight the appropriate element in the element inspector and look at the corresponding style information. In a few cases, this isn't possible. For example, an insertion marker is created only when you drag a child near its parent and is deleted when you let go of the parent to highlight the insertion marker's element. In these cases, you will need to [search Blockly's rules](/guides/configure/web/appearance/css#blockly-css-rules).
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Use CSS in Blockly - Setup
|
||||
slug: /codelabs/css/setup
|
||||
description: Setup for the CSS in Blockly codelab
|
||||
---
|
||||
|
||||
# Use CSS in Blockly
|
||||
|
||||
## 2. Setup
|
||||
|
||||
### Download the sample code
|
||||
|
||||
You can get the sample code for this code by either downloading the zip here:
|
||||
|
||||
[Download zip](https://github.com/RaspberryPiFoundation/blockly/archive/main.zip)
|
||||
|
||||
or by cloning this git repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RaspberryPiFoundation/blockly.git
|
||||
```
|
||||
|
||||
If you downloaded the source as a zip, unpacking it should give you a root folder named `blockly-main`.
|
||||
|
||||
The relevant files are in `docs/docs/codelabs/css`. There are two versions of the app:
|
||||
|
||||
- `starter-code/`: The starter code that you'll build upon in this codelab.
|
||||
- `complete-code/`: The code after completing the codelab, in case you get lost or want to compare to your version.
|
||||
|
||||
Each folder contains:
|
||||
|
||||
- `index.html` - A web page containing a simple Blockly workspace.
|
||||
- `toolbox.js` - A toolbox with multiple categories.
|
||||
- `index.js` - Code to inject a simple workspace.
|
||||
|
||||
The `complete-code` folder also contains the `halloween.css` file you'll create.
|
||||
|
||||
To run the code, simply open `starter-code/index.html` in a browser. You should see a Blockly workspace with multiple categories.
|
||||
|
||||

|
||||
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>CSS Codelab</title>
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="./toolbox.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
float: bottom;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="start()">
|
||||
<h1>CSS Codelab</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
let workspace = null;
|
||||
|
||||
function start() {
|
||||
// Create main workspace.
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: toolboxCategories,
|
||||
renderer: 'thrasos',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
const toolboxCategories = {
|
||||
kind: 'categoryToolbox',
|
||||
contents: [
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Logic',
|
||||
categorystyle: 'logic_category',
|
||||
contents: [
|
||||
{
|
||||
type: 'controls_if',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'logic_compare',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'EQ',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'logic_operation',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'AND',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'logic_negate',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'logic_boolean',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
BOOL: 'TRUE',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'logic_null',
|
||||
kind: 'block',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
type: 'logic_ternary',
|
||||
kind: 'block',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Loops',
|
||||
categorystyle: 'loop_category',
|
||||
contents: [
|
||||
{
|
||||
type: 'controls_repeat_ext',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
TIMES: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_repeat',
|
||||
kind: 'block',
|
||||
enabled: false,
|
||||
fields: {
|
||||
TIMES: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_whileUntil',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'WHILE',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_for',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'i',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
BY: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_forEach',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'j',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'controls_flow_statements',
|
||||
kind: 'block',
|
||||
enabled: false,
|
||||
fields: {
|
||||
FLOW: 'BREAK',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Math',
|
||||
categorystyle: 'math_category',
|
||||
contents: [
|
||||
{
|
||||
type: 'math_number',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
NUM: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_arithmetic',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'ADD',
|
||||
},
|
||||
inputs: {
|
||||
A: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
B: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_single',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'ROOT',
|
||||
},
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_trig',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'SIN',
|
||||
},
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_constant',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
CONSTANT: 'PI',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_number_property',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
PROPERTY: 'EVEN',
|
||||
},
|
||||
inputs: {
|
||||
NUMBER_TO_CHECK: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_round',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'ROUND',
|
||||
},
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 3.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_on_list',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
OP: 'SUM',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_modulo',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
DIVIDEND: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 64,
|
||||
},
|
||||
},
|
||||
},
|
||||
DIVISOR: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_constrain',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
LOW: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
HIGH: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_random_int',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'math_random_float',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'math_atan2',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
X: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
Y: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Text',
|
||||
categorystyle: 'text_category',
|
||||
contents: [
|
||||
{
|
||||
type: 'text',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_join',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'text_append',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
name: 'item',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_length',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_isEmpty',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_indexOf',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
END: 'FIRST',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
FIND: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_charAt',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
WHERE: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_getSubstring',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
WHERE1: 'FROM_START',
|
||||
WHERE2: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
STRING: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_changeCase',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
CASE: 'UPPERCASE',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_trim',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'BOTH',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_count',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
SUB: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_replace',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_reverse',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
type: 'text_print',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text_prompt_ext',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
TYPE: 'TEXT',
|
||||
},
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Lists',
|
||||
categorystyle: 'list_category',
|
||||
contents: [
|
||||
{
|
||||
type: 'lists_create_with',
|
||||
kind: 'block',
|
||||
extraState: {
|
||||
itemCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_create_with',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'lists_repeat',
|
||||
kind: 'block',
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_length',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'lists_isEmpty',
|
||||
kind: 'block',
|
||||
},
|
||||
{
|
||||
type: 'lists_indexOf',
|
||||
kind: 'block',
|
||||
|
||||
fields: {
|
||||
END: 'FIRST',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_getIndex',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'GET',
|
||||
WHERE: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_setIndex',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
MODE: 'SET',
|
||||
WHERE: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
LIST: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_getSublist',
|
||||
kind: 'block',
|
||||
fields: {
|
||||
WHERE1: 'FROM_START',
|
||||
WHERE2: 'FROM_START',
|
||||
},
|
||||
inputs: {
|
||||
LIST: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
fields: {
|
||||
VAR: {
|
||||
name: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_split',
|
||||
kind: 'block',
|
||||
|
||||
fields: {
|
||||
MODE: 'SPLIT',
|
||||
},
|
||||
inputs: {
|
||||
DELIM: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: ',',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_sort',
|
||||
kind: 'block',
|
||||
|
||||
fields: {
|
||||
TYPE: 'NUMERIC',
|
||||
DIRECTION: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lists_reverse',
|
||||
kind: 'block',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'sep',
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Variables',
|
||||
custom: 'VARIABLE',
|
||||
categorystyle: 'variable_category',
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Functions',
|
||||
custom: 'PROCEDURE',
|
||||
categorystyle: 'procedure_category',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
pagination_next: null
|
||||
title: Use CSS in Blockly - Summary
|
||||
slug: /codelabs/css/summary
|
||||
description: Summary of the CSS in Blockly codelab
|
||||
---
|
||||
|
||||
## 7. Summary
|
||||
|
||||
In this codelab, you learned how to use CSS to set the colours of your Blockly editor.
|
||||
|
||||
For more information, see [Style with CSS](/guides/configure/web/appearance/css) in the Blockly user guides.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Use CSS in Blockly - A tour of Blockly's elements
|
||||
slug: /codelabs/css/tour
|
||||
description: Explore Blockly's HTML and SVG elements in the CSS codelab
|
||||
---
|
||||
|
||||
# Use CSS in Blockly
|
||||
|
||||
## 3. A tour of Blockly's elements
|
||||
|
||||
In this section, you'll explore the HTML and SVG elements used by Blockly, as well as the classes assigned to these elements.
|
||||
|
||||
In your Blockly editor, drag a `count with` block from the **Loops** category and an `if do` block from the **Logic** category onto your workspace and open your browser's [developer tools](https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools). Your screen should look something like this:
|
||||
|
||||

|
||||
|
||||
Using the element inspector, explore the elements used by Blockly. For example, see if you can find the SVG `<path>` element used to draw the `count with` block. If you're having trouble finding things, the following outline might help. (Note that it omits some elements and most attributes.)
|
||||
|
||||
```
|
||||
Description DOM elements
|
||||
------------------------------- ----------------------------------------------
|
||||
<body> element <body>
|
||||
App-specific container <div id="blocklyDiv">
|
||||
Injection element <div class="injectionDiv">
|
||||
Toolbox <div class="blocklyToolbox">
|
||||
Main SVG element <svg class="blocklySvg">
|
||||
Main workspace <g class="blocklyWorkspace">
|
||||
Trash <g class="blocklyTrash">
|
||||
Block canvas <g class="blocklyBlockCanvas">
|
||||
Block 1 <g class="controls_for">
|
||||
<path> element <path class="blocklyPath">
|
||||
Child block 1 <g class="math_number">
|
||||
Child block 2 <g class="math_number">
|
||||
... ...
|
||||
Field 1 <g class="blocklyLabelField">
|
||||
Field 2 <g class="blocklyDropdownField">
|
||||
... ...
|
||||
Block 2 <g class="controls_if">
|
||||
Bubble canvas <g class="blocklyBubbleCanvas">
|
||||
Scrollbar background <rect class="blocklyScrollbarBackground">
|
||||
Scrollbars, flyouts, etc. <svg>s
|
||||
Widget, dropdown, tooltip divs <div>s
|
||||
```
|
||||
|
||||
One thing that's important to notice are the `blocklyXxxx` classes assigned to most elements. These explain how Blockly uses each element and will be the targets of many of your CSS rules. If you look closely, you'll notice that many elements have multiple classes -- for example, the `<g>` element for the dropdown field in the `counts with` block has `blocklyField`, `blocklyDropdownField`, `blocklyVariableField`, and `blocklyEditableField` classes. Having multiple classes allows you to write CSS rules that affect different ranges of elements.
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/array-block-generator
|
||||
description: How to write a block code generator that creates an array.
|
||||
---
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 7. Array block generator
|
||||
|
||||
This step will build the generator for the array block. You will learn how to indent code and handle a variable number of inputs.
|
||||
|
||||
The array block uses a mutator to dynamically change the number of inputs it has.
|
||||
|
||||

|
||||
|
||||
The generated code looks like:
|
||||
|
||||
```json
|
||||
[1, "two", false, true]
|
||||
```
|
||||
|
||||
As with member blocks, there are no restrictions on the types of blocks connected to inputs.
|
||||
|
||||
### Gather values
|
||||
|
||||
Each value input on the block has a name: `ADD0`, `ADD1`, etc. Use `valueToCode` in a loop to build an array of values:
|
||||
|
||||
```js
|
||||
const values = [];
|
||||
for (let i = 0; i < block.itemCount_; i++) {
|
||||
const valueCode = generator.valueToCode(block, 'ADD' + i, Order.ATOMIC);
|
||||
if (valueCode) {
|
||||
values.push(valueCode);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the code skips empty inputs by checking if `valueCode` is `null`.
|
||||
|
||||
To include empty inputs, use the string `'null'` as the value:
|
||||
|
||||
```js
|
||||
const values = [];
|
||||
for (let i = 0; i < block.itemCount_; i++) {
|
||||
const valueCode =
|
||||
generator.valueToCode(block, 'ADD' + i, Order.ATOMIC) || 'null';
|
||||
values.push(valueCode);
|
||||
}
|
||||
```
|
||||
|
||||
### Format
|
||||
|
||||
At this point `values` is an array of `string`s. The strings contain the generated code for each input.
|
||||
|
||||
Convert the list into a single `string`, with a comma and newline separating each element:
|
||||
|
||||
```js
|
||||
const valueString = values.join(',\n');
|
||||
```
|
||||
|
||||
Next, use `prefixLines` to add indentation at the beginning of each line:
|
||||
|
||||
```js
|
||||
const indentedValueString = generator.prefixLines(
|
||||
valueString,
|
||||
generator.INDENT,
|
||||
);
|
||||
```
|
||||
|
||||
`INDENT` is a property on the generator. It defaults to two spaces, but language generators may override it to increase indent or change to tabs.
|
||||
|
||||
Finally, wrap the indented values in brackets and return the string:
|
||||
|
||||
```js
|
||||
const codeString = '[\n' + indentedValueString + '\n]';
|
||||
return [codeString, Order.ATOMIC];
|
||||
```
|
||||
|
||||
### Putting it all together
|
||||
|
||||
Here is the final array block generator:
|
||||
|
||||
```js
|
||||
jsonGenerator.forBlock['lists_create_with'] = function (block, generator) {
|
||||
const values = [];
|
||||
for (let i = 0; i < block.itemCount_; i++) {
|
||||
const valueCode = generator.valueToCode(block, 'ADD' + i, Order.ATOMIC);
|
||||
if (valueCode) {
|
||||
values.push(valueCode);
|
||||
}
|
||||
}
|
||||
const valueString = values.join(',\n');
|
||||
const indentedValueString = generator.prefixLines(
|
||||
valueString,
|
||||
generator.INDENT,
|
||||
);
|
||||
const codeString = '[\n' + indentedValueString + '\n]';
|
||||
return [codeString, Order.ATOMIC];
|
||||
};
|
||||
```
|
||||
|
||||
### Test it
|
||||
|
||||
Test the block generator by adding an array to the onscreen blocks and populating it.
|
||||
|
||||
What code does it generate if there are no inputs?
|
||||
|
||||
What if there are five inputs, one of which is empty?
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/block-generator-overview
|
||||
description: Introduction to block generators.
|
||||
---
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 4. Block generator overview
|
||||
|
||||
At its core, a block generator is a function that takes in a block (and optionally the language generator instance), translates the block into code, and returns that code as a string.
|
||||
|
||||
Each language generator has a property called `forBlock`, which is a dictionary object where all block generator functions must be placed. For instance, here is the code to add a block generator for blocks of type `sample_block` on a language generator object (`sampleGenerator`).
|
||||
|
||||
```js
|
||||
sampleGenerator.forBlock['sample_block'] = function (block, generator) {
|
||||
return 'my code string';
|
||||
};
|
||||
```
|
||||
|
||||
### Statement blocks
|
||||
|
||||
Statement blocks represent code that does not return a value.
|
||||
|
||||
A statement block's generator simply returns a string.
|
||||
|
||||
For example, this code defines a block generator that always returns the same function call.
|
||||
|
||||
```js
|
||||
sampleGenerator.forBlock['left_turn_block'] = function (block, generator) {
|
||||
return 'turnLeft()';
|
||||
};
|
||||
```
|
||||
|
||||
### Value blocks
|
||||
|
||||
Value blocks represent code that returns a value.
|
||||
|
||||
A value block's generator returns an array containing a string and a [precedence value](/guides/create-custom-blocks/code-generation/operator-precedence). The built-in generators have predefined operator precedence values exported as an `Order` enum.
|
||||
|
||||
This code defines a block generator that always returns `1 + 1`:
|
||||
|
||||
```js
|
||||
sampleGenerator.forBlock['two_block'] = function (block, generator) {
|
||||
return ['1 + 1', Order.ADDITION];
|
||||
};
|
||||
```
|
||||
|
||||
### Operator precedence
|
||||
|
||||
Operator precedence rules determine how the correct order of operations is maintained during parsing. In Blockly's generators, operator precedence determines when to add parentheses.
|
||||
|
||||
--> Read more about [operator precedence in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence).
|
||||
|
||||
--> Read more about [operator precedence in Blockly](/guides/create-custom-blocks/code-generation/operator-precedence).
|
||||
|
||||
Since JSON does not allow values that are expressions, the code does not need to consider operator precedence for the generator being built in this codelab. The same value can be used everywhere a precedence value is required. Since parentheses never need to be added to the JSON, call this value `ATOMIC`.
|
||||
|
||||
In `src/generators/json.js`, declare a new enum called `Order` and add the `ATOMIC` value:
|
||||
|
||||
```js
|
||||
const Order = {
|
||||
ATOMIC: 0,
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
pagination_prev: null
|
||||
slug: /codelabs/custom-generator/codelab-overview
|
||||
description: Overview of the "Build a custom generator" codelab.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 1. Codelab overview
|
||||
|
||||
### What you'll learn
|
||||
|
||||
- How to create a custom language generator.
|
||||
- How to create block generator definitions for existing blocks.
|
||||
- How to create block generator definitions for new blocks.
|
||||
- How to use a custom generator in an application.
|
||||
|
||||
### What you'll build
|
||||
|
||||
You will build a JSON generator that implements the [JSON language spec](https://www.json.org/json-en.html).
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-generator/json_workspace.png"
|
||||
alt="Screenshot of the toolbox and workspace built in this codelab. It contains blocks that implement the JSON spec, like member, object, lists, strings, and numbers."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
### What you'll need
|
||||
|
||||
- Familiarity with JSON and the JSON specification.
|
||||
- Basic understanding of blocks and toolboxes in Blockly.
|
||||
- NPM installed ([instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)).
|
||||
- Comfort using the command line/terminal.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Blockly Sample App
|
||||
|
||||
## Purpose
|
||||
|
||||
This app illustrates how to use Blockly together with common programming tools like node/npm, webpack, typescript, eslint, and others. You can use it as the starting point for your own application and modify it as much as you'd like. It contains basic infrastructure for running, building, testing, etc. that you can use even if you don't understand how to configure the related tool yet. When your needs outgrow the functionality provided here, you can replace the provided configuration or tool with your own.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. [Install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) npm if you haven't before.
|
||||
2. Run [`npx @blockly/create-package app <application-name>`](https://www.npmjs.com/package/@blockly/create-package) to clone this application to your own machine.
|
||||
3. Run `npm install` to install the required dependencies.
|
||||
4. Run `npm run start` to run the development server and see the app in action.
|
||||
5. If you make any changes to the source code, just refresh the browser while the server is running to see them.
|
||||
|
||||
## Tooling
|
||||
|
||||
The application uses many of the same tools that the Blockly team uses to develop Blockly itself. Following is a brief overview, and you can read more about them on our [developer site](https://developers.google.com/blockly/guides/contribute/get-started/development_tools).
|
||||
|
||||
- Structure: The application is built as an npm package. You can use npm to manage the dependencies of the application.
|
||||
- Modules: ES6 modules to handle imports to/exports from other files.
|
||||
- Building/bundling: Webpack to build the source code and bundle it into one file for serving.
|
||||
- Development server: webpack-dev-server to run locally while in development.
|
||||
- Testing: Mocha to run unit tests.
|
||||
- Linting: Eslint to lint the code and ensure it conforms with a standard style.
|
||||
- UI Framework: Does not use a framework. For more complex applications, you may wish to integrate a UI framework like React or Angular.
|
||||
|
||||
You can disable, reconfigure, or replace any of these tools at any time, but they are preconfigured to get you started developing your Blockly application quickly.
|
||||
|
||||
## Structure
|
||||
|
||||
- `package.json` contains basic information about the app. This is where the scripts to run, build, etc. are listed.
|
||||
- `package-lock.json` is used by npm to manage dependencies
|
||||
- `webpack.config.js` is the configuration for webpack. This handles bundling the application and running our development server.
|
||||
- `src/` contains the rest of the source code.
|
||||
- `dist/` contains the packaged output (that you could host on a server, for example). This is ignored by git and will only appear after you run `npm run build` or `npm run start`.
|
||||
|
||||
### Source Code
|
||||
|
||||
- `index.html` contains the skeleton HTML for the page. This file is modified during the build to import the bundled source code output by webpack.
|
||||
- `index.js` is the entry point of the app. It configures Blockly and sets up the page to show the blocks, the generated code, and the output of running the code in JavaScript.
|
||||
- `serialization.js` has code to save and load the workspace using the browser's local storage. This is how your workspace is saved even after refreshing or leaving the page. You could replace this with code that saves the user's data to a cloud database instead.
|
||||
- `toolbox.js` contains the toolbox definition for the app. The current toolbox contains nearly every block that Blockly provides out of the box. You probably want to replace this definition with your own toolbox that uses your custom blocks and only includes the default blocks that are relevant to your application.
|
||||
- `blocks/text.js` has code for a custom text block, just as an example of creating your own blocks. You probably want to delete this block, and add your own blocks in this directory.
|
||||
- `generators/javascript.js` contains the JavaScript generator for the custom text block. You'll need to include block generators for any custom blocks you create, in whatever programming language(s) your application will use.
|
||||
|
||||
## Serving
|
||||
|
||||
To run your app locally, run `npm run start` to run the development server. This mode generates source maps and ingests the source maps created by Blockly, so that you can debug using unminified code.
|
||||
|
||||
To deploy your app so that others can use it, run `npm run build` to run a production build. This will bundle your code and minify it to reduce its size. You can then host the contents of the `dist` directory on a web server of your choosing. If you're just getting started, try using [GitHub Pages](https://pages.github.com/).
|
||||
+8906
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "generator-sample-app",
|
||||
"version": "1.0.0",
|
||||
"description": "A sample app using Blockly and custom generators",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack --mode production",
|
||||
"start": "webpack serve --open --mode development"
|
||||
},
|
||||
"keywords": [
|
||||
"blockly"
|
||||
],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"css-loader": "^6.7.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"webpack": "^5.93.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview All the custom JSON-related blocks defined in the custom
|
||||
* generator codelab.
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
|
||||
export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
|
||||
{
|
||||
type: 'object',
|
||||
message0: '{ %1 %2 }',
|
||||
args0: [
|
||||
{
|
||||
type: 'input_dummy',
|
||||
},
|
||||
{
|
||||
type: 'input_statement',
|
||||
name: 'MEMBERS',
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
colour: 230,
|
||||
},
|
||||
{
|
||||
type: 'member',
|
||||
message0: '%1 %2 %3',
|
||||
args0: [
|
||||
{
|
||||
type: 'field_input',
|
||||
name: 'MEMBER_NAME',
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
type: 'field_label',
|
||||
name: 'COLON',
|
||||
text: ':',
|
||||
},
|
||||
{
|
||||
type: 'input_value',
|
||||
name: 'MEMBER_VALUE',
|
||||
},
|
||||
],
|
||||
previousStatement: null,
|
||||
nextStatement: null,
|
||||
colour: 230,
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview The full custom JSON generator built during the custom
|
||||
* generator codelab.
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
|
||||
export const jsonGenerator = new Blockly.Generator('JSON');
|
||||
|
||||
const Order = {
|
||||
ATOMIC: 0,
|
||||
};
|
||||
|
||||
jsonGenerator.scrub_ = function (block, code, thisOnly) {
|
||||
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
||||
if (nextBlock && !thisOnly) {
|
||||
return code + ',\n' + jsonGenerator.blockToCode(nextBlock);
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
jsonGenerator.forBlock['logic_null'] = function (block) {
|
||||
return ['null', Order.ATOMIC];
|
||||
};
|
||||
|
||||
jsonGenerator.forBlock['text'] = function (block) {
|
||||
const textValue = block.getFieldValue('TEXT');
|
||||
const code = `"${textValue}"`;
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
|
||||
jsonGenerator.forBlock['math_number'] = function (block) {
|
||||
const code = String(block.getFieldValue('NUM'));
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
|
||||
jsonGenerator.forBlock['logic_boolean'] = function (block) {
|
||||
const code = block.getFieldValue('BOOL') == 'TRUE' ? 'true' : 'false';
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
|
||||
jsonGenerator.forBlock['member'] = function (block, generator) {
|
||||
const name = block.getFieldValue('MEMBER_NAME');
|
||||
const value = generator.valueToCode(block, 'MEMBER_VALUE', Order.ATOMIC);
|
||||
const code = `"${name}": ${value}`;
|
||||
return code;
|
||||
};
|
||||
|
||||
jsonGenerator.forBlock['lists_create_with'] = function (block, generator) {
|
||||
const values = [];
|
||||
for (let i = 0; i < block.itemCount_; i++) {
|
||||
const valueCode = generator.valueToCode(block, 'ADD' + i, Order.ATOMIC);
|
||||
if (valueCode) {
|
||||
values.push(valueCode);
|
||||
}
|
||||
}
|
||||
const valueString = values.join(',\n');
|
||||
const indentedValueString = generator.prefixLines(
|
||||
valueString,
|
||||
generator.INDENT,
|
||||
);
|
||||
const codeString = '[\n' + indentedValueString + '\n]';
|
||||
return [codeString, Order.ATOMIC];
|
||||
};
|
||||
|
||||
jsonGenerator.forBlock['object'] = function (block, generator) {
|
||||
const statementMembers = generator.statementToCode(block, 'MEMBERS');
|
||||
const code = '{\n' + statementMembers + '\n}';
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
body {
|
||||
margin: 0;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#pageContainer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
flex-basis: 100%;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
#outputPane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 400px;
|
||||
flex: 0 0 400px;
|
||||
overflow: auto;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
#generatedCode {
|
||||
height: 50%;
|
||||
background-color: rgb(247, 240, 228);
|
||||
}
|
||||
|
||||
#output {
|
||||
height: 50%;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Blockly Sample App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="pageContainer">
|
||||
<div id="outputPane">
|
||||
<pre id="generatedCode"><code></code></pre>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<div id="blocklyDiv"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
import { blocks } from './blocks/json';
|
||||
import { jsonGenerator } from './generators/json';
|
||||
import { save, load } from './serialization';
|
||||
import { toolbox } from './toolbox';
|
||||
import './index.css';
|
||||
|
||||
// Register the blocks with Blockly
|
||||
Blockly.common.defineBlocks(blocks);
|
||||
|
||||
// Set up UI elements and inject Blockly
|
||||
const codeDiv = document.getElementById('generatedCode').firstChild;
|
||||
const blocklyDiv = document.getElementById('blocklyDiv');
|
||||
const ws = Blockly.inject(blocklyDiv, { toolbox });
|
||||
|
||||
// This function resets the code div and shows the
|
||||
// generated code from the workspace.
|
||||
const runCode = () => {
|
||||
const code = jsonGenerator.workspaceToCode(ws);
|
||||
codeDiv.innerText = code;
|
||||
};
|
||||
|
||||
// Load the initial state from storage and run the code.
|
||||
load(ws);
|
||||
runCode();
|
||||
|
||||
// Every time the workspace changes state, save the changes to storage.
|
||||
ws.addChangeListener((e) => {
|
||||
// UI events are things like scrolling, zooming, etc.
|
||||
// No need to save after one of these.
|
||||
if (e.isUiEvent) return;
|
||||
save(ws);
|
||||
});
|
||||
|
||||
// Whenever the workspace changes meaningfully, run the code again.
|
||||
ws.addChangeListener((e) => {
|
||||
// Don't run the code when the workspace finishes loading; we're
|
||||
// already running it once when the application starts.
|
||||
// Don't run the code during drags; we might have invalid state.
|
||||
if (
|
||||
e.isUiEvent ||
|
||||
e.type == Blockly.Events.FINISHED_LOADING ||
|
||||
ws.isDragging()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
runCode();
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
// Use a unique storage key for this codelab
|
||||
const storageKey = 'jsonGeneratorWorkspace';
|
||||
|
||||
/**
|
||||
* Saves the state of the workspace to browser's local storage.
|
||||
* @param {Blockly.Workspace} workspace Blockly workspace to save.
|
||||
*/
|
||||
export const save = function (workspace) {
|
||||
const data = Blockly.serialization.workspaces.save(workspace);
|
||||
window.localStorage?.setItem(storageKey, JSON.stringify(data));
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads saved state from local storage into the given workspace.
|
||||
* @param {Blockly.Workspace} workspace Blockly workspace to load into.
|
||||
*/
|
||||
export const load = function (workspace) {
|
||||
const data = window.localStorage?.getItem(storageKey);
|
||||
if (!data) return;
|
||||
|
||||
// Don't emit events during loading.
|
||||
Blockly.Events.disable();
|
||||
Blockly.serialization.workspaces.load(JSON.parse(data), workspace, false);
|
||||
Blockly.Events.enable();
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const toolbox = {
|
||||
kind: 'flyoutToolbox',
|
||||
contents: [
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'member',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_number',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_boolean',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_null',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_create_with',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
// Base config that applies to either development or production mode.
|
||||
const config = {
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
// Compile the source files into a bundle.
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
clean: true,
|
||||
},
|
||||
// Enable webpack-dev-server to get hot refresh of the app.
|
||||
devServer: {
|
||||
static: './build',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// Load CSS files. They can be imported into JS files.
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// Generate the HTML index page based on our template.
|
||||
// This will output the same index page with the bundle we
|
||||
// created above added in a script tag.
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
if (argv.mode === 'development') {
|
||||
// Set the output path to the `build` directory
|
||||
// so we don't clobber production builds.
|
||||
config.output.path = path.resolve(__dirname, 'build');
|
||||
|
||||
// Generate source maps for our code for easier debugging.
|
||||
// Not suitable for production builds. If you want source maps in
|
||||
// production, choose a different one from https://webpack.js.org/configuration/devtool
|
||||
config.devtool = 'eval-cheap-module-source-map';
|
||||
|
||||
// Include the source maps for Blockly for easier debugging Blockly code.
|
||||
config.module.rules.push({
|
||||
test: /(blockly[/\\].*\.js)$/,
|
||||
use: [require.resolve('source-map-loader')],
|
||||
enforce: 'pre',
|
||||
});
|
||||
|
||||
// Ignore spurious warnings from source-map-loader
|
||||
// It can't find source maps for some Closure modules and that is expected
|
||||
config.ignoreWarnings = [/Failed to parse source map.*blockly/];
|
||||
}
|
||||
return config;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/generating-a-stack
|
||||
description: How to generate code for the blocks in a stack.
|
||||
---
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 9. Generating a stack
|
||||
|
||||
### The scrub\_ function
|
||||
|
||||
The `scrub_` function is called on every block from `blockToCode`. It takes in three arguments:
|
||||
|
||||
- `block` is the current block.
|
||||
- `code` is the code generated for this block, which includes code from all attached value blocks.
|
||||
- `opt_thisOnly` is an optional `boolean`. If true, code should be generated for this block but no subsequent blocks.
|
||||
|
||||
By default, `scrub_` simply returns the passed-in code. A common pattern is to override the function to also generate code for any blocks that follow the current block in a stack. In this case, the code will add commas and newlines between object members:
|
||||
|
||||
```js
|
||||
jsonGenerator.scrub_ = function (block, code, thisOnly) {
|
||||
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
||||
if (nextBlock && !thisOnly) {
|
||||
return code + ',\n' + jsonGenerator.blockToCode(nextBlock);
|
||||
}
|
||||
return code;
|
||||
};
|
||||
```
|
||||
|
||||
### Testing scrub\_
|
||||
|
||||
Create a stack of `member` blocks on the workspace. There should be generated code for all of the blocks, not just the first one.
|
||||
|
||||
Next, add an `object` block and drag the `member` blocks into it. This case tests `statementToCode`, and should generate code for all of of the blocks.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/member-block-generator
|
||||
description: How to write a block code generator using field and input values.
|
||||
---
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 6. Member block generator
|
||||
|
||||
This step will build the generator for the `member` block. It will use the function `getFieldValue`, and introduce the function `valueToCode`.
|
||||
|
||||
The member block has a text input field and a value input.
|
||||
|
||||

|
||||
|
||||
The generated code looks like `"property name": "property value"`.
|
||||
|
||||
### Field value
|
||||
|
||||
The **property name** is the value of the text input, which is fetched via `getFieldValue`:
|
||||
|
||||
```js
|
||||
const name = block.getFieldValue('MEMBER_NAME');
|
||||
```
|
||||
|
||||
Recall: the name of the value being fetched is `MEMBER_NAME` because that is how it was defined in `src/blocks/json.js`.
|
||||
|
||||
### Input value
|
||||
|
||||
The **property value** is whatever is attached to the value input. A variety of blocks could be attached there: `logic_null`, `text`, `math_number`, `logic_boolean`, or even an array (`lists_create_with`). Use `valueToCode` to get the correct value:
|
||||
|
||||
```js
|
||||
const value = generator.valueToCode(block, 'MEMBER_VALUE', Order.ATOMIC);
|
||||
```
|
||||
|
||||
`valueToCode` does three things:
|
||||
|
||||
- Finds the blocks connected to the named value input (the second argument)
|
||||
- Generates the code for that block
|
||||
- Returns the code as a string
|
||||
|
||||
If no block is attached, `valueToCode` returns `null`. In another generator, `valueToCode` might need to replace `null` with a different default value; in JSON, `null` is fine.
|
||||
|
||||
The third argument is related to operator precedence. It is used to determine if parentheses need to be added around the value. In JSON, parentheses will never be added, as discussed in an earlier section.
|
||||
|
||||
### Build the code string
|
||||
|
||||
Next, assemble the arguments `name` and `value` into the correct code, of the form `"name": value`.
|
||||
|
||||
```js
|
||||
const code = `"${name}": ${value}`;
|
||||
```
|
||||
|
||||
### Put it all together
|
||||
|
||||
All together, here is block generator for the member block:
|
||||
|
||||
```js
|
||||
jsonGenerator.forBlock['member'] = function (block, generator) {
|
||||
const name = block.getFieldValue('MEMBER_NAME');
|
||||
const value = generator.valueToCode(block, 'MEMBER_VALUE', Order.ATOMIC);
|
||||
const code = `"${name}": ${value}`;
|
||||
return code;
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/object-block-generator
|
||||
description: How to write a block code generator that creates an object.
|
||||
---
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 8. Object block generator
|
||||
|
||||
This section will write the generator for the `object` block and will demonstrate how to use `statementToCode`.
|
||||
|
||||
The `object` block generates code for a JSON Object. It has a single statement input, in which member blocks may be stacked.
|
||||
|
||||

|
||||
|
||||
The generated code looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"a": true,
|
||||
"b": "one",
|
||||
"c": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Get the contents
|
||||
|
||||
We'll use `statementToCode` to get the code for the blocks attached to the statement input of the `object` block.
|
||||
|
||||
`statementToCode` does three things:
|
||||
|
||||
- Finds the first block connected to the named statement input (the second argument)
|
||||
- Generates the code for that block
|
||||
- Returns the code as a string
|
||||
|
||||
In this case the input name is `'MEMBERS'`.
|
||||
|
||||
```js
|
||||
const statement_members = generator.statementToCode(block, 'MEMBERS');
|
||||
```
|
||||
|
||||
### Format and return
|
||||
|
||||
Wrap the statements in curly brackets and return the code, using the default precedence:
|
||||
|
||||
```js
|
||||
const code = '{\n' + statement_members + '\n}';
|
||||
return [code, Order.ATOMIC];
|
||||
```
|
||||
|
||||
Note that `statementToCode` handles the indentation automatically.
|
||||
|
||||
### Test it
|
||||
|
||||
Here is the full block generator:
|
||||
|
||||
```js
|
||||
jsonGenerator.forBlock['object'] = function (block, generator) {
|
||||
const statementMembers = generator.statementToCode(block, 'MEMBERS');
|
||||
const code = '{\n' + statementMembers + '\n}';
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
```
|
||||
|
||||
Test it by generating code for an `object` block containing a single `member` block. The result should look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"test": true
|
||||
}
|
||||
```
|
||||
|
||||
Next, add a second member block and rerun the generator. Did the resulting code change? Let's look at the next section to find out why not.
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/setup
|
||||
description: Setting up the "Build a custom generator" codelab.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 2. Setup
|
||||
|
||||
This codelab will demonstrate how to add code to the Blockly sample app to create and use a new generator.
|
||||
|
||||
### The application
|
||||
|
||||
Use the [`npx @blockly/create-package app`](https://www.npmjs.com/package/@blockly/create-package) command to create a standalone application that contains a sample setup of Blockly, including custom blocks and a display of the generated code and output.
|
||||
|
||||
1. Run `npx @blockly/create-package app custom-generator-codelab`. This will create a blockly application in the folder `custom-generator-codelab`.
|
||||
1. `cd` into the new directory: `cd custom-generator-codelab`.
|
||||
1. Run `npm start` to start the server and run the sample application.
|
||||
1. The sample app will automatically run in the browser window that opens.
|
||||
|
||||
The initial application has one custom block and includes JavaScript generator definitions for that block. Since this codelab will be creating a JSON generator instead, it will remove that custom block and add its own.
|
||||
|
||||
The complete code used in this codelab can be viewed in the `blockly` repository under [docs/docs/codelabs/custom-generator](https://github.com/RaspberryPiFoundation/blockly/docs/docs/codelabs/custom-generator/complete-code).
|
||||
|
||||
Before setting up the rest of the application, change the storage key used for this codelab application. This will ensure that the workspace is saved in its own storage, separate from the regular sample app, so that it doesn't interfere with other demos. In `serialization.js`, change the value of `storageKey` to some unique string. `jsonGeneratorWorkspace` will work:
|
||||
|
||||
```js
|
||||
// Use a unique storage key for this codelab
|
||||
const storageKey = 'jsonGeneratorWorkspace';
|
||||
```
|
||||
|
||||
### Blocks
|
||||
|
||||
This codelab will use two custom blocks, as well as five blocks from Blockly's standard set.
|
||||
|
||||
The custom blocks represent the _Object_ and _Member_ sections of the JSON specification.
|
||||
|
||||
The blocks are:
|
||||
|
||||
- `object`
|
||||
- `member`
|
||||
- `math_number`
|
||||
- `text`
|
||||
- `logic_boolean`
|
||||
- `logic_null`
|
||||
- `lists_create_with`
|
||||
|
||||
### Custom block definitions
|
||||
|
||||
Create a new file in the `src/blocks/` directory called `json.js`. This will hold the custom JSON-related blocks. Add the following code:
|
||||
|
||||
```js
|
||||
import * as Blockly from 'blockly';
|
||||
|
||||
export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
|
||||
{
|
||||
type: 'object',
|
||||
message0: '{ %1 %2 }',
|
||||
args0: [
|
||||
{
|
||||
type: 'input_dummy',
|
||||
},
|
||||
{
|
||||
type: 'input_statement',
|
||||
name: 'MEMBERS',
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
colour: 230,
|
||||
},
|
||||
{
|
||||
type: 'member',
|
||||
message0: '%1 %2 %3',
|
||||
args0: [
|
||||
{
|
||||
type: 'field_input',
|
||||
name: 'MEMBER_NAME',
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
type: 'field_label',
|
||||
name: 'COLON',
|
||||
text: ':',
|
||||
},
|
||||
{
|
||||
type: 'input_value',
|
||||
name: 'MEMBER_VALUE',
|
||||
},
|
||||
],
|
||||
previousStatement: null,
|
||||
nextStatement: null,
|
||||
colour: 230,
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
This code creates the block definitions, but it doesn't register the definitions with Blockly to make the blocks usable. We'll do that in `src/index.js`.
|
||||
Currently, the app imports `blocks` from the original sample file, `text.js`. Instead, it should import the definitions that were just added. Remove the original import:
|
||||
|
||||
```js
|
||||
// Remove this!
|
||||
import { blocks } from './blocks/text';
|
||||
```
|
||||
|
||||
and add the import for the new blocks:
|
||||
|
||||
```js
|
||||
import { blocks } from './blocks/json';
|
||||
```
|
||||
|
||||
Later in the file the block definitions are registered with Blockly (this code is already present and does not need to be added):
|
||||
|
||||
```js
|
||||
Blockly.common.defineBlocks(blocks);
|
||||
```
|
||||
|
||||
### Toolbox definition
|
||||
|
||||
Next, define a toolbox that includes these custom blocks. For this example, there's a flyout-only toolbox with seven blocks in it.
|
||||
|
||||
The file `src/toolbox.js` contains the original sample toolbox. Replace the entire contents of that file with this code:
|
||||
|
||||
```js
|
||||
export const toolbox = {
|
||||
kind: 'flyoutToolbox',
|
||||
contents: [
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'object',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'member',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_number',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_boolean',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_null',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_create_with',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Our `index.js` file already handles importing the toolbox and using it in Blockly.
|
||||
|
||||
If the server is already running, refresh the page to see changes. Otherwise, run `npm start` to start the server. New blocks should now exist in the toolbox, like this:
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-generator/toolbox_blocks.png"
|
||||
alt="Screenshot of toolbox showing the added blocks, including the new member and object blocks, plus the built-in number, text, boolean, null, and list blocks."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
The app is still trying to generate and run JavaScript for the workspace, instead of JSON. We will change that soon.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
pagination_next: null
|
||||
slug: /codelabs/custom-generator/summary
|
||||
description: Summary of the "Build a custom generator" codelab.
|
||||
---
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 10. Summary
|
||||
|
||||
In this codelab you learned:
|
||||
|
||||
- How to build a custom language generator to generate JSON.
|
||||
- How to define block generators for built in blocks and for custom blocks.
|
||||
- How to use a custom generator in a sample app.
|
||||
- How to use the core generator functions: `statementToCode`, `valueToCode`, `blockToCode`, and `getFieldValue`.
|
||||
- How to generate code for stacks of blocks.
|
||||
|
||||
JSON is a simple language, and there are many additional features that could be implemented in a custom generator. Blockly's built-in language generators are a good place to learn more about some additional features:
|
||||
|
||||
- Variable definition and use.
|
||||
- Function definition and use.
|
||||
- Initialization and cleanup.
|
||||
- Injecting additional functions and variables.
|
||||
- Handling comments.
|
||||
- Handling parentheses with operator precedence.
|
||||
|
||||
Blockly ships with five language generators: Python, Dart, JavaScript, PHP, and Lua. The language generators and block generators can be found in the [generators directory](https://github.com/RaspberryPiFoundation/blockly/tree/main/generators).
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/the-basics
|
||||
description: How to define and call a language generator.
|
||||
---
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 3. The basics
|
||||
|
||||
A _language generator_ defines the basic properties of a language, such as how indentation works. _Block generators_ define how individual blocks are turned into code, and must be defined for every block used.
|
||||
|
||||
A language generator has a single entry point: `workspaceToCode`. This function takes in a workspace and:
|
||||
|
||||
- Initializes the generator and any necessary state by calling `init`.
|
||||
- Walks the list of top blocks on the workspace and calls `blockToCode` on each top block.
|
||||
- Cleans up any leftover state by calling `finish`.
|
||||
- Returns the generated code.
|
||||
|
||||
### Create the language generator
|
||||
|
||||
The first step is to define and call the custom language generator.
|
||||
|
||||
A custom language generator is simply an instance of `Blockly.Generator`. Create a new file `src/generators/json.js`. In it, import Blockly and call the `Blockly.Generator` constructor, passing in the generator's name and storing the result.
|
||||
|
||||
```js
|
||||
import * as Blockly from 'blockly';
|
||||
|
||||
export const jsonGenerator = new Blockly.CodeGenerator('JSON');
|
||||
```
|
||||
|
||||
### Generate code
|
||||
|
||||
Next, hook up the new generator with the sample app. First, remove the old code that imports the new block generator properties and assigns them to the `javascriptGenerator`. Remove these lines from `src/index.js`:
|
||||
|
||||
```js
|
||||
// Remove these lines!
|
||||
import { forBlock } from './generators/javascript';
|
||||
import { javascriptGenerator } from 'blockly/javascript';
|
||||
|
||||
// Also remove this line! (further down)
|
||||
Object.assign(javascriptGenerator.forBlock, forBlock);
|
||||
```
|
||||
|
||||
Now import the new generator:
|
||||
|
||||
```js
|
||||
import { jsonGenerator } from './generators/json';
|
||||
```
|
||||
|
||||
Currently, there are two panels in the app next to the workspace. One shows the generated JavaScript code, and one executes it. The one panel showing the generated Javascript code will be changed to show the generated JSON code instead. Since JSON can't be directly executed, the panel that shows the execution will be left blank. Change the `runCode` function to the following:
|
||||
|
||||
```js
|
||||
// This function resets the code div and shows the
|
||||
// generated code from the workspace.
|
||||
const runCode = () => {
|
||||
const code = jsonGenerator.workspaceToCode(ws);
|
||||
codeDiv.innerText = code;
|
||||
};
|
||||
```
|
||||
|
||||
Since the bottom panel is not being modified, delete this line:
|
||||
|
||||
```js
|
||||
// Remove this line!
|
||||
const outputDiv = document.getElementById('output');
|
||||
```
|
||||
|
||||
The generated code will now be shown automatically in the top left panel. Refresh the sample app page to see the changes so far.
|
||||
|
||||
### Test it
|
||||
|
||||
Put a number block on the workspace and check the generator output area. It's empty, so check the console. You should see an error:
|
||||
|
||||
```
|
||||
Language "JSON" does not know how to generate code for block type "math_number".
|
||||
```
|
||||
|
||||
This error occurs because there has to be a block generator for each type of block. Read the next section for more details.
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
slug: /codelabs/custom-generator/value-block-generators
|
||||
description: How to write a block code generator for a simple value block.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Build a custom generator
|
||||
|
||||
## 5. Value block generators
|
||||
|
||||
This step will build the generators for the simple value blocks: `logic_null`, `text`, `math_number`, and `logic_boolean`.
|
||||
|
||||
It will use `getFieldValue` on several types of fields.
|
||||
|
||||
### Null
|
||||
|
||||
The simplest block in this example is the `logic_null` block.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-generator/null_block.png"
|
||||
alt='The null block simply returns "null".'
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
No matter what, it generates the code `'null'`. Notice that this is a string, because all generated code is a string.
|
||||
Add the following code to `src/generators/json.js`:
|
||||
|
||||
```js
|
||||
jsonGenerator.forBlock['logic_null'] = function (block) {
|
||||
return ['null', Order.ATOMIC];
|
||||
};
|
||||
```
|
||||
|
||||
### String
|
||||
|
||||
Next is the `text` block.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-generator/text_block.png"
|
||||
alt="The text block has an input for the user to type text into."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
Unlike `logic_null`, there is a single text input field on this block. Use `getFieldValue`:
|
||||
|
||||
```js
|
||||
const textValue = block.getFieldValue('TEXT');
|
||||
```
|
||||
|
||||
Since this is a string in the generated code, wrap the value in quotation marks and return it:
|
||||
|
||||
```js
|
||||
jsonGenerator.forBlock['text'] = function (block) {
|
||||
const textValue = block.getFieldValue('TEXT');
|
||||
const code = `"${textValue}"`;
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
```
|
||||
|
||||
### Number
|
||||
|
||||
The `math_number` block has a number field.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-generator/number_block.png"
|
||||
alt="The number block has an input for a user to type a number"
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
Like the `text` block, the `math_number` block can use `getFieldValue`. Unlike the text block, the function doesn't need to wrap it in additional quotation marks, because in the JSON code, it won't be a string.
|
||||
|
||||
However, like all generated code and as with `null` above, the function needs to return the code as a string from the generator.
|
||||
|
||||
```js
|
||||
jsonGenerator.forBlock['math_number'] = function (block) {
|
||||
const code = String(block.getFieldValue('NUM'));
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
```
|
||||
|
||||
### Boolean
|
||||
|
||||
The `logic_boolean` block has a dropdown field named `BOOL`.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-generator/boolean_block.png"
|
||||
alt="The boolean block lets the user select 'true' or 'false' from a dropdown menu."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
Calling `getFieldValue` on a dropdown field returns the value of the selected option, which may not be the same as the display text. In this case the dropdown has two possible values: `TRUE` and `FALSE`.
|
||||
|
||||
```js
|
||||
jsonGenerator.forBlock['logic_boolean'] = function (block) {
|
||||
const code = block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false';
|
||||
return [code, Order.ATOMIC];
|
||||
};
|
||||
```
|
||||
|
||||
### Summary
|
||||
|
||||
- Value blocks return an array containing the value as a string and the precedence.
|
||||
- `getFieldValue` finds the field with the specified name and returns its value.
|
||||
- The type of the return value from `getFieldValue` depends on the type of the field.
|
||||
- Each field type must document what its value represents.
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
slug: /codelabs/custom-renderer/change-connection-shapes
|
||||
description: How to change connection shapes.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 7. Change connection shapes
|
||||
|
||||
This step will define and use new shapes for previous/next connections and input/output connections. This takes three steps:
|
||||
|
||||
1. Define new shape objects.
|
||||
1. Override `init()` to store the new shape objects.
|
||||
1. Override `shapeFor(connection)` to return the new objects.
|
||||
|
||||
### Define a previous/next connection shape
|
||||
|
||||
An outline path is drawn clockwise around the block, starting at the top left. As a result the previous connection is drawn from left-to-right, while the next connection is drawn from right-to-left.
|
||||
|
||||
Previous and next connections are defined by the same object. The object has four properties:
|
||||
|
||||
- `width`: The width of the connection.
|
||||
- `height`: The height of the connection.
|
||||
- `pathLeft`: The sub-path that describes the connection when drawn from left-to-right.
|
||||
- `pathRight`: The sub-path that describes the connection when drawn from right-to-left.
|
||||
|
||||
Define a new function called `makeRectangularPreviousConn()` and put it inside the `CustomConstantProvider` class definition. Note that `NOTCH_WIDTH` and `NOTCH_HEIGHT` have already been overridden in the `constructor()`, so they'll be reused:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @returns Rectangular notch for use with previous and next connections.
|
||||
*/
|
||||
makeRectangularPreviousConn() {
|
||||
const width = this.NOTCH_WIDTH;
|
||||
const height = this.NOTCH_HEIGHT;
|
||||
|
||||
/**
|
||||
* Since previous and next connections share the same shape you can define
|
||||
* a function to generate the path for both.
|
||||
*
|
||||
* @param dir Multiplier for the horizontal direction of the path (-1 or 1)
|
||||
* @returns SVGPath line for use with previous and next connections.
|
||||
*/
|
||||
function makeMainPath(dir) {
|
||||
return Blockly.utils.svgPaths.line(
|
||||
[
|
||||
Blockly.utils.svgPaths.point(0, height),
|
||||
Blockly.utils.svgPaths.point(dir * width, 0),
|
||||
Blockly.utils.svgPaths.point(0, -height),
|
||||
]);
|
||||
}
|
||||
const pathLeft = makeMainPath(1);
|
||||
const pathRight = makeMainPath(-1);
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
pathLeft: pathLeft,
|
||||
pathRight: pathRight,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Define an input/output connection shape
|
||||
|
||||
Just as previous/next connection shapes are drawn from left-to-right and right-to-left, input/output connection shapes are drawn from top-to-bottom and bottom-to-top.
|
||||
|
||||
Input and output connections are defined by the same object. The object has four properties:
|
||||
|
||||
- `width`: The width of the connection.
|
||||
- `height`: The height of the connection.
|
||||
- `pathUp`: The sub-path that describes the connection when drawn from top-to-bottom.
|
||||
- `pathDown`: The sub-path that describes the connection when drawn from bottom-to-top.
|
||||
|
||||
Define a new function called `makeRectangularInputConn()` and put it inside the `CustomConstantProvider` class definition. Note that `TAB_WIDTH` and `TAB_HEIGHT` have already been overridden in the `constructor()` so they'll be reused:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @returns Rectangular puzzle tab for use with input and output connections.
|
||||
*/
|
||||
makeRectangularInputConn() {
|
||||
const width = this.TAB_WIDTH;
|
||||
const height = this.TAB_HEIGHT;
|
||||
|
||||
/**
|
||||
* Since input and output connections share the same shape you can define
|
||||
* a function to generate the path for both.
|
||||
*
|
||||
* @param dir Multiplier for the vertical direction of the path (-1 or 1)
|
||||
* @returns SVGPath line for use with input and output connections.
|
||||
*/
|
||||
function makeMainPath(dir) {
|
||||
return Blockly.utils.svgPaths.line(
|
||||
[
|
||||
Blockly.utils.svgPaths.point(-width, 0),
|
||||
Blockly.utils.svgPaths.point(0, dir * height),
|
||||
Blockly.utils.svgPaths.point(width, 0),
|
||||
]);
|
||||
}
|
||||
const pathUp = makeMainPath(-1);
|
||||
const pathDown = makeMainPath(1);
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
pathUp: pathUp,
|
||||
pathDown: pathDown,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Override init()
|
||||
|
||||
Override the `init()` function in the `CustomConstantProvider` class definition and store the new shape objects as `RECT_PREV_NEXT` and `RECT_INPUT_OUTPUT`. Make sure to call the superclass `init()` function to store other objects that have not been overridden.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init() {
|
||||
// First, call init() in the base provider to store the default objects.
|
||||
super.init();
|
||||
|
||||
// Add calls to create shape objects for the new connection shapes.
|
||||
this.RECT_PREV_NEXT = this.makeRectangularPreviousConn();
|
||||
this.RECT_INPUT_OUTPUT = this.makeRectangularInputConn();
|
||||
}
|
||||
```
|
||||
|
||||
### Override shapeFor(connection)
|
||||
|
||||
Next, override the `shapeFor(connection)` function in the `CustomConstantProvider` class definition and return the new custom objects:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
shapeFor(connection) {
|
||||
switch (connection.type) {
|
||||
case Blockly.INPUT_VALUE:
|
||||
case Blockly.OUTPUT_VALUE:
|
||||
return this.RECT_INPUT_OUTPUT;
|
||||
case Blockly.PREVIOUS_STATEMENT:
|
||||
case Blockly.NEXT_STATEMENT:
|
||||
return this.RECT_PREV_NEXT;
|
||||
default:
|
||||
throw Error('Unknown connection type');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
Return to the browser, click on the `Loops` entry, and drag out a repeat block. The resulting block should have rectangular connections for all four connection types.
|
||||
|
||||
![[Screenshot of a custom renderer with notches, corners, and tabs with fundamentally different shapes than the defaults.]](../../../static/images/codelabs/custom-renderer/custom_notches.png)
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
pagination_prev: null
|
||||
slug: /codelabs/custom-renderer/codelab-overview
|
||||
description: Overview of the "Build custom renderers" codelab.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 1. Codelab overview
|
||||
|
||||
### What you'll learn
|
||||
|
||||
- How to define and register a custom renderer.
|
||||
- How to override renderer constants.
|
||||
- How to change the shape of connection notches.
|
||||
- How to set a connection's shape based on its type checks.
|
||||
|
||||
### What you'll build
|
||||
|
||||
This codelab builds and uses four renderers:
|
||||
|
||||
1. A minimal custom renderer that extends `Blockly.blockRendering.Renderer` but makes no modifications.
|
||||

|
||||
1. A custom renderer which sets new values for the rendering-related constants `NOTCH_WIDTH`, `NOTCH_HEIGHT`,`CORNER_RADIUS`, and `TAB_HEIGHT` found in `Blockly.blockRendering.ConstantProvider`.
|
||||

|
||||
1. A custom renderer which overrides the functions `Blockly.blockRendering.ConstantProvider.init()` and `Blockly.blockRendering.ConstantProvider.shapeFor(connection)` to define and return custom [SVG paths](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path).
|
||||

|
||||
1. A custom renderer which overrides the function `Blockly.blockRendering.ConstantProvider.shapeFor(connection)` to return different shapes for the input/output connections depending on whether the their type is a `Number`, `String`, or `Boolean`.
|
||||

|
||||
|
||||
### What you'll need
|
||||
|
||||
- Basic understanding of renderers and toolboxes in Blockly.
|
||||
- NPM installed ([instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)).
|
||||
- Comfort using the command line/terminal.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Blockly Sample App
|
||||
|
||||
## Purpose
|
||||
|
||||
This app illustrates how to use Blockly together with common programming tools like node/npm, webpack, typescript, eslint, and others. You can use it as the starting point for your own application and modify it as much as you'd like. It contains basic infrastructure for running, building, testing, etc. that you can use even if you don't understand how to configure the related tool yet. When your needs outgrow the functionality provided here, you can replace the provided configuration or tool with your own.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. [Install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) npm if you haven't before.
|
||||
2. Run [`npx @blockly/create-package app <application-name>`](https://www.npmjs.com/package/@blockly/create-package) to clone this application to your own machine.
|
||||
3. Run `npm install` to install the required dependencies.
|
||||
4. Run `npm run start` to run the development server and see the app in action.
|
||||
5. If you make any changes to the source code, just refresh the browser while the server is running to see them.
|
||||
|
||||
## Tooling
|
||||
|
||||
The application uses many of the same tools that the Blockly team uses to develop Blockly itself. Following is a brief overview, and you can read more about them on our [developer site](https://developers.google.com/blockly/guides/contribute/get-started/development_tools).
|
||||
|
||||
- Structure: The application is built as an npm package. You can use npm to manage the dependencies of the application.
|
||||
- Modules: ES6 modules to handle imports to/exports from other files.
|
||||
- Building/bundling: Webpack to build the source code and bundle it into one file for serving.
|
||||
- Development server: webpack-dev-server to run locally while in development.
|
||||
- Testing: Mocha to run unit tests.
|
||||
- Linting: Eslint to lint the code and ensure it conforms with a standard style.
|
||||
- UI Framework: Does not use a framework. For more complex applications, you may wish to integrate a UI framework like React or Angular.
|
||||
|
||||
You can disable, reconfigure, or replace any of these tools at any time, but they are preconfigured to get you started developing your Blockly application quickly.
|
||||
|
||||
## Structure
|
||||
|
||||
- `package.json` contains basic information about the app. This is where the scripts to run, build, etc. are listed.
|
||||
- `package-lock.json` is used by npm to manage dependencies
|
||||
- `webpack.config.js` is the configuration for webpack. This handles bundling the application and running our development server.
|
||||
- `src/` contains the rest of the source code.
|
||||
- `dist/` contains the packaged output (that you could host on a server, for example). This is ignored by git and will only appear after you run `npm run build` or `npm run start`.
|
||||
|
||||
### Source Code
|
||||
|
||||
- `index.html` contains the skeleton HTML for the page. This file is modified during the build to import the bundled source code output by webpack.
|
||||
- `index.js` is the entry point of the app. It configures Blockly and sets up the page to show the blocks, the generated code, and the output of running the code in JavaScript.
|
||||
- `serialization.js` has code to save and load the workspace using the browser's local storage. This is how your workspace is saved even after refreshing or leaving the page. You could replace this with code that saves the user's data to a cloud database instead.
|
||||
- `toolbox.js` contains the toolbox definition for the app. The current toolbox contains nearly every block that Blockly provides out of the box. You probably want to replace this definition with your own toolbox that uses your custom blocks and only includes the default blocks that are relevant to your application.
|
||||
- `blocks/text.js` has code for a custom text block, just as an example of creating your own blocks. You probably want to delete this block, and add your own blocks in this directory.
|
||||
- `generators/javascript.js` contains the JavaScript generator for the custom text block. You'll need to include block generators for any custom blocks you create, in whatever programming language(s) your application will use.
|
||||
|
||||
## Serving
|
||||
|
||||
To run your app locally, run `npm run start` to run the development server. This mode generates source maps and ingests the source maps created by Blockly, so that you can debug using unminified code.
|
||||
|
||||
To deploy your app so that others can use it, run `npm run build` to run a production build. This will bundle your code and minify it to reduce its size. You can then host the contents of the `dist` directory on a web server of your choosing. If you're just getting started, try using [GitHub Pages](https://pages.github.com/).
|
||||
+8280
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "renderer-sample-app",
|
||||
"version": "1.0.0",
|
||||
"description": "A sample app using Blockly and custom renderers",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack --mode production",
|
||||
"start": "webpack serve --open --mode development"
|
||||
},
|
||||
"keywords": [
|
||||
"blockly"
|
||||
],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"css-loader": "^6.7.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"webpack": "^5.93.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"blockly": "^12.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
// Create a custom block called 'add_text' that adds
|
||||
// text to the output div on the sample app.
|
||||
// This is just an example and you should replace this with your
|
||||
// own custom blocks.
|
||||
const addText = {
|
||||
type: 'add_text',
|
||||
message0: 'Add text %1 with color %2',
|
||||
args0: [
|
||||
{
|
||||
type: 'input_value',
|
||||
name: 'TEXT',
|
||||
check: 'String',
|
||||
},
|
||||
{
|
||||
type: 'input_value',
|
||||
name: 'COLOR',
|
||||
check: 'Colour',
|
||||
},
|
||||
],
|
||||
previousStatement: null,
|
||||
nextStatement: null,
|
||||
colour: 160,
|
||||
tooltip: '',
|
||||
helpUrl: '',
|
||||
};
|
||||
|
||||
// Create the block definitions for the JSON-only blocks.
|
||||
// This does not register their definitions with Blockly.
|
||||
// This file has no side effects!
|
||||
export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
|
||||
addText,
|
||||
]);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Order } from 'blockly/javascript';
|
||||
|
||||
// Export all the code generators for our custom blocks,
|
||||
// but don't register them with Blockly yet.
|
||||
// This file has no side effects!
|
||||
export const forBlock = Object.create(null);
|
||||
|
||||
forBlock['add_text'] = function (block, generator) {
|
||||
const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''";
|
||||
const color =
|
||||
generator.valueToCode(block, 'COLOR', Order.ATOMIC) || "'#ffffff'";
|
||||
|
||||
const addText = generator.provideFunction_(
|
||||
'addText',
|
||||
`function ${generator.FUNCTION_NAME_PLACEHOLDER_}(text, color) {
|
||||
|
||||
// Add text to the output area.
|
||||
const outputDiv = document.getElementById('output');
|
||||
const textEl = document.createElement('p');
|
||||
textEl.innerText = text;
|
||||
textEl.style.color = color;
|
||||
outputDiv.appendChild(textEl);
|
||||
}`,
|
||||
);
|
||||
// Generate the function call for this block.
|
||||
const code = `${addText}(${text}, ${color});\n`;
|
||||
return code;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
body {
|
||||
margin: 0;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#pageContainer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
flex-basis: 100%;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
#outputPane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 400px;
|
||||
flex: 0 0 400px;
|
||||
overflow: auto;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
#generatedCode {
|
||||
height: 50%;
|
||||
background-color: rgb(247, 240, 228);
|
||||
}
|
||||
|
||||
#output {
|
||||
height: 50%;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Blockly Sample App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="pageContainer">
|
||||
<div id="outputPane">
|
||||
<pre id="generatedCode"><code></code></pre>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<div id="blocklyDiv"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
import { blocks } from './blocks/text';
|
||||
import { forBlock } from './generators/javascript';
|
||||
import { javascriptGenerator } from 'blockly/javascript';
|
||||
import { save, load } from './serialization';
|
||||
import { toolbox } from './toolbox';
|
||||
import './renderers/custom';
|
||||
import './index.css';
|
||||
|
||||
// Register the blocks and generator with Blockly
|
||||
Blockly.common.defineBlocks(blocks);
|
||||
Object.assign(javascriptGenerator.forBlock, forBlock);
|
||||
|
||||
// Set up UI elements and inject Blockly
|
||||
const codeDiv = document.getElementById('generatedCode').firstChild;
|
||||
const outputDiv = document.getElementById('output');
|
||||
const blocklyDiv = document.getElementById('blocklyDiv');
|
||||
const ws = Blockly.inject(blocklyDiv, {
|
||||
renderer: 'custom_renderer',
|
||||
toolbox,
|
||||
});
|
||||
|
||||
// This function resets the code and output divs, shows the
|
||||
// generated code from the workspace, and evals the code.
|
||||
// In a real application, you probably shouldn't use `eval`.
|
||||
const runCode = () => {
|
||||
const code = javascriptGenerator.workspaceToCode(ws);
|
||||
codeDiv.innerText = code;
|
||||
|
||||
outputDiv.textContent = '';
|
||||
|
||||
eval(code);
|
||||
};
|
||||
|
||||
// Load the initial state from storage and run the code.
|
||||
load(ws);
|
||||
runCode();
|
||||
|
||||
// Every time the workspace changes state, save the changes to storage.
|
||||
ws.addChangeListener((e) => {
|
||||
// UI events are things like scrolling, zooming, etc.
|
||||
// No need to save after one of these.
|
||||
if (e.isUiEvent) return;
|
||||
save(ws);
|
||||
});
|
||||
|
||||
// Whenever the workspace changes meaningfully, run the code again.
|
||||
ws.addChangeListener((e) => {
|
||||
// Don't run the code when the workspace finishes loading; we're
|
||||
// already running it once when the application starts.
|
||||
// Don't run the code during drags; we might have invalid state.
|
||||
if (
|
||||
e.isUiEvent ||
|
||||
e.type == Blockly.Events.FINISHED_LOADING ||
|
||||
ws.isDragging()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
runCode();
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview The full custom renderer built during custom renderer codelab.
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
/**
|
||||
* The ConstantProvider holds rendering-related constants such as colour
|
||||
* and size information.
|
||||
*/
|
||||
class CustomConstantProvider extends Blockly.blockRendering.ConstantProvider {
|
||||
/** @override */
|
||||
constructor() {
|
||||
// Set up all of the constants from the base provider.
|
||||
super();
|
||||
|
||||
// Override a few properties.
|
||||
/**
|
||||
* The width of the notch used for previous and next connections.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.NOTCH_WIDTH = 20;
|
||||
|
||||
/**
|
||||
* The height of the notch used for previous and next connections.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.NOTCH_HEIGHT = 10;
|
||||
|
||||
/**
|
||||
* Rounded corner radius.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.CORNER_RADIUS = 2;
|
||||
|
||||
/**
|
||||
* The height of the puzzle tab used for input and output connections.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.TAB_HEIGHT = 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init() {
|
||||
// First, call init() in the base provider to store the default objects.
|
||||
super.init();
|
||||
|
||||
// Add calls to create shape objects for the new connection shapes.
|
||||
this.RECT_PREV_NEXT = this.makeRectangularPreviousConn();
|
||||
this.RECT_INPUT_OUTPUT = this.makeRectangularInputConn();
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
shapeFor(connection) {
|
||||
const checks = connection.getCheck();
|
||||
switch (connection.type) {
|
||||
case Blockly.INPUT_VALUE:
|
||||
case Blockly.OUTPUT_VALUE:
|
||||
if (checks && checks.includes('Number')) {
|
||||
return this.RECT_INPUT_OUTPUT;
|
||||
}
|
||||
if (checks && checks.includes('String')) {
|
||||
return this.RECT_INPUT_OUTPUT;
|
||||
}
|
||||
return this.PUZZLE_TAB;
|
||||
case Blockly.PREVIOUS_STATEMENT:
|
||||
case Blockly.NEXT_STATEMENT:
|
||||
return this.NOTCH;
|
||||
default:
|
||||
throw Error('Unknown connection type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object} Rectangular notch for use with previous and next connections.
|
||||
*/
|
||||
makeRectangularPreviousConn() {
|
||||
const width = this.NOTCH_WIDTH;
|
||||
const height = this.NOTCH_HEIGHT;
|
||||
|
||||
/**
|
||||
* Since previous and next connections share the same shape you can define
|
||||
* a function to generate the path for both.
|
||||
* @param {number} dir Multiplier for the horizontal direction of the path (-1 or 1)
|
||||
* @returns {Object} SVGPath line for use with previous and next connections.
|
||||
*/
|
||||
function makeMainPath(dir) {
|
||||
return Blockly.utils.svgPaths.line([
|
||||
Blockly.utils.svgPaths.point(0, height),
|
||||
Blockly.utils.svgPaths.point(dir * width, 0),
|
||||
Blockly.utils.svgPaths.point(0, -height),
|
||||
]);
|
||||
}
|
||||
const pathLeft = makeMainPath(1);
|
||||
const pathRight = makeMainPath(-1);
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
pathLeft: pathLeft,
|
||||
pathRight: pathRight,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object} Rectangular puzzle tab for use with input and output connections.
|
||||
*/
|
||||
makeRectangularInputConn() {
|
||||
const width = this.TAB_WIDTH;
|
||||
const height = this.TAB_HEIGHT;
|
||||
|
||||
/**
|
||||
* Since input and output connections share the same shape you can define
|
||||
* a function to generate the path for both.
|
||||
* @param {number} dir Multiplier for the vertical direction of the path (-1 or 1)
|
||||
* @returns {Object} SVGPath line for use with input and output connections.
|
||||
*/
|
||||
function makeMainPath(dir) {
|
||||
return Blockly.utils.svgPaths.line([
|
||||
Blockly.utils.svgPaths.point(-width, 0),
|
||||
Blockly.utils.svgPaths.point(0, dir * height),
|
||||
Blockly.utils.svgPaths.point(width, 0),
|
||||
]);
|
||||
}
|
||||
const pathUp = makeMainPath(-1);
|
||||
const pathDown = makeMainPath(1);
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
pathUp: pathUp,
|
||||
pathDown: pathDown,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** The CustomRenderer class incorporates our custom ConstantProvider. */
|
||||
export class CustomRenderer extends Blockly.blockRendering.Renderer {
|
||||
/** @override */
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
makeConstants_() {
|
||||
return new CustomConstantProvider();
|
||||
}
|
||||
}
|
||||
|
||||
Blockly.blockRendering.register('custom_renderer', CustomRenderer);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
const storageKey = 'mainWorkspace';
|
||||
|
||||
/**
|
||||
* Saves the state of the workspace to browser's local storage.
|
||||
* @param {Blockly.Workspace} workspace Blockly workspace to save.
|
||||
*/
|
||||
export const save = function (workspace) {
|
||||
const data = Blockly.serialization.workspaces.save(workspace);
|
||||
window.localStorage?.setItem(storageKey, JSON.stringify(data));
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads saved state from local storage into the given workspace.
|
||||
* @param {Blockly.Workspace} workspace Blockly workspace to load into.
|
||||
*/
|
||||
export const load = function (workspace) {
|
||||
const data = window.localStorage?.getItem(storageKey);
|
||||
if (!data) return;
|
||||
|
||||
// Don't emit events during loading.
|
||||
Blockly.Events.disable();
|
||||
Blockly.serialization.workspaces.load(JSON.parse(data), workspace, false);
|
||||
Blockly.Events.enable();
|
||||
};
|
||||
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*
|
||||
This toolbox contains nearly every single built-in block that Blockly offers,
|
||||
in addition to the custom block 'add_text' this sample app adds.
|
||||
You probably don't need every single block, and should consider either rewriting
|
||||
your toolbox from scratch, or carefully choosing whether you need each block
|
||||
listed here.
|
||||
*/
|
||||
|
||||
export const toolbox = {
|
||||
kind: 'categoryToolbox',
|
||||
contents: [
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Logic',
|
||||
categorystyle: 'logic_category',
|
||||
contents: [
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'controls_if',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_compare',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_operation',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_negate',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_boolean',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_null',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'logic_ternary',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Loops',
|
||||
categorystyle: 'loop_category',
|
||||
contents: [
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'controls_repeat_ext',
|
||||
inputs: {
|
||||
TIMES: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'controls_whileUntil',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'controls_for',
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
BY: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'controls_forEach',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'controls_flow_statements',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Math',
|
||||
categorystyle: 'math_category',
|
||||
contents: [
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_arithmetic',
|
||||
inputs: {
|
||||
A: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
B: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_single',
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_trig',
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_constant',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_number_property',
|
||||
inputs: {
|
||||
NUMBER_TO_CHECK: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_round',
|
||||
fields: {
|
||||
OP: 'ROUND',
|
||||
},
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 3.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_on_list',
|
||||
fields: {
|
||||
OP: 'SUM',
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_modulo',
|
||||
inputs: {
|
||||
DIVIDEND: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 64,
|
||||
},
|
||||
},
|
||||
},
|
||||
DIVISOR: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_constrain',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
LOW: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
HIGH: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_random_int',
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_random_float',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'math_atan2',
|
||||
inputs: {
|
||||
X: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
Y: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Text',
|
||||
categorystyle: 'text_category',
|
||||
contents: [
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_join',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_append',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_length',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_isEmpty',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_indexOf',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
},
|
||||
},
|
||||
FIND: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_charAt',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_getSubstring',
|
||||
inputs: {
|
||||
STRING: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_changeCase',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_trim',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: 'abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_count',
|
||||
inputs: {
|
||||
SUB: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_replace',
|
||||
inputs: {
|
||||
FROM: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
TO: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'text_reverse',
|
||||
inputs: {
|
||||
TEXT: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Lists',
|
||||
categorystyle: 'list_category',
|
||||
contents: [
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_create_with',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_create_with',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_repeat',
|
||||
inputs: {
|
||||
NUM: {
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fields: {
|
||||
NUM: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_length',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_isEmpty',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_indexOf',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_getIndex',
|
||||
inputs: {
|
||||
VALUE: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_setIndex',
|
||||
inputs: {
|
||||
LIST: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_getSublist',
|
||||
inputs: {
|
||||
LIST: {
|
||||
block: {
|
||||
type: 'variables_get',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_split',
|
||||
inputs: {
|
||||
DELIM: {
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
TEXT: ',',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_sort',
|
||||
},
|
||||
{
|
||||
kind: 'block',
|
||||
type: 'lists_reverse',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Variables',
|
||||
categorystyle: 'variable_category',
|
||||
custom: 'VARIABLE',
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Functions',
|
||||
categorystyle: 'procedure_category',
|
||||
custom: 'PROCEDURE',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
// Base config that applies to either development or production mode.
|
||||
const config = {
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
// Compile the source files into a bundle.
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
clean: true,
|
||||
},
|
||||
// Enable webpack-dev-server to get hot refresh of the app.
|
||||
devServer: {
|
||||
static: './build',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// Load CSS files. They can be imported into JS files.
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// Generate the HTML index page based on our template.
|
||||
// This will output the same index page with the bundle we
|
||||
// created above added in a script tag.
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/index.html',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
if (argv.mode === 'development') {
|
||||
// Set the output path to the `build` directory
|
||||
// so we don't clobber production builds.
|
||||
config.output.path = path.resolve(__dirname, 'build');
|
||||
|
||||
// Generate source maps for our code for easier debugging.
|
||||
// Not suitable for production builds. If you want source maps in
|
||||
// production, choose a different one from https://webpack.js.org/configuration/devtool
|
||||
config.devtool = 'eval-cheap-module-source-map';
|
||||
|
||||
// Include the source maps for Blockly for easier debugging Blockly code.
|
||||
config.module.rules.push({
|
||||
test: /(blockly[/\\].*\.js)$/,
|
||||
use: [require.resolve('source-map-loader')],
|
||||
enforce: 'pre',
|
||||
});
|
||||
|
||||
// Ignore spurious warnings from source-map-loader
|
||||
// It can't find source maps for some Closure modules and that is expected
|
||||
config.ignoreWarnings = [/Failed to parse source map.*blockly/];
|
||||
}
|
||||
return config;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
slug: /codelabs/custom-renderer/define-and-register-a-custom-renderer
|
||||
description: How to register a custom renderer.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 4. Define and register a custom renderer
|
||||
|
||||
A **Renderer** is the interface between custom rendering code and the rest of Blockly. Blockly provides a base renderer with all required fields already set to usable values.
|
||||
|
||||
To start, create a new directory at `src/renderers` and add a file inside named `custom.js`.
|
||||
|
||||
At the top of the file, import `blockly/core`:
|
||||
|
||||
```js
|
||||
import * as Blockly from 'blockly/core';
|
||||
```
|
||||
|
||||
Then define a new custom renderer and have it extend the base renderer:
|
||||
|
||||
```js
|
||||
class CustomRenderer extends Blockly.blockRendering.Renderer {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After defining the renderer, register it with Blockly and give it the name `custom_renderer`:
|
||||
|
||||
```js
|
||||
Blockly.blockRendering.register('custom_renderer', CustomRenderer);
|
||||
```
|
||||
|
||||
To use the custom renderer, import the new file at the top of `src/index.js`:
|
||||
|
||||
```js
|
||||
import './renderers/javascript';
|
||||
```
|
||||
|
||||
Now, add the the `renderer` property into the configuration struct passed to `Blockly.inject` so that it now looks like this:
|
||||
|
||||
```js
|
||||
const ws = Blockly.inject(blocklyDiv, {
|
||||
renderer: 'custom_renderer',
|
||||
toolbox,
|
||||
});
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
If the server is already running, refresh the page to see the new changes. Otherwise, run `npm start` to start the server. Once the server is running, click on the `Loops` entry in the browser and drag out a repeat block. The resulting block will use the same values already defined in the base `Blockly.blockRendering.Renderer`.
|
||||
|
||||

|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
slug: /codelabs/custom-renderer/observe-the-built-in-renderers
|
||||
description: Introduction to the built-in renderers.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 3. Observe the built-in renderers
|
||||
|
||||
First, visit [the advanced playground](https://blockly-demo.appspot.com/static/tests/playgrounds/advanced_playground.html) to observe what the built-in renderers look like.
|
||||
|
||||
Click on the "Loops" entry and drag out a repeat block. Now, change the selection in the "renderer" drop down to observe the look of each built-in renderer. By default, the renderer named "Geras" is used.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
slug: /codelabs/custom-renderer/override-constants
|
||||
description: How to override the constants in a renderer.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 5. Override constants
|
||||
|
||||
A **ConstantsProvider** holds all rendering-related constants. This includes sizing information and colours. Blockly provides a base **ConstantsProvider** with all required fields set to default values.
|
||||
|
||||
The **ConstantsProvider** `constructor()` sets all static properties, such as `NOTCH_WIDTH` and `NOTCH_HEIGHT`. For a full list of properties, see [constants.ts](https://github.com/RaspberryPiFoundation/blockly/blob/main/packages/blockly/core/renderers/common/constants.ts).
|
||||
|
||||
Only override the necessary subset of the constants, rather than all of them. To do so:
|
||||
|
||||
- Define a constants provider that extends the base `ConstantProvider`.
|
||||
- Call the superclass `super()` in the `constructor()`.
|
||||
- Set individual properties.
|
||||
|
||||
Add this above the `CustomRenderer` definition in `src/renderers/custom.js`:
|
||||
|
||||
```js
|
||||
class CustomConstantProvider extends Blockly.blockRendering.ConstantProvider {
|
||||
constructor() {
|
||||
// Set up all of the constants from the base provider.
|
||||
super();
|
||||
|
||||
// Override a few properties.
|
||||
/**
|
||||
* The width of the notch used for previous and next connections.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.NOTCH_WIDTH = 20;
|
||||
|
||||
/**
|
||||
* The height of the notch used for previous and next connections.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.NOTCH_HEIGHT = 10;
|
||||
|
||||
/**
|
||||
* Rounded corner radius.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.CORNER_RADIUS = 2;
|
||||
|
||||
/**
|
||||
* The height of the puzzle tab used for input and output connections.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
this.TAB_HEIGHT = 8;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To use the new **CustomConstantProvider**, override `makeConstants_()` inside the `CustomRenderer` class. Below the `constructor()`, add:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
makeConstants_() {
|
||||
return new CustomConstantProvider();
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
Return to the browser, click on the `Loops` entry, and drag out a repeat block. The resulting block should have triangular previous and next connections, and skinny input and output connections. Note that the general shapes of the connections have not changed--only parameters such as width and height.
|
||||
|
||||

|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
slug: /codelabs/custom-renderer/setup
|
||||
description: Setting up the "Build custom renderers" codelab.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 2. Setup
|
||||
|
||||
This codelab will add code to the Blockly sample app to create and use a new custom renderer.
|
||||
|
||||
### The application
|
||||
|
||||
Use the Use the [`npx @blockly/create-package`](https://www.npmjs.com/package/@blockly/create-package) command to create a standalone application that contains a sample setup of Blockly, including custom blocks and a display of the generated code and output.
|
||||
|
||||
1. Run `npx @blockly/create-package app custom-renderer-codelab`. This will create a blockly application in the folder `custom-renderer-codelab`.
|
||||
1. `cd` into the new directory: `cd custom-renderer-codelab`.
|
||||
1. Run `npm start` to start the server and run the sample application.
|
||||
1. The sample app will automatically run in the browser window that opens.
|
||||
|
||||
The initial application uses the default renderer and contains no code or definitions for a custom renderer.
|
||||
|
||||
The complete code used in this codelab can be viewed in the `blockly` repository under [docs/docs/codelabs/custom-renderer](https://github.com/RaspberryPiFoundation/blockly/docs/docs/codelabs/custom-renderer/complete-code).
|
||||
|
||||
Before setting up the rest of the application, change the storage key used for this codelab application. This will ensure that the workspace is saved in its own storage, separate from the regular sample app, so that it doesn't interfere with other demos. In `serialization.js`, change the value of `storageKey` to some unique string. `customRenderersWorkspace` will work:
|
||||
|
||||
```js
|
||||
// Use a unique storage key for this codelab
|
||||
const storageKey = 'customRenderersWorkspace';
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
pagination_next: null
|
||||
slug: /codelabs/custom-renderer/summary
|
||||
description: Summary of the "Build custom renderers" codelab.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 9. Summary
|
||||
|
||||
Custom renderers are a powerful way to change the look and feel of Blockly. In this codelab you learned:
|
||||
|
||||
- How to declare and register a custom renderer by extending `Blockly.blockRendering.Renderer`.
|
||||
- How to override renderer constants such as `NOTCH_HEIGHT` in `Blockly.blockRendering.ConstantProvider`.
|
||||
- How to modify connection shapes by creating custom [SVG paths](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path), storing them in `init()`, and finally returning them in `shapeFor(connection)`.
|
||||
- How to update the mapping from connection to connection shape by adding logic in `shapeFor(connection)`.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
slug: /codelabs/custom-renderer/typed-connection-shapes
|
||||
description: How to set connection shapes based on connection types.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 8. Typed connection shapes
|
||||
|
||||
This step will create a renderer that sets connection shapes at runtime based on a connection's type checks. It will use the default connection shapes and the shapes defined in the previous steps.
|
||||
|
||||
### Override `shapeFor(connection)`
|
||||
|
||||
Override the `shapeFor(connection)` function in the `CustomConstantProvider` class definition to return a different connection shape based on the `checks` returned from `connection.getCheck()`. Note the previous definition of `shapeFor(connection)` created in previous steps will need to be deleted.
|
||||
|
||||
The new definition of `shapeFor(connection)` will:
|
||||
|
||||
- Return a rectangular tab for inputs and outputs that accept `Number`s and `String`s.
|
||||
- Return the default puzzle tab for all other inputs and outputs.
|
||||
- Return the normal notch for all previous and next connections.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
shapeFor(connection) {
|
||||
var checks = connection.getCheck();
|
||||
switch (connection.type) {
|
||||
case Blockly.INPUT_VALUE:
|
||||
case Blockly.OUTPUT_VALUE:
|
||||
if (checks && checks.includes('Number')) {
|
||||
return this.RECT_INPUT_OUTPUT;
|
||||
}
|
||||
if (checks && checks.includes('String')) {
|
||||
return this.RECT_INPUT_OUTPUT;
|
||||
}
|
||||
return this.PUZZLE_TAB;
|
||||
case Blockly.PREVIOUS_STATEMENT:
|
||||
case Blockly.NEXT_STATEMENT:
|
||||
return this.NOTCH;
|
||||
default:
|
||||
throw Error('Unknown connection type');
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
Take these steps to fully test this change in the browser:
|
||||
|
||||
1. Click on the `Loops` entry and drag out a repeat block.
|
||||
1. Click on the `Logic` entry and drag the conditional `if` block into the repeat block that was placed on the workspace in the previous step.
|
||||
|
||||
There should be an entry similar to the screenshot below, in which the `Number` inputs and outputs are rectangular, but the boolean input on the `if` block is a puzzle tab.
|
||||

|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
slug: /codelabs/custom-renderer/understand-connection-shapes
|
||||
description: Introduction to connection shapes.
|
||||
---
|
||||
|
||||
# Build custom renderers
|
||||
|
||||
## 6. Understand connection shapes
|
||||
|
||||
A common use case of a custom renderer is changing the shape of connections. This requires a more detailed understanding of how a block is drawn and how SVG paths are defined.
|
||||
|
||||
### The block outline
|
||||
|
||||
The outline of the block is a single [SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path). The outline is built out of many sub-paths (e.g. the path for a previous connection; the path for the top of the block; and the path for an input connection).
|
||||
|
||||
Each sub-path is a string of [path commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands) that describe the appropriate shape. These commands must use relative (rather than absolute) coordinates.
|
||||
|
||||
SVG path commands can be written as strings, but Blockly provides a set of [utility functions](/reference/blockly.utils.svgpaths) to make writing and reading paths easier.
|
||||
|
||||
### `init()`
|
||||
|
||||
A connection's shape is stored as an object with information about its width, height, and sub-path. These objects are created in the `ConstantProvider`s `init()` function. Here is the start of the default implementation. The complete definition can be found inside [`constants.ts`](https://github.com/RaspberryPiFoundation/blockly/blob/main/packages/blockly/core/renderers/common/constants.ts).
|
||||
|
||||
```js
|
||||
/**
|
||||
* Initialize shape objects based on the constants set in the constructor.
|
||||
*/
|
||||
init() {
|
||||
/**
|
||||
* An object containing sizing and path information about collapsed block
|
||||
* indicators.
|
||||
*/
|
||||
this.JAGGED_TEETH = this.makeJaggedTeeth();
|
||||
|
||||
/** An object containing sizing and path information about notches. */
|
||||
this.NOTCH = this.makeNotch();
|
||||
|
||||
// Additional code has been removed for brevity.
|
||||
}
|
||||
```
|
||||
|
||||
**Properties that are primitives should be set in the `constructor()`, while objects should be set in `init()`**. This separation allows a subclass to override a constant such as `NOTCH_WIDTH` and see the change reflected in objects that depend on the constant.
|
||||
|
||||
### `shapeFor(connection)`
|
||||
|
||||
The `shapeFor(connection)` function maps from connection to connection shape. Here is the default implementation, which can be found inside [`constants.ts`](https://github.com/RaspberryPiFoundation/blockly/blob/main/packages/blockly/core/renderers/common/constants.ts). It returns a puzzle tab for input/output connections and a notch for previous/next connections:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Get an object with connection shape and sizing information based on the
|
||||
* type of the connection.
|
||||
*
|
||||
* @param connection The connection to find a shape object for
|
||||
* @returns The shape object for the connection.
|
||||
*/
|
||||
shapeFor(connection: RenderedConnection): Shape {
|
||||
switch (connection.type) {
|
||||
case ConnectionType.INPUT_VALUE:
|
||||
case ConnectionType.OUTPUT_VALUE:
|
||||
return this.PUZZLE_TAB;
|
||||
case ConnectionType.PREVIOUS_STATEMENT:
|
||||
case ConnectionType.NEXT_STATEMENT:
|
||||
return this.NOTCH;
|
||||
default:
|
||||
throw Error('Unknown connection type');
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
slug: /codelabs/custom-toolbox/add-an-icon-to-your-category
|
||||
description: How to add an icon to your category.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing a Blockly toolbox
|
||||
|
||||
## 5. Add an icon to your category
|
||||
|
||||
We are going to add an icon to our "Logic" category by adding an icon library to
|
||||
our `index.html` file, and setting the appropriate CSS class on our category definition.
|
||||
|
||||
To start, we are going to grab an icon library and add it to `index.html`:
|
||||
|
||||
```
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
```
|
||||
|
||||
We are going to add a cog icon from this library to the "Logic" category.
|
||||
To do this, we will add the appropriate CSS classes to our category definition.
|
||||
|
||||
In `index.html` scroll down to your toolbox definition and change the below line:
|
||||
|
||||
```xml
|
||||
<category name="Logic" categorystyle="logic_category">
|
||||
```
|
||||
|
||||
to be:
|
||||
|
||||
```xml
|
||||
<category css-icon="customIcon fa fa-cog" name="Logic" categorystyle="logic_category">
|
||||
```
|
||||
|
||||
All the classes used to create a category can be set similar to how we set the
|
||||
icon class above. See the [Blockly toolbox documentation](/guides/configure/web/toolboxes/appearance#category-css) for more information.
|
||||
|
||||
### Add some CSS
|
||||
|
||||
If you open `index.html` you will notice that the gear icon is positioned
|
||||
incorrectly and is a bit difficult to see. We will use the `customIcon` class to
|
||||
change the color of the icon and use the `blocklyTreeRowContentContainer` class
|
||||
to position the icon above the text.
|
||||
|
||||
In your `toolbox_style.css` file add:
|
||||
|
||||
```css
|
||||
/* Changes color of the icon to white. */
|
||||
.customIcon {
|
||||
color: white;
|
||||
}
|
||||
/* Stacks the icon on top of the label. */
|
||||
.blocklyTreeRowContentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.blocklyToolboxCategory {
|
||||
height: initial;
|
||||
}
|
||||
```
|
||||
|
||||
### Update setSelected
|
||||
|
||||
If you open `index.html` and click on the "Logic" category you will notice
|
||||
that the white icon now blends into the white background.
|
||||
|
||||
In order to fix this, we are going to update our `setSelected` method to set the
|
||||
color of the icon to the category color when the category has been selected.
|
||||
|
||||
Inside `custom_category.js` add the below line to `setSelected` if the category
|
||||
has been selected:
|
||||
|
||||
```js
|
||||
this.iconDom_.style.color = this.colour_;
|
||||
```
|
||||
|
||||
Add the below line to `setSelected` if the category has not been selected:
|
||||
|
||||
```js
|
||||
this.iconDom_.style.color = 'white';
|
||||
```
|
||||
|
||||
Your `setSelected` method should look similar to below:
|
||||
|
||||
```js
|
||||
/** @override */
|
||||
setSelected(isSelected){
|
||||
// We do not store the label span on the category, so use getElementsByClassName.
|
||||
var labelDom = this.rowDiv_.getElementsByClassName('blocklyToolboxCategoryLabel')[0];
|
||||
if (isSelected) {
|
||||
// Change the background color of the div to white.
|
||||
this.rowDiv_.style.backgroundColor = 'white';
|
||||
// Set the colour of the text to the colour of the category.
|
||||
labelDom.style.color = this.colour_;
|
||||
this.iconDom_.style.color = this.colour_;
|
||||
} else {
|
||||
// Set the background back to the original colour.
|
||||
this.rowDiv_.style.backgroundColor = this.colour_;
|
||||
// Set the text back to white.
|
||||
labelDom.style.color = 'white';
|
||||
this.iconDom_.style.color = 'white';
|
||||
}
|
||||
// This is used for accessibility purposes.
|
||||
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
|
||||
Blockly.utils.aria.State.SELECTED, isSelected);
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
If you open your `index.html` file, you should see a white gear above your "Logic"
|
||||
label, and it should change to blue when the category has been selected.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/category_gear.png"
|
||||
alt='A white gear above the word "Logic" on a blue background.'
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/category_gear_selected.png"
|
||||
alt='A blue gear above the word "Logic" on a white background.'
|
||||
className="codelabImage"
|
||||
/>
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
slug: /codelabs/custom-toolbox/adding-a-custom-toolbox-item
|
||||
description: How to add a custom item to a toolbox.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing a Blockly toolbox
|
||||
|
||||
## 7. Adding a custom toolbox item
|
||||
|
||||
In the previous sections we modified the toolbox by extending the base category class.
|
||||
In this section we will make a completely new toolbox item and add it to our toolbox.
|
||||
|
||||
For this example, we are going to create a toolbox label.
|
||||
|
||||
### Setup
|
||||
|
||||
In the same directory as `index.html` create a new file named `toolbox_label.js`.
|
||||
|
||||
Include this file in `index.html`:
|
||||
|
||||
```html
|
||||
<script src="toolbox_label.js"></script>
|
||||
```
|
||||
|
||||
Create a class in `toolbox_label.js` that extends `Blockly.ToolboxItem`
|
||||
and register it.
|
||||
|
||||
```js
|
||||
class ToolboxLabel extends Blockly.ToolboxItem {
|
||||
constructor(toolboxItemDef, parentToolbox) {
|
||||
super(toolboxItemDef, parentToolbox);
|
||||
}
|
||||
}
|
||||
|
||||
Blockly.registry.register(
|
||||
Blockly.registry.Type.TOOLBOX_ITEM,
|
||||
'toolboxlabel',
|
||||
ToolboxLabel,
|
||||
);
|
||||
```
|
||||
|
||||
By registering this toolbox item with the name "toolboxlabel" we can now use this
|
||||
name in our toolbox definition to add our custom item to the toolbox.
|
||||
|
||||
Navigate to `index.html`, and scroll down to the toolbox definition. Add a
|
||||
`<toolboxlabel>` element as the first item in your toolbox definition:
|
||||
|
||||
```xml
|
||||
<toolboxlabel></toolboxlabel>
|
||||
```
|
||||
|
||||
Your toolbox definition should now look something like:
|
||||
|
||||
```xml
|
||||
<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-categories" style="display: none">
|
||||
<toolboxlabel></toolboxlabel>
|
||||
<category css-icon="customIcon fa fa-cog" name="Logic" categorystyle="logic_category">
|
||||
...
|
||||
</xml>
|
||||
```
|
||||
|
||||
### Initialize the toolbox item
|
||||
|
||||
In order to create a toolbox item we must implement one of the toolbox item interfaces.
|
||||
|
||||
For this example, we will be implementing the basic `IToolboxItem` interface.
|
||||
There are three different types of toolbox item interfaces:
|
||||
`IToolboxItem`, `ISelectableToobloxItem` and `ICollapsibleToolboxItem`.
|
||||
Since we do not need our label to be selectable or collapsible, we can implement
|
||||
the basic `IToolboxItem` interface.
|
||||
|
||||
First, we are going to add an init method that will create the dom for our toolbox label:
|
||||
|
||||
```js
|
||||
/** @override */
|
||||
init() {
|
||||
// Create the label.
|
||||
this.label = document.createElement('label');
|
||||
// Set the name.
|
||||
this.label.textContent = 'Label';
|
||||
}
|
||||
```
|
||||
|
||||
Next, we are going to return this element:
|
||||
|
||||
```js
|
||||
/** @override */
|
||||
getDiv() {
|
||||
return this.label;
|
||||
}
|
||||
```
|
||||
|
||||
If you open the `index.html` file you should see a label above your first category.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/toolbox_label.png"
|
||||
alt="The toolbox with a label at the top."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
### Add attributes to the toolbox definition
|
||||
|
||||
The above code is rather limiting since it only allows us to create a toolbox
|
||||
label with the text "Label".
|
||||
To make it possible to create different labels with different text and colour we
|
||||
are going to add `name` and `colour` attributes to our toolbox definition.
|
||||
|
||||
Open `index.html` and navigate to the toolbox definition. Change your
|
||||
`toolboxlabel` element to look like the below line:
|
||||
|
||||
```xml
|
||||
<toolboxlabel name="Custom Toolbox" colour="darkslategrey"></toolboxlabel>
|
||||
```
|
||||
|
||||
These values will get passed in to our `ToolboxLabel` class through the `toolboxItemDef`.
|
||||
Navigate to `toolbox_label.js` and add the following lines to your `init` method:
|
||||
|
||||
```js
|
||||
// Set the name.
|
||||
this.label.textContent = this.toolboxItemDef_['name'];
|
||||
// Set the color.
|
||||
this.label.style.color = this.toolboxItemDef_['colour'];
|
||||
```
|
||||
|
||||
Remove the following line from your init method:
|
||||
|
||||
```js
|
||||
this.label.textContent = 'Label';
|
||||
```
|
||||
|
||||
All attributes on our toolbox definition get added to the `toolboxItemDef_`.
|
||||
`this.toolboxItemDef_` is set in the `Blockly.ToolboxItem` constructor.
|
||||
|
||||
Open your `index.html` in a browser to see the updated label.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/custom_label.png"
|
||||
alt='The toolbox with a label that now says "Custom Toolbox".'
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
### Add some CSS
|
||||
|
||||
Similar to how we added `colour` and `name` above, we are going to add a custom
|
||||
class to our label.
|
||||
|
||||
Navigate to your toolbox definition in `index.html` and modify it to look
|
||||
like the below line.
|
||||
|
||||
```xml
|
||||
<toolboxlabel name="Label" colour="darkslategrey" css-label="customLabel"></toolboxlabel>
|
||||
```
|
||||
|
||||
Any item that begins with `css-` will be added to a `cssconfig` object stored on
|
||||
the `toolboxItemDef`.
|
||||
|
||||
To use this value navigate to `toolbox_label.js` and add the following lines to
|
||||
your `init` method.
|
||||
|
||||
```js
|
||||
// Any attributes that begin with css- will get added to a cssconfig object.
|
||||
const cssConfig = this.toolboxItemDef_['cssconfig'];
|
||||
// Add the class.
|
||||
if (cssConfig) {
|
||||
this.label.classList.add(cssConfig['label']);
|
||||
}
|
||||
```
|
||||
|
||||
The above code will add the class to the label. Now, in `toolbox_style.css` add
|
||||
the below CSS to make the label bold.
|
||||
|
||||
```css
|
||||
.customLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
If you open `index.html` you should now see a bold dark gray label at the
|
||||
top of your toolbox.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/final_toolbox.png"
|
||||
alt="A toolbox with colored background and the blockly label above the category text."
|
||||
className="codelabImage"
|
||||
/>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
slug: /codelabs/custom-toolbox/change-the-category-HTML
|
||||
description: How to change the HTML used by a category.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing a Blockly toolbox
|
||||
|
||||
## 6. Change the category HTML
|
||||
|
||||
If you only need to change the CSS, like we did in the previous section, then using the cssConfig is a great choice.
|
||||
However, if you need to change the html, maybe to add text, an image, or anything else, you can override
|
||||
the corresponding method that creates the dom. In this example, we'll add an `<img>`
|
||||
to our category by overriding the `createIconDom_` method.
|
||||
|
||||
### Change the element for our icon
|
||||
|
||||
By default, the `createIconDom_` method adds a `<span>` element for the category
|
||||
icon. We can override this to return an `<img>` element.
|
||||
|
||||
Add the following methods to `custom_category.js`:
|
||||
|
||||
```js
|
||||
/** @override */
|
||||
createIconDom_() {
|
||||
const img = document.createElement('img');
|
||||
img.src = './logo_only.svg';
|
||||
img.alt = 'Lamp';
|
||||
img.width='15';
|
||||
img.height='15';
|
||||
return img;
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
If you open `index.html` you should now see the blockly logo on top of all your
|
||||
categories
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/image_toolbox.png"
|
||||
alt="A toolbox with the blockly logo on top of the category label."
|
||||
className="codelabImage"
|
||||
/>
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
slug: /codelabs/custom-toolbox/change-the-look-of-a-category
|
||||
description: How to change the look of a category.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing a Blockly toolbox
|
||||
|
||||
## 3. Change the look of a category
|
||||
|
||||
### Change the background of the category
|
||||
|
||||
In the default `ToolboxCategory` class, the `addColourBorder_` method adds a strip of color next
|
||||
to the category name. We can override this method in order to add colour to the entire category div.
|
||||
|
||||
Add the following code to your `CustomCategory` class.
|
||||
|
||||
```js
|
||||
/** @override */
|
||||
addColourBorder_(colour){
|
||||
this.rowDiv_.style.backgroundColor = colour;
|
||||
}
|
||||
```
|
||||
|
||||
The `colour` passed in is calculated from either the `categorystyle` or the `colour`
|
||||
attribute set on the category definition.
|
||||
|
||||
For example, the "Logic" category definition looks like:
|
||||
|
||||
```xml
|
||||
<category name="Logic" categorystyle="logic_category">
|
||||
...
|
||||
</category>
|
||||
```
|
||||
|
||||
The logic_category style looks like:
|
||||
|
||||
```json
|
||||
"logic_category": {
|
||||
"colour": "210"
|
||||
}
|
||||
```
|
||||
|
||||
For more information on Blockly styles please visit the [themes documentation](/guides/configure/web/appearance/themes#category-style).
|
||||
|
||||
### Add some CSS
|
||||
|
||||
Open `index.html` to see your updated toolbox. Your toolbox should look
|
||||
similar to the below toolbox.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/colored_toolbox.png"
|
||||
alt="A toolbox with colors that expand the across the entire category."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
We are going to add some CSS to make it easier to read, and to space out our categories.
|
||||
|
||||
Create a file named `toolbox_style.css` in the same directory as `index.html`
|
||||
and include it in `index.html`:
|
||||
|
||||
```
|
||||
<link rel="stylesheet" href="toolbox_style.css">
|
||||
```
|
||||
|
||||
Copy and paste the following CSS into your `toolbox_style.css` file.
|
||||
|
||||
```css
|
||||
/* Makes our label white. */
|
||||
.blocklyToolboxCategoryLabel {
|
||||
color: white;
|
||||
}
|
||||
/* Adds padding around the group of categories and separators. */
|
||||
.blocklyToolboxCategoryGroup {
|
||||
padding: 0.5em;
|
||||
}
|
||||
/* Adds space between the categories, rounds the corners and adds space around the label. */
|
||||
.blocklyToolboxCategory {
|
||||
padding: 3px;
|
||||
margin-bottom: 0.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
Open `index.html` to see your toolbox.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/styled_toolbox.png"
|
||||
alt="Toolbox with category corners that are rounded and white text."
|
||||
className="codelabImage"
|
||||
/>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
slug: /codelabs/custom-toolbox/change-the-look-of-a-selected-category
|
||||
description: How to change the look of a selected category.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing a Blockly toolbox
|
||||
|
||||
## 4. Change the look of a selected category
|
||||
|
||||
Open your `index.html` and click on a category. You will see that it
|
||||
doesn't give any indication that it has been clicked. Worse than that, if you
|
||||
click on the category a second time the background color will disappear.
|
||||
|
||||
To fix this, we are going to override the `setSelected` method to change the look
|
||||
of a category when it has been clicked. In the default category class this method
|
||||
adds a colour to the entire row when a category is selected. Since we have already
|
||||
expanded the colour over our entire div, we are going to change the background
|
||||
color of the div to white, and the text to the color of the category when it has
|
||||
been selected.
|
||||
|
||||
Add the following code to `custom_category.js`:
|
||||
|
||||
```js
|
||||
/** @override */
|
||||
setSelected(isSelected){
|
||||
// We do not store the label span on the category, so use getElementsByClassName.
|
||||
var labelDom = this.rowDiv_.getElementsByClassName('blocklyToolboxCategoryLabel')[0];
|
||||
if (isSelected) {
|
||||
// Change the background color of the div to white.
|
||||
this.rowDiv_.style.backgroundColor = 'white';
|
||||
// Set the colour of the text to the colour of the category.
|
||||
labelDom.style.color = this.colour_;
|
||||
} else {
|
||||
// Set the background back to the original colour.
|
||||
this.rowDiv_.style.backgroundColor = this.colour_;
|
||||
// Set the text back to white.
|
||||
labelDom.style.color = 'white';
|
||||
}
|
||||
// This is used for accessibility purposes.
|
||||
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
|
||||
Blockly.utils.aria.State.SELECTED, isSelected);
|
||||
}
|
||||
```
|
||||
|
||||
### The result
|
||||
|
||||
Open `index.html` and click on the "Logic" category. You should now see a white
|
||||
category with a colored label.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/category_selected.png"
|
||||
alt="A toolbox with all categories colored except for the first category that has a white background."
|
||||
className="codelabImage"
|
||||
/>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
pagination_prev: null
|
||||
slug: /codelabs/custom-toolbox/codelab-overview
|
||||
description: Overview of the "Customizing a Blockly toolbox" codelab.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing a Blockly toolbox
|
||||
|
||||
## 1. Codelab overview
|
||||
|
||||
### What you'll learn
|
||||
|
||||
This codelab will focus on customizing the Blockly toolbox.
|
||||
|
||||
In this codelab you will learn:
|
||||
|
||||
1. How to add a background color to a toolbox category.
|
||||
1. How to change the look of a selected category.
|
||||
1. How to add a custom CSS classes to a toolbox category.
|
||||
1. How to change the structure of your category HTML.
|
||||
1. How to add a custom toolbox item.
|
||||
|
||||
### What you'll build
|
||||
|
||||
Over the course of this codelab you will customize your toolbox categories as well
|
||||
as create a custom toolbox item.
|
||||
The resulting toolbox is shown below.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/final_toolbox.png"
|
||||
alt="A toolbox with colored background and the blockly label above the category text."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
The code samples are written in ES6 syntax. You can find the code for the [completed custom toolbox](https://github.com/RaspberryPiFoundation/blockly/docs/docs/codelabs/custom-toolbox/complete-code) on GitHub.
|
||||
|
||||
### What you'll need
|
||||
|
||||
- A browser.
|
||||
- A text editor.
|
||||
- Basic knowledge of HTML, CSS, and JavaScript.
|
||||
- Basic understanding of the [Blockly toolbox](/guides/configure/web/toolboxes/toolbox).
|
||||
|
||||
Throughout various parts of this codelab we will be talking about [toolbox definitions](/guides/configure/web/toolboxes/category?tab=xml).
|
||||
The toolbox definition can be written in XML or JSON. We will be using an XML
|
||||
toolbox definition that can be found in the provided code.
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview The toolbox category built during the custom toolbox codelab, in es6.
|
||||
* @author aschmiedt@google.com (Abby Schmiedt)
|
||||
*/
|
||||
|
||||
class CustomCategory extends Blockly.ToolboxCategory {
|
||||
/**
|
||||
* Constructor for a custom category.
|
||||
* @override
|
||||
*/
|
||||
constructor(categoryDef, toolbox, opt_parent) {
|
||||
super(categoryDef, toolbox, opt_parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the colour to the toolbox.
|
||||
* This is called on category creation and whenever the theme changes.
|
||||
* @override
|
||||
*/
|
||||
addColourBorder_(colour) {
|
||||
this.rowDiv_.style.backgroundColor = colour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style for the category when it is selected or deselected.
|
||||
* @param {boolean} isSelected True if the category has been selected,
|
||||
* false otherwise.
|
||||
* @override
|
||||
*/
|
||||
setSelected(isSelected) {
|
||||
// We do not store the label span on the category, so use getElementsByClassName.
|
||||
const labelDom = this.rowDiv_.getElementsByClassName(
|
||||
'blocklyToolboxCategoryLabel',
|
||||
)[0];
|
||||
if (isSelected) {
|
||||
// Change the background color of the div to white.
|
||||
this.rowDiv_.style.backgroundColor = 'white';
|
||||
// Set the colour of the text to the colour of the category.
|
||||
labelDom.style.color = this.colour_;
|
||||
this.iconDom_.style.color = this.colour_;
|
||||
} else {
|
||||
// Set the background back to the original colour.
|
||||
this.rowDiv_.style.backgroundColor = this.colour_;
|
||||
// Set the text back to white.
|
||||
labelDom.style.color = 'white';
|
||||
this.iconDom_.style.color = 'white';
|
||||
}
|
||||
// This is used for accessibility purposes.
|
||||
Blockly.utils.aria.setState(
|
||||
/** @type {!Element} */ (this.htmlDiv_),
|
||||
Blockly.utils.aria.State.SELECTED,
|
||||
isSelected,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the dom used for the icon.
|
||||
* @returns {HTMLElement} The element for the icon.
|
||||
* @override
|
||||
*/
|
||||
createIconDom_() {
|
||||
const iconImg = document.createElement('img');
|
||||
iconImg.src = './logo_only.svg';
|
||||
iconImg.alt = 'Blockly Logo';
|
||||
iconImg.width = '25';
|
||||
iconImg.height = '25';
|
||||
return iconImg;
|
||||
}
|
||||
}
|
||||
|
||||
Blockly.registry.register(
|
||||
Blockly.registry.Type.TOOLBOX_ITEM,
|
||||
Blockly.ToolboxCategory.registrationName,
|
||||
CustomCategory,
|
||||
true,
|
||||
);
|
||||
@@ -0,0 +1,359 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Toolbox Customization Codelab</title>
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="index.js"></script>
|
||||
<script src="custom_category_es6.js"></script>
|
||||
<script src="toolbox_label_es6.js"></script>
|
||||
<link rel="stylesheet" href="toolbox_style.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
float: bottom;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="start()">
|
||||
<h1>Toolbox Customization Codelab</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
|
||||
<!-- Toolbox Definition -->
|
||||
<xml
|
||||
xmlns="https://developers.google.com/blockly/xml"
|
||||
id="toolbox-categories"
|
||||
style="display: none">
|
||||
<toolboxlabel name="Custom Toolbox" colour="darkslategrey"></toolboxlabel>
|
||||
<category
|
||||
css-icon="customIcon fa fa-cog"
|
||||
name="Logic"
|
||||
categorystyle="logic_category">
|
||||
<block type="controls_if"></block>
|
||||
<block type="logic_compare"></block>
|
||||
<block type="logic_operation"></block>
|
||||
<block type="logic_negate"></block>
|
||||
<block type="logic_boolean"></block>
|
||||
<block type="logic_null" disabled="true"></block>
|
||||
<block type="logic_ternary"></block>
|
||||
</category>
|
||||
<category name="Loops" categorystyle="loop_category">
|
||||
<block type="controls_repeat_ext">
|
||||
<value name="TIMES">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_repeat" disabled="true"></block>
|
||||
<block type="controls_whileUntil"></block>
|
||||
<block type="controls_for">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="BY">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_forEach"></block>
|
||||
<block type="controls_flow_statements"></block>
|
||||
</category>
|
||||
<category name="Math" categorystyle="math_category">
|
||||
<block type="math_number" gap="32">
|
||||
<field name="NUM">123</field>
|
||||
</block>
|
||||
<block type="math_arithmetic">
|
||||
<value name="A">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="B">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_single">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">9</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_trig">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">45</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_constant"></block>
|
||||
<block type="math_number_property">
|
||||
<value name="NUMBER_TO_CHECK">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_round">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">3.1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_on_list"></block>
|
||||
<block type="math_modulo">
|
||||
<value name="DIVIDEND">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">64</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="DIVISOR">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_constrain">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">50</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="LOW">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="HIGH">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_int">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_float"></block>
|
||||
<block type="math_atan2">
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Text" categorystyle="text_category">
|
||||
<block type="text"></block>
|
||||
<block type="text_join"></block>
|
||||
<block type="text_append">
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_length">
|
||||
<value name="VALUE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_isEmpty">
|
||||
<value name="VALUE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"></field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_indexOf">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
<value name="FIND">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_charAt">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_getSubstring">
|
||||
<value name="STRING">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_changeCase">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_trim">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_count">
|
||||
<value name="SUB">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_replace">
|
||||
<value name="FROM">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_reverse">
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<label text="Input/Output:" web-class="ioLabel"></label>
|
||||
<block type="text_print">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_prompt_ext">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Lists" categorystyle="list_category">
|
||||
<block type="lists_create_with">
|
||||
<mutation items="0"></mutation>
|
||||
</block>
|
||||
<block type="lists_create_with"></block>
|
||||
<block type="lists_repeat">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">5</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_length"></block>
|
||||
<block type="lists_isEmpty"></block>
|
||||
<block type="lists_indexOf">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_getIndex">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_setIndex">
|
||||
<value name="LIST">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_getSublist">
|
||||
<value name="LIST">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_split">
|
||||
<value name="DELIM">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">,</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_sort"></block>
|
||||
<block type="lists_reverse"></block>
|
||||
</category>
|
||||
<sep></sep>
|
||||
<category
|
||||
name="Variables"
|
||||
categorystyle="variable_category"
|
||||
custom="VARIABLE"></category>
|
||||
<category
|
||||
name="Functions"
|
||||
categorystyle="procedure_category"
|
||||
custom="PROCEDURE"></category>
|
||||
</xml>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
let workspace = null;
|
||||
|
||||
function start() {
|
||||
// Create main workspace.
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: document.getElementById('toolbox-categories'),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="Layer_6"
|
||||
data-name="Layer 6"
|
||||
viewBox="0 0 192 192"
|
||||
version="1.1"
|
||||
sodipodi:docname="logo-only.svg"
|
||||
inkscape:version="0.92.2pre0 (973e216, 2017-07-25)"
|
||||
inkscape:export-filename="/usr/local/google/home/epastern/Documents/Blockly Logos/Square/logo-only.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<metadata
|
||||
id="metadata913">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>logo-only</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1379"
|
||||
id="namedview911"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2"
|
||||
inkscape:cx="239.87642"
|
||||
inkscape:cy="59.742687"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g1013" />
|
||||
<defs
|
||||
id="defs902">
|
||||
<style
|
||||
id="style900">.cls-1{fill:#4285f4;}.cls-2{fill:#c8d1db;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title904">logo-only</title>
|
||||
<g
|
||||
id="g1013"
|
||||
transform="translate(23.500002,-7.9121105)"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<path
|
||||
id="path906"
|
||||
d="M 20.140625,32 C 13.433598,31.994468 7.9944684,37.433598 8,44.140625 V 148.85938 C 7.99447,155.56641 13.433598,161.00553 20.140625,161 h 4.726563 c 2.330826,8.74182 10.245751,14.82585 19.292968,14.83008 C 53.201562,175.81878 61.108176,169.73621 63.4375,161 h 4.841797 15.726562 c 4.418278,0 8,-3.58172 8,-8 V 40 l -8,-8 z"
|
||||
style="fill:#4285f4"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccssccc" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccccccccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path908"
|
||||
d="M 80.007812,31.994141 C 79.997147,49.696887 80,67.396525 80,85.109375 L 63.369141,75.710938 C 60.971784,74.358189 58.004891,76.087168 58,78.839844 v 40.621096 c 0.0049,2.75267 2.971786,4.48165 5.369141,3.1289 L 80,113.18945 v 37.5918 2.21875 8 h 8 1.425781 36.054689 c 6.36195,-2.6e-4 11.51927,-5.15758 11.51953,-11.51953 V 43.480469 C 136.97822,37.133775 131.8272,32.000222 125.48047,32 Z"
|
||||
style="fill:#c8d1db" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview The toolbox label built during the custom toolbox codelab, in es6.
|
||||
* @author aschmiedt@google.com (Abby Schmiedt)
|
||||
*/
|
||||
|
||||
class ToolboxLabel extends Blockly.ToolboxItem {
|
||||
/**
|
||||
* Constructor for a label in the toolbox.
|
||||
* @param {!Blockly.utils.toolbox.ToolboxItemInfo} toolboxItemDef The toolbox
|
||||
* item definition. This comes directly from the toolbox definition.
|
||||
* @param {!Blockly.IToolbox} parentToolbox The toolbox that holds this
|
||||
* toolbox item.
|
||||
* @override
|
||||
*/
|
||||
constructor(toolboxItemDef, parentToolbox) {
|
||||
super(toolboxItemDef, parentToolbox);
|
||||
/**
|
||||
* The button element.
|
||||
* @type {?HTMLLabelElement}
|
||||
*/
|
||||
this.label = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init method for the label.
|
||||
* @override
|
||||
*/
|
||||
init() {
|
||||
// Create the label.
|
||||
this.label = document.createElement('label');
|
||||
// Set the name.
|
||||
this.label.textContent = this.toolboxItemDef_['name'];
|
||||
// Set the color.
|
||||
this.label.style.color = this.toolboxItemDef_['colour'];
|
||||
// Any attributes that begin with css- will get added to a cssconfig.
|
||||
const cssConfig = this.toolboxItemDef_['cssconfig'];
|
||||
// Add the class.
|
||||
if (cssConfig) {
|
||||
this.label.classList.add(cssConfig['label']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the div for the toolbox item.
|
||||
* @returns {HTMLLabelElement} The label element.
|
||||
* @override
|
||||
*/
|
||||
getDiv() {
|
||||
return this.label;
|
||||
}
|
||||
}
|
||||
|
||||
Blockly.registry.register(
|
||||
Blockly.registry.Type.TOOLBOX_ITEM,
|
||||
'toolboxlabel',
|
||||
ToolboxLabel,
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
/* Makes our label white. */
|
||||
.blocklyToolboxCategoryLabel {
|
||||
color: #fff;
|
||||
}
|
||||
/* Adds padding around the group of categories and separators. */
|
||||
.blocklyToolboxCategoryGroup {
|
||||
padding: 0.5em;
|
||||
}
|
||||
/* Adds space between the categories, rounds the corners and adds space around the label. */
|
||||
.blocklyToolboxCategory {
|
||||
padding: 3px;
|
||||
margin-bottom: 0.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
/* Changes color of the icon to white. */
|
||||
.customIcon {
|
||||
color: #fff;
|
||||
}
|
||||
/* Stacks the icon on top of the label. */
|
||||
.blocklyTreeRowContentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.blocklyToolboxCategory {
|
||||
height: initial;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
slug: /codelabs/custom-toolbox/setup
|
||||
description: Setting up the "Customizing a Blockly toolbox" codelab.
|
||||
---
|
||||
|
||||
import Image from '@site/src/components/Image';
|
||||
|
||||
# Customizing a Blockly toolbox
|
||||
|
||||
## 2. Setup
|
||||
|
||||
### Download the sample code
|
||||
|
||||
You can get the sample code for this codelab by either downloading the zip here:
|
||||
|
||||
[Download zip](https://github.com/RaspberryPiFoundation/blockly/archive/main.zip)
|
||||
|
||||
or by cloning this git repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RaspberryPiFoundation/blockly.git
|
||||
```
|
||||
|
||||
If you downloaded the source as a zip, unpacking it should give you a root folder named `blockly-main`.
|
||||
|
||||
The relevant files are in `docs/docs/codelabs/custom-toolbox`. There are two versions of the app:
|
||||
|
||||
- `starter-code/`: The starter code that you'll build upon in this codelab.
|
||||
- `complete-code/`: The code after completing the codelab, in case you get lost or want to compare to your version.
|
||||
|
||||
Each folder contains:
|
||||
|
||||
- `index.js` - The codelab's logic. To start, it just injects a simple workspace.
|
||||
- `index.html` - A web page containing a simple blockly workspace.
|
||||
|
||||
To run the code, simply open `starter-code/index.html` in a browser. You should see a Blockly workspace with a toolbox.
|
||||
|
||||

|
||||
|
||||
### Define and register a custom category
|
||||
|
||||
To start, create a file named `custom_category.js` in the `starter-code`
|
||||
directory. Include your new file by adding a script tag to `index.html`.
|
||||
|
||||
```html
|
||||
<script src="custom_category.js"></script>
|
||||
```
|
||||
|
||||
In order to create a custom category we will create a new category that extends
|
||||
the default `Blockly.ToolboxCategory` class. Add the following code to your
|
||||
`custom_category.js` file.
|
||||
|
||||
```js
|
||||
class CustomCategory extends Blockly.ToolboxCategory {
|
||||
/**
|
||||
* Constructor for a custom category.
|
||||
* @override
|
||||
*/
|
||||
constructor(categoryDef, toolbox, opt_parent) {
|
||||
super(categoryDef, toolbox, opt_parent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After defining your category you need to tell Blockly that it exists.
|
||||
Register your category by adding the below code to the end of `custom_category.js`.
|
||||
|
||||
```js
|
||||
Blockly.registry.register(
|
||||
Blockly.registry.Type.TOOLBOX_ITEM,
|
||||
Blockly.ToolboxCategory.registrationName,
|
||||
CustomCategory,
|
||||
true,
|
||||
);
|
||||
```
|
||||
|
||||
By registering our `CustomCategory` with `Blockly.ToolboxCategory.registrationName`
|
||||
we are overriding the default category in Blockly. Because we are overriding a
|
||||
toolbox item instead of adding a new one, we must pass in `true` as the last
|
||||
argument. If this flag is `false`, `Blockly.registry.register` will throw
|
||||
an error because we are overriding an existing class.
|
||||
|
||||
### The result
|
||||
|
||||
To test, open `index.html` in a browser. Your toolbox should look the same as it
|
||||
did before.
|
||||
|
||||
<Image
|
||||
src="/images/codelabs/custom-toolbox/base_toolbox.png"
|
||||
alt="The default toolbox. A list of categories with a strip of colour to the left."
|
||||
className="codelabImage"
|
||||
/>
|
||||
|
||||
However, if you run the below commands in your console you will see that
|
||||
your toolbox is now using the `CustomCategory` class.
|
||||
|
||||
```js
|
||||
var toolbox = Blockly.common.getMainWorkspace().getToolbox();
|
||||
toolbox.getToolboxItems()[0];
|
||||
```
|
||||
@@ -0,0 +1,349 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Toolbox Customization Codelab</title>
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="index.js"></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#blocklyDiv {
|
||||
float: bottom;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="start()">
|
||||
<h1>Toolbox Customization Codelab</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
|
||||
<!-- Toolbox Definition -->
|
||||
<xml
|
||||
xmlns="https://developers.google.com/blockly/xml"
|
||||
id="toolbox-categories"
|
||||
style="display: none">
|
||||
<category name="Logic" categorystyle="logic_category">
|
||||
<block type="controls_if"></block>
|
||||
<block type="logic_compare"></block>
|
||||
<block type="logic_operation"></block>
|
||||
<block type="logic_negate"></block>
|
||||
<block type="logic_boolean"></block>
|
||||
<block type="logic_null" disabled="true"></block>
|
||||
<block type="logic_ternary"></block>
|
||||
</category>
|
||||
<category name="Loops" categorystyle="loop_category">
|
||||
<block type="controls_repeat_ext">
|
||||
<value name="TIMES">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_repeat" disabled="true"></block>
|
||||
<block type="controls_whileUntil"></block>
|
||||
<block type="controls_for">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="BY">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_forEach"></block>
|
||||
<block type="controls_flow_statements"></block>
|
||||
</category>
|
||||
<category name="Math" categorystyle="math_category">
|
||||
<block type="math_number" gap="32">
|
||||
<field name="NUM">123</field>
|
||||
</block>
|
||||
<block type="math_arithmetic">
|
||||
<value name="A">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="B">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_single">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">9</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_trig">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">45</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_constant"></block>
|
||||
<block type="math_number_property">
|
||||
<value name="NUMBER_TO_CHECK">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_round">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">3.1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_on_list"></block>
|
||||
<block type="math_modulo">
|
||||
<value name="DIVIDEND">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">64</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="DIVISOR">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_constrain">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">50</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="LOW">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="HIGH">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_int">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_float"></block>
|
||||
<block type="math_atan2">
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Text" categorystyle="text_category">
|
||||
<block type="text"></block>
|
||||
<block type="text_join"></block>
|
||||
<block type="text_append">
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_length">
|
||||
<value name="VALUE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_isEmpty">
|
||||
<value name="VALUE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"></field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_indexOf">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
<value name="FIND">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_charAt">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_getSubstring">
|
||||
<value name="STRING">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_changeCase">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_trim">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_count">
|
||||
<value name="SUB">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_replace">
|
||||
<value name="FROM">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_reverse">
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<label text="Input/Output:" web-class="ioLabel"></label>
|
||||
<block type="text_print">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_prompt_ext">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Lists" categorystyle="list_category">
|
||||
<block type="lists_create_with">
|
||||
<mutation items="0"></mutation>
|
||||
</block>
|
||||
<block type="lists_create_with"></block>
|
||||
<block type="lists_repeat">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">5</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_length"></block>
|
||||
<block type="lists_isEmpty"></block>
|
||||
<block type="lists_indexOf">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_getIndex">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_setIndex">
|
||||
<value name="LIST">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_getSublist">
|
||||
<value name="LIST">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_split">
|
||||
<value name="DELIM">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">,</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_sort"></block>
|
||||
<block type="lists_reverse"></block>
|
||||
</category>
|
||||
<sep></sep>
|
||||
<category
|
||||
name="Variables"
|
||||
categorystyle="variable_category"
|
||||
custom="VARIABLE"></category>
|
||||
<category
|
||||
name="Functions"
|
||||
categorystyle="procedure_category"
|
||||
custom="PROCEDURE"></category>
|
||||
</xml>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
let workspace = null;
|
||||
|
||||
function start() {
|
||||
// Create main workspace.
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: document.getElementById('toolbox-categories'),
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user