From 05c7552d0f0e11a6ccb9a24ba7d8b30276b4e14c Mon Sep 17 00:00:00 2001 From: Zhuziexe Date: Wed, 8 Jul 2026 14:21:41 +0800 Subject: [PATCH 1/4] Add an extension --- extensions/Zhuziexe/inputboxes.js | 702 ++++++++++++++++++++++++++++++ extensions/extensions.json | 1 + 2 files changed, 703 insertions(+) create mode 100644 extensions/Zhuziexe/inputboxes.js diff --git a/extensions/Zhuziexe/inputboxes.js b/extensions/Zhuziexe/inputboxes.js new file mode 100644 index 0000000000..964eec594c --- /dev/null +++ b/extensions/Zhuziexe/inputboxes.js @@ -0,0 +1,702 @@ +// Name: Input Boxes +// ID: inputboxes +// Description: A simple way to add better input boxes to your project! +// By: Zhuziexe +// License: MPL-2.0 + +class InputBoxExtension { + constructor() { + this.inputs = new Map(); + this.container = null; + this._lastEnterId = null; + this._resizeTimer = null; + + this._updatePending = false; this._bindDocumentEvents(); + this._bindResizeAndFullscreen(); + + this._observeCanvas(); + this._observeModals(); + + setTimeout(() => this._updateAllInputs(), 100); + } + + _observeModals() { + this._checkModalOpen(); + const observer = new MutationObserver(() => { + this._checkModalOpen(); + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'style'] + }); + this._modalObserver = observer; + } + + _checkModalOpen() { + if (!this.container) return; + const modalSelectors = [ + '.modal', '.ReactModalPortal', '.ReactModal__Overlay', + '.ReactModal__Content', '.modal-content', '.dialog', + '[role="dialog"]', '[role="modal"]' + ]; + let hasModal = false; + for (const sel of modalSelectors) { + if (document.querySelector(sel)) { + hasModal = true; + break; + } + } + this.container.style.display = hasModal ? 'none' : 'block'; + } + + _bindDocumentEvents() { + if (this._bound) return; + this._bound = true; + + document.addEventListener('keydown', (e) => { + const target = e.target; + if (target && target.dataset && target.dataset.inputBoxId) { + const id = target.dataset.inputBoxId; + const meta = this.inputs.get(id); + if (!meta) return; + + if (e.key === 'Enter' && !e.shiftKey) { + const mode = meta.enterMode || 'both'; + let trigger = false, prevent = false; + switch (mode) { + case 'trigger': trigger = true; prevent = true; break; + case 'newline': trigger = false; prevent = false; break; + case 'both': trigger = true; prevent = false; break; + default: trigger = true; prevent = false; + } + if (prevent) e.preventDefault(); + + if (trigger) { + this._lastEnterId = id; + const fullOpcode = 'inputboxes_eventOnEnter'; + this._safeStartHats(fullOpcode); + } + } + } + }, true); + } + + _safeStartHats(opcode) { + const runtime = Scratch && Scratch.vm && Scratch.vm.runtime; + if (!runtime) { + setTimeout(() => this._safeStartHats(opcode), 100); + return; + } + if (!runtime._cache) { + try { const scripts = runtime.scripts; } catch (e) {} + if (!runtime._cache) runtime._cache = {}; + } + try { + runtime.startHats(opcode); + } catch (e) { + setTimeout(() => { + if (runtime._cache) { + try { runtime.startHats(opcode); } catch (ex) {} + } + }, 50); + } + } + + _bindResizeAndFullscreen() { + const events = ['resize', 'fullscreenchange', 'webkitfullscreenchange']; + const handler = () => { + if (this._resizeTimer) clearTimeout(this._resizeTimer); + this._resizeTimer = setTimeout(() => { + this._updateAllInputs(); + this._checkModalOpen(); + this._resizeTimer = null; + }, 50); + }; + for (const ev of events) { + window.addEventListener(ev, handler); + } + this._resizeHandler = handler; + } + + _observeCanvas() { + const canvas = document.querySelector('canvas'); + if (!canvas) return; + const observer = new MutationObserver(() => { + if (!this._updatePending) { + this._updatePending = true; + requestAnimationFrame(() => { + this._updateAllInputs(); + this._updatePending = false; + }); + } + }); + observer.observe(canvas, { + attributes: true, + attributeFilter: ['style', 'class', 'width', 'height'], + subtree: false + }); + this._canvasObserver = observer; + const parent = canvas.parentElement; + if (parent) { + const parentObserver = new MutationObserver(() => { + if (!this._updatePending) { + this._updatePending = true; + requestAnimationFrame(() => { + this._updateAllInputs(); + this._updatePending = false; + }); + } + }); + parentObserver.observe(parent, { + attributes: true, + attributeFilter: ['style', 'class'], + subtree: false + }); + this._parentObserver = parentObserver; + } + } + + _updateAllInputs() { + if (this.inputs.size === 0) return; + const canvas = document.querySelector('canvas'); + if (!canvas) return; + const container = this.getContainer(); + if (!container) return; + + const rect = canvas.getBoundingClientRect(); + const scale = rect.width / 480; + + for (const [id, meta] of this.inputs) { + if (!meta.element) continue; + const el = meta.element; + const left = meta.logicX * scale + rect.width / 2; + const top = rect.height / 2 - meta.logicY * scale; + el.style.left = left + 'px'; + el.style.top = top + 'px'; + el.style.width = (meta.logicWidth * scale) + 'px'; + el.style.height = (meta.logicHeight * scale) + 'px'; + } + this._checkModalOpen(); + } + + getContainer() { + const canvas = document.querySelector('canvas'); + if (!canvas) { + if (!this.container) { + const container = document.createElement('div'); + container.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:1000'; + document.body.appendChild(container); + this.container = container; + } + return this.container; + } + + const parent = canvas.parentElement; + if (getComputedStyle(parent).position === 'static') { + parent.style.position = 'relative'; + } + + if (this.container && this.container.parentElement !== parent) { + this.container.remove(); + this.container = null; + } + + if (!this.container) { + const container = document.createElement('div'); + container.id = 'tw-input-box-container'; + container.style.position = 'absolute'; + container.style.top = '0'; + container.style.left = '0'; + container.style.pointerEvents = 'none'; + container.style.overflow = 'hidden'; + container.style.zIndex = '1000'; + parent.appendChild(container); + this.container = container; + } + + const rect = canvas.getBoundingClientRect(); + const parentRect = parent.getBoundingClientRect(); + this.container.style.left = (rect.left - parentRect.left) + 'px'; + this.container.style.top = (rect.top - parentRect.top) + 'px'; + this.container.style.width = rect.width + 'px'; + this.container.style.height = rect.height + 'px'; + + this._checkModalOpen(); + return this.container; + } + + cmdRefreshLayout() { + this._updateAllInputs(); + } + + _createInputElement(type) { + const isMulti = (type === 'multiline'); + const el = isMulti ? document.createElement('textarea') : document.createElement('input'); + if (!isMulti) el.type = 'text'; + el.style.cssText = + 'position:absolute;box-sizing:border-box;border:1px solid #000;outline:none;background:#fff;padding:4px;font-size:14px;pointer-events:auto;z-index:1001;border-radius:2px;' + + (isMulti ? 'resize:none;font-family:sans-serif;' : ''); + return el; + } + + _getOrCreateInput(id, type = 'single') { + let meta = this.inputs.get(id); + if (!meta) { + const el = this._createInputElement(type); + el.dataset.inputBoxId = id; + const container = this.getContainer(); + if (container) container.appendChild(el); + meta = { + element: el, + type: type, + interactive: true, + borderVisible: true, + enterMode: 'both', + bgColor: '#ffffff', + logicX: 0, + logicY: 0, + logicWidth: 120, + logicHeight: 30 + }; + this.inputs.set(id, meta); + } + return meta; + } + + _applyMeta(meta) { + if (!meta || !meta.element) return; + const el = meta.element; + try { + el.style.border = meta.borderVisible ? '1px solid #000' : 'none'; + el.disabled = !meta.interactive; + } catch (e) {} + } + + _switchType(id, newType) { + const meta = this.inputs.get(id); + if (!meta || meta.type === newType) return; + const old = meta.element; + const backup = { + value: old.value || '', + left: old.style.left || '0px', + top: old.style.top || '0px', + width: old.style.width || '120px', + height: old.style.height || '30px', + bg: old.style.backgroundColor || '#ffffff', + placeholder: old.placeholder || '' + }; + old.remove(); + + const el = this._createInputElement(newType); + el.dataset.inputBoxId = id; + el.value = backup.value; + el.style.left = backup.left; + el.style.top = backup.top; + el.style.width = backup.width; + el.style.height = backup.height; + el.style.backgroundColor = backup.bg; + el.placeholder = backup.placeholder; + + meta.element = el; + meta.type = newType; + meta.bgColor = backup.bg; + this._applyMeta(meta); + const container = this.getContainer(); + if (container) container.appendChild(el); + this._updateAllInputs(); + } + cmdCreateOrMove(args) { + try { + const { ID, X, Y, WIDTH, HEIGHT } = args; + const meta = this._getOrCreateInput(ID, 'single'); + meta.logicX = X; + meta.logicY = Y; + meta.logicWidth = WIDTH; + meta.logicHeight = HEIGHT; + this._updateAllInputs(); + this._applyMeta(meta); + } catch (e) {} + } + + cmdDeleteAll() { + try { + for (const [id, meta] of this.inputs) if (meta.element) meta.element.remove(); + this.inputs.clear(); + } catch (e) {} + } + + cmdDeleteById(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + if (meta) { + if (meta.element) meta.element.remove(); + this.inputs.delete(ID); + } + } catch (e) {} + } + + cmdSetBorder(args) { + try { + const { ID, VISIBILITY } = args; + const meta = this.inputs.get(ID); + if (meta) { + meta.borderVisible = (VISIBILITY === 'visible'); + this._applyMeta(meta); + } + } catch (e) {} + } + + cmdSetBg(args) { + try { + const { ID, BG_COLOR } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) { + meta.element.style.backgroundColor = BG_COLOR; + meta.bgColor = BG_COLOR; + } + } catch (e) {} + } + + repTransparent() { return 'transparent'; } + + cmdSetValue(args) { + try { + const { ID, CONTENT } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) meta.element.value = CONTENT; + } catch (e) {} + } + + cmdSetHint(args) { + try { + const { ID, HINT } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) meta.element.placeholder = HINT; + } catch (e) {} + } + + repGetValue(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return (meta && meta.element) ? meta.element.value : ''; + } catch (e) { return ''; } + } + + repGetAllIds() { + try { + return JSON.stringify(Array.from(this.inputs.keys())); + } catch (e) { return '[]'; } + } + + boolExists(args) { + try { + const { ID } = args; + return this.inputs.has(ID); + } catch (e) { return false; } + } + + repLastEnteredId() { + return this._lastEnterId || ''; + } + + cmdFocus(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) { + if (meta.element.disabled) { + meta.element.disabled = false; + meta.interactive = true; + } + meta.element.focus(); + meta.element.select(); + } + } catch (e) {} + } + + cmdSetMode(args) { + try { + const { ID, MODE } = args; + if (MODE === 'single' || MODE === 'multiline') this._switchType(ID, MODE); + } catch (e) {} + } + + cmdSetEnabled(args) { + try { + const { ID, STATE } = args; + const meta = this.inputs.get(ID); + if (meta) { + meta.interactive = (STATE === 'enabled'); + this._applyMeta(meta); + } + } catch (e) {} + } + + queryBorderVisible(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? meta.borderVisible : false; + } catch (e) { return false; } + } + + queryInteractive(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? meta.interactive : false; + } catch (e) { return false; } + } + + queryIsMultiline(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? (meta.type === 'multiline') : false; + } catch (e) { return false; } + } + + repNewline() { return '\n'; } + + cmdSetEnterAction(args) { + try { + const { ID, MODE } = args; + const meta = this.inputs.get(ID); + if (meta && ['trigger', 'newline', 'both'].includes(MODE)) meta.enterMode = MODE; + } catch (e) {} + } + + queryEnterAction(args) { + try { + const { ID, MODE } = args; + const meta = this.inputs.get(ID); + return meta ? (meta.enterMode === MODE) : false; + } catch (e) { return false; } + } + + eventOnEnter() {} + + //-------------------------------------------------------------------------------- + + getInfo() { + const BT = Scratch.BlockType; + + //Easier function to translate + const tr = (defaultText, desc = '') => { + if (typeof Scratch.translate === 'function') { + return Scratch.translate({ default: defaultText, description: desc }); + } + return defaultText; + }; + + return { + id: 'inputboxes', + name: tr('Input Boxes'), + menus: { + visibilityMenu: { + items: [ + { text: tr('visible'), value: 'visible' }, + { text: tr('hidden'), value: 'hidden' } + ] + }, + typeMenu: { + items: [ + { text: tr('single line'), value: 'single' }, + { text: tr('multiline'), value: 'multiline' } + ] + }, + stateMenu: { + items: [ + { text: tr('enabled'), value: 'enabled' }, + { text: tr('disabled'), value: 'disabled' } + ] + }, + enterModeMenu: { + items: [ + { text: tr('trigger event only'), value: 'trigger' }, + { text: tr('newline only (no event)'), value: 'newline' }, + { text: tr('both trigger and newline'), value: 'both' } + ] + } + }, + blocks: [ + //BT.COMMAND Blocks + { + opcode: 'cmdCreateOrMove', + blockType: BT.COMMAND, + text: tr('create/move input box ID: [ID] x: [X] y: [Y] width: [WIDTH] height: [HEIGHT]', + 'ID is the unique identifier for the input box.'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + X: { type: 'number', defaultValue: 0 }, + Y: { type: 'number', defaultValue: 0 }, + WIDTH: { type: 'number', defaultValue: 120 }, + HEIGHT: { type: 'number', defaultValue: 30 } + } + }, + { + opcode: 'cmdDeleteAll', + blockType: BT.COMMAND, + text: tr('delete all input boxes') + }, + { + opcode: 'cmdDeleteById', + blockType: BT.COMMAND, + text: tr('delete input box with ID [ID]'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'cmdSetBorder', + blockType: BT.COMMAND, + text: tr('set border of input box [ID] to [VISIBILITY]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + VISIBILITY: { type: 'string', menu: 'visibilityMenu', defaultValue: 'visible' } + } + }, + { + opcode: 'cmdSetBg', + blockType: BT.COMMAND, + text: tr('set background color of input box [ID] to [BG_COLOR]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + BG_COLOR: { type: 'color', defaultValue: '#ffffff' } + } + }, + { + opcode: 'cmdSetValue', + blockType: BT.COMMAND, + text: tr('set input box [ID] value to [CONTENT]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + CONTENT: { type: 'string', defaultValue: '' } + } + }, + { + opcode: 'cmdSetHint', + blockType: BT.COMMAND, + text: tr('set input box [ID] hint to [HINT]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + HINT: { type: 'string', defaultValue: '' } + } + }, + { + opcode: 'cmdFocus', + blockType: BT.COMMAND, + text: tr('focus input box [ID]'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'cmdSetMode', + blockType: BT.COMMAND, + text: tr('set input box [ID] to [MODE]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + MODE: { type: 'string', menu: 'typeMenu', defaultValue: 'single' } + } + }, + { + opcode: 'cmdSetEnabled', + blockType: BT.COMMAND, + text: tr('set input box [ID] interactive to [STATE]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + STATE: { type: 'string', menu: 'stateMenu', defaultValue: 'enabled' } + } + }, + { + opcode: 'cmdSetEnterAction', + blockType: BT.COMMAND, + text: tr('when Enter pressed on input box [ID], do [MODE]in multiline mode'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } + } + }, + { + blockType: 'label', + text: tr('If set to trigger event, use Shift+Enter for newline') + }, + { + opcode: 'cmdRefreshLayout', + blockType: BT.COMMAND, + text: tr('refresh input box layout') + }, + //BT.REPORTER Blocks + { + opcode: 'repTransparent', + blockType: BT.REPORTER, + text: tr('transparent') + }, + { + opcode: 'repGetValue', + blockType: BT.REPORTER, + text: tr('value of input box [ID]'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'repGetAllIds', + blockType: BT.REPORTER, + text: tr('all input boxes (JSON array)') + }, + { + opcode: 'repLastEnteredId', + blockType: BT.REPORTER, + text: tr('the ID of the input box where Enter was pressed') + }, + { + opcode: 'repNewline', + blockType: BT.REPORTER, + text: tr('newline') + }, + //BT.BOOLEAN Blocks + { + opcode: 'boolExists', + blockType: BT.BOOLEAN, + text: tr('does input box [ID] exist?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryBorderVisible', + blockType: BT.BOOLEAN, + text: tr('is border of input box [ID] visible?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryInteractive', + blockType: BT.BOOLEAN, + text: tr('is input box [ID] interactive?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryIsMultiline', + blockType: BT.BOOLEAN, + text: tr('is input box [ID] multiline?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryEnterAction', + blockType: BT.BOOLEAN, + text: tr('when Enter pressed on input box [ID], is the mode [MODE]?'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } + } + }, + { + opcode: 'eventOnEnter', + blockType: BT.EVENT, + text: tr('when user presses Enter on an input box'), + isEdgeActivated: false + } + ] + }; + } +} + +Scratch.extensions.register(new InputBoxExtension()); \ No newline at end of file diff --git a/extensions/extensions.json b/extensions/extensions.json index a4fac91813..409993d862 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -103,5 +103,6 @@ "itchio", "gamejolt", "obviousAlexC/newgroundsIO", + "Zhuziexe/inputboxes", "Lily/McUtils" // McUtils should always be the last item. ] From 5b90ccc1a86c228a370756e9317caeabd16c0209 Mon Sep 17 00:00:00 2001 From: Zhuziexe Date: Wed, 8 Jul 2026 15:15:52 +0800 Subject: [PATCH 2/4] Update file --- extensions/Zhuziexe/inputboxes.js | 1238 +++++++++++++++-------------- 1 file changed, 622 insertions(+), 616 deletions(-) diff --git a/extensions/Zhuziexe/inputboxes.js b/extensions/Zhuziexe/inputboxes.js index 964eec594c..185e7e5f64 100644 --- a/extensions/Zhuziexe/inputboxes.js +++ b/extensions/Zhuziexe/inputboxes.js @@ -4,143 +4,131 @@ // By: Zhuziexe // License: MPL-2.0 -class InputBoxExtension { - constructor() { - this.inputs = new Map(); - this.container = null; - this._lastEnterId = null; - this._resizeTimer = null; - - this._updatePending = false; this._bindDocumentEvents(); - this._bindResizeAndFullscreen(); - - this._observeCanvas(); - this._observeModals(); - - setTimeout(() => this._updateAllInputs(), 100); - } +(function (Scratch) { + "use strict"; + if (!Scratch.extensions.unsandboxed) { + throw new Error("SearchParams must be run unsandboxed."); + } + class InputBoxExtension { + constructor() { + this.inputs = new Map(); + this.container = null; + this._lastEnterId = null; + this._resizeTimer = null; - _observeModals() { - this._checkModalOpen(); - const observer = new MutationObserver(() => { - this._checkModalOpen(); - }); - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['class', 'style'] - }); - this._modalObserver = observer; - } + this._updatePending = false; this._bindDocumentEvents(); + this._bindResizeAndFullscreen(); - _checkModalOpen() { - if (!this.container) return; - const modalSelectors = [ - '.modal', '.ReactModalPortal', '.ReactModal__Overlay', - '.ReactModal__Content', '.modal-content', '.dialog', - '[role="dialog"]', '[role="modal"]' - ]; - let hasModal = false; - for (const sel of modalSelectors) { - if (document.querySelector(sel)) { - hasModal = true; - break; - } + this._observeCanvas(); + this._observeModals(); + + setTimeout(() => this._updateAllInputs(), 100); } - this.container.style.display = hasModal ? 'none' : 'block'; - } - _bindDocumentEvents() { - if (this._bound) return; - this._bound = true; - - document.addEventListener('keydown', (e) => { - const target = e.target; - if (target && target.dataset && target.dataset.inputBoxId) { - const id = target.dataset.inputBoxId; - const meta = this.inputs.get(id); - if (!meta) return; - - if (e.key === 'Enter' && !e.shiftKey) { - const mode = meta.enterMode || 'both'; - let trigger = false, prevent = false; - switch (mode) { - case 'trigger': trigger = true; prevent = true; break; - case 'newline': trigger = false; prevent = false; break; - case 'both': trigger = true; prevent = false; break; - default: trigger = true; prevent = false; - } - if (prevent) e.preventDefault(); + _observeModals() { + this._checkModalOpen(); + const observer = new MutationObserver(() => { + this._checkModalOpen(); + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'style'] + }); + this._modalObserver = observer; + } - if (trigger) { - this._lastEnterId = id; - const fullOpcode = 'inputboxes_eventOnEnter'; - this._safeStartHats(fullOpcode); - } + _checkModalOpen() { + if (!this.container) return; + const modalSelectors = [ + '.modal', '.ReactModalPortal', '.ReactModal__Overlay', + '.ReactModal__Content', '.modal-content', '.dialog', + '[role="dialog"]', '[role="modal"]' + ]; + let hasModal = false; + for (const sel of modalSelectors) { + if (document.querySelector(sel)) { + hasModal = true; + break; } } - }, true); - } + this.container.style.display = hasModal ? 'none' : 'block'; + } - _safeStartHats(opcode) { - const runtime = Scratch && Scratch.vm && Scratch.vm.runtime; - if (!runtime) { - setTimeout(() => this._safeStartHats(opcode), 100); - return; - } - if (!runtime._cache) { - try { const scripts = runtime.scripts; } catch (e) {} - if (!runtime._cache) runtime._cache = {}; - } - try { - runtime.startHats(opcode); - } catch (e) { - setTimeout(() => { - if (runtime._cache) { - try { runtime.startHats(opcode); } catch (ex) {} + _bindDocumentEvents() { + if (this._bound) return; + this._bound = true; + + document.addEventListener('keydown', (e) => { + const target = e.target; + if (target && target.dataset && target.dataset.inputBoxId) { + const id = target.dataset.inputBoxId; + const meta = this.inputs.get(id); + if (!meta) return; + + if (e.key === 'Enter' && !e.shiftKey) { + const mode = meta.enterMode || 'both'; + let trigger = false, prevent = false; + switch (mode) { + case 'trigger': trigger = true; prevent = true; break; + case 'newline': trigger = false; prevent = false; break; + case 'both': trigger = true; prevent = false; break; + default: trigger = true; prevent = false; + } + if (prevent) e.preventDefault(); + + if (trigger) { + this._lastEnterId = id; + const fullOpcode = 'inputboxes_eventOnEnter'; + this._safeStartHats(fullOpcode); + } + } } - }, 50); + }, true); } - } - _bindResizeAndFullscreen() { - const events = ['resize', 'fullscreenchange', 'webkitfullscreenchange']; - const handler = () => { - if (this._resizeTimer) clearTimeout(this._resizeTimer); - this._resizeTimer = setTimeout(() => { - this._updateAllInputs(); - this._checkModalOpen(); - this._resizeTimer = null; - }, 50); - }; - for (const ev of events) { - window.addEventListener(ev, handler); + _safeStartHats(opcode) { + const runtime = Scratch && Scratch.vm && Scratch.vm.runtime; + if (!runtime) { + setTimeout(() => this._safeStartHats(opcode), 100); + return; + } + if (!runtime._cache) { + try { const scripts = runtime.scripts; } catch (e) { } + if (!runtime._cache) runtime._cache = {}; + } + try { + runtime.startHats(opcode); + } catch (e) { + setTimeout(() => { + if (runtime._cache) { + try { runtime.startHats(opcode); } catch (ex) { } + } + }, 50); + } } - this._resizeHandler = handler; - } - _observeCanvas() { - const canvas = document.querySelector('canvas'); - if (!canvas) return; - const observer = new MutationObserver(() => { - if (!this._updatePending) { - this._updatePending = true; - requestAnimationFrame(() => { + _bindResizeAndFullscreen() { + const events = ['resize', 'fullscreenchange', 'webkitfullscreenchange']; + const handler = () => { + if (this._resizeTimer) clearTimeout(this._resizeTimer); + this._resizeTimer = setTimeout(() => { this._updateAllInputs(); - this._updatePending = false; - }); + this._checkModalOpen(); + this._resizeTimer = null; + }, 50); + }; + for (const ev of events) { + window.addEventListener(ev, handler); } - }); - observer.observe(canvas, { - attributes: true, - attributeFilter: ['style', 'class', 'width', 'height'], - subtree: false - }); - this._canvasObserver = observer; - const parent = canvas.parentElement; - if (parent) { - const parentObserver = new MutationObserver(() => { + this._resizeHandler = handler; + } + + _observeCanvas() { + const canvas = document.querySelector('canvas'); + if (!canvas) return; + const observer = new MutationObserver(() => { if (!this._updatePending) { this._updatePending = true; requestAnimationFrame(() => { @@ -149,554 +137,572 @@ class InputBoxExtension { }); } }); - parentObserver.observe(parent, { + observer.observe(canvas, { attributes: true, - attributeFilter: ['style', 'class'], + attributeFilter: ['style', 'class', 'width', 'height'], subtree: false }); - this._parentObserver = parentObserver; + this._canvasObserver = observer; + const parent = canvas.parentElement; + if (parent) { + const parentObserver = new MutationObserver(() => { + if (!this._updatePending) { + this._updatePending = true; + requestAnimationFrame(() => { + this._updateAllInputs(); + this._updatePending = false; + }); + } + }); + parentObserver.observe(parent, { + attributes: true, + attributeFilter: ['style', 'class'], + subtree: false + }); + this._parentObserver = parentObserver; + } + } + + _updateAllInputs() { + if (this.inputs.size === 0) return; + const canvas = document.querySelector('canvas'); + if (!canvas) return; + const container = this.getContainer(); + if (!container) return; + + const rect = canvas.getBoundingClientRect(); + const scale = rect.width / 480; + + for (const [id, meta] of this.inputs) { + if (!meta.element) continue; + const el = meta.element; + const left = meta.logicX * scale + rect.width / 2; + const top = rect.height / 2 - meta.logicY * scale; + el.style.left = left + 'px'; + el.style.top = top + 'px'; + el.style.width = (meta.logicWidth * scale) + 'px'; + el.style.height = (meta.logicHeight * scale) + 'px'; + } + this._checkModalOpen(); } - } - _updateAllInputs() { - if (this.inputs.size === 0) return; - const canvas = document.querySelector('canvas'); - if (!canvas) return; - const container = this.getContainer(); - if (!container) return; + getContainer() { + const canvas = document.querySelector('canvas'); + if (!canvas) { + if (!this.container) { + const container = document.createElement('div'); + container.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:1000'; + document.body.appendChild(container); + this.container = container; + } + return this.container; + } - const rect = canvas.getBoundingClientRect(); - const scale = rect.width / 480; + const parent = canvas.parentElement; + if (getComputedStyle(parent).position === 'static') { + parent.style.position = 'relative'; + } - for (const [id, meta] of this.inputs) { - if (!meta.element) continue; - const el = meta.element; - const left = meta.logicX * scale + rect.width / 2; - const top = rect.height / 2 - meta.logicY * scale; - el.style.left = left + 'px'; - el.style.top = top + 'px'; - el.style.width = (meta.logicWidth * scale) + 'px'; - el.style.height = (meta.logicHeight * scale) + 'px'; - } - this._checkModalOpen(); - } + if (this.container && this.container.parentElement !== parent) { + this.container.remove(); + this.container = null; + } - getContainer() { - const canvas = document.querySelector('canvas'); - if (!canvas) { if (!this.container) { const container = document.createElement('div'); - container.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:1000'; - document.body.appendChild(container); + container.id = 'tw-input-box-container'; + container.style.position = 'absolute'; + container.style.top = '0'; + container.style.left = '0'; + container.style.pointerEvents = 'none'; + container.style.overflow = 'hidden'; + container.style.zIndex = '1000'; + parent.appendChild(container); this.container = container; } + + const rect = canvas.getBoundingClientRect(); + const parentRect = parent.getBoundingClientRect(); + this.container.style.left = (rect.left - parentRect.left) + 'px'; + this.container.style.top = (rect.top - parentRect.top) + 'px'; + this.container.style.width = rect.width + 'px'; + this.container.style.height = rect.height + 'px'; + + this._checkModalOpen(); return this.container; } - const parent = canvas.parentElement; - if (getComputedStyle(parent).position === 'static') { - parent.style.position = 'relative'; + cmdRefreshLayout() { + this._updateAllInputs(); } - if (this.container && this.container.parentElement !== parent) { - this.container.remove(); - this.container = null; + _createInputElement(type) { + const isMulti = (type === 'multiline'); + const el = isMulti ? document.createElement('textarea') : document.createElement('input'); + if (!isMulti) el.type = 'text'; + el.style.cssText = + 'position:absolute;box-sizing:border-box;border:1px solid #000;outline:none;background:#fff;padding:4px;font-size:14px;pointer-events:auto;z-index:1001;border-radius:2px;' + + (isMulti ? 'resize:none;font-family:sans-serif;' : ''); + return el; } - if (!this.container) { - const container = document.createElement('div'); - container.id = 'tw-input-box-container'; - container.style.position = 'absolute'; - container.style.top = '0'; - container.style.left = '0'; - container.style.pointerEvents = 'none'; - container.style.overflow = 'hidden'; - container.style.zIndex = '1000'; - parent.appendChild(container); - this.container = container; - } - - const rect = canvas.getBoundingClientRect(); - const parentRect = parent.getBoundingClientRect(); - this.container.style.left = (rect.left - parentRect.left) + 'px'; - this.container.style.top = (rect.top - parentRect.top) + 'px'; - this.container.style.width = rect.width + 'px'; - this.container.style.height = rect.height + 'px'; - - this._checkModalOpen(); - return this.container; - } + _getOrCreateInput(id, type = 'single') { + let meta = this.inputs.get(id); + if (!meta) { + const el = this._createInputElement(type); + el.dataset.inputBoxId = id; + const container = this.getContainer(); + if (container) container.appendChild(el); + meta = { + element: el, + type: type, + interactive: true, + borderVisible: true, + enterMode: 'both', + bgColor: '#ffffff', + logicX: 0, + logicY: 0, + logicWidth: 120, + logicHeight: 30 + }; + this.inputs.set(id, meta); + } + return meta; + } - cmdRefreshLayout() { - this._updateAllInputs(); - } + _applyMeta(meta) { + if (!meta || !meta.element) return; + const el = meta.element; + try { + el.style.border = meta.borderVisible ? '1px solid #000' : 'none'; + el.disabled = !meta.interactive; + } catch (e) { } + } - _createInputElement(type) { - const isMulti = (type === 'multiline'); - const el = isMulti ? document.createElement('textarea') : document.createElement('input'); - if (!isMulti) el.type = 'text'; - el.style.cssText = - 'position:absolute;box-sizing:border-box;border:1px solid #000;outline:none;background:#fff;padding:4px;font-size:14px;pointer-events:auto;z-index:1001;border-radius:2px;' + - (isMulti ? 'resize:none;font-family:sans-serif;' : ''); - return el; - } + _switchType(id, newType) { + const meta = this.inputs.get(id); + if (!meta || meta.type === newType) return; + const old = meta.element; + const backup = { + value: old.value || '', + left: old.style.left || '0px', + top: old.style.top || '0px', + width: old.style.width || '120px', + height: old.style.height || '30px', + bg: old.style.backgroundColor || '#ffffff', + placeholder: old.placeholder || '' + }; + old.remove(); - _getOrCreateInput(id, type = 'single') { - let meta = this.inputs.get(id); - if (!meta) { - const el = this._createInputElement(type); + const el = this._createInputElement(newType); el.dataset.inputBoxId = id; + el.value = backup.value; + el.style.left = backup.left; + el.style.top = backup.top; + el.style.width = backup.width; + el.style.height = backup.height; + el.style.backgroundColor = backup.bg; + el.placeholder = backup.placeholder; + + meta.element = el; + meta.type = newType; + meta.bgColor = backup.bg; + this._applyMeta(meta); const container = this.getContainer(); if (container) container.appendChild(el); - meta = { - element: el, - type: type, - interactive: true, - borderVisible: true, - enterMode: 'both', - bgColor: '#ffffff', - logicX: 0, - logicY: 0, - logicWidth: 120, - logicHeight: 30 - }; - this.inputs.set(id, meta); - } - return meta; - } - - _applyMeta(meta) { - if (!meta || !meta.element) return; - const el = meta.element; - try { - el.style.border = meta.borderVisible ? '1px solid #000' : 'none'; - el.disabled = !meta.interactive; - } catch (e) {} - } - - _switchType(id, newType) { - const meta = this.inputs.get(id); - if (!meta || meta.type === newType) return; - const old = meta.element; - const backup = { - value: old.value || '', - left: old.style.left || '0px', - top: old.style.top || '0px', - width: old.style.width || '120px', - height: old.style.height || '30px', - bg: old.style.backgroundColor || '#ffffff', - placeholder: old.placeholder || '' - }; - old.remove(); - - const el = this._createInputElement(newType); - el.dataset.inputBoxId = id; - el.value = backup.value; - el.style.left = backup.left; - el.style.top = backup.top; - el.style.width = backup.width; - el.style.height = backup.height; - el.style.backgroundColor = backup.bg; - el.placeholder = backup.placeholder; - - meta.element = el; - meta.type = newType; - meta.bgColor = backup.bg; - this._applyMeta(meta); - const container = this.getContainer(); - if (container) container.appendChild(el); - this._updateAllInputs(); - } - cmdCreateOrMove(args) { - try { - const { ID, X, Y, WIDTH, HEIGHT } = args; - const meta = this._getOrCreateInput(ID, 'single'); - meta.logicX = X; - meta.logicY = Y; - meta.logicWidth = WIDTH; - meta.logicHeight = HEIGHT; this._updateAllInputs(); - this._applyMeta(meta); - } catch (e) {} - } + } + cmdCreateOrMove(args) { + try { + const { ID, X, Y, WIDTH, HEIGHT } = args; + const meta = this._getOrCreateInput(ID, 'single'); + meta.logicX = X; + meta.logicY = Y; + meta.logicWidth = WIDTH; + meta.logicHeight = HEIGHT; + this._updateAllInputs(); + this._applyMeta(meta); + } catch (e) { } + } - cmdDeleteAll() { - try { - for (const [id, meta] of this.inputs) if (meta.element) meta.element.remove(); - this.inputs.clear(); - } catch (e) {} - } + cmdDeleteAll() { + try { + for (const [id, meta] of this.inputs) if (meta.element) meta.element.remove(); + this.inputs.clear(); + } catch (e) { } + } - cmdDeleteById(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - if (meta) { - if (meta.element) meta.element.remove(); - this.inputs.delete(ID); - } - } catch (e) {} - } + cmdDeleteById(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + if (meta) { + if (meta.element) meta.element.remove(); + this.inputs.delete(ID); + } + } catch (e) { } + } - cmdSetBorder(args) { - try { - const { ID, VISIBILITY } = args; - const meta = this.inputs.get(ID); - if (meta) { - meta.borderVisible = (VISIBILITY === 'visible'); - this._applyMeta(meta); - } - } catch (e) {} - } + cmdSetBorder(args) { + try { + const { ID, VISIBILITY } = args; + const meta = this.inputs.get(ID); + if (meta) { + meta.borderVisible = (VISIBILITY === 'visible'); + this._applyMeta(meta); + } + } catch (e) { } + } - cmdSetBg(args) { - try { - const { ID, BG_COLOR } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) { - meta.element.style.backgroundColor = BG_COLOR; - meta.bgColor = BG_COLOR; - } - } catch (e) {} - } + cmdSetBg(args) { + try { + const { ID, BG_COLOR } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) { + meta.element.style.backgroundColor = BG_COLOR; + meta.bgColor = BG_COLOR; + } + } catch (e) { } + } - repTransparent() { return 'transparent'; } + repTransparent() { return 'transparent'; } - cmdSetValue(args) { - try { - const { ID, CONTENT } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) meta.element.value = CONTENT; - } catch (e) {} - } + cmdSetValue(args) { + try { + const { ID, CONTENT } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) meta.element.value = CONTENT; + } catch (e) { } + } - cmdSetHint(args) { - try { - const { ID, HINT } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) meta.element.placeholder = HINT; - } catch (e) {} - } + cmdSetHint(args) { + try { + const { ID, HINT } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) meta.element.placeholder = HINT; + } catch (e) { } + } - repGetValue(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return (meta && meta.element) ? meta.element.value : ''; - } catch (e) { return ''; } - } + repGetValue(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return (meta && meta.element) ? meta.element.value : ''; + } catch (e) { return ''; } + } - repGetAllIds() { - try { - return JSON.stringify(Array.from(this.inputs.keys())); - } catch (e) { return '[]'; } - } + repGetAllIds() { + try { + return JSON.stringify(Array.from(this.inputs.keys())); + } catch (e) { return '[]'; } + } - boolExists(args) { - try { - const { ID } = args; - return this.inputs.has(ID); - } catch (e) { return false; } - } + boolExists(args) { + try { + const { ID } = args; + return this.inputs.has(ID); + } catch (e) { return false; } + } - repLastEnteredId() { - return this._lastEnterId || ''; - } + repLastEnteredId() { + return this._lastEnterId || ''; + } - cmdFocus(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) { - if (meta.element.disabled) { - meta.element.disabled = false; - meta.interactive = true; + cmdFocus(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) { + if (meta.element.disabled) { + meta.element.disabled = false; + meta.interactive = true; + } + meta.element.focus(); + meta.element.select(); } - meta.element.focus(); - meta.element.select(); - } - } catch (e) {} - } + } catch (e) { } + } - cmdSetMode(args) { - try { - const { ID, MODE } = args; - if (MODE === 'single' || MODE === 'multiline') this._switchType(ID, MODE); - } catch (e) {} - } + cmdSetMode(args) { + try { + const { ID, MODE } = args; + if (MODE === 'single' || MODE === 'multiline') this._switchType(ID, MODE); + } catch (e) { } + } - cmdSetEnabled(args) { - try { - const { ID, STATE } = args; - const meta = this.inputs.get(ID); - if (meta) { - meta.interactive = (STATE === 'enabled'); - this._applyMeta(meta); - } - } catch (e) {} - } + cmdSetEnabled(args) { + try { + const { ID, STATE } = args; + const meta = this.inputs.get(ID); + if (meta) { + meta.interactive = (STATE === 'enabled'); + this._applyMeta(meta); + } + } catch (e) { } + } - queryBorderVisible(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return meta ? meta.borderVisible : false; - } catch (e) { return false; } - } + queryBorderVisible(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? meta.borderVisible : false; + } catch (e) { return false; } + } - queryInteractive(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return meta ? meta.interactive : false; - } catch (e) { return false; } - } + queryInteractive(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? meta.interactive : false; + } catch (e) { return false; } + } - queryIsMultiline(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return meta ? (meta.type === 'multiline') : false; - } catch (e) { return false; } - } + queryIsMultiline(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? (meta.type === 'multiline') : false; + } catch (e) { return false; } + } - repNewline() { return '\n'; } + repNewline() { return '\n'; } - cmdSetEnterAction(args) { - try { - const { ID, MODE } = args; - const meta = this.inputs.get(ID); - if (meta && ['trigger', 'newline', 'both'].includes(MODE)) meta.enterMode = MODE; - } catch (e) {} - } + cmdSetEnterAction(args) { + try { + const { ID, MODE } = args; + const meta = this.inputs.get(ID); + if (meta && ['trigger', 'newline', 'both'].includes(MODE)) meta.enterMode = MODE; + } catch (e) { } + } - queryEnterAction(args) { - try { - const { ID, MODE } = args; - const meta = this.inputs.get(ID); - return meta ? (meta.enterMode === MODE) : false; - } catch (e) { return false; } - } + queryEnterAction(args) { + try { + const { ID, MODE } = args; + const meta = this.inputs.get(ID); + return meta ? (meta.enterMode === MODE) : false; + } catch (e) { return false; } + } - eventOnEnter() {} + eventOnEnter() { } - //-------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------- - getInfo() { - const BT = Scratch.BlockType; + getInfo() { + const BT = Scratch.BlockType; - //Easier function to translate - const tr = (defaultText, desc = '') => { - if (typeof Scratch.translate === 'function') { - return Scratch.translate({ default: defaultText, description: desc }); - } - return defaultText; - }; - - return { - id: 'inputboxes', - name: tr('Input Boxes'), - menus: { - visibilityMenu: { - items: [ - { text: tr('visible'), value: 'visible' }, - { text: tr('hidden'), value: 'hidden' } - ] - }, - typeMenu: { - items: [ - { text: tr('single line'), value: 'single' }, - { text: tr('multiline'), value: 'multiline' } - ] - }, - stateMenu: { - items: [ - { text: tr('enabled'), value: 'enabled' }, - { text: tr('disabled'), value: 'disabled' } - ] - }, - enterModeMenu: { - items: [ - { text: tr('trigger event only'), value: 'trigger' }, - { text: tr('newline only (no event)'), value: 'newline' }, - { text: tr('both trigger and newline'), value: 'both' } - ] + //Easier function to translate + const tr = (defaultText, desc = '') => { + if (typeof Scratch.translate === 'function') { + return Scratch.translate({ default: defaultText, description: desc }); } - }, - blocks: [ - //BT.COMMAND Blocks - { - opcode: 'cmdCreateOrMove', - blockType: BT.COMMAND, - text: tr('create/move input box ID: [ID] x: [X] y: [Y] width: [WIDTH] height: [HEIGHT]', - 'ID is the unique identifier for the input box.'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - X: { type: 'number', defaultValue: 0 }, - Y: { type: 'number', defaultValue: 0 }, - WIDTH: { type: 'number', defaultValue: 120 }, - HEIGHT: { type: 'number', defaultValue: 30 } - } - }, - { - opcode: 'cmdDeleteAll', - blockType: BT.COMMAND, - text: tr('delete all input boxes') - }, - { - opcode: 'cmdDeleteById', - blockType: BT.COMMAND, - text: tr('delete input box with ID [ID]'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'cmdSetBorder', - blockType: BT.COMMAND, - text: tr('set border of input box [ID] to [VISIBILITY]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - VISIBILITY: { type: 'string', menu: 'visibilityMenu', defaultValue: 'visible' } - } - }, - { - opcode: 'cmdSetBg', - blockType: BT.COMMAND, - text: tr('set background color of input box [ID] to [BG_COLOR]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - BG_COLOR: { type: 'color', defaultValue: '#ffffff' } - } - }, - { - opcode: 'cmdSetValue', - blockType: BT.COMMAND, - text: tr('set input box [ID] value to [CONTENT]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - CONTENT: { type: 'string', defaultValue: '' } - } - }, - { - opcode: 'cmdSetHint', - blockType: BT.COMMAND, - text: tr('set input box [ID] hint to [HINT]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - HINT: { type: 'string', defaultValue: '' } - } - }, - { - opcode: 'cmdFocus', - blockType: BT.COMMAND, - text: tr('focus input box [ID]'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'cmdSetMode', - blockType: BT.COMMAND, - text: tr('set input box [ID] to [MODE]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - MODE: { type: 'string', menu: 'typeMenu', defaultValue: 'single' } - } - }, - { - opcode: 'cmdSetEnabled', - blockType: BT.COMMAND, - text: tr('set input box [ID] interactive to [STATE]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - STATE: { type: 'string', menu: 'stateMenu', defaultValue: 'enabled' } + return defaultText; + }; + + return { + id: 'inputboxes', + name: tr('Input Boxes'), + menus: { + visibilityMenu: { + items: [ + { text: tr('visible'), value: 'visible' }, + { text: tr('hidden'), value: 'hidden' } + ] + }, + typeMenu: { + items: [ + { text: tr('single line'), value: 'single' }, + { text: tr('multiline'), value: 'multiline' } + ] + }, + stateMenu: { + items: [ + { text: tr('enabled'), value: 'enabled' }, + { text: tr('disabled'), value: 'disabled' } + ] + }, + enterModeMenu: { + items: [ + { text: tr('trigger event only'), value: 'trigger' }, + { text: tr('newline only (no event)'), value: 'newline' }, + { text: tr('both trigger and newline'), value: 'both' } + ] } }, - { - opcode: 'cmdSetEnterAction', - blockType: BT.COMMAND, - text: tr('when Enter pressed on input box [ID], do [MODE]in multiline mode'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } + blocks: [ + //BT.COMMAND Blocks + { + opcode: 'cmdCreateOrMove', + blockType: BT.COMMAND, + text: tr('create/move input box ID: [ID] x: [X] y: [Y] width: [WIDTH] height: [HEIGHT]', + 'ID is the unique identifier for the input box.'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + X: { type: 'number', defaultValue: 0 }, + Y: { type: 'number', defaultValue: 0 }, + WIDTH: { type: 'number', defaultValue: 120 }, + HEIGHT: { type: 'number', defaultValue: 30 } + } + }, + { + opcode: 'cmdDeleteAll', + blockType: BT.COMMAND, + text: tr('delete all input boxes') + }, + { + opcode: 'cmdDeleteById', + blockType: BT.COMMAND, + text: tr('delete input box with ID [ID]'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'cmdSetBorder', + blockType: BT.COMMAND, + text: tr('set border of input box [ID] to [VISIBILITY]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + VISIBILITY: { type: 'string', menu: 'visibilityMenu', defaultValue: 'visible' } + } + }, + { + opcode: 'cmdSetBg', + blockType: BT.COMMAND, + text: tr('set background color of input box [ID] to [BG_COLOR]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + BG_COLOR: { type: 'color', defaultValue: '#ffffff' } + } + }, + { + opcode: 'cmdSetValue', + blockType: BT.COMMAND, + text: tr('set input box [ID] value to [CONTENT]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + CONTENT: { type: 'string', defaultValue: '' } + } + }, + { + opcode: 'cmdSetHint', + blockType: BT.COMMAND, + text: tr('set input box [ID] hint to [HINT]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + HINT: { type: 'string', defaultValue: '' } + } + }, + { + opcode: 'cmdFocus', + blockType: BT.COMMAND, + text: tr('focus input box [ID]'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'cmdSetMode', + blockType: BT.COMMAND, + text: tr('set input box [ID] to [MODE]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + MODE: { type: 'string', menu: 'typeMenu', defaultValue: 'single' } + } + }, + { + opcode: 'cmdSetEnabled', + blockType: BT.COMMAND, + text: tr('set input box [ID] interactive to [STATE]'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + STATE: { type: 'string', menu: 'stateMenu', defaultValue: 'enabled' } + } + }, + { + opcode: 'cmdSetEnterAction', + blockType: BT.COMMAND, + text: tr('when Enter pressed on input box [ID], do [MODE]in multiline mode'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } + } + }, + { + blockType: 'label', + text: tr('If set to trigger event, use Shift+Enter for newline') + }, + { + opcode: 'cmdRefreshLayout', + blockType: BT.COMMAND, + text: tr('refresh input box layout') + }, + //BT.REPORTER Blocks + { + opcode: 'repTransparent', + blockType: BT.REPORTER, + text: tr('transparent') + }, + { + opcode: 'repGetValue', + blockType: BT.REPORTER, + text: tr('value of input box [ID]'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'repGetAllIds', + blockType: BT.REPORTER, + text: tr('all input boxes (JSON array)') + }, + { + opcode: 'repLastEnteredId', + blockType: BT.REPORTER, + text: tr('the ID of the input box where Enter was pressed') + }, + { + opcode: 'repNewline', + blockType: BT.REPORTER, + text: tr('newline') + }, + //BT.BOOLEAN Blocks + { + opcode: 'boolExists', + blockType: BT.BOOLEAN, + text: tr('does input box [ID] exist?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryBorderVisible', + blockType: BT.BOOLEAN, + text: tr('is border of input box [ID] visible?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryInteractive', + blockType: BT.BOOLEAN, + text: tr('is input box [ID] interactive?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryIsMultiline', + blockType: BT.BOOLEAN, + text: tr('is input box [ID] multiline?'), + arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } + }, + { + opcode: 'queryEnterAction', + blockType: BT.BOOLEAN, + text: tr('when Enter pressed on input box [ID], is the mode [MODE]?'), + arguments: { + ID: { type: 'string', defaultValue: 'inputbox' }, + MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } + } + }, + { + opcode: 'eventOnEnter', + blockType: BT.EVENT, + text: tr('when user presses Enter on an input box'), + isEdgeActivated: false } - }, - { - blockType: 'label', - text: tr('If set to trigger event, use Shift+Enter for newline') - }, - { - opcode: 'cmdRefreshLayout', - blockType: BT.COMMAND, - text: tr('refresh input box layout') - }, - //BT.REPORTER Blocks - { - opcode: 'repTransparent', - blockType: BT.REPORTER, - text: tr('transparent') - }, - { - opcode: 'repGetValue', - blockType: BT.REPORTER, - text: tr('value of input box [ID]'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'repGetAllIds', - blockType: BT.REPORTER, - text: tr('all input boxes (JSON array)') - }, - { - opcode: 'repLastEnteredId', - blockType: BT.REPORTER, - text: tr('the ID of the input box where Enter was pressed') - }, - { - opcode: 'repNewline', - blockType: BT.REPORTER, - text: tr('newline') - }, - //BT.BOOLEAN Blocks - { - opcode: 'boolExists', - blockType: BT.BOOLEAN, - text: tr('does input box [ID] exist?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryBorderVisible', - blockType: BT.BOOLEAN, - text: tr('is border of input box [ID] visible?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryInteractive', - blockType: BT.BOOLEAN, - text: tr('is input box [ID] interactive?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryIsMultiline', - blockType: BT.BOOLEAN, - text: tr('is input box [ID] multiline?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryEnterAction', - blockType: BT.BOOLEAN, - text: tr('when Enter pressed on input box [ID], is the mode [MODE]?'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } - } - }, - { - opcode: 'eventOnEnter', - blockType: BT.EVENT, - text: tr('when user presses Enter on an input box'), - isEdgeActivated: false - } - ] - }; + ] + }; + } } -} -Scratch.extensions.register(new InputBoxExtension()); \ No newline at end of file + Scratch.extensions.register(new InputBoxExtension()); +})(Scratch); \ No newline at end of file From 3723630d01b552b2d9742b27d2d7db38b5c9185e Mon Sep 17 00:00:00 2001 From: Zhuziexe Date: Wed, 8 Jul 2026 15:16:53 +0800 Subject: [PATCH 3/4] Add Input Boxes Extension --- extensions/Zhuziexe/inputboxes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Zhuziexe/inputboxes.js b/extensions/Zhuziexe/inputboxes.js index 185e7e5f64..5a20f0d8c4 100644 --- a/extensions/Zhuziexe/inputboxes.js +++ b/extensions/Zhuziexe/inputboxes.js @@ -7,7 +7,7 @@ (function (Scratch) { "use strict"; if (!Scratch.extensions.unsandboxed) { - throw new Error("SearchParams must be run unsandboxed."); + throw new Error("Input Boxes must be run unsandboxed."); } class InputBoxExtension { constructor() { From b0fb699ce50fc6d1a1d5fc0434245aabd82b8e3f Mon Sep 17 00:00:00 2001 From: "DangoCat[bot]" Date: Wed, 8 Jul 2026 07:34:58 +0000 Subject: [PATCH 4/4] [Automated] Format code --- extensions/Zhuziexe/inputboxes.js | 1388 +++++++++++++++-------------- 1 file changed, 732 insertions(+), 656 deletions(-) diff --git a/extensions/Zhuziexe/inputboxes.js b/extensions/Zhuziexe/inputboxes.js index 5a20f0d8c4..0744cb1c13 100644 --- a/extensions/Zhuziexe/inputboxes.js +++ b/extensions/Zhuziexe/inputboxes.js @@ -5,704 +5,780 @@ // License: MPL-2.0 (function (Scratch) { - "use strict"; - if (!Scratch.extensions.unsandboxed) { - throw new Error("Input Boxes must be run unsandboxed."); + "use strict"; + if (!Scratch.extensions.unsandboxed) { + throw new Error("Input Boxes must be run unsandboxed."); + } + class InputBoxExtension { + constructor() { + this.inputs = new Map(); + this.container = null; + this._lastEnterId = null; + this._resizeTimer = null; + + this._updatePending = false; + this._bindDocumentEvents(); + this._bindResizeAndFullscreen(); + + this._observeCanvas(); + this._observeModals(); + + setTimeout(() => this._updateAllInputs(), 100); } - class InputBoxExtension { - constructor() { - this.inputs = new Map(); - this.container = null; - this._lastEnterId = null; - this._resizeTimer = null; - this._updatePending = false; this._bindDocumentEvents(); - this._bindResizeAndFullscreen(); - - this._observeCanvas(); - this._observeModals(); + _observeModals() { + this._checkModalOpen(); + const observer = new MutationObserver(() => { + this._checkModalOpen(); + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["class", "style"], + }); + this._modalObserver = observer; + } - setTimeout(() => this._updateAllInputs(), 100); - } + _checkModalOpen() { + if (!this.container) return; + const modalSelectors = [ + ".modal", + ".ReactModalPortal", + ".ReactModal__Overlay", + ".ReactModal__Content", + ".modal-content", + ".dialog", + '[role="dialog"]', + '[role="modal"]', + ]; + let hasModal = false; + for (const sel of modalSelectors) { + if (document.querySelector(sel)) { + hasModal = true; + break; + } + } + this.container.style.display = hasModal ? "none" : "block"; + } - _observeModals() { - this._checkModalOpen(); - const observer = new MutationObserver(() => { - this._checkModalOpen(); - }); - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['class', 'style'] - }); - this._modalObserver = observer; - } + _bindDocumentEvents() { + if (this._bound) return; + this._bound = true; - _checkModalOpen() { - if (!this.container) return; - const modalSelectors = [ - '.modal', '.ReactModalPortal', '.ReactModal__Overlay', - '.ReactModal__Content', '.modal-content', '.dialog', - '[role="dialog"]', '[role="modal"]' - ]; - let hasModal = false; - for (const sel of modalSelectors) { - if (document.querySelector(sel)) { - hasModal = true; - break; - } + document.addEventListener( + "keydown", + (e) => { + const target = e.target; + if (target && target.dataset && target.dataset.inputBoxId) { + const id = target.dataset.inputBoxId; + const meta = this.inputs.get(id); + if (!meta) return; + + if (e.key === "Enter" && !e.shiftKey) { + const mode = meta.enterMode || "both"; + let trigger = false, + prevent = false; + switch (mode) { + case "trigger": + trigger = true; + prevent = true; + break; + case "newline": + trigger = false; + prevent = false; + break; + case "both": + trigger = true; + prevent = false; + break; + default: + trigger = true; + prevent = false; + } + if (prevent) e.preventDefault(); + + if (trigger) { + this._lastEnterId = id; + const fullOpcode = "inputboxes_eventOnEnter"; + this._safeStartHats(fullOpcode); + } } - this.container.style.display = hasModal ? 'none' : 'block'; - } - - _bindDocumentEvents() { - if (this._bound) return; - this._bound = true; - - document.addEventListener('keydown', (e) => { - const target = e.target; - if (target && target.dataset && target.dataset.inputBoxId) { - const id = target.dataset.inputBoxId; - const meta = this.inputs.get(id); - if (!meta) return; - - if (e.key === 'Enter' && !e.shiftKey) { - const mode = meta.enterMode || 'both'; - let trigger = false, prevent = false; - switch (mode) { - case 'trigger': trigger = true; prevent = true; break; - case 'newline': trigger = false; prevent = false; break; - case 'both': trigger = true; prevent = false; break; - default: trigger = true; prevent = false; - } - if (prevent) e.preventDefault(); - - if (trigger) { - this._lastEnterId = id; - const fullOpcode = 'inputboxes_eventOnEnter'; - this._safeStartHats(fullOpcode); - } - } - } - }, true); - } + } + }, + true + ); + } - _safeStartHats(opcode) { - const runtime = Scratch && Scratch.vm && Scratch.vm.runtime; - if (!runtime) { - setTimeout(() => this._safeStartHats(opcode), 100); - return; - } - if (!runtime._cache) { - try { const scripts = runtime.scripts; } catch (e) { } - if (!runtime._cache) runtime._cache = {}; - } + _safeStartHats(opcode) { + const runtime = Scratch && Scratch.vm && Scratch.vm.runtime; + if (!runtime) { + setTimeout(() => this._safeStartHats(opcode), 100); + return; + } + if (!runtime._cache) { + try { + const scripts = runtime.scripts; + } catch (e) {} + if (!runtime._cache) runtime._cache = {}; + } + try { + runtime.startHats(opcode); + } catch (e) { + setTimeout(() => { + if (runtime._cache) { try { - runtime.startHats(opcode); - } catch (e) { - setTimeout(() => { - if (runtime._cache) { - try { runtime.startHats(opcode); } catch (ex) { } - } - }, 50); - } - } + runtime.startHats(opcode); + } catch (ex) {} + } + }, 50); + } + } - _bindResizeAndFullscreen() { - const events = ['resize', 'fullscreenchange', 'webkitfullscreenchange']; - const handler = () => { - if (this._resizeTimer) clearTimeout(this._resizeTimer); - this._resizeTimer = setTimeout(() => { - this._updateAllInputs(); - this._checkModalOpen(); - this._resizeTimer = null; - }, 50); - }; - for (const ev of events) { - window.addEventListener(ev, handler); - } - this._resizeHandler = handler; - } + _bindResizeAndFullscreen() { + const events = ["resize", "fullscreenchange", "webkitfullscreenchange"]; + const handler = () => { + if (this._resizeTimer) clearTimeout(this._resizeTimer); + this._resizeTimer = setTimeout(() => { + this._updateAllInputs(); + this._checkModalOpen(); + this._resizeTimer = null; + }, 50); + }; + for (const ev of events) { + window.addEventListener(ev, handler); + } + this._resizeHandler = handler; + } - _observeCanvas() { - const canvas = document.querySelector('canvas'); - if (!canvas) return; - const observer = new MutationObserver(() => { - if (!this._updatePending) { - this._updatePending = true; - requestAnimationFrame(() => { - this._updateAllInputs(); - this._updatePending = false; - }); - } - }); - observer.observe(canvas, { - attributes: true, - attributeFilter: ['style', 'class', 'width', 'height'], - subtree: false + _observeCanvas() { + const canvas = document.querySelector("canvas"); + if (!canvas) return; + const observer = new MutationObserver(() => { + if (!this._updatePending) { + this._updatePending = true; + requestAnimationFrame(() => { + this._updateAllInputs(); + this._updatePending = false; + }); + } + }); + observer.observe(canvas, { + attributes: true, + attributeFilter: ["style", "class", "width", "height"], + subtree: false, + }); + this._canvasObserver = observer; + const parent = canvas.parentElement; + if (parent) { + const parentObserver = new MutationObserver(() => { + if (!this._updatePending) { + this._updatePending = true; + requestAnimationFrame(() => { + this._updateAllInputs(); + this._updatePending = false; }); - this._canvasObserver = observer; - const parent = canvas.parentElement; - if (parent) { - const parentObserver = new MutationObserver(() => { - if (!this._updatePending) { - this._updatePending = true; - requestAnimationFrame(() => { - this._updateAllInputs(); - this._updatePending = false; - }); - } - }); - parentObserver.observe(parent, { - attributes: true, - attributeFilter: ['style', 'class'], - subtree: false - }); - this._parentObserver = parentObserver; - } - } - - _updateAllInputs() { - if (this.inputs.size === 0) return; - const canvas = document.querySelector('canvas'); - if (!canvas) return; - const container = this.getContainer(); - if (!container) return; - - const rect = canvas.getBoundingClientRect(); - const scale = rect.width / 480; - - for (const [id, meta] of this.inputs) { - if (!meta.element) continue; - const el = meta.element; - const left = meta.logicX * scale + rect.width / 2; - const top = rect.height / 2 - meta.logicY * scale; - el.style.left = left + 'px'; - el.style.top = top + 'px'; - el.style.width = (meta.logicWidth * scale) + 'px'; - el.style.height = (meta.logicHeight * scale) + 'px'; - } - this._checkModalOpen(); - } - - getContainer() { - const canvas = document.querySelector('canvas'); - if (!canvas) { - if (!this.container) { - const container = document.createElement('div'); - container.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:1000'; - document.body.appendChild(container); - this.container = container; - } - return this.container; - } - - const parent = canvas.parentElement; - if (getComputedStyle(parent).position === 'static') { - parent.style.position = 'relative'; - } - - if (this.container && this.container.parentElement !== parent) { - this.container.remove(); - this.container = null; - } - - if (!this.container) { - const container = document.createElement('div'); - container.id = 'tw-input-box-container'; - container.style.position = 'absolute'; - container.style.top = '0'; - container.style.left = '0'; - container.style.pointerEvents = 'none'; - container.style.overflow = 'hidden'; - container.style.zIndex = '1000'; - parent.appendChild(container); - this.container = container; - } + } + }); + parentObserver.observe(parent, { + attributes: true, + attributeFilter: ["style", "class"], + subtree: false, + }); + this._parentObserver = parentObserver; + } + } - const rect = canvas.getBoundingClientRect(); - const parentRect = parent.getBoundingClientRect(); - this.container.style.left = (rect.left - parentRect.left) + 'px'; - this.container.style.top = (rect.top - parentRect.top) + 'px'; - this.container.style.width = rect.width + 'px'; - this.container.style.height = rect.height + 'px'; + _updateAllInputs() { + if (this.inputs.size === 0) return; + const canvas = document.querySelector("canvas"); + if (!canvas) return; + const container = this.getContainer(); + if (!container) return; + + const rect = canvas.getBoundingClientRect(); + const scale = rect.width / 480; + + for (const [id, meta] of this.inputs) { + if (!meta.element) continue; + const el = meta.element; + const left = meta.logicX * scale + rect.width / 2; + const top = rect.height / 2 - meta.logicY * scale; + el.style.left = left + "px"; + el.style.top = top + "px"; + el.style.width = meta.logicWidth * scale + "px"; + el.style.height = meta.logicHeight * scale + "px"; + } + this._checkModalOpen(); + } - this._checkModalOpen(); - return this.container; - } + getContainer() { + const canvas = document.querySelector("canvas"); + if (!canvas) { + if (!this.container) { + const container = document.createElement("div"); + container.style.cssText = + "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:1000"; + document.body.appendChild(container); + this.container = container; + } + return this.container; + } + + const parent = canvas.parentElement; + if (getComputedStyle(parent).position === "static") { + parent.style.position = "relative"; + } + + if (this.container && this.container.parentElement !== parent) { + this.container.remove(); + this.container = null; + } + + if (!this.container) { + const container = document.createElement("div"); + container.id = "tw-input-box-container"; + container.style.position = "absolute"; + container.style.top = "0"; + container.style.left = "0"; + container.style.pointerEvents = "none"; + container.style.overflow = "hidden"; + container.style.zIndex = "1000"; + parent.appendChild(container); + this.container = container; + } + + const rect = canvas.getBoundingClientRect(); + const parentRect = parent.getBoundingClientRect(); + this.container.style.left = rect.left - parentRect.left + "px"; + this.container.style.top = rect.top - parentRect.top + "px"; + this.container.style.width = rect.width + "px"; + this.container.style.height = rect.height + "px"; + + this._checkModalOpen(); + return this.container; + } - cmdRefreshLayout() { - this._updateAllInputs(); - } + cmdRefreshLayout() { + this._updateAllInputs(); + } - _createInputElement(type) { - const isMulti = (type === 'multiline'); - const el = isMulti ? document.createElement('textarea') : document.createElement('input'); - if (!isMulti) el.type = 'text'; - el.style.cssText = - 'position:absolute;box-sizing:border-box;border:1px solid #000;outline:none;background:#fff;padding:4px;font-size:14px;pointer-events:auto;z-index:1001;border-radius:2px;' + - (isMulti ? 'resize:none;font-family:sans-serif;' : ''); - return el; - } + _createInputElement(type) { + const isMulti = type === "multiline"; + const el = isMulti + ? document.createElement("textarea") + : document.createElement("input"); + if (!isMulti) el.type = "text"; + el.style.cssText = + "position:absolute;box-sizing:border-box;border:1px solid #000;outline:none;background:#fff;padding:4px;font-size:14px;pointer-events:auto;z-index:1001;border-radius:2px;" + + (isMulti ? "resize:none;font-family:sans-serif;" : ""); + return el; + } - _getOrCreateInput(id, type = 'single') { - let meta = this.inputs.get(id); - if (!meta) { - const el = this._createInputElement(type); - el.dataset.inputBoxId = id; - const container = this.getContainer(); - if (container) container.appendChild(el); - meta = { - element: el, - type: type, - interactive: true, - borderVisible: true, - enterMode: 'both', - bgColor: '#ffffff', - logicX: 0, - logicY: 0, - logicWidth: 120, - logicHeight: 30 - }; - this.inputs.set(id, meta); - } - return meta; - } + _getOrCreateInput(id, type = "single") { + let meta = this.inputs.get(id); + if (!meta) { + const el = this._createInputElement(type); + el.dataset.inputBoxId = id; + const container = this.getContainer(); + if (container) container.appendChild(el); + meta = { + element: el, + type: type, + interactive: true, + borderVisible: true, + enterMode: "both", + bgColor: "#ffffff", + logicX: 0, + logicY: 0, + logicWidth: 120, + logicHeight: 30, + }; + this.inputs.set(id, meta); + } + return meta; + } - _applyMeta(meta) { - if (!meta || !meta.element) return; - const el = meta.element; - try { - el.style.border = meta.borderVisible ? '1px solid #000' : 'none'; - el.disabled = !meta.interactive; - } catch (e) { } - } + _applyMeta(meta) { + if (!meta || !meta.element) return; + const el = meta.element; + try { + el.style.border = meta.borderVisible ? "1px solid #000" : "none"; + el.disabled = !meta.interactive; + } catch (e) {} + } - _switchType(id, newType) { - const meta = this.inputs.get(id); - if (!meta || meta.type === newType) return; - const old = meta.element; - const backup = { - value: old.value || '', - left: old.style.left || '0px', - top: old.style.top || '0px', - width: old.style.width || '120px', - height: old.style.height || '30px', - bg: old.style.backgroundColor || '#ffffff', - placeholder: old.placeholder || '' - }; - old.remove(); - - const el = this._createInputElement(newType); - el.dataset.inputBoxId = id; - el.value = backup.value; - el.style.left = backup.left; - el.style.top = backup.top; - el.style.width = backup.width; - el.style.height = backup.height; - el.style.backgroundColor = backup.bg; - el.placeholder = backup.placeholder; - - meta.element = el; - meta.type = newType; - meta.bgColor = backup.bg; - this._applyMeta(meta); - const container = this.getContainer(); - if (container) container.appendChild(el); - this._updateAllInputs(); - } - cmdCreateOrMove(args) { - try { - const { ID, X, Y, WIDTH, HEIGHT } = args; - const meta = this._getOrCreateInput(ID, 'single'); - meta.logicX = X; - meta.logicY = Y; - meta.logicWidth = WIDTH; - meta.logicHeight = HEIGHT; - this._updateAllInputs(); - this._applyMeta(meta); - } catch (e) { } - } + _switchType(id, newType) { + const meta = this.inputs.get(id); + if (!meta || meta.type === newType) return; + const old = meta.element; + const backup = { + value: old.value || "", + left: old.style.left || "0px", + top: old.style.top || "0px", + width: old.style.width || "120px", + height: old.style.height || "30px", + bg: old.style.backgroundColor || "#ffffff", + placeholder: old.placeholder || "", + }; + old.remove(); + + const el = this._createInputElement(newType); + el.dataset.inputBoxId = id; + el.value = backup.value; + el.style.left = backup.left; + el.style.top = backup.top; + el.style.width = backup.width; + el.style.height = backup.height; + el.style.backgroundColor = backup.bg; + el.placeholder = backup.placeholder; + + meta.element = el; + meta.type = newType; + meta.bgColor = backup.bg; + this._applyMeta(meta); + const container = this.getContainer(); + if (container) container.appendChild(el); + this._updateAllInputs(); + } + cmdCreateOrMove(args) { + try { + const { ID, X, Y, WIDTH, HEIGHT } = args; + const meta = this._getOrCreateInput(ID, "single"); + meta.logicX = X; + meta.logicY = Y; + meta.logicWidth = WIDTH; + meta.logicHeight = HEIGHT; + this._updateAllInputs(); + this._applyMeta(meta); + } catch (e) {} + } - cmdDeleteAll() { - try { - for (const [id, meta] of this.inputs) if (meta.element) meta.element.remove(); - this.inputs.clear(); - } catch (e) { } - } + cmdDeleteAll() { + try { + for (const [id, meta] of this.inputs) + if (meta.element) meta.element.remove(); + this.inputs.clear(); + } catch (e) {} + } - cmdDeleteById(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - if (meta) { - if (meta.element) meta.element.remove(); - this.inputs.delete(ID); - } - } catch (e) { } + cmdDeleteById(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + if (meta) { + if (meta.element) meta.element.remove(); + this.inputs.delete(ID); } + } catch (e) {} + } - cmdSetBorder(args) { - try { - const { ID, VISIBILITY } = args; - const meta = this.inputs.get(ID); - if (meta) { - meta.borderVisible = (VISIBILITY === 'visible'); - this._applyMeta(meta); - } - } catch (e) { } + cmdSetBorder(args) { + try { + const { ID, VISIBILITY } = args; + const meta = this.inputs.get(ID); + if (meta) { + meta.borderVisible = VISIBILITY === "visible"; + this._applyMeta(meta); } + } catch (e) {} + } - cmdSetBg(args) { - try { - const { ID, BG_COLOR } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) { - meta.element.style.backgroundColor = BG_COLOR; - meta.bgColor = BG_COLOR; - } - } catch (e) { } + cmdSetBg(args) { + try { + const { ID, BG_COLOR } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) { + meta.element.style.backgroundColor = BG_COLOR; + meta.bgColor = BG_COLOR; } + } catch (e) {} + } - repTransparent() { return 'transparent'; } + repTransparent() { + return "transparent"; + } - cmdSetValue(args) { - try { - const { ID, CONTENT } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) meta.element.value = CONTENT; - } catch (e) { } - } + cmdSetValue(args) { + try { + const { ID, CONTENT } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) meta.element.value = CONTENT; + } catch (e) {} + } - cmdSetHint(args) { - try { - const { ID, HINT } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) meta.element.placeholder = HINT; - } catch (e) { } - } + cmdSetHint(args) { + try { + const { ID, HINT } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) meta.element.placeholder = HINT; + } catch (e) {} + } - repGetValue(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return (meta && meta.element) ? meta.element.value : ''; - } catch (e) { return ''; } - } + repGetValue(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta && meta.element ? meta.element.value : ""; + } catch (e) { + return ""; + } + } - repGetAllIds() { - try { - return JSON.stringify(Array.from(this.inputs.keys())); - } catch (e) { return '[]'; } - } + repGetAllIds() { + try { + return JSON.stringify(Array.from(this.inputs.keys())); + } catch (e) { + return "[]"; + } + } - boolExists(args) { - try { - const { ID } = args; - return this.inputs.has(ID); - } catch (e) { return false; } - } + boolExists(args) { + try { + const { ID } = args; + return this.inputs.has(ID); + } catch (e) { + return false; + } + } - repLastEnteredId() { - return this._lastEnterId || ''; - } + repLastEnteredId() { + return this._lastEnterId || ""; + } - cmdFocus(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - if (meta && meta.element) { - if (meta.element.disabled) { - meta.element.disabled = false; - meta.interactive = true; - } - meta.element.focus(); - meta.element.select(); - } - } catch (e) { } - } + cmdFocus(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + if (meta && meta.element) { + if (meta.element.disabled) { + meta.element.disabled = false; + meta.interactive = true; + } + meta.element.focus(); + meta.element.select(); + } + } catch (e) {} + } - cmdSetMode(args) { - try { - const { ID, MODE } = args; - if (MODE === 'single' || MODE === 'multiline') this._switchType(ID, MODE); - } catch (e) { } - } + cmdSetMode(args) { + try { + const { ID, MODE } = args; + if (MODE === "single" || MODE === "multiline") + this._switchType(ID, MODE); + } catch (e) {} + } - cmdSetEnabled(args) { - try { - const { ID, STATE } = args; - const meta = this.inputs.get(ID); - if (meta) { - meta.interactive = (STATE === 'enabled'); - this._applyMeta(meta); - } - } catch (e) { } + cmdSetEnabled(args) { + try { + const { ID, STATE } = args; + const meta = this.inputs.get(ID); + if (meta) { + meta.interactive = STATE === "enabled"; + this._applyMeta(meta); } + } catch (e) {} + } - queryBorderVisible(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return meta ? meta.borderVisible : false; - } catch (e) { return false; } - } + queryBorderVisible(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? meta.borderVisible : false; + } catch (e) { + return false; + } + } - queryInteractive(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return meta ? meta.interactive : false; - } catch (e) { return false; } - } + queryInteractive(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? meta.interactive : false; + } catch (e) { + return false; + } + } - queryIsMultiline(args) { - try { - const { ID } = args; - const meta = this.inputs.get(ID); - return meta ? (meta.type === 'multiline') : false; - } catch (e) { return false; } - } + queryIsMultiline(args) { + try { + const { ID } = args; + const meta = this.inputs.get(ID); + return meta ? meta.type === "multiline" : false; + } catch (e) { + return false; + } + } - repNewline() { return '\n'; } + repNewline() { + return "\n"; + } - cmdSetEnterAction(args) { - try { - const { ID, MODE } = args; - const meta = this.inputs.get(ID); - if (meta && ['trigger', 'newline', 'both'].includes(MODE)) meta.enterMode = MODE; - } catch (e) { } - } + cmdSetEnterAction(args) { + try { + const { ID, MODE } = args; + const meta = this.inputs.get(ID); + if (meta && ["trigger", "newline", "both"].includes(MODE)) + meta.enterMode = MODE; + } catch (e) {} + } - queryEnterAction(args) { - try { - const { ID, MODE } = args; - const meta = this.inputs.get(ID); - return meta ? (meta.enterMode === MODE) : false; - } catch (e) { return false; } - } + queryEnterAction(args) { + try { + const { ID, MODE } = args; + const meta = this.inputs.get(ID); + return meta ? meta.enterMode === MODE : false; + } catch (e) { + return false; + } + } - eventOnEnter() { } - - //-------------------------------------------------------------------------------- - - getInfo() { - const BT = Scratch.BlockType; - - //Easier function to translate - const tr = (defaultText, desc = '') => { - if (typeof Scratch.translate === 'function') { - return Scratch.translate({ default: defaultText, description: desc }); - } - return defaultText; - }; - - return { - id: 'inputboxes', - name: tr('Input Boxes'), - menus: { - visibilityMenu: { - items: [ - { text: tr('visible'), value: 'visible' }, - { text: tr('hidden'), value: 'hidden' } - ] - }, - typeMenu: { - items: [ - { text: tr('single line'), value: 'single' }, - { text: tr('multiline'), value: 'multiline' } - ] - }, - stateMenu: { - items: [ - { text: tr('enabled'), value: 'enabled' }, - { text: tr('disabled'), value: 'disabled' } - ] - }, - enterModeMenu: { - items: [ - { text: tr('trigger event only'), value: 'trigger' }, - { text: tr('newline only (no event)'), value: 'newline' }, - { text: tr('both trigger and newline'), value: 'both' } - ] - } - }, - blocks: [ - //BT.COMMAND Blocks - { - opcode: 'cmdCreateOrMove', - blockType: BT.COMMAND, - text: tr('create/move input box ID: [ID] x: [X] y: [Y] width: [WIDTH] height: [HEIGHT]', - 'ID is the unique identifier for the input box.'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - X: { type: 'number', defaultValue: 0 }, - Y: { type: 'number', defaultValue: 0 }, - WIDTH: { type: 'number', defaultValue: 120 }, - HEIGHT: { type: 'number', defaultValue: 30 } - } - }, - { - opcode: 'cmdDeleteAll', - blockType: BT.COMMAND, - text: tr('delete all input boxes') - }, - { - opcode: 'cmdDeleteById', - blockType: BT.COMMAND, - text: tr('delete input box with ID [ID]'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'cmdSetBorder', - blockType: BT.COMMAND, - text: tr('set border of input box [ID] to [VISIBILITY]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - VISIBILITY: { type: 'string', menu: 'visibilityMenu', defaultValue: 'visible' } - } - }, - { - opcode: 'cmdSetBg', - blockType: BT.COMMAND, - text: tr('set background color of input box [ID] to [BG_COLOR]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - BG_COLOR: { type: 'color', defaultValue: '#ffffff' } - } - }, - { - opcode: 'cmdSetValue', - blockType: BT.COMMAND, - text: tr('set input box [ID] value to [CONTENT]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - CONTENT: { type: 'string', defaultValue: '' } - } - }, - { - opcode: 'cmdSetHint', - blockType: BT.COMMAND, - text: tr('set input box [ID] hint to [HINT]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - HINT: { type: 'string', defaultValue: '' } - } - }, - { - opcode: 'cmdFocus', - blockType: BT.COMMAND, - text: tr('focus input box [ID]'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'cmdSetMode', - blockType: BT.COMMAND, - text: tr('set input box [ID] to [MODE]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - MODE: { type: 'string', menu: 'typeMenu', defaultValue: 'single' } - } - }, - { - opcode: 'cmdSetEnabled', - blockType: BT.COMMAND, - text: tr('set input box [ID] interactive to [STATE]'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - STATE: { type: 'string', menu: 'stateMenu', defaultValue: 'enabled' } - } - }, - { - opcode: 'cmdSetEnterAction', - blockType: BT.COMMAND, - text: tr('when Enter pressed on input box [ID], do [MODE]in multiline mode'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } - } - }, - { - blockType: 'label', - text: tr('If set to trigger event, use Shift+Enter for newline') - }, - { - opcode: 'cmdRefreshLayout', - blockType: BT.COMMAND, - text: tr('refresh input box layout') - }, - //BT.REPORTER Blocks - { - opcode: 'repTransparent', - blockType: BT.REPORTER, - text: tr('transparent') - }, - { - opcode: 'repGetValue', - blockType: BT.REPORTER, - text: tr('value of input box [ID]'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'repGetAllIds', - blockType: BT.REPORTER, - text: tr('all input boxes (JSON array)') - }, - { - opcode: 'repLastEnteredId', - blockType: BT.REPORTER, - text: tr('the ID of the input box where Enter was pressed') - }, - { - opcode: 'repNewline', - blockType: BT.REPORTER, - text: tr('newline') - }, - //BT.BOOLEAN Blocks - { - opcode: 'boolExists', - blockType: BT.BOOLEAN, - text: tr('does input box [ID] exist?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryBorderVisible', - blockType: BT.BOOLEAN, - text: tr('is border of input box [ID] visible?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryInteractive', - blockType: BT.BOOLEAN, - text: tr('is input box [ID] interactive?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryIsMultiline', - blockType: BT.BOOLEAN, - text: tr('is input box [ID] multiline?'), - arguments: { ID: { type: 'string', defaultValue: 'inputbox' } } - }, - { - opcode: 'queryEnterAction', - blockType: BT.BOOLEAN, - text: tr('when Enter pressed on input box [ID], is the mode [MODE]?'), - arguments: { - ID: { type: 'string', defaultValue: 'inputbox' }, - MODE: { type: 'string', menu: 'enterModeMenu', defaultValue: 'both' } - } - }, - { - opcode: 'eventOnEnter', - blockType: BT.EVENT, - text: tr('when user presses Enter on an input box'), - isEdgeActivated: false - } - ] - }; - } + eventOnEnter() {} + + //-------------------------------------------------------------------------------- + + getInfo() { + const BT = Scratch.BlockType; + + //Easier function to translate + const tr = (defaultText, desc = "") => { + if (typeof Scratch.translate === "function") { + return Scratch.translate({ default: defaultText, description: desc }); + } + return defaultText; + }; + + return { + id: "inputboxes", + name: tr("Input Boxes"), + menus: { + visibilityMenu: { + items: [ + { text: tr("visible"), value: "visible" }, + { text: tr("hidden"), value: "hidden" }, + ], + }, + typeMenu: { + items: [ + { text: tr("single line"), value: "single" }, + { text: tr("multiline"), value: "multiline" }, + ], + }, + stateMenu: { + items: [ + { text: tr("enabled"), value: "enabled" }, + { text: tr("disabled"), value: "disabled" }, + ], + }, + enterModeMenu: { + items: [ + { text: tr("trigger event only"), value: "trigger" }, + { text: tr("newline only (no event)"), value: "newline" }, + { text: tr("both trigger and newline"), value: "both" }, + ], + }, + }, + blocks: [ + //BT.COMMAND Blocks + { + opcode: "cmdCreateOrMove", + blockType: BT.COMMAND, + text: tr( + "create/move input box ID: [ID] x: [X] y: [Y] width: [WIDTH] height: [HEIGHT]", + "ID is the unique identifier for the input box." + ), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + X: { type: "number", defaultValue: 0 }, + Y: { type: "number", defaultValue: 0 }, + WIDTH: { type: "number", defaultValue: 120 }, + HEIGHT: { type: "number", defaultValue: 30 }, + }, + }, + { + opcode: "cmdDeleteAll", + blockType: BT.COMMAND, + text: tr("delete all input boxes"), + }, + { + opcode: "cmdDeleteById", + blockType: BT.COMMAND, + text: tr("delete input box with ID [ID]"), + arguments: { ID: { type: "string", defaultValue: "inputbox" } }, + }, + { + opcode: "cmdSetBorder", + blockType: BT.COMMAND, + text: tr("set border of input box [ID] to [VISIBILITY]"), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + VISIBILITY: { + type: "string", + menu: "visibilityMenu", + defaultValue: "visible", + }, + }, + }, + { + opcode: "cmdSetBg", + blockType: BT.COMMAND, + text: tr("set background color of input box [ID] to [BG_COLOR]"), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + BG_COLOR: { type: "color", defaultValue: "#ffffff" }, + }, + }, + { + opcode: "cmdSetValue", + blockType: BT.COMMAND, + text: tr("set input box [ID] value to [CONTENT]"), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + CONTENT: { type: "string", defaultValue: "" }, + }, + }, + { + opcode: "cmdSetHint", + blockType: BT.COMMAND, + text: tr("set input box [ID] hint to [HINT]"), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + HINT: { type: "string", defaultValue: "" }, + }, + }, + { + opcode: "cmdFocus", + blockType: BT.COMMAND, + text: tr("focus input box [ID]"), + arguments: { ID: { type: "string", defaultValue: "inputbox" } }, + }, + { + opcode: "cmdSetMode", + blockType: BT.COMMAND, + text: tr("set input box [ID] to [MODE]"), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + MODE: { + type: "string", + menu: "typeMenu", + defaultValue: "single", + }, + }, + }, + { + opcode: "cmdSetEnabled", + blockType: BT.COMMAND, + text: tr("set input box [ID] interactive to [STATE]"), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + STATE: { + type: "string", + menu: "stateMenu", + defaultValue: "enabled", + }, + }, + }, + { + opcode: "cmdSetEnterAction", + blockType: BT.COMMAND, + text: tr( + "when Enter pressed on input box [ID], do [MODE]in multiline mode" + ), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + MODE: { + type: "string", + menu: "enterModeMenu", + defaultValue: "both", + }, + }, + }, + { + blockType: "label", + text: tr("If set to trigger event, use Shift+Enter for newline"), + }, + { + opcode: "cmdRefreshLayout", + blockType: BT.COMMAND, + text: tr("refresh input box layout"), + }, + //BT.REPORTER Blocks + { + opcode: "repTransparent", + blockType: BT.REPORTER, + text: tr("transparent"), + }, + { + opcode: "repGetValue", + blockType: BT.REPORTER, + text: tr("value of input box [ID]"), + arguments: { ID: { type: "string", defaultValue: "inputbox" } }, + }, + { + opcode: "repGetAllIds", + blockType: BT.REPORTER, + text: tr("all input boxes (JSON array)"), + }, + { + opcode: "repLastEnteredId", + blockType: BT.REPORTER, + text: tr("the ID of the input box where Enter was pressed"), + }, + { + opcode: "repNewline", + blockType: BT.REPORTER, + text: tr("newline"), + }, + //BT.BOOLEAN Blocks + { + opcode: "boolExists", + blockType: BT.BOOLEAN, + text: tr("does input box [ID] exist?"), + arguments: { ID: { type: "string", defaultValue: "inputbox" } }, + }, + { + opcode: "queryBorderVisible", + blockType: BT.BOOLEAN, + text: tr("is border of input box [ID] visible?"), + arguments: { ID: { type: "string", defaultValue: "inputbox" } }, + }, + { + opcode: "queryInteractive", + blockType: BT.BOOLEAN, + text: tr("is input box [ID] interactive?"), + arguments: { ID: { type: "string", defaultValue: "inputbox" } }, + }, + { + opcode: "queryIsMultiline", + blockType: BT.BOOLEAN, + text: tr("is input box [ID] multiline?"), + arguments: { ID: { type: "string", defaultValue: "inputbox" } }, + }, + { + opcode: "queryEnterAction", + blockType: BT.BOOLEAN, + text: tr( + "when Enter pressed on input box [ID], is the mode [MODE]?" + ), + arguments: { + ID: { type: "string", defaultValue: "inputbox" }, + MODE: { + type: "string", + menu: "enterModeMenu", + defaultValue: "both", + }, + }, + }, + { + opcode: "eventOnEnter", + blockType: BT.EVENT, + text: tr("when user presses Enter on an input box"), + isEdgeActivated: false, + }, + ], + }; } + } - Scratch.extensions.register(new InputBoxExtension()); -})(Scratch); \ No newline at end of file + Scratch.extensions.register(new InputBoxExtension()); +})(Scratch);