diff --git a/extensions/SharkPool/Popup-Phoenix.js b/extensions/SharkPool/Popup-Phoenix.js new file mode 100644 index 0000000000..6b53f5c52b --- /dev/null +++ b/extensions/SharkPool/Popup-Phoenix.js @@ -0,0 +1,1829 @@ +// Name: Popup Phoenix +// ID: SPpopups +// Description: Create custom Popups. Inspired by 'Better Input'. +// By: SharkPool +// Licence: MIT + +// Version V.1.0.1 + +(function (Scratch) { + "use strict"; + if (!Scratch.extensions.unsandboxed) + throw new Error("Popup Phoenix must run unsandboxed!"); + + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4Ny41MzIiIGhlaWdodD0iODcuNTMyIiB2aWV3Qm94PSIwIDAgODcuNTMyIDg3LjUzMiI+PGcgc3Ryb2tlLXdpZHRoPSIwIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiPjxwYXRoIGQ9Ik0uMDAxIDQzLjc2N0MuMDAxIDE5LjU5NSAxOS41OTQuMDAxIDQzLjc2Ni4wMDFzNDMuNzY2IDE5LjU5NCA0My43NjYgNDMuNzY2LTE5LjU5NCA0My43NjYtNDMuNzY2IDQzLjc2NlMwIDY3LjkzOSAwIDQzLjc2NyIgZmlsbD0iIzgyMTliZiIvPjxwYXRoIGQ9Ik01LjE2NSA0My43NjdjMC0yMS4zMiAxNy4yODItMzguNjAyIDM4LjYwMi0zOC42MDJzMzguNjAyIDE3LjI4MiAzOC42MDIgMzguNjAyLTE3LjI4MiAzOC42MDItMzguNjAyIDM4LjYwMlM1LjE2NSA2NS4wODcgNS4xNjUgNDMuNzY3IiBmaWxsPSIjYTgxMmZmIi8+PHBhdGggZD0ibTQwLjM5NyA1OC4wODctMy44NS42MDctLjE4MSA0Ljg3LTMuMTYgMS44MjUtMS40NDQtNi4yMS00LjEzOS0yLjU1NiAxLjkwNC0zLjMzNCA0LjQ3NiAyLjAwOCA0LjQ3NS0xLjQ0MS0zLjM4NC00LjUyNi0yLjY4OSAyLjAwMS00LjkyNy0uODc0IDIuNDY1LTMuNTktOC40NjUgMi4yOTItMi41NTYtNC4wNDUgOC4wNi0xLjgyOC0xMC4wNjgtLjc2My0xLjg0Ni01LjQ1MSAxMC4yOTEgMS4yMjctMTAuMzYtMy45OC45Ny02Ljc3OCA5LjQ4MyA1LjkzNC04LjA3NS05Ljc0NSAzLjI5Ni01LjM1NiAxMi40MjYgMTQuODA4LS4zNzQgNy41OCA1LjMzOSAxLjAyMSA1LjM2NS0xMS4xOTggOC4wMDMgMS41ODQtLjI5IDQuMzk3LTIuODk5LTEuMDM1LS4yMzMgMy43MSAxLjgzNSAyLjU0MiA0Ljc1Ny0xLjEtLjE2Mi02LjY5NCA3LjAyMy03LjY2OCA1LjI5Ny03LjQwOCAzLjI3MiAzLjkzNS03LjgxMSAxMS4yNDcgOS4xNzUtNy4wNi4xODggNy40MzMtOS40MDEgNC4yNTEgMTAuMzUtMS41My0xLjYyMSA2LjAzLTEwLjEzMy4xNDkgOS4xMTUgMS41Ni0zLjkwNyA0Ljc0OC04LjktMi45MTYgMi4zNDggMy4zODctNC43MDMgMS41Mi0yLjI4LTIuMjIzLTMuNjc5IDQuOTU0IDQuMTQ3LjcwMiA1LjEwNy0yLjMgMS41NjUgMy44NC00LjMyMyAyLS45NzEgNi41MzgtMy41ODgtMS4xMzMuMDM5LTUuMDY1LTMuOTM3LS42MzMuNDMgMi43MDMgNC4zIDYuNTI4LTIuODMyIDMuMzQtMi45NzMtMy4wMjMuNzUgNS4zNy0zLjE5NyAyLjg5NC0yLjk0LTIuOTYuNTI2LTUuMjk3LTIuNzggMy4xNTUtMi41NjQtMy42NCA0LjM2OS02LjU2NHoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"; + const getBlockIcon = (name) => { + // lower file size moment + const start = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI"; + return ( + start + + { + main: "3OS4xOTkiIGhlaWdodD0iODcuOTIyIiB2aWV3Qm94PSIwIDAgNzkuMTk5IDg3LjkyMiI+PHBhdGggZD0ibTI1LjkwMSA2NS41MDctNS4yNTUtLjA4Yy0xMS4wMy0uMDgtMTEuNzA3LS4wOC0xMi44ODktLjU5OC0uMzctLjE1Mi0yLjc1OC0yLjQyOS0yLjc1OC0yLjQyOVYyNS40NzdzMi4wODMtMi4xNSAyLjc1OC0yLjQzYzEuMTI2LS40NzggMS44NTgtLjUxOCAxMi44OS0uNTk3bDUuMjUzLS4wOHoiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIgb3BhY2l0eT0iLjI1Ii8+PHBhdGggZD0iTTMwLjQ2OSA2NS41MDdzLTIxLjUzLS4xNi0yMi43MS0uNjc3Yy0uNjc3LS4yOC0yLjc2LTIuNDMtMi43Ni0yLjQzVjI1LjQ3OHMyLjA4My0yLjE1MSAyLjc1OC0yLjQzYzEuMTI3LS40NzggMjIuNzEyLS42NzcgMjIuNzEyLS42Nzd6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTUzLjYyOCAyMi4zNyA0LjkyMy4wOGMxMS4wMzEuMDggMTEuNzYzLjEyIDEyLjg5LjU5OC42NzQuMjc5IDIuNzU3IDIuNDMgMi43NTcgMi40M3YzNi45MjNzLTIuMDgzIDIuMTUtMi43NTggMi40MjljLTEuMTgyLjUxOC0xLjg1OC41MTgtMTIuODg5LjU5OGwtNC45MjMuMDh6IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiIG9wYWNpdHk9Ii4yNSIvPjxwYXRoIGQ9Ik00OC44OTUgMjIuMzdzMjEuNTg1LjIgMjIuNzEuNjc4Yy42NzYuMjc5IDIuNzU5IDIuNDMgMi43NTkgMi40M3YzNi45MjNzLTIuMDgzIDIuMTUtMi43NTggMi40MjljLTEuMTgyLjUxOC0yMi43MTEuNjc3LTIyLjcxMS42Nzd6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTI5LjI3OCA1LjAwNSAxMC44MzIuMDRjMTAuNTE2LjA4IDEwLjgzNC4xMiAxMS41MTEuNTk2IDIuNDMgMS44NzIgMi4zOSA0Ljk0LS4xNTkgNi42NTItLjc1Ny41MTgtMS4xMTUuNTU3LTQuMzQuNTU3aC0zLjUwNnY2Mi4xNzZoMy40NjVjMy45NDMgMCA0LjkuMzE4IDUuODE1IDEuOTUxIDEuMDM2IDEuODMzLjUxOCAzLjk0NC0xLjI3NSA1LjI1OC0uNjc3LjUxOC0uOTk1LjUxOC0xMS41MS41OThsLTEwLjgzNC4wOC0xLjExNi0uNTU4Yy0xLjE1NS0uNjM4LTIuMTktMi4xNS0yLjE5LTMuMzg2IDAtLjg3NS45OTUtMi42NjggMS43OTItMy4yNjUuNTk3LS40NzggMS4xMTUtLjUxOCA0LjM0Mi0uNTk4bDMuNjY0LS4wOFYxMi44OWwtMy42NjQtLjA4Yy0zLjIyNy0uMDgtMy43NDUtLjExOS00LjM0Mi0uNTk3LS43OTctLjU1OC0xLjc5Mi0yLjM1LTEuNzkyLTMuMjY2IDAtMS4yMzUuOTk1LTIuNzg4IDIuMTktMy4zODZ6IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiIG9wYWNpdHk9Ii4yNSIvPjxwYXRoIGQ9Im0yOS4yNzggNS4wMDUgMTAuODMyLjA0YzEwLjUxNi4wOCAxMC44MzQuMTIgMTEuNTExLjU5NiAyLjQzIDEuODcyIDIuMzkgNC45NC0uMTU5IDYuNjUyLS43NTcuNTE4LTEuMTE1LjU1Ny00LjM0LjU1N2gtMy41MDZ2NjIuMTc2aDMuNDY1YzMuOTQzIDAgNC45LjMxOCA1LjgxNSAxLjk1MSAxLjAzNiAxLjgzMy41MTggMy45NDQtMS4yNzUgNS4yNTgtLjY3Ny41MTgtLjk5NS41MTgtMTEuNTEuNTk4bC0xMC44MzQuMDgtMS4xMTYtLjU1OGMtMS4xNTUtLjYzOC0yLjE5LTIuMTUtMi4xOS0zLjM4NiAwLS44NzUuOTk1LTIuNjY4IDEuNzkyLTMuMjY1LjU5Ny0uNDc4IDEuMTE1LS41MTggNC4zNDItLjU5OGwzLjY2NC0uMDhWMTIuODlsLTMuNjY0LS4wOGMtMy4yMjctLjA4LTMuNzQ1LS4xMTktNC4zNDItLjU5Ny0uNzk3LS41NTgtMS43OTItMi4zNS0xLjc5Mi0zLjI2NiAwLTEuMjM1Ljk5NS0yLjc4OCAyLjE5LTMuMzg2eiIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg==", + color: + "3NS41MzUiIGhlaWdodD0iNzEuNDk3IiB2aWV3Qm94PSIwIDAgNzUuNTM1IDcxLjQ5NyI+PHBhdGggZD0iTTIxLjMwNyAyMS40NjJDMjEuMzA3IDEyLjM3IDI4LjY3NyA1IDM3Ljc2OCA1czE2LjQ2MSA3LjM3IDE2LjQ2MSAxNi40NjItNy4zNyAxNi40NjEtMTYuNDYxIDE2LjQ2MS0xNi40NjEtNy4zNy0xNi40NjEtMTYuNDYxeiIgc3Ryb2tlPSIjMDAwIiBvcGFjaXR5PSIuMjUiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNNSA1MC4wMzZjMC05LjA5MSA3LjM3LTE2LjQ2MSAxNi40NjItMTYuNDYxczE2LjQ2MSA3LjM3IDE2LjQ2MSAxNi40NjFjMCA5LjA5Mi03LjM3IDE2LjQ2Mi0xNi40NjEgMTYuNDYyQzEyLjM3IDY2LjQ5OCA1IDU5LjEyOCA1IDUwLjAzNnoiIHN0cm9rZT0iIzAwMCIgb3BhY2l0eT0iLjI1IiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTM3LjYxMyA1MC4wMzZjMC05LjA5MSA3LjM3LTE2LjQ2MSAxNi40NjEtMTYuNDYxIDkuMDkyIDAgMTYuNDYyIDcuMzcgMTYuNDYyIDE2LjQ2MSAwIDkuMDkyLTcuMzcgMTYuNDYyLTE2LjQ2MiAxNi40NjJzLTE2LjQ2MS03LjM3LTE2LjQ2MS0xNi40NjJ6IiBzdHJva2U9IiMwMDAiIG9wYWNpdHk9Ii4yNSIgc3Ryb2tlLXdpZHRoPSIxMCIvPjxwYXRoIGQ9Ik0yMS4zMDcgMjEuNDYyQzIxLjMwNyAxMi4zNyAyOC42NzcgNSAzNy43NjggNXMxNi40NjEgNy4zNyAxNi40NjEgMTYuNDYyLTcuMzcgMTYuNDYxLTE2LjQ2MSAxNi40NjEtMTYuNDYxLTcuMzctMTYuNDYxLTE2LjQ2MSIgZmlsbD0iIzBmMCIvPjxwYXRoIGQ9Ik01IDUwLjAzNmMwLTkuMDkxIDcuMzctMTYuNDYxIDE2LjQ2Mi0xNi40NjFzMTYuNDYxIDcuMzcgMTYuNDYxIDE2LjQ2MWMwIDkuMDkyLTcuMzcgMTYuNDYyLTE2LjQ2MSAxNi40NjJDMTIuMzcgNjYuNDk4IDUgNTkuMTI4IDUgNTAuMDM2IiBmaWxsPSJyZWQiLz48cGF0aCBkPSJNMzcuNjEzIDUwLjAzNmMwLTkuMDkxIDcuMzctMTYuNDYxIDE2LjQ2MS0xNi40NjEgOS4wOTIgMCAxNi40NjIgNy4zNyAxNi40NjIgMTYuNDYxIDAgOS4wOTItNy4zNyAxNi40NjItMTYuNDYyIDE2LjQ2MnMtMTYuNDYxLTcuMzctMTYuNDYxLTE2LjQ2MiIgZmlsbD0iIzAwZiIvPjwvc3ZnPg==", + effect: + "1Ny40NDIiIGhlaWdodD0iNzAuNDUiIHZpZXdCb3g9IjAgMCA1Ny40NDIgNzAuNDUiPjxwYXRoIGQ9Ik01IDQ5LjA0N2MtMy45ODgtOC4xMTctMy45OTYtMTguMjQzLjE1My0yNi40MUM5Ljg0IDEzLjA2MiAxNy43MyA4LjgxOCAyMC45IDcuNDM4YTMxLjQgMzEuNCAwIDAgMSA2LjA3Ni0yLjA4M2wyLjEyLS40NTdjMS41NTgtLjMyIDMuMTEuNjQ2IDMuNDQ3IDIuMTg3LjM0IDEuNTQ1LS42NTEgMy4wNjYtMi4yMTYgMy40MDJsLTIuMDYzLjQ0NWEyNS40IDI1LjQgMCAwIDAtNC45NiAxLjcxNmMtMi4wODMuOTAyLTguOTc0IDQuMzkyLTEyLjk0OSAxMi41MTUtMy4wMTcgNS45MzMtMy45MTcgMTUuMzY2LjgzMiAyMy4xNSA0LjMwMyA3LjQ5OCAxMy4xMTIgMTIuMDk1IDIxLjQ2OCAxMS4zMjEgNy45MDMtLjU2OCAxNS4xNTUtNS44MjggMTcuNjk4LTEyLjc5NSAyLjU4MS02LjU1Mi44NjYtMTMuMjY5LTEuOTY3LTE3LjA1Mi0zLjQxMy00LjY2LTcuNjYtNi4wOTQtOS4zMjgtNi40OTUtLjI0OS0uMDc0LTYuMDg1LTEuODI3LTExLjI0Ljc1NC0yLjIyIDEuMDY0LTUuMDkgMy40NTYtNi42MjggNy4wNS0xLjY2IDMuNzEyLTEuMjMgOC40ODQgMS4wMiAxMS42NTUgMi4yNjMgMy4zOTkgNi41MzIgNS4zMjMgMTAuMjAzIDQuNjcgMy42NTUtLjU3NSA2LjEyNy0zLjI5NCA2LjcxMy01LjY4My42ODktMi41NjgtLjM2OC00LjczOS0xLjE5My01LjUyLTEuNDAxLTEuMzgzLTIuNjExLTEuNDA2LTIuNjYzLTEuNDFhNSA1IDAgMCAwLTEuMDI2LjA1NGMtLjY1NC4yNC0xLjU4Mi43NjQtMS44NTQgMS4zMTYtLjA0OC4wOS0uMTcuMzQuMDY0Ljk2My41NjMgMS40NzctLjE5NyAzLjEzLTEuNjk0IDMuNjg1LTEuNDkuNTU4LTMuMTY3LS4xOTItMy43MzMtMS42NzMtLjkzMS0yLjQ1LS40MTYtNC4zNjguMTgtNS41NDYgMS41NTgtMy4wNjYgNS4xNzYtNC4xNzYgNS41ODUtNC4yOTQuMTg3LS4wNTcuMzg1LS4wOTEuNTgtLjEwOC4zOTgtLjA2IDEuMS0uMTU1IDIuMDUyLS4xMTggMi4xNzguMDI3IDQuNjYgMS4xNjUgNi41NjMgMy4wNDYgMi4yNDYgMi4xMiA0LjAwNiA2LjM4IDIuNzU4IDExLjAxMS0xLjIxNyA0Ljk1Ny01Ljg5NSA5LjA1Ni0xMS4zNTMgOS45MTQtNS44NyAxLjA0Ny0xMi40ODItMS44NDctMTUuOTczLTcuMDk0LTMuMzY1LTQuNzI4LTQuMDA5LTExLjY0Ny0xLjU0OC0xNy4xMzYgMi43LTYuMzIgNy44NTktOS4xOCA5LjM3My05LjkwNyA3LjI3OS0zLjYzNSAxNS4wNTktMS4yMzUgMTUuMzg2LTEuMTM0IDIuMDkzLjQ5NCA3LjkxMyAyLjQ0MyAxMi40MzcgOC42MjUgMy40NzcgNC42NSA2LjI5NyAxMy4zMzcgMi43MTQgMjIuNDMtMy4yNzMgOC45NjItMTIuNTkgMTUuNzctMjIuNjM4IDE2LjQ5My0uNzg4LjA3NC0xLjU4NS4xMTUtMi4zOC4xMTUtOS43NSAwLTE5LjYyNy01LjYyLTI0LjU4LTE0LjI1IDAgMC0uODIxLTEuNDE4LTEuMTgzLTIuMTUzeiIgc3Ryb2tlPSIjMDAwIiBvcGFjaXR5PSIuMjUiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNi4xODIgNTEuMjAxYy01LjEzNC04LjQyLTUuNTUzLTE5LjY1Ny0xLjAzLTI4LjU2NUM5Ljg0IDEzLjA2MiAxNy43MyA4LjgxOCAyMC45IDcuNDM4YTMxLjQgMzEuNCAwIDAgMSA2LjA3Ni0yLjA4M2wyLjEyLS40NTdjMS41NTgtLjMyIDMuMTEuNjQ2IDMuNDQ3IDIuMTg3LjM0IDEuNTQ1LS42NTEgMy4wNjYtMi4yMTYgMy40MDJsLTIuMDYzLjQ0NWEyNS40IDI1LjQgMCAwIDAtNC45NiAxLjcxNmMtMi4wODMuOTAyLTguOTc0IDQuMzkyLTEyLjk0OSAxMi41MTUtMy4wMTcgNS45MzMtMy45MTcgMTUuMzY2LjgzMiAyMy4xNSA0LjMwMyA3LjQ5OCAxMy4xMTIgMTIuMDk1IDIxLjQ2OCAxMS4zMjEgNy45MDMtLjU2OCAxNS4xNTUtNS44MjggMTcuNjk4LTEyLjc5NSAyLjU4MS02LjU1Mi44NjYtMTMuMjY5LTEuOTY3LTE3LjA1Mi0zLjQxMy00LjY2LTcuNjYtNi4wOTQtOS4zMjgtNi40OTUtLjI0OS0uMDc0LTYuMDg1LTEuODI3LTExLjI0Ljc1NC0yLjIyIDEuMDY0LTUuMDkgMy40NTYtNi42MjggNy4wNS0xLjY2IDMuNzEyLTEuMjMgOC40ODQgMS4wMiAxMS42NTUgMi4yNjMgMy4zOTkgNi41MzIgNS4zMjMgMTAuMjAzIDQuNjcgMy42NTUtLjU3NSA2LjEyNy0zLjI5NCA2LjcxMy01LjY4My42ODktMi41NjgtLjM2OC00LjczOS0xLjE5My01LjUyLTEuNDAxLTEuMzgzLTIuNjExLTEuNDA2LTIuNjYzLTEuNDFhNSA1IDAgMCAwLTEuMDI2LjA1NGMtLjY1NC4yNC0xLjU4Mi43NjQtMS44NTQgMS4zMTYtLjA0OC4wOS0uMTcuMzQuMDY0Ljk2My41NjMgMS40NzctLjE5NyAzLjEzLTEuNjk0IDMuNjg1LTEuNDkuNTU4LTMuMTY3LS4xOTItMy43MzMtMS42NzMtLjkzMS0yLjQ1LS40MTYtNC4zNjguMTgtNS41NDYgMS41NTgtMy4wNjYgNS4xNzYtNC4xNzYgNS41ODUtNC4yOTQuMTg3LS4wNTcuMzg1LS4wOTEuNTgtLjEwOC4zOTgtLjA2IDEuMS0uMTU1IDIuMDUyLS4xMTggMi4xNzguMDI3IDQuNjYgMS4xNjUgNi41NjMgMy4wNDYgMi4yNDYgMi4xMiA0LjAwNiA2LjM4IDIuNzU4IDExLjAxMS0xLjIxNyA0Ljk1Ny01Ljg5NSA5LjA1Ni0xMS4zNTMgOS45MTQtNS44NyAxLjA0Ny0xMi40ODItMS44NDctMTUuOTczLTcuMDk0LTMuMzY1LTQuNzI4LTQuMDA5LTExLjY0Ny0xLjU0OC0xNy4xMzYgMi43LTYuMzIgNy44NTktOS4xOCA5LjM3My05LjkwNyA3LjI3OS0zLjYzNSAxNS4wNTktMS4yMzUgMTUuMzg2LTEuMTM0IDIuMDkzLjQ5NCA3LjkxMyAyLjQ0MyAxMi40MzcgOC42MjUgMy40NzcgNC42NSA2LjI5NyAxMy4zMzcgMi43MTQgMjIuNDMtMy4yNzMgOC45NjItMTIuNTkgMTUuNzctMjIuNjM4IDE2LjQ5My0uNzg4LjA3NC0xLjU4NS4xMTUtMi4zOC4xMTUtOS43NSAwLTE5LjYyNy01LjYyLTI0LjU4LTE0LjI1Ii8+PC9zdmc+", + }[name] + ); + }; + + const Cast = Scratch.Cast; + const vm = Scratch.vm; + const runtime = vm.runtime; + const render = vm.renderer; + const keyUtil = runtime.ioDevices.keyboard; + + const BUILT_IN_FONTS = [ + ...new Set(Array.from(document.fonts).map((f) => f.family)), + ].map((f) => ({ text: Scratch.translate(f), value: f })); + + const translatedInputs = { + popup: Scratch.translate("default-popup"), + label: Scratch.translate("new-label"), + input: Scratch.translate("new-input"), + btnRow: Scratch.translate("default-button-row"), + button: Scratch.translate("submit-button"), + }; + + const validInputs = [ + createMenuItem("text", "text"), + createMenuItem("multi-line text", "textarea"), + createMenuItem("password", "password"), + createMenuItem("number", "number"), + createMenuItem("color", "color"), + createMenuItem("date", "date"), + createMenuItem("dropdown", "select"), + createMenuItem("multi-select dropdown", "multiSelect"), + createMenuItem("horizontal slider", "rangeHorz"), + createMenuItem("vertical slider", "rangeVert"), + ]; + const validBorders = [ + createMenuItem("none", "none"), + createMenuItem("solid", "solid"), + createMenuItem("dotted", "dotted"), + createMenuItem("dashed", "dashed"), + createMenuItem("double", "double"), + createMenuItem("groove", "groove"), + createMenuItem("ridge", "ridge"), + createMenuItem("inset", "inset"), + createMenuItem("outset", "outset"), + ]; + const validEffects = [ + createMenuItem("blur", "blur"), + createMenuItem("brightness", "brightness"), + createMenuItem("opacity", "opacity"), + createMenuItem("saturation", "saturate"), + createMenuItem("hue", "hue-rotate"), + createMenuItem("invert", "invert"), + createMenuItem("sepia", "sepia"), + createMenuItem("contrast", "contrast"), + createMenuItem("direction", "rotate"), + createMenuItem("scale x", "scaleX"), + createMenuItem("scale y", "scaleY"), + createMenuItem("skew x", "skewX"), + createMenuItem("skew y", "skewY"), + ]; + + // internals + const styleStorage = { + popups: Object.create(null), + labels: Object.create(null), + inputs: Object.create(null), + buttons: Object.create(null), + }; + const elementStorage = { + popups: Object.create(null), + labels: Object.create(null), + inputs: Object.create(null), + buttonRows: Object.create(null), + buttons: Object.create(null), + }; + + const xmlEscape = function (unsafe) { + return Cast.toString(unsafe).replace(new RegExp(`[<>&'"]`, "g"), (c) => { + switch (c) { + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + case "'": + return "'"; + case '"': + return """; + } + }); + }; + + const createLabel = (id, text) => { + const element = document.createElement("div"); + element.style.width = "max-content"; + const label = document.createElement("pre"); + label.id = xmlEscape(id); + label.innerHTML = xmlEscape(text).replace(/\n/g, "
"); + label.setAttribute("style", compileStyles(id, "label")); + + element.appendChild(label); + return { + type: "label", + data: { id, text }, + DOMelement: element, + }; + }; + + const createInput = (id, type, options) => { + const inputHolder = document.createElement("div"); + inputHolder.id = xmlEscape(id); + inputHolder.style.width = "max-content"; + let element; + + const setElementValue = (value) => { + inputHolder.setAttribute("returnValue", JSON.stringify(value)); + }; + + if (type === "select") { + const selectors = options.selectors ?? ["no options created"]; + element = document.createElement("select"); + selectors.forEach((option) => { + let opt = document.createElement("option"); + opt.value = option; + opt.text = option; + element.appendChild(opt); + }); + + element.addEventListener("focus", () => { + options.menuOpened = true; + }); + element.addEventListener("blur", () => { + options.menuOpened = false; + }); + element.addEventListener("change", (e) => + setElementValue([e.target.value]) + ); + setElementValue([selectors[0]]); + } else if (type === "multiSelect") { + const createOption = (text, index) => { + const holder = document.createElement("div"); + holder.setAttribute( + "style", + "display: flex; padding: 5px; cursor: pointer" + ); + + const check = document.createElement("input"); + check.type = "checkbox"; + check.checked = index === 0 ? true : false; + const label = document.createElement("pre"); + label.innerHTML = xmlEscape(text).replace(/\n/g, "
"); + label.setAttribute("style", "margin:0 0 0 5px; font-family: inherit;"); + + holder.append(check, label); + return holder; + }; + + const selectors = options.selectors ?? ["no options created"]; + let response = [selectors[0]]; + options.menuOpened = true; + + element = document.createElement("div"); + const optionsDiv = document.createElement("div"); + optionsDiv.setAttribute( + "style", + "overflow-y: scroll; height: calc(100% - 1px);" + ); + element.appendChild(optionsDiv); + + setElementValue(response); + for (let i = 0; i < selectors.length; i++) { + const option = createOption(selectors[i], i); + optionsDiv.appendChild(option); + + option.addEventListener("click", (e) => { + if (e.target.tagName === "PRE") + e.target.parentNode.querySelector("input").click(); + else if (e.target.tagName === "INPUT") { + if (e.target.checked) response.push(selectors[i]); + else response.splice(response.indexOf(selectors[i]), 1); + setElementValue(response); + } + e.stopPropagation(); + }); + } + } else if (type === "textarea") { + element = document.createElement("textarea"); + element.value = options.defaultValue ?? ""; + } else if (type.startsWith("range")) { + element = document.createElement("div"); + + const counter = document.createElement("div"); + counter.setAttribute("style", "text-align: center"); + counter.innerText = options.defaultValue ?? 0; + + const range = document.createElement("input"); + range.setAttribute("style", "display: block; margin: auto;"); + range.type = "range"; + if (type.endsWith("Vert")) range.style.writingMode = "sideways-lr"; + + range.setAttribute("max", options.maxV ?? 100); + range.setAttribute("min", options.minV ?? 0); + range.value = options.defaultValue ?? 0; + range.addEventListener("input", (e) => { + const value = e.target.value; + setElementValue([value]); + counter.innerText = value; + }); + + element.append(range, counter); + setElementValue([counter.innerText]); + } else { + element = document.createElement("input"); + element.type = type; + element.value = options.defaultValue ?? ""; + + if (type === "password") { + element.setAttribute("autocomplete", "off"); + element.setAttribute("name", `SPpopupPheonixPass-${Math.random()}`); + } else if (type === "color") { + inputHolder.setAttribute( + "style", + "display: flex; justify-content: center; width: 100%;" + ); + const colorStyle = document.createElement("style"); + colorStyle.textContent = ` + input[type="color"] { + cursor: pointer; + } + input[type="color"]::-webkit-color-swatch { + border: black solid 1px; + margin: 1px; + border-radius: 5px; + } + input[type="color"]::-moz-color-swatch { + border: black solid 1px; + margin: 1px; + border-radius: 5px; + } + input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0px; + } + `; + element.append(colorStyle); + } + } + + element.setAttribute("style", compileStyles(id, "input")); + inputHolder.appendChild(element); + return { + type: "input", + data: { id, type, options }, + DOMelement: inputHolder, + }; + }; + + const createButtonRow = (id) => { + const buttonRow = document.createElement("div"); + buttonRow.id = xmlEscape(id); + buttonRow.style.width = "max-content"; + + return { + type: "buttonRow", + data: { id, buttons: [] }, + DOMelement: buttonRow, + }; + }; + + const createButton = (id, text) => { + const element = document.createElement("button"); + element.id = xmlEscape(id); + element.innerHTML = xmlEscape(text).replace(/\n/g, "
"); + element.setAttribute("returnValue", id); + element.setAttribute("style", compileStyles(id, "button")); + + return { + type: "button", + data: { id, text }, + DOMelement: element, + }; + }; + + const genDefaultStyles = (id, type) => { + if (type.endsWith("s")) type = type.substring(0, type.length - 1); + if (type === "popup") { + styleStorage.popups[id] = { + SPstorage: {}, + display: "flex", + "flex-direction": "column", + position: "absolute", + "pointer-events": "auto", + transform: "translate(-50%, -50%)", + filter: "", + left: "calc(50% + 0px)", + top: "calc(50% + 0px)", + width: "max-content", + height: "auto", + padding: "15px", + background: "#fff", + "box-shadow": "#000 0px 0px 5px", + border: "1px none #000", + "border-radius": "5px", + }; + } else if (type === "label") { + styleStorage.labels[id] = { + SPstorage: {}, + "font-size": "14px", + "font-family": "Sans Serif", + margin: "5px", + color: "#000", + "text-shadow": "none", + "-webkit-text-stroke-width": "0px", + transform: "", + filter: "", + }; + } else if (type === "input") { + styleStorage.inputs[id] = { + SPstorage: {}, + width: "max-content", + height: "auto", + margin: "5px", + padding: "5px", + border: "1px solid #000", + "border-radius": "4px", + "text-align": "left", + "font-size": "14px", + "font-family": "Sans Serif", + background: "#a5aec3", + "accent-color": "#9f50d9", + color: "#000", + "text-shadow": "none", + "-webkit-text-stroke-width": "0px", + transform: "", + filter: "", + }; + } else if (type === "button") { + styleStorage.buttons[id] = { + SPstorage: {}, + width: "auto", + height: "auto", + cursor: "pointer", + margin: "5px", + padding: "5px 10px", + "border-radius": "5px", + border: "1px none #fff", + background: "#0074d9", + "font-size": "14px", + "font-family": "Sans Serif", + color: "#fff", + "text-shadow": "none", + "-webkit-text-stroke-width": "0px", + transform: "", + filter: "", + }; + } + return styleStorage[type + "s"][id]; + }; + + const genDefaultBox = (id, optEmptyElements) => { + genDefaultStyles(id, "popup"); + const defaultElements = []; + let submitBtn, cancelBtn; + if (!optEmptyElements) { + defaultElements.push( + createLabel("default-label", Scratch.translate("Whats Your Name?")) + ); + defaultElements.push( + createInput("default-input", "text", { + defaultValue: Scratch.translate("...type your name"), + }) + ); + + const buttonRow = createButtonRow("default-button-row"); + submitBtn = createButton("submit-button", Scratch.translate("Submit")); + cancelBtn = createButton("cancel-button", Scratch.translate("Cancel")); + buttonRow.data.buttons.push(submitBtn.data, cancelBtn.data); + buttonRow.DOMelement.append(submitBtn.DOMelement, cancelBtn.DOMelement); + defaultElements.push(buttonRow); + } + + const defaultBox = { + waiting: false, + isWindowed: false, + focused: false, + autoControl: true, + forceKey: undefined, + lastBtnPressed: undefined, + value: undefined, + DOMelement: undefined, + elements: defaultElements, + }; + if (!optEmptyElements) { + elementStorage.labels["default-label"] = defaultElements[0]; + elementStorage.inputs["default-input"] = defaultElements[1]; + elementStorage.buttons["submit-button"] = submitBtn; + elementStorage.buttons["cancel-button"] = cancelBtn; + elementStorage.buttonRows["default-button-row"] = defaultElements[2]; + } + elementStorage.popups[id] = defaultBox; + return defaultBox; + }; + + // popup box GUI + const compileStyles = (id, type) => { + const boxInfo = elementStorage[type + "s"][id]; + const styleObj = styleStorage[type + "s"][id] ?? genDefaultStyles(id, type); + if (type === "input" && boxInfo?.data.type === "color") { + // color inputs with these options look weird + if (styleObj["width"] === "max-content") styleObj["width"] = "80%"; + if (styleObj["height"] === "auto") styleObj["height"] = "30px"; + } + + let styleString = ""; + Object.entries(styleObj).forEach((style) => { + styleString += `${style[0]}: ${style[1]};`; + }); + return styleString; + }; + + const compileBox = (id) => { + const boxInfo = elementStorage.popups[id]; + boxInfo.waiting = true; + let boxInputs = []; + + const popupContainer = document.createElement("div"); + popupContainer.setAttribute( + "style", + "position: fixed; width: 100%; height: 100%;" + ); + popupContainer.style.pointerEvents = boxInfo.focused ? "auto" : "none"; + if (boxInfo.focused) + popupContainer.style.backgroundColor = "rgba(0,0,0,0.5)"; + if (boxInfo.isWindowed) popupContainer.style.zIndex = "9999"; + else { + popupContainer.style.left = "-50%"; + popupContainer.style.top = "-50%"; + } + + const popupPanel = document.createElement("div"); + popupPanel.classList.add("SP-Popup-Box"); + popupPanel.id = xmlEscape(id); + popupPanel.setAttribute("style", compileStyles(id, "popup")); + + // various element utils + const handleClose = (event) => { + // if theres no input, set the value to the button name + if (event) + boxInfo.lastBtnPressed = event.target.getAttribute("returnValue"); + if (boxInputs.length === 0) boxInfo.value = boxInfo.lastBtnPressed ?? ""; + else compileInputValues(); + runtime.off("KEY_PRESSED", forceKeyHandler); + closeBox(id); + }; + const forceKeyHandler = (key) => { + if (boxInfo.forceKey === "shiftEnter") { + if (key === "enter" && runtime.ioDevices.keyboard.getKeyIsDown("shift")) + handleClose(); + return; + } + if (key.toLowerCase() == boxInfo.forceKey.toLowerCase()) handleClose(); + }; + + const compileInputValues = () => { + if (boxInputs.length === 0) boxInfo.value = ""; + else if (boxInputs.length === 1) { + const input = boxInputs[0]; + const element = input.DOMelement; + + const returnValue = element.getAttribute("returnValue"); + boxInfo.value = returnValue + ? JSON.parse(returnValue) + : element.firstChild.value; + } else { + boxInfo.value = Object.create(null); + for (let i = 0; i < boxInputs.length; i++) { + const input = boxInputs[i]; + const element = input.DOMelement; + + const returnValue = element.getAttribute("returnValue"); + boxInfo.value[input.data.id] = returnValue + ? JSON.parse(returnValue) + : element.firstChild.value; + } + } + }; + + // append elements to panel + for (let i = 0; i < boxInfo.elements.length; i++) { + const item = boxInfo.elements[i]; + let element = item.DOMelement; + if (item.type === "label") { + const refreshedLabel = elementStorage.labels[item.data.id]; + element = refreshedLabel.DOMelement; + } else if (item.type === "input") { + boxInputs.push(item); + + const refreshedInput = elementStorage.inputs[item.data.id]; + if (!refreshedInput) continue; + const recentData = refreshedInput.data; + element = createInput( + recentData.id, + recentData.type, + recentData.options + ).DOMelement; + item.DOMelement = element; + } + popupPanel.appendChild(element); + + // add event listeners + if (item.type === "input") { + element.addEventListener("change", compileInputValues); + if (boxInfo.forceKey !== undefined) + element.addEventListener("keydown", (e) => { + if (boxInfo.forceKey === keyUtil._keyStringToScratchKey(e.key)) { + handleClose(); + e.preventDefault(); + } + }); + } else if (item.type === "buttonRow") { + element.addEventListener("click", (e) => { + if (e.target.tagName === "BUTTON") handleClose(e); + e.stopPropagation(); + }); + } + } + + popupContainer.appendChild(popupPanel); + if (boxInfo.isWindowed) document.body.appendChild(popupContainer); + else render.addOverlay(popupContainer, "scale-centered"); + compileInputValues(); + boxInfo.DOMelement = popupContainer; + + // extra event listeners + if (boxInfo.forceKey !== undefined) + runtime.on("KEY_PRESSED", forceKeyHandler); + }; + + const closeBox = (id, optForce) => { + const boxInfo = elementStorage.popups[id]; + boxInfo.waiting = false; + + if (optForce || boxInfo.autoControl) { + if (boxInfo.isWindowed) boxInfo.DOMelement?.remove(); + else render.removeOverlay(boxInfo.DOMelement); + boxInfo.DOMelement = undefined; + } + }; + + const updateBoxVisuals = (id, type) => { + const boxInfo = elementStorage[type][id]; + if (!boxInfo || !boxInfo.DOMelement) return; + const element = + type === "buttons" ? boxInfo.DOMelement : boxInfo.DOMelement.firstChild; + element.setAttribute( + "style", + compileStyles(id, type.slice(0, type.length - 1)) + ); + }; + + // extension utilities + function createBlockLabel(text) { + return { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate(text), + }; + } + + function createMenuItem(text, value) { + return { + text: Scratch.translate(text), + value, + }; + } + + genDefaultBox("default-popup", false); + + class SPpopups { + constructor() { + runtime.on("PROJECT_START", () => this.removeAllPopups()); + runtime.on("PROJECT_STOP_ALL", () => this.removeAllPopups()); + } + getInfo() { + return { + id: "SPpopups", + name: Scratch.translate("Popup Phoenix"), + color1: "#a812ff", + color2: "#941cd9", + color3: "#8219bf", + menuIconURI, + blockIconURI: getBlockIcon("main"), + blocks: [ + { + opcode: "showPopup", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("show popup with ID [ID] and [TYPE]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "AWAITS" }, + }, + }, + { + opcode: "showPopupReport", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("show popup with ID [ID] and wait"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + { + opcode: "userInput", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("response from [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + "---", + { + opcode: "removePopup", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("remove popup with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + { + opcode: "removeAllPopups", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("remove all popups"), + }, + createBlockLabel("Popups"), + { + opcode: "createPopup", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("create new popup with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("new-popup"), + }, + }, + }, + { + opcode: "deletePopup", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("delete popup with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + "---", + { + opcode: "append2Popup", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "append [TYPE] with ID [ELEMENT_ID] to popup [POP_ID]" + ), + arguments: { + TYPE: { type: Scratch.ArgumentType.STRING, menu: "UI_ELEMENTS" }, + ELEMENT_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.label, + }, + POP_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + { + opcode: "removeFromPopup", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "remove [TYPE] with ID [ELEMENT_ID] from popup [POP_ID]" + ), + arguments: { + TYPE: { type: Scratch.ArgumentType.STRING, menu: "UI_ELEMENTS" }, + ELEMENT_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.label, + }, + POP_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + { + opcode: "removePopupContent", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("remove all elements from popup [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + "---", + { + opcode: "setPopupPos", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set position of [ID] to x: [X] y: [Y]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + X: { type: Scratch.ArgumentType.NUMBER }, + Y: { type: Scratch.ArgumentType.NUMBER }, + }, + }, + { + opcode: "getPos", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("[TYPE] position of [ID]"), + arguments: { + TYPE: { type: Scratch.ArgumentType.STRING, menu: "POSITION" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + createBlockLabel("Labels"), + { + opcode: "createPopLabel", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "create new label with ID [ID] and text [TEXT]" + ), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.label, + }, + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("whats your name?"), + }, + }, + }, + { + opcode: "deletePopLabel", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("delete label with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.label, + }, + }, + }, + createBlockLabel("Inputs"), + { + opcode: "createPopInput", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("create new [TYPE] input with ID [ID]"), + arguments: { + TYPE: { type: Scratch.ArgumentType.STRING, menu: "INPUTS" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + }, + }, + { + opcode: "deletePopInput", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("delete input with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + }, + }, + "---", + { + opcode: "setDefaultValue", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "set default value of input [ID] to [VALUE]" + ), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate(`...type here`), + }, + }, + }, + "---", + { + opcode: "setSliderParams", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set slider [ID] min [MIN] max [MAX]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + MIN: { type: Scratch.ArgumentType.NUMBER, defaultValue: -100 }, + MAX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, + }, + }, + "---", + { + opcode: "setDropOptions", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set options of dropdown [ID] to [ARRAY]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + ARRAY: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate( + `["Option 1", "Option 2", "Option 3"]` + ), + }, + }, + }, + { + opcode: "isDropOpen", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate( + "is dropdown [DROP_ID] in popup [POP_ID] open?" + ), + arguments: { + DROP_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + POP_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + createBlockLabel("Buttons"), + { + opcode: "createBtnRow", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("create new button row with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.btnRow, + }, + }, + }, + { + opcode: "createPopBtn", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "create new button with ID [ID] and text [TEXT]" + ), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.button, + }, + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("Submit"), + }, + }, + }, + { + opcode: "deleteBtnRow", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("delete button row with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.btnRow, + }, + }, + }, + { + opcode: "deletePopBtn", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("delete button with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.button, + }, + }, + }, + "---", + { + opcode: "append2Row", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("append button [BTN_ID] to row [ROW_ID]"), + arguments: { + BTN_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.button, + }, + ROW_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.btnRow, + }, + }, + }, + { + opcode: "removeFromRow", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("remove button [BTN_ID] from row [ROW_ID]"), + arguments: { + BTN_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.button, + }, + ROW_ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.btnRow, + }, + }, + }, + { + opcode: "removeRowContent", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("remove all buttons from row [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.btnRow, + }, + }, + }, + createBlockLabel("Operators"), + { + opcode: "allElements", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("all [TYPE] IDs"), + arguments: { + TYPE: { type: Scratch.ArgumentType.STRING, menu: "UI_SELECT" }, + }, + }, + { + opcode: "popupWaiting", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("is popup [ID] waiting?"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + { + opcode: "lastButtonPress", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("last button pressed in [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + { + opcode: "focusInput", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("focus on input with ID [ID]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + }, + }, + "---", + { + opcode: "setPopupTarget", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "show next popup with ID [ID] on the [TYPE]" + ), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "POPUP_LOCATION", + }, + }, + }, + { + opcode: "toggleOption", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("toggle [OPTION] in popup [ID] [TYPE]"), + arguments: { + OPTION: { type: Scratch.ArgumentType.STRING, menu: "EXTRA_OPTS" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "TOGGLER" }, + }, + }, + { + opcode: "setForceClose", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "set auto-close action in popup [ID] to [KEY] key" + ), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + KEY: { type: Scratch.ArgumentType.STRING, menu: "FORCE_KEY" }, + }, + }, + createBlockLabel("Customization"), + { + opcode: "setPopupAlign", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate("set alignment of popup [ID] to [TYPE]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "ALIGNMENT" }, + }, + }, + { + opcode: "setInputAlign", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate("set alignment of input [ID] to [TYPE]"), + arguments: { + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.input, + }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "ALIGNMENT" }, + }, + }, + "---", + { + opcode: "setInputFont", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate("set font of [ELEMENT] [ID] to [FONT]"), + arguments: { + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN2" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.label, + }, + FONT: { type: Scratch.ArgumentType.STRING, menu: "FONTS" }, + }, + }, + { + opcode: "setInputFontSZ", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate( + "set font size of [ELEMENT] [ID] to [SIZE]" + ), + arguments: { + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN2" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.label, + }, + SIZE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 15 }, + }, + }, + { + opcode: "setElementOutline", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate( + "set text outline of [ELEMENT] [ID] to color [COLOR] weight [WEIGHT]" + ), + arguments: { + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN2" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.label, + }, + COLOR: { type: Scratch.ArgumentType.COLOR }, + WEIGHT: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, + }, + }, + "---", + { + opcode: "setElementAtt", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate("set [PARAM] of [ELEMENT] [ID] to [VALUE]"), + arguments: { + PARAM: { type: Scratch.ArgumentType.STRING, menu: "POP_STYLES" }, + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "#ff0000", + }, + }, + }, + { + opcode: "setElementBorder", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate( + "set border of [ELEMENT] [ID] to [TYPE] [COLOR] weight [WEIGHT]" + ), + arguments: { + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "BORDERS" }, + COLOR: { type: Scratch.ArgumentType.COLOR }, + WEIGHT: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, + }, + }, + { + opcode: "setElementShadow", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("color"), + text: Scratch.translate( + "set shadow of [ELEMENT] [ID] to x: [X] y: [Y] color [COLOR] blur [AMT] strength [BLUR]" + ), + arguments: { + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + X: { type: Scratch.ArgumentType.NUMBER }, + Y: { type: Scratch.ArgumentType.NUMBER }, + COLOR: { type: Scratch.ArgumentType.COLOR }, + AMT: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, + BLUR: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, + }, + }, + "---", + { + opcode: "resetElementEffect", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("effect"), + text: Scratch.translate("reset effects of [ELEMENT] [ID]"), + arguments: { + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + { + opcode: "setElementEffect", + blockType: Scratch.BlockType.COMMAND, + blockIconURI: getBlockIcon("effect"), + text: Scratch.translate( + "set [EFFECT] of [ELEMENT] [ID] to [VALUE]" + ), + arguments: { + EFFECT: { type: Scratch.ArgumentType.STRING, menu: "EFFECTS" }, + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + VALUE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, + }, + }, + { + opcode: "getElementEffect", + blockType: Scratch.BlockType.REPORTER, + blockIconURI: getBlockIcon("effect"), + text: Scratch.translate("[EFFECT] of [ELEMENT] [ID]"), + arguments: { + EFFECT: { type: Scratch.ArgumentType.STRING, menu: "EFFECTS" }, + ELEMENT: { type: Scratch.ArgumentType.STRING, menu: "UI_MAIN" }, + ID: { + type: Scratch.ArgumentType.STRING, + defaultValue: translatedInputs.popup, + }, + }, + }, + ], + menus: { + FONTS: { acceptReporters: true, items: "getFonts" }, + POP_STYLES: { acceptReporters: true, items: this.genStyleList() }, + INPUTS: { acceptReporters: true, items: validInputs }, + BORDERS: { acceptReporters: true, items: validBorders }, + EFFECTS: { acceptReporters: true, items: validEffects }, + UI_ELEMENTS: { + acceptReporters: true, + items: this.genElementList(false, true, false), + }, + UI_MAIN: { + acceptReporters: true, + items: this.genElementList(true, false, true), + }, + UI_MAIN2: { + acceptReporters: true, + items: this.genElementList(false, false, true), + }, + UI_SELECT: { + acceptReporters: true, + items: this.genElementList(true, true, true), + }, + AWAITS: { + acceptReporters: false, + items: [ + createMenuItem("wait", "wait"), + createMenuItem("continue", "continue"), + ], + }, + TOGGLER: { + acceptReporters: false, + items: [createMenuItem("on", "on"), createMenuItem("off", "off")], + }, + POSITION: { + acceptReporters: false, + items: [createMenuItem("x", "x"), createMenuItem("y", "y")], + }, + POPUP_LOCATION: { + acceptReporters: false, + items: [ + createMenuItem("window", "window"), + createMenuItem("canvas", "canvas"), + ], + }, + ALIGNMENT: { + acceptReporters: false, + items: [ + createMenuItem("left", "left"), + createMenuItem("right", "flex-end"), + createMenuItem("center", "center"), + ], + }, + FORCE_KEY: { + acceptReporters: true, + items: [ + createMenuItem("none", "none"), + createMenuItem("enter", "enter"), + createMenuItem("shift & enter", "shiftEnter"), + ], + }, + EXTRA_OPTS: { + acceptReporters: true, + items: [ + createMenuItem("focus mode", "focus"), + createMenuItem("auto close", "autoClose"), + ], + }, + }, + }; + } + + // Helper Funcs + getFonts() { + const custFonts = runtime.fontManager + ? runtime.fontManager + .getFonts() + .map((i) => ({ text: i.name, value: i.family })) + : []; + return [...BUILT_IN_FONTS, ...custFonts]; + } + + genElementList(addPopup, addRow, addButton) { + const list = []; + if (addPopup) list.push(createMenuItem("popup", "popup")); + list.push( + createMenuItem("label", "label"), + createMenuItem("input", "input") + ); + if (addRow) list.push(createMenuItem("button row", "buttonRow")); + if (addButton) list.push(createMenuItem("button", "button")); + return list; + } + + genStyleList() { + return [ + createMenuItem("background color", "background"), + createMenuItem("text color", "color"), + createMenuItem("accent color", "accent-color"), + createMenuItem("margins", "margin"), + createMenuItem("padding", "padding"), + createMenuItem("width", "width"), + createMenuItem("height", "height"), + createMenuItem("border radius", "border-radius"), + ]; + } + + getElementPath(path, useBtn, usePopup) { + const validPaths = ["label", "input", useBtn ? "button" : "buttonRow"]; + if (usePopup) validPaths.push("popup"); + return validPaths.includes(path) ? path : "label"; + } + + openPopupHandler(args, util, reportResponse) { + const id = Cast.toString(args.ID); + let popup = elementStorage.popups[id]; + if (!util.stackFrame.awaitingInput) { + if (popup === undefined) popup = genDefaultBox(id, false); + if (popup.DOMelement) closeBox(id, true); + compileBox(id); + util.stackFrame.awaitingInput = true; + } + + if ((reportResponse ? true : args.TYPE === "wait") && popup?.waiting) + util.yield(); + if (reportResponse) + return typeof popup.value === "object" + ? JSON.stringify(popup.value) + : popup.value || ""; + } + + // Block Funcs + /* main */ + showPopup(args, util) { + this.openPopupHandler(args, util, false); + } + + showPopupReport(args, util) { + return this.openPopupHandler(args, util, true); + } + + userInput(args) { + const id = Cast.toString(args.ID); + const popup = elementStorage.popups[id]; + if (popup === undefined) return ""; + return typeof popup.value === "object" + ? JSON.stringify(popup.value) + : popup.value || ""; + } + + removePopup(args) { + const id = Cast.toString(args.ID); + const popup = elementStorage.popups[id]; + if (popup !== undefined && popup.DOMelement) closeBox(id, true); + } + + removeAllPopups() { + const allPopups = Object.entries(elementStorage.popups); + for (let i = 0; i < allPopups.length; i++) { + const entry = allPopups[i]; + if (entry[1].DOMelement) closeBox(entry[0], true); + } + } + + /* popups */ + createPopup(args) { + this.removePopup(args); + const id = Cast.toString(args.ID); + genDefaultBox(id, true); + } + + deletePopup(args) { + this.removePopup(args); + const id = Cast.toString(args.ID); + delete styleStorage.popups[id]; + delete elementStorage.popups[id]; + } + + append2Popup(args) { + const popup = elementStorage.popups[Cast.toString(args.POP_ID)]; + const path = this.getElementPath(Cast.toString(args.TYPE)); + const elementID = Cast.toString(args.ELEMENT_ID); + const element = elementStorage[path + "s"][elementID]; + if (popup === undefined || element === undefined) return; + + const elementIndex = popup.elements.findIndex((i) => { + return i.type === path && i.data.id === elementID; + }); + if (elementIndex === -1) popup.elements.push(element); + } + + removeFromPopup(args) { + const popup = elementStorage.popups[Cast.toString(args.POP_ID)]; + const path = this.getElementPath(Cast.toString(args.TYPE)); + const elementID = Cast.toString(args.ELEMENT_ID); + if (popup === undefined) return; + + const elementIndex = popup.elements.findIndex((i) => { + return i.type === path && i.data.id === elementID; + }); + if (elementIndex > -1) popup.elements.splice(elementIndex, 1); + } + + removePopupContent(args) { + const id = Cast.toString(args.ID); + const popup = elementStorage.popups[id]; + if (popup !== undefined) popup.elements = []; + } + + setPopupPos(args) { + const id = Cast.toString(args.ID); + const styles = styleStorage.popups[id] ?? genDefaultStyles(id, "popup"); + styles.left = `calc(50% + ${Cast.toNumber(args.X)}px)`; + styles.top = `calc(50% + ${Cast.toNumber(args.Y) * -1}px)`; + updateBoxVisuals(id, "popups"); + } + + getPos(args) { + const id = Cast.toString(args.ID); + const styles = styleStorage.popups[id] ?? genDefaultStyles(id, "popup"); + if (args.TYPE === "x") + return parseFloat(styles.left.replace("calc(50% + ", "")); + else return parseFloat(styles.top.replace("calc(50% + ", "")) * -1; + } + + /* labels */ + createPopLabel(args) { + const id = Cast.toString(args.ID); + const text = Cast.toString(args.TEXT); + elementStorage.labels[id] = createLabel(id, text); + } + + deletePopLabel(args) { + const id = Cast.toString(args.ID); + delete styleStorage.labels[id]; + delete elementStorage.labels[id]; + } + + /* inputs */ + createPopInput(args) { + const id = Cast.toString(args.ID); + let type = Cast.toString(args.TYPE); + + const isValidInput = validInputs.findIndex((i) => i.value === type); + if (isValidInput === -1) type = "text"; + elementStorage.inputs[id] = createInput(id, type, {}); + } + + deletePopInput(args) { + const id = Cast.toString(args.ID); + delete styleStorage.inputs[id]; + delete elementStorage.inputs[id]; + } + + setDefaultValue(args) { + const input = elementStorage.inputs[Cast.toString(args.ID)]; + if (input) input.data.options.defaultValue = Cast.toString(args.VALUE); + } + + setSliderParams(args) { + const input = elementStorage.inputs[Cast.toString(args.ID)]; + if (input) { + input.data.options.minV = Cast.toNumber(args.MIN); + input.data.options.maxV = Cast.toNumber(args.MAX); + } + } + + setDropOptions(args) { + try { + const array = JSON.parse(args.ARRAY); + if (array.length === 0) array.push("no options created"); + const id = Cast.toString(args.ID); + const input = elementStorage.inputs[id]; + if (input) input.data.options.selectors = array; + } catch { + console.warn("Failed to set Dropdown Options, invalid Array!"); + } + } + + isDropOpen(args) { + const popup = elementStorage.popups[Cast.toString(args.POP_ID)]; + const dropID = Cast.toString(args.DROP_ID); + if (popup === undefined || !popup.waiting) return false; + const dropdown = popup.elements.find((e) => { + return e.type === "input" && e.data.id === dropID; + }); + if (dropdown) return dropdown.data.options.menuOpened ?? false; + return false; + } + + /* buttons */ + createBtnRow(args) { + const id = Cast.toString(args.ID); + elementStorage.buttonRows[id] = createButtonRow(id); + } + + createPopBtn(args) { + const id = Cast.toString(args.ID); + const text = Cast.toString(args.TEXT); + elementStorage.buttons[id] = createButton(id, text); + } + + deleteBtnRow(args) { + const id = Cast.toString(args.ID); + delete elementStorage.buttonRows[id]; + } + + deletePopBtn(args) { + const id = Cast.toString(args.ID); + delete styleStorage.buttons[id]; + delete elementStorage.buttons[id]; + } + + append2Row(args) { + const btn = elementStorage.buttons[Cast.toString(args.BTN_ID)]; + const row = elementStorage.buttonRows[Cast.toString(args.ROW_ID)]; + if (row === undefined || btn === undefined) return; + + const btnList = row.data.buttons; + const btnIndex = btnList.findIndex((i) => i.id === btn.data.id); + if (btnIndex > -1) return; + btnList.push(btn.data); + row.DOMelement.appendChild(btn.DOMelement); + } + + removeFromRow(args) { + const btn = elementStorage.buttons[Cast.toString(args.BTN_ID)]; + const row = elementStorage.buttonRows[Cast.toString(args.ROW_ID)]; + if (row === undefined || btn === undefined) return; + + const btnList = row.data.buttons; + const btnIndex = btnList.findIndex((i) => i.id === btn.data.id); + if (btnIndex === -1) return; + btnList.splice(btnIndex, 1); + row.DOMelement.children[btnIndex].remove(); + } + + removeRowContent(args) { + const row = elementStorage.buttonRows[Cast.toString(args.ID)]; + if (row === undefined) return; + + Array.from(row.DOMelement.children).forEach((button) => button.remove()); + row.data.buttons = []; + } + + /* operations */ + allElements(args) { + const namespace = Cast.toString(args.TYPE) + "s"; + const items = elementStorage[namespace]; + if (!items) return "[]"; + else return JSON.stringify(Object.keys(items)); + } + + popupWaiting(args) { + const id = Cast.toString(args.ID); + let popup = elementStorage.popups[id]; + if (popup === undefined) return false; + return popup.waiting; + } + + lastButtonPress(args) { + const id = Cast.toString(args.ID); + let popup = elementStorage.popups[id]; + if (popup === undefined) return ""; + return popup.lastBtnPressed ?? ""; + } + + focusInput(args) { + const input = elementStorage.inputs[Cast.toString(args.ID)]; + if (input) { + input.DOMelement.firstChild.focus(); + } + } + + setPopupTarget(args) { + const id = Cast.toString(args.ID); + let popup = elementStorage.popups[id]; + if (popup === undefined) return; + popup.isWindowed = args.TYPE === "window"; + } + + toggleOption(args) { + const id = Cast.toString(args.ID); + let popup = elementStorage.popups[id]; + if (popup === undefined) return; + + const option = Cast.toString(args.OPTION); + popup[option === "focus" ? "focused" : "autoControl"] = + args.TYPE === "on"; + } + + setForceClose(args) { + const id = Cast.toString(args.ID); + let popup = elementStorage.popups[id]; + if (popup === undefined) return; + + const key = Cast.toString(args.KEY); + popup.forceKey = key === "none" ? undefined : key; + } + + /* customization */ + setPopupAlign(args) { + const id = Cast.toString(args.ID); + const styles = styleStorage.popups[id] ?? genDefaultStyles(id, "popup"); + styles["align-items"] = args.TYPE; + styles["justify-content"] = args.TYPE; + updateBoxVisuals(id, "popups"); + } + + setInputAlign(args) { + const id = Cast.toString(args.ID); + let isLabel = false; + let styles = styleStorage.inputs[id]; + if (!styles) { + styles = styleStorage.labels[id]; + if (styles) isLabel = true; + else styles = genDefaultStyles(id, "input"); + } + + styles["text-align"] = args.TYPE === "flex-end" ? "right" : args.TYPE; + updateBoxVisuals(id, isLabel ? "labels" : "inputs"); + } + + setInputFont(args) { + const id = Cast.toString(args.ID); + const path = this.getElementPath(Cast.toString(args.ELEMENT), true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + styles["font-family"] = Cast.toString(args.FONT); + updateBoxVisuals(id, path); + } + + setInputFontSZ(args) { + const id = Cast.toString(args.ID); + const path = this.getElementPath(Cast.toString(args.ELEMENT), true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + styles["font-size"] = `${Cast.toNumber(args.SIZE)}px`; + updateBoxVisuals(id, path); + } + + setElementOutline(args) { + const id = Cast.toString(args.ID); + const path = this.getElementPath(Cast.toString(args.ELEMENT), true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + styles["-webkit-text-stroke-width"] = `${Cast.toNumber(args.WEIGHT)}px`; + styles["-webkit-text-stroke-color"] = args.COLOR; + updateBoxVisuals(id, path); + } + + setElementAtt(args) { + const id = Cast.toString(args.ID); + const param = Cast.toString(args.PARAM); + let value = Cast.toString(args.VALUE); + const path = + this.getElementPath(Cast.toString(args.ELEMENT), true, true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + const isValidStyle = this.genStyleList().findIndex( + (i) => i.value === param + ); + + if (!param.includes("color") && param !== "background") { + if (param === "width" || param === "height") { + // allow users to input things like "auto" while still adding "px" to numbers + value = /^\d+$/.test(value) ? `${value}px` : value; + } else { + // handle cases where users to input specific attributes like "margin-left" + if (value.includes(" ")) + value = /^\d+$/.test(value.replaceAll(" ", "")) + ? value.replaceAll(" ", "px ") + : value; + else value = `${value}px`; + } + } + + if (isValidStyle > -1) styles[param] = value; + updateBoxVisuals(id, path); + } + + setElementBorder(args) { + const id = Cast.toString(args.ID); + const weight = Cast.toNumber(args.WEIGHT); + const isBorderValid = validBorders.findIndex( + (i) => i.value === args.TYPE + ); + const path = + this.getElementPath(Cast.toString(args.ELEMENT), true, true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + styles[path === "labels" ? "border-bottom" : "border"] = + `${isBorderValid > -1 ? args.TYPE : "none"} ${args.COLOR} ${weight}px`; + updateBoxVisuals(id, path); + } + + setElementShadow(args) { + const id = Cast.toString(args.ID); + const pos = [Cast.toNumber(args.X), Cast.toNumber(args.Y)]; + const weight = [Cast.toNumber(args.AMT), Cast.toNumber(args.BLUR)]; + const path = + this.getElementPath(Cast.toString(args.ELEMENT), true, true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + if (path === "labels") + styles["text-shadow"] = + `${args.COLOR} ${pos[0]}px ${pos[1]}px ${weight[0]}px`; + else + styles["box-shadow"] = + `${args.COLOR} ${pos[0]}px ${pos[1]}px ${weight[0]}px ${weight[1]}px`; + updateBoxVisuals(id, path); + } + + resetElementEffect(args) { + const id = Cast.toString(args.ID); + const path = + this.getElementPath(Cast.toString(args.ELEMENT), true, true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + if (path === "popups") styles["transform"] = "translate(-50%, -50%)"; + styles["filter"] = ""; + updateBoxVisuals(id, path); + } + + setElementEffect(args) { + const effect = Cast.toString(args.EFFECT); + const effectIndex = validEffects.findIndex((i) => i.value === effect); + if (effectIndex === -1) return; + const id = Cast.toString(args.ID); + const path = + this.getElementPath(Cast.toString(args.ELEMENT), true, true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + + let value = Cast.toNumber(args.VALUE); + const cssTarget = effectIndex > 7 ? "transform" : "filter"; + const regex = new RegExp(`${effect}\\(([^)]*)\\)`); + styles["SPstorage"][effect] = value; + + switch (effect) { + case "blur": + value += "px"; + break; + case "brightness": + value = `${100 - value * -1}%`; + break; + case "opacity": + value = 1 - value / 100; + break; + case "saturate": + case "sepia": + case "invert": + value += "%"; + break; + case "hue-rotate": + value += "deg"; + break; + case "contrast": + value = `${100 + value}%`; + break; + case "rotate": + value = `${value - 90}deg`; + break; + case "scaleX": + case "scaleY": + value /= 100; + break; + case "skewX": + case "skewY": + value += "deg"; + break; + } + + const newCSS = styles[cssTarget].replace(regex, ""); + styles[cssTarget] = newCSS + `${effect}(${value})`; + updateBoxVisuals(id, path); + } + + getElementEffect(args) { + const effect = Cast.toString(args.EFFECT); + const isValidEffect = validEffects.findIndex((i) => i.value === effect); + if (isValidEffect === -1) return ""; + + const id = Cast.toString(args.ID); + const path = + this.getElementPath(Cast.toString(args.ELEMENT), true, true) + "s"; + const styles = styleStorage[path][id] ?? genDefaultStyles(id, path); + return styles["SPstorage"][effect] ?? ""; + } + } + + Scratch.extensions.register(new SPpopups()); +})(Scratch); diff --git a/images/SharkPool/Popup-Phoenix.svg b/images/SharkPool/Popup-Phoenix.svg new file mode 100644 index 0000000000..80dbf09dc3 --- /dev/null +++ b/images/SharkPool/Popup-Phoenix.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +