From b5ff2f02fc01b98b38b9d109d40406bc3ab7dea2 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 14:37:04 +0200 Subject: [PATCH 01/40] [NEW]: created modal component to add blocks to the toolbox dynamically --- .../blocks-modal/blocks-modal.component.html | 29 +++++++ .../blocks-modal/blocks-modal.component.ts | 85 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/app/components/blocks-modal/blocks-modal.component.html create mode 100644 src/app/components/blocks-modal/blocks-modal.component.ts diff --git a/src/app/components/blocks-modal/blocks-modal.component.html b/src/app/components/blocks-modal/blocks-modal.component.html new file mode 100644 index 0000000..3210627 --- /dev/null +++ b/src/app/components/blocks-modal/blocks-modal.component.html @@ -0,0 +1,29 @@ +
+
+
+

Select the blocks

+ +
+
+ @for (block of blockTypes; track block) { +
+
+ + +
+ } +
+
+
diff --git a/src/app/components/blocks-modal/blocks-modal.component.ts b/src/app/components/blocks-modal/blocks-modal.component.ts new file mode 100644 index 0000000..1a62d26 --- /dev/null +++ b/src/app/components/blocks-modal/blocks-modal.component.ts @@ -0,0 +1,85 @@ +import {AfterViewInit, Component, EventEmitter, Input, Output, signal} from '@angular/core'; +import {ButtonComponent} from '../button/button.component'; +import * as Blockly from 'blockly'; + +@Component({ + selector: 'blearn-blocks-modal', + imports: [ + ButtonComponent + ], + templateUrl: './blocks-modal.component.html', +}) +export class BlocksModalComponent implements AfterViewInit { + @Input() toolbox = signal({ + kind: 'flyoutToolbox', + contents: [ + {kind: '', text: '', callbackKey: ''}, + {kind: '', type: ''}, + ] + }); + @Output() blockAdded = new EventEmitter(); + @Output() close = new EventEmitter(); + + protected blockTypes = [ + 'event_start', + 'controls_repeat', + 'controls_repeat_forever', + 'controls_if', + 'controls_wait', + 'movement_jump', + 'movement_turn_left', + 'movement_turn_right', + 'movement_forward', + 'movement_set_direction' + ]; + + ngAfterViewInit(): void { + this.renderBlocks() + } + + private renderBlocks() { + const list = document.querySelector('#list')!; + this.blockTypes.forEach((block) => { + const tmpWorkspace = Blockly.inject(document.getElementById(block)!, { + toolbox: this.toolbox(), + readOnly: true, + renderer: 'Zelos', + scrollbars: false, + move: { + scrollbars: {horizontal: false, vertical: true}, + drag: false, + wheel: false, + }, + zoom: { + controls: false, + wheel: false, + }, + }); + + const rect = list.querySelectorAll("rect"); + rect.forEach((r) => { + r.style.display = "none"; + }); + + const newBlock = tmpWorkspace.newBlock(block); + newBlock.initSvg(); + newBlock.render(); + tmpWorkspace.zoomToFit(); + }); + } + + protected addBlock(type: string) { + const newToolbox = { + ...this.toolbox(), + contents: [...this.toolbox().contents, {kind: 'block', type}], + }; + this.toolbox.set(newToolbox); + + // Emits an event that a new block has been added to the toolbox + this.blockAdded.emit(); + } + + protected onClose() { + this.close.emit(); + } +} From 78440f8c21bc3428876cd15d8bf5414be354028c Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 14:38:49 +0200 Subject: [PATCH 02/40] [UPDATE]: added new toolbox attribute to keep record of the blocks added to it --- src/app/models/activity.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/models/activity.ts b/src/app/models/activity.ts index b1cb028..c50cb1d 100644 --- a/src/app/models/activity.ts +++ b/src/app/models/activity.ts @@ -4,4 +4,5 @@ export interface Activity { description: string, dueDate: string, workspace: string, + toolbox: string } From f3ebec577fe239dbe727fd01d5d9a2ea0d5204b5 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 14:39:37 +0200 Subject: [PATCH 03/40] [NEW]: created JSON with custom definition of blocks --- assets/blocks.json | 184 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 assets/blocks.json diff --git a/assets/blocks.json b/assets/blocks.json new file mode 100644 index 0000000..0d34aa3 --- /dev/null +++ b/assets/blocks.json @@ -0,0 +1,184 @@ +[ + { + "type": "event_start", + "message0": "Start program %1 %2", + "style": { + "hat": "cap" + }, + "args0": [ + { + "type": "field_image", + "src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWZsYWctaWNvbiBsdWNpZGUtZmxhZyI+PHBhdGggZD0iTTQgMTVzMS0xIDQtMSA1IDIgOCAyIDQtMSA0LTFWM3MtMSAxLTQgMS01LTItOC0yLTQgMS00IDF6Ii8+PGxpbmUgeDE9IjQiIHgyPSI0IiB5MT0iMjIiIHkyPSIxNSIvPjwvc3ZnPg==", + "width": 20, + "height": 20, + "alt": "*", + "flipRtl": "FALSE" + }, + { + "type": "input_dummy", + "name": "jump" + } + ], + "nextStatement": null, + "colour": "#ffb309" + }, + { + "type": "movement_jump", + "tooltip": "", + "helpUrl": "", + "message0": "jump to x: %1 y: %2 %3", + "args0": [ + { + "type": "field_number", + "name": "x", + "value": 0 + }, + { + "type": "field_number", + "name": "y", + "value": 0 + }, + { + "type": "input_dummy", + "name": "jump" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 285, + "inputsInline": true + }, + { + "type": "movement_turn_left", + "tooltip": "", + "helpUrl": "", + "message0": "turn left %1 %2 %3º", + "args0": [ + { + "type": "field_image", + "src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXJvdGF0ZS1jY3ctaWNvbiBsdWNpZGUtcm90YXRlLWNjdyI+PHBhdGggZD0iTTMgMTJhOSA5IDAgMSAwIDktOSA5Ljc1IDkuNzUgMCAwIDAtNi43NCAyLjc0TDMgOCIvPjxwYXRoIGQ9Ik0zIDN2NWg1Ii8+PC9zdmc+", + "width": 20, + "height": 20, + "alt": "*", + "flipRtl": "FALSE" + }, + { + "type": "field_number", + "name": "angle", + "value": 0 + }, + { + "type": "input_dummy" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 285, + "inputsInline": true + }, + { + "type": "movement_turn_right", + "tooltip": "", + "helpUrl": "", + "message0": "turn right %1 %2 %3º", + "args0": [ + { + "type": "field_image", + "src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXJvdGF0ZS1jdy1pY29uIGx1Y2lkZS1yb3RhdGUtY3ciPjxwYXRoIGQ9Ik0yMSAxMmE5IDkgMCAxIDEtOS05YzIuNTIgMCA0LjkzIDEgNi43NCAyLjc0TDIxIDgiLz48cGF0aCBkPSJNMjEgM3Y1aC01Ii8+PC9zdmc+", + "width": 20, + "height": 20, + "alt": "*", + "flipRtl": "FALSE" + }, + { + "type": "field_number", + "name": "angle", + "value": 0 + }, + { + "type": "input_dummy" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 285, + "inputsInline": true + }, + { + "type": "movement_forward", + "tooltip": "", + "helpUrl": "", + "message0": "forward %1 %2", + "args0": [ + { + "type": "field_number", + "name": "forward", + "value": 0 + }, + { + "type": "input_dummy", + "name": "forward" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 285 + }, + { + "type": "controls_wait", + "tooltip": "", + "helpUrl": "", + "message0": "wait %1 seconds %2", + "args0": [ + { + "type": "field_number", + "name": "seconds", + "value": 0 + }, + { + "type": "input_dummy", + "name": "NAME" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 210 + }, + { + "type": "movement_set_direction", + "tooltip": "", + "helpUrl": "", + "message0": "set direction %1 %2", + "args0": [ + { + "type": "field_number", + "name": "angle", + "value": 0 + }, + { + "type": "input_dummy", + "name": "direction" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 285 + }, + { + "type": "controls_repeat_forever", + "tooltip": "", + "helpUrl": "", + "message0": "repeat forever %1 do %2", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "input_statement", + "name": "statement" + } + ], + "previousStatement": null, + "colour": 120 + } +] From 31ef5b39567bf78cdfd654e7d4379d898be27aa1 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 14:41:10 +0200 Subject: [PATCH 04/40] [UPDATE]: updated tests to match new activity model definition --- .../activity/activity.component.spec.ts | 3 +- src/app/services/activity.service.spec.ts | 36 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/app/components/activity/activity.component.spec.ts b/src/app/components/activity/activity.component.spec.ts index b65557a..48932df 100644 --- a/src/app/components/activity/activity.component.spec.ts +++ b/src/app/components/activity/activity.component.spec.ts @@ -19,7 +19,8 @@ describe('ActivityComponent', () => { title: 'Activity 1', description: 'Description', dueDate: '03/03', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }; it('should toggle the menu when clicking the button', async () => { diff --git a/src/app/services/activity.service.spec.ts b/src/app/services/activity.service.spec.ts index 6ccee44..50b4292 100644 --- a/src/app/services/activity.service.spec.ts +++ b/src/app/services/activity.service.spec.ts @@ -39,7 +39,8 @@ describe('ActivityService', () => { title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }; modeService.setMode('student'); @@ -57,7 +58,8 @@ describe('ActivityService', () => { title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }; modeService.setMode('teacher'); @@ -75,7 +77,8 @@ describe('ActivityService', () => { title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }]; browserStorageService.loadData.mockReturnValue(mockActivities); @@ -104,14 +107,16 @@ describe('ActivityService', () => { title: 'Activity 2', description: 'Description 2', dueDate: '2/2/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }; const existingActivities: Activity[] = [{ id: '1', title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }]; browserStorageService.loadData.mockReturnValue(existingActivities); @@ -126,14 +131,16 @@ describe('ActivityService', () => { title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }, { id: '2', title: 'Activity 2', description: 'Description 2', dueDate: '2/2/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' } ] ); @@ -147,14 +154,16 @@ describe('ActivityService', () => { title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }, { id: '2', title: 'Activity 2', description: 'Description 2', dueDate: '2/2/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' } ]; browserStorageService.loadData.mockReturnValue(existingActivities); @@ -169,7 +178,8 @@ describe('ActivityService', () => { title: 'Activity 2', description: 'Description 2', dueDate: '2/2/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' } ); }); @@ -181,7 +191,8 @@ describe('ActivityService', () => { title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' }; browserStorageService.loadData.mockReturnValue([mockActivity]); @@ -201,7 +212,8 @@ describe('ActivityService', () => { title: 'Activity 1', description: 'Description', dueDate: '1/1/2000', - workspace: '{}' + workspace: '{}', + toolbox: '{}' } ]; browserStorageService.loadData.mockImplementation(() => mockActivity); From 3cfc21f57544ba60db91610a1bd854a07901eb8e Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 14:42:02 +0200 Subject: [PATCH 05/40] [UPDATE]: changed the way of creating and importing tasks to match new definition --- src/app/pages/home/home.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index 933c34c..64e3b5f 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -39,6 +39,7 @@ export class HomeComponent { description: '', dueDate: '', workspace: '{}', + toolbox: '{"kind": "flyoutToolbox", "contents": [{ "kind": "", "text": "", "callbackKey": ""}, {"kind": "", "type": ""}]}' }; this.activityService.addActivity(newActivity); this.activityList.set(this.activityService.loadActivities()); @@ -88,6 +89,7 @@ export class HomeComponent { description: jsonData.description, dueDate: jsonData.dueDate, workspace: jsonData.workspace, + toolbox: jsonData.toolbox, } } } From 805517dbef1e96748494fac1a49c12359e242d47 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 23:40:15 +0200 Subject: [PATCH 06/40] [UPDATE]: added toolbox max number of instances each type --- src/app/models/activity.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/models/activity.ts b/src/app/models/activity.ts index c50cb1d..bafd458 100644 --- a/src/app/models/activity.ts +++ b/src/app/models/activity.ts @@ -4,5 +4,8 @@ export interface Activity { description: string, dueDate: string, workspace: string, - toolbox: string + toolboxInfo: { + BLOCK_LIMITS: { [key: string]: number }, + toolboxDefinition: string, + } } From bd8c2a2b8bc3829bac03bb3f61431e43bf6ec016 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 23:43:49 +0200 Subject: [PATCH 07/40] [UPDATE]: updated tests to match new activity model definition --- .../activity/activity.component.spec.ts | 5 +- src/app/services/activity.service.spec.ts | 60 +++++++++++++++---- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/app/components/activity/activity.component.spec.ts b/src/app/components/activity/activity.component.spec.ts index 48932df..676e10c 100644 --- a/src/app/components/activity/activity.component.spec.ts +++ b/src/app/components/activity/activity.component.spec.ts @@ -20,7 +20,10 @@ describe('ActivityComponent', () => { description: 'Description', dueDate: '03/03', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }; it('should toggle the menu when clicking the button', async () => { diff --git a/src/app/services/activity.service.spec.ts b/src/app/services/activity.service.spec.ts index 50b4292..3fb0481 100644 --- a/src/app/services/activity.service.spec.ts +++ b/src/app/services/activity.service.spec.ts @@ -40,7 +40,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }; modeService.setMode('student'); @@ -59,7 +62,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }; modeService.setMode('teacher'); @@ -78,7 +84,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }]; browserStorageService.loadData.mockReturnValue(mockActivities); @@ -108,7 +117,10 @@ describe('ActivityService', () => { description: 'Description 2', dueDate: '2/2/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }; const existingActivities: Activity[] = [{ id: '1', @@ -116,7 +128,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }]; browserStorageService.loadData.mockReturnValue(existingActivities); @@ -132,7 +147,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolbox: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }, { id: '2', @@ -140,7 +158,10 @@ describe('ActivityService', () => { description: 'Description 2', dueDate: '2/2/2000', workspace: '{}', - toolbox: '{}' + toolbox: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } } ] ); @@ -155,7 +176,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }, { id: '2', @@ -163,7 +187,10 @@ describe('ActivityService', () => { description: 'Description 2', dueDate: '2/2/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } } ]; browserStorageService.loadData.mockReturnValue(existingActivities); @@ -179,7 +206,10 @@ describe('ActivityService', () => { description: 'Description 2', dueDate: '2/2/2000', workspace: '{}', - toolbox: '{}' + toolbox: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } } ); }); @@ -192,7 +222,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } }; browserStorageService.loadData.mockReturnValue([mockActivity]); @@ -213,7 +246,10 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: '{}' + toolboxInfo: { + toolboxDefinition: '', + BLOCK_LIMITS: {} + } } ]; browserStorageService.loadData.mockImplementation(() => mockActivity); From 4456c21902c30f23896aed1c2679d9411ebaf698 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sat, 5 Apr 2025 23:45:04 +0200 Subject: [PATCH 08/40] [UPDATE]: adapted creation of tasks to new toolboxInfo --- src/app/pages/home/home.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index 64e3b5f..0ef30ff 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -39,7 +39,10 @@ export class HomeComponent { description: '', dueDate: '', workspace: '{}', - toolbox: '{"kind": "flyoutToolbox", "contents": [{ "kind": "", "text": "", "callbackKey": ""}, {"kind": "", "type": ""}]}' + toolboxInfo: { + toolboxDefinition: '{"kind": "flyoutToolbox", "contents": [{ "kind": "", "text": "", "callbackKey": ""}, {"kind": "", "type": ""}]}', + BLOCK_LIMITS: {} + } }; this.activityService.addActivity(newActivity); this.activityList.set(this.activityService.loadActivities()); @@ -89,7 +92,7 @@ export class HomeComponent { description: jsonData.description, dueDate: jsonData.dueDate, workspace: jsonData.workspace, - toolbox: jsonData.toolbox, + toolboxInfo: jsonData.toolboxInfo, } } } From 49a45bf9d28ff8a0ec33d5c54cc3c16b6edb1c59 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sun, 6 Apr 2025 10:31:20 +0200 Subject: [PATCH 09/40] [UPDATE]: added disability of custom button --- src/app/components/button/button.component.html | 6 +++++- src/app/components/button/button.component.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/components/button/button.component.html b/src/app/components/button/button.component.html index 3de5e95..a389be5 100644 --- a/src/app/components/button/button.component.html +++ b/src/app/components/button/button.component.html @@ -1,6 +1,10 @@ diff --git a/src/app/components/button/button.component.ts b/src/app/components/button/button.component.ts index efb8e6b..b1b4ed4 100644 --- a/src/app/components/button/button.component.ts +++ b/src/app/components/button/button.component.ts @@ -17,6 +17,7 @@ export class ButtonComponent { @Input() teacherText: string = ''; @Input() studentStyle: string = ''; @Input() teacherStyle: string = ''; + @Input() disabled: boolean = false; buttonText = computed(() => (this.modeService.getMode() === 'student' ? this.studentText : this.teacherText)); buttonStyle = computed(() => (this.modeService.getMode() === 'student' ? this.studentStyle : this.teacherStyle)); From 5c903fe2637a4c40d8169b4f8c4026bb80ee1d58 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sun, 6 Apr 2025 10:44:36 +0200 Subject: [PATCH 10/40] [UPDATE]: added option to see the limit number of each block and add or remove from the modal --- .../blocks-modal/blocks-modal.component.html | 10 ++++--- .../blocks-modal/blocks-modal.component.ts | 29 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/app/components/blocks-modal/blocks-modal.component.html b/src/app/components/blocks-modal/blocks-modal.component.html index 3210627..cdc14e1 100644 --- a/src/app/components/blocks-modal/blocks-modal.component.html +++ b/src/app/components/blocks-modal/blocks-modal.component.html @@ -13,12 +13,14 @@

Select the blocks

+

{{BLOCKS_LIMIT.get(block) || 0}}

diff --git a/src/app/components/blocks-modal/blocks-modal.component.ts b/src/app/components/blocks-modal/blocks-modal.component.ts index 1a62d26..7f9e3a0 100644 --- a/src/app/components/blocks-modal/blocks-modal.component.ts +++ b/src/app/components/blocks-modal/blocks-modal.component.ts @@ -10,14 +10,9 @@ import * as Blockly from 'blockly'; templateUrl: './blocks-modal.component.html', }) export class BlocksModalComponent implements AfterViewInit { - @Input() toolbox = signal({ - kind: 'flyoutToolbox', - contents: [ - {kind: '', text: '', callbackKey: ''}, - {kind: '', type: ''}, - ] - }); - @Output() blockAdded = new EventEmitter(); + @Input() BLOCKS_LIMIT: Map = new Map(); + @Output() blockAdded = new EventEmitter(); + @Output() blockRemoved = new EventEmitter(); @Output() close = new EventEmitter(); protected blockTypes = [ @@ -41,7 +36,12 @@ export class BlocksModalComponent implements AfterViewInit { const list = document.querySelector('#list')!; this.blockTypes.forEach((block) => { const tmpWorkspace = Blockly.inject(document.getElementById(block)!, { - toolbox: this.toolbox(), + toolbox: { + kind: 'flyoutToolbox', + contents: [ + {kind: '', type: ''}, + ] + }, readOnly: true, renderer: 'Zelos', scrollbars: false, @@ -69,14 +69,11 @@ export class BlocksModalComponent implements AfterViewInit { } protected addBlock(type: string) { - const newToolbox = { - ...this.toolbox(), - contents: [...this.toolbox().contents, {kind: 'block', type}], - }; - this.toolbox.set(newToolbox); + this.blockAdded.emit(type); + } - // Emits an event that a new block has been added to the toolbox - this.blockAdded.emit(); + protected removeBlock(type: string) { + this.blockRemoved.emit(type); } protected onClose() { From 29f676b86a86f68b186c820c5ae24a8c80f4ccf0 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sun, 6 Apr 2025 10:45:43 +0200 Subject: [PATCH 11/40] [UPDATE]: loaded custom blocks when app loads --- src/app/app.component.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3ba7bb8..12740b4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,7 +1,9 @@ -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; import {HeaderComponent} from './layout/header/header.component'; import {FooterComponent} from './layout/footer/footer.component'; +import * as Blockly from 'blockly'; +import blocks from '../../assets/blocks.json'; @Component({ selector: 'blearn-root', @@ -10,5 +12,7 @@ import {FooterComponent} from './layout/footer/footer.component'; styleUrl: './app.component.css' }) export class AppComponent { - + constructor() { + Blockly.defineBlocksWithJsonArray(blocks); + } } From 9261132696011dec64c0524bc9b61bc4feb3a6c8 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sun, 6 Apr 2025 11:27:16 +0200 Subject: [PATCH 12/40] [UPDATE]: added provisional activity detail, need refactor --- .../blockly-editor.component.html | 4 +- .../blockly-editor.component.ts | 6 + .../activity-detail.component.html | 25 +- .../activity-detail.component.ts | 267 +++++++++++++++++- 4 files changed, 285 insertions(+), 17 deletions(-) diff --git a/src/app/components/blockly-editor/blockly-editor.component.html b/src/app/components/blockly-editor/blockly-editor.component.html index 38fe985..aa4454e 100644 --- a/src/app/components/blockly-editor/blockly-editor.component.html +++ b/src/app/components/blockly-editor/blockly-editor.component.html @@ -1 +1,3 @@ -
+
+
+
diff --git a/src/app/components/blockly-editor/blockly-editor.component.ts b/src/app/components/blockly-editor/blockly-editor.component.ts index ac68453..e4d3a65 100644 --- a/src/app/components/blockly-editor/blockly-editor.component.ts +++ b/src/app/components/blockly-editor/blockly-editor.component.ts @@ -9,10 +9,15 @@ import 'blockly/blocks'; }) export class BlocklyEditorComponent implements AfterViewInit { @ViewChild('blocklyDiv') blocklyDiv!: ElementRef; + @ViewChild('blocklyArea') blocklyArea!: ElementRef; @Input() workspaceJSON!: string; private workspace!: Blockly.WorkspaceSvg; ngAfterViewInit(): void { + this.initBlockly(); + } + + private initBlockly() { this.workspace = Blockly.inject(this.blocklyDiv.nativeElement, { toolbox: { kind: 'flyoutToolbox', @@ -48,6 +53,7 @@ export class BlocklyEditorComponent implements AfterViewInit { trashcan: true, scrollbars: true, }); + const jsonWorkspace = JSON.parse(this.workspaceJSON); Blockly.serialization.workspaces.load(jsonWorkspace, this.workspace); } diff --git a/src/app/pages/activity-detail/activity-detail.component.html b/src/app/pages/activity-detail/activity-detail.component.html index c675d7f..a2a555a 100644 --- a/src/app/pages/activity-detail/activity-detail.component.html +++ b/src/app/pages/activity-detail/activity-detail.component.html @@ -1,5 +1,5 @@ -
-
+
+
@if (modeService.getMode() === 'student') { } + @let style = "text-white"; @let buttonText = "Download activity";
-

{{ activity()?.description }}

- +
+
+
+
+ + +
+ +
+ +
+
+
diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index eb6c9aa..16f5690 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -1,4 +1,13 @@ -import {Component, computed, inject, signal, ViewChild} from '@angular/core'; +import { + AfterViewInit, + Component, + computed, + ElementRef, + inject, + signal, + ViewChild, + ViewContainerRef +} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {ActivityService} from '../../services/activity.service'; import {toSignal} from '@angular/core/rxjs-interop'; @@ -9,6 +18,8 @@ import {FormsModule} from '@angular/forms'; import {ModeService} from '../../services/mode.service'; import {TitleComponent} from '../../components/title/title.component'; import {ButtonComponent} from '../../components/button/button.component'; +import * as Blockly from 'blockly'; +import {BlocksModalComponent} from '../../components/blocks-modal/blocks-modal.component'; @Component({ selector: 'blearn-activity-detail', @@ -16,23 +27,39 @@ import {ButtonComponent} from '../../components/button/button.component'; BlocklyEditorComponent, FormsModule, TitleComponent, - ButtonComponent + ButtonComponent, ], templateUrl: './activity-detail.component.html', }) -export class ActivityDetailComponent { +export class ActivityDetailComponent implements AfterViewInit { private route = inject(ActivatedRoute); protected activityService = inject(ActivityService); protected modeService = inject(ModeService); private router = inject(Router); - @ViewChild(BlocklyEditorComponent) blocklyEditorComponent!: BlocklyEditorComponent; + //@ViewChild(BlocklyEditorComponent) blocklyEditorComponent!: BlocklyEditorComponent; + @ViewChild('blocklyDiv') blocklyDiv!: ElementRef; + @ViewChild('blocklyArea') blocklyArea!: ElementRef; + @ViewChild('modalHost', {read: ViewContainerRef}) modalHost!: ViewContainerRef; + @ViewChild('scene') scene!: ElementRef; + @ViewChild('canvas') canvas!: ElementRef; - private activityId = toSignal( - this.route.paramMap.pipe( - map(params => params.get('id') || null) - ) - ); + private ctx: CanvasRenderingContext2D | null = null; + private images: Array<{ img: HTMLImageElement, x: number, y: number, width: number, height: number }> = []; + private draggingImage: any = null; + private offsetX: number = 0; + private offsetY: number = 0; + + private workspace!: Blockly.WorkspaceSvg; + protected toolbox = signal({ + kind: 'flyoutToolbox', + contents: [ + {kind: '', text: '', callbackKey: ''}, + {kind: '', type: ''}, + ] + }); + + private readonly BLOCK_LIMITS: Map; protected activity = signal(null); @@ -54,18 +81,234 @@ export class ActivityDetailComponent { }); this.activity.set(computedActivity()); + console.log(this.activity()!.toolboxInfo.BLOCK_LIMITS) + this.BLOCK_LIMITS = new Map(Object.entries(this.activity()!.toolboxInfo.BLOCK_LIMITS)); + console.log(this.BLOCK_LIMITS); + } + + private updateToolboxLimits(workspace: Blockly.WorkspaceSvg) { + const blockCounts = new Map(); + workspace.getAllBlocks(false).forEach(block => { + const type = block.type; + blockCounts.set(type, (blockCounts.get(type) || 0) + 1); + }); + + const newToolbox = { + kind: 'flyoutToolbox', + contents: this.toolbox().contents.map((entry: any) => { + if (entry.kind === 'block') { + const currentCount = blockCounts.get(entry.type) || 0; + const limit = this.BLOCK_LIMITS.get(entry.type); + return { + ...entry, + enabled: limit !== undefined ? currentCount < limit : true, + }; + } else return entry; + }) + } + + workspace.updateToolbox(newToolbox); + } + + ngAfterViewInit(): void { + this.initBlockly(); + this.initCanvas(); + } + + private openBlocksModal() { + const modalRef = this.modalHost.createComponent(BlocksModalComponent); + modalRef.instance.BLOCKS_LIMIT = this.BLOCK_LIMITS; + + modalRef.instance.blockAdded.subscribe(type => { + if (this.BLOCK_LIMITS.has(type)) this.BLOCK_LIMITS.set(type, this.BLOCK_LIMITS.get(type)! + 1) + else this.BLOCK_LIMITS.set(type, 1); + + if (!this.toolbox().contents.some(block => block.type === type)) { + const newToolbox = { + ...this.toolbox(), + contents: [...this.toolbox().contents, {kind: 'block', type}], + }; + this.toolbox.set(newToolbox); + } + + this.updateToolboxLimits(this.workspace); + }); + + modalRef.instance.blockRemoved.subscribe(type => { + if (this.BLOCK_LIMITS.get(type) === 1) { + this.BLOCK_LIMITS.delete(type); + + if (this.toolbox().contents.some(block => block.type === type)) { + const newToolbox = { + ...this.toolbox(), + contents: this.toolbox().contents.filter(block => block.type !== type) + } + this.toolbox.set(newToolbox); + } + } else if (this.BLOCK_LIMITS.has(type) && this.BLOCK_LIMITS.get(type)! > 1) this.BLOCK_LIMITS.set(type, this.BLOCK_LIMITS.get(type)! - 1); + + this.updateToolboxLimits(this.workspace); + }); + modalRef.instance.close.subscribe(() => modalRef.destroy()); } + private activityId = toSignal( + this.route.paramMap.pipe( + map(params => params.get('id') || null) + ) + ); + updateTitle(newTitle: string) { if (this.activity()) { - this.activity.set({ ...this.activity()!, title: newTitle }); + this.activity.set({...this.activity()!, title: newTitle}); this.activityService.updateActivity(this.activityId()!, this.activity()!); } } saveWorkspace() { - const workspaceJSON = this.blocklyEditorComponent.saveWorkspaceAsJson(); - this.activity.set({ ...this.activity()!, workspace: workspaceJSON }); + const jsonWorkspace = Blockly.serialization.workspaces.save(this.workspace); + if (this.modeService.getMode() === 'teacher') this.toolbox().contents.shift(); + const jsonToolbox = JSON.stringify(this.toolbox()); + const workspaceJSON = JSON.stringify(jsonWorkspace); + this.activity.set({ + ...this.activity()!, + workspace: workspaceJSON, + toolboxInfo: {BLOCK_LIMITS: Object.fromEntries(this.BLOCK_LIMITS), toolboxDefinition: jsonToolbox} + }); + console.log(this.activity()!); this.activityService.updateActivity(this.activityId()!, this.activity()!); + //const workspaceJSON = this.blocklyEditorComponent.saveWorkspaceAsJson(); + //this.activity.set({ ...this.activity()!, workspace: workspaceJSON }); + //this.activityService.updateActivity(this.activityId()!, this.activity()!); + } + + private initBlockly() { + this.toolbox.set(JSON.parse(this.activity()!.toolboxInfo.toolboxDefinition)); + + if (this.modeService.getMode() === 'teacher') { + const newToolbox = { + ...this.toolbox(), + contents: [ + { + kind: 'button', + text: '+ Add Blocks to this toolbox', + callbackKey: 'addNewBlock' + }, + ...this.toolbox().contents + ], + }; + this.toolbox.set(newToolbox); + } + + this.workspace = Blockly.inject(this.blocklyDiv.nativeElement, { + toolbox: this.toolbox(), + renderer: 'Zelos', + grid: { + colour: '#ccc', + snap: true, + spacing: 20, + length: 3 + }, + trashcan: true, + scrollbars: true, + }); + + this.workspace.addChangeListener((event) => { + if ( + event.type === Blockly.Events.BLOCK_CREATE || + event.type === Blockly.Events.BLOCK_DELETE + ) { + this.updateToolboxLimits(this.workspace); + } + }) + + this.workspace.registerButtonCallback('addNewBlock', () => { + this.openBlocksModal(); + }) + + this.resizeBlockly(); + + const jsonWorkspace = JSON.parse(this.activity()!.workspace); + Blockly.serialization.workspaces.load(jsonWorkspace, this.workspace); + } + + private resizeBlockly(): void { + const blocklyArea = this.blocklyArea.nativeElement; + const blocklyDiv = this.blocklyDiv.nativeElement; + + // Ensure Blockly workspace resizes properly with the area + blocklyDiv.style.width = `${blocklyArea.offsetWidth}px`; + blocklyDiv.style.height = `${blocklyArea.offsetHeight}px`; + Blockly.svgResize(this.workspace); // Resize the workspace after adjusting the div size + } + + private initCanvas() { + this.ctx = this.canvas.nativeElement.getContext('2d'); + + // Initialize the canvas size based on the scene + this.canvas.nativeElement.width = this.scene.nativeElement.offsetWidth; + this.canvas.nativeElement.height = this.scene.nativeElement.offsetHeight; + + // Create an image object + const img = new Image(); + img.src = 'https://avatars.githubusercontent.com/u/105555875?v=4'; // Replace with your image URL + img.onload = () => { + // Once the image is loaded, draw it to the canvas + this.images.push({img, x: 50, y: 50, width: 100, height: 100}); + this.drawImages(); + }; + + // Set up mouse event listeners for dragging + this.setupMouseEvents(); + } + + private setupMouseEvents() { + this.canvas.nativeElement.addEventListener('mousedown', (e: MouseEvent) => { + const mouseX = e.offsetX; + const mouseY = e.offsetY; + + // Check if the mouse click is on one of the images + for (let imgObj of this.images) { + if (mouseX >= imgObj.x && mouseX <= imgObj.x + imgObj.width && mouseY >= imgObj.y && mouseY <= imgObj.y + imgObj.height) { + this.draggingImage = imgObj; + this.offsetX = mouseX - imgObj.x; + this.offsetY = mouseY - imgObj.y; + } + } + }); + + this.canvas.nativeElement.addEventListener('mousemove', (e: MouseEvent) => { + if (this.draggingImage) { + const mouseX = e.offsetX; + const mouseY = e.offsetY; + + // Move the image based on mouse position + this.draggingImage.x = mouseX - this.offsetX; + this.draggingImage.y = mouseY - this.offsetY; + + this.drawImages(); + } + }); + + this.canvas.nativeElement.addEventListener('mouseup', () => { + this.draggingImage = null; + }); + + this.canvas.nativeElement.addEventListener('mouseleave', () => { + this.draggingImage = null; + }); + } + + + private drawImages() { + if (!this.ctx) return; + + // Clear the canvas + this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height); + + // Draw each image on the canvas + for (let imgObj of this.images) { + this.ctx.drawImage(imgObj.img, imgObj.x, imgObj.y, imgObj.width, imgObj.height); + } } } From 1bf10f5bcc68eccdbb0ef2461a1b5642f72391a4 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sun, 6 Apr 2025 13:14:08 +0200 Subject: [PATCH 13/40] [UPDATE]: refactored provisional code into his own component --- .../blockly-editor.component.html | 4 +- .../blockly-editor.component.ts | 87 +++++++++++++------ 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/app/components/blockly-editor/blockly-editor.component.html b/src/app/components/blockly-editor/blockly-editor.component.html index aa4454e..beabbc5 100644 --- a/src/app/components/blockly-editor/blockly-editor.component.html +++ b/src/app/components/blockly-editor/blockly-editor.component.html @@ -1,3 +1 @@ -
-
-
+
diff --git a/src/app/components/blockly-editor/blockly-editor.component.ts b/src/app/components/blockly-editor/blockly-editor.component.ts index e4d3a65..e377892 100644 --- a/src/app/components/blockly-editor/blockly-editor.component.ts +++ b/src/app/components/blockly-editor/blockly-editor.component.ts @@ -1,6 +1,18 @@ -import {AfterViewInit, Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, EventEmitter, inject, + Input, + OnChanges, + OnInit, Output, + signal, + SimpleChanges, + ViewChild +} from '@angular/core'; import * as Blockly from 'blockly'; import 'blockly/blocks'; +import {ModeService} from '../../services/mode.service'; +import {WorkspaceSvg} from 'blockly'; @Component({ selector: 'blearn-blockly-editor', @@ -8,9 +20,23 @@ import 'blockly/blocks'; templateUrl: './blockly-editor.component.html', }) export class BlocklyEditorComponent implements AfterViewInit { + private modeService = inject(ModeService); + @ViewChild('blocklyDiv') blocklyDiv!: ElementRef; - @ViewChild('blocklyArea') blocklyArea!: ElementRef; + //@ViewChild('blocklyArea') blocklyArea!: ElementRef; + + @Input() toolbox = signal({ + kind: 'flyoutToolbox', + contents: [ + {kind: '', text: '', callbackKey: ''}, + {kind: '', type: ''}, + ] + }); @Input() workspaceJSON!: string; + @Input() BLOCKS_LIMITS: Map = new Map(); + @Output() openModal = new EventEmitter(); + @Output() updateLimits = new EventEmitter(); + private workspace!: Blockly.WorkspaceSvg; ngAfterViewInit(): void { @@ -18,32 +44,24 @@ export class BlocklyEditorComponent implements AfterViewInit { } private initBlockly() { - this.workspace = Blockly.inject(this.blocklyDiv.nativeElement, { - toolbox: { - kind: 'flyoutToolbox', + if (this.modeService.getMode() === 'teacher') { + const newToolbox = { + ...this.toolbox(), contents: [ { - kind: 'block', - type: 'procedures_defnoreturn' + kind: 'button', + text: '+ Add Blocks to this toolbox', + callbackKey: 'addNewBlock' }, - { - kind: 'block', - type: 'text_print' - }, - { - kind: 'block', - type: 'text_print' - }, - { - kind: 'block', - type: 'controls_if' - }, - { - kind: 'block', - type: 'controls_whileUntil' - } + ...this.toolbox().contents ], - }, + }; + this.toolbox.set(newToolbox); + } + + this.workspace = Blockly.inject(this.blocklyDiv.nativeElement, { + toolbox: this.toolbox(), + renderer: 'Zelos', grid: { colour: '#ccc', snap: true, @@ -54,12 +72,27 @@ export class BlocklyEditorComponent implements AfterViewInit { scrollbars: true, }); + this.workspace.addChangeListener((event) => { + if ( + event.type === Blockly.Events.BLOCK_CREATE || + event.type === Blockly.Events.BLOCK_DELETE + ) { + this.updateLimits.emit(); + } + }) + + this.workspace.registerButtonCallback('addNewBlock', () => { + //this.openBlocksModal(); + this.openModal.emit(); + }) + + //this.resizeBlockly(); + const jsonWorkspace = JSON.parse(this.workspaceJSON); Blockly.serialization.workspaces.load(jsonWorkspace, this.workspace); } - saveWorkspaceAsJson(): string { - const jsonWorkspace = Blockly.serialization.workspaces.save(this.workspace); - return JSON.stringify(jsonWorkspace); + public getWorkspace(): WorkspaceSvg { + return this.workspace; } } From 6d33d412bc75ebf6068fa31e953377b6f8607397 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sun, 6 Apr 2025 13:17:28 +0200 Subject: [PATCH 14/40] [UPDATE]: remove unnecessary code from activity detail --- .../activity-detail.component.html | 13 ++-- .../activity-detail.component.ts | 68 ++----------------- 2 files changed, 13 insertions(+), 68 deletions(-) diff --git a/src/app/pages/activity-detail/activity-detail.component.html b/src/app/pages/activity-detail/activity-detail.component.html index a2a555a..025bc10 100644 --- a/src/app/pages/activity-detail/activity-detail.component.html +++ b/src/app/pages/activity-detail/activity-detail.component.html @@ -28,15 +28,14 @@

{{ activity()?.description }}

-
-
-
- + (updateLimits)="updateToolboxLimits(workspace)" + />
diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index 16f5690..5e7de07 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -37,7 +37,7 @@ export class ActivityDetailComponent implements AfterViewInit { protected modeService = inject(ModeService); private router = inject(Router); - //@ViewChild(BlocklyEditorComponent) blocklyEditorComponent!: BlocklyEditorComponent; + @ViewChild(BlocklyEditorComponent) blocklyEditorComponent!: BlocklyEditorComponent; @ViewChild('blocklyDiv') blocklyDiv!: ElementRef; @ViewChild('blocklyArea') blocklyArea!: ElementRef; @ViewChild('modalHost', {read: ViewContainerRef}) modalHost!: ViewContainerRef; @@ -50,7 +50,7 @@ export class ActivityDetailComponent implements AfterViewInit { private offsetX: number = 0; private offsetY: number = 0; - private workspace!: Blockly.WorkspaceSvg; + protected workspace!: Blockly.WorkspaceSvg; protected toolbox = signal({ kind: 'flyoutToolbox', contents: [ @@ -59,7 +59,7 @@ export class ActivityDetailComponent implements AfterViewInit { ] }); - private readonly BLOCK_LIMITS: Map; + protected readonly BLOCK_LIMITS: Map; protected activity = signal(null); @@ -81,12 +81,11 @@ export class ActivityDetailComponent implements AfterViewInit { }); this.activity.set(computedActivity()); - console.log(this.activity()!.toolboxInfo.BLOCK_LIMITS) + this.toolbox.set(JSON.parse(this.activity()!.toolboxInfo.toolboxDefinition)); this.BLOCK_LIMITS = new Map(Object.entries(this.activity()!.toolboxInfo.BLOCK_LIMITS)); - console.log(this.BLOCK_LIMITS); } - private updateToolboxLimits(workspace: Blockly.WorkspaceSvg) { + protected updateToolboxLimits(workspace: Blockly.WorkspaceSvg) { const blockCounts = new Map(); workspace.getAllBlocks(false).forEach(block => { const type = block.type; @@ -111,11 +110,11 @@ export class ActivityDetailComponent implements AfterViewInit { } ngAfterViewInit(): void { - this.initBlockly(); + this.workspace = this.blocklyEditorComponent.getWorkspace(); this.initCanvas(); } - private openBlocksModal() { + protected openBlocksModal() { const modalRef = this.modalHost.createComponent(BlocksModalComponent); modalRef.instance.BLOCKS_LIMIT = this.BLOCK_LIMITS; @@ -177,59 +176,6 @@ export class ActivityDetailComponent implements AfterViewInit { }); console.log(this.activity()!); this.activityService.updateActivity(this.activityId()!, this.activity()!); - //const workspaceJSON = this.blocklyEditorComponent.saveWorkspaceAsJson(); - //this.activity.set({ ...this.activity()!, workspace: workspaceJSON }); - //this.activityService.updateActivity(this.activityId()!, this.activity()!); - } - - private initBlockly() { - this.toolbox.set(JSON.parse(this.activity()!.toolboxInfo.toolboxDefinition)); - - if (this.modeService.getMode() === 'teacher') { - const newToolbox = { - ...this.toolbox(), - contents: [ - { - kind: 'button', - text: '+ Add Blocks to this toolbox', - callbackKey: 'addNewBlock' - }, - ...this.toolbox().contents - ], - }; - this.toolbox.set(newToolbox); - } - - this.workspace = Blockly.inject(this.blocklyDiv.nativeElement, { - toolbox: this.toolbox(), - renderer: 'Zelos', - grid: { - colour: '#ccc', - snap: true, - spacing: 20, - length: 3 - }, - trashcan: true, - scrollbars: true, - }); - - this.workspace.addChangeListener((event) => { - if ( - event.type === Blockly.Events.BLOCK_CREATE || - event.type === Blockly.Events.BLOCK_DELETE - ) { - this.updateToolboxLimits(this.workspace); - } - }) - - this.workspace.registerButtonCallback('addNewBlock', () => { - this.openBlocksModal(); - }) - - this.resizeBlockly(); - - const jsonWorkspace = JSON.parse(this.activity()!.workspace); - Blockly.serialization.workspaces.load(jsonWorkspace, this.workspace); } private resizeBlockly(): void { From 288cf89b8a55fc9776561df601d2ac513b2bb180 Mon Sep 17 00:00:00 2001 From: jcasben Date: Sun, 6 Apr 2025 19:51:57 +0200 Subject: [PATCH 15/40] [FIX]: fixed some test with wrong definition of activity --- src/app/services/activity.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/services/activity.service.spec.ts b/src/app/services/activity.service.spec.ts index 3fb0481..4cc9845 100644 --- a/src/app/services/activity.service.spec.ts +++ b/src/app/services/activity.service.spec.ts @@ -147,7 +147,7 @@ describe('ActivityService', () => { description: 'Description', dueDate: '1/1/2000', workspace: '{}', - toolbox: { + toolboxInfo: { toolboxDefinition: '', BLOCK_LIMITS: {} } @@ -158,7 +158,7 @@ describe('ActivityService', () => { description: 'Description 2', dueDate: '2/2/2000', workspace: '{}', - toolbox: { + toolboxInfo: { toolboxDefinition: '', BLOCK_LIMITS: {} } @@ -206,7 +206,7 @@ describe('ActivityService', () => { description: 'Description 2', dueDate: '2/2/2000', workspace: '{}', - toolbox: { + toolboxInfo: { toolboxDefinition: '', BLOCK_LIMITS: {} } From e7c8e9a10744af5b92ca690cfde29b7d3ffc8cd7 Mon Sep 17 00:00:00 2001 From: jcasben Date: Mon, 7 Apr 2025 13:48:12 +0200 Subject: [PATCH 16/40] [NEW]: created modal component to interact with activity description and due date --- .../description-modal.component.html | 35 +++++++++++++++++++ .../description-modal.component.spec.ts | 23 ++++++++++++ .../description-modal.component.ts | 32 +++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 src/app/components/description-modal/description-modal.component.html create mode 100644 src/app/components/description-modal/description-modal.component.spec.ts create mode 100644 src/app/components/description-modal/description-modal.component.ts diff --git a/src/app/components/description-modal/description-modal.component.html b/src/app/components/description-modal/description-modal.component.html new file mode 100644 index 0000000..c3485f1 --- /dev/null +++ b/src/app/components/description-modal/description-modal.component.html @@ -0,0 +1,35 @@ +
+
+
+
+

{{ activity()!.title }}

+
+

Due on:

+ @if (modeService.getMode() === 'student') { +

{{ activity()!.dueDate | date: 'MM/dd/yyyy' }}

+ } @else { + + } +
+
+ + @let closeStyle = 'bg-red-500 text-white'; + +
+
+

Description

+ @if (modeService.getMode() === 'student') { +

{{activity()!.description}}

+ } @else { + + } +
+
+
diff --git a/src/app/components/description-modal/description-modal.component.spec.ts b/src/app/components/description-modal/description-modal.component.spec.ts new file mode 100644 index 0000000..4aa74b6 --- /dev/null +++ b/src/app/components/description-modal/description-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DescriptionModalComponent } from './description-modal.component'; + +describe('DescriptionModalComponent', () => { + let component: DescriptionModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DescriptionModalComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DescriptionModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/description-modal/description-modal.component.ts b/src/app/components/description-modal/description-modal.component.ts new file mode 100644 index 0000000..f122031 --- /dev/null +++ b/src/app/components/description-modal/description-modal.component.ts @@ -0,0 +1,32 @@ +import {Component, EventEmitter, inject, Input, Output, signal} from '@angular/core'; +import {ButtonComponent} from '../button/button.component'; +import {Activity} from '../../models/activity'; +import {FormsModule} from '@angular/forms'; +import {ModeService} from '../../services/mode.service'; +import {DatePipe} from '@angular/common'; + +@Component({ + selector: 'blearn-description-modal', + imports: [ + ButtonComponent, + FormsModule, + DatePipe + ], + templateUrl: './description-modal.component.html', +}) +export class DescriptionModalComponent { + protected modeService = inject(ModeService); + + @Input() activity = signal(null); + @Output() close = new EventEmitter(); + @Output() dueDateUpdated = new EventEmitter(); + @Output() descriptionUpdated = new EventEmitter(); + + protected updateDueDate(newDate: string) { + this.dueDateUpdated.emit(newDate); + } + + protected updateDescription(newDescription: string) { + this.descriptionUpdated.emit(newDescription); + } +} From 3b5ad746fd37aabc850e5d1fd0c53a938c9e8b93 Mon Sep 17 00:00:00 2001 From: jcasben Date: Mon, 7 Apr 2025 13:49:40 +0200 Subject: [PATCH 17/40] [UPDATE]: moved all styles to ngClass --- src/app/components/button/button.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/button/button.component.html b/src/app/components/button/button.component.html index a389be5..2367191 100644 --- a/src/app/components/button/button.component.html +++ b/src/app/components/button/button.component.html @@ -1,6 +1,6 @@
- -

{{ activity()?.description }}

-
modalRef.destroy()); + modalRef.instance.close.subscribe(() => { + this.saveWorkspace(false); + modalRef.destroy(); + }); } private activityId = toSignal( @@ -166,7 +173,7 @@ export class ActivityDetailComponent implements AfterViewInit { saveWorkspace() { const jsonWorkspace = Blockly.serialization.workspaces.save(this.workspace); - if (this.modeService.getMode() === 'teacher') this.toolbox().contents.shift(); + if (onStorage && this.modeService.getMode() === 'teacher') this.toolbox().contents.shift(); const jsonToolbox = JSON.stringify(this.toolbox()); const workspaceJSON = JSON.stringify(jsonWorkspace); this.activity.set({ @@ -174,8 +181,7 @@ export class ActivityDetailComponent implements AfterViewInit { workspace: workspaceJSON, toolboxInfo: {BLOCK_LIMITS: Object.fromEntries(this.BLOCK_LIMITS), toolboxDefinition: jsonToolbox} }); - console.log(this.activity()!); - this.activityService.updateActivity(this.activityId()!, this.activity()!); + if (onStorage) this.activityService.updateActivity(this.activityId()!, this.activity()!); } private resizeBlockly(): void { From 1ea7b207649826bb2342f689fd442aa397eb1c66 Mon Sep 17 00:00:00 2001 From: jcasben Date: Mon, 7 Apr 2025 13:56:23 +0200 Subject: [PATCH 19/40] [UPDATE]: modified toolbox button message --- .../blockly-editor/blockly-editor.component.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/components/blockly-editor/blockly-editor.component.ts b/src/app/components/blockly-editor/blockly-editor.component.ts index fc4cb78..a46b0af 100644 --- a/src/app/components/blockly-editor/blockly-editor.component.ts +++ b/src/app/components/blockly-editor/blockly-editor.component.ts @@ -1,18 +1,18 @@ import { AfterViewInit, Component, - ElementRef, EventEmitter, inject, + ElementRef, + EventEmitter, + inject, Input, - OnChanges, - OnInit, Output, + Output, signal, - SimpleChanges, ViewChild } from '@angular/core'; import * as Blockly from 'blockly'; +import {WorkspaceSvg} from 'blockly'; import 'blockly/blocks'; import {ModeService} from '../../services/mode.service'; -import {WorkspaceSvg} from 'blockly'; @Component({ selector: 'blearn-blockly-editor', @@ -51,7 +51,7 @@ export class BlocklyEditorComponent implements AfterViewInit { contents: [ { kind: 'button', - text: '+ Add Blocks to this toolbox', + text: 'Add / Remove blocks', callbackKey: 'addNewBlock' }, ...this.toolbox().contents From 796ab99ae250c3d3681658b6426b6d278e79c5f2 Mon Sep 17 00:00:00 2001 From: jcasben Date: Mon, 7 Apr 2025 13:58:12 +0200 Subject: [PATCH 20/40] [UPDATE]: refactor some code and added description modal call --- .../activity-detail.component.html | 41 ++++--- .../activity-detail.component.ts | 101 ++++++++++-------- 2 files changed, 84 insertions(+), 58 deletions(-) diff --git a/src/app/pages/activity-detail/activity-detail.component.html b/src/app/pages/activity-detail/activity-detail.component.html index aa1fd3a..0ebf5ea 100644 --- a/src/app/pages/activity-detail/activity-detail.component.html +++ b/src/app/pages/activity-detail/activity-detail.component.html @@ -1,5 +1,8 @@
-
+
@if (modeService.getMode() === 'student') { + @let downloadText = "Download activity"; + @let descriptionText = "Description"; +
+ + +
- -
- -
- +
+
+
+
diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index 9662bd1..75e2a8d 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -3,7 +3,7 @@ import { Component, computed, ElementRef, - inject, + inject, OnDestroy, signal, ViewChild, ViewContainerRef @@ -20,6 +20,8 @@ import {TitleComponent} from '../../components/title/title.component'; import {ButtonComponent} from '../../components/button/button.component'; import * as Blockly from 'blockly'; import {BlocksModalComponent} from '../../components/blocks-modal/blocks-modal.component'; +import {DescriptionModalComponent} from '../../components/description-modal/description-modal.component'; +import {NgClass} from '@angular/common'; @Component({ selector: 'blearn-activity-detail', @@ -28,6 +30,7 @@ import {BlocksModalComponent} from '../../components/blocks-modal/blocks-modal.c FormsModule, TitleComponent, ButtonComponent, + NgClass, ], templateUrl: './activity-detail.component.html', }) @@ -38,8 +41,6 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { private router = inject(Router); @ViewChild(BlocklyEditorComponent) blocklyEditorComponent!: BlocklyEditorComponent; - @ViewChild('blocklyDiv') blocklyDiv!: ElementRef; - @ViewChild('blocklyArea') blocklyArea!: ElementRef; @ViewChild('modalHost', {read: ViewContainerRef}) modalHost!: ViewContainerRef; @ViewChild('scene') scene!: ElementRef; @ViewChild('canvas') canvas!: ElementRef; @@ -61,6 +62,11 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { protected readonly BLOCK_LIMITS: Map; + private activityId = toSignal( + this.route.paramMap.pipe( + map(params => params.get('id') || null) + ) + ); protected activity = signal(null); constructor() { @@ -85,30 +91,6 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { this.BLOCK_LIMITS = new Map(Object.entries(this.activity()!.toolboxInfo.BLOCK_LIMITS)); } - protected updateToolboxLimits(workspace: Blockly.WorkspaceSvg) { - const blockCounts = new Map(); - workspace.getAllBlocks(false).forEach(block => { - const type = block.type; - blockCounts.set(type, (blockCounts.get(type) || 0) + 1); - }); - - const newToolbox = { - kind: 'flyoutToolbox', - contents: this.toolbox().contents.map((entry: any) => { - if (entry.kind === 'block') { - const currentCount = blockCounts.get(entry.type) || 0; - const limit = this.BLOCK_LIMITS.get(entry.type); - return { - ...entry, - enabled: limit !== undefined ? currentCount < limit : true, - }; - } else return entry; - }) - } - - workspace.updateToolbox(newToolbox); - } - ngAfterViewInit(): void { this.workspace = this.blocklyEditorComponent.getWorkspace(); this.initCanvas(); @@ -158,20 +140,61 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { }); } - private activityId = toSignal( - this.route.paramMap.pipe( - map(params => params.get('id') || null) - ) - ); + protected openDescriptionModal() { + const modalRef = this.modalHost.createComponent(DescriptionModalComponent); + modalRef.instance.activity = this.activity; + + modalRef.instance.dueDateUpdated.subscribe(dueDate => this.updateDueDate(dueDate)); + modalRef.instance.descriptionUpdated.subscribe(description => this.updateDescription(description)); + modalRef.instance.close.subscribe(() => modalRef.destroy()); + } - updateTitle(newTitle: string) { + protected updateToolboxLimits(workspace: Blockly.WorkspaceSvg) { + const blockCounts = new Map(); + workspace.getAllBlocks(false).forEach(block => { + const type = block.type; + blockCounts.set(type, (blockCounts.get(type) || 0) + 1); + }); + + const newToolbox = { + kind: 'flyoutToolbox', + contents: this.toolbox().contents.map((entry: any) => { + if (entry.kind === 'block') { + const currentCount = blockCounts.get(entry.type) || 0; + const limit = this.BLOCK_LIMITS.get(entry.type); + return { + ...entry, + enabled: limit !== undefined ? currentCount < limit : true, + }; + } else return entry; + }) + } + + workspace.updateToolbox(newToolbox); + } + + protected updateTitle(newTitle: string) { if (this.activity()) { this.activity.set({...this.activity()!, title: newTitle}); this.activityService.updateActivity(this.activityId()!, this.activity()!); } } - saveWorkspace() { + protected updateDueDate(newDueDate: string) { + if (this.activity()) { + this.activity.set({...this.activity()!, dueDate: newDueDate }); + this.activityService.updateActivity(this.activityId()!, this.activity()!); + } + } + + protected updateDescription(newDescription: string) { + if (this.activity()) { + this.activity.set({...this.activity()!, description: newDescription }); + this.activityService.updateActivity(this.activityId()!, this.activity()!); + } + } + + saveWorkspace(onStorage: boolean) { const jsonWorkspace = Blockly.serialization.workspaces.save(this.workspace); if (onStorage && this.modeService.getMode() === 'teacher') this.toolbox().contents.shift(); const jsonToolbox = JSON.stringify(this.toolbox()); @@ -184,16 +207,6 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { if (onStorage) this.activityService.updateActivity(this.activityId()!, this.activity()!); } - private resizeBlockly(): void { - const blocklyArea = this.blocklyArea.nativeElement; - const blocklyDiv = this.blocklyDiv.nativeElement; - - // Ensure Blockly workspace resizes properly with the area - blocklyDiv.style.width = `${blocklyArea.offsetWidth}px`; - blocklyDiv.style.height = `${blocklyArea.offsetHeight}px`; - Blockly.svgResize(this.workspace); // Resize the workspace after adjusting the div size - } - private initCanvas() { this.ctx = this.canvas.nativeElement.getContext('2d'); From 25277935580f348f4fcefd690130e0a70cca58c6 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:31:31 +0200 Subject: [PATCH 21/40] [FIX]: fixed bug in the disabling button option --- src/app/components/button/button.component.html | 2 +- src/app/components/button/button.component.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/components/button/button.component.html b/src/app/components/button/button.component.html index 2367191..d971bbb 100644 --- a/src/app/components/button/button.component.html +++ b/src/app/components/button/button.component.html @@ -4,7 +4,7 @@ buttonStyle(), disabled ? 'opacity-50 cursor-not-allowed pointer-events-none' : '' ]" - [disabled]="disabled" + (click)="onClick()" > {{ buttonText() }} diff --git a/src/app/components/button/button.component.ts b/src/app/components/button/button.component.ts index b1b4ed4..830d48a 100644 --- a/src/app/components/button/button.component.ts +++ b/src/app/components/button/button.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, Component, computed, inject, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, computed, EventEmitter, inject, Input, Output} from '@angular/core'; import {ModeService} from '../../services/mode.service'; import {NgClass} from '@angular/common'; @@ -19,6 +19,12 @@ export class ButtonComponent { @Input() teacherStyle: string = ''; @Input() disabled: boolean = false; + @Output() clicked = new EventEmitter(); + buttonText = computed(() => (this.modeService.getMode() === 'student' ? this.studentText : this.teacherText)); buttonStyle = computed(() => (this.modeService.getMode() === 'student' ? this.studentStyle : this.teacherStyle)); + + protected onClick() { + if (!this.disabled) this.clicked.emit(); + } } From d6c7d9f3f4cf658193267984020181a6f08e72d0 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:31:57 +0200 Subject: [PATCH 22/40] [UPDATE]: added dependency js-interpreter --- package.json | 1 + pnpm-lock.yaml | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6a5b773..47129d6 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser-dynamic": "^19.2.3", "@angular/router": "^19.2.3", "blockly": "^11.2.2", + "js-interpreter": "^6.0.1", "rxjs": "~7.8.2", "tslib": "^2.8.1", "zone.js": "~0.15.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e81c1bf..13c2390 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: blockly: specifier: ^11.2.2 version: 11.2.2 + js-interpreter: + specifier: ^6.0.1 + version: 6.0.1 rxjs: specifier: ~7.8.2 version: 7.8.2 @@ -3520,6 +3523,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + js-interpreter@6.0.1: + resolution: {integrity: sha512-XfPw6y1FzFwHcGYB62jzPUoSCoCSIL+dICMjRJx6f8V/AmTczeodDOaVxWc4GU4p7qeN7ieuMXNKxScoaBkJ6A==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -9326,6 +9333,10 @@ snapshots: jiti@1.21.7: {} + js-interpreter@6.0.1: + dependencies: + minimist: 1.2.8 + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -9679,8 +9690,7 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimist@1.2.8: - optional: true + minimist@1.2.8: {} minipass-collect@2.0.1: dependencies: From fbb1de00746e9d09e4251c34c49ebc0be090931d Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:33:16 +0200 Subject: [PATCH 23/40] [UPDATE]: corrected disabling button bug --- src/app/components/blocks-modal/blocks-modal.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/blocks-modal/blocks-modal.component.html b/src/app/components/blocks-modal/blocks-modal.component.html index cdc14e1..39c14f2 100644 --- a/src/app/components/blocks-modal/blocks-modal.component.html +++ b/src/app/components/blocks-modal/blocks-modal.component.html @@ -15,7 +15,7 @@

Select the blocks

{{BLOCKS_LIMIT.get(block) || 0}}

From da689649601e1e0e56c7d9c85fb64bd71fd5aa38 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:35:41 +0200 Subject: [PATCH 24/40] [UPDATE]: renamed movement_forward block field --- assets/blocks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/blocks.json b/assets/blocks.json index 0d34aa3..37fbace 100644 --- a/assets/blocks.json +++ b/assets/blocks.json @@ -112,7 +112,7 @@ "args0": [ { "type": "field_number", - "name": "forward", + "name": "steps", "value": 0 }, { From 58a7f19f4dec0fe20fb790c251f5e035c0cbc9d5 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:36:25 +0200 Subject: [PATCH 25/40] [NEW]: created service to initialize and generate the code for custom blocks --- src/app/app.component.ts | 8 ++--- src/app/services/blockly.service.ts | 55 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/app/services/blockly.service.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 12740b4..0034ae9 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,8 +2,7 @@ import {Component} from '@angular/core'; import {RouterOutlet} from '@angular/router'; import {HeaderComponent} from './layout/header/header.component'; import {FooterComponent} from './layout/footer/footer.component'; -import * as Blockly from 'blockly'; -import blocks from '../../assets/blocks.json'; +import {BlocklyService} from './services/blockly.service'; @Component({ selector: 'blearn-root', @@ -12,7 +11,8 @@ import blocks from '../../assets/blocks.json'; styleUrl: './app.component.css' }) export class AppComponent { - constructor() { - Blockly.defineBlocksWithJsonArray(blocks); + constructor(blocklyService: BlocklyService) { + blocklyService.defineCustomBlocks(); + blocklyService.defineCodeGenerationForCustomBlocks(); } } diff --git a/src/app/services/blockly.service.ts b/src/app/services/blockly.service.ts new file mode 100644 index 0000000..1931809 --- /dev/null +++ b/src/app/services/blockly.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import * as Blockly from 'blockly'; +import blocks from '../../../assets/blocks.json'; +import {javascriptGenerator} from 'blockly/javascript'; + +@Injectable({ + providedIn: 'root' +}) +export class BlocklyService { + defineCustomBlocks() { + Blockly.defineBlocksWithJsonArray(blocks); + } + + defineCodeGenerationForCustomBlocks() { + javascriptGenerator.forBlock['event_start'] = function () { + return `// Start of the program\n`; + }; + + javascriptGenerator.forBlock['movement_jump'] = function (block: any) { + const x = block.getFieldValue('x'); + const y = block.getFieldValue('y'); + return `moveTo(${x}, ${y});\n`; + }; + + javascriptGenerator.forBlock['movement_forward'] = function(block: any) { + const steps = block.getFieldValue('steps'); + return `moveForward(${steps});\n`; + }; + + javascriptGenerator.forBlock['movement_set_direction'] = function(block: any){ + const angle = block.getFieldValue('angle'); + return `setDirection(${angle});\n`; + }; + + javascriptGenerator.forBlock['movement_turn_left'] = function (block: any) { + const angle = block.getFieldValue('angle'); + return `turnLeft(${angle});\n`; + }; + + javascriptGenerator.forBlock['movement_turn_right'] = function (block: any) { + const angle = block.getFieldValue('angle'); + return `turnRight(${angle});\n`; + }; + + javascriptGenerator.forBlock['controls_wait'] = function(block: any) { + const seconds = block.getFieldValue('seconds'); + return `waitSeconds(${seconds});\n`; + }; + + javascriptGenerator.forBlock['controls_repeat_forever'] = function(block: any) { + const innerCode = javascriptGenerator.statementToCode(block, 'statement'); + return `while (true) {\n${innerCode}}\n`; + } + } +} From 4eeffe17eae25785dd672d4278d45314ab8ff5b5 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:38:10 +0200 Subject: [PATCH 26/40] [NEW]: created custom module to avoid typescript errors when using js-interpreter --- src/js-interpreter.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/js-interpreter.d.ts diff --git a/src/js-interpreter.d.ts b/src/js-interpreter.d.ts new file mode 100644 index 0000000..295cfac --- /dev/null +++ b/src/js-interpreter.d.ts @@ -0,0 +1,11 @@ +declare module 'js-interpreter' { + class Interpreter { + constructor(code: string, initFunc?: (interpreter: Interpreter, globalObject: any) => void); + step(): boolean; + setProperty(obj: any, prop: string, value: any): void; + createNativeFunction(fn: Function): any; + createAsyncFunction(fn: Function): any; + } + + export = Interpreter; +} From bcbe2c7b038518f9cdf7c8b02faa8cca9461d4d2 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:42:34 +0200 Subject: [PATCH 27/40] [NEW]: created new component where the task work will be represented --- src/app/components/scene/scene.component.html | 22 +++ src/app/components/scene/scene.component.ts | 125 ++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 src/app/components/scene/scene.component.html create mode 100644 src/app/components/scene/scene.component.ts diff --git a/src/app/components/scene/scene.component.html b/src/app/components/scene/scene.component.html new file mode 100644 index 0000000..1f0e300 --- /dev/null +++ b/src/app/components/scene/scene.component.html @@ -0,0 +1,22 @@ +
+
+ + +
+ + +
diff --git a/src/app/components/scene/scene.component.ts b/src/app/components/scene/scene.component.ts new file mode 100644 index 0000000..110a4c9 --- /dev/null +++ b/src/app/components/scene/scene.component.ts @@ -0,0 +1,125 @@ +import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, signal, ViewChild} from '@angular/core'; +import {SceneObject} from '../../models/SceneObject'; +import {ButtonComponent} from '../button/button.component'; + +@Component({ + selector: 'blearn-scene', + imports: [ + ButtonComponent + ], + templateUrl: './scene.component.html', +}) +export class SceneComponent implements AfterViewInit { + @ViewChild('canvas') canvas!: ElementRef; + @ViewChild('scene') scene!: ElementRef; + + @Input() isRunning = signal(false); + @Output() runCode = new EventEmitter(); + @Output() stopCode = new EventEmitter(); + + private ctx: CanvasRenderingContext2D | null = null; + private sceneObjects: Array = []; + private draggingObject: SceneObject | null = null; + private offsetX: number = 0; + private offsetY: number = 0; + + ngAfterViewInit(): void { + this.initCanvas(); + } + + private initCanvas() { + this.ctx = this.canvas.nativeElement.getContext('2d'); + + // Initialize the canvas size based on the scene + this.canvas.nativeElement.width = this.scene.nativeElement.offsetWidth; + this.canvas.nativeElement.height = this.scene.nativeElement.offsetHeight; + + // Create an image object + const img = new Image(); + img.src = 'https://avatars.githubusercontent.com/u/105555875?v=4'; // Replace with your image URL + img.onload = () => { + // Once the image is loaded, draw it to the canvas + this.sceneObjects.push({img, x: 50, y: 50, rotation: 0, width: 100, height: 100}); + this.drawImages(); + }; + + // Set up mouse event listeners for dragging + this.setupMouseEvents(); + } + + private setupMouseEvents() { + this.canvas.nativeElement.addEventListener('mousedown', (e: MouseEvent) => { + const mouseX = e.offsetX; + const mouseY = e.offsetY; + + // Check if the mouse click is on one of the images + for (let sceneObj of this.sceneObjects) { + if (mouseX >= sceneObj.x && mouseX <= sceneObj.x + sceneObj.width && mouseY >= sceneObj.y && mouseY <= sceneObj.y + sceneObj.height) { + this.draggingObject = sceneObj; + this.offsetX = mouseX - sceneObj.x; + this.offsetY = mouseY - sceneObj.y; + } + } + }); + + this.canvas.nativeElement.addEventListener('mousemove', (e: MouseEvent) => { + if (this.draggingObject) { + const mouseX = e.offsetX; + const mouseY = e.offsetY; + + // Move the image based on mouse position + this.draggingObject.x = mouseX - this.offsetX; + this.draggingObject.y = mouseY - this.offsetY; + + this.drawImages(); + } + }); + + this.canvas.nativeElement.addEventListener('mouseup', () => { + this.draggingObject = null; + }); + + this.canvas.nativeElement.addEventListener('mouseleave', () => { + this.draggingObject = null; + }); + } + + private drawImages() { + if (!this.ctx) return; + + // Clear the canvas + this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height); + + // Draw each image on the canvas + for (let imgObj of this.sceneObjects) { + this.ctx.drawImage(imgObj.img, imgObj.x, imgObj.y, imgObj.width, imgObj.height); + } + } + + moveTo(x: number, y: number) { + if (this.sceneObjects.length > 0) { + const obj = this.sceneObjects[0]; + obj.x = x; + obj.y = y; + this.drawImages(); + } + } + + moveForward(steps: number) { + const obj = this.sceneObjects[0]; + obj.x += steps; + this.drawImages(); + } + + setDirection(angle: number) { + + } + + turnLeft(angle: number) { + + } + + turnRight(angle: number) { + + } +} From 034ef64f0c394f423078ed4315f9cb83586959e2 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:42:51 +0200 Subject: [PATCH 28/40] [NEW]: created new model to represent objects in a scene --- src/app/models/SceneObject.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/app/models/SceneObject.ts diff --git a/src/app/models/SceneObject.ts b/src/app/models/SceneObject.ts new file mode 100644 index 0000000..0fdd024 --- /dev/null +++ b/src/app/models/SceneObject.ts @@ -0,0 +1,8 @@ +export interface SceneObject { + img: HTMLImageElement; + x: number; + y: number; + rotation: number; + width: number; + height: number; +} From 74c47f401ad0185027d1c08c2d4082f4ecf71fcd Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:44:50 +0200 Subject: [PATCH 29/40] [UPDATE]: added Scene component --- .../pages/activity-detail/activity-detail.component.html | 9 ++++++--- .../pages/activity-detail/activity-detail.component.ts | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/pages/activity-detail/activity-detail.component.html b/src/app/pages/activity-detail/activity-detail.component.html index 0ebf5ea..4d04035 100644 --- a/src/app/pages/activity-detail/activity-detail.component.html +++ b/src/app/pages/activity-detail/activity-detail.component.html @@ -48,9 +48,12 @@ (saveWorkspace)="saveWorkspace(false)" />
-
- -
+
diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index 75e2a8d..6147c56 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -31,6 +31,7 @@ import {NgClass} from '@angular/common'; TitleComponent, ButtonComponent, NgClass, + SceneComponent, ], templateUrl: './activity-detail.component.html', }) @@ -41,6 +42,7 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { private router = inject(Router); @ViewChild(BlocklyEditorComponent) blocklyEditorComponent!: BlocklyEditorComponent; + @ViewChild(SceneComponent) sceneComponent!: SceneComponent; @ViewChild('modalHost', {read: ViewContainerRef}) modalHost!: ViewContainerRef; @ViewChild('scene') scene!: ElementRef; @ViewChild('canvas') canvas!: ElementRef; From 48bcfcc9862d203101d45b47d26d6887a57dce65 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 11:45:28 +0200 Subject: [PATCH 30/40] [UPDATE]: implemented js-interpreter to execute the generated code --- .../activity-detail.component.ts | 119 ++++++++---------- 1 file changed, 51 insertions(+), 68 deletions(-) diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index 6147c56..3ae1099 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -22,6 +22,9 @@ import * as Blockly from 'blockly'; import {BlocksModalComponent} from '../../components/blocks-modal/blocks-modal.component'; import {DescriptionModalComponent} from '../../components/description-modal/description-modal.component'; import {NgClass} from '@angular/common'; +import {SceneComponent} from '../../components/scene/scene.component'; +import {javascriptGenerator} from 'blockly/javascript'; +import Interpreter from 'js-interpreter'; @Component({ selector: 'blearn-activity-detail', @@ -47,12 +50,6 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { @ViewChild('scene') scene!: ElementRef; @ViewChild('canvas') canvas!: ElementRef; - private ctx: CanvasRenderingContext2D | null = null; - private images: Array<{ img: HTMLImageElement, x: number, y: number, width: number, height: number }> = []; - private draggingImage: any = null; - private offsetX: number = 0; - private offsetY: number = 0; - protected workspace!: Blockly.WorkspaceSvg; protected toolbox = signal({ kind: 'flyoutToolbox', @@ -64,6 +61,9 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { protected readonly BLOCK_LIMITS: Map; + private interpreter: Interpreter | null = null; + protected isRunning = signal(false); + private activityId = toSignal( this.route.paramMap.pipe( map(params => params.get('id') || null) @@ -95,7 +95,6 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { ngAfterViewInit(): void { this.workspace = this.blocklyEditorComponent.getWorkspace(); - this.initCanvas(); } ngOnDestroy(): void { @@ -184,14 +183,14 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { protected updateDueDate(newDueDate: string) { if (this.activity()) { - this.activity.set({...this.activity()!, dueDate: newDueDate }); + this.activity.set({...this.activity()!, dueDate: newDueDate}); this.activityService.updateActivity(this.activityId()!, this.activity()!); } } protected updateDescription(newDescription: string) { if (this.activity()) { - this.activity.set({...this.activity()!, description: newDescription }); + this.activity.set({...this.activity()!, description: newDescription}); this.activityService.updateActivity(this.activityId()!, this.activity()!); } } @@ -209,73 +208,57 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { if (onStorage) this.activityService.updateActivity(this.activityId()!, this.activity()!); } - private initCanvas() { - this.ctx = this.canvas.nativeElement.getContext('2d'); - - // Initialize the canvas size based on the scene - this.canvas.nativeElement.width = this.scene.nativeElement.offsetWidth; - this.canvas.nativeElement.height = this.scene.nativeElement.offsetHeight; - - // Create an image object - const img = new Image(); - img.src = 'https://avatars.githubusercontent.com/u/105555875?v=4'; // Replace with your image URL - img.onload = () => { - // Once the image is loaded, draw it to the canvas - this.images.push({img, x: 50, y: 50, width: 100, height: 100}); - this.drawImages(); - }; - - // Set up mouse event listeners for dragging - this.setupMouseEvents(); + runInterpreter(code: string) { + this.interpreter = new Interpreter(code, this.initApi.bind(this)); + this.stepExecution(); } - private setupMouseEvents() { - this.canvas.nativeElement.addEventListener('mousedown', (e: MouseEvent) => { - const mouseX = e.offsetX; - const mouseY = e.offsetY; - - // Check if the mouse click is on one of the images - for (let imgObj of this.images) { - if (mouseX >= imgObj.x && mouseX <= imgObj.x + imgObj.width && mouseY >= imgObj.y && mouseY <= imgObj.y + imgObj.height) { - this.draggingImage = imgObj; - this.offsetX = mouseX - imgObj.x; - this.offsetY = mouseY - imgObj.y; - } - } - }); - - this.canvas.nativeElement.addEventListener('mousemove', (e: MouseEvent) => { - if (this.draggingImage) { - const mouseX = e.offsetX; - const mouseY = e.offsetY; - - // Move the image based on mouse position - this.draggingImage.x = mouseX - this.offsetX; - this.draggingImage.y = mouseY - this.offsetY; + stepExecution() { + if (!this.interpreter || !this.isRunning()) return; - this.drawImages(); - } - }); - - this.canvas.nativeElement.addEventListener('mouseup', () => { - this.draggingImage = null; - }); - - this.canvas.nativeElement.addEventListener('mouseleave', () => { - this.draggingImage = null; - }); + const hasMoreCode = this.interpreter.step(); + if (hasMoreCode) { + setTimeout(() => this.stepExecution(), 0.5); + } else console.log('Execution finished'); } + initApi(interpreter: Interpreter, globalObject: any) { + const addFunction = (name: string, fn: Function) => { + interpreter.setProperty( + globalObject, + name, + interpreter.createNativeFunction(fn.bind(this.sceneComponent)) + ); + }; - private drawImages() { - if (!this.ctx) return; + addFunction('moveForward', this.sceneComponent.moveForward); + addFunction('moveTo', this.sceneComponent.moveTo); + addFunction('setDirection', this.sceneComponent.setDirection) + addFunction('turnLeft', this.sceneComponent.turnLeft); + addFunction('turnRight', this.sceneComponent.turnRight); + + interpreter.setProperty( + globalObject, + 'waitSeconds', + interpreter.createAsyncFunction((seconds: number, callback: Function) => { + setTimeout(() => callback(), seconds * 1000); + }) + ); + } - // Clear the canvas - this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height); + protected onRunCode() { + javascriptGenerator.init(this.workspace); + const startBlock = this.workspace.getTopBlocks(true) + .find(block => block.type === 'event_start'); - // Draw each image on the canvas - for (let imgObj of this.images) { - this.ctx.drawImage(imgObj.img, imgObj.x, imgObj.y, imgObj.width, imgObj.height); + if (!startBlock) { + alert('You should start your program with the "Start program" block!'); + return; } + const code = javascriptGenerator.blockToCode(startBlock) as string; + console.log(code); + + this.isRunning.set(true); + this.runInterpreter(code); } } From 26c08bcb0f5b64220288bebbd71bca7887527f9e Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 13:51:46 +0200 Subject: [PATCH 31/40] [UPDATE]: moved type declaration to types folder --- src/js-interpreter.d.ts | 11 ----------- src/types/js-interpreter.d.ts | 12 ++++++++++++ tsconfig.app.json | 3 ++- 3 files changed, 14 insertions(+), 12 deletions(-) delete mode 100644 src/js-interpreter.d.ts create mode 100644 src/types/js-interpreter.d.ts diff --git a/src/js-interpreter.d.ts b/src/js-interpreter.d.ts deleted file mode 100644 index 295cfac..0000000 --- a/src/js-interpreter.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare module 'js-interpreter' { - class Interpreter { - constructor(code: string, initFunc?: (interpreter: Interpreter, globalObject: any) => void); - step(): boolean; - setProperty(obj: any, prop: string, value: any): void; - createNativeFunction(fn: Function): any; - createAsyncFunction(fn: Function): any; - } - - export = Interpreter; -} diff --git a/src/types/js-interpreter.d.ts b/src/types/js-interpreter.d.ts new file mode 100644 index 0000000..040f1ab --- /dev/null +++ b/src/types/js-interpreter.d.ts @@ -0,0 +1,12 @@ +declare class Interpreter { + constructor(code: string, initFunc?: (interpreter: Interpreter, globalObject: any) => void); + + step(): boolean; + setProperty(obj: any, prop: string, value: any): void; + createNativeFunction(fn: Function): any; + createAsyncFunction(fn: Function): any; + + stateStack: any[]; + globalObject: any; +} + diff --git a/tsconfig.app.json b/tsconfig.app.json index 3775b37..3bc9b1d 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -10,6 +10,7 @@ "src/main.ts" ], "include": [ - "src/**/*.d.ts" + "src/**/*.d.ts", + "src/types/**.d.ts" ] } From e2ef927e4e121b38ad33d6df350d84b259b5f2f3 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 13:54:08 +0200 Subject: [PATCH 32/40] [UPDATE]: removed npm package js-interpreter --- package.json | 1 - pnpm-lock.yaml | 3 --- 2 files changed, 4 deletions(-) diff --git a/package.json b/package.json index 47129d6..6a5b773 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@angular/platform-browser-dynamic": "^19.2.3", "@angular/router": "^19.2.3", "blockly": "^11.2.2", - "js-interpreter": "^6.0.1", "rxjs": "~7.8.2", "tslib": "^2.8.1", "zone.js": "~0.15.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13c2390..c60ca91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: blockly: specifier: ^11.2.2 version: 11.2.2 - js-interpreter: - specifier: ^6.0.1 - version: 6.0.1 rxjs: specifier: ~7.8.2 version: 7.8.2 From 853517931ed95981619a91e094725354c40ba2cf Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 13:54:35 +0200 Subject: [PATCH 33/40] [NEW]: manually added js-interpreter library --- assets/libs/js-interpreter.js | 143 ++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 assets/libs/js-interpreter.js diff --git a/assets/libs/js-interpreter.js b/assets/libs/js-interpreter.js new file mode 100644 index 0000000..808e797 --- /dev/null +++ b/assets/libs/js-interpreter.js @@ -0,0 +1,143 @@ +/* + + Copyright 2012 Marijn Haverbeke + SPDX-License-Identifier: MIT +*/ +var p; +var ba="undefined"===typeof globalThis?this||window:globalThis,ca=function(a){function b(f){return 48>f?36===f:58>f?!0:65>f?!1:91>f?!0:97>f?95===f:123>f?!0:170<=f&&Kc.test(String.fromCharCode(f))}function d(f){return 65>f?36===f:91>f?!0:97>f?95===f:123>f?!0:170<=f&&Qb.test(String.fromCharCode(f))}function c(f,h){var l=r;for(var n=1,w=0;;){Ta.lastIndex=w;var K=Ta.exec(l);if(K&&K.indexf)++m;else if(47===f)if(f=r.charCodeAt(m+1),42===f){f=void 0;var h=z.Aa&&z.D&&new g,l=m,n=r.indexOf("*/",m+=2);-1===n&&c(m-2,"Unterminated comment");m=n+2;if(z.D)for(Ta.lastIndex=l;(f=Ta.exec(r))&&f.index=f?Rb(!0):(++m,k(Sb));return;case 40:return++m,k(X);case 41:return++m,k(V);case 59:return++m,k(Y);case 44:return++m,k(ea);case 91:return++m,k(db);case 93:return++m,k(eb);case 123:return++m,k(za);case 125:return++m,k(pa);case 58:return++m,k(Aa);case 63:return++m,k(Tb);case 48:if(f=r.charCodeAt(m+1),120===f||88===f){m+=2;f=Ba(16);null===f&&c(I+2,"Expected hexadecimal number");d(r.charCodeAt(m))&&c(m,"Identifier directly after number");k(Ca,f);return}case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:return Rb(!1); +case 34:case 39:m++;for(var h="";;){m>=oa&&c(I,"Unterminated string constant");var l=r.charCodeAt(m);if(l===f){++m;k(Ua,h);break}if(92===l){l=r.charCodeAt(++m);var n=/^[0-7]+/.exec(r.slice(m,m+3));for(n&&(n=n[0]);n&&255=oa)return k(gb);f=r.charCodeAt(m);if(d(f)||92===f)return $b();if(!1===R(f)){f=String.fromCharCode(f);if("\\"===f||Qb.test(f))return $b();c(m,"Unexpected character '"+f+"'")}}function L(f,h){var l=r.slice(m,m+h);m+=h;k(f,l)}function Ub(){for(var f, +h,l=m;;){m>=oa&&c(l,"Unterminated regexp");var n=r.charAt(m);Wa.test(n)&&c(l,"Unterminated regexp");if(f)f=!1;else{if("["===n)h=!0;else if("]"===n&&h)h=!1;else if("/"===n&&!h)break;f="\\"===n}++m}f=r.slice(l,m);++m;(h=ac())&&!/^[gmi]*$/.test(h)&&c(l,"Invalid regexp flag");try{var w=new RegExp(f,h)}catch(K){throw K instanceof SyntaxError&&c(l,K.message),K;}k(bc,w)}function Ba(f,h){for(var l=m,n=0,w=void 0===h?Infinity:h,K=0;K=O? +O-48:Infinity;if(O>=f)break;++m;n=n*f+O}return m===l||void 0!==h&&m-l!==h?null:n}function Rb(f){var h=m,l=!1,n=48===r.charCodeAt(m);f||null!==Ba(10)||c(h,"Invalid number");46===r.charCodeAt(m)&&(++m,Ba(10),l=!0);f=r.charCodeAt(m);if(69===f||101===f)f=r.charCodeAt(++m),43!==f&&45!==f||++m,null===Ba(10)&&c(h,"Invalid number"),l=!0;d(r.charCodeAt(m))&&c(m,"Identifier directly after number");f=r.slice(h,m);var w;l?w=parseFloat(f):n&&1!==f.length?/[89]/.test(f)||S?c(h,"Invalid number"):w=parseInt(f,8): +w=parseInt(f,10);k(Ca,w)}function Va(f){f=Ba(16,f);null===f&&c(I,"Bad character escape sequence");return f}function ac(){qa=!1;for(var f,h=!0,l=m;;){var n=r.charCodeAt(m);if(b(n))qa&&(f+=r.charAt(m)),++m;else if(92===n){qa||(f=r.slice(l,m));qa=!0;117!==r.charCodeAt(++m)&&c(m,"Expecting Unicode escape sequence \\uXXXX");++m;n=Va(4);var w=String.fromCharCode(n);w||c(m-1,"Invalid Unicode escape");(h?d(n):b(n))||c(m-4,"Invalid Unicode escape");f+=w}else break;h=!1}return qa?f:r.slice(l,m)}function $b(){var f= +ac(),h=ra;!qa&&Vc(f)&&(h=Wc[f]);k(h,f)}function B(){hb=I;fa=na;ib=cb;ha()}function jb(f){S=f;m=I;if(z.D)for(;mh){var w=ia(f);w.left=f;w.operator=T;f=x;B();w.right=wb(xb(),n,l);n=y(w,f===Wb||f===Xb?"LogicalExpression":"BinaryExpression");return wb(n,h,l)}return f}function xb(){if(x.prefix){var f= +M(),h=x.Yb;f.operator=T;ya=f.prefix=!0;B();f.J=xb();h?Ya(f.J):S&&"delete"===f.operator&&"Identifier"===f.J.type&&c(f.start,"Deleting local variable in strict mode");return y(f,h?"UpdateExpression":"UnaryExpression")}for(h=Ga(ab());x.ac&&!Xa();)f=ia(h),f.operator=T,f.prefix=!1,f.J=h,Ya(h),B(),h=y(f,"UpdateExpression");return h}function Ga(f,h){if(E(Sb)){var l=ia(f);l.object=f;l.Ya=aa(!0);l.fb=!1;return Ga(y(l,"MemberExpression"),h)}return E(db)?(l=ia(f),l.object=f,l.Ya=N(),l.fb=!0,F(eb),Ga(y(l,"MemberExpression"), +h)):!h&&E(X)?(l=ia(f),l.callee=f,l.arguments=yb(V,!1),Ga(y(l,"CallExpression"),h)):f}function ab(){switch(x){case tc:var f=M();B();return y(f,"ThisExpression");case ra:return aa();case Ca:case Ua:case bc:return f=M(),f.value=T,f.raw=r.slice(I,na),B(),y(f,"Literal");case uc:case vc:case wc:return f=M(),f.value=x.cb,f.raw=x.l,B(),y(f,"Literal");case X:f=fb;var h=I;B();var l=N();l.start=h;l.end=na;z.D&&(l.O.start=f,l.O.end=cb);z.Za&&(l.j=[h,na]);F(V);return l;case db:return f=M(),B(),f.elements=yb(eb, +!0,!0),y(f,"ArrayExpression");case za:f=M();h=!0;l=!1;f.h=[];for(B();!E(pa);){if(h)h=!1;else if(F(ea),z.sb&&E(pa))break;var n={key:x===Ca||x===Ua?ab():aa(!0)},w=!1;if(E(Aa)){n.value=N(!0);var K=n.kind="init"}else"Identifier"!==n.key.type||"get"!==n.key.name&&"set"!==n.key.name?Z():(w=l=!0,K=n.kind=n.key.name,n.key=x===Ca||x===Ua?ab():aa(!0),x!==X&&Z(),n.value=sb(M(),!1));if("Identifier"===n.key.type&&(S||l))for(var O=0;Ol?f.id:f.sa[l],(yc(n.name)||Za(n.name))&&c(n.start,"Defining '"+n.name+"' in strict mode"),0<=l)for(var w=0;w>>0;return b===Number(a)?b:NaN}function Sa(a){var b=a>>>0;return String(b)===String(a)&&4294967295!==b?b:NaN}function ta(a,b,d){b?a.start=b:delete a.start;d?a.end=d:delete a.end;for(var c in a)if(a[c]!==a.O&&a.hasOwnProperty(c)){var e=a[c];e&&"object"===typeof e&&ta(e,b,d)}}t.prototype.REGEXP_MODE=2;t.prototype.REGEXP_THREAD_TIMEOUT=1E3;t.prototype.POLYFILL_TIMEOUT=1E3;p=t.prototype;p.R=!1;p.Ma=!1;p.Ib=0;p.hc=0; +function da(a,b){var d={},c;for(c in va)d[c]=va[c];d.sourceFile=b;return Pa.j.parse(a,d)}p.Hb=function(a){var b=this.j[0];if(!b||"Program"!==b.node.type)throw Error("Expecting original AST to start with a Program node");"string"===typeof a&&(a=da(a,"appendCode"+this.Ib++));if(!a||"Program"!==a.type)throw Error("Expecting new AST to start with a Program node");bb(this,a,b.scope);Array.prototype.push.apply(b.node.body,a.body);b.node.body.lb=null;b.done=!1}; +p.nb=function(){var a=this.j,b;do{var d=a[a.length-1];if(this.wa)break;else if(!d||"Program"===d.node.type&&d.done){if(!this.$.length)return!1;d=this.$[0];if(!d||d.time>Date.now())d=null;else{this.$.shift();0<=d.j&&Ab(this,d,d.j);var c=new u(d.node,d.scope);d.o&&(c.ma=2,c.C=this.Qa,c.aa=d.o,c.Ta=!0,c.G=d.A);d=c}if(!d)break}c=d.node;var e=Oa;Oa=this;try{var g=this.rb[c.type](a,d,c)}catch(k){if(k!==Ia)throw this.value!==k&&(this.value=void 0),k;}finally{Oa=e}g&&a.push(g);if(this.R)throw this.value= +void 0,Error("Getter not supported in this context");if(this.Ma)throw this.value=void 0,Error("Setter not supported in this context");b||c.end||(b=Date.now()+this.POLYFILL_TIMEOUT)}while(!c.end&&b>Date.now());return!0};p.Cb=function(){for(;!this.wa&&this.nb(););return this.wa};p.Wb=function(){if(this.wa)return ua.ASYNC;var a=this.j;return!(a=a[a.length-1])||"Program"===a.node.type&&a.done?(a=this.$[0])?a.time>Date.now()?ua.TASK:ua.STEP:ua.DONE:ua.STEP}; +function Bb(a,b){a.g(b,"NaN",NaN,xa);a.g(b,"Infinity",Infinity,xa);a.g(b,"undefined",void 0,xa);a.g(b,"window",b,wa);a.g(b,"this",b,xa);a.g(b,"self",b);a.L=new D(null);a.X=new D(a.L);Cb(a,b);Db(a,b);b.Ca=a.L;a.g(b,"constructor",a.u,v);Eb(a,b);Fb(a,b);Gb(a,b);Hb(a,b);Ib(a,b);Jb(a,b);Kb(a,b);Lb(a,b);Mb(a,b);var d=a.i(function(){throw EvalError("Can't happen");},!1);d.eval=!0;a.g(b,"eval",d,v);a.g(b,"parseInt",a.i(parseInt,!1),v);a.g(b,"parseFloat",a.i(parseFloat,!1),v);a.g(b,"isNaN",a.i(isNaN,!1),v); +a.g(b,"isFinite",a.i(isFinite,!1),v);for(var c=[[escape,"escape"],[unescape,"unescape"],[decodeURI,"decodeURI"],[decodeURIComponent,"decodeURIComponent"],[encodeURI,"encodeURI"],[encodeURIComponent,"encodeURIComponent"]],e=0;e>>0;if(!b||0>b)c.length=0;else{b--;var d=c[b];delete c[b];c.length=b;return d}});g("push",function(c){if(!this)throw TypeError();for(var b=Object(this),d=b.length>>>0,a=0;a>>0;if(!b||0>b)c.length=0;else{for(var d=c[0],a=0;a>>0;if(!d||0>d)d=0;for(var a=d-1;0<=a;a--)a in b?b[a+arguments.length]=b[a]:delete b[a+arguments.length];for(a=0;a>>0;if(!b||2>b)return c;for(var d=0;d>>0;b|=0;if(!a||b>=a)return-1;for(b=Math.max(0<=b?b:a-Math.abs(b),0);b>>0;if(!a)return-1;var e=a-1;1>>0;c|=0;c=0<=c?c:Math.max(0,a+c);"undefined"!==typeof b?(Infinity!==b&&(b|=0),b=0>b?a+b:Math.min(b,a)):b=a;b-=c;a=Array(b);for(var e=0;e>>0;c|=0;c=0>c?Math.max(e+\nc,0):Math.min(c,e);b=2>arguments.length?e-c:Math.max(0,Math.min(b|0,e-c));for(var h=[],f=c;f=c;f--)f in a?a[f+k]=a[f]:delete a[f+k];e+=k;for(f=2;f>>0;c="undefined"===typeof c?",":""+c;for(var a="",e=0;e>>0;for(1>>0,e=[],h=2<=arguments.length?arguments[1]:void 0,f=0;f>>0;for(1>>0;1>>0,a=0;if(2===arguments.length)var e=arguments[1];else{for(;a=d)throw TypeError("Reduce of empty array with no initial value");e=b[a++]}for(;a>>0)-1;if(2<=arguments.length)var a=arguments[1];else{for(;0<=d&&!(d in b);)d--;if(0>d)throw TypeError("Reduce of empty array with no initial value");a=b[d--]}for(;0<=d;d--)d in b&&(a=c(a,b[d],d,b));return a});g("some",function(c){if(!this||"function"!==typeof c)throw TypeError();for(var b=Object(this),d=b.length>>>0,a=2<=arguments.length?arguments[1]:void 0,e=0;eString(this[a+1])){var e=this[a],h=a in this;a+1 in this?this[a]=this[a+1]:delete this[a];h?this[a+1]=e:delete this[a+1];d++}if(!d)break}return this});g("toLocaleString",function(){if(!this)throw TypeError();for(var c=Object(this),b=c.length>>>0,d=[],a=0;ab.charCodeAt(0)&&P(this,a,this.F)){var d=Sa(b);if(!isNaN(d)&&d>=":c>>=e;break;case ">>>=":c>>>=e;break;case "&=":c&=e;break;case "^=":c^=e;break;case "|=":c|=e;break;default:throw SyntaxError("Unknown assignment expression: "+d.operator);}if(d=hd(this,b.Ia,c))return b.ya=!0,b.kb=c,ld(this,d,b.Ia,c);a.pop();a[a.length-1].value=c}}; +t.prototype.stepBinaryExpression=function(a,b,d){if(!b.na)return b.na=!0,new u(d.left,b.scope);if(!b.Ga)return b.Ga=!0,b.qa=b.value,new u(d.right,b.scope);a.pop();var c=b.qa;b=b.value;switch(d.operator){case "==":d=c==b;break;case "!=":d=c!=b;break;case "===":d=c===b;break;case "!==":d=c!==b;break;case ">":d=c>b;break;case ">=":d=c>=b;break;case "<":d=c>":d=c>>b;break;case ">>>":d=c>>>b;break;case "in":b instanceof D||H(this,this.o,"'in' expects an object, not '"+b+"'");d=ad(this,b,c);break;case "instanceof":P(this,b,this.P)||H(this,this.o,"'instanceof' expects an object, not '"+b+"'");d=c instanceof D?P(this,c,b):!1;break;default:throw SyntaxError("Unknown binary operator: "+d.operator);}a[a.length-1].value=d}; +t.prototype.stepBlockStatement=function(a,b,d){var c=b.B||0;if(d=d.body[c])return b.B=c+1,new u(d,b.scope);a.pop()};t.prototype.stepBreakStatement=function(a,b,d){id(this,1,void 0,d.label&&d.label.name)};t.prototype.Fb=0; +t.prototype.stepCallExpression=function(a,b,d){if(!b.ma){b.ma=1;var c=new u(d.callee,b.scope);c.xa=!0;return c}if(1===b.ma){b.ma=2;var e=b.value;if(Array.isArray(e)){if(b.aa=gd(this,e),e[0]===Ja?b.Mb="eval"===e[1]:b.C=e[0],e=b.aa,this.R)return b.ma=1,kd(this,e,b.value)}else b.aa=e;b.G=[];b.B=0}e=b.aa;if(!b.Ta){0!==b.B&&b.G.push(b.value);if(d.arguments[b.B])return new u(d.arguments[b.B++],b.scope);if("NewExpression"===d.type){e instanceof D&&!e.zb||H(this,this.o,Q(this,d.callee)+" is not a constructor"); +if(e===this.ua)b.C=Cc(this);else{var g=e.h.prototype;if("object"!==typeof g||null===g)g=this.L;b.C=this.s(g)}b.isConstructor=!0}b.Ta=!0}if(b.hb)a.pop(),a[a.length-1].value=b.isConstructor&&"object"!==typeof b.value?b.C:b.value;else{b.hb=!0;e instanceof D||H(this,this.o,Q(this,d.callee)+" is not a function");if(a=e.node){d=ja(this,a.body,e.Xa);c=Cc(this);for(e=0;ee?b.G[e]: +void 0);d.U||(b.C=md(this,b.C));this.g(d.object,"this",b.C,wa);b.value=void 0;return new u(a.body,d)}if(e.eval)if(e=b.G[0],"string"!==typeof e)b.value=e;else{try{c=da(String(e),"eval"+this.Fb++)}catch(q){H(this,this.Y,"Invalid code: "+q.message)}e=this.Da();e.type="EvalProgram_";e.body=c.body;ta(e,d.start,d.end);d=b.Mb?b.scope:this.M;d.U?d=ja(this,c,d):bb(this,c,d);this.value=void 0;return new u(e,d)}else if(e.Va)b.scope.U||(b.C=md(this,b.C)),b.value=e.Va.apply(b.C,b.G);else if(e.bb){var k=this;c= +e.bb.length-1;c=b.G.concat(Array(c)).slice(0,c);c.push(function(q){b.value=q;k.wa=!1});this.wa=!0;b.scope.U||(b.C=md(this,b.C));e.bb.apply(b.C,c)}else H(this,this.o,Q(this,d.callee)+" is not callable")}}; +t.prototype.stepConditionalExpression=function(a,b,d){var c=b.ra||0;if(0===c)return b.ra=1,new u(d.test,b.scope);if(1===c){b.ra=2;if((c=!!b.value)&&d.fa)return new u(d.fa,b.scope);if(!c&&d.alternate)return new u(d.alternate,b.scope);this.value=void 0}a.pop();"ConditionalExpression"===d.type&&(a[a.length-1].value=b.value)};t.prototype.stepContinueStatement=function(a,b,d){id(this,2,void 0,d.label&&d.label.name)};t.prototype.stepDebuggerStatement=function(a){a.pop()}; +t.prototype.stepDoWhileStatement=function(a,b,d){"DoWhileStatement"===d.type&&void 0===b.ka&&(b.value=!0,b.ka=!0);if(!b.ka)return b.ka=!0,new u(d.test,b.scope);if(!b.value)a.pop();else if(d.body)return b.ka=!1,b.ca=!0,new u(d.body,b.scope)};t.prototype.stepEmptyStatement=function(a){a.pop()};t.prototype.stepEvalProgram_=function(a,b,d){var c=b.B||0;if(d=d.body[c])return b.B=c+1,new u(d,b.scope);a.pop();a[a.length-1].value=this.value}; +t.prototype.stepExpressionStatement=function(a,b,d){if(!b.oa)return this.value=void 0,b.oa=!0,new u(d.pa,b.scope);a.pop();this.value=b.value}; +t.prototype.stepForInStatement=function(a,b,d){if(!b.Rb&&(b.Rb=!0,d.left.ia&&d.left.ia[0].za))return b.scope.U&&H(this,this.Y,"for-in loop variable declaration may not have an initializer"),new u(d.left,b.scope);if(!b.Fa)return b.Fa=!0,b.ta||(b.ta=b.value),new u(d.right,b.scope);b.ca||(b.ca=!0,b.v=b.value,b.mb=Object.create(null));if(void 0===b.Ua)a:for(;;){if(b.v instanceof D)for(b.Ba||(b.Ba=Object.getOwnPropertyNames(b.v.h));;){var c=b.Ba.shift();if(void 0===c)break;if(Object.prototype.hasOwnProperty.call(b.v.h, +c)&&!b.mb[c]&&(b.mb[c]=!0,Object.prototype.propertyIsEnumerable.call(b.v.h,c))){b.Ua=c;break a}}else if(null!==b.v&&void 0!==b.v)for(b.Ba||(b.Ba=Object.getOwnPropertyNames(b.v));;){c=b.Ba.shift();if(void 0===c)break;b.mb[c]=!0;if(Object.prototype.propertyIsEnumerable.call(b.v,c)){b.Ua=c;break a}}b.v=Bc(this,b.v);b.Ba=null;if(null===b.v){a.pop();return}}if(!b.wb)if(b.wb=!0,a=d.left,"VariableDeclaration"===a.type)b.ta=[Ja,a.ia[0].id.name];else return b.ta=null,b=new u(a,b.scope),b.xa=!0,b;b.ta||(b.ta= +b.value);if(!b.ya&&(b.ya=!0,a=b.Ua,c=hd(this,b.ta,a)))return ld(this,c,b.ta,a);b.Ua=void 0;b.wb=!1;b.ya=!1;if(d.body)return new u(d.body,b.scope)};t.prototype.stepForStatement=function(a,b,d){switch(b.ra){default:b.ra=1;if(d.za)return new u(d.za,b.scope);break;case 1:b.ra=2;if(d.test)return new u(d.test,b.scope);break;case 2:b.ra=3;if(d.test&&!b.value)a.pop();else return b.ca=!0,new u(d.body,b.scope);break;case 3:if(b.ra=1,d.update)return new u(d.update,b.scope)}}; +t.prototype.stepFunctionDeclaration=function(a){a.pop()};t.prototype.stepFunctionExpression=function(a,b,d){a.pop();b=a[a.length-1];a=b.scope;d.id&&(a=dd(this,a));b.value=Pb(this,d,a,b.Sa);d.id&&this.g(a.object,d.id.name,b.value,wa)};t.prototype.stepIdentifier=function(a,b,d){a.pop();if(b.xa)a[a.length-1].value=[Ja,d.name];else{b=ed(this,d.name);if(this.R)return kd(this,b,this.Qa);a[a.length-1].value=b}};t.prototype.stepIfStatement=t.prototype.stepConditionalExpression; +t.prototype.stepLabeledStatement=function(a,b,d){a.pop();a=b.labels||[];a.push(d.label.name);b=new u(d.body,b.scope);b.labels=a;return b};t.prototype.stepLiteral=function(a,b,d){a.pop();b=d.value;b instanceof RegExp&&(d=this.s(this.Pa),Ic(this,d,b),b=d);a[a.length-1].value=b}; +t.prototype.stepLogicalExpression=function(a,b,d){if("&&"!==d.operator&&"||"!==d.operator)throw SyntaxError("Unknown logical operator: "+d.operator);if(!b.na)return b.na=!0,new u(d.left,b.scope);if(b.Ga)a.pop(),a[a.length-1].value=b.value;else if("&&"===d.operator&&!b.value||"||"===d.operator&&b.value)a.pop(),a[a.length-1].value=b.value;else return b.Ga=!0,new u(d.right,b.scope)}; +t.prototype.stepMemberExpression=function(a,b,d){if(!b.Fa)return b.Fa=!0,new u(d.object,b.scope);if(d.fb)if(b.Sb)d=b.value;else return b.v=b.value,b.Sb=!0,new u(d.Ya,b.scope);else b.v=b.value,d=d.Ya.name;a.pop();if(b.xa)a[a.length-1].value=[b.v,d];else{d=this.N(b.v,d);if(this.R)return kd(this,d,b.v);a[a.length-1].value=d}};t.prototype.stepNewExpression=t.prototype.stepCallExpression; +t.prototype.stepObjectExpression=function(a,b,d){var c=b.B||0,e=d.h[c];if(b.v){var g=b.Sa;b.La[g]||(b.La[g]={});b.La[g][e.kind]=b.value;b.B=++c;e=d.h[c]}else b.v=this.s(this.L),b.La=Object.create(null);if(e){var k=e.key;if("Identifier"===k.type)g=k.name;else if("Literal"===k.type)g=k.value;else throw SyntaxError("Unknown object structure: "+k.type);b.Sa=g;return new u(e.value,b.scope)}for(k in b.La)d=b.La[k],"get"in d||"set"in d?this.g(b.v,k,Ka,{configurable:!0,enumerable:!0,get:d.get,set:d.set}): +this.g(b.v,k,d.init);a.pop();a[a.length-1].value=b.v};t.prototype.stepProgram=function(a,b,d){if(a=d.body.shift())return b.done=!1,new u(a,b.scope);b.done=!0};t.prototype.stepReturnStatement=function(a,b,d){if(d.J&&!b.oa)return b.oa=!0,new u(d.J,b.scope);id(this,3,b.value)};t.prototype.stepSequenceExpression=function(a,b,d){var c=b.B||0;if(d=d.xb[c])return b.B=c+1,new u(d,b.scope);a.pop();a[a.length-1].value=b.value}; +t.prototype.stepSwitchStatement=function(a,b,d){if(!b.ka)return b.ka=1,new u(d.Nb,b.scope);1===b.ka&&(b.ka=2,b.fc=b.value,b.gb=-1);for(;;){var c=b.jb||0,e=d.tb[c];if(b.Ka||!e||e.test)if(e||b.Ka||-1===b.gb)if(e){if(!b.Ka&&!b.Db&&e.test)return b.Db=!0,new u(e.test,b.scope);if(b.Ka||b.value===b.fc){b.Ka=!0;var g=b.B||0;if(e.fa[g])return b.Xb=!0,b.B=g+1,new u(e.fa[g],b.scope)}b.Db=!1;b.B=0;b.jb=c+1}else{a.pop();break}else b.Ka=!0,b.jb=b.gb;else b.gb=c,b.jb=c+1}}; +t.prototype.stepThisExpression=function(a){a.pop();a[a.length-1].value=ed(this,"this")};t.prototype.stepThrowStatement=function(a,b,d){if(b.oa)H(this,b.value);else return b.oa=!0,new u(d.J,b.scope)}; +t.prototype.stepTryStatement=function(a,b,d){if(!b.Ob)return b.Ob=!0,new u(d.block,b.scope);if(b.ha&&4===b.ha.type&&!b.Qb&&d.Ha)return b.Qb=!0,a=dd(this,b.scope),this.g(a.object,d.Ha.Wa.name,b.ha.value),b.ha=void 0,new u(d.Ha.body,a);if(!b.Pb&&d.ib)return b.Pb=!0,new u(d.ib,b.scope);a.pop();b.ha&&id(this,b.ha.type,b.ha.value,b.ha.label)}; +t.prototype.stepUnaryExpression=function(a,b,d){if(!b.oa)return b.oa=!0,a=new u(d.J,b.scope),a.xa="delete"===d.operator,a;a.pop();var c=b.value;switch(d.operator){case "-":c=-c;break;case "+":c=+c;break;case "!":c=!c;break;case "~":c=~c;break;case "delete":d=!0;if(Array.isArray(c)){var e=c[0];e===Ja&&(e=b.scope);c=String(c[1]);try{delete e.h[c]}catch(g){b.scope.U?H(this,this.o,"Cannot delete property '"+c+"' of '"+e+"'"):d=!1}}c=d;break;case "typeof":c=c&&"Function"===c.H?"function":typeof c;break; +case "void":c=void 0;break;default:throw SyntaxError("Unknown unary operator: "+d.operator);}a[a.length-1].value=c}; +t.prototype.stepUpdateExpression=function(a,b,d){if(!b.na)return b.na=!0,a=new u(d.J,b.scope),a.xa=!0,a;b.Ja||(b.Ja=b.value);b.Ea&&(b.qa=b.value);if(!b.Ea){var c=gd(this,b.Ja);b.qa=c;if(this.R)return b.Ea=!0,kd(this,c,b.Ja)}if(b.ya)a.pop(),a[a.length-1].value=b.kb;else{c=Number(b.qa);if("++"===d.operator)var e=c+1;else if("--"===d.operator)e=c-1;else throw SyntaxError("Unknown update expression: "+d.operator);d=d.prefix?e:c;if(c=hd(this,b.Ja,e))return b.ya=!0,b.kb=d,ld(this,c,b.Ja,e);a.pop();a[a.length- +1].value=d}};t.prototype.stepVariableDeclaration=function(a,b,d){d=d.ia;var c=b.B||0,e=d[c];b.Ab&&e&&(fd(this,e.id.name,b.value),b.Ab=!1,e=d[++c]);for(;e;){if(e.za)return b.B=c,b.Ab=!0,b.Sa=e.id.name,new u(e.za,b.scope);e=d[++c]}a.pop()};t.prototype.stepWithStatement=function(a,b,d){if(!b.Fa)return b.Fa=!0,new u(d.object,b.scope);a.pop();a=dd(this,b.scope,b.value);return new u(d.body,a)};t.prototype.stepWhileStatement=t.prototype.stepDoWhileStatement;Pa.Interpreter=t;t.prototype.step=t.prototype.nb; +t.prototype.run=t.prototype.Cb;t.prototype.getStatus=t.prototype.Wb;t.prototype.appendCode=t.prototype.Hb;t.prototype.createObject=t.prototype.ga;t.prototype.createObjectProto=t.prototype.s;t.prototype.createNativeFunction=t.prototype.i;t.prototype.createAsyncFunction=t.prototype.ub;t.prototype.getProperty=t.prototype.N;t.prototype.setProperty=t.prototype.g;t.prototype.nativeToPseudo=t.prototype.S;t.prototype.pseudoToNative=t.prototype.T;t.prototype.getGlobalScope=t.prototype.Ub; +t.prototype.setGlobalScope=t.prototype.cc;t.prototype.getStateStack=t.prototype.Vb;t.prototype.setStateStack=t.prototype.dc;t.Status=ua;t.VALUE_IN_DESCRIPTOR=Ka; From 3e173fdf5a292773f087d8112842a5558db23c8f Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 13:55:50 +0200 Subject: [PATCH 34/40] [UPDATE]: changed some angular configurations no prevent some warnings in the build --- angular.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/angular.json b/angular.json index e987836..6664e17 100644 --- a/angular.json +++ b/angular.json @@ -29,15 +29,23 @@ "styles": [ "src/styles.css" ], - "scripts": [] + "scripts": [ + "assets/libs/js-interpreter.js" + ], + "allowedCommonJsDependencies": [ + "blockly", + "blockly/core", + "blockly/msg/en", + "blockly/blocks" + ] }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" + "maximumWarning": "2MB", + "maximumError": "5MB" }, { "type": "anyComponentStyle", From ec115341e644d810a30afd92435b2951db7a7eca Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 13:56:05 +0200 Subject: [PATCH 35/40] [UPDATE]: deleted js-interpreter import --- src/app/pages/activity-detail/activity-detail.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index 3ae1099..2fffe78 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -24,7 +24,6 @@ import {DescriptionModalComponent} from '../../components/description-modal/desc import {NgClass} from '@angular/common'; import {SceneComponent} from '../../components/scene/scene.component'; import {javascriptGenerator} from 'blockly/javascript'; -import Interpreter from 'js-interpreter'; @Component({ selector: 'blearn-activity-detail', From 5f0ce0f5ffb20141cfd4eec9b33f62d04a51bfc1 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 8 Apr 2025 15:31:36 +0200 Subject: [PATCH 36/40] [UPDATE]: set running status to false when finished executing code --- src/app/pages/activity-detail/activity-detail.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index 2fffe78..cf2820c 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -218,7 +218,10 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy { const hasMoreCode = this.interpreter.step(); if (hasMoreCode) { setTimeout(() => this.stepExecution(), 0.5); - } else console.log('Execution finished'); + } else { + this.isRunning.set(false); + console.log('Execution finished'); + } } initApi(interpreter: Interpreter, globalObject: any) { From 8fdd20425408a7508e528416b418d913c4028f9a Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 15 Apr 2025 13:50:29 +0200 Subject: [PATCH 37/40] [UPDATE]: refactor file name to match with conventions --- src/app/models/{SceneObject.ts => scene-object.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/app/models/{SceneObject.ts => scene-object.ts} (100%) diff --git a/src/app/models/SceneObject.ts b/src/app/models/scene-object.ts similarity index 100% rename from src/app/models/SceneObject.ts rename to src/app/models/scene-object.ts From 21d48406effc3fa9e8c8f7ac6a89de422192a54d Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 15 Apr 2025 13:50:54 +0200 Subject: [PATCH 38/40] [UPDATE]: added button to add new objects to the scene --- src/app/components/scene/scene.component.html | 7 +++++ src/app/components/scene/scene.component.ts | 29 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/app/components/scene/scene.component.html b/src/app/components/scene/scene.component.html index 1f0e300..07091ac 100644 --- a/src/app/components/scene/scene.component.html +++ b/src/app/components/scene/scene.component.html @@ -16,6 +16,13 @@ (clicked)="stopCode.emit()" [disabled]="!isRunning()" /> + @if (modeService.getMode() === 'teacher') { + + }
diff --git a/src/app/components/scene/scene.component.ts b/src/app/components/scene/scene.component.ts index 110a4c9..e44623f 100644 --- a/src/app/components/scene/scene.component.ts +++ b/src/app/components/scene/scene.component.ts @@ -1,6 +1,17 @@ -import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, signal, ViewChild} from '@angular/core'; -import {SceneObject} from '../../models/SceneObject'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + inject, + Input, + Output, + signal, + ViewChild +} from '@angular/core'; +import {SceneObject} from '../../models/scene-object'; import {ButtonComponent} from '../button/button.component'; +import {ModeService} from '../../services/mode.service'; @Component({ selector: 'blearn-scene', @@ -10,6 +21,8 @@ import {ButtonComponent} from '../button/button.component'; templateUrl: './scene.component.html', }) export class SceneComponent implements AfterViewInit { + protected modeService = inject(ModeService); + @ViewChild('canvas') canvas!: ElementRef; @ViewChild('scene') scene!: ElementRef; @@ -18,7 +31,7 @@ export class SceneComponent implements AfterViewInit { @Output() stopCode = new EventEmitter(); private ctx: CanvasRenderingContext2D | null = null; - private sceneObjects: Array = []; + protected sceneObjects: Array = []; private draggingObject: SceneObject | null = null; private offsetX: number = 0; private offsetY: number = 0; @@ -47,6 +60,16 @@ export class SceneComponent implements AfterViewInit { this.setupMouseEvents(); } + protected addObject() { + const img = new Image(); + img.src = 'https://avatars.githubusercontent.com/u/105555875?v=4'; // Replace with your image URL + img.onload = () => { + // Once the image is loaded, draw it to the canvas + this.sceneObjects.push({img, x: 50, y: 50, rotation: 0, width: 100, height: 100}); + this.drawImages(); + }; + } + private setupMouseEvents() { this.canvas.nativeElement.addEventListener('mousedown', (e: MouseEvent) => { const mouseX = e.offsetX; From fb338d477b6aa6202a13218034e43b00bf9ca4ee Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 15 Apr 2025 13:51:16 +0200 Subject: [PATCH 39/40] [UPDATE]: refactor imports --- src/app/pages/activity-detail/activity-detail.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts index cf2820c..ad048c7 100644 --- a/src/app/pages/activity-detail/activity-detail.component.ts +++ b/src/app/pages/activity-detail/activity-detail.component.ts @@ -3,7 +3,8 @@ import { Component, computed, ElementRef, - inject, OnDestroy, + inject, + OnDestroy, signal, ViewChild, ViewContainerRef From 2993cfbf1eeb1a62aaff522fa56a90fb79483857 Mon Sep 17 00:00:00 2001 From: jcasben Date: Tue, 15 Apr 2025 13:52:53 +0200 Subject: [PATCH 40/40] [UPDATE]: deleted unimplemented test --- .../description-modal.component.spec.ts | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 src/app/components/description-modal/description-modal.component.spec.ts diff --git a/src/app/components/description-modal/description-modal.component.spec.ts b/src/app/components/description-modal/description-modal.component.spec.ts deleted file mode 100644 index 4aa74b6..0000000 --- a/src/app/components/description-modal/description-modal.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DescriptionModalComponent } from './description-modal.component'; - -describe('DescriptionModalComponent', () => { - let component: DescriptionModalComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DescriptionModalComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(DescriptionModalComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -});