8789 lines
306 KiB
JavaScript
8789 lines
306 KiB
JavaScript
(window["webpackJsonpGUI"] = window["webpackJsonpGUI"] || []).push([["addon-default-entry"],{
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/color-picker/style.css":
|
|
/*!****************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/color-picker/style.css ***!
|
|
\****************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
|
|
|
|
// module
|
|
exports.push([module.i, ".sa-color-picker {\n display: flex;\n}\n\n.sa-color-picker-code {\n margin: 8px 0;\n}\n\n.sa-color-picker-paint {\n margin-top: 16px;\n margin-bottom: 4px;\n}\n\n.sa-color-picker > .sa-color-picker-color {\n border: none;\n border-top-left-radius: 1rem;\n border-bottom-left-radius: 1rem;\n padding: 0;\n padding-left: 0.6rem;\n padding-right: 0.4rem;\n margin-left: 0.5rem;\n outline: none;\n box-sizing: border-box;\n width: 3rem;\n height: 2rem;\n}\n[theme=\"dark\"] .sa-color-picker > .sa-color-picker-color {\n background: var(--ui-secondary);\n}\n\n.sa-color-picker > .sa-color-picker-text {\n box-sizing: border-box;\n width: calc(150px - 3rem);\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n[dir=\"rtl\"] .sa-color-picker > .sa-color-picker-color {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n border-top-right-radius: 1rem;\n border-bottom-right-radius: 1rem;\n margin-left: 0;\n margin-right: 0.5rem;\n}\n\n[dir=\"rtl\"] .sa-color-picker > .sa-color-picker-text {\n border-top-left-radius: 1rem;\n border-bottom-left-radius: 1rem;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\nbody.sa-hide-eye-dropper-background div[class*=\"stage_color-picker-background\"] {\n /* Do not show eye dropper background if the color picker is \"fake\" */\n display: none;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/editor-comment-previews/userstyle.css":
|
|
/*!*******************************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/editor-comment-previews/userstyle.css ***!
|
|
\*******************************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
|
|
|
|
// module
|
|
exports.push([module.i, ".sa-comment-preview-outer {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 100000000;\n pointer-events: none;\n}\n\n.sa-comment-preview-inner {\n width: calc(200px - 16px);\n max-height: calc(132px - 8px);\n padding: 8px;\n overflow: hidden;\n\n font-size: 12px;\n white-space: pre-wrap;\n pointer-events: none;\n\n color: rgb(87, 94, 117);\n background-color: rgb(255 255 255 / 90%);\n border-style: none;\n border-radius: 8px;\n filter: drop-shadow(0px 5px 5px rgb(0 0 0 / 10%));\n\n transform: perspective(200px);\n}\n\n@supports (backdrop-filter: blur(16px)) {\n .sa-comment-preview-inner {\n background-color: rgb(255 255 255 / 75%);\n backdrop-filter: blur(16px);\n }\n}\n\n.sa-comment-preview-fade {\n transition: opacity 0.1s, filter 0.1s, transform 0.1s linear;\n}\n\n.sa-comment-preview-hidden {\n opacity: 0;\n filter: none;\n transform: perspective(200px) translateZ(-20px);\n}\n\n.sa-comment-preview-reduce-transparency {\n background-color: rgb(255 255 255);\n backdrop-filter: none;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/editor-searchable-dropdowns/userscript.css":
|
|
/*!************************************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/editor-searchable-dropdowns/userscript.css ***!
|
|
\************************************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
|
|
|
|
// module
|
|
exports.push([module.i, ".u-dropdown-searchbar {\n width: 100%;\n box-sizing: border-box;\n /* based on styles for the title input */\n color: white;\n background-color: hsla(0, 100%, 100%, 0.25);\n border: 1px solid hsla(0, 0%, 0%, 0.15);\n padding: 0.5rem;\n outline: none;\n transition: 0.25s ease-out;\n font-size: 13px;\n font-weight: bold;\n border-radius: 4px;\n}\n.u-dropdown-searchbar:hover {\n background-color: hsla(0, 100%, 100%, 0.5);\n}\n.u-dropdown-searchbar:focus {\n background-color: white;\n color: black;\n}\n.blocklyDropDownDiv .goog-menu {\n overflow-x: hidden;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/editor-theme3/compatibility.css":
|
|
/*!*************************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/editor-theme3/compatibility.css ***!
|
|
\*************************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
|
|
|
|
// module
|
|
exports.push([module.i, "/* Imported by other addons */\n\n.sa-block-color {\n --sa-block-colored-background: var(--sa-block-background-primary);\n --sa-block-colored-background-secondary: var(--sa-block-field-background);\n --sa-block-bright-background: var(--sa-block-background-primary);\n --sa-block-text: white;\n --sa-block-gray-text: white;\n --sa-block-colored-text: var(--sa-block-background-primary);\n --sa-block-text-on-bright-background: white;\n}\n\n.sa-block-color-motion {\n --sa-block-background-primary: var(--editorTheme3-motion-primary, #4c97ff);\n --sa-block-background-secondary: var(--editorTheme3-motion-secondary, #4280d7);\n --sa-block-background-tertiary: var(--editorTheme3-motion-tertiary, #3373cc);\n --sa-block-field-background: var(--editorTheme3-motion-field, #3373cc);\n}\n\n.sa-block-color-looks {\n --sa-block-background-primary: var(--editorTheme3-looks-primary, #9966ff);\n --sa-block-background-secondary: var(--editorTheme3-looks-secondary, #855cd6);\n --sa-block-background-tertiary: var(--editorTheme3-looks-tertiary, #774dcb);\n --sa-block-field-background: var(--editorTheme3-looks-field, #774dcb);\n}\n\n.sa-block-color-sounds {\n --sa-block-background-primary: var(--editorTheme3-sounds-primary, #cf63cf);\n --sa-block-background-secondary: var(--editorTheme3-sounds-secondary, #c94fc9);\n --sa-block-background-tertiary: var(--editorTheme3-sounds-tertiary, #bd42bd);\n --sa-block-field-background: var(--editorTheme3-sounds-field, #bd42bd);\n}\n\n.sa-block-color-events {\n --sa-block-background-primary: var(--editorTheme3-event-primary, #ffbf00);\n --sa-block-background-secondary: var(--editorTheme3-event-secondary, #e6ac00);\n --sa-block-background-tertiary: var(--editorTheme3-event-tertiary, #cc9900);\n --sa-block-field-background: var(--editorTheme3-event-field, #cc9900);\n}\n\n.sa-block-color-control {\n --sa-block-background-primary: var(--editorTheme3-control-primary, #ffab19);\n --sa-block-background-secondary: var(--editorTheme3-control-secondary, #ec9c13);\n --sa-block-background-tertiary: var(--editorTheme3-control-tertiary, #cf8b17);\n --sa-block-field-background: var(--editorTheme3-control-field, #cf8b17);\n}\n\n.sa-block-color-sensing {\n --sa-block-background-primary: var(--editorTheme3-sensing-primary, #5cb1d6);\n --sa-block-background-secondary: var(--editorTheme3-sensing-secondary, #47a8d1);\n --sa-block-background-tertiary: var(--editorTheme3-sensing-tertiary, #2e8eb8);\n --sa-block-field-background: var(--editorTheme3-sensing-field, #2e8eb8);\n}\n\n.sa-block-color-operators {\n --sa-block-background-primary: var(--editorTheme3-operators-primary, #59c059);\n --sa-block-background-secondary: var(--editorTheme3-operators-secondary, #46b946);\n --sa-block-background-tertiary: var(--editorTheme3-operators-tertiary, #389438);\n --sa-block-field-background: var(--editorTheme3-operators-field, #389438);\n}\n\n.sa-block-color-data {\n --sa-block-background-primary: var(--editorTheme3-data-primary, #ff8c1a);\n --sa-block-background-secondary: var(--editorTheme3-data-secondary, #ff8000);\n --sa-block-background-tertiary: var(--editorTheme3-data-tertiary, #db6e00);\n --sa-block-field-background: var(--editorTheme3-data-field, #db6e00);\n}\n\n.sa-block-color-data-lists,\n.sa-block-color-list {\n --sa-block-background-primary: var(--editorTheme3-data_lists-primary, #ff661a);\n --sa-block-background-secondary: var(--editorTheme3-data_lists-secondary, #ff5500);\n --sa-block-background-tertiary: var(--editorTheme3-data_lists-tertiary, #e64d00);\n --sa-block-field-background: var(--editorTheme3-data_lists-field, #e64d00);\n}\n\n.sa-block-color-more,\n.sa-block-color-null {\n --sa-block-background-primary: var(--editorTheme3-more-primary, #ff6680);\n --sa-block-background-secondary: var(--editorTheme3-more-secondary, #ff4d6a);\n --sa-block-background-tertiary: var(--editorTheme3-more-tertiary, #ff3355);\n --sa-block-field-background: var(--editorTheme3-more-field, #ff3355);\n}\n\n.sa-block-color-pen {\n --sa-block-background-primary: var(--editorTheme3-pen-primary, #0fbd8c);\n --sa-block-background-secondary: var(--editorTheme3-pen-secondary, #0da57a);\n --sa-block-background-tertiary: var(--editorTheme3-pen-tertiary, #0b8e69);\n --sa-block-field-background: var(--editorTheme3-pen-field, #0b8e69);\n}\n\n.sa-block-color-addon-custom-block {\n --sa-block-background-primary: var(--editorTheme3-sa-primary, #29beb8);\n --sa-block-background-secondary: var(--editorTheme3-sa-secondary, #3aa8a4);\n --sa-block-background-tertiary: var(--editorTheme3-sa-tertiary, #3aa8a4);\n --sa-block-field-background: var(--editorTheme3-sa-field, #3aa8a4);\n}\n\n.sa-block-color-TurboWarp {\n --sa-block-background-primary: var(--editorTheme3-tw-primary, #ff4c4c);\n --sa-block-background-secondary: var(--editorTheme3-tw-secondary, #e64444);\n --sa-block-background-tertiary: var(--editorTheme3-tw-tertiary, #e64444);\n --sa-block-field-background: var(--editorTheme3-tw-field, #e64444);\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/find-bar/userstyle.css":
|
|
/*!****************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/find-bar/userstyle.css ***!
|
|
\****************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
exports.i(__webpack_require__(/*! -!../../../../node_modules/css-loader!../editor-theme3/compatibility.css */ "./node_modules/css-loader/index.js!./src/addons/addons/editor-theme3/compatibility.css"), "");
|
|
|
|
// module
|
|
exports.push([module.i, ".sa-find-bar {\n display: flex;\n align-items: center;\n white-space: nowrap;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n width: 100%;\n height: 100%;\n margin-left: 1em;\n}\n.sa-find-bar[hidden] {\n /* !important to override displayNoneWhileDisabled */\n display: none !important;\n}\n\n.sa-find-wrapper {\n overflow: visible;\n position: relative;\n height: 2rem;\n width: 100%;\n max-width: 16em;\n}\n\n.sa-find-dropdown-out {\n display: block;\n top: -6px;\n z-index: 100;\n width: 100%;\n max-width: 16em;\n position: relative;\n padding: 4px;\n border: none;\n border-radius: 4px;\n margin-top: 6px;\n}\n\n.sa-find-dropdown-out.visible {\n position: absolute;\n width: 16em;\n box-shadow: 0px 0px 8px 1px var(--ui-black-transparent, rgba(0, 0, 0, 0.3));\n background-color: var(--ui-primary, white);\n}\n\n/* We need to modify Scratch styles so that the place where the find bar is injected */\n/* has actually correct size information, which is used to make the find bar not cover up controls */\n[class*=\"gui_tab-list_\"] {\n width: 100%;\n}\n[class*=\"gui_tab_\"] {\n flex-grow: 0;\n}\n\n.sa-find-input {\n width: 100%;\n box-sizing: border-box !important;\n /* !important required for extension, because CSS injection method (and hence order) differs from addon */\n height: 1.5rem;\n\n /* Change Scratch default styles */\n border-radius: 0.25rem;\n font-size: 0.75rem;\n padding-left: 0.4em;\n}\n\n.sa-find-input:focus {\n /* Change Scratch default styles */\n box-shadow: none;\n}\n\n.sa-find-dropdown {\n display: none;\n position: relative;\n padding: 0.2em 0;\n font-size: 0.75rem;\n line-height: 1;\n overflow-y: auto;\n min-height: 128px;\n max-height: 65vh;\n user-select: none;\n max-width: 100%;\n margin-top: 6px;\n border: none;\n}\n\n.sa-find-dropdown-out.visible > .sa-find-dropdown {\n display: block;\n}\n\n.sa-find-dropdown > li {\n display: block;\n padding: 0.5em 0.3em;\n white-space: nowrap;\n margin: 0;\n font-weight: bold;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\n.sa-find-dropdown > li > b {\n background-color: #aaffaa;\n color: black;\n}\n\n/* Drop down items */\n.sa-find-dropdown > li:hover,\n.sa-find-dropdown > li.sel {\n color: var(--sa-block-text-on-bright-background);\n cursor: pointer;\n}\n\n.sa-find-dropdown > li::before {\n content: \"\\25CF \"; /* ● */\n}\n\n.sa-find-flag {\n color: #4cbf56;\n}\n/* .sa-find-dropdown added for specificity */\n.sa-find-dropdown > .sa-find-flag:hover,\n.sa-find-dropdown > .sa-find-flag.sel {\n background-color: #4cbf56;\n color: white;\n}\n\n.sa-find-dropdown .sa-block-color {\n color: var(--sa-block-colored-text);\n}\n.sa-find-dropdown .sa-block-color:hover,\n.sa-find-dropdown .sa-block-color.sel {\n background-color: var(--sa-block-bright-background);\n}\n\n.sa-find-carousel {\n font-weight: normal;\n position: absolute;\n right: 0;\n white-space: nowrap;\n background-color: inherit;\n z-index: 1;\n padding: 0;\n}\n\n.sa-find-carousel-control {\n padding: 0 6px;\n}\n\n.sa-find-carousel-control:hover {\n color: #ffff80;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/folders/style.css":
|
|
/*!***********************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/folders/style.css ***!
|
|
\***********************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
|
|
|
|
// module
|
|
exports.push([module.i, ".sa-folders-contextmenu-item {\n max-width: 250px;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n[sa-folders-context-type=\"folder\"] .react-contextmenu > :not(.sa-ctx-menu) {\n display: none;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/middle-click-popup/userstyle.css":
|
|
/*!**************************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/middle-click-popup/userstyle.css ***!
|
|
\**************************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
exports.i(__webpack_require__(/*! -!../../../../node_modules/css-loader!../editor-theme3/compatibility.css */ "./node_modules/css-loader/index.js!./src/addons/addons/editor-theme3/compatibility.css"), "");
|
|
|
|
// module
|
|
exports.push([module.i, "/* Find Input Box */\n.sa-float-bar-input {\n width: 100%;\n box-sizing: border-box !important;\n /* !important required for extension, because CSS injection method (and hence order) differs from addon */\n height: 1.5rem;\n\n /* Change Scratch default styles */\n border-radius: 0.25rem;\n font-size: 0.75rem;\n padding-left: 0.4em;\n}\n[theme=\"dark\"] input.s3devInp {\n color: #eee;\n background: #3333;\n}\n[theme=\"dark\"] input.s3devInp:hover {\n background: #333;\n}\n\n.sa-float-bar-input:focus {\n /* Change Scratch default styles */\n box-shadow: none;\n}\n\n/* Drop down from find button */\n.sa-float-bar-dropdown-out {\n display: block;\n top: -6px;\n z-index: 100;\n max-width: 16em;\n padding: 4px;\n position: absolute;\n width: 16em;\n box-shadow: 0px 0px 8px 1px var(--ui-black-transparent, rgba(0, 0, 0, 0.3));\n background-color: var(--ui-primary, white);\n border: none;\n border-radius: 4px;\n}\n\n/* Drop down from find button */\n.sa-float-bar-dropdown {\n display: none;\n position: relative;\n padding: 0.2em 0;\n font-size: 0.75rem;\n line-height: 1;\n overflow-y: auto;\n min-height: 128px;\n user-select: none;\n max-width: 100%;\n max-height: 200px;\n margin-bottom: 0;\n}\n\n.sa-float-bar-dropdown-out.vis .sa-float-bar-dropdown {\n display: block;\n border: none;\n}\n\n/* Drop down items */\n.sa-float-bar-dropdown > li {\n display: block;\n padding: 0.5em 0.3em;\n white-space: nowrap;\n margin: 0;\n font-weight: bold;\n text-overflow: ellipsis;\n overflow: hidden;\n cursor: pointer;\n}\n\n.sa-float-bar-dropdown > li > b {\n background-color: #aaffaa;\n color: black;\n}\n\n.sa-float-bar-dropdown > li {\n height: 19px;\n padding: 3px 8px;\n margin: 2px 0.3em;\n box-sizing: border-box;\n position: relative;\n background-color: var(--sa-block-colored-background);\n color: var(--sa-block-text);\n font-weight: bold;\n width: min-content;\n}\n.sa-float-bar-dropdown > li:hover,\n.sa-float-bar-dropdown > li.sel {\n background-color: var(--sa-block-colored-background-secondary);\n}\n\n.sa-float-bar-dropdown > li.sa-hat {\n border-radius: 14px 14px 3px 3px;\n}\n.sa-float-bar-dropdown > li.sa-block {\n border-radius: 3px;\n}\n.sa-float-bar-dropdown > li.sa-reporter {\n border-radius: 10px;\n}\n\n.sa-float-bar-dropdown > li.sa-boolean {\n width: min-content;\n}\n.sa-float-bar-dropdown > li.sa-boolean::before {\n content: \"\";\n position: absolute;\n left: 0;\n top: 0;\n width: 0;\n height: 0;\n border-right: 9px solid transparent;\n border-top: 9px solid var(--ui-primary, white);\n border-bottom: 10px solid var(--ui-primary, white);\n}\n.sa-float-bar-dropdown > li.sa-boolean::after {\n content: \"\";\n position: absolute;\n right: 0;\n top: 0;\n width: 0;\n height: 0;\n border-left: 9px solid transparent;\n border-top: 9px solid var(--ui-primary, white);\n border-bottom: 10px solid var(--ui-primary, white);\n}\n[theme=\"dark\"] .s3devDD > li.boolean::before {\n border-top-color: #111;\n border-bottom-color: #111;\n}\n[theme=\"dark\"] .s3devDD > li.boolean::after {\n border-top-color: #111;\n border-bottom-color: #111;\n}\n\n.sa-float-bar {\n display: flex;\n white-space: nowrap;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n\n position: absolute;\n min-width: 128px;\n background-color: white;\n border-radius: 4px;\n box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.2) 0 3px 10px;\n\n z-index: 999;\n}\n[theme=\"dark\"] #s3devFloatingBar {\n background-color: #111;\n}\n\n.sa-float-bar-dropdown > li > b {\n background-color: rgba(0, 0, 0, 0.6);\n color: white;\n}\n\n[data-highlighted=\"true\"] {\n background-color: hsla(30, 100%, 55%, 1) !important; /* orange */\n color: white !important;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/onion-skinning/style.css":
|
|
/*!******************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/onion-skinning/style.css ***!
|
|
\******************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
|
|
|
|
// module
|
|
exports.push([module.i, ".sa-onion-button {\n position: relative;\n}\n.sa-onion-button:focus-within {\n background-color: hsla(0, 100%, 65%, 0.2);\n}\n[theme=\"dark\"] .sa-onion-image {\n filter: brightness(0) invert(0.8);\n}\n.sa-onion-button[data-enabled=\"true\"] .sa-onion-image {\n filter: brightness(0) invert(1);\n}\n.sa-onion-button[data-enabled=\"true\"] {\n background-color: #ff4c4c;\n}\n\n.sa-onion-group {\n position: relative;\n flex-direction: row;\n}\n\n.sa-onion-settings-wrapper {\n position: absolute;\n justify-items: center;\n left: 50%;\n width: 1.95rem;\n height: 1.95rem;\n display: grid;\n}\n\n.sa-onion-settings {\n position: absolute;\n bottom: 100%;\n /* based on the styles for the color dropdown */\n padding: 4px;\n border-radius: 4px;\n border: 1px solid var(--paint-ui-pane-border, #ddd);\n box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.3);\n transition-property: bottom, opacity;\n transition-duration: 500ms;\n transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);\n opacity: 0;\n pointer-events: none;\n background: var(--ui-primary, white);\n min-height: 100%;\n min-width: 100%;\n display: flex;\n flex-direction: column;\n gap: 0.25em;\n}\n.sa-onion-settings[data-visible=\"true\"] {\n bottom: calc(100% + 22px);\n pointer-events: auto;\n opacity: 1;\n}\n\n.sa-onion-settings-line {\n display: flex;\n justify-content: flex-end;\n align-items: baseline;\n gap: 0.25em;\n}\n\n.sa-onion-settings-input {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n text-align: center;\n border: 0;\n background: transparent;\n -moz-appearance: textfield;\n border: 0;\n outline: 0;\n}\n\n.sa-onion-settings-input::-webkit-outer-spin-button,\n.sa-onion-settings-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\n\n.sa-onion-settings-tip {\n position: absolute;\n bottom: 0;\n transform: translateY(100%);\n right: calc(50% - 7px);\n}\n.sa-onion-settings-polygon {\n fill: var(--ui-primary, white);\n stroke: var(--paint-ui-pane-border, #ddd);\n}\n\n.sa-onion-settings-label {\n white-space: nowrap;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/css-loader/index.js!./src/addons/addons/pick-colors-from-stage/style.css":
|
|
/*!**************************************************************************************!*\
|
|
!*** ./node_modules/css-loader!./src/addons/addons/pick-colors-from-stage/style.css ***!
|
|
\**************************************************************************************/
|
|
/*! no static exports found */
|
|
/***/ (function(module, exports, __webpack_require__) {
|
|
|
|
exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false);
|
|
// imports
|
|
|
|
|
|
// module
|
|
exports.push([module.i, ".sa-stage-color-picker-picking [class^=\"stage_color-picker-background_\"] {\n display: none;\n}\n", ""]);
|
|
|
|
// exports
|
|
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/editor-devtools/icon--close.svg":
|
|
/*!*************************************************************************************************!*\
|
|
!*** ./node_modules/url-loader/dist/cjs.js!./src/addons/addons/editor-devtools/icon--close.svg ***!
|
|
\*************************************************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = ("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3LjQ4IDcuNDgiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDpub25lO3N0cm9rZTojZmZmO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6MnB4O308L3N0eWxlPjwvZGVmcz48dGl0bGU+aWNvbi0tYWRkPC90aXRsZT48bGluZSBjbGFzcz0iY2xzLTEiIHgxPSIzLjc0IiB5MT0iNi40OCIgeDI9IjMuNzQiIHkyPSIxIi8+PGxpbmUgY2xhc3M9ImNscy0xIiB4MT0iMSIgeTE9IjMuNzQiIHgyPSI2LjQ4IiB5Mj0iMy43NCIvPjwvc3ZnPg==");
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/folders/folder.svg":
|
|
/*!************************************************************************************!*\
|
|
!*** ./node_modules/url-loader/dist/cjs.js!./src/addons/addons/folders/folder.svg ***!
|
|
\************************************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = ("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xMCA0SDRjLTEuMSAwLTEuOTkuOS0xLjk5IDJMMiAxOGMwIDEuMS45IDIgMiAyaDE2YzEuMSAwIDItLjkgMi0yVjhjMC0xLjEtLjktMi0yLTJoLThsLTItMnoiLz48L3N2Zz4=");
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/decrement.svg":
|
|
/*!**********************************************************************************************!*\
|
|
!*** ./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/decrement.svg ***!
|
|
\**********************************************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = ("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggc3R5bGU9ImZpbGw6IzU3NWU3NTtzdHJva2Utd2lkdGg6LjczNDczNiIgZD0iTTMuMjUgMTEuMzU4aDE3LjUwMXYxLjI4NUgzLjI1eiIvPjwvc3ZnPg==");
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/increment.svg":
|
|
/*!**********************************************************************************************!*\
|
|
!*** ./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/increment.svg ***!
|
|
\**********************************************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = ("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCI+PHBhdGggc3R5bGU9ImZpbGw6IzU3NWU3NTtzdHJva2Utd2lkdGg6LjczNDczNiIgZD0iTTMuMjUgMTEuMzU4aDE3LjUwMXYxLjI4NUgzLjI1eiIvPjxwYXRoIHRyYW5zZm9ybT0icm90YXRlKDkwKSIgc3R5bGU9ImZpbGw6IzU3NWU3NTtzdHJva2Utd2lkdGg6LjczNDczNiIgZD0iTTMuMjUtMTIuNjQyaDE3LjUwMXYxLjI4NUgzLjI1eiIvPjwvc3ZnPg==");
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/settings.svg":
|
|
/*!*********************************************************************************************!*\
|
|
!*** ./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/settings.svg ***!
|
|
\*********************************************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = ("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggc3R5bGU9Im9wYWNpdHk6Ljc1O2ZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6NDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLW9wYWNpdHk6MSIgZD0iTTU2IDE2djk2IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMi4zNzYgLTIuMzc2KSBzY2FsZSguMjI0NjIpIi8+PHBhdGggc3R5bGU9Im9wYWNpdHk6LjU7ZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2Utb3BhY2l0eToxIiBkPSJNNDAgMzJ2NjQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yLjM3NiAtMi4zNzYpIHNjYWxlKC4yMjQ2MikiLz48cGF0aCBzdHlsZT0ib3BhY2l0eTouMjU7ZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2Utb3BhY2l0eToxIiBkPSJNMjQgNDh2MzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yLjM3NiAtMi4zNzYpIHNjYWxlKC4yMjQ2MikiLz48cGF0aCBzdHlsZT0ib3BhY2l0eTouNzU7ZmlsbDpub25lO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo0O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2Utb3BhY2l0eToxIiBkPSJNNzIgMTZ2OTYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yLjM3NiAtMi4zNzYpIHNjYWxlKC4yMjQ2MikiLz48cGF0aCBzdHlsZT0ib3BhY2l0eTouNTtmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjEiIGQ9Ik04OCAzMnY2NCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIuMzc2IC0yLjM3Nikgc2NhbGUoLjIyNDYyKSIvPjxwYXRoIHN0eWxlPSJvcGFjaXR5Oi4yNTtmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjEiIGQ9Ik0xMDQgNDh2MzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yLjM3NiAtMi4zNzYpIHNjYWxlKC4yMjQ2MikiLz48L3N2Zz4=");
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/toggle.svg":
|
|
/*!*******************************************************************************************!*\
|
|
!*** ./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/toggle.svg ***!
|
|
\*******************************************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = ("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PGcgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZT0iIzAwMTAyNiI+PGcgc3R5bGU9Im9wYWNpdHk6LjU7c3Ryb2tlLXdpZHRoOjQ7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLWRhc2hhcnJheTpub25lIj48cGF0aCBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6bm9ybWFsO2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2Utd2lkdGg6NDtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MCIgZmlsbD0iI2JmYmZiZiIgZD0iTTY4LjQ1IDMyLjQ1N2MuMy0uMi44LS4xLjkuM2wyLjYgMTAuN3M2LjQgNC43IDguMyA4YzMuMiA1LjUgMy4zIDEwIDMuMyAxMHM3LjEgMi4xIDguMyA3LjhjMS4yIDUuNy0zLjIgMTYuNS0yMiAyMC4yLTE4LjggMy43LTMzLjktMS40LTQxLTEyLjgtNy4xLTExLjQgNC4xLTI1IDMuNS0yNC4ybC0yLjEtMTcuOWMtLjEtLjQuNC0uNy44LS41bDEyLjEgNy45czQuNS0xLjcgOS4yLTEuOWMyLjgtLjIgNS4yIDAgNy41LjR6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42NTIgLTQuNjUpIHNjYWxlKC4yMzI5KSIvPjxwYXRoIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpub3JtYWw7ZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZS13aWR0aDo0O3N0cm9rZS1taXRlcmxpbWl0OjEwO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiIGQ9Ik0xMDEuNjUgNjIuOTU3Yy00LjcgMy44LTExLjkgMy45LTExLjkgMy45bTEwLjYgNy4zYy02LjMuNS0xMC4yLTEuNC0xMC4yLTEuNG0tNjguOC0xMC4xczguNiAyLjggMTIuMSA1LjltLjMgMy41Yy00LjMgMS43LTExLjcuNi0xMS43LjYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjY1MiAtNC42NSkgc2NhbGUoLjIzMjkpIi8+PC9nPjxnIHN0eWxlPSJvcGFjaXR5Oi43NTtzdHJva2Utd2lkdGg6NDtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmUiPjxwYXRoIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpub3JtYWw7ZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZS13aWR0aDo0O3N0cm9rZS1taXRlcmxpbWl0OjEwO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowIiBmaWxsPSIjYmZiZmJmIiBkPSJNNjguNDUgMzIuNDU3Yy4zLS4yLjgtLjEuOS4zbDIuNiAxMC43czYuNCA0LjcgOC4zIDhjMy4yIDUuNSAzLjMgMTAgMy4zIDEwczcuMSAyLjEgOC4zIDcuOGMxLjIgNS43LTMuMiAxNi41LTIyIDIwLjItMTguOCAzLjctMzMuOS0xLjQtNDEtMTIuOC03LjEtMTEuNCA0LjEtMjUgMy41LTI0LjJsLTIuMS0xNy45Yy0uMS0uNC40LS43LjgtLjVsMTIuMSA3LjlzNC41LTEuNyA5LjItMS45YzIuOC0uMiA1LjIgMCA3LjUuNHoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yLjMyMyAtMi4zMjEpIHNjYWxlKC4yMzI5KSIvPjxwYXRoIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpub3JtYWw7ZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZS13aWR0aDo0O3N0cm9rZS1taXRlcmxpbWl0OjEwO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiIGQ9Ik0xMDEuNjUgNjIuOTU3Yy00LjcgMy44LTExLjkgMy45LTExLjkgMy45bTEwLjYgNy4zYy02LjMuNS0xMC4yLTEuNC0xMC4yLTEuNG0tNjguOC0xMC4xczguNiAyLjggMTIuMSA1LjltLjMgMy41Yy00LjMgMS43LTExLjcuNi0xMS43LjYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yLjMyMyAtMi4zMjEpIHNjYWxlKC4yMzI5KSIvPjwvZz48ZyBzdHlsZT0ic3Ryb2tlLXdpZHRoOjQ7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLWRhc2hhcnJheTpub25lIj48cGF0aCBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6bm9ybWFsO2ZpbGw6I2ZmZjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2Utd2lkdGg6NDtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MCIgZmlsbD0iI2JmYmZiZiIgZD0iTTczLjQ1IDM3LjQ1N2MuMy0uMi44LS4xLjkuM2wyLjYgMTAuN3M2LjQgNC43IDguMyA4YzMuMiA1LjUgMy4zIDEwIDMuMyAxMHM3LjEgMi4xIDguMyA3LjhjMS4yIDUuNy0zLjIgMTYuNS0yMiAyMC4yLTE4LjggMy43LTMzLjktMS40LTQxLTEyLjgtNy4xLTExLjQgNC4xLTI1IDMuNS0yNC4ybC0yLjEtMTcuOWMtLjEtLjQuNC0uNy44LS41bDEyLjEgNy45czQuNS0xLjcgOS4yLTEuOWMyLjgtLjIgNS4yIDAgNy41LjR6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMS4xNTkgLTEuMTU3KSBzY2FsZSguMjMyOSkiLz48cGF0aCBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6bm9ybWFsO2ZpbGw6bm9uZTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2Utd2lkdGg6NDtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIiBkPSJNMTA2LjY1IDY3Ljk1N2MtNC43IDMuOC0xMS45IDMuOS0xMS45IDMuOW0xMC42IDcuM2MtNi4zLjUtMTAuMi0xLjQtMTAuMi0xLjRtLTY4LjgtMTAuMXM4LjYgMi44IDEyLjEgNS45bS4zIDMuNWMtNC4zIDEuNy0xMS43LjYtMTEuNy42IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMS4xNTkgLTEuMTU3KSBzY2FsZSguMjMyOSkiLz48L2c+PC9nPjwvc3ZnPg==");
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/bitmap-copy/_runtime_entry.js":
|
|
/*!*********************************************************!*\
|
|
!*** ./src/addons/addons/bitmap-copy/_runtime_entry.js ***!
|
|
\*********************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/bitmap-copy/userscript.js");
|
|
/* generated by pull.js */
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/bitmap-copy/userscript.js":
|
|
/*!*****************************************************!*\
|
|
!*** ./src/addons/addons/bitmap-copy/userscript.js ***!
|
|
\*****************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async ({
|
|
addon,
|
|
console
|
|
}) => {
|
|
if (!addon.tab.redux.state) return console.warn("Redux is not available!");
|
|
addon.tab.redux.initialize();
|
|
addon.tab.redux.addEventListener("statechanged", ({
|
|
detail
|
|
}) => {
|
|
if (addon.self.disabled) return;
|
|
const e = detail;
|
|
if (!e.action || e.action.type !== "scratch-paint/clipboard/SET") return;
|
|
const items = e.next.scratchPaint.clipboard.items;
|
|
if (items.length !== 1) return;
|
|
const firstItem = items[0]; // TODO vector support
|
|
|
|
if (!Array.isArray(firstItem) || firstItem[0] !== "Raster") return console.log("copied element is vector");
|
|
const dataURL = firstItem[1].source;
|
|
addon.tab.copyImage(dataURL).then(() => console.log("Image successfully copied")).catch(e => console.error("Image could not be copied: ".concat(e)));
|
|
});
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/block-cherry-picking/_runtime_entry.js":
|
|
/*!******************************************************************!*\
|
|
!*** ./src/addons/addons/block-cherry-picking/_runtime_entry.js ***!
|
|
\******************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/block-cherry-picking/userscript.js");
|
|
/* generated by pull.js */
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/block-cherry-picking/userscript.js":
|
|
/*!**************************************************************!*\
|
|
!*** ./src/addons/addons/block-cherry-picking/userscript.js ***!
|
|
\**************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony import */ var _block_duplicate_module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../block-duplicate/module.js */ "./src/addons/addons/block-duplicate/module.js");
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console
|
|
}) {
|
|
const update = () => {
|
|
_block_duplicate_module_js__WEBPACK_IMPORTED_MODULE_0__["setCherryPicking"](!addon.self.disabled, addon.settings.get("invertDrag"));
|
|
};
|
|
|
|
addon.self.addEventListener("disabled", update);
|
|
addon.self.addEventListener("reenabled", update);
|
|
addon.settings.addEventListener("change", update);
|
|
update();
|
|
_block_duplicate_module_js__WEBPACK_IMPORTED_MODULE_0__["load"](addon);
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/block-duplicate/_runtime_entry.js":
|
|
/*!*************************************************************!*\
|
|
!*** ./src/addons/addons/block-duplicate/_runtime_entry.js ***!
|
|
\*************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/block-duplicate/userscript.js");
|
|
/* generated by pull.js */
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/block-duplicate/module.js":
|
|
/*!*****************************************************!*\
|
|
!*** ./src/addons/addons/block-duplicate/module.js ***!
|
|
\*****************************************************/
|
|
/*! exports provided: setCherryPicking, setDuplication, load */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "setCherryPicking", function() { return setCherryPicking; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "setDuplication", function() { return setDuplication; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "load", function() { return load; });
|
|
let enableCherryPicking = false;
|
|
let invertCherryPicking = false;
|
|
function setCherryPicking(newEnabled, newInverted) {
|
|
enableCherryPicking = newEnabled; // If cherry picking is disabled, also disable invert. Duplicating blocks can still cause
|
|
// this setting to be used.
|
|
|
|
invertCherryPicking = newEnabled && newInverted;
|
|
}
|
|
let enableDuplication = false;
|
|
function setDuplication(newEnabled) {
|
|
enableDuplication = newEnabled;
|
|
} // mostRecentEvent_ is sometimes a fake event, so we can't rely on reading its properties.
|
|
|
|
let ctrlOrMetaPressed = false;
|
|
let altPressed = false;
|
|
document.addEventListener("mousedown", function (e) {
|
|
ctrlOrMetaPressed = e.ctrlKey || e.metaKey;
|
|
altPressed = e.altKey;
|
|
}, {
|
|
capture: true
|
|
});
|
|
let loaded = false;
|
|
async function load(addon) {
|
|
if (loaded) {
|
|
return;
|
|
}
|
|
|
|
loaded = true;
|
|
const ScratchBlocks = await addon.tab.traps.getBlockly(); // https://github.com/LLK/scratch-blocks/blob/912b8cc728bea8fd91af85078c64fcdbfe21c87a/core/gesture.js#L454
|
|
|
|
const originalStartDraggingBlock = ScratchBlocks.Gesture.prototype.startDraggingBlock_;
|
|
|
|
ScratchBlocks.Gesture.prototype.startDraggingBlock_ = function (...args) {
|
|
let block = this.targetBlock_; // Scratch uses fake mouse events to implement right click > duplicate
|
|
|
|
const isRightClickDuplicate = !(this.mostRecentEvent_ instanceof MouseEvent);
|
|
const isDuplicating = enableDuplication && altPressed && !isRightClickDuplicate && !this.flyout_ && !this.shouldDuplicateOnDrag_ && this.targetBlock_.type !== "procedures_definition";
|
|
const isCherryPickingInverted = invertCherryPicking && !isRightClickDuplicate && block.getParent();
|
|
const canCherryPick = enableCherryPicking || isDuplicating;
|
|
const isCherryPicking = canCherryPick && ctrlOrMetaPressed === !isCherryPickingInverted && !block.isShadow();
|
|
|
|
if (isDuplicating || isCherryPicking) {
|
|
if (!ScratchBlocks.Events.getGroup()) {
|
|
// Scratch will disable grouping on its own later.
|
|
ScratchBlocks.Events.setGroup(true);
|
|
}
|
|
}
|
|
|
|
if (isDuplicating) {
|
|
// Based on https://github.com/LLK/scratch-blocks/blob/feda366947432b9d82a4f212f43ff6d4ab6f252f/core/scratch_blocks_utils.js#L171
|
|
// Setting this.shouldDuplicateOnDrag_ = true does NOT work because it doesn't call changeObscuredShadowIds
|
|
this.startWorkspace_.setResizesEnabled(false);
|
|
ScratchBlocks.Events.disable();
|
|
let newBlock;
|
|
|
|
try {
|
|
const xmlBlock = ScratchBlocks.Xml.blockToDom(block);
|
|
newBlock = ScratchBlocks.Xml.domToBlock(xmlBlock, this.startWorkspace_);
|
|
ScratchBlocks.scratchBlocksUtils.changeObscuredShadowIds(newBlock);
|
|
const xy = block.getRelativeToSurfaceXY();
|
|
newBlock.moveBy(xy.x, xy.y);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
ScratchBlocks.Events.enable();
|
|
|
|
if (newBlock) {
|
|
block = newBlock;
|
|
this.targetBlock_ = newBlock;
|
|
|
|
if (ScratchBlocks.Events.isEnabled()) {
|
|
ScratchBlocks.Events.fire(new ScratchBlocks.Events.BlockCreate(newBlock));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isCherryPicking) {
|
|
if (isRightClickDuplicate || isDuplicating) {
|
|
const nextBlock = block.getNextBlock();
|
|
|
|
if (nextBlock) {
|
|
nextBlock.dispose();
|
|
}
|
|
}
|
|
|
|
block.unplug(true);
|
|
}
|
|
|
|
return originalStartDraggingBlock.call(this, ...args);
|
|
};
|
|
}
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/block-duplicate/userscript.js":
|
|
/*!*********************************************************!*\
|
|
!*** ./src/addons/addons/block-duplicate/userscript.js ***!
|
|
\*********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module.js */ "./src/addons/addons/block-duplicate/module.js");
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console
|
|
}) {
|
|
const update = () => {
|
|
_module_js__WEBPACK_IMPORTED_MODULE_0__["setDuplication"](!addon.self.disabled);
|
|
};
|
|
|
|
addon.self.addEventListener("disabled", update);
|
|
addon.self.addEventListener("reenabled", update);
|
|
update();
|
|
_module_js__WEBPACK_IMPORTED_MODULE_0__["load"](addon);
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/block-switching/_runtime_entry.js":
|
|
/*!*************************************************************!*\
|
|
!*** ./src/addons/addons/block-switching/_runtime_entry.js ***!
|
|
\*************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/block-switching/userscript.js");
|
|
/* generated by pull.js */
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/block-switching/userscript.js":
|
|
/*!*********************************************************!*\
|
|
!*** ./src/addons/addons/block-switching/userscript.js ***!
|
|
\*********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console,
|
|
msg
|
|
}) {
|
|
const ScratchBlocks = await addon.tab.traps.getBlockly();
|
|
const vm = addon.tab.traps.vm;
|
|
let blockSwitches = {};
|
|
let procedureSwitches = {};
|
|
const noopSwitch = {
|
|
isNoop: true
|
|
};
|
|
|
|
const randomColor = () => {
|
|
const num = Math.floor(Math.random() * 256 * 256 * 256);
|
|
return "#".concat(num.toString(16).padStart(6, "0"));
|
|
};
|
|
|
|
const buildSwitches = () => {
|
|
blockSwitches = {};
|
|
procedureSwitches = {};
|
|
|
|
if (addon.settings.get("motion")) {
|
|
blockSwitches["motion_turnright"] = [noopSwitch, {
|
|
opcode: "motion_turnleft"
|
|
}];
|
|
blockSwitches["motion_turnleft"] = [{
|
|
opcode: "motion_turnright"
|
|
}, noopSwitch];
|
|
blockSwitches["motion_setx"] = [noopSwitch, {
|
|
opcode: "motion_changexby",
|
|
remapInputName: {
|
|
X: "DX"
|
|
}
|
|
}, {
|
|
opcode: "motion_sety",
|
|
remapInputName: {
|
|
X: "Y"
|
|
}
|
|
}, {
|
|
opcode: "motion_changeyby",
|
|
remapInputName: {
|
|
X: "DY"
|
|
}
|
|
}];
|
|
blockSwitches["motion_changexby"] = [{
|
|
opcode: "motion_setx",
|
|
remapInputName: {
|
|
DX: "X"
|
|
}
|
|
}, noopSwitch, {
|
|
opcode: "motion_sety",
|
|
remapInputName: {
|
|
DX: "Y"
|
|
}
|
|
}, {
|
|
opcode: "motion_changeyby",
|
|
remapInputName: {
|
|
DX: "DY"
|
|
}
|
|
}];
|
|
blockSwitches["motion_sety"] = [{
|
|
opcode: "motion_setx",
|
|
remapInputName: {
|
|
Y: "X"
|
|
}
|
|
}, {
|
|
opcode: "motion_changexby",
|
|
remapInputName: {
|
|
Y: "DX"
|
|
}
|
|
}, noopSwitch, {
|
|
opcode: "motion_changeyby",
|
|
remapInputName: {
|
|
Y: "DY"
|
|
}
|
|
}];
|
|
blockSwitches["motion_changeyby"] = [{
|
|
opcode: "motion_setx",
|
|
remapInputName: {
|
|
DY: "X"
|
|
}
|
|
}, {
|
|
opcode: "motion_changexby",
|
|
remapInputName: {
|
|
DY: "DX"
|
|
}
|
|
}, {
|
|
opcode: "motion_sety",
|
|
remapInputName: {
|
|
DY: "Y"
|
|
}
|
|
}, noopSwitch];
|
|
blockSwitches["motion_xposition"] = [noopSwitch, {
|
|
opcode: "motion_yposition"
|
|
}];
|
|
blockSwitches["motion_yposition"] = [{
|
|
opcode: "motion_xposition"
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("looks")) {
|
|
blockSwitches["looks_seteffectto"] = [noopSwitch, {
|
|
opcode: "looks_changeeffectby",
|
|
remapInputName: {
|
|
VALUE: "CHANGE"
|
|
}
|
|
}];
|
|
blockSwitches["looks_changeeffectby"] = [{
|
|
opcode: "looks_seteffectto",
|
|
remapInputName: {
|
|
CHANGE: "VALUE"
|
|
}
|
|
}, noopSwitch];
|
|
blockSwitches["looks_setsizeto"] = [noopSwitch, {
|
|
opcode: "looks_changesizeby",
|
|
remapInputName: {
|
|
SIZE: "CHANGE"
|
|
}
|
|
}];
|
|
blockSwitches["looks_changesizeby"] = [{
|
|
opcode: "looks_setsizeto",
|
|
remapInputName: {
|
|
CHANGE: "SIZE"
|
|
}
|
|
}, noopSwitch];
|
|
blockSwitches["looks_costumenumbername"] = [noopSwitch, {
|
|
opcode: "looks_backdropnumbername"
|
|
}];
|
|
blockSwitches["looks_backdropnumbername"] = [{
|
|
opcode: "looks_costumenumbername"
|
|
}, noopSwitch];
|
|
blockSwitches["looks_show"] = [noopSwitch, {
|
|
opcode: "looks_hide"
|
|
}];
|
|
blockSwitches["looks_hide"] = [{
|
|
opcode: "looks_show"
|
|
}, noopSwitch];
|
|
blockSwitches["looks_nextcostume"] = [noopSwitch, {
|
|
opcode: "looks_nextbackdrop"
|
|
}];
|
|
blockSwitches["looks_nextbackdrop"] = [{
|
|
opcode: "looks_nextcostume"
|
|
}, noopSwitch];
|
|
blockSwitches["looks_say"] = [noopSwitch, {
|
|
opcode: "looks_sayforsecs",
|
|
createInputs: {
|
|
SECS: {
|
|
shadowType: "math_number",
|
|
value: "2"
|
|
}
|
|
}
|
|
}, {
|
|
opcode: "looks_think"
|
|
}, {
|
|
opcode: "looks_thinkforsecs",
|
|
createInputs: {
|
|
SECS: {
|
|
shadowType: "math_number",
|
|
value: "2"
|
|
}
|
|
}
|
|
}];
|
|
blockSwitches["looks_think"] = [{
|
|
opcode: "looks_say"
|
|
}, {
|
|
opcode: "looks_sayforsecs",
|
|
createInputs: {
|
|
SECS: {
|
|
shadowType: "math_number",
|
|
value: "2"
|
|
}
|
|
}
|
|
}, noopSwitch, {
|
|
opcode: "looks_thinkforsecs",
|
|
createInputs: {
|
|
SECS: {
|
|
shadowType: "math_number",
|
|
value: "2"
|
|
}
|
|
}
|
|
}];
|
|
blockSwitches["looks_sayforsecs"] = [{
|
|
opcode: "looks_say",
|
|
splitInputs: ["SECS"]
|
|
}, {
|
|
opcode: "looks_think",
|
|
splitInputs: ["SECS"]
|
|
}, noopSwitch, {
|
|
opcode: "looks_thinkforsecs"
|
|
}];
|
|
blockSwitches["looks_thinkforsecs"] = [{
|
|
opcode: "looks_say",
|
|
splitInputs: ["SECS"]
|
|
}, {
|
|
opcode: "looks_think",
|
|
splitInputs: ["SECS"]
|
|
}, {
|
|
opcode: "looks_sayforsecs"
|
|
}, noopSwitch];
|
|
blockSwitches["looks_switchbackdropto"] = [noopSwitch, {
|
|
opcode: "looks_switchbackdroptoandwait"
|
|
}];
|
|
blockSwitches["looks_switchbackdroptoandwait"] = [{
|
|
opcode: "looks_switchbackdropto"
|
|
}, noopSwitch];
|
|
blockSwitches["looks_gotofrontback"] = [noopSwitch, {
|
|
opcode: "looks_goforwardbackwardlayers",
|
|
remapInputName: {
|
|
FRONT_BACK: "FORWARD_BACKWARD"
|
|
},
|
|
mapFieldValues: {
|
|
FRONT_BACK: {
|
|
front: "forward",
|
|
back: "backward"
|
|
}
|
|
},
|
|
createInputs: {
|
|
NUM: {
|
|
shadowType: "math_integer",
|
|
value: "1"
|
|
}
|
|
}
|
|
}];
|
|
blockSwitches["looks_goforwardbackwardlayers"] = [{
|
|
opcode: "looks_gotofrontback",
|
|
splitInputs: ["NUM"],
|
|
remapInputName: {
|
|
FORWARD_BACKWARD: "FRONT_BACK"
|
|
},
|
|
mapFieldValues: {
|
|
FORWARD_BACKWARD: {
|
|
forward: "front",
|
|
backward: "back"
|
|
}
|
|
}
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("sound")) {
|
|
blockSwitches["sound_play"] = [noopSwitch, {
|
|
opcode: "sound_playuntildone"
|
|
}];
|
|
blockSwitches["sound_playuntildone"] = [{
|
|
opcode: "sound_play"
|
|
}, noopSwitch];
|
|
blockSwitches["sound_seteffectto"] = [noopSwitch, {
|
|
opcode: "sound_changeeffectby"
|
|
}];
|
|
blockSwitches["sound_changeeffectby"] = [{
|
|
opcode: "sound_seteffectto"
|
|
}, noopSwitch];
|
|
blockSwitches["sound_setvolumeto"] = [noopSwitch, {
|
|
opcode: "sound_changevolumeby"
|
|
}];
|
|
blockSwitches["sound_changevolumeby"] = [{
|
|
opcode: "sound_setvolumeto"
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("event")) {
|
|
blockSwitches["event_broadcast"] = [noopSwitch, {
|
|
opcode: "event_broadcastandwait"
|
|
}];
|
|
blockSwitches["event_broadcastandwait"] = [{
|
|
opcode: "event_broadcast"
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("control")) {
|
|
blockSwitches["control_if"] = [noopSwitch, {
|
|
opcode: "control_if_else"
|
|
}];
|
|
blockSwitches["control_if_else"] = [{
|
|
opcode: "control_if",
|
|
splitInputs: ["SUBSTACK2"]
|
|
}, noopSwitch];
|
|
blockSwitches["control_repeat_until"] = [noopSwitch, {
|
|
opcode: "control_wait_until",
|
|
splitInputs: ["SUBSTACK"]
|
|
}, {
|
|
opcode: "control_forever",
|
|
splitInputs: ["CONDITION"]
|
|
}];
|
|
blockSwitches["control_forever"] = [{
|
|
opcode: "control_repeat_until"
|
|
}, noopSwitch];
|
|
blockSwitches["control_wait_until"] = [{
|
|
opcode: "control_repeat_until"
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("operator")) {
|
|
blockSwitches["operator_equals"] = [{
|
|
opcode: "operator_gt"
|
|
}, noopSwitch, {
|
|
opcode: "operator_lt"
|
|
}];
|
|
blockSwitches["operator_gt"] = [noopSwitch, {
|
|
opcode: "operator_equals"
|
|
}, {
|
|
opcode: "operator_lt"
|
|
}];
|
|
blockSwitches["operator_lt"] = [{
|
|
opcode: "operator_gt"
|
|
}, {
|
|
opcode: "operator_equals"
|
|
}, noopSwitch];
|
|
blockSwitches["operator_add"] = [noopSwitch, {
|
|
opcode: "operator_subtract"
|
|
}, {
|
|
opcode: "operator_multiply"
|
|
}, {
|
|
opcode: "operator_divide"
|
|
}, {
|
|
opcode: "operator_mod"
|
|
}];
|
|
blockSwitches["operator_subtract"] = [{
|
|
opcode: "operator_add"
|
|
}, noopSwitch, {
|
|
opcode: "operator_multiply"
|
|
}, {
|
|
opcode: "operator_divide"
|
|
}, {
|
|
opcode: "operator_mod"
|
|
}];
|
|
blockSwitches["operator_multiply"] = [{
|
|
opcode: "operator_add"
|
|
}, {
|
|
opcode: "operator_subtract"
|
|
}, noopSwitch, {
|
|
opcode: "operator_divide"
|
|
}, {
|
|
opcode: "operator_mod"
|
|
}];
|
|
blockSwitches["operator_divide"] = [{
|
|
opcode: "operator_add"
|
|
}, {
|
|
opcode: "operator_subtract"
|
|
}, {
|
|
opcode: "operator_multiply"
|
|
}, noopSwitch, {
|
|
opcode: "operator_mod"
|
|
}];
|
|
blockSwitches["operator_mod"] = [{
|
|
opcode: "operator_add"
|
|
}, {
|
|
opcode: "operator_subtract"
|
|
}, {
|
|
opcode: "operator_multiply"
|
|
}, {
|
|
opcode: "operator_divide"
|
|
}, noopSwitch];
|
|
blockSwitches["operator_and"] = [noopSwitch, {
|
|
opcode: "operator_or"
|
|
}];
|
|
blockSwitches["operator_or"] = [{
|
|
opcode: "operator_and"
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("sensing")) {
|
|
blockSwitches["sensing_mousex"] = [noopSwitch, {
|
|
opcode: "sensing_mousey"
|
|
}];
|
|
blockSwitches["sensing_mousey"] = [{
|
|
opcode: "sensing_mousex"
|
|
}, noopSwitch];
|
|
blockSwitches["sensing_touchingcolor"] = [noopSwitch, {
|
|
opcode: "sensing_coloristouchingcolor",
|
|
createInputs: {
|
|
COLOR2: {
|
|
shadowType: "colour_picker",
|
|
value: randomColor
|
|
}
|
|
}
|
|
}];
|
|
blockSwitches["sensing_coloristouchingcolor"] = [{
|
|
opcode: "sensing_touchingcolor",
|
|
splitInputs: ["COLOR2"]
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("data")) {
|
|
blockSwitches["data_setvariableto"] = [noopSwitch, {
|
|
opcode: "data_changevariableby",
|
|
remapShadowType: {
|
|
VALUE: "math_number"
|
|
}
|
|
}];
|
|
blockSwitches["data_changevariableby"] = [{
|
|
opcode: "data_setvariableto",
|
|
remapShadowType: {
|
|
VALUE: "text"
|
|
}
|
|
}, noopSwitch];
|
|
blockSwitches["data_showvariable"] = [noopSwitch, {
|
|
opcode: "data_hidevariable"
|
|
}];
|
|
blockSwitches["data_hidevariable"] = [{
|
|
opcode: "data_showvariable"
|
|
}, noopSwitch];
|
|
blockSwitches["data_showlist"] = [noopSwitch, {
|
|
opcode: "data_hidelist"
|
|
}];
|
|
blockSwitches["data_hidelist"] = [{
|
|
opcode: "data_showlist"
|
|
}, noopSwitch];
|
|
blockSwitches["data_replaceitemoflist"] = [noopSwitch, {
|
|
opcode: "data_insertatlist"
|
|
}];
|
|
blockSwitches["data_insertatlist"] = [{
|
|
opcode: "data_replaceitemoflist"
|
|
}, noopSwitch];
|
|
blockSwitches["data_deleteoflist"] = [noopSwitch, {
|
|
opcode: "data_deletealloflist",
|
|
splitInputs: ["INDEX"]
|
|
}];
|
|
blockSwitches["data_deletealloflist"] = [{
|
|
opcode: "data_deleteoflist",
|
|
createInputs: {
|
|
INDEX: {
|
|
shadowType: "math_integer",
|
|
value: "1"
|
|
}
|
|
}
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("extension")) {
|
|
blockSwitches["pen_penDown"] = [noopSwitch, {
|
|
opcode: "pen_penUp"
|
|
}];
|
|
blockSwitches["pen_penUp"] = [{
|
|
opcode: "pen_penDown"
|
|
}, noopSwitch];
|
|
blockSwitches["pen_setPenColorParamTo"] = [noopSwitch, {
|
|
opcode: "pen_changePenColorParamBy"
|
|
}];
|
|
blockSwitches["pen_changePenColorParamBy"] = [{
|
|
opcode: "pen_setPenColorParamTo"
|
|
}, noopSwitch];
|
|
blockSwitches["pen_setPenHueToNumber"] = [noopSwitch, {
|
|
opcode: "pen_changePenHueBy"
|
|
}];
|
|
blockSwitches["pen_changePenHueBy"] = [{
|
|
opcode: "pen_setPenHueToNumber"
|
|
}, noopSwitch];
|
|
blockSwitches["pen_setPenShadeToNumber"] = [noopSwitch, {
|
|
opcode: "pen_changePenShadeBy"
|
|
}];
|
|
blockSwitches["pen_changePenShadeBy"] = [{
|
|
opcode: "pen_setPenShadeToNumber"
|
|
}, noopSwitch];
|
|
blockSwitches["pen_setPenSizeTo"] = [noopSwitch, {
|
|
opcode: "pen_changePenSizeBy"
|
|
}];
|
|
blockSwitches["pen_changePenSizeBy"] = [{
|
|
opcode: "pen_setPenSizeTo"
|
|
}, noopSwitch];
|
|
blockSwitches["music_setTempo"] = [noopSwitch, {
|
|
opcode: "music_changeTempo"
|
|
}];
|
|
blockSwitches["music_changeTempo"] = [{
|
|
opcode: "music_setTempo"
|
|
}, noopSwitch];
|
|
}
|
|
|
|
if (addon.settings.get("sa")) {
|
|
const logProc = "\u200B\u200Blog\u200B\u200B %s";
|
|
const warnProc = "\u200B\u200Bwarn\u200B\u200B %s";
|
|
const errorProc = "\u200B\u200Berror\u200B\u200B %s";
|
|
const logMessage = msg("debugger_log");
|
|
const warnMessage = msg("debugger_warn");
|
|
const errorMessage = msg("debugger_error");
|
|
const logSwitch = {
|
|
mutate: {
|
|
proccode: logProc
|
|
},
|
|
msg: logMessage
|
|
};
|
|
const warnSwitch = {
|
|
mutate: {
|
|
proccode: warnProc
|
|
},
|
|
msg: warnMessage
|
|
};
|
|
const errorSwitch = {
|
|
mutate: {
|
|
proccode: errorProc
|
|
},
|
|
msg: errorMessage
|
|
};
|
|
procedureSwitches[logProc] = [{
|
|
msg: logMessage,
|
|
isNoop: true
|
|
}, warnSwitch, errorSwitch];
|
|
procedureSwitches[warnProc] = [logSwitch, {
|
|
msg: warnMessage,
|
|
isNoop: true
|
|
}, errorSwitch];
|
|
procedureSwitches[errorProc] = [logSwitch, warnSwitch, {
|
|
msg: errorMessage,
|
|
isNoop: true
|
|
}];
|
|
} // Switching for these is implemented by Scratch. We only define them here to optionally add a border.
|
|
// Because we don't implement the switching ourselves, this is not controlled by the data category option.
|
|
|
|
|
|
blockSwitches["data_variable"] = [];
|
|
blockSwitches["data_listcontents"] = [];
|
|
};
|
|
|
|
buildSwitches();
|
|
addon.settings.addEventListener("change", buildSwitches);
|
|
/**
|
|
* @param {*} workspace
|
|
* @param {Element} xmlBlock
|
|
*/
|
|
|
|
const pasteBlockXML = (workspace, xmlBlock) => {
|
|
// Similar to https://github.com/LLK/scratch-blocks/blob/7575c9a0f2c267676569c4b102b76d77f35d9fd6/core/workspace_svg.js#L1020
|
|
// but without the collision checking.
|
|
const block = ScratchBlocks.Xml.domToBlock(xmlBlock, workspace);
|
|
const x = +xmlBlock.getAttribute("x");
|
|
const y = +xmlBlock.getAttribute("y"); // Don't need to handle RTL here
|
|
|
|
block.moveBy(x, y);
|
|
return block;
|
|
};
|
|
/**
|
|
* @param {string} shadowType The type of shadow eg. "math_number"
|
|
* @returns {string} The name of the shadow's inner field that contains the user-visible value
|
|
*/
|
|
|
|
|
|
const getShadowFieldName = shadowType => {
|
|
// This is non-comprehensive.
|
|
if (shadowType === "text") {
|
|
return "TEXT";
|
|
}
|
|
|
|
if (shadowType === "colour_picker") {
|
|
return "COLOUR";
|
|
}
|
|
|
|
return "NUM";
|
|
};
|
|
/**
|
|
* @template T
|
|
* @param {T|()=>T} value
|
|
* @returns {T}
|
|
*/
|
|
|
|
|
|
const callIfFunction = value => {
|
|
if (typeof value === "function") {
|
|
return value();
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
const menuCallbackFactory = (block, opcodeData) => () => {
|
|
if (opcodeData.isNoop) {
|
|
return;
|
|
}
|
|
|
|
if (opcodeData.fieldValue) {
|
|
block.setFieldValue(opcodeData.fieldValue, "VALUE");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
ScratchBlocks.Events.setGroup(true);
|
|
const workspace = block.workspace;
|
|
const blocksToBringToForeground = []; // Split inputs before we clone the block.
|
|
|
|
if (opcodeData.splitInputs) {
|
|
for (const inputName of opcodeData.splitInputs) {
|
|
const input = block.getInput(inputName);
|
|
|
|
if (!input) {
|
|
continue;
|
|
}
|
|
|
|
const connection = input.connection;
|
|
|
|
if (!connection) {
|
|
continue;
|
|
}
|
|
|
|
if (connection.isConnected()) {
|
|
const targetBlock = connection.targetBlock();
|
|
|
|
if (targetBlock.isShadow()) {// Deleting shadows is handled later.
|
|
} else {
|
|
connection.disconnect();
|
|
blocksToBringToForeground.push(targetBlock);
|
|
}
|
|
}
|
|
}
|
|
} // Make a copy of the block with the proper type set.
|
|
// It doesn't seem to be possible to change a Block's type after it's created, so we'll just make a new block instead.
|
|
|
|
|
|
const xml = ScratchBlocks.Xml.blockToDom(block); // blockToDomWithXY's handling of RTL is strange, so we encode the position ourselves.
|
|
|
|
const position = block.getRelativeToSurfaceXY();
|
|
xml.setAttribute("x", position.x);
|
|
xml.setAttribute("y", position.y);
|
|
|
|
if (opcodeData.opcode) {
|
|
xml.setAttribute("type", opcodeData.opcode);
|
|
}
|
|
|
|
const parentBlock = block.getParent();
|
|
let parentConnection;
|
|
let blockConnectionType;
|
|
|
|
if (parentBlock) {
|
|
// If the block has a parent, find the parent -> child connection that will be reattached later.
|
|
const parentConnections = parentBlock.getConnections_();
|
|
parentConnection = parentConnections.find(c => c.targetConnection && c.targetConnection.sourceBlock_ === block); // There's two types of connections from child -> parent. We need to figure out which one is used.
|
|
|
|
const blockConnections = block.getConnections_();
|
|
const blockToParentConnection = blockConnections.find(c => c.targetConnection && c.targetConnection.sourceBlock_ === parentBlock);
|
|
blockConnectionType = blockToParentConnection.type;
|
|
} // Array.from creates a clone of the children list. This is important as we may remove
|
|
// children as we iterate.
|
|
|
|
|
|
for (const child of Array.from(xml.children)) {
|
|
const oldName = child.getAttribute("name"); // Any inputs that were supposed to be split that were not should be removed.
|
|
// (eg. shadow inputs)
|
|
|
|
if (opcodeData.splitInputs && opcodeData.splitInputs.includes(oldName)) {
|
|
xml.removeChild(child);
|
|
continue;
|
|
}
|
|
|
|
const newName = opcodeData.remapInputName && opcodeData.remapInputName[oldName];
|
|
|
|
if (newName) {
|
|
child.setAttribute("name", newName);
|
|
}
|
|
|
|
const newShadowType = opcodeData.remapShadowType && opcodeData.remapShadowType[oldName];
|
|
|
|
if (newShadowType) {
|
|
const valueNode = child.firstChild;
|
|
const fieldNode = valueNode.firstChild;
|
|
valueNode.setAttribute("type", newShadowType);
|
|
fieldNode.setAttribute("name", getShadowFieldName(newShadowType));
|
|
}
|
|
|
|
const fieldValueMap = opcodeData.mapFieldValues && opcodeData.mapFieldValues[oldName];
|
|
|
|
if (fieldValueMap && child.tagName === "FIELD") {
|
|
const oldValue = child.innerText;
|
|
const newValue = fieldValueMap[oldValue];
|
|
|
|
if (typeof newValue === "string") {
|
|
child.innerText = newValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (opcodeData.mutate) {
|
|
const mutation = xml.querySelector("mutation");
|
|
|
|
for (const [key, value] of Object.entries(opcodeData.mutate)) {
|
|
mutation.setAttribute(key, value);
|
|
}
|
|
}
|
|
|
|
if (opcodeData.createInputs) {
|
|
for (const [inputName, inputData] of Object.entries(opcodeData.createInputs)) {
|
|
const valueElement = document.createElement("value");
|
|
valueElement.setAttribute("name", inputName);
|
|
const shadowElement = document.createElement("shadow");
|
|
shadowElement.setAttribute("type", inputData.shadowType);
|
|
const shadowFieldElement = document.createElement("field");
|
|
shadowFieldElement.setAttribute("name", getShadowFieldName(inputData.shadowType));
|
|
shadowFieldElement.innerText = callIfFunction(inputData.value);
|
|
shadowElement.appendChild(shadowFieldElement);
|
|
valueElement.appendChild(shadowElement);
|
|
xml.appendChild(valueElement);
|
|
}
|
|
} // Remove the old block and insert the new one.
|
|
|
|
|
|
block.dispose();
|
|
const newBlock = pasteBlockXML(workspace, xml);
|
|
|
|
if (parentConnection) {
|
|
// Search for the same type of connection on the new block as on the old block.
|
|
const newBlockConnections = newBlock.getConnections_();
|
|
const newBlockConnection = newBlockConnections.find(c => c.type === blockConnectionType);
|
|
newBlockConnection.connect(parentConnection);
|
|
}
|
|
|
|
for (const otherBlock of blocksToBringToForeground) {
|
|
// By re-appending the element, we move it to the end, which will make it display
|
|
// on top.
|
|
const svgRoot = otherBlock.getSvgRoot();
|
|
svgRoot.parentNode.appendChild(svgRoot);
|
|
}
|
|
} finally {
|
|
ScratchBlocks.Events.setGroup(false);
|
|
}
|
|
};
|
|
|
|
const uniques = array => [...new Set(array)];
|
|
|
|
addon.tab.createBlockContextMenu((items, block) => {
|
|
if (!addon.self.disabled) {
|
|
const type = block.type;
|
|
let switches = blockSwitches[block.type] || [];
|
|
const customArgsMode = addon.settings.get("customargs") ? addon.settings.get("customargsmode") : "off";
|
|
|
|
if (customArgsMode !== "off" && ["argument_reporter_boolean", "argument_reporter_string_number"].includes(type) && // if the arg is a shadow, it's in a procedures_prototype so we don't want it to be switchable
|
|
!block.isShadow()) {
|
|
const customBlocks = getCustomBlocks();
|
|
|
|
if (customArgsMode === "all") {
|
|
switch (type) {
|
|
case "argument_reporter_string_number":
|
|
switches = Object.values(customBlocks).map(cb => cb.stringArgs).flat(1);
|
|
break;
|
|
|
|
case "argument_reporter_boolean":
|
|
switches = Object.values(customBlocks).map(cb => cb.boolArgs).flat(1);
|
|
break;
|
|
}
|
|
} else if (customArgsMode === "defOnly") {
|
|
const root = block.getRootBlock();
|
|
if (root.type !== "procedures_definition") return items;
|
|
const customBlockObj = customBlocks[root.getChildren(true)[0].getProcCode()];
|
|
|
|
switch (type) {
|
|
case "argument_reporter_string_number":
|
|
switches = customBlockObj.stringArgs;
|
|
break;
|
|
|
|
case "argument_reporter_boolean":
|
|
switches = customBlockObj.boolArgs;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const currentValue = block.getFieldValue("VALUE");
|
|
switches = uniques(switches).map(i => ({
|
|
isNoop: i === currentValue,
|
|
fieldValue: i,
|
|
msg: i
|
|
}));
|
|
}
|
|
|
|
if (block.type === "procedures_call") {
|
|
const proccode = block.getProcCode();
|
|
|
|
if (procedureSwitches[proccode]) {
|
|
switches = procedureSwitches[proccode];
|
|
}
|
|
}
|
|
|
|
if (!addon.settings.get("noop")) {
|
|
switches = switches.filter(i => !i.isNoop);
|
|
}
|
|
|
|
switches.forEach((opcodeData, i) => {
|
|
const makeSpaceItemIndex = items.findIndex(obj => obj._isDevtoolsFirstItem);
|
|
const insertBeforeIndex = makeSpaceItemIndex !== -1 ? // If "make space" button exists, add own items before it
|
|
makeSpaceItemIndex : // If there's no such button, insert at end
|
|
items.length;
|
|
const text = opcodeData.msg ? opcodeData.msg : opcodeData.opcode ? msg(opcodeData.opcode) : msg(block.type);
|
|
items.splice(insertBeforeIndex, 0, {
|
|
enabled: true,
|
|
text,
|
|
callback: menuCallbackFactory(block, opcodeData),
|
|
separator: i === 0
|
|
});
|
|
});
|
|
|
|
if (block.type === "data_variable" || block.type === "data_listcontents") {
|
|
// Add top border to first variable (if it exists)
|
|
const delBlockIndex = items.findIndex(item => item.text === ScratchBlocks.Msg.DELETE_BLOCK); // firstVariableItem might be undefined, a variable to switch to,
|
|
// or an item added by editor-devtools (or any addon before this one)
|
|
|
|
const firstVariableItem = items[delBlockIndex + 1];
|
|
if (firstVariableItem) firstVariableItem.separator = true;
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}, {
|
|
blocks: true
|
|
}); // https://github.com/LLK/scratch-blocks/blob/abbfe93136fef57fdfb9a077198b0bc64726f012/blocks_vertical/procedures.js#L207-L215
|
|
// Returns a list like ["%s", "%d"]
|
|
|
|
const parseArguments = code => code.split(/(?=[^\\]%[nbs])/g).map(i => i.trim()).filter(i => i.charAt(0) === "%").map(i => i.substring(0, 2));
|
|
|
|
const getCustomBlocks = () => {
|
|
const customBlocks = {};
|
|
const target = vm.editingTarget;
|
|
Object.values(target.blocks._blocks).filter(block => block.opcode === "procedures_prototype").forEach(block => {
|
|
const procCode = block.mutation.proccode;
|
|
const argumentNames = JSON.parse(block.mutation.argumentnames); // argumentdefaults is unreliable, so we have to parse the procedure code to determine argument types
|
|
|
|
const parsedArguments = parseArguments(procCode);
|
|
const stringArgs = [];
|
|
const boolArgs = [];
|
|
|
|
for (let i = 0; i < argumentNames.length; i++) {
|
|
if (parsedArguments[i] === "%b") {
|
|
boolArgs.push(argumentNames[i]);
|
|
} else {
|
|
stringArgs.push(argumentNames[i]);
|
|
}
|
|
}
|
|
|
|
customBlocks[procCode] = {
|
|
stringArgs,
|
|
boolArgs
|
|
};
|
|
});
|
|
return customBlocks;
|
|
};
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/color-picker/_runtime_entry.js":
|
|
/*!**********************************************************!*\
|
|
!*** ./src/addons/addons/color-picker/_runtime_entry.js ***!
|
|
\**********************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/color-picker/userscript.js");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./style.css */ "./node_modules/css-loader/index.js!./src/addons/addons/color-picker/style.css");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* generated by pull.js */
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"style.css": _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default.a
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/color-picker/code-editor.js":
|
|
/*!*******************************************************!*\
|
|
!*** ./src/addons/addons/color-picker/code-editor.js ***!
|
|
\*******************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony import */ var _libraries_common_cs_normalize_color_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../libraries/common/cs/normalize-color.js */ "./src/addons/libraries/common/cs/normalize-color.js");
|
|
/* harmony import */ var _libraries_common_cs_rate_limiter_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../libraries/common/cs/rate-limiter.js */ "./src/addons/libraries/common/cs/rate-limiter.js");
|
|
/* harmony import */ var _libraries_thirdparty_cs_tinycolor_min_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../libraries/thirdparty/cs/tinycolor-min.js */ "./src/addons/libraries/thirdparty/cs/tinycolor-min.js");
|
|
|
|
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async ({
|
|
addon,
|
|
console,
|
|
msg
|
|
}) => {
|
|
// 250-ms rate limit
|
|
const rateLimiter = new _libraries_common_cs_rate_limiter_js__WEBPACK_IMPORTED_MODULE_1__["default"](250);
|
|
|
|
const getColor = element => {
|
|
const {
|
|
children
|
|
} = element.parentElement; // h: 0 - 360
|
|
|
|
const h = children[1].getAttribute("aria-valuenow"); // s: 0 - 1
|
|
|
|
const s = children[3].getAttribute("aria-valuenow"); // v: 0 - 255, divide by 255
|
|
|
|
const vMultipliedBy255 = children[5].getAttribute("aria-valuenow");
|
|
const v = Number(vMultipliedBy255) / 255;
|
|
return Object(_libraries_thirdparty_cs_tinycolor_min_js__WEBPACK_IMPORTED_MODULE_2__["default"])("hsv(".concat(h, ", ").concat(s, ", ").concat(v || 0, ")")).toHexString();
|
|
};
|
|
|
|
const setColor = (hex, element) => {
|
|
hex = Object(_libraries_common_cs_normalize_color_js__WEBPACK_IMPORTED_MODULE_0__["normalizeHex"])(hex);
|
|
if (!addon.tab.redux.state || !addon.tab.redux.state.scratchGui) return; // The only way to reliably set color is to invoke eye dropper via click()
|
|
// then faking that the eye dropper reported the value.
|
|
|
|
const onEyeDropperClosed = ({
|
|
detail
|
|
}) => {
|
|
if (detail.action.type !== "scratch-gui/color-picker/DEACTIVATE_COLOR_PICKER") return;
|
|
addon.tab.redux.removeEventListener("statechanged", onEyeDropperClosed);
|
|
setTimeout(() => {
|
|
document.body.classList.remove("sa-hide-eye-dropper-background");
|
|
}, 50);
|
|
};
|
|
|
|
const onEyeDropperOpened = ({
|
|
detail
|
|
}) => {
|
|
if (detail.action.type !== "scratch-gui/color-picker/ACTIVATE_COLOR_PICKER") return;
|
|
addon.tab.redux.removeEventListener("statechanged", onEyeDropperOpened);
|
|
addon.tab.redux.addEventListener("statechanged", onEyeDropperClosed);
|
|
setTimeout(() => {
|
|
addon.tab.redux.dispatch({
|
|
type: "scratch-gui/color-picker/DEACTIVATE_COLOR_PICKER",
|
|
color: hex
|
|
});
|
|
}, 50);
|
|
};
|
|
|
|
addon.tab.redux.addEventListener("statechanged", onEyeDropperOpened);
|
|
document.body.classList.add("sa-hide-eye-dropper-background");
|
|
element.click();
|
|
};
|
|
|
|
const addColorPicker = () => {
|
|
const element = document.querySelector("button.scratchEyedropper");
|
|
rateLimiter.abort(false);
|
|
addon.tab.redux.initialize();
|
|
const defaultColor = getColor(element);
|
|
const saColorPicker = Object.assign(document.createElement("div"), {
|
|
className: "sa-color-picker sa-color-picker-code"
|
|
});
|
|
addon.tab.displayNoneWhileDisabled(saColorPicker, {
|
|
display: "flex"
|
|
});
|
|
const saColorPickerColor = Object.assign(document.createElement("input"), {
|
|
className: "sa-color-picker-color sa-color-picker-code-color",
|
|
type: "color",
|
|
value: defaultColor || "#000000"
|
|
});
|
|
const saColorPickerText = Object.assign(document.createElement("input"), {
|
|
className: addon.tab.scratchClass("input_input-form", {
|
|
others: "sa-color-picker-text sa-color-picker-code-text"
|
|
}),
|
|
type: "text",
|
|
pattern: "^#?([0-9a-fA-F]{3}){1,2}$",
|
|
placeholder: msg("hex"),
|
|
value: defaultColor || ""
|
|
});
|
|
saColorPickerColor.addEventListener("input", () => rateLimiter.limit(() => setColor(saColorPickerText.value = saColorPickerColor.value, element)));
|
|
saColorPickerText.addEventListener("change", () => {
|
|
const {
|
|
value
|
|
} = saColorPickerText;
|
|
if (!Object(_libraries_common_cs_normalize_color_js__WEBPACK_IMPORTED_MODULE_0__["getHexRegex"])().test(value)) return;
|
|
setColor(saColorPickerColor.value = Object(_libraries_common_cs_normalize_color_js__WEBPACK_IMPORTED_MODULE_0__["normalizeHex"])(value), element);
|
|
});
|
|
saColorPicker.appendChild(saColorPickerColor);
|
|
saColorPicker.appendChild(saColorPickerText);
|
|
element.parentElement.insertBefore(saColorPicker, element);
|
|
};
|
|
|
|
const ScratchBlocks = await addon.tab.traps.getBlockly();
|
|
const originalShowEditor = ScratchBlocks.FieldColourSlider.prototype.showEditor_;
|
|
|
|
ScratchBlocks.FieldColourSlider.prototype.showEditor_ = function (...args) {
|
|
const r = originalShowEditor.call(this, ...args);
|
|
addColorPicker();
|
|
return r;
|
|
};
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/color-picker/userscript.js":
|
|
/*!******************************************************!*\
|
|
!*** ./src/addons/addons/color-picker/userscript.js ***!
|
|
\******************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony import */ var _code_editor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./code-editor.js */ "./src/addons/addons/color-picker/code-editor.js");
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async api => {
|
|
Object(_code_editor_js__WEBPACK_IMPORTED_MODULE_0__["default"])(api);
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-comment-previews/_runtime_entry.js":
|
|
/*!*********************************************************************!*\
|
|
!*** ./src/addons/addons/editor-comment-previews/_runtime_entry.js ***!
|
|
\*********************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/editor-comment-previews/userscript.js");
|
|
/* harmony import */ var _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./userstyle.css */ "./node_modules/css-loader/index.js!./src/addons/addons/editor-comment-previews/userstyle.css");
|
|
/* harmony import */ var _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* generated by pull.js */
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"userstyle.css": _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1___default.a
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-comment-previews/userscript.js":
|
|
/*!*****************************************************************!*\
|
|
!*** ./src/addons/addons/editor-comment-previews/userscript.js ***!
|
|
\*****************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console
|
|
}) {
|
|
const vm = addon.tab.traps.vm;
|
|
|
|
const updateStyles = () => {
|
|
previewInner.classList.toggle("sa-comment-preview-delay", addon.settings.get("delay") !== "none");
|
|
previewInner.classList.toggle("sa-comment-preview-reduce-transparency", addon.settings.get("reduce-transparency"));
|
|
previewInner.classList.toggle("sa-comment-preview-fade", !addon.settings.get("reduce-animation"));
|
|
};
|
|
|
|
const afterDelay = cb => {
|
|
if (!previewInner.classList.contains("sa-comment-preview-hidden")) {
|
|
// If not hidden, updating immediately is preferred
|
|
cb();
|
|
return;
|
|
}
|
|
|
|
const delay = addon.settings.get("delay");
|
|
if (delay === "long") return setTimeout(cb, 500);
|
|
if (delay === "short") return setTimeout(cb, 300);
|
|
cb();
|
|
};
|
|
|
|
let hoveredElement = null;
|
|
let showTimeout = null;
|
|
let mouseX = 0;
|
|
let mouseY = 0;
|
|
let doNotShowUntilMoveMouse = false;
|
|
const previewOuter = document.createElement("div");
|
|
previewOuter.classList.add("sa-comment-preview-outer");
|
|
const previewInner = document.createElement("div");
|
|
previewInner.classList.add("sa-comment-preview-inner");
|
|
previewInner.classList.add("sa-comment-preview-hidden");
|
|
updateStyles();
|
|
addon.settings.addEventListener("change", updateStyles);
|
|
previewOuter.appendChild(previewInner);
|
|
document.body.appendChild(previewOuter);
|
|
|
|
const getBlock = id => vm.editingTarget.blocks.getBlock(id) || vm.runtime.flyoutBlocks.getBlock(id);
|
|
|
|
const getComment = block => block && block.comment && vm.editingTarget.comments[block.comment];
|
|
|
|
const getProcedureDefinitionBlock = procCode => {
|
|
const procedurePrototype = Object.values(vm.editingTarget.blocks._blocks).find(i => i.opcode === "procedures_prototype" && i.mutation.proccode === procCode);
|
|
|
|
if (procedurePrototype) {
|
|
// Usually `parent` will exist but sometimes it doesn't
|
|
if (procedurePrototype.parent) {
|
|
return getBlock(procedurePrototype.parent);
|
|
}
|
|
|
|
const id = procedurePrototype.id;
|
|
return Object.values(vm.editingTarget.blocks._blocks).find(i => i.opcode === "procedures_definition" && i.inputs.custom_block && i.inputs.custom_block.block === id);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const setText = text => {
|
|
previewInner.innerText = text;
|
|
previewInner.classList.remove("sa-comment-preview-hidden");
|
|
updateMousePosition();
|
|
};
|
|
|
|
const updateMousePosition = () => {
|
|
previewOuter.style.transform = "translate(".concat(mouseX + 8, "px, ").concat(mouseY + 8, "px)");
|
|
};
|
|
|
|
const hidePreview = () => {
|
|
if (hoveredElement) {
|
|
hoveredElement = null;
|
|
previewInner.classList.add("sa-comment-preview-hidden");
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mouseover", e => {
|
|
if (addon.self.disabled) {
|
|
return;
|
|
}
|
|
|
|
clearTimeout(showTimeout);
|
|
|
|
if (doNotShowUntilMoveMouse) {
|
|
return;
|
|
}
|
|
|
|
const el = e.target.closest(".blocklyBubbleCanvas > g, .blocklyBlockCanvas .blocklyDraggable[data-id]");
|
|
|
|
if (el === hoveredElement) {
|
|
// Nothing to do.
|
|
return;
|
|
}
|
|
|
|
if (!el) {
|
|
hidePreview();
|
|
return;
|
|
}
|
|
|
|
let text = null;
|
|
|
|
if (addon.settings.get("hover-view") && e.target.closest(".blocklyBubbleCanvas > g") && // Hovering over the thin line that connects comments to blocks should never show a preview
|
|
!e.target.closest("line")) {
|
|
const collapsedText = el.querySelector("text.scratchCommentText");
|
|
|
|
if (collapsedText.getAttribute("display") !== "none") {
|
|
const textarea = el.querySelector("textarea");
|
|
text = textarea.value;
|
|
}
|
|
} else if (e.target.closest(".blocklyBlockCanvas .blocklyDraggable[data-id]")) {
|
|
const id = el.dataset.id;
|
|
const block = getBlock(id);
|
|
const comment = getComment(block);
|
|
|
|
if (addon.settings.get("hover-view-block") && comment) {
|
|
text = comment.text;
|
|
} else if (block && block.opcode === "procedures_call" && addon.settings.get("hover-view-procedure")) {
|
|
const procCode = block.mutation.proccode;
|
|
const procedureDefinitionBlock = getProcedureDefinitionBlock(procCode);
|
|
const procedureComment = getComment(procedureDefinitionBlock);
|
|
|
|
if (procedureComment) {
|
|
text = procedureComment.text;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (text !== null && text.trim() !== "") {
|
|
showTimeout = afterDelay(() => {
|
|
hoveredElement = el;
|
|
setText(text);
|
|
});
|
|
} else {
|
|
hidePreview();
|
|
}
|
|
});
|
|
document.addEventListener("mousemove", e => {
|
|
mouseX = e.clientX;
|
|
mouseY = e.clientY;
|
|
doNotShowUntilMoveMouse = false;
|
|
|
|
if (addon.settings.get("follow-mouse") && !previewInner.classList.contains("sa-comment-preview-hidden")) {
|
|
updateMousePosition();
|
|
}
|
|
});
|
|
document.addEventListener("mousedown", () => {
|
|
hidePreview();
|
|
doNotShowUntilMoveMouse = true;
|
|
}, {
|
|
capture: true
|
|
});
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-devtools/DevTools.js":
|
|
/*!*******************************************************!*\
|
|
!*** ./src/addons/addons/editor-devtools/DevTools.js ***!
|
|
\*******************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return DevTools; });
|
|
/* harmony import */ var _DomHelpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./DomHelpers.js */ "./src/addons/addons/editor-devtools/DomHelpers.js");
|
|
/* harmony import */ var _UndoGroup_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./UndoGroup.js */ "./src/addons/addons/editor-devtools/UndoGroup.js");
|
|
// import ShowBroadcast from "./show-broadcast.js";
|
|
|
|
|
|
class DevTools {
|
|
constructor(addon, msg, m) {
|
|
this.addon = addon;
|
|
this.msg = msg;
|
|
this.m = m;
|
|
/**
|
|
* @type {VirtualMachine}
|
|
*/
|
|
|
|
this.domHelpers = new _DomHelpers_js__WEBPACK_IMPORTED_MODULE_0__["default"](addon);
|
|
this.codeTab = null;
|
|
this.costTab = null;
|
|
this.costTabBody = null;
|
|
this.selVarID = null;
|
|
this.canShare = false;
|
|
this.mouseXY = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
}
|
|
|
|
async init() {
|
|
this.addContextMenus();
|
|
|
|
while (true) {
|
|
const root = await this.addon.tab.waitForElement("ul[class*=gui_tab-list_]", {
|
|
markAsSeen: true,
|
|
reduxEvents: ["scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED", "scratch-gui/locales/SELECT_LOCALE"],
|
|
reduxCondition: state => !state.scratchGui.mode.isPlayerOnly
|
|
});
|
|
this.initInner(root);
|
|
}
|
|
}
|
|
|
|
async addContextMenus() {
|
|
const blockly = await this.addon.tab.traps.getBlockly();
|
|
const oldCleanUpFunc = blockly.WorkspaceSvg.prototype.cleanUp;
|
|
const self = this;
|
|
|
|
blockly.WorkspaceSvg.prototype.cleanUp = function () {
|
|
if (self.addon.settings.get("enableCleanUpPlus")) {
|
|
self.doCleanUp();
|
|
} else {
|
|
oldCleanUpFunc.call(this);
|
|
}
|
|
};
|
|
|
|
let originalMsg = blockly.Msg.CLEAN_UP;
|
|
if (this.addon.settings.get("enableCleanUpPlus")) blockly.Msg.CLEAN_UP = this.m("clean-plus");
|
|
this.addon.settings.addEventListener("change", () => {
|
|
if (this.addon.settings.get("enableCleanUpPlus")) blockly.Msg.CLEAN_UP = this.m("clean-plus");else blockly.Msg.CLEAN_UP = originalMsg;
|
|
});
|
|
this.addon.tab.createBlockContextMenu((items, block) => {
|
|
items.push({
|
|
enabled: blockly.clipboardXml_,
|
|
text: this.m("paste"),
|
|
separator: true,
|
|
_isDevtoolsFirstItem: true,
|
|
callback: () => {
|
|
let ids = this.getTopBlockIDs();
|
|
document.dispatchEvent(new KeyboardEvent("keydown", {
|
|
keyCode: 86,
|
|
ctrlKey: true,
|
|
griff: true
|
|
}));
|
|
setTimeout(() => {
|
|
this.beginDragOfNewBlocksNotInIDs(ids);
|
|
}, 10);
|
|
}
|
|
});
|
|
return items;
|
|
}, {
|
|
workspace: true
|
|
});
|
|
this.addon.tab.createBlockContextMenu((items, block) => {
|
|
items.push({
|
|
enabled: true,
|
|
text: this.m("make-space"),
|
|
_isDevtoolsFirstItem: true,
|
|
callback: () => {
|
|
this.doCleanUp(block);
|
|
},
|
|
separator: true
|
|
}, {
|
|
enabled: true,
|
|
text: this.m("copy-all"),
|
|
callback: () => {
|
|
this.eventCopyClick(block);
|
|
},
|
|
separator: true
|
|
}, {
|
|
enabled: true,
|
|
text: this.m("copy-block"),
|
|
callback: () => {
|
|
this.eventCopyClick(block, 1);
|
|
}
|
|
}, {
|
|
enabled: true,
|
|
text: this.m("cut-block"),
|
|
callback: () => {
|
|
this.eventCopyClick(block, 2);
|
|
}
|
|
}); // const BROADCAST_BLOCKS = ["event_whenbroadcastreceived", "event_broadcast", "event_broadcastandwait"];
|
|
// if (BROADCAST_BLOCKS.includes(block.type)) {
|
|
// // Show Broadcast
|
|
// const broadcastId = this.showBroadcastSingleton.getAssociatedBroadcastId(block.id);
|
|
// if (broadcastId) {
|
|
// ["Senders", "Receivers"].forEach((showKey, i) => {
|
|
// items.push({
|
|
// enabled: true,
|
|
// text: this.msg(`show-${showKey}`.toLowerCase()),
|
|
// callback: () => {
|
|
// this.showBroadcastSingleton[`show${showKey}`](broadcastId);
|
|
// },
|
|
// separator: i == 0,
|
|
// });
|
|
// });
|
|
// }
|
|
// }
|
|
|
|
return items;
|
|
}, {
|
|
blocks: true
|
|
});
|
|
this.addon.tab.createBlockContextMenu((items, block) => {
|
|
if (block.getCategory() === "data" || block.getCategory() === "data-lists") {
|
|
this.selVarID = block.getVars()[0];
|
|
items.push({
|
|
enabled: true,
|
|
text: this.m("swap", {
|
|
var: block.getCategory() === "data" ? this.m("variables") : this.m("lists")
|
|
}),
|
|
callback: async () => {
|
|
let wksp = this.getWorkspace();
|
|
let v = wksp.getVariableById(this.selVarID); // prompt() returns Promise in desktop app
|
|
|
|
let varName = await window.prompt(this.msg("replace", {
|
|
name: v.name
|
|
}));
|
|
|
|
if (varName) {
|
|
this.doReplaceVariable(this.selVarID, varName, v.type);
|
|
}
|
|
},
|
|
separator: true
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}, {
|
|
blocks: true,
|
|
flyout: true
|
|
});
|
|
}
|
|
|
|
getWorkspace() {
|
|
return Blockly.getMainWorkspace();
|
|
}
|
|
|
|
isCostumeEditor() {
|
|
return this.costTab.className.indexOf("gui_is-selected") >= 0;
|
|
}
|
|
/**
|
|
* A nicely ordered version of the top blocks
|
|
* @returns {[Blockly.Block]}
|
|
*/
|
|
|
|
|
|
getTopBlocks() {
|
|
let result = this.getOrderedTopBlockColumns();
|
|
let columns = result.cols;
|
|
/**
|
|
* @type {[[Blockly.Block]]}
|
|
*/
|
|
|
|
let topBlocks = [];
|
|
|
|
for (const col of columns) {
|
|
topBlocks = topBlocks.concat(col.blocks);
|
|
}
|
|
|
|
return topBlocks;
|
|
}
|
|
/**
|
|
* A much nicer way of laying out the blocks into columns
|
|
*/
|
|
|
|
|
|
doCleanUp(block) {
|
|
let workspace = this.getWorkspace();
|
|
let makeSpaceForBlock = block && block.getRootBlock();
|
|
_UndoGroup_js__WEBPACK_IMPORTED_MODULE_1__["default"].startUndoGroup(workspace);
|
|
let result = this.getOrderedTopBlockColumns(true);
|
|
let columns = result.cols;
|
|
let orphanCount = result.orphans.blocks.length;
|
|
|
|
if (orphanCount > 0 && !block) {
|
|
let message = this.msg("orphaned", {
|
|
count: orphanCount
|
|
});
|
|
|
|
if (confirm(message)) {
|
|
for (const block of result.orphans.blocks) {
|
|
block.dispose();
|
|
}
|
|
} else {
|
|
columns.unshift(result.orphans);
|
|
}
|
|
}
|
|
|
|
let cursorX = 48;
|
|
let maxWidths = result.maxWidths;
|
|
|
|
for (const column of columns) {
|
|
let cursorY = 64;
|
|
let maxWidth = 0;
|
|
|
|
for (const block of column.blocks) {
|
|
let extraWidth = block === makeSpaceForBlock ? 380 : 0;
|
|
let extraHeight = block === makeSpaceForBlock ? 480 : 72;
|
|
let xy = block.getRelativeToSurfaceXY();
|
|
|
|
if (cursorX - xy.x !== 0 || cursorY - xy.y !== 0) {
|
|
block.moveBy(cursorX - xy.x, cursorY - xy.y);
|
|
}
|
|
|
|
let heightWidth = block.getHeightWidth();
|
|
cursorY += heightWidth.height + extraHeight;
|
|
let maxWidthWithComments = maxWidths[block.id] || 0;
|
|
maxWidth = Math.max(maxWidth, Math.max(heightWidth.width + extraWidth, maxWidthWithComments));
|
|
}
|
|
|
|
cursorX += maxWidth + 96;
|
|
}
|
|
|
|
let topComments = workspace.getTopComments();
|
|
|
|
for (const comment of topComments) {
|
|
if (comment.setVisible) {
|
|
comment.setVisible(false);
|
|
comment.needsAutoPositioning_ = true;
|
|
comment.setVisible(true);
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
// Locate unused local variables...
|
|
let workspace = this.getWorkspace();
|
|
let map = workspace.getVariableMap();
|
|
let vars = map.getVariablesOfType("");
|
|
let unusedLocals = [];
|
|
|
|
for (const row of vars) {
|
|
if (row.isLocal) {
|
|
let usages = map.getVariableUsesById(row.getId());
|
|
|
|
if (!usages || usages.length === 0) {
|
|
unusedLocals.push(row);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unusedLocals.length > 0) {
|
|
const unusedCount = unusedLocals.length;
|
|
let message = this.msg("unused-var", {
|
|
count: unusedCount
|
|
});
|
|
|
|
for (let i = 0; i < unusedLocals.length; i++) {
|
|
let orphan = unusedLocals[i];
|
|
|
|
if (i > 0) {
|
|
message += ", ";
|
|
}
|
|
|
|
message += orphan.name;
|
|
}
|
|
|
|
if (confirm(message)) {
|
|
for (const orphan of unusedLocals) {
|
|
workspace.deleteVariableById(orphan.getId());
|
|
}
|
|
}
|
|
} // Locate unused local lists...
|
|
|
|
|
|
let lists = map.getVariablesOfType("list");
|
|
let unusedLists = [];
|
|
|
|
for (const row of lists) {
|
|
if (row.isLocal) {
|
|
let usages = map.getVariableUsesById(row.getId());
|
|
|
|
if (!usages || usages.length === 0) {
|
|
unusedLists.push(row);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (unusedLists.length > 0) {
|
|
const unusedCount = unusedLists.length;
|
|
let message = this.msg("unused-list", {
|
|
count: unusedCount
|
|
});
|
|
|
|
for (let i = 0; i < unusedLists.length; i++) {
|
|
let orphan = unusedLists[i];
|
|
|
|
if (i > 0) {
|
|
message += ", ";
|
|
}
|
|
|
|
message += orphan.name;
|
|
}
|
|
|
|
if (confirm(message)) {
|
|
for (const orphan of unusedLists) {
|
|
workspace.deleteVariableById(orphan.getId());
|
|
}
|
|
}
|
|
}
|
|
|
|
_UndoGroup_js__WEBPACK_IMPORTED_MODULE_1__["default"].endUndoGroup(workspace);
|
|
}, 100);
|
|
}
|
|
/**
|
|
* Badly Orphaned - might want to delete these!
|
|
* @param topBlock
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
|
|
isBlockAnOrphan(topBlock) {
|
|
return !!topBlock.outputConnection;
|
|
}
|
|
/**
|
|
* Split the top blocks into ordered columns
|
|
* @param separateOrphans true to keep all orphans separate
|
|
* @returns {{orphans: {blocks: [Block], x: number, count: number}, cols: [Col]}}
|
|
*/
|
|
|
|
|
|
getOrderedTopBlockColumns(separateOrphans) {
|
|
let w = this.getWorkspace();
|
|
let topBlocks = w.getTopBlocks();
|
|
let maxWidths = {};
|
|
|
|
if (separateOrphans) {
|
|
let topComments = w.getTopComments(); // todo: tie comments to blocks... find widths and width of block stack row...
|
|
|
|
for (const comment of topComments) {
|
|
// comment.autoPosition_();
|
|
// Hiding and showing repositions the comment right next to it's block - nice!
|
|
if (comment.setVisible) {
|
|
comment.setVisible(false);
|
|
comment.needsAutoPositioning_ = true;
|
|
comment.setVisible(true); // let bb = comment.block_.svgPath_.getBBox();
|
|
|
|
let right = comment.getBoundingRectangle().bottomRight.x; // Get top block for stack...
|
|
|
|
let root = comment.block_.getRootBlock();
|
|
let left = root.getBoundingRectangle().topLeft.x;
|
|
maxWidths[root.id] = Math.max(right - left, maxWidths[root.id] || 0);
|
|
}
|
|
}
|
|
} // Default scratch ordering is horrid... Lets try something more clever.
|
|
|
|
/**
|
|
* @type {Col[]}
|
|
*/
|
|
|
|
|
|
let cols = [];
|
|
const TOLERANCE = 256;
|
|
let orphans = {
|
|
x: -999999,
|
|
count: 0,
|
|
blocks: []
|
|
};
|
|
|
|
for (const topBlock of topBlocks) {
|
|
// let r = b.getBoundingRectangle();
|
|
let position = topBlock.getRelativeToSurfaceXY();
|
|
/**
|
|
* @type {Col}
|
|
*/
|
|
|
|
let bestCol = null;
|
|
let bestError = TOLERANCE;
|
|
|
|
if (separateOrphans && this.isBlockAnOrphan(topBlock)) {
|
|
orphans.blocks.push(topBlock);
|
|
continue;
|
|
} // Find best columns
|
|
|
|
|
|
for (const col of cols) {
|
|
let err = Math.abs(position.x - col.x);
|
|
|
|
if (err < bestError) {
|
|
bestError = err;
|
|
bestCol = col;
|
|
}
|
|
}
|
|
|
|
if (bestCol) {
|
|
// We found a column that we fitted into
|
|
bestCol.x = (bestCol.x * bestCol.count + position.x) / ++bestCol.count; // re-average the columns as more items get added...
|
|
|
|
bestCol.blocks.push(topBlock);
|
|
} else {
|
|
// Create a new column
|
|
cols.push(new Col(position.x, 1, [topBlock]));
|
|
}
|
|
} // if (orphans.blocks.length > 0) {
|
|
// cols.push(orphans);
|
|
// }
|
|
// Sort columns, then blocks inside the columns
|
|
|
|
|
|
cols.sort((a, b) => a.x - b.x);
|
|
|
|
for (const col of cols) {
|
|
col.blocks.sort((a, b) => a.getRelativeToSurfaceXY().y - b.getRelativeToSurfaceXY().y);
|
|
}
|
|
|
|
return {
|
|
cols: cols,
|
|
orphans: orphans,
|
|
maxWidths: maxWidths
|
|
};
|
|
}
|
|
/**
|
|
* Find all the uses of a named variable.
|
|
* @param {string} id ID of the variable to find.
|
|
* @return {!Array.<!Blockly.Block>} Array of block usages.
|
|
*/
|
|
|
|
|
|
getVariableUsesById(id) {
|
|
let uses = [];
|
|
let topBlocks = this.getTopBlocks(true); // todo: Confirm this was the right getTopBlocks?
|
|
|
|
for (const topBlock of topBlocks) {
|
|
/** @type {!Array<!Blockly.Block>} */
|
|
let kids = topBlock.getDescendants();
|
|
|
|
for (const block of kids) {
|
|
/** @type {!Array<!Blockly.VariableModel>} */
|
|
let blockVariables = block.getVarModels();
|
|
|
|
if (blockVariables) {
|
|
for (const blockVar of blockVariables) {
|
|
if (blockVar.getId() === id) {
|
|
uses.push(block);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return uses;
|
|
}
|
|
/**
|
|
* Quick and dirty replace all instances of one variable / list with another variable / list
|
|
* @param varId original variable name
|
|
* @param newVarName new variable name
|
|
* @param type type of variable ("" = variable, anything else is a list?
|
|
*/
|
|
|
|
|
|
doReplaceVariable(varId, newVarName, type) {
|
|
let wksp = this.getWorkspace();
|
|
let v = wksp.getVariable(newVarName, type);
|
|
|
|
if (!v) {
|
|
alert(this.msg("var-not-exist"));
|
|
return;
|
|
}
|
|
|
|
let newVId = v.getId();
|
|
_UndoGroup_js__WEBPACK_IMPORTED_MODULE_1__["default"].startUndoGroup(wksp);
|
|
let blocks = this.getVariableUsesById(varId);
|
|
|
|
for (const block of blocks) {
|
|
try {
|
|
if (type === "") {
|
|
block.getField("VARIABLE").setValue(newVId);
|
|
} else {
|
|
block.getField("LIST").setValue(newVId);
|
|
}
|
|
} catch (e) {// ignore
|
|
}
|
|
}
|
|
|
|
_UndoGroup_js__WEBPACK_IMPORTED_MODULE_1__["default"].endUndoGroup(wksp);
|
|
}
|
|
/*
|
|
function doInjectScripts(codeString) {
|
|
let w = getWorkspace();
|
|
let xml = new XML(); // document.implementation.createDocument(null, "xml");
|
|
let x = xml.xmlDoc.firstChild;
|
|
let tree = math.parse(codeString);
|
|
console.log(tree);
|
|
const binaryOperatorTypes = {
|
|
add: "operator_add",
|
|
subtract: "operator_subtract",
|
|
this.multiply: "operator_multiply",
|
|
divide: "operator_divide",
|
|
};
|
|
const BLOCK_TYPE = {
|
|
number: "math_number",
|
|
text: "text",
|
|
};
|
|
function translateMathToXml(x, tree, shadowType) {
|
|
let xShadowField = null;
|
|
if (shadowType) {
|
|
let xShadow = xml.newXml(x, "shadow", { type: shadowType });
|
|
if (shadowType === BLOCK_TYPE.number) {
|
|
xShadowField = xml.newXml(xShadow, "field", { name: "NUM" });
|
|
} else if (shadowType === BLOCK_TYPE.text) {
|
|
xShadowField = xml.newXml(xShadow, "field", { name: "TEXT" });
|
|
}
|
|
}
|
|
if (!tree || !tree.type) {
|
|
return;
|
|
}
|
|
if (tree.type === "OperatorNode") {
|
|
let operatorType = binaryOperatorTypes[tree.fn];
|
|
if (operatorType) {
|
|
let xOp = newXml(x, "block", { type: operatorType });
|
|
translateMathToXml(xml.newXml(xOp, "value", { name: "NUM1" }), tree.args[0], BLOCK_TYPE.number);
|
|
translateMathToXml(xml.newXml(xOp, "value", { name: "NUM2" }), tree.args[1], BLOCK_TYPE.number);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
if (tree.type === "ConstantNode") {
|
|
// number or text in quotes
|
|
if (xShadowField) {
|
|
xml.setAttr(xShadowField, { text: tree.value });
|
|
}
|
|
return;
|
|
}
|
|
if (tree.type === "SymbolNode") {
|
|
// variable
|
|
let xVar = xml.newXml(x, "block", { type: "data_variable" });
|
|
xml.newXml(xVar, "field", { name: "VARIABLE", text: tree.name });
|
|
return;
|
|
}
|
|
if (tree.type === "FunctionNode") {
|
|
// Method Call
|
|
if (tree.fn.name === "join") {
|
|
let xOp = newXml(x, "block", { type: "operator_join" });
|
|
translateMathToXml(xml.newXml(xOp, "value", { name: "STRING1" }), tree.args[0], BLOCK_TYPE.text);
|
|
translateMathToXml(xml.newXml(xOp, "value", { name: "STRING2" }), tree.args[1], BLOCK_TYPE.text);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
translateMathToXml(x, tree);
|
|
console.log(x);
|
|
let ids = Blockly.Xml.domToWorkspace(x, w);
|
|
console.log(ids);
|
|
}
|
|
*/
|
|
|
|
/*
|
|
function clickInject(e) {
|
|
let codeString = window.prompt("Griffpatch: Enter an expression (i.e. a+2*3)");
|
|
if (codeString) {
|
|
doInjectScripts(codeString);
|
|
}
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Returns a Set of the top blocks in this workspace / sprite
|
|
* @returns {Set<any>} Set of top blocks
|
|
*/
|
|
|
|
|
|
getTopBlockIDs() {
|
|
let wksp = this.getWorkspace();
|
|
let topBlocks = wksp.getTopBlocks();
|
|
let ids = new Set();
|
|
|
|
for (const block of topBlocks) {
|
|
ids.add(block.id);
|
|
}
|
|
|
|
return ids;
|
|
}
|
|
/**
|
|
* Initiates a drag event for all block stacks except those in the set of ids.
|
|
* But why? - Because we know all the ids of the existing stacks before we paste / duplicate - so we can find the
|
|
* new stack by excluding all the known ones.
|
|
* @param ids Set of previously known ids
|
|
*/
|
|
|
|
|
|
beginDragOfNewBlocksNotInIDs(ids) {
|
|
if (!this.addon.settings.get("enablePasteBlocksAtMouse")) {
|
|
return;
|
|
}
|
|
|
|
let wksp = this.getWorkspace();
|
|
let topBlocks = wksp.getTopBlocks();
|
|
|
|
for (const block of topBlocks) {
|
|
if (!ids.has(block.id)) {
|
|
// console.log("I found a new block!!! - " + block.id);
|
|
// todo: move the block to the mouse pointer?
|
|
let mouseXYClone = {
|
|
x: this.mouseXY.x,
|
|
y: this.mouseXY.y
|
|
};
|
|
block.setIntersects(true); // fixes offscreen block pasting in TurboWarp
|
|
|
|
this.domHelpers.triggerDragAndDrop(block.svgPath_, null, mouseXYClone);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateMousePosition(e) {
|
|
this.mouseXY.x = e.clientX;
|
|
this.mouseXY.y = e.clientY;
|
|
}
|
|
|
|
eventMouseMove(e) {
|
|
this.updateMousePosition(e);
|
|
}
|
|
|
|
eventKeyDown(e) {
|
|
const switchCostume = up => {
|
|
// todo: select previous costume
|
|
let selected = this.costTabBody.querySelector("div[class*='sprite-selector-item_is-selected']");
|
|
let node = up ? selected.parentNode.previousSibling : selected.parentNode.nextSibling;
|
|
|
|
if (node) {
|
|
let wrapper = node.closest("div[class*=gui_flex-wrapper]");
|
|
node.querySelector("div[class^='sprite-selector-item_sprite-name']").click();
|
|
node.scrollIntoView({
|
|
behavior: "auto",
|
|
block: "center",
|
|
inline: "start"
|
|
});
|
|
wrapper.scrollTop = 0;
|
|
}
|
|
};
|
|
|
|
if (document.URL.indexOf("editor") <= 0) {
|
|
return;
|
|
}
|
|
|
|
let ctrlKey = e.ctrlKey || e.metaKey;
|
|
|
|
if (e.keyCode === 37 && ctrlKey) {
|
|
// Ctrl + Left Arrow Key
|
|
if (document.activeElement.tagName === "INPUT") {
|
|
return;
|
|
}
|
|
|
|
if (this.isCostumeEditor()) {
|
|
switchCostume(true);
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (e.keyCode === 39 && ctrlKey) {
|
|
// Ctrl + Right Arrow Key
|
|
if (document.activeElement.tagName === "INPUT") {
|
|
return;
|
|
}
|
|
|
|
if (this.isCostumeEditor()) {
|
|
switchCostume(false);
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (e.keyCode === 86 && ctrlKey && !e.griff) {
|
|
// Ctrl + V
|
|
// Set a timeout so we can take control of the paste after the event
|
|
let ids = this.getTopBlockIDs();
|
|
setTimeout(() => {
|
|
this.beginDragOfNewBlocksNotInIDs(ids);
|
|
}, 10);
|
|
} // if (e.keyCode === 220 && (!document.activeElement || document.activeElement.tagName === 'INPUT')) {
|
|
//
|
|
// }
|
|
|
|
}
|
|
|
|
eventCopyClick(block, blockOnly) {
|
|
let wksp = this.getWorkspace();
|
|
|
|
if (block) {
|
|
block.select();
|
|
let next = blockOnly ? block.getNextBlock() : null;
|
|
|
|
if (next) {
|
|
next.unplug(false); // setParent(null);
|
|
} // separate child temporarily
|
|
|
|
|
|
document.dispatchEvent(new KeyboardEvent("keydown", {
|
|
keyCode: 67,
|
|
ctrlKey: true
|
|
}));
|
|
|
|
if (next || blockOnly === 2) {
|
|
setTimeout(() => {
|
|
if (next) {
|
|
wksp.undo(); // undo the unplug above...
|
|
}
|
|
|
|
if (blockOnly === 2) {
|
|
_UndoGroup_js__WEBPACK_IMPORTED_MODULE_1__["default"].startUndoGroup(wksp);
|
|
block.dispose(true);
|
|
_UndoGroup_js__WEBPACK_IMPORTED_MODULE_1__["default"].endUndoGroup(wksp);
|
|
}
|
|
}, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
eventMouseDown(e) {
|
|
this.updateMousePosition(e);
|
|
}
|
|
|
|
eventMouseUp(e) {
|
|
this.updateMousePosition(e);
|
|
}
|
|
|
|
initInner(root) {
|
|
let guiTabs = root.childNodes;
|
|
|
|
if (this.codeTab && guiTabs[0] !== this.codeTab) {
|
|
// We have been CHANGED!!! - Happens when going to project page, and then back inside again!!!
|
|
this.domHelpers.unbindAllEvents();
|
|
}
|
|
|
|
this.codeTab = guiTabs[0];
|
|
this.costTab = guiTabs[1];
|
|
this.costTabBody = document.querySelector("div[aria-labelledby=" + this.costTab.id + "]");
|
|
this.domHelpers.bindOnce(document, "keydown", (...e) => this.eventKeyDown(...e), true);
|
|
this.domHelpers.bindOnce(document, "mousemove", (...e) => this.eventMouseMove(...e), true);
|
|
this.domHelpers.bindOnce(document, "mousedown", (...e) => this.eventMouseDown(...e), true); // true to capture all mouse downs 'before' the dom events handle them
|
|
|
|
this.domHelpers.bindOnce(document, "mouseup", (...e) => this.eventMouseUp(...e), true);
|
|
}
|
|
|
|
}
|
|
|
|
class Col {
|
|
/**
|
|
* @param x {Number} x position (for ordering)
|
|
* @param count {Number}
|
|
* @param blocks {[Block]}
|
|
*/
|
|
constructor(x, count, blocks) {
|
|
/**
|
|
* x position (for ordering)
|
|
* @type {Number}
|
|
*/
|
|
this.x = x;
|
|
/**
|
|
* @type {Number}
|
|
*/
|
|
|
|
this.count = count;
|
|
/**
|
|
* @type {[Blockly.Block]}
|
|
*/
|
|
|
|
this.blocks = blocks;
|
|
}
|
|
|
|
}
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-devtools/DomHelpers.js":
|
|
/*!*********************************************************!*\
|
|
!*** ./src/addons/addons/editor-devtools/DomHelpers.js ***!
|
|
\*********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return DomHelpers; });
|
|
class DomHelpers {
|
|
constructor(addon) {
|
|
this.addon = addon;
|
|
this.vm = addon.tab.traps.vm;
|
|
/**
|
|
* @type {eventDetails[]}
|
|
*/
|
|
|
|
this.events = [];
|
|
}
|
|
/**
|
|
* Simulate a drag and drop programmatically through javascript
|
|
* @param selectorDrag
|
|
* @param selectorDrop
|
|
* @param mouseXY
|
|
* @param [shiftKey=false]
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
|
|
triggerDragAndDrop(selectorDrag, selectorDrop, mouseXY, shiftKey) {
|
|
// function for triggering mouse events
|
|
shiftKey = shiftKey || false;
|
|
|
|
let fireMouseEvent = function fireMouseEvent(type, elem, centerX, centerY) {
|
|
let evt = document.createEvent("MouseEvents");
|
|
evt.initMouseEvent(type, true, true, window, 1, 1, 1, centerX, centerY, shiftKey, false, false, false, 0, elem);
|
|
elem.dispatchEvent(evt);
|
|
}; // fetch target elements
|
|
|
|
|
|
let elemDrag = selectorDrag; // document.querySelector(selectorDrag);
|
|
|
|
let elemDrop = selectorDrop; // document.querySelector(selectorDrop);
|
|
|
|
if (!elemDrag
|
|
/* || !elemDrop*/
|
|
) {
|
|
return false;
|
|
} // calculate positions
|
|
|
|
|
|
let pos = elemDrag.getBoundingClientRect();
|
|
let center1X = Math.floor((pos.left + pos.right) / 2);
|
|
let center1Y = Math.floor((pos.top + pos.bottom) / 2); // mouse over dragged element and mousedown
|
|
|
|
fireMouseEvent("mouseover", elemDrag, center1X, center1Y);
|
|
fireMouseEvent("mousedown", elemDrag, center1X, center1Y); // start dragging process over to drop target
|
|
|
|
fireMouseEvent("dragstart", elemDrag, center1X, center1Y);
|
|
fireMouseEvent("drag", elemDrag, center1X, center1Y);
|
|
fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
|
|
|
|
if (!elemDrop) {
|
|
if (mouseXY) {
|
|
// console.log(mouseXY);
|
|
let center2X = mouseXY.x;
|
|
let center2Y = mouseXY.y;
|
|
fireMouseEvent("drag", elemDrag, center2X, center2Y);
|
|
fireMouseEvent("mousemove", elemDrag, center2X, center2Y);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
pos = elemDrop.getBoundingClientRect();
|
|
let center2X = Math.floor((pos.left + pos.right) / 2);
|
|
let center2Y = Math.floor((pos.top + pos.bottom) / 2);
|
|
fireMouseEvent("drag", elemDrag, center2X, center2Y);
|
|
fireMouseEvent("mousemove", elemDrop, center2X, center2Y); // trigger dragging process on top of drop target
|
|
|
|
fireMouseEvent("mouseenter", elemDrop, center2X, center2Y);
|
|
fireMouseEvent("dragenter", elemDrop, center2X, center2Y);
|
|
fireMouseEvent("mouseover", elemDrop, center2X, center2Y);
|
|
fireMouseEvent("dragover", elemDrop, center2X, center2Y); // release dragged element on top of drop target
|
|
|
|
fireMouseEvent("drop", elemDrop, center2X, center2Y);
|
|
fireMouseEvent("dragend", elemDrag, center2X, center2Y);
|
|
fireMouseEvent("mouseup", elemDrag, center2X, center2Y);
|
|
return true;
|
|
}
|
|
|
|
bindOnce(dom, event, func, capture) {
|
|
capture = !!capture;
|
|
dom.removeEventListener(event, func, capture);
|
|
dom.addEventListener(event, func, capture);
|
|
this.events.push(new eventDetails(dom, event, func, capture));
|
|
}
|
|
|
|
unbindAllEvents() {
|
|
for (const event of this.events) {
|
|
event.dom.removeEventListener(event.event, event.func, event.capture);
|
|
}
|
|
|
|
this.events = [];
|
|
}
|
|
|
|
}
|
|
/**
|
|
* A record of an event
|
|
*/
|
|
|
|
class eventDetails {
|
|
constructor(dom, event, func, capture) {
|
|
this.dom = dom;
|
|
this.event = event;
|
|
this.func = func;
|
|
this.capture = capture;
|
|
}
|
|
|
|
}
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-devtools/UndoGroup.js":
|
|
/*!********************************************************!*\
|
|
!*** ./src/addons/addons/editor-devtools/UndoGroup.js ***!
|
|
\********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return UndoGroup; });
|
|
/**
|
|
* This class is dedicated to maintaining the Undo stack of Blockly
|
|
* It allows us to initiate an undo group such that all subsequent operations are recorded as a single
|
|
* undoable transaction.
|
|
*/
|
|
class UndoGroup {
|
|
/**
|
|
* Start an Undo group - begin recording
|
|
* @param workspace the workspace
|
|
*/
|
|
static startUndoGroup(workspace) {
|
|
const undoStack = workspace.undoStack_;
|
|
|
|
if (undoStack.length) {
|
|
undoStack[undoStack.length - 1]._devtoolsLastUndo = true;
|
|
}
|
|
}
|
|
/**
|
|
* End an Undo group - stops recording
|
|
* @param workspace the workspace
|
|
*/
|
|
|
|
|
|
static endUndoGroup(workspace) {
|
|
const undoStack = workspace.undoStack_; // Events (responsible for undoStack updates) are delayed with a setTimeout(f, 0)
|
|
// https://github.com/LLK/scratch-blocks/blob/f159a1779e5391b502d374fb2fdd0cb5ca43d6a2/core/events.js#L182
|
|
|
|
setTimeout(() => {
|
|
const group = generateUID();
|
|
|
|
for (let i = undoStack.length - 1; i >= 0 && !undoStack[i]._devtoolsLastUndo; i--) {
|
|
undoStack[i].group = group;
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
}
|
|
/**
|
|
* https://github.com/LLK/scratch-blocks/blob/f159a1779e5391b502d374fb2fdd0cb5ca43d6a2/core/events.js#L182
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
|
|
function generateUID() {
|
|
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%()*+,-./:;=?@[]^_`{|}~";
|
|
let result = "";
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
result += CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-devtools/_runtime_entry.js":
|
|
/*!*************************************************************!*\
|
|
!*** ./src/addons/addons/editor-devtools/_runtime_entry.js ***!
|
|
\*************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/editor-devtools/userscript.js");
|
|
/* harmony import */ var _url_loader_icon_close_svg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! url-loader!./icon--close.svg */ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/editor-devtools/icon--close.svg");
|
|
/* generated by pull.js */
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"icon--close.svg": _url_loader_icon_close_svg__WEBPACK_IMPORTED_MODULE_1__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-devtools/userscript.js":
|
|
/*!*********************************************************!*\
|
|
!*** ./src/addons/addons/editor-devtools/userscript.js ***!
|
|
\*********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony import */ var _DevTools_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./DevTools.js */ "./src/addons/addons/editor-devtools/DevTools.js");
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console,
|
|
msg,
|
|
safeMsg: m
|
|
}) {
|
|
const devTools = new _DevTools_js__WEBPACK_IMPORTED_MODULE_0__["default"](addon, msg, m);
|
|
devTools.init();
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-searchable-dropdowns/_runtime_entry.js":
|
|
/*!*************************************************************************!*\
|
|
!*** ./src/addons/addons/editor-searchable-dropdowns/_runtime_entry.js ***!
|
|
\*************************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/editor-searchable-dropdowns/userscript.js");
|
|
/* harmony import */ var _css_loader_userscript_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./userscript.css */ "./node_modules/css-loader/index.js!./src/addons/addons/editor-searchable-dropdowns/userscript.css");
|
|
/* harmony import */ var _css_loader_userscript_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_userscript_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* generated by pull.js */
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"userscript.css": _css_loader_userscript_css__WEBPACK_IMPORTED_MODULE_1___default.a
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/editor-searchable-dropdowns/userscript.js":
|
|
/*!*********************************************************************!*\
|
|
!*** ./src/addons/addons/editor-searchable-dropdowns/userscript.js ***!
|
|
\*********************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console,
|
|
msg
|
|
}) {
|
|
const Blockly = await addon.tab.traps.getBlockly();
|
|
const vm = addon.tab.traps.vm;
|
|
const SCRATCH_ITEMS_TO_HIDE = ["RENAME_VARIABLE_ID", "DELETE_VARIABLE_ID", "NEW_BROADCAST_MESSAGE_ID", // From rename-broadcasts addon
|
|
"RENAME_BROADCAST_MESSAGE_ID"];
|
|
|
|
const canUseAsGlobalVariableName = (name, type) => {
|
|
return !vm.runtime.getAllVarNamesOfType(type).includes(name);
|
|
};
|
|
|
|
const canUseAsLocalVariableName = (name, type) => {
|
|
return !vm.editingTarget.lookupVariableByNameAndType(name, type);
|
|
};
|
|
|
|
const ADDON_ITEMS = {
|
|
createGlobalVariable: {
|
|
enabled: name => canUseAsGlobalVariableName(name, ""),
|
|
createVariable: (workspace, name) => workspace.createVariable(name)
|
|
},
|
|
createLocalVariable: {
|
|
enabled: name => canUseAsLocalVariableName(name, ""),
|
|
createVariable: (workspace, name) => workspace.createVariable(name, "", null, true)
|
|
},
|
|
createGlobalList: {
|
|
enabled: name => canUseAsGlobalVariableName(name, "list"),
|
|
createVariable: (workspace, name) => workspace.createVariable(name, "list")
|
|
},
|
|
createLocalList: {
|
|
enabled: name => canUseAsLocalVariableName(name, "list"),
|
|
createVariable: (workspace, name) => workspace.createVariable(name, "list", null, true)
|
|
},
|
|
createBroadcast: {
|
|
enabled: name => canUseAsGlobalVariableName(name, "broadcast_msg"),
|
|
createVariable: (workspace, name) => workspace.createVariable(name, "broadcast_msg")
|
|
}
|
|
};
|
|
let blocklyDropDownContent = null;
|
|
let blocklyDropdownMenu = null;
|
|
let searchBar = null; // Contains DOM and addon state
|
|
|
|
let items = [];
|
|
let searchedItems = []; // Tracks internal Scratch state
|
|
|
|
let currentDropdownOptions = [];
|
|
let resultOfLastGetOptions = [];
|
|
const oldDropDownDivShow = Blockly.DropDownDiv.show;
|
|
|
|
Blockly.DropDownDiv.show = function (...args) {
|
|
blocklyDropdownMenu = document.querySelector(".blocklyDropdownMenu");
|
|
|
|
if (!blocklyDropdownMenu) {
|
|
return oldDropDownDivShow.call(this, ...args);
|
|
}
|
|
|
|
blocklyDropdownMenu.focus = () => {}; // no-op focus() so it can't steal it from the search bar
|
|
|
|
|
|
searchBar = document.createElement("input");
|
|
addon.tab.displayNoneWhileDisabled(searchBar, {
|
|
display: "flex"
|
|
});
|
|
searchBar.type = "text";
|
|
searchBar.addEventListener("input", updateSearch);
|
|
searchBar.addEventListener("keydown", handleKeyDownEvent);
|
|
searchBar.classList.add("u-dropdown-searchbar");
|
|
blocklyDropdownMenu.insertBefore(searchBar, blocklyDropdownMenu.firstChild);
|
|
items = Array.from(blocklyDropdownMenu.children).filter(child => child.tagName !== "INPUT").map(element => ({
|
|
element,
|
|
text: element.textContent
|
|
}));
|
|
currentDropdownOptions = resultOfLastGetOptions;
|
|
updateSearch(); // Call the original show method after adding everything so that it can perform the correct size calculations
|
|
|
|
const ret = oldDropDownDivShow.call(this, ...args); // Lock the size of the dropdown
|
|
|
|
blocklyDropDownContent = Blockly.DropDownDiv.getContentDiv();
|
|
blocklyDropDownContent.style.width = getComputedStyle(blocklyDropDownContent).width;
|
|
blocklyDropDownContent.style.height = getComputedStyle(blocklyDropDownContent).height; // This is really strange, but if you don't reinsert the search bar into the DOM then focus() doesn't work
|
|
|
|
blocklyDropdownMenu.insertBefore(searchBar, blocklyDropdownMenu.firstChild);
|
|
searchBar.focus();
|
|
return ret;
|
|
};
|
|
|
|
const oldDropDownDivClearContent = Blockly.DropDownDiv.clearContent;
|
|
|
|
Blockly.DropDownDiv.clearContent = function () {
|
|
oldDropDownDivClearContent.call(this);
|
|
items = [];
|
|
searchedItems = [];
|
|
Blockly.DropDownDiv.content_.style.height = "";
|
|
};
|
|
|
|
const oldFieldDropdownGetOptions = Blockly.FieldDropdown.prototype.getOptions;
|
|
|
|
Blockly.FieldDropdown.prototype.getOptions = function () {
|
|
const options = oldFieldDropdownGetOptions.call(this);
|
|
const block = this.sourceBlock_;
|
|
const isStage = vm.editingTarget && vm.editingTarget.isStage;
|
|
|
|
if (block) {
|
|
if (block.category_ === "data") {
|
|
options.push(getMenuItemMessage("createGlobalVariable"));
|
|
|
|
if (!isStage) {
|
|
options.push(getMenuItemMessage("createLocalVariable"));
|
|
}
|
|
} else if (block.category_ === "data-lists") {
|
|
options.push(getMenuItemMessage("createGlobalList"));
|
|
|
|
if (!isStage) {
|
|
options.push(getMenuItemMessage("createLocalList"));
|
|
}
|
|
} else if (block.type === "event_broadcast_menu" || block.type === "event_whenbroadcastreceived") {
|
|
options.push(getMenuItemMessage("createBroadcast"));
|
|
}
|
|
} // Options aren't normally stored anywhere, so we'll store them ourselves.
|
|
|
|
|
|
resultOfLastGetOptions = options;
|
|
return options;
|
|
};
|
|
|
|
const oldFieldVariableOnItemSelected = Blockly.FieldVariable.prototype.onItemSelected;
|
|
|
|
Blockly.FieldVariable.prototype.onItemSelected = function (menu, menuItem) {
|
|
const sourceBlock = this.sourceBlock_;
|
|
|
|
if (sourceBlock && sourceBlock.workspace && searchBar.value.length !== 0) {
|
|
const workspace = sourceBlock.workspace;
|
|
const optionId = menuItem.getValue();
|
|
|
|
if (Object.prototype.hasOwnProperty.call(ADDON_ITEMS, optionId)) {
|
|
const addonItem = ADDON_ITEMS[optionId];
|
|
Blockly.Events.setGroup(true);
|
|
const variable = addonItem.createVariable(workspace, searchBar.value.trim());
|
|
if (this.sourceBlock_) this.setValue(variable.getId());
|
|
Blockly.Events.setGroup(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
return oldFieldVariableOnItemSelected.call(this, menu, menuItem);
|
|
};
|
|
|
|
function selectItem(item, click) {
|
|
// You can't just use click() or focus() because Blockly uses mousedown and mouseup handlers, not click handlers.
|
|
item.dispatchEvent(new MouseEvent("mousedown", {
|
|
relatedTarget: item,
|
|
bubbles: true
|
|
}));
|
|
if (click) item.dispatchEvent(new MouseEvent("mouseup", {
|
|
relatedTarget: item,
|
|
bubbles: true
|
|
})); // Scroll the item into view if it is offscreen.
|
|
|
|
const itemTop = item.offsetTop;
|
|
const itemEnd = itemTop + item.offsetHeight;
|
|
const scrollTop = blocklyDropDownContent.scrollTop;
|
|
const scrollHeight = blocklyDropDownContent.offsetHeight;
|
|
const scrollEnd = scrollTop + scrollHeight;
|
|
|
|
if (scrollTop > itemTop) {
|
|
blocklyDropDownContent.scrollTop = itemTop;
|
|
} else if (itemEnd > scrollEnd) {
|
|
blocklyDropDownContent.scrollTop = itemEnd - scrollHeight;
|
|
}
|
|
}
|
|
|
|
function performSearch() {
|
|
const rawQuery = searchBar.value.trim();
|
|
const query = rawQuery.trim().toLowerCase();
|
|
|
|
const rank = (item, index) => {
|
|
// Negative number will hide
|
|
// Higher numbers will appear first
|
|
const option = currentDropdownOptions[index];
|
|
const optionId = option[1];
|
|
|
|
if (SCRATCH_ITEMS_TO_HIDE.includes(optionId)) {
|
|
return query ? -1 : 0;
|
|
} else if (Object.prototype.hasOwnProperty.call(ADDON_ITEMS, optionId)) {
|
|
if (!query) {
|
|
return -1;
|
|
}
|
|
|
|
const addonInfo = ADDON_ITEMS[optionId];
|
|
|
|
if (addonInfo.enabled(rawQuery)) {
|
|
item.element.lastChild.lastChild.textContent = getMenuItemMessage(optionId)[0];
|
|
return 0;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
const itemText = item.text.toLowerCase();
|
|
|
|
if (query === itemText) {
|
|
return 2;
|
|
}
|
|
|
|
if (itemText.startsWith(query)) {
|
|
return 1;
|
|
}
|
|
|
|
if (itemText.includes(query)) {
|
|
return 0;
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
return items.map((item, index) => ({
|
|
item,
|
|
score: rank(item, index)
|
|
})).sort(({
|
|
score: scoreA
|
|
}, {
|
|
score: scoreB
|
|
}) => Math.max(0, scoreB) - Math.max(0, scoreA));
|
|
}
|
|
|
|
function updateSearch() {
|
|
const previousSearchedItems = searchedItems;
|
|
searchedItems = performSearch();
|
|
let needToUpdateDOM = previousSearchedItems.length !== searchedItems.length;
|
|
|
|
if (!needToUpdateDOM) {
|
|
for (let i = 0; i < searchedItems.length; i++) {
|
|
if (searchedItems[i].item !== previousSearchedItems[i].item) {
|
|
needToUpdateDOM = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (needToUpdateDOM && previousSearchedItems.length > 0) {
|
|
for (const {
|
|
item
|
|
} of previousSearchedItems) {
|
|
item.element.remove();
|
|
}
|
|
|
|
for (const {
|
|
item
|
|
} of searchedItems) {
|
|
blocklyDropdownMenu.appendChild(item.element);
|
|
}
|
|
}
|
|
|
|
for (const {
|
|
item,
|
|
score
|
|
} of searchedItems) {
|
|
item.element.hidden = score < 0;
|
|
}
|
|
}
|
|
|
|
function handleKeyDownEvent(event) {
|
|
if (event.key === "Enter") {
|
|
// Reimplement enter to select item to account for hidden items and default to the top item.
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
const selectedItem = document.querySelector(".goog-menuitem-highlight");
|
|
|
|
if (selectedItem && !selectedItem.hidden) {
|
|
selectItem(selectedItem, true);
|
|
return;
|
|
}
|
|
|
|
const selectedBlock = Blockly.selected;
|
|
|
|
if (searchBar.value === "" && selectedBlock) {
|
|
if (selectedBlock.type === "event_broadcast" || selectedBlock.type === "event_broadcastandwait" || selectedBlock.type === "event_whenbroadcastreceived") {
|
|
// The top item of these dropdowns is always "New message"
|
|
// When pressing enter on an empty search bar, we close the dropdown instead of making a new broadcast.
|
|
Blockly.DropDownDiv.hide();
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (const {
|
|
item
|
|
} of searchedItems) {
|
|
if (!item.element.hidden) {
|
|
selectItem(item.element, true);
|
|
break;
|
|
}
|
|
} // If there is no top value, do nothing and leave the dropdown open
|
|
|
|
} else if (event.key === "Escape") {
|
|
Blockly.DropDownDiv.hide();
|
|
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
// Reimplement keyboard navigation to account for hidden items.
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const items = searchedItems.filter(i => i.score >= 0).map(i => i.item);
|
|
|
|
if (items.length === 0) {
|
|
return;
|
|
}
|
|
|
|
let selectedIndex = -1;
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
if (items[i].element.classList.contains("goog-menuitem-highlight")) {
|
|
selectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const lastIndex = items.length - 1;
|
|
let newIndex = 0;
|
|
|
|
if (event.key === "ArrowDown") {
|
|
if (selectedIndex === -1 || selectedIndex === lastIndex) {
|
|
newIndex = 0;
|
|
} else {
|
|
newIndex = selectedIndex + 1;
|
|
}
|
|
} else {
|
|
if (selectedIndex === -1 || selectedIndex === 0) {
|
|
newIndex = lastIndex;
|
|
} else {
|
|
newIndex = selectedIndex - 1;
|
|
}
|
|
}
|
|
|
|
selectItem(items[newIndex].element, false);
|
|
}
|
|
}
|
|
|
|
function getMenuItemMessage(message) {
|
|
var _searchBar;
|
|
|
|
// Format used internally by Scratch:
|
|
// [human readable name, internal name]
|
|
return [msg(message, {
|
|
name: ((_searchBar = searchBar) === null || _searchBar === void 0 ? void 0 : _searchBar.value.trim()) || ""
|
|
}), message];
|
|
}
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/find-bar/_runtime_entry.js":
|
|
/*!******************************************************!*\
|
|
!*** ./src/addons/addons/find-bar/_runtime_entry.js ***!
|
|
\******************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/find-bar/userscript.js");
|
|
/* harmony import */ var _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./userstyle.css */ "./node_modules/css-loader/index.js!./src/addons/addons/find-bar/userstyle.css");
|
|
/* harmony import */ var _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* generated by pull.js */
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"userstyle.css": _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1___default.a
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/find-bar/blockly/BlockItem.js":
|
|
/*!*********************************************************!*\
|
|
!*** ./src/addons/addons/find-bar/blockly/BlockItem.js ***!
|
|
\*********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return BlockItem; });
|
|
class BlockItem {
|
|
constructor(cls, procCode, labelID, y) {
|
|
this.cls = cls;
|
|
this.procCode = procCode;
|
|
this.labelID = labelID;
|
|
this.y = y;
|
|
this.lower = procCode.toLowerCase();
|
|
/**
|
|
* An Array of block ids
|
|
* @type {Array.<string>}
|
|
*/
|
|
|
|
this.clones = null;
|
|
this.eventName = null;
|
|
}
|
|
/**
|
|
* True if the blockID matches a black represented by this BlockItem
|
|
* @param id
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
|
|
matchesID(id) {
|
|
if (this.labelID === id) {
|
|
return true;
|
|
}
|
|
|
|
if (this.clones) {
|
|
for (const cloneID of this.clones) {
|
|
if (cloneID === id) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/find-bar/userscript.js":
|
|
/*!**************************************************!*\
|
|
!*** ./src/addons/addons/find-bar/userscript.js ***!
|
|
\**************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony import */ var _blockly_BlockItem_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./blockly/BlockItem.js */ "./src/addons/addons/find-bar/blockly/BlockItem.js");
|
|
/* harmony import */ var _blockly_BlockInstance_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./blockly/BlockInstance.js */ "./src/addons/addons/find-bar/blockly/BlockInstance.js");
|
|
/* harmony import */ var _blockly_Utils_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./blockly/Utils.js */ "./src/addons/addons/find-bar/blockly/Utils.js");
|
|
|
|
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
msg,
|
|
console
|
|
}) {
|
|
const Blockly = await addon.tab.traps.getBlockly();
|
|
|
|
class FindBar {
|
|
constructor() {
|
|
this.utils = new _blockly_Utils_js__WEBPACK_IMPORTED_MODULE_2__["default"](addon);
|
|
this.prevValue = "";
|
|
this.findBarOuter = null;
|
|
this.findWrapper = null;
|
|
this.findInput = null;
|
|
this.dropdownOut = null;
|
|
this.dropdown = new Dropdown(this.utils);
|
|
document.addEventListener("keydown", e => this.eventKeyDown(e), true);
|
|
}
|
|
|
|
get workspace() {
|
|
return Blockly.getMainWorkspace();
|
|
}
|
|
|
|
createDom(root) {
|
|
this.findBarOuter = document.createElement("div");
|
|
this.findBarOuter.className = "sa-find-bar";
|
|
addon.tab.displayNoneWhileDisabled(this.findBarOuter, {
|
|
display: "flex"
|
|
});
|
|
root.appendChild(this.findBarOuter);
|
|
this.findWrapper = this.findBarOuter.appendChild(document.createElement("span"));
|
|
this.findWrapper.className = "sa-find-wrapper";
|
|
this.dropdownOut = this.findWrapper.appendChild(document.createElement("label"));
|
|
this.dropdownOut.className = "sa-find-dropdown-out";
|
|
this.findInput = this.dropdownOut.appendChild(document.createElement("input"));
|
|
this.findInput.className = addon.tab.scratchClass("input_input-form", {
|
|
others: "sa-find-input"
|
|
}); // for <label>
|
|
|
|
this.findInput.id = "sa-find-input";
|
|
this.findInput.type = "search";
|
|
this.findInput.placeholder = msg("find-placeholder");
|
|
this.findInput.autocomplete = "off";
|
|
this.dropdownOut.appendChild(this.dropdown.createDom());
|
|
this.bindEvents();
|
|
this.tabChanged();
|
|
}
|
|
|
|
bindEvents() {
|
|
this.findInput.addEventListener("focus", () => this.inputChange());
|
|
this.findInput.addEventListener("keydown", e => this.inputKeyDown(e));
|
|
this.findInput.addEventListener("keyup", () => this.inputChange());
|
|
this.findInput.addEventListener("focusout", () => this.hideDropDown());
|
|
}
|
|
|
|
tabChanged() {
|
|
if (!this.findBarOuter) {
|
|
return;
|
|
}
|
|
|
|
const tab = addon.tab.redux.state.scratchGui.editorTab.activeTabIndex;
|
|
const visible = tab === 0 || tab === 1 || tab === 2;
|
|
this.findBarOuter.hidden = !visible;
|
|
}
|
|
|
|
inputChange() {
|
|
this.showDropDown(); // Filter the list...
|
|
|
|
let val = (this.findInput.value || "").toLowerCase();
|
|
|
|
if (val === this.prevValue) {
|
|
// No change so don't re-filter
|
|
return;
|
|
}
|
|
|
|
this.prevValue = val;
|
|
this.dropdown.blocks = null; // Hide items in list that do not contain filter text
|
|
|
|
let listLI = this.dropdown.items;
|
|
|
|
for (const li of listLI) {
|
|
let procCode = li.data.procCode;
|
|
let i = li.data.lower.indexOf(val);
|
|
|
|
if (i >= 0) {
|
|
li.style.display = "block";
|
|
|
|
while (li.firstChild) {
|
|
li.removeChild(li.firstChild);
|
|
}
|
|
|
|
if (i > 0) {
|
|
li.appendChild(document.createTextNode(procCode.substring(0, i)));
|
|
}
|
|
|
|
let bText = document.createElement("b");
|
|
bText.appendChild(document.createTextNode(procCode.substr(i, val.length)));
|
|
li.appendChild(bText);
|
|
|
|
if (i + val.length < procCode.length) {
|
|
li.appendChild(document.createTextNode(procCode.substr(i + val.length)));
|
|
}
|
|
} else {
|
|
li.style.display = "none";
|
|
}
|
|
}
|
|
}
|
|
|
|
inputKeyDown(e) {
|
|
this.dropdown.inputKeyDown(e); // Enter
|
|
|
|
if (e.keyCode === 13) {
|
|
this.findInput.blur();
|
|
return;
|
|
} // Escape
|
|
|
|
|
|
if (e.keyCode === 27) {
|
|
if (this.findInput.value.length > 0) {
|
|
this.findInput.value = ""; // Clear search first, then close on second press
|
|
|
|
this.inputChange();
|
|
} else {
|
|
this.findInput.blur();
|
|
}
|
|
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
|
|
eventKeyDown(e) {
|
|
if (addon.self.disabled || !this.findBarOuter) return;
|
|
let ctrlKey = e.ctrlKey || e.metaKey;
|
|
|
|
if (e.key === "f" && ctrlKey && !e.shiftKey) {
|
|
// Ctrl + F (Override default Ctrl+F find)
|
|
this.findInput.focus();
|
|
this.findInput.select();
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
|
|
if (e.keyCode === 37 && ctrlKey) {
|
|
// Ctrl + Left Arrow Key
|
|
if (document.activeElement.tagName === "INPUT") {
|
|
return;
|
|
}
|
|
|
|
if (this.selectedTab === 0) {
|
|
this.utils.navigationHistory.goBack();
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (e.keyCode === 39 && ctrlKey) {
|
|
// Ctrl + Right Arrow Key
|
|
if (document.activeElement.tagName === "INPUT") {
|
|
return;
|
|
}
|
|
|
|
if (this.selectedTab === 0) {
|
|
this.utils.navigationHistory.goForward();
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
showDropDown(focusID, instanceBlock) {
|
|
if (!focusID && this.dropdownOut.classList.contains("visible")) {
|
|
return;
|
|
} // special '' vs null... - null forces a reevaluation
|
|
|
|
|
|
this.prevValue = focusID ? "" : null; // Clear the previous value of the input search
|
|
|
|
this.dropdownOut.classList.add("visible");
|
|
let scratchBlocks = this.selectedTab === 0 ? this.getScratchBlocks() : this.selectedTab === 1 ? this.getScratchCostumes() : this.selectedTab === 2 ? this.getScratchSounds() : [];
|
|
this.dropdown.empty();
|
|
|
|
for (const proc of scratchBlocks) {
|
|
let item = this.dropdown.addItem(proc);
|
|
|
|
if (focusID) {
|
|
if (proc.matchesID(focusID)) {
|
|
this.dropdown.onItemClick(item, instanceBlock);
|
|
} else {
|
|
item.style.display = "none";
|
|
}
|
|
}
|
|
}
|
|
|
|
this.utils.offsetX = this.dropdownOut.getBoundingClientRect().width + 32;
|
|
this.utils.offsetY = 32;
|
|
}
|
|
|
|
hideDropDown() {
|
|
this.dropdownOut.classList.remove("visible");
|
|
}
|
|
|
|
get selectedTab() {
|
|
return addon.tab.redux.state.scratchGui.editorTab.activeTabIndex;
|
|
}
|
|
|
|
getScratchBlocks() {
|
|
let myBlocks = [];
|
|
let myBlocksByProcCode = {};
|
|
let topBlocks = this.workspace.getTopBlocks();
|
|
/**
|
|
* @param cls
|
|
* @param txt
|
|
* @param root
|
|
* @returns BlockItem
|
|
*/
|
|
|
|
function addBlock(cls, txt, root) {
|
|
let id = root.id ? root.id : root.getId ? root.getId() : null;
|
|
let clone = myBlocksByProcCode[txt];
|
|
|
|
if (clone) {
|
|
if (!clone.clones) {
|
|
clone.clones = [];
|
|
}
|
|
|
|
clone.clones.push(id);
|
|
return clone;
|
|
}
|
|
|
|
let items = new _blockly_BlockItem_js__WEBPACK_IMPORTED_MODULE_0__["default"](cls, txt, id, 0);
|
|
items.y = root.getRelativeToSurfaceXY ? root.getRelativeToSurfaceXY().y : null;
|
|
myBlocks.push(items);
|
|
myBlocksByProcCode[txt] = items;
|
|
return items;
|
|
}
|
|
|
|
function getDescFromField(root) {
|
|
let fields = root.inputList[0];
|
|
let desc;
|
|
|
|
for (const fieldRow of fields.fieldRow) {
|
|
desc = (desc ? desc + " " : "") + fieldRow.getText();
|
|
}
|
|
|
|
return desc;
|
|
}
|
|
|
|
for (const root of topBlocks) {
|
|
if (root.type === "procedures_definition") {
|
|
const label = root.getChildren()[0];
|
|
const procCode = label.getProcCode();
|
|
|
|
if (!procCode) {
|
|
continue;
|
|
}
|
|
|
|
const indexOfLabel = root.inputList.findIndex(i => i.fieldRow.length > 0);
|
|
|
|
if (indexOfLabel === -1) {
|
|
continue;
|
|
}
|
|
|
|
const translatedDefine = root.inputList[indexOfLabel].fieldRow[0].getText();
|
|
const message = indexOfLabel === 0 ? "".concat(translatedDefine, " ").concat(procCode) : "".concat(procCode, " ").concat(translatedDefine);
|
|
addBlock("define", message, root);
|
|
continue;
|
|
}
|
|
|
|
if (root.type === "event_whenflagclicked") {
|
|
addBlock("flag", getDescFromField(root), root); // "When Flag Clicked"
|
|
|
|
continue;
|
|
}
|
|
|
|
if (root.type === "event_whenbroadcastreceived") {
|
|
const fieldRow = root.inputList[0].fieldRow;
|
|
let eventName = fieldRow.find(input => input.name === "BROADCAST_OPTION").getText();
|
|
addBlock("receive", "event " + eventName, root).eventName = eventName;
|
|
continue;
|
|
}
|
|
|
|
if (root.type.substr(0, 10) === "event_when") {
|
|
addBlock("event", getDescFromField(root), root); // "When Flag Clicked"
|
|
|
|
continue;
|
|
}
|
|
|
|
if (root.type === "control_start_as_clone") {
|
|
addBlock("event", getDescFromField(root), root); // "when I start as a clone"
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let map = this.workspace.getVariableMap();
|
|
let vars = map.getVariablesOfType("");
|
|
|
|
for (const row of vars) {
|
|
addBlock(row.isLocal ? "var" : "VAR", (row.isLocal ? "var " : "VAR ") + row.name, row);
|
|
}
|
|
|
|
let lists = map.getVariablesOfType("list");
|
|
|
|
for (const row of lists) {
|
|
addBlock(row.isLocal ? "list" : "LIST", (row.isLocal ? "list " : "LIST ") + row.name, row);
|
|
}
|
|
|
|
const events = this.getCallsToEvents();
|
|
|
|
for (const event of events) {
|
|
addBlock("receive", "event " + event.eventName, event.block).eventName = event.eventName;
|
|
}
|
|
|
|
const clsOrder = {
|
|
flag: 0,
|
|
receive: 1,
|
|
event: 2,
|
|
define: 3,
|
|
var: 4,
|
|
VAR: 5,
|
|
list: 6,
|
|
LIST: 7
|
|
};
|
|
myBlocks.sort((a, b) => {
|
|
let t = clsOrder[a.cls] - clsOrder[b.cls];
|
|
|
|
if (t !== 0) {
|
|
return t;
|
|
}
|
|
|
|
if (a.lower < b.lower) {
|
|
return -1;
|
|
}
|
|
|
|
if (a.lower > b.lower) {
|
|
return 1;
|
|
}
|
|
|
|
return a.y - b.y;
|
|
});
|
|
return myBlocks;
|
|
}
|
|
|
|
getScratchCostumes() {
|
|
let costumes = this.utils.getEditingTarget().getCostumes();
|
|
let items = [];
|
|
let i = 0;
|
|
|
|
for (const costume of costumes) {
|
|
let item = new _blockly_BlockItem_js__WEBPACK_IMPORTED_MODULE_0__["default"]("costume", costume.name, costume.assetId, i);
|
|
items.push(item);
|
|
i++;
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
getScratchSounds() {
|
|
let sounds = this.utils.getEditingTarget().getSounds();
|
|
let items = [];
|
|
let i = 0;
|
|
|
|
for (const sound of sounds) {
|
|
let item = new _blockly_BlockItem_js__WEBPACK_IMPORTED_MODULE_0__["default"]("sound", sound.name, sound.assetId, i);
|
|
items.push(item);
|
|
i++;
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
getCallsToEvents() {
|
|
const uses = [];
|
|
const alreadyFound = new Set();
|
|
|
|
for (const block of this.workspace.getAllBlocks()) {
|
|
if (block.type !== "event_broadcast" && block.type !== "event_broadcastandwait") {
|
|
continue;
|
|
}
|
|
|
|
const broadcastInput = block.getChildren()[0];
|
|
|
|
if (!broadcastInput) {
|
|
continue;
|
|
}
|
|
|
|
let eventName = "";
|
|
|
|
if (broadcastInput.type === "event_broadcast_menu") {
|
|
eventName = broadcastInput.inputList[0].fieldRow[0].getText();
|
|
} else {
|
|
eventName = msg("complex-broadcast");
|
|
}
|
|
|
|
if (!alreadyFound.has(eventName)) {
|
|
alreadyFound.add(eventName);
|
|
uses.push({
|
|
eventName: eventName,
|
|
block: block
|
|
});
|
|
}
|
|
}
|
|
|
|
return uses;
|
|
}
|
|
|
|
}
|
|
|
|
class Dropdown {
|
|
constructor(utils) {
|
|
this.utils = utils;
|
|
this.el = null;
|
|
this.items = [];
|
|
this.selected = null;
|
|
this.carousel = new Carousel(this.utils);
|
|
}
|
|
|
|
get workspace() {
|
|
return Blockly.getMainWorkspace();
|
|
}
|
|
|
|
createDom() {
|
|
this.el = document.createElement("ul");
|
|
this.el.className = "sa-find-dropdown";
|
|
return this.el;
|
|
}
|
|
|
|
inputKeyDown(e) {
|
|
// Up Arrow
|
|
if (e.keyCode === 38) {
|
|
this.navigateFilter(-1);
|
|
e.preventDefault();
|
|
return;
|
|
} // Down Arrow
|
|
|
|
|
|
if (e.keyCode === 40) {
|
|
this.navigateFilter(1);
|
|
e.preventDefault();
|
|
return;
|
|
} // Enter
|
|
|
|
|
|
if (e.keyCode === 13) {
|
|
// Any selected on enter? if not select now
|
|
if (this.selected) {
|
|
this.navigateFilter(1);
|
|
}
|
|
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
this.carousel.inputKeyDown(e);
|
|
}
|
|
|
|
navigateFilter(dir) {
|
|
let nxt;
|
|
|
|
if (this.selected && this.selected.style.display !== "none") {
|
|
nxt = dir === -1 ? this.selected.previousSibling : this.selected.nextSibling;
|
|
} else {
|
|
nxt = this.items[0];
|
|
dir = 1;
|
|
}
|
|
|
|
while (nxt && nxt.style.display === "none") {
|
|
nxt = dir === -1 ? nxt.previousSibling : nxt.nextSibling;
|
|
}
|
|
|
|
if (nxt) {
|
|
nxt.scrollIntoView({
|
|
block: "nearest"
|
|
});
|
|
this.onItemClick(nxt);
|
|
}
|
|
}
|
|
|
|
addItem(proc) {
|
|
const item = document.createElement("li");
|
|
item.innerText = proc.procCode;
|
|
item.data = proc;
|
|
const colorIds = {
|
|
receive: "events",
|
|
event: "events",
|
|
define: "more",
|
|
var: "data",
|
|
VAR: "data",
|
|
list: "data-lists",
|
|
LIST: "data-lists",
|
|
costume: "looks",
|
|
sound: "sounds"
|
|
};
|
|
|
|
if (proc.cls === "flag") {
|
|
item.className = "sa-find-flag";
|
|
} else {
|
|
const colorId = colorIds[proc.cls];
|
|
item.className = "sa-block-color sa-block-color-".concat(colorId);
|
|
}
|
|
|
|
item.addEventListener("mousedown", e => {
|
|
this.onItemClick(item);
|
|
e.preventDefault();
|
|
e.cancelBubble = true;
|
|
return false;
|
|
});
|
|
this.items.push(item);
|
|
this.el.appendChild(item);
|
|
return item;
|
|
}
|
|
|
|
onItemClick(item, instanceBlock) {
|
|
if (this.selected && this.selected !== item) {
|
|
this.selected.classList.remove("sel");
|
|
this.selected = null;
|
|
}
|
|
|
|
if (this.selected !== item) {
|
|
item.classList.add("sel");
|
|
this.selected = item;
|
|
}
|
|
|
|
let cls = item.data.cls;
|
|
|
|
if (cls === "costume" || cls === "sound") {
|
|
// Viewing costumes/sounds - jump to selected costume/sound
|
|
const assetPanel = document.querySelector("[class^=asset-panel_wrapper]");
|
|
|
|
if (assetPanel) {
|
|
const reactInstance = assetPanel[addon.tab.traps.getInternalKey(assetPanel)];
|
|
const reactProps = reactInstance.child.stateNode.props;
|
|
reactProps.onItemClick(item.data.y);
|
|
const selectorList = assetPanel.firstChild.firstChild;
|
|
selectorList.children[item.data.y].scrollIntoView({
|
|
behavior: "auto",
|
|
block: "center",
|
|
inline: "start"
|
|
}); // The wrapper seems to scroll when we use the function above.
|
|
|
|
let wrapper = assetPanel.closest("div[class*=gui_flex-wrapper]");
|
|
wrapper.scrollTop = 0;
|
|
}
|
|
} else if (cls === "var" || cls === "VAR" || cls === "list" || cls === "LIST") {
|
|
// Search now for all instances
|
|
let blocks = this.getVariableUsesById(item.data.labelID);
|
|
this.carousel.build(item, blocks, instanceBlock);
|
|
} else if (cls === "define") {
|
|
let blocks = this.getCallsToProcedureById(item.data.labelID);
|
|
this.carousel.build(item, blocks, instanceBlock);
|
|
} else if (cls === "receive") {
|
|
/*
|
|
let blocks = [this.workspace.getBlockById(li.data.labelID)];
|
|
if (li.data.clones) {
|
|
for (const cloneID of li.data.clones) {
|
|
blocks.push(this.workspace.getBlockById(cloneID))
|
|
}
|
|
}
|
|
blocks = blocks.concat(getCallsToEventsByName(li.data.eventName));
|
|
*/
|
|
// Now, fetch the events from the scratch runtime instead of blockly
|
|
let blocks = this.getCallsToEventsByName(item.data.eventName);
|
|
|
|
if (!instanceBlock) {
|
|
// Can we start by selecting the first block on 'this' sprite
|
|
const currentTargetID = this.utils.getEditingTarget().id;
|
|
|
|
for (const block of blocks) {
|
|
if (block.targetId === currentTargetID) {
|
|
instanceBlock = block;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.carousel.build(item, blocks, instanceBlock);
|
|
} else if (item.data.clones) {
|
|
let blocks = [this.workspace.getBlockById(item.data.labelID)];
|
|
|
|
for (const cloneID of item.data.clones) {
|
|
blocks.push(this.workspace.getBlockById(cloneID));
|
|
}
|
|
|
|
this.carousel.build(item, blocks, instanceBlock);
|
|
} else {
|
|
this.utils.scrollBlockIntoView(item.data.labelID);
|
|
this.carousel.remove();
|
|
}
|
|
}
|
|
|
|
getVariableUsesById(id) {
|
|
let uses = [];
|
|
let topBlocks = this.workspace.getTopBlocks();
|
|
|
|
for (const topBlock of topBlocks) {
|
|
/** @type {!Array<!Blockly.Block>} */
|
|
let kids = topBlock.getDescendants();
|
|
|
|
for (const block of kids) {
|
|
/** @type {!Array<!Blockly.VariableModel>} */
|
|
let blockVariables = block.getVarModels();
|
|
|
|
if (blockVariables) {
|
|
for (const blockVar of blockVariables) {
|
|
if (blockVar.getId() === id) {
|
|
uses.push(block);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return uses;
|
|
}
|
|
|
|
getCallsToProcedureById(id) {
|
|
let procBlock = this.workspace.getBlockById(id);
|
|
let label = procBlock.getChildren()[0];
|
|
let procCode = label.getProcCode();
|
|
let uses = [procBlock]; // Definition First, then calls to it
|
|
|
|
let topBlocks = this.workspace.getTopBlocks();
|
|
|
|
for (const topBlock of topBlocks) {
|
|
/** @type {!Array<!Blockly.Block>} */
|
|
let kids = topBlock.getDescendants();
|
|
|
|
for (const block of kids) {
|
|
if (block.type === "procedures_call") {
|
|
if (block.getProcCode() === procCode) {
|
|
uses.push(block);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return uses;
|
|
}
|
|
|
|
getCallsToEventsByName(name) {
|
|
let uses = []; // Definition First, then calls to it
|
|
|
|
const runtime = addon.tab.traps.vm.runtime;
|
|
const targets = runtime.targets; // The sprites / stage
|
|
|
|
for (const target of targets) {
|
|
if (!target.isOriginal) {
|
|
continue; // Skip clones
|
|
}
|
|
|
|
const blocks = target.blocks;
|
|
|
|
if (!blocks._blocks) {
|
|
continue;
|
|
}
|
|
|
|
for (const id of Object.keys(blocks._blocks)) {
|
|
const block = blocks._blocks[id];
|
|
|
|
if (block.opcode === "event_whenbroadcastreceived" && block.fields.BROADCAST_OPTION.value === name) {
|
|
uses.push(new _blockly_BlockInstance_js__WEBPACK_IMPORTED_MODULE_1__["default"](target, block));
|
|
} else if (block.opcode === "event_broadcast" || block.opcode === "event_broadcastandwait") {
|
|
const broadcastInputBlockId = block.inputs.BROADCAST_INPUT.block;
|
|
const broadcastInputBlock = blocks._blocks[broadcastInputBlockId];
|
|
|
|
if (broadcastInputBlock) {
|
|
let eventName;
|
|
|
|
if (broadcastInputBlock.opcode === "event_broadcast_menu") {
|
|
eventName = broadcastInputBlock.fields.BROADCAST_OPTION.value;
|
|
} else {
|
|
eventName = msg("complex-broadcast");
|
|
}
|
|
|
|
if (eventName === name) {
|
|
uses.push(new _blockly_BlockInstance_js__WEBPACK_IMPORTED_MODULE_1__["default"](target, block));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return uses;
|
|
}
|
|
|
|
empty() {
|
|
for (const item of this.items) {
|
|
if (this.el.contains(item)) {
|
|
this.el.removeChild(item);
|
|
}
|
|
}
|
|
|
|
this.items = [];
|
|
this.selected = null;
|
|
}
|
|
|
|
}
|
|
|
|
class Carousel {
|
|
constructor(utils) {
|
|
this.utils = utils;
|
|
this.el = null;
|
|
this.count = null;
|
|
this.blocks = [];
|
|
this.idx = 0;
|
|
}
|
|
|
|
build(item, blocks, instanceBlock) {
|
|
if (this.el && this.el.parentNode === item) {
|
|
// Same control... click again to go to next
|
|
this.navRight();
|
|
} else {
|
|
this.remove();
|
|
this.blocks = blocks;
|
|
item.appendChild(this.createDom());
|
|
this.idx = 0;
|
|
|
|
if (instanceBlock) {
|
|
for (const idx of Object.keys(this.blocks)) {
|
|
const block = this.blocks[idx];
|
|
|
|
if (block.id === instanceBlock.id) {
|
|
this.idx = Number(idx);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.idx < this.blocks.length) {
|
|
this.utils.scrollBlockIntoView(this.blocks[this.idx]);
|
|
}
|
|
}
|
|
}
|
|
|
|
createDom() {
|
|
this.el = document.createElement("span");
|
|
this.el.className = "sa-find-carousel";
|
|
const leftControl = this.el.appendChild(document.createElement("span"));
|
|
leftControl.className = "sa-find-carousel-control";
|
|
leftControl.textContent = "◀";
|
|
leftControl.addEventListener("mousedown", e => this.navLeft(e));
|
|
this.count = this.el.appendChild(document.createElement("span"));
|
|
this.count.innerText = this.blocks.length > 0 ? this.idx + 1 + " / " + this.blocks.length : "0";
|
|
const rightControl = this.el.appendChild(document.createElement("span"));
|
|
rightControl.className = "sa-find-carousel-control";
|
|
rightControl.textContent = "▶";
|
|
rightControl.addEventListener("mousedown", e => this.navRight(e));
|
|
return this.el;
|
|
}
|
|
|
|
inputKeyDown(e) {
|
|
// Left Arrow
|
|
if (e.keyCode === 37) {
|
|
if (this.el && this.blocks) {
|
|
this.navLeft(e);
|
|
}
|
|
} // Right Arrow
|
|
|
|
|
|
if (e.keyCode === 39) {
|
|
if (this.el && this.blocks) {
|
|
this.navRight(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
navLeft(e) {
|
|
return this.navSideways(e, -1);
|
|
}
|
|
|
|
navRight(e) {
|
|
return this.navSideways(e, 1);
|
|
}
|
|
|
|
navSideways(e, dir) {
|
|
if (this.blocks.length > 0) {
|
|
this.idx = (this.idx + dir + this.blocks.length) % this.blocks.length; // + length to fix negative modulo js issue.
|
|
|
|
this.count.innerText = this.idx + 1 + " / " + this.blocks.length;
|
|
this.utils.scrollBlockIntoView(this.blocks[this.idx]);
|
|
}
|
|
|
|
if (e) {
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
remove() {
|
|
if (this.el) {
|
|
this.el.remove();
|
|
this.blocks = [];
|
|
this.idx = 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const findBar = new FindBar();
|
|
const _doBlockClick_ = Blockly.Gesture.prototype.doBlockClick_;
|
|
|
|
Blockly.Gesture.prototype.doBlockClick_ = function () {
|
|
if (!addon.self.disabled && (this.mostRecentEvent_.button === 1 || this.mostRecentEvent_.shiftKey)) {
|
|
// Wheel button...
|
|
// Intercept clicks to allow jump to...?
|
|
let block = this.startBlock_;
|
|
|
|
for (; block; block = block.getSurroundParent()) {
|
|
if (block.type === "procedures_definition" || !this.jumpToDef && block.type === "procedures_call") {
|
|
let id = block.id ? block.id : block.getId ? block.getId() : null;
|
|
findBar.findInput.focus();
|
|
findBar.showDropDown(id);
|
|
return;
|
|
}
|
|
|
|
if (block.type === "data_variable" || block.type === "data_changevariableby" || block.type === "data_setvariableto") {
|
|
let id = block.getVars()[0];
|
|
findBar.findInput.focus();
|
|
findBar.showDropDown(id, block);
|
|
findBar.selVarID = id;
|
|
return;
|
|
}
|
|
|
|
if (block.type === "event_whenbroadcastreceived" || block.type === "event_broadcastandwait" || block.type === "event_broadcast") {
|
|
// todo: actually index the broadcasts...!
|
|
let id = block.id;
|
|
findBar.findInput.focus();
|
|
findBar.showDropDown(id, block);
|
|
findBar.selVarID = id;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
_doBlockClick_.call(this);
|
|
};
|
|
|
|
addon.tab.redux.initialize();
|
|
addon.tab.redux.addEventListener("statechanged", e => {
|
|
if (e.detail.action.type === "scratch-gui/navigation/ACTIVATE_TAB") {
|
|
findBar.tabChanged();
|
|
}
|
|
});
|
|
|
|
while (true) {
|
|
const root = await addon.tab.waitForElement("ul[class*=gui_tab-list_]", {
|
|
markAsSeen: true,
|
|
reduxEvents: ["scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED", "scratch-gui/locales/SELECT_LOCALE"],
|
|
reduxCondition: state => !state.scratchGui.mode.isPlayerOnly
|
|
});
|
|
findBar.createDom(root);
|
|
}
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/folders/_runtime_entry.js":
|
|
/*!*****************************************************!*\
|
|
!*** ./src/addons/addons/folders/_runtime_entry.js ***!
|
|
\*****************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/folders/userscript.js");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./style.css */ "./node_modules/css-loader/index.js!./src/addons/addons/folders/style.css");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var _url_loader_folder_svg__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! url-loader!./folder.svg */ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/folders/folder.svg");
|
|
/* generated by pull.js */
|
|
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"style.css": _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default.a,
|
|
"folder.svg": _url_loader_folder_svg__WEBPACK_IMPORTED_MODULE_2__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/folders/userscript.js":
|
|
/*!*************************************************!*\
|
|
!*** ./src/addons/addons/folders/userscript.js ***!
|
|
\*************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
|
|
|
|
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
|
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console,
|
|
msg
|
|
}) {
|
|
// The basic premise of how this addon works is relative simple.
|
|
// scratch-gui renders the sprite selectors and asset selectors in a hierarchy like this:
|
|
// <SelectorHOC>
|
|
// <SpriteSelectorItem />
|
|
// <SpriteSelectorItem />
|
|
// <SpriteSelectorItem />
|
|
// <SpriteSelectorItem />
|
|
// ...
|
|
// </SelectorHOC>
|
|
// It's obviously more complicated than that, but there are two important parts:
|
|
// SelectorHOC - We override this to change which items are displayed
|
|
// SpriteSelectorItem - We override this to change how items are displayed.
|
|
// Folders are just items rendered differently
|
|
// These two components communicate through the `name` property of the items.
|
|
// We touch some things on the VM to make dragging items work properly.
|
|
const REACT_INTERNAL_PREFIX = "__reactInternalInstance$";
|
|
const TYPE_SPRITES = 1;
|
|
const TYPE_ASSETS = 2; // We run too early, will be set later
|
|
|
|
let vm;
|
|
let reactInternalKey;
|
|
let currentSpriteFolder;
|
|
let currentAssetFolder;
|
|
let currentSpriteItems;
|
|
let currentAssetItems;
|
|
const DIVIDER = "//";
|
|
/**
|
|
* getFolderFromName("B") === null
|
|
* getFolderFromName("A//b") === "A"
|
|
*/
|
|
|
|
const getFolderFromName = name => {
|
|
const idx = name.indexOf(DIVIDER);
|
|
|
|
if (idx === -1 || idx === 0) {
|
|
return null;
|
|
}
|
|
|
|
return name.substr(0, idx);
|
|
};
|
|
/**
|
|
* getNameWithoutFolder("B") === "B"
|
|
* getNameWithoutFolder("A//b") === "b"
|
|
*/
|
|
|
|
|
|
const getNameWithoutFolder = name => {
|
|
const idx = name.indexOf(DIVIDER);
|
|
|
|
if (idx === -1 || idx === 0) {
|
|
return name;
|
|
}
|
|
|
|
return name.substr(idx + DIVIDER.length);
|
|
};
|
|
/**
|
|
* setFolderOfName("B", "y") === "y//B"
|
|
* setFolderOfName("c//B", "y") === "y//B"
|
|
* setFolderOfName("B", null) === "B"
|
|
* setFolderOfName("c//B", null) === "B"
|
|
*/
|
|
|
|
|
|
const setFolderOfName = (name, folder) => {
|
|
const basename = getNameWithoutFolder(name);
|
|
|
|
if (folder) {
|
|
return "".concat(folder).concat(DIVIDER).concat(basename);
|
|
}
|
|
|
|
return basename;
|
|
};
|
|
|
|
const isValidFolderName = name => {
|
|
return !name.includes(DIVIDER) && !name.endsWith("/");
|
|
};
|
|
|
|
const RESERVED_NAMES = ["_mouse_", "_stage_", "_edge_", "_myself_", "_random_"];
|
|
|
|
const ensureNotReserved = name => {
|
|
if (name === "") return "2";
|
|
if (RESERVED_NAMES.includes(name)) return "".concat(name, "2");
|
|
return name;
|
|
};
|
|
|
|
const getSortableHOCFromElement = el => {
|
|
const nearestSpriteSelector = el.closest("[class*='sprite-selector_sprite-selector']");
|
|
|
|
if (nearestSpriteSelector) {
|
|
return nearestSpriteSelector[reactInternalKey].child.sibling.child.stateNode;
|
|
}
|
|
|
|
const nearestAssetPanelWrapper = el.closest('[class*="asset-panel_wrapper"]');
|
|
|
|
if (nearestAssetPanelWrapper) {
|
|
return nearestAssetPanelWrapper[reactInternalKey].child.child.stateNode;
|
|
}
|
|
|
|
throw new Error("cannot find SortableHOC");
|
|
};
|
|
|
|
const getBackpackFromElement = el => {
|
|
const gui = el.closest('[class*="gui_editor-wrapper"]');
|
|
if (!gui) throw new Error("cannot find Backpack");
|
|
return gui[reactInternalKey].child.sibling.child.child.stateNode;
|
|
};
|
|
|
|
const clamp = (n, min, max) => {
|
|
return Math.min(Math.max(n, min), max);
|
|
};
|
|
/**
|
|
* @typedef {Object} ItemData
|
|
* @property {string} realName
|
|
* @property {number} realIndex
|
|
* @property {string} inFolder
|
|
* @property {string} folder
|
|
* @property {boolean} folderOpen
|
|
*/
|
|
|
|
/**
|
|
* @returns {ItemData|null}
|
|
*/
|
|
|
|
|
|
const getItemData = item => {
|
|
if (item && item.name && typeof item.name === "object") {
|
|
return item.name;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const openFolderAsset = {
|
|
assetId: "&__sa_folders_folder",
|
|
|
|
encodeDataURI() {
|
|
// Doesn't actually need to be a data: URI
|
|
return addon.self.getResource("/folder.svg")
|
|
/* rewritten by pull.js */
|
|
;
|
|
}
|
|
|
|
}; // https://github.com/LLK/scratch-gui/blob/develop/src/components/asset-panel/icon--sound.svg
|
|
|
|
const imageIconSource = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg width=\"100px\" height=\"100px\" viewBox=\"0 0 20 20\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n <g id=\"Sound\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\">\n <path d=\"M12.4785058,12.6666667 C12.3144947,12.6666667 12.1458852,12.6272044 11.9926038,12.5440517 C11.537358,12.2960031 11.3856094,11.7562156 11.6553847,11.3376335 C12.1688774,10.5371131 12.1688774,9.54491867 11.6553847,8.74580756 C11.3856094,8.32581618 11.537358,7.78602861 11.9926038,7.53798001 C12.452448,7.29275014 13.0379829,7.43086811 13.3046926,7.84804076 C14.1737981,9.20103311 14.1737981,10.8809986 13.3046926,12.233991 C13.1268862,12.5130457 12.806528,12.6666667 12.4785058,12.6666667 Z M15.3806784,13.8333333 C15.2408902,13.8333333 15.0958763,13.796281 14.9665396,13.7182064 C14.5785295,13.485306 14.4491928,12.9784829 14.6791247,12.5854634 C15.5949331,11.0160321 15.5949331,9.065491 14.6791247,7.49738299 C14.4491928,7.10436352 14.5785295,6.59621712 14.9665396,6.36331669 C15.3558562,6.13438616 15.8549129,6.26274605 16.0848448,6.65444223 C17.3050517,8.74260632 17.3050517,11.3389168 16.0848448,13.4270809 C15.9319924,13.6890939 15.6602547,13.8333333 15.3806784,13.8333333 Z M10.3043478,5.62501557 L10.3043478,13.873675 C10.3043478,14.850934 9.10969849,15.3625101 8.36478311,14.7038052 L6.7566013,13.2797607 C6.18712394,12.7762834 5.44499329,12.4968737 4.67362297,12.4968737 L4.3923652,12.4968737 C3.62377961,12.4968737 3,11.8935108 3,11.1470686 L3,8.36646989 C3,7.62137743 3.62377961,7.01666471 4.3923652,7.01666471 L4.65830695,7.01666471 C5.42967727,7.01666471 6.17180792,6.73725504 6.74128529,6.23377771 L8.36478311,4.79623519 C9.10969849,4.13753026 10.3043478,4.64910643 10.3043478,5.62501557 Z\" id=\"Combined-Shape\" fill=\"#575E75\"></path>\n </g>\n</svg>";
|
|
const soundIconHref = "data:image/svg+xml;base64,".concat(btoa(imageIconSource));
|
|
let folderColorStylesheet = null;
|
|
const folderColors = Object.create(null);
|
|
|
|
const getFolderColorClass = folderName => {
|
|
const mulberry32 = a => {
|
|
// https://stackoverflow.com/a/47593316
|
|
return function () {
|
|
var t = a += 0x6d2b79f5;
|
|
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
};
|
|
};
|
|
|
|
const hashCode = str => {
|
|
// Based on Java's String.hashCode
|
|
// https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/lang/String.java#l1452
|
|
let hash = 0;
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = 31 * hash + str.charCodeAt(i);
|
|
hash = hash | 0;
|
|
}
|
|
|
|
return hash;
|
|
};
|
|
|
|
const random = str => {
|
|
const seed = hashCode(str);
|
|
const rng = mulberry32(seed); // Run RNG a few times to get more random numbers, otherwise similar seeds tend to give somewhat similar results
|
|
|
|
rng();
|
|
rng();
|
|
rng();
|
|
rng();
|
|
return rng();
|
|
};
|
|
|
|
if (!folderColors[folderName]) {
|
|
if (!folderColorStylesheet) {
|
|
folderColorStylesheet = document.createElement("style");
|
|
document.head.appendChild(folderColorStylesheet);
|
|
}
|
|
|
|
const hue = random(folderName) * 360;
|
|
const color = "hsla(".concat(hue, "deg, 100%, 85%, 0.5)");
|
|
const id = Object.keys(folderColors).length;
|
|
const className = "sa-folders-color-".concat(id);
|
|
folderColors[folderName] = className;
|
|
folderColorStylesheet.textContent += ".".concat(className, "{background-color:").concat(color, " !important;}");
|
|
folderColorStylesheet.textContent += ".".concat(className, "[class*=\"sprite-selector_raised\"]:not([class*=\"sa-folders-folder\"]){background-color:hsla(").concat(hue, "deg, 100%, 77%, 1) !important;}");
|
|
}
|
|
|
|
return folderColors[folderName];
|
|
};
|
|
|
|
const fixOrderOfItemsInFolders = items => {
|
|
const folders = Object.create(null);
|
|
const result = [];
|
|
|
|
for (const item of items) {
|
|
const name = item.getName ? item.getName() : item.name;
|
|
const folder = getFolderFromName(name);
|
|
|
|
if (typeof folder === "string") {
|
|
if (!folders[folder]) {
|
|
folders[folder] = [];
|
|
result.push(folders[folder]);
|
|
}
|
|
|
|
folders[folder].push(item);
|
|
} else {
|
|
result.push(item);
|
|
}
|
|
}
|
|
|
|
const flatResult = result.flat();
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
if (result[i] !== items[i]) {
|
|
return {
|
|
items: flatResult,
|
|
changed: true
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
items: flatResult,
|
|
changed: false
|
|
};
|
|
};
|
|
|
|
const fixTargetOrder = () => {
|
|
const {
|
|
items,
|
|
changed
|
|
} = fixOrderOfItemsInFolders(vm.runtime.targets);
|
|
|
|
if (changed) {
|
|
vm.runtime.targets = items;
|
|
vm.emitTargetsUpdate();
|
|
}
|
|
};
|
|
|
|
const fixCostumeOrder = (target = vm.editingTarget) => {
|
|
const {
|
|
items,
|
|
changed
|
|
} = fixOrderOfItemsInFolders(target.sprite.costumes);
|
|
|
|
if (changed) {
|
|
target.sprite.costumes = items;
|
|
vm.emitTargetsUpdate();
|
|
}
|
|
};
|
|
|
|
const fixSoundOrder = (target = vm.editingTarget) => {
|
|
const {
|
|
items,
|
|
changed
|
|
} = fixOrderOfItemsInFolders(target.sprite.sounds);
|
|
|
|
if (changed) {
|
|
target.sprite.sounds = items;
|
|
vm.emitTargetsUpdate();
|
|
}
|
|
};
|
|
|
|
const verifySortableHOC = sortableHOCInstance => {
|
|
const SortableHOC = sortableHOCInstance.constructor;
|
|
if (Array.isArray(sortableHOCInstance.props.items) && (typeof sortableHOCInstance.props.selectedId === "string" || typeof sortableHOCInstance.props.selectedItemIndex === "number") && typeof sortableHOCInstance.containerBox !== "undefined" && typeof SortableHOC.prototype.componentDidMount === "undefined" && typeof SortableHOC.prototype.componentDidUpdate === "undefined" && typeof SortableHOC.prototype.handleAddSortable === "function" && typeof SortableHOC.prototype.handleRemoveSortable === "function" && typeof SortableHOC.prototype.setRef === "function") return;
|
|
throw new Error("Can not comprehend SortableHOC");
|
|
};
|
|
|
|
const verifySpriteSelectorItem = spriteSelectorItemInstance => {
|
|
const SpriteSelectorItem = spriteSelectorItemInstance.constructor;
|
|
if (typeof spriteSelectorItemInstance.props.asset === "object" && typeof spriteSelectorItemInstance.props.name === "string" && typeof spriteSelectorItemInstance.props.dragType === "string" && typeof SpriteSelectorItem.prototype.handleClick === "function" && typeof SpriteSelectorItem.prototype.setRef === "function" && typeof SpriteSelectorItem.prototype.handleDrag === "function" && typeof SpriteSelectorItem.prototype.handleDragEnd === "function" && typeof SpriteSelectorItem.prototype.handleDelete === "function" && typeof SpriteSelectorItem.prototype.handleDuplicate === "function" && typeof SpriteSelectorItem.prototype.handleExport === "function") return;
|
|
throw new Error("Can not comprehend SpriteSelectorItem");
|
|
};
|
|
|
|
const verifyVM = vm => {
|
|
const target = vm.runtime.targets[0];
|
|
if (typeof vm.installTargets === "function" && typeof vm.reorderTarget === "function" && typeof target.reorderCostume === "function" && typeof target.reorderSound === "function" && typeof target.addCostume === "function" && typeof target.addSound === "function") return;
|
|
throw new Error("Can not comprehend VM");
|
|
};
|
|
|
|
const verifyBackpack = backpackInstance => {
|
|
const Backpack = backpackInstance.constructor;
|
|
|
|
if (typeof Backpack.prototype.handleDrop === "function" && typeof Backpack.prototype.componentDidUpdate === "undefined") {
|
|
return;
|
|
}
|
|
|
|
throw new Error("Can not comprehend Backpack");
|
|
};
|
|
|
|
class Cache {
|
|
constructor() {
|
|
this.cache = new Map();
|
|
this.usedThisTick = new Set();
|
|
}
|
|
|
|
has(id) {
|
|
return this.cache.has(id);
|
|
}
|
|
|
|
get(id) {
|
|
this.usedThisTick.add(id);
|
|
return this.cache.get(id);
|
|
}
|
|
|
|
set(id, value) {
|
|
this.usedThisTick.add(id);
|
|
this.cache.set(id, value);
|
|
}
|
|
|
|
startTick() {
|
|
this.usedThisTick.clear();
|
|
}
|
|
|
|
endTick() {
|
|
for (const id of Array.from(this.cache.keys())) {
|
|
if (!this.usedThisTick.has(id)) {
|
|
this.cache.delete(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.usedThisTick.clear();
|
|
this.cache.clear();
|
|
}
|
|
|
|
}
|
|
|
|
const patchSortableHOC = (SortableHOC, type) => {
|
|
// SortableHOC should be: https://github.com/LLK/scratch-gui/blob/29d9851778febe4e69fa5111bf7559160611e366/src/lib/sortable-hoc.jsx#L8
|
|
const itemCache = new Cache();
|
|
const folderItemCache = new Cache();
|
|
const folderAssetCache = new Cache();
|
|
const PREVIEW_SIZE = 80;
|
|
const PREVIEW_POSITIONS = [// x, y
|
|
[0, 0], [PREVIEW_SIZE / 2, 0], [0, PREVIEW_SIZE / 2], [PREVIEW_SIZE / 2, PREVIEW_SIZE / 2]];
|
|
|
|
const createFolderPreview = items => {
|
|
// Directly generate a string instead of using DOM API for performance as we deal with very large inlined images
|
|
// Because the result is only used as an img src, XSS shouldn't be a concern
|
|
let result = "data:image/svg+xml;,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"".concat(PREVIEW_SIZE, "\" height=\"").concat(PREVIEW_SIZE, "\">");
|
|
|
|
for (let i = 0; i < Math.min(PREVIEW_POSITIONS.length, items.length); i++) {
|
|
const item = items[i];
|
|
const width = PREVIEW_SIZE / 2;
|
|
const height = PREVIEW_SIZE / 2;
|
|
const [x, y] = PREVIEW_POSITIONS[i];
|
|
let src;
|
|
|
|
if (item.asset) {
|
|
// TW: We can be 100% certain that escaping here is unnecessary
|
|
src = item.asset.encodeDataURI();
|
|
} else if (item.costume && item.costume.asset) {
|
|
src = item.costume.asset.encodeDataURI();
|
|
} else if (item.url) {
|
|
src = soundIconHref;
|
|
}
|
|
|
|
if (src) {
|
|
result += "<image width=\"".concat(width, "\" height=\"").concat(height, "\" x=\"").concat(x, "\" y=\"").concat(y, "\" href=\"").concat(src, "\"/>");
|
|
}
|
|
}
|
|
|
|
result += "</svg>";
|
|
return result;
|
|
};
|
|
|
|
const getUniqueIdOfFolderItems = items => {
|
|
let id = "sa_folder&&";
|
|
|
|
for (let i = 0; i < Math.min(PREVIEW_POSITIONS.length, items.length); i++) {
|
|
const item = items[i];
|
|
|
|
if (item.asset) {
|
|
id += item.asset.assetId;
|
|
} else if (item.costume && item.costume.asset) {
|
|
id += item.costume.asset.assetId;
|
|
} else if (item.url) {
|
|
id += item.url;
|
|
}
|
|
|
|
id += "&&";
|
|
}
|
|
|
|
return id;
|
|
};
|
|
|
|
const processItems = (openFolders, props) => {
|
|
const processItem = item => {
|
|
const itemId = item.name;
|
|
let newItem;
|
|
let itemData;
|
|
|
|
if (itemCache.has(itemId)) {
|
|
newItem = itemCache.get(itemId);
|
|
itemData = newItem.name;
|
|
} else {
|
|
itemData = {
|
|
toString() {
|
|
return "_".concat(item.name);
|
|
}
|
|
|
|
};
|
|
newItem = {};
|
|
itemCache.set(itemId, newItem);
|
|
}
|
|
|
|
const itemFolderName = getFolderFromName(item.name);
|
|
Object.assign(newItem, item);
|
|
itemData.realName = item.name;
|
|
itemData.realIndex = i;
|
|
itemData.inFolder = itemFolderName;
|
|
newItem.name = itemData;
|
|
return {
|
|
newItem,
|
|
itemData
|
|
};
|
|
};
|
|
|
|
itemCache.startTick();
|
|
folderItemCache.startTick();
|
|
folderAssetCache.startTick();
|
|
const folderOccurrences = new Map();
|
|
const items = [];
|
|
const result = {
|
|
items
|
|
};
|
|
let i = 0;
|
|
|
|
while (i < props.items.length) {
|
|
const item = props.items[i];
|
|
const folderName = getFolderFromName(item.name);
|
|
|
|
if (folderName === null) {
|
|
items.push(processItem(item).newItem);
|
|
|
|
if (type === TYPE_ASSETS) {
|
|
const isSelected = props.selectedItemIndex === i;
|
|
|
|
if (isSelected) {
|
|
result.selectedItemIndex = items.length - 1;
|
|
}
|
|
}
|
|
} else {
|
|
const isOpen = openFolders.indexOf(folderName) !== -1;
|
|
const folderItems = [];
|
|
|
|
while (i < props.items.length) {
|
|
const childItem = props.items[i];
|
|
const processedItem = processItem(childItem);
|
|
|
|
if (getFolderFromName(childItem.name) !== folderName) {
|
|
break;
|
|
}
|
|
|
|
folderItems.push(processedItem.newItem);
|
|
|
|
if (type === TYPE_ASSETS) {
|
|
const isSelected = props.selectedItemIndex === i;
|
|
|
|
if (isSelected) {
|
|
if (isOpen) {
|
|
result.selectedItemIndex = items.length + folderItems.length;
|
|
} else {
|
|
result.selectedItemIndex = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
i--;
|
|
const occurrence = folderOccurrences.get(folderName) || 0;
|
|
folderOccurrences.set(folderName, occurrence + 1);
|
|
const baseUniqueId = getUniqueIdOfFolderItems(folderItems);
|
|
const itemUniqueId = "".concat(isOpen, "&").concat(occurrence, "&").concat(folderName, "&").concat(baseUniqueId, "&");
|
|
const reactKey = "&__".concat(occurrence, "_").concat(folderName);
|
|
const assetUniqueId = baseUniqueId;
|
|
let folderItem;
|
|
let folderData;
|
|
|
|
if (folderItemCache.has(itemUniqueId)) {
|
|
folderItem = folderItemCache.get(itemUniqueId);
|
|
folderData = folderItem.name;
|
|
} else {
|
|
folderItem = {
|
|
// Can be used as a react key
|
|
id: {
|
|
toString() {
|
|
return reactKey;
|
|
}
|
|
|
|
}
|
|
};
|
|
folderData = {
|
|
// Can be used as a react key
|
|
toString() {
|
|
return reactKey;
|
|
}
|
|
|
|
};
|
|
folderItemCache.set(itemUniqueId, folderItem);
|
|
}
|
|
|
|
folderData.folder = folderName;
|
|
folderData.folderOpen = isOpen;
|
|
folderItem.items = folderItems;
|
|
folderItem.name = folderData;
|
|
let folderAsset;
|
|
|
|
if (isOpen) {
|
|
folderAsset = openFolderAsset;
|
|
} else {
|
|
if (folderAssetCache.has(assetUniqueId)) {
|
|
folderAsset = folderAssetCache.get(assetUniqueId);
|
|
} else {
|
|
folderAsset = {
|
|
assetId: assetUniqueId,
|
|
|
|
encodeDataURI() {
|
|
return createFolderPreview(folderItems);
|
|
}
|
|
|
|
};
|
|
folderAssetCache.set(assetUniqueId, folderAsset);
|
|
}
|
|
}
|
|
|
|
if (type === TYPE_SPRITES) {
|
|
if (!folderItem.costume) folderItem.costume = {};
|
|
folderItem.costume.asset = folderAsset; // For sprite items, `id` is used as the drag payload and toString is used as a React key
|
|
|
|
if (!folderItem.id) folderItem.id = {};
|
|
folderItem.id.sa_folder_items = folderItems;
|
|
|
|
folderItem.id.toString = () => reactKey;
|
|
} else {
|
|
folderItem.asset = folderAsset;
|
|
if (!folderItem.dragPayload) folderItem.dragPayload = {};
|
|
folderItem.dragPayload.sa_folder_items = folderItems;
|
|
}
|
|
|
|
items.push(folderItem);
|
|
|
|
if (isOpen) {
|
|
for (const item of folderItems) {
|
|
items.push(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
itemCache.endTick();
|
|
folderItemCache.endTick();
|
|
folderAssetCache.endTick();
|
|
return result;
|
|
};
|
|
|
|
const getSelectedItem = sortable => {
|
|
if (type === TYPE_SPRITES) {
|
|
const selectedItem = sortable.props.items.find(i => i.id === sortable.props.selectedId);
|
|
return selectedItem;
|
|
} else if (type === TYPE_ASSETS) {
|
|
const selectedItem = sortable.props.items[sortable.props.selectedItemIndex];
|
|
return selectedItem;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
SortableHOC.prototype.saInitialSetup = function () {
|
|
itemCache.clear();
|
|
folderItemCache.clear();
|
|
folderAssetCache.clear();
|
|
const folders = [];
|
|
const selectedItem = getSelectedItem(this);
|
|
|
|
if (selectedItem && !selectedItem.isStage) {
|
|
const folder = getFolderFromName(selectedItem.name);
|
|
folders.push(folder);
|
|
|
|
if (type === TYPE_SPRITES) {
|
|
currentSpriteFolder = folder;
|
|
} else if (type === TYPE_ASSETS) {
|
|
currentAssetFolder = folder;
|
|
}
|
|
}
|
|
|
|
this.setState({
|
|
folders
|
|
});
|
|
};
|
|
|
|
SortableHOC.prototype.componentDidMount = function () {
|
|
// Do part of componentDidUpdate on mount as well
|
|
const selectedItem = getSelectedItem(this);
|
|
|
|
if (selectedItem) {
|
|
const folder = getFolderFromName(selectedItem.name);
|
|
|
|
if (type === TYPE_SPRITES) {
|
|
currentSpriteFolder = folder;
|
|
} else if (type === TYPE_ASSETS) {
|
|
currentAssetFolder = folder;
|
|
}
|
|
}
|
|
|
|
this.saInitialSetup();
|
|
};
|
|
|
|
SortableHOC.prototype.componentDidUpdate = function (prevProps, prevState) {
|
|
const selectedItem = getSelectedItem(this);
|
|
|
|
if (selectedItem) {
|
|
const folder = getFolderFromName(selectedItem.name);
|
|
const currentFolder = this.state.folders.includes(folder) ? folder : null;
|
|
|
|
if (type === TYPE_SPRITES) {
|
|
currentSpriteFolder = currentFolder;
|
|
} else if (type === TYPE_ASSETS) {
|
|
currentAssetFolder = currentFolder;
|
|
}
|
|
|
|
let selectedItemChanged;
|
|
|
|
if (this.props.selectedId) {
|
|
selectedItemChanged = this.props.selectedId !== prevProps.selectedId;
|
|
} else {
|
|
selectedItemChanged = this.props.items[this.props.selectedItemIndex] && prevProps.items[prevProps.selectedItemIndex] && this.props.items[this.props.selectedItemIndex].name !== prevProps.items[prevProps.selectedItemIndex].name;
|
|
}
|
|
|
|
if (selectedItemChanged) {
|
|
if (!selectedItem.isStage) {
|
|
if (typeof folder === "string" && !this.state.folders.includes(folder)) {
|
|
this.setState(prevState => ({
|
|
folders: [...prevState.folders, folder]
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const originalSortableHOCRender = SortableHOC.prototype.render;
|
|
|
|
SortableHOC.prototype.render = function () {
|
|
const originalProps = this.props;
|
|
this.props = _objectSpread(_objectSpread({}, this.props), processItems(this.state && this.state.folders || [], this.props));
|
|
|
|
if (type === TYPE_SPRITES) {
|
|
currentSpriteItems = this.props.items;
|
|
} else if (type === TYPE_ASSETS) {
|
|
currentAssetItems = this.props.items;
|
|
}
|
|
|
|
const result = originalSortableHOCRender.call(this);
|
|
this.props = originalProps;
|
|
return result;
|
|
};
|
|
};
|
|
|
|
const getAllFolders = component => {
|
|
const result = new Set();
|
|
let items;
|
|
|
|
if (component.props.dragType === "SPRITE") {
|
|
items = currentSpriteItems;
|
|
} else {
|
|
items = currentAssetItems;
|
|
}
|
|
|
|
for (const item of items) {
|
|
const data = getItemData(item);
|
|
|
|
if (typeof data.folder === "string") {
|
|
result.add(data.folder);
|
|
}
|
|
}
|
|
|
|
return Array.from(result);
|
|
};
|
|
|
|
const isFolderOpen = (component, folder) => {
|
|
const sortableHOCInstance = getSortableHOCFromElement(component.ref);
|
|
const folders = sortableHOCInstance.state && sortableHOCInstance.state.folders || [];
|
|
return folders.includes(folder);
|
|
};
|
|
|
|
const setFolderOpen = (component, folder, open) => {
|
|
const sortableHOCInstance = getSortableHOCFromElement(component.ref);
|
|
sortableHOCInstance.setState(prevState => {
|
|
let folders = prevState && prevState.folders || [];
|
|
folders = folders.filter(i => i !== folder);
|
|
|
|
if (open) {
|
|
return {
|
|
folders: [...folders, folder]
|
|
};
|
|
}
|
|
|
|
return {
|
|
folders
|
|
};
|
|
});
|
|
};
|
|
|
|
addon.tab.createEditorContextMenu((ctxType, ctx) => {
|
|
if (ctxType !== "sprite" && ctxType !== "costume" && ctxType !== "sound") return;
|
|
const component = ctx.target[addon.tab.traps.getInternalKey(ctx.target)].return.return.return.stateNode;
|
|
const data = getItemData(component.props);
|
|
if (!data) return;
|
|
|
|
if (typeof data.folder === "string") {
|
|
ctx.target.setAttribute("sa-folders-context-type", "folder");
|
|
|
|
const renameItems = newName => {
|
|
const isOpen = isFolderOpen(component, data.folder);
|
|
setFolderOpen(component, data.folder, false);
|
|
|
|
if (isOpen && typeof newName === "string") {
|
|
setFolderOpen(component, newName, true);
|
|
}
|
|
|
|
if (component.props.dragType === "SPRITE") {
|
|
for (const target of vm.runtime.targets) {
|
|
if (target.isOriginal) {
|
|
if (getFolderFromName(target.getName()) === data.folder) {
|
|
vm.renameSprite(target.id, ensureNotReserved(setFolderOfName(target.getName(), newName)));
|
|
}
|
|
}
|
|
}
|
|
|
|
vm.emitWorkspaceUpdate();
|
|
fixTargetOrder();
|
|
} else if (component.props.dragType === "COSTUME") {
|
|
for (let i = 0; i < vm.editingTarget.sprite.costumes.length; i++) {
|
|
const costume = vm.editingTarget.sprite.costumes[i];
|
|
|
|
if (getFolderFromName(costume.name) === data.folder) {
|
|
vm.renameCostume(i, setFolderOfName(costume.name, newName));
|
|
}
|
|
}
|
|
|
|
fixCostumeOrder();
|
|
} else if (component.props.dragType === "SOUND") {
|
|
for (let i = 0; i < vm.editingTarget.sprite.sounds.length; i++) {
|
|
const sound = vm.editingTarget.sprite.sounds[i];
|
|
|
|
if (getFolderFromName(sound.name) === data.folder) {
|
|
vm.renameSound(i, setFolderOfName(sound.name, newName));
|
|
}
|
|
}
|
|
|
|
fixSoundOrder();
|
|
}
|
|
};
|
|
|
|
const renameFolder = async () => {
|
|
let newName = await addon.tab.prompt(msg("rename-folder-prompt-title"), msg("rename-folder-prompt"), data.folder, {
|
|
useEditorClasses: true
|
|
}); // Prompt cancelled, do not rename
|
|
|
|
if (newName === null) {
|
|
return;
|
|
}
|
|
|
|
if (!isValidFolderName(newName)) {
|
|
alert(msg("name-not-allowed"));
|
|
return;
|
|
} // Empty name will remove the folder
|
|
|
|
|
|
if (!newName) {
|
|
newName = null;
|
|
}
|
|
|
|
renameItems(newName);
|
|
};
|
|
|
|
const removeFolder = () => {
|
|
renameItems(null);
|
|
};
|
|
|
|
return [{
|
|
className: "sa-folders-rename-folder",
|
|
label: msg("rename-folder"),
|
|
callback: renameFolder,
|
|
position: "assetContextMenuAfterDelete",
|
|
order: 10
|
|
}, {
|
|
className: "sa-folders-remove-folder",
|
|
label: msg("remove-folder"),
|
|
callback: removeFolder,
|
|
position: "assetContextMenuAfterDelete",
|
|
order: 11
|
|
}];
|
|
} else {
|
|
ctx.target.setAttribute("sa-folders-context-type", "asset");
|
|
|
|
const setFolder = folder => {
|
|
if (component.props.dragType === "SPRITE") {
|
|
const target = vm.runtime.getTargetById(component.props.id);
|
|
vm.renameSprite(component.props.id, ensureNotReserved(setFolderOfName(target.getName(), folder)));
|
|
fixTargetOrder();
|
|
vm.emitWorkspaceUpdate();
|
|
} else if (component.props.dragType === "COSTUME") {
|
|
const data = getItemData(component.props);
|
|
const index = data.realIndex;
|
|
const asset = vm.editingTarget.sprite.costumes[index];
|
|
vm.renameCostume(vm.editingTarget.sprite.costumes.indexOf(asset), setFolderOfName(asset.name, folder));
|
|
fixCostumeOrder();
|
|
} else if (component.props.dragType === "SOUND") {
|
|
const data = getItemData(component.props);
|
|
const index = data.realIndex;
|
|
const asset = vm.editingTarget.sprite.sounds[index];
|
|
vm.renameSound(vm.editingTarget.sprite.sounds.indexOf(asset), setFolderOfName(asset.name, folder));
|
|
fixSoundOrder();
|
|
}
|
|
};
|
|
|
|
const createFolder = async () => {
|
|
const name = await addon.tab.prompt(msg("name-prompt-title"), msg("name-prompt"), getNameWithoutFolder(data.realName), {
|
|
useEditorClasses: true
|
|
});
|
|
|
|
if (name === null) {
|
|
return;
|
|
}
|
|
|
|
if (!isValidFolderName(name)) {
|
|
alert(msg("name-not-allowed"));
|
|
return;
|
|
}
|
|
|
|
setFolder(name);
|
|
};
|
|
|
|
const base = [{
|
|
border: true,
|
|
className: "sa-folders-create-folder",
|
|
label: msg("create-folder"),
|
|
callback: createFolder,
|
|
position: "assetContextMenuAfterDelete",
|
|
order: 13
|
|
}];
|
|
const currentFolder = data.inFolder;
|
|
|
|
if (typeof currentFolder === "string") {
|
|
base.push({
|
|
className: "sa-folders-remove-from-folder",
|
|
label: msg("remove-from-folder"),
|
|
callback: () => setFolder(null),
|
|
position: "assetContextMenuAfterDelete",
|
|
order: 14
|
|
});
|
|
}
|
|
|
|
return base.concat(getAllFolders(component).filter(folder => folder !== currentFolder).map((folder, i) => {
|
|
return {
|
|
className: "sa-folders-add-to-folder",
|
|
label: msg("add-to-folder", {
|
|
folder
|
|
}),
|
|
callback: () => setFolder(folder),
|
|
position: "assetContextMenuAfterDelete",
|
|
order: 20 + i
|
|
};
|
|
}));
|
|
}
|
|
});
|
|
|
|
const patchSpriteSelectorItem = SpriteSelectorItem => {
|
|
for (const method of ["handleDelete", "handleDuplicate", "handleExport"]) {
|
|
const original = SpriteSelectorItem.prototype[method];
|
|
|
|
SpriteSelectorItem.prototype[method] = function (...args) {
|
|
if (typeof this.props.id === "number") {
|
|
const itemData = getItemData(this.props);
|
|
|
|
if (itemData) {
|
|
const originalProps = this.props;
|
|
this.props = _objectSpread(_objectSpread({}, originalProps), {}, {
|
|
id: itemData.realIndex
|
|
});
|
|
const ret = original.call(this, ...args);
|
|
this.props = originalProps;
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return original.call(this, ...args);
|
|
};
|
|
}
|
|
|
|
const originalHandleDragEnd = SpriteSelectorItem.prototype.handleDragEnd;
|
|
|
|
SpriteSelectorItem.prototype.handleDragEnd = function (...args) {
|
|
const itemData = getItemData(this.props);
|
|
|
|
if (itemData) {
|
|
if (typeof itemData.realIndex === "number" && this.props.dragging) {
|
|
// If the item is being dragged onto another group (eg. costume list -> sprite list)
|
|
// then we fake a drag event to make the `index` be the real index
|
|
const originalIndex = this.props.index;
|
|
const realIndex = itemData.realIndex;
|
|
|
|
if (originalIndex !== realIndex) {
|
|
const currentOffset = addon.tab.redux.state.scratchGui.assetDrag.currentOffset;
|
|
const sortableHOCInstance = getSortableHOCFromElement(this.ref);
|
|
|
|
if (currentOffset && sortableHOCInstance && sortableHOCInstance.getMouseOverIndex() === null) {
|
|
this.props.index = realIndex;
|
|
this.handleDrag(currentOffset);
|
|
this.props.index = originalIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return originalHandleDragEnd.call(this, ...args);
|
|
};
|
|
|
|
const originalHandleClick = SpriteSelectorItem.prototype.handleClick;
|
|
|
|
SpriteSelectorItem.prototype.handleClick = function (...args) {
|
|
const e = args[0];
|
|
|
|
if (e && !this.noClick) {
|
|
const itemData = getItemData(this.props);
|
|
|
|
if (itemData) {
|
|
if (typeof itemData.folder === "string") {
|
|
e.preventDefault();
|
|
setFolderOpen(this, itemData.folder, !isFolderOpen(this, itemData.folder));
|
|
return;
|
|
}
|
|
|
|
if (typeof this.props.number === "number" && typeof itemData.realIndex === "number") {
|
|
e.preventDefault();
|
|
|
|
if (this.props.onClick) {
|
|
this.props.onClick(itemData.realIndex);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
return originalHandleClick.call(this, ...args);
|
|
};
|
|
|
|
const originalRender = SpriteSelectorItem.prototype.render;
|
|
|
|
SpriteSelectorItem.prototype.render = function () {
|
|
const itemData = getItemData(this.props);
|
|
|
|
if (itemData) {
|
|
const originalProps = this.props;
|
|
this.props = _objectSpread({}, this.props);
|
|
|
|
if (typeof itemData.realName === "string") {
|
|
this.props.name = getNameWithoutFolder(itemData.realName);
|
|
}
|
|
|
|
if (typeof this.props.number === "number" && typeof itemData.realIndex === "number") {
|
|
// Convert 0-indexed to 1-indexed
|
|
this.props.number = itemData.realIndex + 1;
|
|
}
|
|
|
|
if (typeof itemData.folder === "string") {
|
|
this.props.name = itemData.folder;
|
|
|
|
if (itemData.folderOpen) {
|
|
this.props.details = msg("open-folder");
|
|
} else {
|
|
this.props.details = msg("closed-folder");
|
|
}
|
|
|
|
this.props.selected = false;
|
|
this.props.number = null;
|
|
this.props.className += " ".concat(getFolderColorClass(itemData.folder), " sa-folders-folder");
|
|
}
|
|
|
|
if (typeof itemData.inFolder === "string") {
|
|
this.props.className += " ".concat(getFolderColorClass(itemData.inFolder));
|
|
}
|
|
|
|
const result = originalRender.call(this);
|
|
this.props = originalProps;
|
|
return result;
|
|
}
|
|
|
|
return originalRender.call(this);
|
|
};
|
|
};
|
|
|
|
const patchVM = () => {
|
|
const RenderedTarget = vm.runtime.targets[0].constructor;
|
|
const originalInstallTargets = vm.installTargets;
|
|
|
|
vm.installTargets = function (...args) {
|
|
if (currentSpriteFolder !== null) {
|
|
const targets = args[0];
|
|
const wholeProject = args[2];
|
|
|
|
if (Array.isArray(targets) && !wholeProject) {
|
|
for (const target of targets) {
|
|
if (target.sprite) {
|
|
target.sprite.name = setFolderOfName(target.sprite.name, currentSpriteFolder);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return originalInstallTargets.call(this, ...args).then(r => {
|
|
fixTargetOrder();
|
|
return r;
|
|
});
|
|
};
|
|
|
|
const originalAddCostume = RenderedTarget.prototype.addCostume;
|
|
|
|
RenderedTarget.prototype.addCostume = function (...args) {
|
|
if (currentAssetFolder !== null) {
|
|
const costume = args[0];
|
|
|
|
if (costume && typeof getFolderFromName(costume.name) !== "string") {
|
|
costume.name = setFolderOfName(costume.name, currentAssetFolder);
|
|
}
|
|
}
|
|
|
|
const r = originalAddCostume.call(this, ...args);
|
|
fixCostumeOrder(this);
|
|
return r;
|
|
};
|
|
|
|
const originalAddSound = RenderedTarget.prototype.addSound;
|
|
|
|
RenderedTarget.prototype.addSound = function (...args) {
|
|
if (currentAssetFolder !== null) {
|
|
const sound = args[0];
|
|
|
|
if (sound && typeof getFolderFromName(sound.name) !== "string") {
|
|
sound.name = setFolderOfName(sound.name, currentAssetFolder);
|
|
}
|
|
}
|
|
|
|
const r = originalAddSound.call(this, ...args);
|
|
fixSoundOrder(this);
|
|
return r;
|
|
};
|
|
|
|
const abstractReorder = ({
|
|
guiItems,
|
|
getAll,
|
|
set,
|
|
rename,
|
|
getVMItemFromGUIItem,
|
|
zeroIndexed,
|
|
onFolderChanged
|
|
}, itemIndex, newIndex) => {
|
|
// First index depends on zeroIndexed
|
|
itemIndex = clamp(itemIndex, zeroIndexed ? 0 : 1, zeroIndexed ? guiItems.length - 1 : guiItems.length);
|
|
newIndex = clamp(newIndex, zeroIndexed ? 0 : 1, zeroIndexed ? guiItems.length - 1 : guiItems.length);
|
|
|
|
if (itemIndex === newIndex) {
|
|
return false;
|
|
}
|
|
|
|
let assets = getAll();
|
|
const originalAssets = getAll();
|
|
const targetItem = guiItems[itemIndex - (zeroIndexed ? 0 : 1)];
|
|
const itemAtNewIndex = guiItems[newIndex - (zeroIndexed ? 0 : 1)];
|
|
const targetItemData = getItemData(targetItem);
|
|
const itemAtNewIndexData = getItemData(itemAtNewIndex);
|
|
|
|
if (!targetItemData || !itemAtNewIndexData) {
|
|
console.warn("should never happen");
|
|
return false;
|
|
}
|
|
|
|
const reorderingItems = typeof targetItemData.folder === "string" ? targetItem.items : [targetItem];
|
|
const reorderingAssets = reorderingItems.map(i => getVMItemFromGUIItem(i, assets)).filter(i => i);
|
|
|
|
if (typeof itemAtNewIndexData.realIndex === "number") {
|
|
const newTarget = getVMItemFromGUIItem(itemAtNewIndex, assets);
|
|
|
|
if (!newTarget || reorderingAssets.includes(newTarget)) {
|
|
// Dragging folder into itself or target doesn't exist. Ignore.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let newFolder = null;
|
|
assets = assets.filter(i => !reorderingAssets.includes(i));
|
|
let realNewIndex;
|
|
|
|
if (newIndex === (zeroIndexed ? 0 : 1)) {
|
|
realNewIndex = zeroIndexed ? 0 : 1;
|
|
} else if (newIndex === guiItems.length - (zeroIndexed ? 1 : 0)) {
|
|
realNewIndex = assets.length;
|
|
} else if (typeof itemAtNewIndexData.realIndex === "number") {
|
|
newFolder = typeof itemAtNewIndexData.inFolder === "string" ? itemAtNewIndexData.inFolder : null;
|
|
let newAsset = getVMItemFromGUIItem(itemAtNewIndex, assets);
|
|
|
|
if (!newAsset) {
|
|
console.warn("should never happen");
|
|
return false;
|
|
}
|
|
|
|
realNewIndex = assets.indexOf(newAsset);
|
|
|
|
if (newIndex > itemIndex) {
|
|
realNewIndex++;
|
|
}
|
|
} else if (typeof itemAtNewIndexData.folder === "string") {
|
|
let item;
|
|
let offset = 0;
|
|
|
|
if (newIndex < itemIndex) {
|
|
// A B [C D E] F G
|
|
// ^----------*
|
|
// A B C [D] E F G
|
|
// ^--------*
|
|
item = itemAtNewIndex.items[0];
|
|
} else if (itemAtNewIndexData.folderOpen) {
|
|
// A B [C D E] F G
|
|
// *---^
|
|
item = itemAtNewIndex.items[0];
|
|
newFolder = itemAtNewIndexData.folder;
|
|
} else {
|
|
// A B [C] D E F G
|
|
// *----^
|
|
item = itemAtNewIndex.items[itemAtNewIndex.items.length - 1];
|
|
offset = 1;
|
|
}
|
|
|
|
let newAsset = getVMItemFromGUIItem(item, assets);
|
|
|
|
if (newAsset) {
|
|
realNewIndex = assets.indexOf(newAsset) + offset;
|
|
} else {
|
|
// Edge case: Dragging the first item of a list on top of the folder item
|
|
// A B [C D E] F G
|
|
// ^---*
|
|
newAsset = getVMItemFromGUIItem(item, originalAssets);
|
|
|
|
if (!newAsset) {
|
|
console.warn("should never happen");
|
|
return false;
|
|
}
|
|
|
|
realNewIndex = originalAssets.indexOf(newAsset) + offset;
|
|
}
|
|
} else {
|
|
console.warn("should never happen");
|
|
return false;
|
|
}
|
|
|
|
if (typeof targetItemData.folder === "string" && newFolder !== null) {
|
|
// Cannot drag a folder into another folder
|
|
return;
|
|
}
|
|
|
|
if (realNewIndex < (zeroIndexed ? 0 : 1) || realNewIndex > assets.length) {
|
|
console.warn("should never happen");
|
|
return false;
|
|
}
|
|
|
|
assets.splice(realNewIndex, 0, ...reorderingAssets);
|
|
set(assets); // If the folder has changed, update item names to match.
|
|
|
|
if (typeof targetItemData.folder !== "string" && targetItemData.inFolder !== newFolder) {
|
|
for (const asset of reorderingAssets) {
|
|
const name = asset.getName ? asset.getName() : asset.name;
|
|
rename(asset, setFolderOfName(name, newFolder));
|
|
}
|
|
|
|
if (onFolderChanged) {
|
|
onFolderChanged();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
vm.constructor.prototype.reorderTarget = function (targetIndex, newIndex) {
|
|
return abstractReorder({
|
|
getAll: () => {
|
|
return this.runtime.targets;
|
|
},
|
|
set: targets => {
|
|
this.runtime.targets = targets;
|
|
this.emitTargetsUpdate();
|
|
},
|
|
rename: (item, name) => {
|
|
this.renameSprite(item.id, ensureNotReserved(name));
|
|
},
|
|
getVMItemFromGUIItem: (item, targets) => {
|
|
return targets.find(i => i.id === item.id);
|
|
},
|
|
onFolderChanged: () => {
|
|
this.emitWorkspaceUpdate();
|
|
},
|
|
guiItems: currentSpriteItems,
|
|
zeroIndexed: false
|
|
}, targetIndex, newIndex);
|
|
};
|
|
|
|
RenderedTarget.prototype.reorderCostume = function (costumeIndex, newIndex) {
|
|
return abstractReorder({
|
|
getAll: () => {
|
|
return this.sprite.costumes;
|
|
},
|
|
set: assets => {
|
|
this.sprite.costumes = assets;
|
|
},
|
|
rename: (item, name) => {
|
|
this.renameCostume(this.sprite.costumes.indexOf(item), name);
|
|
},
|
|
getVMItemFromGUIItem: (item, costumes) => {
|
|
const itemData = getItemData(item);
|
|
return costumes.find(c => c.name === itemData.realName);
|
|
},
|
|
guiItems: currentAssetItems,
|
|
zeroIndexed: true
|
|
}, costumeIndex, newIndex);
|
|
};
|
|
|
|
RenderedTarget.prototype.reorderSound = function (soundIndex, newIndex) {
|
|
return abstractReorder({
|
|
getAll: () => {
|
|
return this.sprite.sounds;
|
|
},
|
|
set: assets => {
|
|
this.sprite.sounds = assets;
|
|
},
|
|
rename: (item, name) => {
|
|
this.renameSound(this.sprite.sounds.indexOf(item), name);
|
|
},
|
|
getVMItemFromGUIItem: (item, sounds) => {
|
|
const itemData = getItemData(item);
|
|
return sounds.find(c => c.name === itemData.realName);
|
|
},
|
|
guiItems: currentAssetItems,
|
|
zeroIndexed: true
|
|
}, soundIndex, newIndex);
|
|
}; // Temporal bug fix for #5762
|
|
|
|
|
|
const originalShareSoundToTarget = vm.shareSoundToTarget;
|
|
|
|
vm.shareSoundToTarget = function (...args) {
|
|
const target = this.runtime.getTargetById(args[1]);
|
|
|
|
if (!target) {
|
|
// Avoid reading property from null
|
|
return Promise.reject(new Error("Dropping sound into folder is not supported")); // This would also work no matter what we returned, probably
|
|
// Original method returns a promise, so here too
|
|
}
|
|
|
|
return originalShareSoundToTarget.call(this, ...args);
|
|
};
|
|
};
|
|
|
|
const patchBackpack = backpackInstance => {
|
|
const Backpack = backpackInstance.constructor;
|
|
|
|
Backpack.prototype.sa_loadNextItem = function () {
|
|
if (!this.sa_queuedItems) return;
|
|
const item = this.sa_queuedItems.pop();
|
|
|
|
if (item) {
|
|
let payload;
|
|
let type;
|
|
|
|
if (item.dragPayload) {
|
|
if (item.url) {
|
|
type = "SOUND";
|
|
} else {
|
|
type = "COSTUME";
|
|
}
|
|
|
|
payload = item.dragPayload;
|
|
} else if (item.id) {
|
|
type = "SPRITE";
|
|
payload = item.id;
|
|
}
|
|
|
|
if (type && payload) {
|
|
originalHandleDrop.call(this, {
|
|
dragType: type,
|
|
payload: payload
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
Backpack.prototype.componentDidUpdate = function (prevProps, prevState) {
|
|
if (!this.state.loading && prevState.loading && !this.state.error) {
|
|
this.sa_loadNextItem();
|
|
}
|
|
};
|
|
|
|
const originalHandleDrop = Backpack.prototype.handleDrop;
|
|
|
|
Backpack.prototype.handleDrop = function (...args) {
|
|
// When a folder is dropped into the backpack, upload all the items in the folder.
|
|
const dragInfo = args[0];
|
|
const folderItems = dragInfo && dragInfo.payload && dragInfo.payload.sa_folder_items;
|
|
|
|
if (Array.isArray(folderItems)) {
|
|
addon.tab.confirm("", msg("confirm-backpack-folder"), {
|
|
useEditorClasses: true
|
|
}).then(result => {
|
|
if (!result) return;
|
|
this.sa_queuedItems = folderItems;
|
|
this.sa_loadNextItem();
|
|
});
|
|
return;
|
|
}
|
|
|
|
return originalHandleDrop.call(this, ...args);
|
|
};
|
|
|
|
backpackInstance.handleDrop = Backpack.prototype.handleDrop.bind(backpackInstance);
|
|
}; // Backpack
|
|
|
|
|
|
{
|
|
const clickListener = e => {
|
|
if (!e.target.closest('[class*="backpack_backpack-header_"]')) {
|
|
return;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const backpackContainer = document.querySelector("[class^='backpack_backpack-list_']");
|
|
|
|
if (!backpackContainer) {
|
|
return;
|
|
}
|
|
|
|
document.removeEventListener("click", clickListener);
|
|
const backpackInstance = getBackpackFromElement(backpackContainer);
|
|
verifyBackpack(backpackInstance);
|
|
patchBackpack(backpackInstance);
|
|
});
|
|
};
|
|
|
|
document.addEventListener("click", clickListener, true);
|
|
} // Sprite list
|
|
|
|
{
|
|
const spriteSelectorItemElement = await addon.tab.waitForElement("[class^='sprite-selector_sprite-wrapper']", {
|
|
reduxCondition: state => !state.scratchGui.mode.isPlayerOnly
|
|
});
|
|
vm = addon.tab.traps.vm;
|
|
reactInternalKey = Object.keys(spriteSelectorItemElement).find(i => i.startsWith(REACT_INTERNAL_PREFIX));
|
|
const sortableHOCInstance = getSortableHOCFromElement(spriteSelectorItemElement);
|
|
const spriteSelectorItemInstance = spriteSelectorItemElement[reactInternalKey].child.child.child.stateNode;
|
|
verifySortableHOC(sortableHOCInstance);
|
|
verifySpriteSelectorItem(spriteSelectorItemInstance);
|
|
verifyVM(vm);
|
|
patchSortableHOC(sortableHOCInstance.constructor, TYPE_SPRITES);
|
|
patchSpriteSelectorItem(spriteSelectorItemInstance.constructor);
|
|
sortableHOCInstance.saInitialSetup();
|
|
patchVM();
|
|
} // Costume and sound list
|
|
|
|
{
|
|
const selectorListItem = await addon.tab.waitForElement("[class*='selector_list-item']", {
|
|
reduxCondition: state => state.scratchGui.editorTab.activeTabIndex !== 0 && !state.scratchGui.mode.isPlayerOnly
|
|
});
|
|
const sortableHOCInstance = getSortableHOCFromElement(selectorListItem);
|
|
verifySortableHOC(sortableHOCInstance);
|
|
patchSortableHOC(sortableHOCInstance.constructor, TYPE_ASSETS);
|
|
sortableHOCInstance.saInitialSetup();
|
|
}
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/middle-click-popup/_runtime_entry.js":
|
|
/*!****************************************************************!*\
|
|
!*** ./src/addons/addons/middle-click-popup/_runtime_entry.js ***!
|
|
\****************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/middle-click-popup/userscript.js");
|
|
/* harmony import */ var _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./userstyle.css */ "./node_modules/css-loader/index.js!./src/addons/addons/middle-click-popup/userstyle.css");
|
|
/* harmony import */ var _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* generated by pull.js */
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"userstyle.css": _css_loader_userstyle_css__WEBPACK_IMPORTED_MODULE_1___default.a
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/middle-click-popup/userscript.js":
|
|
/*!************************************************************!*\
|
|
!*** ./src/addons/addons/middle-click-popup/userscript.js ***!
|
|
\************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
msg,
|
|
console
|
|
}) {
|
|
const Blockly = await addon.tab.traps.getBlockly();
|
|
let mouse = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
|
|
class FloatingInput {
|
|
constructor() {
|
|
this.floatBar = null;
|
|
this.floatInput = null;
|
|
this.dropdownOut = null;
|
|
this.dropdown = null;
|
|
this.prevVal = "";
|
|
this.DROPDOWN_BLOCK_LIST_MAX_ROWS = 25;
|
|
this.createDom();
|
|
}
|
|
|
|
get workspace() {
|
|
return Blockly.getMainWorkspace();
|
|
}
|
|
|
|
get selectedTab() {
|
|
return addon.tab.redux.state.scratchGui.editorTab.activeTabIndex;
|
|
}
|
|
|
|
createDom() {
|
|
// Popup new input box for block injection
|
|
this.floatBar = document.body.appendChild(document.createElement("div"));
|
|
this.floatBar.className = "sa-float-bar";
|
|
this.floatBar.dir = addon.tab.direction;
|
|
this.floatBar.style.display = "none";
|
|
this.dropdownOut = this.floatBar.appendChild(document.createElement("div"));
|
|
this.dropdownOut.className = "sa-float-bar-dropdown-out";
|
|
this.floatInput = this.dropdownOut.appendChild(document.createElement("input"));
|
|
this.floatInput.placeholder = msg("start-typing");
|
|
this.floatInput.className = "sa-float-bar-input";
|
|
this.floatInput.className = addon.tab.scratchClass("input_input-form", {
|
|
others: "sa-float-bar-input"
|
|
});
|
|
this.dropdown = this.dropdownOut.appendChild(document.createElement("ul"));
|
|
this.dropdown.className = "sa-float-bar-dropdown";
|
|
this.floatInput.addEventListener("keyup", () => this.inputChange());
|
|
this.floatInput.addEventListener("focus", () => this.inputChange());
|
|
this.floatInput.addEventListener("keydown", (...e) => this.inputKeyDown(...e));
|
|
this.floatInput.addEventListener("focusout", () => this.hide());
|
|
this.dropdownOut.addEventListener("mousedown", (...e) => this.onClick(...e));
|
|
document.addEventListener("keydown", e => {
|
|
if (addon.tab.editorMode !== "editor") {
|
|
return;
|
|
}
|
|
|
|
let ctrlKey = e.ctrlKey || e.metaKey;
|
|
|
|
if (e.key === " " && ctrlKey) {
|
|
// Ctrl + Space (Inject Code)
|
|
this.show(e);
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
show(e) {
|
|
var _e$clientX, _e$clientY;
|
|
|
|
if (this.selectedTab !== 0) {
|
|
return;
|
|
}
|
|
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
this.buildFilterList();
|
|
this.floatBar.style.left = ((_e$clientX = e.clientX) !== null && _e$clientX !== void 0 ? _e$clientX : mouse.x) + 16 + "px";
|
|
this.floatBar.style.top = ((_e$clientY = e.clientY) !== null && _e$clientY !== void 0 ? _e$clientY : mouse.y) - 8 + "px";
|
|
this.floatBar.style.display = "";
|
|
this.floatInput.value = "";
|
|
this.floatInput.focus();
|
|
}
|
|
|
|
onClick(e) {
|
|
e.cancelBubble = true;
|
|
|
|
if (!e.target.closest("input")) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
let sel = e && e.target;
|
|
|
|
if (sel.tagName === "B") {
|
|
sel = sel.parentNode;
|
|
}
|
|
|
|
if (e instanceof MouseEvent && sel.tagName !== "LI") {
|
|
// Mouse clicks need to be on a block...
|
|
return;
|
|
}
|
|
|
|
if (!sel || !sel.data) {
|
|
sel = this.dropdown.querySelector(".sel");
|
|
}
|
|
|
|
if (!sel) {
|
|
return;
|
|
}
|
|
|
|
this.createDraggingBlock(sel, e);
|
|
|
|
if (e.shiftKey) {
|
|
this.floatBar.style.display = "";
|
|
this.floatInput.focus();
|
|
}
|
|
}
|
|
|
|
createDraggingBlock(sel, e) {
|
|
let option = sel.data.option; // block:option.block, dom:option.dom, option:option.option
|
|
|
|
if (option.option) {
|
|
// We need to tweak the dropdown in this xml...
|
|
let field = option.dom.querySelector("field[name=" + option.pickField + "]");
|
|
|
|
if (field.getAttribute("id")) {
|
|
field.innerText = option.option[0];
|
|
field.setAttribute("id", option.option[1] + "-" + option.option[0]);
|
|
} else {
|
|
field.innerText = option.option[1];
|
|
} // Handle "stop other scripts in sprite"
|
|
|
|
|
|
if (option.option[1] === "other scripts in sprite") {
|
|
option.dom.querySelector("mutation").setAttribute("hasnext", "true");
|
|
}
|
|
} // This is mostly copied from https://github.com/LLK/scratch-blocks/blob/893c7e7ad5bfb416eaed75d9a1c93bdce84e36ab/core/scratch_blocks_utils.js#L171
|
|
// Some bits were removed or changed to fit our needs.
|
|
|
|
|
|
this.workspace.setResizesEnabled(false);
|
|
Blockly.Events.disable();
|
|
|
|
try {
|
|
var newBlock = Blockly.Xml.domToBlock(option.dom, this.workspace);
|
|
Blockly.scratchBlocksUtils.changeObscuredShadowIds(newBlock);
|
|
var svgRootNew = newBlock.getSvgRoot();
|
|
|
|
if (!svgRootNew) {
|
|
throw new Error("newBlock is not rendered.");
|
|
}
|
|
|
|
let blockBounds = newBlock.svgPath_.getBoundingClientRect();
|
|
let newBlockX = Math.floor((mouse.x - (blockBounds.left + blockBounds.right) / 2) / this.workspace.scale);
|
|
let newBlockY = Math.floor((mouse.y - (blockBounds.top + blockBounds.bottom) / 2) / this.workspace.scale);
|
|
newBlock.moveBy(newBlockX, newBlockY);
|
|
} finally {
|
|
Blockly.Events.enable();
|
|
}
|
|
|
|
if (Blockly.Events.isEnabled()) {
|
|
Blockly.Events.fire(new Blockly.Events.BlockCreate(newBlock));
|
|
}
|
|
|
|
var fakeEvent = {
|
|
clientX: mouse.x,
|
|
clientY: mouse.y,
|
|
type: "mousedown",
|
|
preventDefault: function preventDefault() {
|
|
e.preventDefault();
|
|
},
|
|
stopPropagation: function stopPropagation() {
|
|
e.stopPropagation();
|
|
},
|
|
target: sel
|
|
};
|
|
this.workspace.startDragWithFakeEvent(fakeEvent, newBlock);
|
|
}
|
|
|
|
inputChange() {
|
|
// Filter the list...
|
|
let val = (this.floatInput.value || "").toLowerCase();
|
|
|
|
if (val === this.prevVal) {
|
|
return;
|
|
}
|
|
|
|
this.prevVal = val;
|
|
let p = this.dropdown.parentNode;
|
|
this.dropdown.remove();
|
|
let count = 0;
|
|
let split = val.split(" ");
|
|
let listLI = this.dropdown.getElementsByTagName("li");
|
|
|
|
for (const li of listLI) {
|
|
const procCode = li.data.text;
|
|
const lower = li.data.lower; // let i = li.data.lower.indexOf(val);
|
|
// let array = regExp.exec(li.data.lower);
|
|
|
|
let im = 0;
|
|
let match = [];
|
|
|
|
for (let si = 0; si < split.length; si++) {
|
|
let find = " " + split[si];
|
|
let idx = lower.indexOf(find, im);
|
|
|
|
if (idx === -1) {
|
|
match = null;
|
|
break;
|
|
}
|
|
|
|
match.push(idx);
|
|
im = idx + find.length;
|
|
}
|
|
|
|
if (count < this.DROPDOWN_BLOCK_LIST_MAX_ROWS && match) {
|
|
li.style.display = "block";
|
|
|
|
while (li.firstChild) {
|
|
li.removeChild(li.firstChild);
|
|
}
|
|
|
|
let i = 0;
|
|
|
|
for (let iM = 0; iM < match.length; iM++) {
|
|
let im = match[iM];
|
|
|
|
if (im > i) {
|
|
li.appendChild(document.createTextNode(procCode.substring(i, im)));
|
|
i = im;
|
|
}
|
|
|
|
let bText = document.createElement("b");
|
|
let len = split[iM].length;
|
|
bText.appendChild(document.createTextNode(procCode.substr(i, len)));
|
|
li.appendChild(bText);
|
|
i += len;
|
|
}
|
|
|
|
if (i < procCode.length) {
|
|
li.appendChild(document.createTextNode(procCode.substr(i)));
|
|
}
|
|
|
|
if (count === 0) {
|
|
li.classList.add("sel");
|
|
} else {
|
|
li.classList.remove("sel");
|
|
}
|
|
|
|
count++;
|
|
} else {
|
|
li.style.display = "none";
|
|
li.classList.remove("sel");
|
|
}
|
|
}
|
|
|
|
p.append(this.dropdown);
|
|
}
|
|
|
|
inputKeyDown(e) {
|
|
if (e.keyCode === 38) {
|
|
this.navigateFloatFilter(-1);
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (e.keyCode === 40) {
|
|
this.navigateFloatFilter(1);
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (e.keyCode === 13) {
|
|
// Enter
|
|
let sel = this.dropdown.querySelector(".sel");
|
|
|
|
if (sel) {
|
|
this.onClick(e);
|
|
this.hide();
|
|
}
|
|
|
|
e.cancelBubble = true;
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (e.keyCode === 27) {
|
|
// Escape
|
|
if (this.floatInput.value.length > 0) {
|
|
this.floatInput.value = ""; // Clear search first, then close on second press
|
|
|
|
this.inputChange(e);
|
|
} else {
|
|
this.hide();
|
|
}
|
|
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
|
|
buildFilterList() {
|
|
let options = [];
|
|
let toolbox = this.workspace.getToolbox(); // This can happen during custom block creation, for example
|
|
|
|
if (!toolbox) return;
|
|
let blocks = toolbox.flyout_.getWorkspace().getTopBlocks(); // 107 blocks, not in order... but we can sort by y value or description right :)
|
|
|
|
let fullDom = Blockly.Xml.workspaceToDom(toolbox.flyout_.getWorkspace());
|
|
const doms = {};
|
|
|
|
for (const x of fullDom.children) {
|
|
if (x.tagName === "BLOCK") {
|
|
let id = x.getAttribute("id");
|
|
doms[id] = x;
|
|
}
|
|
}
|
|
|
|
for (const block of blocks) {
|
|
this.getBlockText(block, options, doms);
|
|
}
|
|
|
|
options.sort((a, b) => a.desc.length < b.desc.length ? -1 : a.desc.length > b.desc.length ? 1 : a.desc.localeCompare(b.desc));
|
|
let count = 0;
|
|
|
|
while (this.dropdown.firstChild) {
|
|
this.dropdown.removeChild(this.dropdown.firstChild);
|
|
}
|
|
|
|
for (const option of options) {
|
|
const li = document.createElement("li");
|
|
const desc = option.desc; // bType = hat block reporter boolean
|
|
|
|
let bType = this.getEdgeTypeClass(option.block);
|
|
count++;
|
|
li.innerText = desc;
|
|
li.data = {
|
|
text: desc,
|
|
lower: " " + desc.toLowerCase(),
|
|
option: option
|
|
};
|
|
const blockTypes = {
|
|
// Some of these blocks in the flyout have a category of `null` for some reason, the
|
|
// same as procedures. Without making bigger changes to the custom block color system
|
|
// hardcoding these is the best solution for now.
|
|
sensing_of: "sensing",
|
|
event_whenbackdropswitchesto: "events"
|
|
};
|
|
let ending = option.block.getCategory() || blockTypes[option.block.type] || "null";
|
|
|
|
if (option.block.isScratchExtension) {
|
|
ending = "pen";
|
|
} else if (addon.tab.getCustomBlock(option.block.procCode_)) {
|
|
ending = "addon-custom-block";
|
|
}
|
|
|
|
li.className = "sa-block-color sa-block-color-" + ending + " sa-" + bType;
|
|
|
|
if (count > this.DROPDOWN_BLOCK_LIST_MAX_ROWS) {
|
|
// Limit maximum number of rows to prevent lag when no filter is applied
|
|
li.style.display = "none";
|
|
}
|
|
|
|
this.dropdown.appendChild(li);
|
|
}
|
|
|
|
this.dropdownOut.classList.add("vis");
|
|
}
|
|
|
|
navigateFloatFilter(dir) {
|
|
let sel = this.dropdown.getElementsByClassName("sel");
|
|
let nxt;
|
|
|
|
if (sel.length > 0 && sel[0].style.display !== "none") {
|
|
nxt = dir === -1 ? sel[0].previousSibling : sel[sel.length - 1].nextSibling;
|
|
} else {
|
|
nxt = this.dropdown.children[0];
|
|
dir = 1;
|
|
}
|
|
|
|
while (nxt && nxt.style.display === "none") {
|
|
nxt = dir === -1 ? nxt.previousSibling : nxt.nextSibling;
|
|
}
|
|
|
|
if (nxt) {
|
|
for (const i of sel) {
|
|
i.classList.remove("sel");
|
|
}
|
|
|
|
nxt.classList.add("sel"); // centerTop(nxt.data.labelID);
|
|
}
|
|
}
|
|
|
|
getBlockText(block, options, doms) {
|
|
// block.type; "looks_nextbackdrop"
|
|
let desc;
|
|
let picklist, pickField;
|
|
let dom = doms[block.id]; // dom = doms[block.type];
|
|
|
|
const process = block => {
|
|
for (const input of block.inputList) {
|
|
// input.name = "", input.type = 5
|
|
let fields = input.fieldRow;
|
|
|
|
for (const field of fields) {
|
|
// field --- Blockly.FieldLabel .className = "blocklyText"
|
|
// Blockly.FieldDropdown --- .className = "blocklyText blocklyDropdownText"
|
|
let text;
|
|
|
|
if (!picklist && field.className_ === "blocklyText blocklyDropdownText") {
|
|
picklist = field.getOptions();
|
|
pickField = field.name;
|
|
|
|
if (picklist && picklist.length > 0) {
|
|
text = "^^";
|
|
} else {
|
|
text = field.getText();
|
|
}
|
|
} else {
|
|
text = field.getText();
|
|
}
|
|
|
|
desc = (desc ? desc + " " : "") + text;
|
|
}
|
|
|
|
if (input.connection) {
|
|
let innerBlock = input.connection.targetBlock();
|
|
|
|
if (innerBlock) {
|
|
process(innerBlock); // Recursive process connected child blocks...
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
process(block);
|
|
|
|
if (picklist) {
|
|
for (const item of picklist) {
|
|
let code = item[1];
|
|
|
|
if (typeof code !== "string" || // Audio Record is a function!
|
|
code === "DELETE_VARIABLE_ID" || code === "RENAME_VARIABLE_ID" || code === "NEW_BROADCAST_MESSAGE_ID" || code === "NEW_BROADCAST_MESSAGE_ID" || // editor-searchable-dropdowns compatibility
|
|
code === "createGlobalVariable" || code === "createLocalVariable" || code === "createGlobalList" || code === "createLocalList" || code === "createBroadcast" || // rename-broadcasts compatibility
|
|
code === "RENAME_BROADCAST_MESSAGE_ID") {
|
|
continue; // Skip these
|
|
}
|
|
|
|
options.push({
|
|
desc: desc.replace("^^", item[0]),
|
|
block: block,
|
|
dom: dom,
|
|
option: item,
|
|
pickField: pickField
|
|
});
|
|
}
|
|
} else {
|
|
options.push({
|
|
desc: desc,
|
|
block: block,
|
|
dom: dom
|
|
});
|
|
}
|
|
|
|
return desc;
|
|
}
|
|
|
|
getEdgeTypeClass(block) {
|
|
switch (block.edgeShape_) {
|
|
case 1:
|
|
return "boolean";
|
|
|
|
case 2:
|
|
return "reporter";
|
|
|
|
default:
|
|
return block.startHat_ ? "hat" : "block";
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
this.floatBar.style.display = "none";
|
|
}
|
|
|
|
}
|
|
|
|
const floatingInput = new FloatingInput();
|
|
const _doWorkspaceClick_ = Blockly.Gesture.prototype.doWorkspaceClick_;
|
|
|
|
Blockly.Gesture.prototype.doWorkspaceClick_ = function () {
|
|
if (!addon.self.disabled && (this.mostRecentEvent_.button === 1 || this.mostRecentEvent_.shiftKey)) {
|
|
// Wheel button...
|
|
floatingInput.show(this.mostRecentEvent_);
|
|
}
|
|
|
|
_doWorkspaceClick_.call(this);
|
|
};
|
|
|
|
document.addEventListener("mousemove", e => {
|
|
mouse = {
|
|
x: e.clientX,
|
|
y: e.clientY
|
|
};
|
|
});
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/move-to-top-bottom/_runtime_entry.js":
|
|
/*!****************************************************************!*\
|
|
!*** ./src/addons/addons/move-to-top-bottom/_runtime_entry.js ***!
|
|
\****************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/move-to-top-bottom/userscript.js");
|
|
/* generated by pull.js */
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/move-to-top-bottom/userscript.js":
|
|
/*!************************************************************!*\
|
|
!*** ./src/addons/addons/move-to-top-bottom/userscript.js ***!
|
|
\************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async ({
|
|
addon,
|
|
console,
|
|
msg
|
|
}) => {
|
|
const types = ["sound", "costume"];
|
|
addon.tab.createEditorContextMenu(ctx => {
|
|
const target = addon.tab.traps.vm.editingTarget;
|
|
|
|
if (ctx.type === "sound") {
|
|
target.reorderSound(ctx.index, 0);
|
|
} else {
|
|
target.reorderCostume(ctx.index, 0);
|
|
}
|
|
|
|
queueMicrotask(() => {
|
|
addon.tab.traps.vm.emitTargetsUpdate();
|
|
addon.tab.traps.vm.runtime.emitProjectChanged();
|
|
ctx.target.click();
|
|
});
|
|
}, {
|
|
types,
|
|
position: "assetContextMenuAfterExport",
|
|
order: 1,
|
|
label: msg("top"),
|
|
condition: ctx => ctx.index !== 0
|
|
});
|
|
addon.tab.createEditorContextMenu(ctx => {
|
|
const target = addon.tab.traps.vm.editingTarget;
|
|
|
|
if (ctx.type === "sound") {
|
|
target.reorderSound(ctx.index, Infinity);
|
|
} else {
|
|
target.reorderCostume(ctx.index, Infinity);
|
|
}
|
|
|
|
queueMicrotask(() => {
|
|
addon.tab.traps.vm.emitTargetsUpdate();
|
|
addon.tab.traps.vm.runtime.emitProjectChanged();
|
|
ctx.target.click();
|
|
});
|
|
}, {
|
|
types,
|
|
position: "assetContextMenuAfterExport",
|
|
order: 2,
|
|
label: msg("bottom"),
|
|
condition: ctx => ctx.index !== ctx.target.parentNode.parentNode.childElementCount - 1
|
|
});
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/onion-skinning/_runtime_entry.js":
|
|
/*!************************************************************!*\
|
|
!*** ./src/addons/addons/onion-skinning/_runtime_entry.js ***!
|
|
\************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/onion-skinning/userscript.js");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./style.css */ "./node_modules/css-loader/index.js!./src/addons/addons/onion-skinning/style.css");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* harmony import */ var _url_loader_decrement_svg__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! url-loader!./decrement.svg */ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/decrement.svg");
|
|
/* harmony import */ var _url_loader_increment_svg__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! url-loader!./increment.svg */ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/increment.svg");
|
|
/* harmony import */ var _url_loader_settings_svg__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! url-loader!./settings.svg */ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/settings.svg");
|
|
/* harmony import */ var _url_loader_toggle_svg__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! url-loader!./toggle.svg */ "./node_modules/url-loader/dist/cjs.js!./src/addons/addons/onion-skinning/toggle.svg");
|
|
/* generated by pull.js */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"style.css": _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default.a,
|
|
"decrement.svg": _url_loader_decrement_svg__WEBPACK_IMPORTED_MODULE_2__["default"],
|
|
"increment.svg": _url_loader_increment_svg__WEBPACK_IMPORTED_MODULE_3__["default"],
|
|
"settings.svg": _url_loader_settings_svg__WEBPACK_IMPORTED_MODULE_4__["default"],
|
|
"toggle.svg": _url_loader_toggle_svg__WEBPACK_IMPORTED_MODULE_5__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/onion-skinning/userscript.js":
|
|
/*!********************************************************!*\
|
|
!*** ./src/addons/addons/onion-skinning/userscript.js ***!
|
|
\********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
console,
|
|
msg
|
|
}) {
|
|
const paper = await addon.tab.traps.getPaper();
|
|
const paintEditorCanvasContainer = await addon.tab.waitForElement("[class^='paint-editor_canvas-container']");
|
|
|
|
try {
|
|
if (!("colorIndex" in addon.tab.redux.state.scratchPaint.fillMode)) {
|
|
console.error("Detected new paint editor; this will be supported in future versions.");
|
|
return;
|
|
}
|
|
} catch (_) {// The check can technically fail when Redux isn't supported (rare cases)
|
|
// Just ignore in this case
|
|
}
|
|
|
|
const paperCanvas = paintEditorCanvasContainer[addon.tab.traps.getInternalKey(paintEditorCanvasContainer)].child.child.child.stateNode;
|
|
const storedOnionLayers = [];
|
|
|
|
const parseHexColor = color => {
|
|
const hexString = color.substr(1);
|
|
const hexNumber = parseInt(hexString, 16);
|
|
return [hexNumber >> 16 & 0xff, // R
|
|
hexNumber >> 8 & 0xff, // G
|
|
hexNumber & 0xff // B
|
|
];
|
|
};
|
|
|
|
const settings = {
|
|
enabled: addon.settings.get("default") && !addon.self.disabled,
|
|
previous: +addon.settings.get("previous"),
|
|
next: +addon.settings.get("next"),
|
|
opacity: +addon.settings.get("opacity"),
|
|
opacityStep: +addon.settings.get("opacityStep"),
|
|
layering: addon.settings.get("layering"),
|
|
mode: addon.settings.get("mode"),
|
|
beforeTint: parseHexColor(addon.settings.get("beforeTint")),
|
|
afterTint: parseHexColor(addon.settings.get("afterTint"))
|
|
};
|
|
|
|
const getPaperCenter = () => {
|
|
const backgroundGuideLayer = paper.project.layers.find(i => i.data.isBackgroundGuideLayer);
|
|
return backgroundGuideLayer.children[0].position;
|
|
};
|
|
|
|
const injectPaper = () => {
|
|
// When background guide layer is added, show onion layers.
|
|
// https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/helper/layer.js#L145
|
|
const originalAddLayer = paper.Project.prototype.addLayer;
|
|
|
|
paper.Project.prototype.addLayer = function (layer) {
|
|
const result = originalAddLayer.call(this, layer);
|
|
|
|
if (layer.data.isBackgroundGuideLayer) {
|
|
let onion;
|
|
|
|
while (onion = storedOnionLayers.shift()) {
|
|
originalAddLayer.call(this, onion);
|
|
}
|
|
|
|
relayerOnionLayers();
|
|
}
|
|
|
|
return result;
|
|
}; // Scratch uses importJSON to undo or redo
|
|
// https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/helper/undo.js#L37
|
|
// The code prior to this will remove our onion layers, so we have to manually add them back.
|
|
|
|
|
|
const originalImportJSON = paper.Project.prototype.importJSON;
|
|
|
|
paper.Project.prototype.importJSON = function (json) {
|
|
const result = originalImportJSON.call(this, json);
|
|
|
|
if (settings.enabled) {
|
|
updateOnionLayers();
|
|
}
|
|
|
|
return result;
|
|
}; // https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/helper/layer.js#L114
|
|
// When background guide layer is removed, hide onion layers.
|
|
|
|
|
|
const originalRemoveLayer = paper.Layer.prototype.remove;
|
|
|
|
paper.Layer.prototype.remove = function () {
|
|
if (this.data.isBackgroundGuideLayer) {
|
|
for (const layer of paper.project.layers) {
|
|
if (layer.data.sa_isOnionLayer) {
|
|
storedOnionLayers.push(layer);
|
|
}
|
|
}
|
|
|
|
for (const layer of storedOnionLayers) {
|
|
layer.remove();
|
|
}
|
|
}
|
|
|
|
return originalRemoveLayer.call(this);
|
|
};
|
|
};
|
|
|
|
const injectPaperCanvas = () => {
|
|
let expectingImport = false;
|
|
const PaperCanvas = paperCanvas.constructor; // importImage is called to start loading an image.
|
|
// https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/containers/paper-canvas.jsx#L124
|
|
|
|
const originalImportImage = PaperCanvas.prototype.importImage;
|
|
|
|
PaperCanvas.prototype.importImage = function (...args) {
|
|
expectingImport = true;
|
|
removeOnionLayers();
|
|
return originalImportImage.call(this, ...args);
|
|
}; // recalibrateSize is called when the canvas finishes loading an image.
|
|
// all paths of importImage will result in a call to this method.
|
|
// https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/containers/paper-canvas.jsx#L310-L327
|
|
// We use this to know when to add layers.
|
|
|
|
|
|
const originalRecalibrateSize = PaperCanvas.prototype.recalibrateSize;
|
|
|
|
PaperCanvas.prototype.recalibrateSize = function (callback) {
|
|
return originalRecalibrateSize.call(this, () => {
|
|
if (callback) callback();
|
|
|
|
if (expectingImport) {
|
|
expectingImport = false;
|
|
|
|
if (settings.enabled) {
|
|
updateOnionLayers();
|
|
}
|
|
}
|
|
});
|
|
}; // Prototype overrides will work for all future instances, but Scratch manually binds some methods to `this`
|
|
// so we have to manually copy them for the current instance (but not future instances)
|
|
|
|
|
|
paperCanvas.recalibrateSize = PaperCanvas.prototype.recalibrateSize.bind(paperCanvas);
|
|
paperCanvas.importImage = PaperCanvas.prototype.importImage.bind(paperCanvas);
|
|
};
|
|
|
|
const createOnionLayer = () => {
|
|
const layer = new paper.Layer();
|
|
layer.locked = true;
|
|
layer.guide = true;
|
|
layer.data.sa_isOnionLayer = true;
|
|
return layer;
|
|
}; // Each onion layer update is given an ID
|
|
// Because updating layers is async, we need this to cancel all but the most recent update
|
|
|
|
|
|
let globalUpdateId = 0;
|
|
|
|
const cancelOngoingUpdatesAndGetNewId = () => ++globalUpdateId;
|
|
|
|
const removeOnionLayers = () => {
|
|
cancelOngoingUpdatesAndGetNewId();
|
|
const project = paper.project;
|
|
|
|
if (!project) {
|
|
return;
|
|
}
|
|
|
|
storedOnionLayers.length = 0;
|
|
const layers = project.layers; // Iterate downward because we remove items mid-iteration
|
|
|
|
for (let i = layers.length - 1; i >= 0; i--) {
|
|
const layer = layers[i];
|
|
|
|
if (layer.data.sa_isOnionLayer) {
|
|
layer.remove();
|
|
}
|
|
}
|
|
};
|
|
|
|
const relayerOnionLayers = () => {
|
|
const project = paper.project;
|
|
|
|
if (!project) {
|
|
return;
|
|
}
|
|
|
|
const onionLayer = project.layers.find(i => i.data.sa_isOnionLayer);
|
|
|
|
if (!onionLayer) {
|
|
return;
|
|
}
|
|
|
|
if (settings.layering === "front") {
|
|
project.addLayer(onionLayer);
|
|
} else {
|
|
const rasterLayer = project.layers.find(i => i.data.isRasterLayer);
|
|
|
|
if (rasterLayer.index === 0) {
|
|
project.insertLayer(0, onionLayer);
|
|
} else {
|
|
project.insertLayer(1, onionLayer);
|
|
}
|
|
}
|
|
};
|
|
|
|
const recursePaperItem = (item, callback) => {
|
|
if (item.children) {
|
|
for (const child of item.children) {
|
|
recursePaperItem(child, callback);
|
|
}
|
|
}
|
|
|
|
callback(item);
|
|
};
|
|
|
|
const getTint = (red, green, blue, isBefore) => {
|
|
const referenceColor = isBefore ? settings.beforeTint : settings.afterTint;
|
|
const colorAverage = (red + green + blue) / 3 / 255;
|
|
const WEIGHT = 1.5;
|
|
const weighted = colorAverage / WEIGHT + (1 - 1 / WEIGHT);
|
|
return [referenceColor[0] * weighted, referenceColor[1] * weighted, referenceColor[2] * weighted];
|
|
};
|
|
|
|
const toHexColor = ([red, green, blue]) => {
|
|
const r = Math.round(red).toString(16).padStart(2, "0");
|
|
const g = Math.round(green).toString(16).padStart(2, "0");
|
|
const b = Math.round(blue).toString(16).padStart(2, "0");
|
|
return "#".concat(r).concat(g).concat(b);
|
|
};
|
|
|
|
const getPaperColorTint = (color, isBefore) => toHexColor(getTint(color.red * 255, color.green * 255, color.blue * 255, isBefore));
|
|
|
|
const tintRaster = (raster, isBefore) => {
|
|
const {
|
|
width,
|
|
height
|
|
} = raster.canvas;
|
|
const context = raster.context; // TODO: check to see if this is a performance issue
|
|
|
|
const imageData = context.getImageData(0, 0, width, height);
|
|
const data = imageData.data;
|
|
|
|
for (let i = 0; i < data.length; i += 4
|
|
/* RGBA */
|
|
) {
|
|
const red = data[i + 0];
|
|
const green = data[i + 1];
|
|
const blue = data[i + 2];
|
|
const alpha = data[i + 3];
|
|
|
|
if (alpha === 0) {
|
|
continue;
|
|
}
|
|
|
|
const newTint = getTint(red, green, blue, isBefore);
|
|
data[i + 0] = newTint[0];
|
|
data[i + 1] = newTint[1];
|
|
data[i + 2] = newTint[2];
|
|
}
|
|
|
|
context.putImageData(imageData, 0, 0);
|
|
};
|
|
|
|
const waitForAllRastersToLoad = root => {
|
|
const promises = [];
|
|
recursePaperItem(root, item => {
|
|
if (item instanceof paper.Raster) {
|
|
promises.push(new Promise((resolve, reject) => {
|
|
item.on("load", () => resolve());
|
|
item.on("error", () => reject(new Error("Raster inside SVG failed to load")));
|
|
}));
|
|
}
|
|
});
|
|
return Promise.all(promises);
|
|
};
|
|
|
|
const rasterizeVector = root => {
|
|
const bounds = root.strokeBounds;
|
|
const {
|
|
width,
|
|
height
|
|
} = bounds; // Some browsers experience extremely poor performance when this value exceeds 3840.
|
|
|
|
const MAX_SIZE = 3000;
|
|
const maxScale = Math.min(MAX_SIZE / width, MAX_SIZE / height);
|
|
const raster = new paper.Raster(new paper.Size(width, height));
|
|
raster.remove();
|
|
raster.smoothing = true;
|
|
raster.guide = true;
|
|
raster.locked = true;
|
|
let renderedAtScale = 0;
|
|
const originalDraw = raster.draw;
|
|
|
|
raster.draw = function (...args) {
|
|
const displayedSize = this.getView().getZoom() * window.devicePixelRatio;
|
|
const newScale = Math.max(1, Math.min(maxScale, 2 ** Math.ceil(Math.log2(displayedSize))));
|
|
|
|
if (newScale > renderedAtScale) {
|
|
renderedAtScale = newScale;
|
|
const canvas = this.canvas;
|
|
const ctx = this.context; // Based on https://github.com/LLK/paper.js/blob/16d5ff0267e3a0ef647c25e58182a27300afad20/src/item/Item.js#L1761
|
|
|
|
const scaledWidth = width * newScale;
|
|
const scaledHeight = height * newScale;
|
|
canvas.width = scaledWidth;
|
|
canvas.height = scaledHeight;
|
|
this._size = new paper.Size(scaledWidth, scaledHeight);
|
|
const topLeft = bounds.getTopLeft();
|
|
const bottomRight = bounds.getBottomRight();
|
|
const size = new paper.Size(bottomRight.subtract(topLeft));
|
|
const matrix = new paper.Matrix().scale(newScale).translate(topLeft.negate());
|
|
ctx.save();
|
|
matrix.applyToContext(ctx);
|
|
root.draw(ctx, new paper.Base({
|
|
matrices: [matrix]
|
|
}));
|
|
ctx.restore();
|
|
this.matrix.reset();
|
|
this.transform(new paper.Matrix().translate(topLeft.add(size.divide(2))).scale(1 / newScale));
|
|
}
|
|
|
|
return originalDraw.call(this, ...args);
|
|
};
|
|
|
|
return raster;
|
|
};
|
|
|
|
const makeVectorOnion = (opacity, costume, asset, isBefore) => new Promise((resolve, reject) => {
|
|
const {
|
|
rotationCenterX,
|
|
rotationCenterY
|
|
} = costume; // https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/containers/paper-canvas.jsx#L196-L218
|
|
|
|
asset = asset.split(/<\s*svg:/).join("<");
|
|
asset = asset.split(/<\/\s*svg:/).join("</");
|
|
const svgAttrs = asset.match(/<svg [^>]*>/);
|
|
|
|
if (svgAttrs && svgAttrs[0].indexOf("xmlns=") === -1) {
|
|
asset = asset.replace("<svg ", '<svg xmlns="http://www.w3.org/2000/svg" ');
|
|
}
|
|
|
|
const parser = new DOMParser();
|
|
const svgDom = parser.parseFromString(asset, "text/xml");
|
|
const viewBox = svgDom.documentElement.attributes.viewBox ? svgDom.documentElement.attributes.viewBox.value.match(/\S+/g) : null;
|
|
|
|
if (viewBox) {
|
|
for (let i = 0; i < viewBox.length; i++) {
|
|
viewBox[i] = parseFloat(viewBox[i]);
|
|
}
|
|
}
|
|
|
|
const handleLoad = root => {
|
|
root.opacity = opacity; // https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/containers/paper-canvas.jsx#L274-L275
|
|
|
|
recursePaperItem(root, i => {
|
|
if (i.className === "PathItem") {
|
|
i.clockwise = true;
|
|
}
|
|
|
|
if (i.className !== "PointText" && !i.children) {
|
|
if (i.strokeWidth) {
|
|
i.strokeWidth = i.strokeWidth * 2;
|
|
}
|
|
}
|
|
|
|
i.locked = true;
|
|
i.guide = true;
|
|
});
|
|
root.scale(2, new paper.Point(0, 0));
|
|
|
|
if (settings.mode === "tint") {
|
|
const gradients = new Set();
|
|
recursePaperItem(root, i => {
|
|
if (i.strokeColor) {
|
|
i.strokeColor = getPaperColorTint(i.strokeColor, isBefore);
|
|
}
|
|
|
|
if (i.fillColor) {
|
|
const gradient = i.fillColor.gradient;
|
|
|
|
if (gradient) {
|
|
if (gradients.has(gradient)) return;
|
|
gradients.add(gradient);
|
|
|
|
for (const stop of gradient.stops) {
|
|
stop.color = getPaperColorTint(stop.color, isBefore);
|
|
}
|
|
} else {
|
|
i.fillColor = getPaperColorTint(i.fillColor, isBefore);
|
|
}
|
|
}
|
|
|
|
if (i.canvas) {
|
|
tintRaster(i, isBefore);
|
|
}
|
|
});
|
|
}
|
|
|
|
const paperCenter = getPaperCenter(); // https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/containers/paper-canvas.jsx#L277-L287
|
|
|
|
if (typeof rotationCenterX !== "undefined" && typeof rotationCenterY !== "undefined") {
|
|
let rotationPoint = new paper.Point(rotationCenterX, rotationCenterY);
|
|
|
|
if (viewBox && viewBox.length >= 2 && !isNaN(viewBox[0]) && !isNaN(viewBox[1])) {
|
|
rotationPoint = rotationPoint.subtract(viewBox[0], viewBox[1]);
|
|
}
|
|
|
|
root.translate(paperCenter.subtract(rotationPoint.multiply(2)));
|
|
} else {
|
|
root.translate(paperCenter.subtract(root.bounds.width, root.bounds.height));
|
|
}
|
|
|
|
return rasterizeVector(root);
|
|
};
|
|
|
|
paper.project.importSVG(asset, {
|
|
expandShapes: true,
|
|
insert: false,
|
|
onLoad: root => {
|
|
if (!root) {
|
|
reject(new Error("could not load onion skin"));
|
|
return;
|
|
}
|
|
|
|
resolve(waitForAllRastersToLoad(root).then(() => handleLoad(root)));
|
|
}
|
|
});
|
|
});
|
|
|
|
const makeRasterOnion = (opacity, costume, asset, isBefore) => new Promise((resolve, reject) => {
|
|
let {
|
|
rotationCenterX,
|
|
rotationCenterY
|
|
} = costume;
|
|
const image = new Image();
|
|
|
|
image.onload = () => {
|
|
const paperCenter = getPaperCenter();
|
|
const width = Math.min(paperCenter.x * 2, image.width);
|
|
const height = Math.min(paperCenter.y * 2, image.height); // https://github.com/LLK/scratch-paint/blob/cdf0afc217633e6cfb8ba90ea4ae38b79882cf6c/src/containers/paper-canvas.jsx#L151-L156
|
|
|
|
if (typeof rotationCenterX === "undefined") {
|
|
rotationCenterX = width / 2;
|
|
}
|
|
|
|
if (typeof rotationCenterY === "undefined") {
|
|
rotationCenterY = height / 2;
|
|
}
|
|
|
|
const raster = new paper.Raster(image);
|
|
raster.opacity = opacity;
|
|
raster.guide = true;
|
|
raster.locked = true;
|
|
const x = width / 2 + (paperCenter.x - rotationCenterX);
|
|
const y = height / 2 + (paperCenter.y - rotationCenterY);
|
|
raster.position = new paper.Point(x, y);
|
|
raster.remove();
|
|
|
|
if (settings.mode === "tint") {
|
|
tintRaster(raster, isBefore);
|
|
}
|
|
|
|
resolve(raster);
|
|
};
|
|
|
|
image.onerror = () => {
|
|
reject(new Error("could not load image"));
|
|
};
|
|
|
|
image.src = asset;
|
|
});
|
|
|
|
const getSelectedCostumeIndex = () => {
|
|
const item = document.querySelector("[class*='selector_list-item'][class*='sprite-selector-item_is-selected']");
|
|
if (!item) return -1;
|
|
const numberEl = item.querySelector("[class*='sprite-selector-item_number']");
|
|
if (!numberEl) return -1;
|
|
return +numberEl.textContent - 1;
|
|
};
|
|
|
|
const updateOnionLayers = async () => {
|
|
const project = paper.project;
|
|
|
|
if (!project) {
|
|
return;
|
|
}
|
|
|
|
const selectedCostumeIndex = getSelectedCostumeIndex();
|
|
|
|
if (selectedCostumeIndex === -1) {
|
|
return;
|
|
}
|
|
|
|
removeOnionLayers();
|
|
const localUpdateId = cancelOngoingUpdatesAndGetNewId();
|
|
const vm = addon.tab.traps.vm;
|
|
|
|
if (!vm) {
|
|
return;
|
|
}
|
|
|
|
const originalActiveLayer = project.activeLayer;
|
|
const costumes = vm.editingTarget.sprite.costumes;
|
|
const startIndex = Math.max(0, selectedCostumeIndex - settings.previous);
|
|
const endIndex = Math.min(costumes.length - 1, selectedCostumeIndex + settings.next);
|
|
|
|
try {
|
|
const layersToCreate = [];
|
|
|
|
for (let i = startIndex; i <= endIndex; i++) {
|
|
if (i === selectedCostumeIndex) {
|
|
continue;
|
|
}
|
|
|
|
const isBefore = i < selectedCostumeIndex;
|
|
const distance = Math.abs(i - selectedCostumeIndex) - 1;
|
|
const opacity = (settings.opacity - settings.opacityStep * distance) / 100;
|
|
|
|
if (opacity <= 0) {
|
|
continue;
|
|
}
|
|
|
|
layersToCreate.push({
|
|
index: i,
|
|
isBefore,
|
|
opacity
|
|
});
|
|
}
|
|
|
|
const onions = await Promise.all(layersToCreate.map(({
|
|
index,
|
|
isBefore,
|
|
opacity
|
|
}) => {
|
|
const onionCostume = costumes[index];
|
|
const onionAsset = vm.getCostume(index);
|
|
|
|
if (onionCostume.dataFormat === "svg") {
|
|
return makeVectorOnion(opacity, onionCostume, onionAsset, isBefore);
|
|
} else if (onionCostume.dataFormat === "png" || onionCostume.dataFormat === "jpg") {
|
|
return makeRasterOnion(opacity, onionCostume, onionAsset, isBefore);
|
|
} else {
|
|
throw new Error("Unknown data format: ".concat(onionCostume.dataFormat));
|
|
}
|
|
})); // Make sure we haven't been cancelled
|
|
|
|
if (globalUpdateId === localUpdateId) {
|
|
const layer = createOnionLayer();
|
|
|
|
for (const item of onions) {
|
|
layer.addChild(item);
|
|
}
|
|
|
|
relayerOnionLayers();
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
} // We must make sure to always reset the active layer to avoid corruption.
|
|
|
|
|
|
originalActiveLayer.activate();
|
|
};
|
|
|
|
const setEnabled = _enabled => {
|
|
if (settings.enabled === _enabled) {
|
|
return;
|
|
}
|
|
|
|
settings.enabled = _enabled;
|
|
|
|
if (settings.enabled) {
|
|
if (settings.next === 0 && settings.previous === 0) {
|
|
settings.previous = 1;
|
|
layerInputs.previous.value = settings.previous;
|
|
}
|
|
|
|
if (settings.opacity === 0) {
|
|
settings.opacity = 25;
|
|
layerInputs.opacity.value = settings.opacity;
|
|
}
|
|
|
|
updateOnionLayers();
|
|
} else {
|
|
removeOnionLayers();
|
|
}
|
|
|
|
toggleButton.dataset.enabled = settings.enabled;
|
|
}; //
|
|
// Controls below editor
|
|
//
|
|
|
|
|
|
const settingsChanged = onlyRelayerNeeded => {
|
|
if (settings.previous === 0 && settings.next === 0 || settings.opacity === 0) {
|
|
setEnabled(false);
|
|
return;
|
|
}
|
|
|
|
if (settings.enabled) {
|
|
if (onlyRelayerNeeded) {
|
|
relayerOnionLayers();
|
|
} else {
|
|
updateOnionLayers();
|
|
}
|
|
} else if (settings.previous > 0 || settings.next > 0) {
|
|
setEnabled(true);
|
|
}
|
|
};
|
|
|
|
const createGroup = () => {
|
|
const el = document.createElement("div");
|
|
el.className = "sa-onion-group";
|
|
return el;
|
|
};
|
|
|
|
const createButton = ({
|
|
useButtonTag
|
|
} = {}) => {
|
|
const el = document.createElement(useButtonTag ? "button" : "span");
|
|
el.className = "sa-onion-button";
|
|
el.setAttribute("role", "button");
|
|
return el;
|
|
};
|
|
|
|
const createButtonImage = name => {
|
|
const el = document.createElement("img");
|
|
el.className = "sa-onion-image";
|
|
el.draggable = false;
|
|
el.dataset.image = name;
|
|
el.loading = "lazy";
|
|
el.src = addon.self.getResource("/" + name + ".svg")
|
|
/* rewritten by pull.js */
|
|
;
|
|
return el;
|
|
};
|
|
|
|
const toggleControlsGroup = createGroup();
|
|
addon.tab.displayNoneWhileDisabled(toggleControlsGroup, {
|
|
display: "flex"
|
|
});
|
|
const toggleButton = createButton();
|
|
toggleButton.dataset.enabled = settings.enabled;
|
|
toggleButton.addEventListener("click", () => setEnabled(!settings.enabled));
|
|
toggleButton.title = msg("toggle");
|
|
toggleButton.appendChild(createButtonImage("toggle"));
|
|
const settingButton = createButton();
|
|
settingButton.addEventListener("click", () => setSettingsOpen(!areSettingsOpen()));
|
|
settingButton.title = msg("settings");
|
|
settingButton.appendChild(createButtonImage("settings")); //
|
|
// Settings page
|
|
//
|
|
|
|
const settingPageWrapper = document.createElement("div");
|
|
settingPageWrapper.className = "sa-onion-settings-wrapper";
|
|
toggleControlsGroup.append(settingPageWrapper, toggleButton, settingButton);
|
|
const settingsPage = document.createElement("div");
|
|
settingsPage.className = "sa-onion-settings";
|
|
|
|
const setSettingsOpen = open => {
|
|
settingButton.dataset.enabled = open;
|
|
settingsPage.dataset.visible = open;
|
|
};
|
|
|
|
const areSettingsOpen = () => settingsPage.dataset.visible === "true";
|
|
|
|
const layerInputs = {};
|
|
|
|
for (const type of ["previous", "next", "opacity", "opacityStep"]) {
|
|
const container = document.createElement("label");
|
|
container.className = "sa-onion-settings-line";
|
|
const label = document.createElement("div");
|
|
label.className = "sa-onion-settings-label";
|
|
label.textContent = msg(type);
|
|
container.appendChild(label);
|
|
const group = createGroup();
|
|
const currentButton = createButton();
|
|
const filler = document.createElement("div");
|
|
filler.style.width = "20px";
|
|
currentButton.appendChild(filler);
|
|
const currentInput = document.createElement("input");
|
|
layerInputs[type] = currentInput;
|
|
currentInput.className = "sa-onion-settings-input";
|
|
currentInput.type = "number";
|
|
currentInput.step = "1";
|
|
currentInput.min = "0";
|
|
currentInput.max = "100";
|
|
currentInput.value = settings[type];
|
|
currentInput.addEventListener("input", e => {
|
|
if (currentInput.value.length === 0) {
|
|
settings[type] = 0;
|
|
settingsChanged();
|
|
return;
|
|
}
|
|
|
|
let value = +currentInput.value;
|
|
|
|
if (value > +currentInput.max) {
|
|
value = +currentInput.max;
|
|
} else if (value < 0) {
|
|
value = 0;
|
|
}
|
|
|
|
currentInput.value = value;
|
|
settings[type] = value;
|
|
settingsChanged();
|
|
});
|
|
currentInput.addEventListener("blur", () => {
|
|
if (!currentInput.value) {
|
|
currentInput.value = "0";
|
|
}
|
|
});
|
|
currentButton.appendChild(currentInput);
|
|
const decrementButton = createButton();
|
|
decrementButton.appendChild(createButtonImage("decrement"));
|
|
decrementButton.addEventListener("click", () => {
|
|
if (settings[type] > 0) {
|
|
settings[type]--;
|
|
currentInput.value = settings[type];
|
|
settingsChanged();
|
|
}
|
|
});
|
|
const incrementButton = createButton();
|
|
incrementButton.appendChild(createButtonImage("increment"));
|
|
incrementButton.addEventListener("click", () => {
|
|
if (settings[type] < +currentInput.max) {
|
|
settings[type]++;
|
|
currentInput.value = settings[type];
|
|
settingsChanged();
|
|
}
|
|
});
|
|
group.appendChild(decrementButton);
|
|
group.appendChild(currentButton);
|
|
group.appendChild(incrementButton);
|
|
container.appendChild(group);
|
|
settingsPage.appendChild(container);
|
|
}
|
|
|
|
const modeContainer = document.createElement("div");
|
|
modeContainer.className = "sa-onion-settings-line";
|
|
const modeLabel = document.createElement("div");
|
|
modeLabel.className = "sa-onion-settings-label";
|
|
modeLabel.textContent = msg("mode");
|
|
const modeGroup = createGroup();
|
|
modeContainer.appendChild(modeLabel);
|
|
const modeMergeButton = createButton({
|
|
useButtonTag: true
|
|
});
|
|
modeMergeButton.appendChild(document.createTextNode(msg("merge")));
|
|
modeGroup.appendChild(modeMergeButton);
|
|
modeMergeButton.addEventListener("click", e => {
|
|
settings.mode = "merge";
|
|
modeTintButton.dataset.enabled = false;
|
|
modeMergeButton.dataset.enabled = true;
|
|
settingsChanged();
|
|
});
|
|
modeMergeButton.dataset.enabled = settings.mode === "merge";
|
|
const modeTintButton = createButton({
|
|
useButtonTag: true
|
|
});
|
|
modeTintButton.appendChild(document.createTextNode(msg("tint")));
|
|
modeGroup.appendChild(modeTintButton);
|
|
modeTintButton.addEventListener("click", e => {
|
|
settings.mode = "tint";
|
|
modeTintButton.dataset.enabled = true;
|
|
modeMergeButton.dataset.enabled = false;
|
|
settingsChanged();
|
|
});
|
|
modeTintButton.dataset.enabled = settings.mode === "tint";
|
|
modeContainer.appendChild(modeGroup);
|
|
settingsPage.appendChild(modeContainer);
|
|
const layeringContainer = document.createElement("div");
|
|
layeringContainer.className = "sa-onion-settings-line";
|
|
const layeringLabel = document.createElement("div");
|
|
layeringLabel.className = "sa-onion-settings-label";
|
|
layeringLabel.textContent = msg("layering");
|
|
const layeringGroup = createGroup();
|
|
layeringContainer.appendChild(layeringLabel);
|
|
const layeringFrontButton = createButton({
|
|
useButtonTag: true
|
|
});
|
|
layeringFrontButton.appendChild(document.createTextNode(msg("front")));
|
|
layeringGroup.appendChild(layeringFrontButton);
|
|
layeringFrontButton.addEventListener("click", e => {
|
|
settings.layering = "front";
|
|
layeringBehindButton.dataset.enabled = false;
|
|
layeringFrontButton.dataset.enabled = true;
|
|
settingsChanged(true);
|
|
});
|
|
layeringFrontButton.dataset.enabled = settings.layering === "front";
|
|
const layeringBehindButton = createButton({
|
|
useButtonTag: true
|
|
});
|
|
layeringBehindButton.appendChild(document.createTextNode(msg("behind")));
|
|
layeringGroup.appendChild(layeringBehindButton);
|
|
layeringBehindButton.addEventListener("click", e => {
|
|
settings.layering = "behind";
|
|
layeringBehindButton.dataset.enabled = true;
|
|
layeringFrontButton.dataset.enabled = false;
|
|
settingsChanged(true);
|
|
});
|
|
layeringBehindButton.dataset.enabled = settings.layering === "behind";
|
|
layeringContainer.appendChild(layeringGroup);
|
|
settingsPage.appendChild(layeringContainer);
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
const settingsTip = document.createElementNS(SVG_NS, "svg");
|
|
settingsTip.setAttribute("class", "sa-onion-settings-tip");
|
|
settingsTip.setAttribute("width", "14");
|
|
settingsTip.setAttribute("height", "7");
|
|
const settingsTipShape = document.createElementNS(SVG_NS, "polygon");
|
|
settingsTipShape.setAttribute("class", "sa-onion-settings-polygon");
|
|
settingsTipShape.setAttribute("points", "0,0 7,7, 14,0");
|
|
settingsTip.appendChild(settingsTipShape);
|
|
settingsPage.appendChild(settingsTip);
|
|
let oldEnabled = null;
|
|
addon.self.addEventListener("disabled", () => {
|
|
setSettingsOpen(false);
|
|
oldEnabled = settings.enabled;
|
|
setEnabled(false);
|
|
});
|
|
addon.self.addEventListener("reenabled", () => {
|
|
setEnabled(oldEnabled);
|
|
});
|
|
|
|
const controlsLoop = async () => {
|
|
let hasRunOnce = false;
|
|
|
|
while (true) {
|
|
const canvasControls = await addon.tab.waitForElement("[class^='paint-editor_canvas-controls']", {
|
|
markAsSeen: true,
|
|
reduxEvents: ["scratch-gui/navigation/ACTIVATE_TAB", "scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED", "scratch-gui/locales/SELECT_LOCALE", "scratch-gui/targets/UPDATE_TARGET_LIST"],
|
|
reduxCondition: state => state.scratchGui.editorTab.activeTabIndex === 1 && !state.scratchGui.mode.isPlayerOnly
|
|
});
|
|
const zoomControlsContainer = canvasControls.querySelector("[class^='paint-editor_zoom-controls']");
|
|
addon.tab.appendToSharedSpace({
|
|
space: "paintEditorZoomControls",
|
|
element: toggleControlsGroup,
|
|
order: 1
|
|
});
|
|
settingPageWrapper.appendChild(settingsPage);
|
|
|
|
if (!hasRunOnce) {
|
|
hasRunOnce = true;
|
|
const groupClass = zoomControlsContainer.firstChild.className;
|
|
const buttonClass = zoomControlsContainer.firstChild.firstChild.className;
|
|
const imageClass = zoomControlsContainer.firstChild.firstChild.firstChild.className;
|
|
|
|
for (const el of document.querySelectorAll(".sa-onion-group")) {
|
|
el.className += " " + groupClass;
|
|
}
|
|
|
|
for (const el of document.querySelectorAll(".sa-onion-button")) {
|
|
el.className += " " + buttonClass;
|
|
}
|
|
|
|
for (const el of document.querySelectorAll(".sa-onion-image")) {
|
|
el.className += " " + imageClass;
|
|
}
|
|
}
|
|
|
|
if (settings.enabled) {
|
|
updateOnionLayers();
|
|
}
|
|
}
|
|
};
|
|
|
|
injectPaper();
|
|
injectPaperCanvas();
|
|
controlsLoop();
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/pick-colors-from-stage/_runtime_entry.js":
|
|
/*!********************************************************************!*\
|
|
!*** ./src/addons/addons/pick-colors-from-stage/_runtime_entry.js ***!
|
|
\********************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/pick-colors-from-stage/userscript.js");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./style.css */ "./node_modules/css-loader/index.js!./src/addons/addons/pick-colors-from-stage/style.css");
|
|
/* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__);
|
|
/* generated by pull.js */
|
|
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"],
|
|
"style.css": _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default.a
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/pick-colors-from-stage/userscript.js":
|
|
/*!****************************************************************!*\
|
|
!*** ./src/addons/addons/pick-colors-from-stage/userscript.js ***!
|
|
\****************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
|
|
|
|
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
|
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
msg,
|
|
console
|
|
}) {
|
|
const brand = Symbol();
|
|
|
|
const setIsPicking = picking => document.body.classList.toggle("sa-stage-color-picker-picking", picking); // We only want to handle color picker events from the user clicking on the button, not from
|
|
// addons or other scripts pressing it with click().
|
|
|
|
|
|
let isMostRecentClickUserInitiated = false;
|
|
document.addEventListener("click", e => {
|
|
isMostRecentClickUserInitiated = e.isTrusted;
|
|
}, {
|
|
capture: true
|
|
});
|
|
addon.tab.redux.initialize();
|
|
addon.tab.redux.addEventListener("statechanged", e => {
|
|
const action = e.detail.action; // Do not process events emitted by ourselves.
|
|
|
|
if (action[brand]) {
|
|
return;
|
|
}
|
|
|
|
if (!addon.self.disabled && isMostRecentClickUserInitiated && action.type === "scratch-paint/eye-dropper/ACTIVATE_COLOR_PICKER") {
|
|
setIsPicking(true); // When scratch-paint's color picker is activated, also activate scratch-gui's color picker.
|
|
|
|
addon.tab.redux.dispatch({
|
|
type: "scratch-gui/color-picker/ACTIVATE_COLOR_PICKER",
|
|
callback: color => {
|
|
// callback is called from reducer; do not dispatch events in reducer
|
|
queueMicrotask(() => {
|
|
// By the time we get here, scratch-paint will have already deactivated its eye dropper.
|
|
// If we were to just call the callback, the color would indeed update, but the sliders
|
|
// in the color selector would not update.
|
|
// https://github.com/LLK/scratch-paint/blob/970b72c3e75d0ad44ab54e403a44786ca5f45512/src/containers/color-picker.jsx#L64
|
|
// To work around this, we will re-enable the color picker before running the callback.
|
|
addon.tab.redux.dispatch(_objectSpread(_objectSpread({}, action), {}, {
|
|
[brand]: true
|
|
}));
|
|
action.callback(color);
|
|
|
|
if (action.previousMode) {
|
|
action.previousMode.activate();
|
|
}
|
|
|
|
addon.tab.redux.dispatch({
|
|
type: "scratch-paint/eye-dropper/DEACTIVATE_COLOR_PICKER",
|
|
[brand]: true
|
|
});
|
|
setIsPicking(false);
|
|
});
|
|
}
|
|
});
|
|
} // Don't check for addon being disabled here in case we were dynamically disabled while color
|
|
// picking. This code won't do anything anyways when the previous code doesn't run.
|
|
|
|
|
|
if (action.type === "scratch-paint/eye-dropper/DEACTIVATE_COLOR_PICKER") {
|
|
setIsPicking(false); // When someone selects a color in the scratch-paint picker, cancel the scratch-gui picker
|
|
|
|
if (addon.tab.redux.state.scratchGui.colorPicker.active) {
|
|
addon.tab.redux.dispatch({
|
|
type: "scratch-gui/color-picker/DEACTIVATE_COLOR_PICKER",
|
|
[brand]: true
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/rename-broadcasts/_runtime_entry.js":
|
|
/*!***************************************************************!*\
|
|
!*** ./src/addons/addons/rename-broadcasts/_runtime_entry.js ***!
|
|
\***************************************************************/
|
|
/*! exports provided: resources */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; });
|
|
/* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/rename-broadcasts/userscript.js");
|
|
/* generated by pull.js */
|
|
|
|
const resources = {
|
|
"userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"]
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/addons/rename-broadcasts/userscript.js":
|
|
/*!***********************************************************!*\
|
|
!*** ./src/addons/addons/rename-broadcasts/userscript.js ***!
|
|
\***********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony default export */ __webpack_exports__["default"] = (async function ({
|
|
addon,
|
|
msg,
|
|
console
|
|
}) {
|
|
const vm = addon.tab.traps.vm;
|
|
const Blockly = await addon.tab.traps.getBlockly(); // editor-searchable-dropdowns relies on this value
|
|
|
|
const RENAME_BROADCAST_MESSAGE_ID = "RENAME_BROADCAST_MESSAGE_ID";
|
|
const BROADCAST_MESSAGE_TYPE = Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE;
|
|
const _dropdownCreate = Blockly.FieldVariable.dropdownCreate;
|
|
|
|
Blockly.FieldVariable.dropdownCreate = function () {
|
|
const options = _dropdownCreate.call(this);
|
|
|
|
if (!addon.self.disabled && this.defaultType_ === BROADCAST_MESSAGE_TYPE && // Disable when workspace has no actual broadcast to rename
|
|
this.sourceBlock_.workspace.getVariableTypes().includes("broadcast_msg")) {
|
|
options.push([msg("RENAME_BROADCAST"), RENAME_BROADCAST_MESSAGE_ID]);
|
|
}
|
|
|
|
return options;
|
|
};
|
|
|
|
const _onItemSelected = Blockly.FieldVariable.prototype.onItemSelected;
|
|
|
|
Blockly.FieldVariable.prototype.onItemSelected = function (menu, menuItem) {
|
|
const workspace = this.sourceBlock_.workspace;
|
|
|
|
if (this.sourceBlock_ && workspace) {
|
|
if (menuItem.getValue() === RENAME_BROADCAST_MESSAGE_ID) {
|
|
promptRenameBroadcast(workspace, this.variable_);
|
|
return;
|
|
}
|
|
}
|
|
|
|
return _onItemSelected.call(this, menu, menuItem);
|
|
};
|
|
|
|
const resetVMCaches = () => {
|
|
const blockContainers = new Set(vm.runtime.targets.map(i => i.blocks));
|
|
|
|
for (const blocks of blockContainers) {
|
|
blocks.resetCache();
|
|
}
|
|
};
|
|
|
|
const addUndoRedoHook = callback => {
|
|
const eventQueue = Blockly.Events.FIRE_QUEUE_; // After a rename is emitted, some unrelated garbage events also get emitted
|
|
// So we should trap the first event
|
|
|
|
const undoItem = eventQueue[0];
|
|
const originalRun = undoItem.run;
|
|
|
|
undoItem.run = function (isRedo) {
|
|
originalRun.call(this, isRedo);
|
|
callback(isRedo);
|
|
};
|
|
};
|
|
|
|
const renameBroadcastInVM = (id, newName) => {
|
|
// Editor's rename won't completely rename the variable.
|
|
const vmVariable = vm.runtime.getTargetForStage().variables[id];
|
|
vmVariable.name = newName;
|
|
vmVariable.value = newName; // Update all references to the broadcast. Broadcasts won't work if these
|
|
// don't match.
|
|
|
|
const blockContainers = new Set(vm.runtime.targets.map(i => i.blocks));
|
|
|
|
for (const blockContainer of blockContainers) {
|
|
for (const block of Object.values(blockContainer._blocks)) {
|
|
const broadcastOption = block.fields && block.fields.BROADCAST_OPTION;
|
|
|
|
if (broadcastOption && broadcastOption.id === id) {
|
|
broadcastOption.value = newName;
|
|
}
|
|
}
|
|
}
|
|
|
|
resetVMCaches();
|
|
};
|
|
|
|
const renameBroadcast = (workspace, id, oldName, newName) => {
|
|
// Rename in editor. Undo/redo will work automatically.
|
|
workspace.renameVariableById(id, newName); // Rename in VM. Need to manually implement undo/redo.
|
|
|
|
renameBroadcastInVM(id, newName);
|
|
addUndoRedoHook(isRedo => {
|
|
if (isRedo) {
|
|
renameBroadcastInVM(id, newName);
|
|
} else {
|
|
renameBroadcastInVM(id, oldName);
|
|
}
|
|
});
|
|
};
|
|
|
|
const mergeBroadcast = (workspace, oldId, oldName, newName) => {
|
|
const newVmVariable = vm.runtime.getTargetForStage().lookupBroadcastByInputValue(newName);
|
|
const newId = newVmVariable.id; // Merge in editor. Undo/redo will work automatically for this.
|
|
// Use group so that everything here is undone/redone at the same time.
|
|
|
|
Blockly.Events.setGroup(true);
|
|
|
|
for (const block of workspace.getAllBlocks()) {
|
|
for (const input of block.inputList) {
|
|
for (const field of input.fieldRow) {
|
|
if (field.name === "BROADCAST_OPTION" && field.getValue() === oldId) {
|
|
field.setValue(newId);
|
|
}
|
|
}
|
|
}
|
|
} // Remove the broadcast from the editor so it doesn't appear in dropdowns.
|
|
// Undo/redo will work automatically for this.
|
|
|
|
|
|
workspace.deleteVariableById(oldId);
|
|
Blockly.Events.setGroup(false); // Merge in VM to update sprites that aren't open. Need to manually implement undo/redo.
|
|
// To figure out how to undo this operation, we first figure out which blocks we're
|
|
// going to touch and keep hold of that list.
|
|
|
|
const vmBlocksToUpdate = [];
|
|
const blockContainers = new Set(vm.runtime.targets.map(i => i.blocks));
|
|
|
|
for (const blockContainer of blockContainers) {
|
|
for (const block of Object.values(blockContainer._blocks)) {
|
|
const broadcastOption = block.fields && block.fields.BROADCAST_OPTION;
|
|
|
|
if (broadcastOption && broadcastOption.id === oldId) {
|
|
vmBlocksToUpdate.push(block);
|
|
}
|
|
}
|
|
}
|
|
|
|
const applyVmEdits = isRedo => {
|
|
const idToReplaceWith = isRedo ? newId : oldId;
|
|
const nameToReplaceWith = isRedo ? newName : oldName;
|
|
|
|
for (const block of vmBlocksToUpdate) {
|
|
const broadcastOption = block.fields.BROADCAST_OPTION;
|
|
broadcastOption.id = idToReplaceWith;
|
|
broadcastOption.value = nameToReplaceWith;
|
|
}
|
|
|
|
resetVMCaches();
|
|
};
|
|
|
|
applyVmEdits(true); // Earlier editor updates are guaranteed to generate at least 1 event that we can hook as the
|
|
// broadcast block must exist in the editor for the user to rename it.
|
|
|
|
addUndoRedoHook(isRedo => {
|
|
applyVmEdits(isRedo);
|
|
});
|
|
};
|
|
|
|
const promptRenameBroadcast = (workspace, variable) => {
|
|
const modalTitle = msg("RENAME_BROADCAST_MODAL_TITLE");
|
|
const oldName = variable.name;
|
|
const id = variable.getId();
|
|
const promptText = msg("RENAME_BROADCAST_TITLE", {
|
|
name: oldName
|
|
});
|
|
const promptDefaultText = oldName;
|
|
Blockly.prompt(promptText, promptDefaultText, function (newName) {
|
|
newName = Blockly.Variables.trimName_(newName);
|
|
const nameIsEmpty = !newName;
|
|
|
|
if (nameIsEmpty) {
|
|
return;
|
|
}
|
|
|
|
const variableAlreadyExists = !!workspace.getVariable(newName, BROADCAST_MESSAGE_TYPE);
|
|
|
|
if (variableAlreadyExists) {
|
|
mergeBroadcast(workspace, id, oldName, newName);
|
|
} else {
|
|
renameBroadcast(workspace, id, oldName, newName);
|
|
}
|
|
}, modalTitle, BROADCAST_MESSAGE_TYPE);
|
|
};
|
|
|
|
const updateExistingMenuGenerators = () => {
|
|
const workspace = Blockly.getMainWorkspace();
|
|
const flyout = workspace && workspace.getFlyout();
|
|
|
|
if (workspace && flyout) {
|
|
const allBlocks = [...workspace.getAllBlocks(), ...flyout.getWorkspace().getAllBlocks()];
|
|
|
|
for (const block of allBlocks) {
|
|
for (const input of block.inputList) {
|
|
for (const field of input.fieldRow) {
|
|
if (field instanceof Blockly.FieldVariable) {
|
|
field.menuGenerator_ = Blockly.FieldVariable.dropdownCreate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
updateExistingMenuGenerators();
|
|
});
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/libraries/common/cs/normalize-color.js":
|
|
/*!***********************************************************!*\
|
|
!*** ./src/addons/libraries/common/cs/normalize-color.js ***!
|
|
\***********************************************************/
|
|
/*! exports provided: getHexRegex, normalizeHex */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getHexRegex", function() { return getHexRegex; });
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "normalizeHex", function() { return normalizeHex; });
|
|
const getHexRegex = () => /^#?[0-9a-fA-F]{3,8}$/;
|
|
const normalizeHex = input => {
|
|
let hex = String(input);
|
|
if (!getHexRegex().test(hex)) return "#000000";
|
|
if (!hex.startsWith("#")) hex = "#".concat(hex);
|
|
|
|
if (hex.length === 4) {
|
|
const [_, r, g, b] = hex;
|
|
hex = "#".concat(r).concat(r).concat(g).concat(g).concat(b).concat(b);
|
|
}
|
|
|
|
return hex.toLowerCase();
|
|
};
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/libraries/common/cs/rate-limiter.js":
|
|
/*!********************************************************!*\
|
|
!*** ./src/addons/libraries/common/cs/rate-limiter.js ***!
|
|
\********************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return RateLimiter; });
|
|
class RateLimiter {
|
|
constructor(wait) {
|
|
this.timeout = null;
|
|
this.callback = null;
|
|
this.wait = wait;
|
|
}
|
|
|
|
abort(call = true) {
|
|
if (this.timeout) {
|
|
clearTimeout(this.timeout);
|
|
if (call) this.callback();
|
|
this.timeout = this.callback = null;
|
|
}
|
|
}
|
|
|
|
limit(callback) {
|
|
this.abort(false);
|
|
this.callback = callback;
|
|
this.timeout = setTimeout(() => {
|
|
this.timeout = this.callback = null;
|
|
callback();
|
|
}, this.wait);
|
|
}
|
|
|
|
}
|
|
|
|
/***/ }),
|
|
|
|
/***/ "./src/addons/libraries/thirdparty/cs/tinycolor-min.js":
|
|
/*!*************************************************************!*\
|
|
!*** ./src/addons/libraries/thirdparty/cs/tinycolor-min.js ***!
|
|
\*************************************************************/
|
|
/*! exports provided: default */
|
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
|
|
"use strict";
|
|
__webpack_require__.r(__webpack_exports__);
|
|
// TinyColor v1.4.2
|
|
// https://github.com/bgrins/TinyColor
|
|
// Brian Grinstead, MIT License
|
|
// Modified to use ES6 export
|
|
const tinycolor = function (Math) {
|
|
var trimLeft = /^\s+/,
|
|
trimRight = /\s+$/,
|
|
tinyCounter = 0,
|
|
mathRound = Math.round,
|
|
mathMin = Math.min,
|
|
mathMax = Math.max,
|
|
mathRandom = Math.random;
|
|
|
|
function tinycolor(color, opts) {
|
|
color = color ? color : '';
|
|
opts = opts || {}; // If input is already a tinycolor, return itself
|
|
|
|
if (color instanceof tinycolor) {
|
|
return color;
|
|
} // If we are called as a function, call using new instead
|
|
|
|
|
|
if (!(this instanceof tinycolor)) {
|
|
return new tinycolor(color, opts);
|
|
}
|
|
|
|
var rgb = inputToRGB(color);
|
|
this._originalInput = color, this._r = rgb.r, this._g = rgb.g, this._b = rgb.b, this._a = rgb.a, this._roundA = mathRound(100 * this._a) / 100, this._format = opts.format || rgb.format;
|
|
this._gradientType = opts.gradientType; // Don't let the range of [0,255] come back in [0,1].
|
|
// Potentially lose a little bit of precision here, but will fix issues where
|
|
// .5 gets interpreted as half of the total, instead of half of 1
|
|
// If it was supposed to be 128, this was already taken care of by `inputToRgb`
|
|
|
|
if (this._r < 1) {
|
|
this._r = mathRound(this._r);
|
|
}
|
|
|
|
if (this._g < 1) {
|
|
this._g = mathRound(this._g);
|
|
}
|
|
|
|
if (this._b < 1) {
|
|
this._b = mathRound(this._b);
|
|
}
|
|
|
|
this._ok = rgb.ok;
|
|
this._tc_id = tinyCounter++;
|
|
}
|
|
|
|
tinycolor.prototype = {
|
|
isDark: function isDark() {
|
|
return this.getBrightness() < 128;
|
|
},
|
|
isLight: function isLight() {
|
|
return !this.isDark();
|
|
},
|
|
isValid: function isValid() {
|
|
return this._ok;
|
|
},
|
|
getOriginalInput: function getOriginalInput() {
|
|
return this._originalInput;
|
|
},
|
|
getFormat: function getFormat() {
|
|
return this._format;
|
|
},
|
|
getAlpha: function getAlpha() {
|
|
return this._a;
|
|
},
|
|
getBrightness: function getBrightness() {
|
|
//http://www.w3.org/TR/AERT#color-contrast
|
|
var rgb = this.toRgb();
|
|
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
|
|
},
|
|
getLuminance: function getLuminance() {
|
|
//http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
|
var rgb = this.toRgb();
|
|
var RsRGB, GsRGB, BsRGB, R, G, B;
|
|
RsRGB = rgb.r / 255;
|
|
GsRGB = rgb.g / 255;
|
|
BsRGB = rgb.b / 255;
|
|
|
|
if (RsRGB <= 0.03928) {
|
|
R = RsRGB / 12.92;
|
|
} else {
|
|
R = Math.pow((RsRGB + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
if (GsRGB <= 0.03928) {
|
|
G = GsRGB / 12.92;
|
|
} else {
|
|
G = Math.pow((GsRGB + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
if (BsRGB <= 0.03928) {
|
|
B = BsRGB / 12.92;
|
|
} else {
|
|
B = Math.pow((BsRGB + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
},
|
|
setAlpha: function setAlpha(value) {
|
|
this._a = boundAlpha(value);
|
|
this._roundA = mathRound(100 * this._a) / 100;
|
|
return this;
|
|
},
|
|
toHsv: function toHsv() {
|
|
var hsv = rgbToHsv(this._r, this._g, this._b);
|
|
return {
|
|
h: hsv.h * 360,
|
|
s: hsv.s,
|
|
v: hsv.v,
|
|
a: this._a
|
|
};
|
|
},
|
|
toHsvString: function toHsvString() {
|
|
var hsv = rgbToHsv(this._r, this._g, this._b);
|
|
var h = mathRound(hsv.h * 360),
|
|
s = mathRound(hsv.s * 100),
|
|
v = mathRound(hsv.v * 100);
|
|
return this._a == 1 ? "hsv(" + h + ", " + s + "%, " + v + "%)" : "hsva(" + h + ", " + s + "%, " + v + "%, " + this._roundA + ")";
|
|
},
|
|
toHsl: function toHsl() {
|
|
var hsl = rgbToHsl(this._r, this._g, this._b);
|
|
return {
|
|
h: hsl.h * 360,
|
|
s: hsl.s,
|
|
l: hsl.l,
|
|
a: this._a
|
|
};
|
|
},
|
|
toHslString: function toHslString() {
|
|
var hsl = rgbToHsl(this._r, this._g, this._b);
|
|
var h = mathRound(hsl.h * 360),
|
|
s = mathRound(hsl.s * 100),
|
|
l = mathRound(hsl.l * 100);
|
|
return this._a == 1 ? "hsl(" + h + ", " + s + "%, " + l + "%)" : "hsla(" + h + ", " + s + "%, " + l + "%, " + this._roundA + ")";
|
|
},
|
|
toHex: function toHex(allow3Char) {
|
|
return rgbToHex(this._r, this._g, this._b, allow3Char);
|
|
},
|
|
toHexString: function toHexString(allow3Char) {
|
|
return '#' + this.toHex(allow3Char);
|
|
},
|
|
toHex8: function toHex8(allow4Char) {
|
|
return rgbaToHex(this._r, this._g, this._b, this._a, allow4Char);
|
|
},
|
|
toHex8String: function toHex8String(allow4Char) {
|
|
return '#' + this.toHex8(allow4Char);
|
|
},
|
|
toRgb: function toRgb() {
|
|
return {
|
|
r: mathRound(this._r),
|
|
g: mathRound(this._g),
|
|
b: mathRound(this._b),
|
|
a: this._a
|
|
};
|
|
},
|
|
toRgbString: function toRgbString() {
|
|
return this._a == 1 ? "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" : "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
|
|
},
|
|
toPercentageRgb: function toPercentageRgb() {
|
|
return {
|
|
r: mathRound(bound01(this._r, 255) * 100) + "%",
|
|
g: mathRound(bound01(this._g, 255) * 100) + "%",
|
|
b: mathRound(bound01(this._b, 255) * 100) + "%",
|
|
a: this._a
|
|
};
|
|
},
|
|
toPercentageRgbString: function toPercentageRgbString() {
|
|
return this._a == 1 ? "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" : "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
|
|
},
|
|
toName: function toName() {
|
|
if (this._a === 0) {
|
|
return "transparent";
|
|
}
|
|
|
|
if (this._a < 1) {
|
|
return false;
|
|
}
|
|
|
|
return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
|
|
},
|
|
toFilter: function toFilter(secondColor) {
|
|
var hex8String = '#' + rgbaToArgbHex(this._r, this._g, this._b, this._a);
|
|
var secondHex8String = hex8String;
|
|
var gradientType = this._gradientType ? "GradientType = 1, " : "";
|
|
|
|
if (secondColor) {
|
|
var s = tinycolor(secondColor);
|
|
secondHex8String = '#' + rgbaToArgbHex(s._r, s._g, s._b, s._a);
|
|
}
|
|
|
|
return "progid:DXImageTransform.Microsoft.gradient(" + gradientType + "startColorstr=" + hex8String + ",endColorstr=" + secondHex8String + ")";
|
|
},
|
|
toString: function toString(format) {
|
|
var formatSet = !!format;
|
|
format = format || this._format;
|
|
var formattedString = false;
|
|
var hasAlpha = this._a < 1 && this._a >= 0;
|
|
var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "hex4" || format === "hex8" || format === "name");
|
|
|
|
if (needsAlphaFormat) {
|
|
// Special case for "transparent", all other non-alpha formats
|
|
// will return rgba when there is transparency.
|
|
if (format === "name" && this._a === 0) {
|
|
return this.toName();
|
|
}
|
|
|
|
return this.toRgbString();
|
|
}
|
|
|
|
if (format === "rgb") {
|
|
formattedString = this.toRgbString();
|
|
}
|
|
|
|
if (format === "prgb") {
|
|
formattedString = this.toPercentageRgbString();
|
|
}
|
|
|
|
if (format === "hex" || format === "hex6") {
|
|
formattedString = this.toHexString();
|
|
}
|
|
|
|
if (format === "hex3") {
|
|
formattedString = this.toHexString(true);
|
|
}
|
|
|
|
if (format === "hex4") {
|
|
formattedString = this.toHex8String(true);
|
|
}
|
|
|
|
if (format === "hex8") {
|
|
formattedString = this.toHex8String();
|
|
}
|
|
|
|
if (format === "name") {
|
|
formattedString = this.toName();
|
|
}
|
|
|
|
if (format === "hsl") {
|
|
formattedString = this.toHslString();
|
|
}
|
|
|
|
if (format === "hsv") {
|
|
formattedString = this.toHsvString();
|
|
}
|
|
|
|
return formattedString || this.toHexString();
|
|
},
|
|
clone: function clone() {
|
|
return tinycolor(this.toString());
|
|
},
|
|
_applyModification: function _applyModification(fn, args) {
|
|
var color = fn.apply(null, [this].concat([].slice.call(args)));
|
|
this._r = color._r;
|
|
this._g = color._g;
|
|
this._b = color._b;
|
|
this.setAlpha(color._a);
|
|
return this;
|
|
},
|
|
lighten: function lighten() {
|
|
return this._applyModification(_lighten, arguments);
|
|
},
|
|
brighten: function brighten() {
|
|
return this._applyModification(_brighten, arguments);
|
|
},
|
|
darken: function darken() {
|
|
return this._applyModification(_darken, arguments);
|
|
},
|
|
desaturate: function desaturate() {
|
|
return this._applyModification(_desaturate, arguments);
|
|
},
|
|
saturate: function saturate() {
|
|
return this._applyModification(_saturate, arguments);
|
|
},
|
|
greyscale: function greyscale() {
|
|
return this._applyModification(_greyscale, arguments);
|
|
},
|
|
spin: function spin() {
|
|
return this._applyModification(_spin, arguments);
|
|
},
|
|
_applyCombination: function _applyCombination(fn, args) {
|
|
return fn.apply(null, [this].concat([].slice.call(args)));
|
|
},
|
|
analogous: function analogous() {
|
|
return this._applyCombination(_analogous, arguments);
|
|
},
|
|
complement: function complement() {
|
|
return this._applyCombination(_complement, arguments);
|
|
},
|
|
monochromatic: function monochromatic() {
|
|
return this._applyCombination(_monochromatic, arguments);
|
|
},
|
|
splitcomplement: function splitcomplement() {
|
|
return this._applyCombination(_splitcomplement, arguments);
|
|
},
|
|
triad: function triad() {
|
|
return this._applyCombination(_triad, arguments);
|
|
},
|
|
tetrad: function tetrad() {
|
|
return this._applyCombination(_tetrad, arguments);
|
|
}
|
|
}; // If input is an object, force 1 into "1.0" to handle ratios properly
|
|
// String input requires "1.0" as input, so 1 will be treated as 1
|
|
|
|
tinycolor.fromRatio = function (color, opts) {
|
|
if (typeof color == "object") {
|
|
var newColor = {};
|
|
|
|
for (var i in color) {
|
|
if (color.hasOwnProperty(i)) {
|
|
if (i === "a") {
|
|
newColor[i] = color[i];
|
|
} else {
|
|
newColor[i] = convertToPercentage(color[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
color = newColor;
|
|
}
|
|
|
|
return tinycolor(color, opts);
|
|
}; // Given a string or object, convert that input to RGB
|
|
// Possible string inputs:
|
|
//
|
|
// "red"
|
|
// "#f00" or "f00"
|
|
// "#ff0000" or "ff0000"
|
|
// "#ff000000" or "ff000000"
|
|
// "rgb 255 0 0" or "rgb (255, 0, 0)"
|
|
// "rgb 1.0 0 0" or "rgb (1, 0, 0)"
|
|
// "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
|
|
// "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
|
|
// "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
|
|
// "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
|
|
// "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
|
|
//
|
|
|
|
|
|
function inputToRGB(color) {
|
|
var rgb = {
|
|
r: 0,
|
|
g: 0,
|
|
b: 0
|
|
};
|
|
var a = 1;
|
|
var s = null;
|
|
var v = null;
|
|
var l = null;
|
|
var ok = false;
|
|
var format = false;
|
|
|
|
if (typeof color == "string") {
|
|
color = stringInputToObject(color);
|
|
}
|
|
|
|
if (typeof color == "object") {
|
|
if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
|
|
rgb = rgbToRgb(color.r, color.g, color.b);
|
|
ok = true;
|
|
format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
|
|
} else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
|
|
s = convertToPercentage(color.s);
|
|
v = convertToPercentage(color.v);
|
|
rgb = hsvToRgb(color.h, s, v);
|
|
ok = true;
|
|
format = "hsv";
|
|
} else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
|
|
s = convertToPercentage(color.s);
|
|
l = convertToPercentage(color.l);
|
|
rgb = hslToRgb(color.h, s, l);
|
|
ok = true;
|
|
format = "hsl";
|
|
}
|
|
|
|
if (color.hasOwnProperty("a")) {
|
|
a = color.a;
|
|
}
|
|
}
|
|
|
|
a = boundAlpha(a);
|
|
return {
|
|
ok: ok,
|
|
format: color.format || format,
|
|
r: mathMin(255, mathMax(rgb.r, 0)),
|
|
g: mathMin(255, mathMax(rgb.g, 0)),
|
|
b: mathMin(255, mathMax(rgb.b, 0)),
|
|
a: a
|
|
};
|
|
} // Conversion Functions
|
|
// --------------------
|
|
// `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
|
|
// <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
|
|
// `rgbToRgb`
|
|
// Handle bounds / percentage checking to conform to CSS color spec
|
|
// <http://www.w3.org/TR/css3-color/>
|
|
// *Assumes:* r, g, b in [0, 255] or [0, 1]
|
|
// *Returns:* { r, g, b } in [0, 255]
|
|
|
|
|
|
function rgbToRgb(r, g, b) {
|
|
return {
|
|
r: bound01(r, 255) * 255,
|
|
g: bound01(g, 255) * 255,
|
|
b: bound01(b, 255) * 255
|
|
};
|
|
} // `rgbToHsl`
|
|
// Converts an RGB color value to HSL.
|
|
// *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
|
|
// *Returns:* { h, s, l } in [0,1]
|
|
|
|
|
|
function rgbToHsl(r, g, b) {
|
|
r = bound01(r, 255);
|
|
g = bound01(g, 255);
|
|
b = bound01(b, 255);
|
|
var max = mathMax(r, g, b),
|
|
min = mathMin(r, g, b);
|
|
var h,
|
|
s,
|
|
l = (max + min) / 2;
|
|
|
|
if (max == min) {
|
|
h = s = 0; // achromatic
|
|
} else {
|
|
var d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
break;
|
|
|
|
case g:
|
|
h = (b - r) / d + 2;
|
|
break;
|
|
|
|
case b:
|
|
h = (r - g) / d + 4;
|
|
break;
|
|
}
|
|
|
|
h /= 6;
|
|
}
|
|
|
|
return {
|
|
h: h,
|
|
s: s,
|
|
l: l
|
|
};
|
|
} // `hslToRgb`
|
|
// Converts an HSL color value to RGB.
|
|
// *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
|
|
// *Returns:* { r, g, b } in the set [0, 255]
|
|
|
|
|
|
function hslToRgb(h, s, l) {
|
|
var r, g, b;
|
|
h = bound01(h, 360);
|
|
s = bound01(s, 100);
|
|
l = bound01(l, 100);
|
|
|
|
function hue2rgb(p, q, t) {
|
|
if (t < 0) t += 1;
|
|
if (t > 1) t -= 1;
|
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
if (t < 1 / 2) return q;
|
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
return p;
|
|
}
|
|
|
|
if (s === 0) {
|
|
r = g = b = l; // achromatic
|
|
} else {
|
|
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
var p = 2 * l - q;
|
|
r = hue2rgb(p, q, h + 1 / 3);
|
|
g = hue2rgb(p, q, h);
|
|
b = hue2rgb(p, q, h - 1 / 3);
|
|
}
|
|
|
|
return {
|
|
r: r * 255,
|
|
g: g * 255,
|
|
b: b * 255
|
|
};
|
|
} // `rgbToHsv`
|
|
// Converts an RGB color value to HSV
|
|
// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
|
|
// *Returns:* { h, s, v } in [0,1]
|
|
|
|
|
|
function rgbToHsv(r, g, b) {
|
|
r = bound01(r, 255);
|
|
g = bound01(g, 255);
|
|
b = bound01(b, 255);
|
|
var max = mathMax(r, g, b),
|
|
min = mathMin(r, g, b);
|
|
var h,
|
|
s,
|
|
v = max;
|
|
var d = max - min;
|
|
s = max === 0 ? 0 : d / max;
|
|
|
|
if (max == min) {
|
|
h = 0; // achromatic
|
|
} else {
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
break;
|
|
|
|
case g:
|
|
h = (b - r) / d + 2;
|
|
break;
|
|
|
|
case b:
|
|
h = (r - g) / d + 4;
|
|
break;
|
|
}
|
|
|
|
h /= 6;
|
|
}
|
|
|
|
return {
|
|
h: h,
|
|
s: s,
|
|
v: v
|
|
};
|
|
} // `hsvToRgb`
|
|
// Converts an HSV color value to RGB.
|
|
// *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
|
|
// *Returns:* { r, g, b } in the set [0, 255]
|
|
|
|
|
|
function hsvToRgb(h, s, v) {
|
|
h = bound01(h, 360) * 6;
|
|
s = bound01(s, 100);
|
|
v = bound01(v, 100);
|
|
var i = Math.floor(h),
|
|
f = h - i,
|
|
p = v * (1 - s),
|
|
q = v * (1 - f * s),
|
|
t = v * (1 - (1 - f) * s),
|
|
mod = i % 6,
|
|
r = [v, q, p, p, t, v][mod],
|
|
g = [t, v, v, q, p, p][mod],
|
|
b = [p, p, t, v, v, q][mod];
|
|
return {
|
|
r: r * 255,
|
|
g: g * 255,
|
|
b: b * 255
|
|
};
|
|
} // `rgbToHex`
|
|
// Converts an RGB color to hex
|
|
// Assumes r, g, and b are contained in the set [0, 255]
|
|
// Returns a 3 or 6 character hex
|
|
|
|
|
|
function rgbToHex(r, g, b, allow3Char) {
|
|
var hex = [pad2(mathRound(r).toString(16)), pad2(mathRound(g).toString(16)), pad2(mathRound(b).toString(16))]; // Return a 3 character hex if possible
|
|
|
|
if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
|
|
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
|
|
}
|
|
|
|
return hex.join("");
|
|
} // `rgbaToHex`
|
|
// Converts an RGBA color plus alpha transparency to hex
|
|
// Assumes r, g, b are contained in the set [0, 255] and
|
|
// a in [0, 1]. Returns a 4 or 8 character rgba hex
|
|
|
|
|
|
function rgbaToHex(r, g, b, a, allow4Char) {
|
|
var hex = [pad2(mathRound(r).toString(16)), pad2(mathRound(g).toString(16)), pad2(mathRound(b).toString(16)), pad2(convertDecimalToHex(a))]; // Return a 4 character hex if possible
|
|
|
|
if (allow4Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1) && hex[3].charAt(0) == hex[3].charAt(1)) {
|
|
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
|
|
}
|
|
|
|
return hex.join("");
|
|
} // `rgbaToArgbHex`
|
|
// Converts an RGBA color to an ARGB Hex8 string
|
|
// Rarely used, but required for "toFilter()"
|
|
|
|
|
|
function rgbaToArgbHex(r, g, b, a) {
|
|
var hex = [pad2(convertDecimalToHex(a)), pad2(mathRound(r).toString(16)), pad2(mathRound(g).toString(16)), pad2(mathRound(b).toString(16))];
|
|
return hex.join("");
|
|
} // `equals`
|
|
// Can be called with any tinycolor input
|
|
|
|
|
|
tinycolor.equals = function (color1, color2) {
|
|
if (!color1 || !color2) {
|
|
return false;
|
|
}
|
|
|
|
return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
|
|
};
|
|
|
|
tinycolor.random = function () {
|
|
return tinycolor.fromRatio({
|
|
r: mathRandom(),
|
|
g: mathRandom(),
|
|
b: mathRandom()
|
|
});
|
|
}; // Modification Functions
|
|
// ----------------------
|
|
// Thanks to less.js for some of the basics here
|
|
// <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>
|
|
|
|
|
|
function _desaturate(color, amount) {
|
|
amount = amount === 0 ? 0 : amount || 10;
|
|
var hsl = tinycolor(color).toHsl();
|
|
hsl.s -= amount / 100;
|
|
hsl.s = clamp01(hsl.s);
|
|
return tinycolor(hsl);
|
|
}
|
|
|
|
function _saturate(color, amount) {
|
|
amount = amount === 0 ? 0 : amount || 10;
|
|
var hsl = tinycolor(color).toHsl();
|
|
hsl.s += amount / 100;
|
|
hsl.s = clamp01(hsl.s);
|
|
return tinycolor(hsl);
|
|
}
|
|
|
|
function _greyscale(color) {
|
|
return tinycolor(color).desaturate(100);
|
|
}
|
|
|
|
function _lighten(color, amount) {
|
|
amount = amount === 0 ? 0 : amount || 10;
|
|
var hsl = tinycolor(color).toHsl();
|
|
hsl.l += amount / 100;
|
|
hsl.l = clamp01(hsl.l);
|
|
return tinycolor(hsl);
|
|
}
|
|
|
|
function _brighten(color, amount) {
|
|
amount = amount === 0 ? 0 : amount || 10;
|
|
var rgb = tinycolor(color).toRgb();
|
|
rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * -(amount / 100))));
|
|
rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * -(amount / 100))));
|
|
rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * -(amount / 100))));
|
|
return tinycolor(rgb);
|
|
}
|
|
|
|
function _darken(color, amount) {
|
|
amount = amount === 0 ? 0 : amount || 10;
|
|
var hsl = tinycolor(color).toHsl();
|
|
hsl.l -= amount / 100;
|
|
hsl.l = clamp01(hsl.l);
|
|
return tinycolor(hsl);
|
|
} // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
|
|
// Values outside of this range will be wrapped into this range.
|
|
|
|
|
|
function _spin(color, amount) {
|
|
var hsl = tinycolor(color).toHsl();
|
|
var hue = (hsl.h + amount) % 360;
|
|
hsl.h = hue < 0 ? 360 + hue : hue;
|
|
return tinycolor(hsl);
|
|
} // Combination Functions
|
|
// ---------------------
|
|
// Thanks to jQuery xColor for some of the ideas behind these
|
|
// <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>
|
|
|
|
|
|
function _complement(color) {
|
|
var hsl = tinycolor(color).toHsl();
|
|
hsl.h = (hsl.h + 180) % 360;
|
|
return tinycolor(hsl);
|
|
}
|
|
|
|
function _triad(color) {
|
|
var hsl = tinycolor(color).toHsl();
|
|
var h = hsl.h;
|
|
return [tinycolor(color), tinycolor({
|
|
h: (h + 120) % 360,
|
|
s: hsl.s,
|
|
l: hsl.l
|
|
}), tinycolor({
|
|
h: (h + 240) % 360,
|
|
s: hsl.s,
|
|
l: hsl.l
|
|
})];
|
|
}
|
|
|
|
function _tetrad(color) {
|
|
var hsl = tinycolor(color).toHsl();
|
|
var h = hsl.h;
|
|
return [tinycolor(color), tinycolor({
|
|
h: (h + 90) % 360,
|
|
s: hsl.s,
|
|
l: hsl.l
|
|
}), tinycolor({
|
|
h: (h + 180) % 360,
|
|
s: hsl.s,
|
|
l: hsl.l
|
|
}), tinycolor({
|
|
h: (h + 270) % 360,
|
|
s: hsl.s,
|
|
l: hsl.l
|
|
})];
|
|
}
|
|
|
|
function _splitcomplement(color) {
|
|
var hsl = tinycolor(color).toHsl();
|
|
var h = hsl.h;
|
|
return [tinycolor(color), tinycolor({
|
|
h: (h + 72) % 360,
|
|
s: hsl.s,
|
|
l: hsl.l
|
|
}), tinycolor({
|
|
h: (h + 216) % 360,
|
|
s: hsl.s,
|
|
l: hsl.l
|
|
})];
|
|
}
|
|
|
|
function _analogous(color, results, slices) {
|
|
results = results || 6;
|
|
slices = slices || 30;
|
|
var hsl = tinycolor(color).toHsl();
|
|
var part = 360 / slices;
|
|
var ret = [tinycolor(color)];
|
|
|
|
for (hsl.h = (hsl.h - (part * results >> 1) + 720) % 360; --results;) {
|
|
hsl.h = (hsl.h + part) % 360;
|
|
ret.push(tinycolor(hsl));
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
function _monochromatic(color, results) {
|
|
results = results || 6;
|
|
var hsv = tinycolor(color).toHsv();
|
|
var h = hsv.h,
|
|
s = hsv.s,
|
|
v = hsv.v;
|
|
var ret = [];
|
|
var modification = 1 / results;
|
|
|
|
while (results--) {
|
|
ret.push(tinycolor({
|
|
h: h,
|
|
s: s,
|
|
v: v
|
|
}));
|
|
v = (v + modification) % 1;
|
|
}
|
|
|
|
return ret;
|
|
} // Utility Functions
|
|
// ---------------------
|
|
|
|
|
|
tinycolor.mix = function (color1, color2, amount) {
|
|
amount = amount === 0 ? 0 : amount || 50;
|
|
var rgb1 = tinycolor(color1).toRgb();
|
|
var rgb2 = tinycolor(color2).toRgb();
|
|
var p = amount / 100;
|
|
var rgba = {
|
|
r: (rgb2.r - rgb1.r) * p + rgb1.r,
|
|
g: (rgb2.g - rgb1.g) * p + rgb1.g,
|
|
b: (rgb2.b - rgb1.b) * p + rgb1.b,
|
|
a: (rgb2.a - rgb1.a) * p + rgb1.a
|
|
};
|
|
return tinycolor(rgba);
|
|
}; // Readability Functions
|
|
// ---------------------
|
|
// <http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef (WCAG Version 2)
|
|
// `contrast`
|
|
// Analyze the 2 colors and returns the color contrast defined by (WCAG Version 2)
|
|
|
|
|
|
tinycolor.readability = function (color1, color2) {
|
|
var c1 = tinycolor(color1);
|
|
var c2 = tinycolor(color2);
|
|
return (Math.max(c1.getLuminance(), c2.getLuminance()) + 0.05) / (Math.min(c1.getLuminance(), c2.getLuminance()) + 0.05);
|
|
}; // `isReadable`
|
|
// Ensure that foreground and background color combinations meet WCAG2 guidelines.
|
|
// The third argument is an optional Object.
|
|
// the 'level' property states 'AA' or 'AAA' - if missing or invalid, it defaults to 'AA';
|
|
// the 'size' property states 'large' or 'small' - if missing or invalid, it defaults to 'small'.
|
|
// If the entire object is absent, isReadable defaults to {level:"AA",size:"small"}.
|
|
// *Example*
|
|
// tinycolor.isReadable("#000", "#111") => false
|
|
// tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false
|
|
|
|
|
|
tinycolor.isReadable = function (color1, color2, wcag2) {
|
|
var readability = tinycolor.readability(color1, color2);
|
|
var wcag2Parms, out;
|
|
out = false;
|
|
wcag2Parms = validateWCAG2Parms(wcag2);
|
|
|
|
switch (wcag2Parms.level + wcag2Parms.size) {
|
|
case "AAsmall":
|
|
case "AAAlarge":
|
|
out = readability >= 4.5;
|
|
break;
|
|
|
|
case "AAlarge":
|
|
out = readability >= 3;
|
|
break;
|
|
|
|
case "AAAsmall":
|
|
out = readability >= 7;
|
|
break;
|
|
}
|
|
|
|
return out;
|
|
}; // `mostReadable`
|
|
// Given a base color and a list of possible foreground or background
|
|
// colors for that base, returns the most readable color.
|
|
// Optionally returns Black or White if the most readable color is unreadable.
|
|
// *Example*
|
|
// tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255"
|
|
// tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff"
|
|
// tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3"
|
|
// tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff"
|
|
|
|
|
|
tinycolor.mostReadable = function (baseColor, colorList, args) {
|
|
var bestColor = null;
|
|
var bestScore = 0;
|
|
var readability;
|
|
var includeFallbackColors, level, size;
|
|
args = args || {};
|
|
includeFallbackColors = args.includeFallbackColors;
|
|
level = args.level;
|
|
size = args.size;
|
|
|
|
for (var i = 0; i < colorList.length; i++) {
|
|
readability = tinycolor.readability(baseColor, colorList[i]);
|
|
|
|
if (readability > bestScore) {
|
|
bestScore = readability;
|
|
bestColor = tinycolor(colorList[i]);
|
|
}
|
|
}
|
|
|
|
if (tinycolor.isReadable(baseColor, bestColor, {
|
|
"level": level,
|
|
"size": size
|
|
}) || !includeFallbackColors) {
|
|
return bestColor;
|
|
} else {
|
|
args.includeFallbackColors = false;
|
|
return tinycolor.mostReadable(baseColor, ["#fff", "#000"], args);
|
|
}
|
|
}; // Big List of Colors
|
|
// ------------------
|
|
// <http://www.w3.org/TR/css3-color/#svg-color>
|
|
|
|
|
|
var names = tinycolor.names = {
|
|
aliceblue: "f0f8ff",
|
|
antiquewhite: "faebd7",
|
|
aqua: "0ff",
|
|
aquamarine: "7fffd4",
|
|
azure: "f0ffff",
|
|
beige: "f5f5dc",
|
|
bisque: "ffe4c4",
|
|
black: "000",
|
|
blanchedalmond: "ffebcd",
|
|
blue: "00f",
|
|
blueviolet: "8a2be2",
|
|
brown: "a52a2a",
|
|
burlywood: "deb887",
|
|
burntsienna: "ea7e5d",
|
|
cadetblue: "5f9ea0",
|
|
chartreuse: "7fff00",
|
|
chocolate: "d2691e",
|
|
coral: "ff7f50",
|
|
cornflowerblue: "6495ed",
|
|
cornsilk: "fff8dc",
|
|
crimson: "dc143c",
|
|
cyan: "0ff",
|
|
darkblue: "00008b",
|
|
darkcyan: "008b8b",
|
|
darkgoldenrod: "b8860b",
|
|
darkgray: "a9a9a9",
|
|
darkgreen: "006400",
|
|
darkgrey: "a9a9a9",
|
|
darkkhaki: "bdb76b",
|
|
darkmagenta: "8b008b",
|
|
darkolivegreen: "556b2f",
|
|
darkorange: "ff8c00",
|
|
darkorchid: "9932cc",
|
|
darkred: "8b0000",
|
|
darksalmon: "e9967a",
|
|
darkseagreen: "8fbc8f",
|
|
darkslateblue: "483d8b",
|
|
darkslategray: "2f4f4f",
|
|
darkslategrey: "2f4f4f",
|
|
darkturquoise: "00ced1",
|
|
darkviolet: "9400d3",
|
|
deeppink: "ff1493",
|
|
deepskyblue: "00bfff",
|
|
dimgray: "696969",
|
|
dimgrey: "696969",
|
|
dodgerblue: "1e90ff",
|
|
firebrick: "b22222",
|
|
floralwhite: "fffaf0",
|
|
forestgreen: "228b22",
|
|
fuchsia: "f0f",
|
|
gainsboro: "dcdcdc",
|
|
ghostwhite: "f8f8ff",
|
|
gold: "ffd700",
|
|
goldenrod: "daa520",
|
|
gray: "808080",
|
|
green: "008000",
|
|
greenyellow: "adff2f",
|
|
grey: "808080",
|
|
honeydew: "f0fff0",
|
|
hotpink: "ff69b4",
|
|
indianred: "cd5c5c",
|
|
indigo: "4b0082",
|
|
ivory: "fffff0",
|
|
khaki: "f0e68c",
|
|
lavender: "e6e6fa",
|
|
lavenderblush: "fff0f5",
|
|
lawngreen: "7cfc00",
|
|
lemonchiffon: "fffacd",
|
|
lightblue: "add8e6",
|
|
lightcoral: "f08080",
|
|
lightcyan: "e0ffff",
|
|
lightgoldenrodyellow: "fafad2",
|
|
lightgray: "d3d3d3",
|
|
lightgreen: "90ee90",
|
|
lightgrey: "d3d3d3",
|
|
lightpink: "ffb6c1",
|
|
lightsalmon: "ffa07a",
|
|
lightseagreen: "20b2aa",
|
|
lightskyblue: "87cefa",
|
|
lightslategray: "789",
|
|
lightslategrey: "789",
|
|
lightsteelblue: "b0c4de",
|
|
lightyellow: "ffffe0",
|
|
lime: "0f0",
|
|
limegreen: "32cd32",
|
|
linen: "faf0e6",
|
|
magenta: "f0f",
|
|
maroon: "800000",
|
|
mediumaquamarine: "66cdaa",
|
|
mediumblue: "0000cd",
|
|
mediumorchid: "ba55d3",
|
|
mediumpurple: "9370db",
|
|
mediumseagreen: "3cb371",
|
|
mediumslateblue: "7b68ee",
|
|
mediumspringgreen: "00fa9a",
|
|
mediumturquoise: "48d1cc",
|
|
mediumvioletred: "c71585",
|
|
midnightblue: "191970",
|
|
mintcream: "f5fffa",
|
|
mistyrose: "ffe4e1",
|
|
moccasin: "ffe4b5",
|
|
navajowhite: "ffdead",
|
|
navy: "000080",
|
|
oldlace: "fdf5e6",
|
|
olive: "808000",
|
|
olivedrab: "6b8e23",
|
|
orange: "ffa500",
|
|
orangered: "ff4500",
|
|
orchid: "da70d6",
|
|
palegoldenrod: "eee8aa",
|
|
palegreen: "98fb98",
|
|
paleturquoise: "afeeee",
|
|
palevioletred: "db7093",
|
|
papayawhip: "ffefd5",
|
|
peachpuff: "ffdab9",
|
|
peru: "cd853f",
|
|
pink: "ffc0cb",
|
|
plum: "dda0dd",
|
|
powderblue: "b0e0e6",
|
|
purple: "800080",
|
|
rebeccapurple: "663399",
|
|
red: "f00",
|
|
rosybrown: "bc8f8f",
|
|
royalblue: "4169e1",
|
|
saddlebrown: "8b4513",
|
|
salmon: "fa8072",
|
|
sandybrown: "f4a460",
|
|
seagreen: "2e8b57",
|
|
seashell: "fff5ee",
|
|
sienna: "a0522d",
|
|
silver: "c0c0c0",
|
|
skyblue: "87ceeb",
|
|
slateblue: "6a5acd",
|
|
slategray: "708090",
|
|
slategrey: "708090",
|
|
snow: "fffafa",
|
|
springgreen: "00ff7f",
|
|
steelblue: "4682b4",
|
|
tan: "d2b48c",
|
|
teal: "008080",
|
|
thistle: "d8bfd8",
|
|
tomato: "ff6347",
|
|
turquoise: "40e0d0",
|
|
violet: "ee82ee",
|
|
wheat: "f5deb3",
|
|
white: "fff",
|
|
whitesmoke: "f5f5f5",
|
|
yellow: "ff0",
|
|
yellowgreen: "9acd32"
|
|
}; // Make it easy to access colors via `hexNames[hex]`
|
|
|
|
var hexNames = tinycolor.hexNames = flip(names); // Utilities
|
|
// ---------
|
|
// `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
|
|
|
|
function flip(o) {
|
|
var flipped = {};
|
|
|
|
for (var i in o) {
|
|
if (o.hasOwnProperty(i)) {
|
|
flipped[o[i]] = i;
|
|
}
|
|
}
|
|
|
|
return flipped;
|
|
} // Return a valid alpha value [0,1] with all invalid values being set to 1
|
|
|
|
|
|
function boundAlpha(a) {
|
|
a = parseFloat(a);
|
|
|
|
if (isNaN(a) || a < 0 || a > 1) {
|
|
a = 1;
|
|
}
|
|
|
|
return a;
|
|
} // Take input from [0, n] and return it as [0, 1]
|
|
|
|
|
|
function bound01(n, max) {
|
|
if (isOnePointZero(n)) {
|
|
n = "100%";
|
|
}
|
|
|
|
var processPercent = isPercentage(n);
|
|
n = mathMin(max, mathMax(0, parseFloat(n))); // Automatically convert percentage into number
|
|
|
|
if (processPercent) {
|
|
n = parseInt(n * max, 10) / 100;
|
|
} // Handle floating point rounding errors
|
|
|
|
|
|
if (Math.abs(n - max) < 0.000001) {
|
|
return 1;
|
|
} // Convert into [0, 1] range if it isn't already
|
|
|
|
|
|
return n % max / parseFloat(max);
|
|
} // Force a number between 0 and 1
|
|
|
|
|
|
function clamp01(val) {
|
|
return mathMin(1, mathMax(0, val));
|
|
} // Parse a base-16 hex value into a base-10 integer
|
|
|
|
|
|
function parseIntFromHex(val) {
|
|
return parseInt(val, 16);
|
|
} // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
|
|
// <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
|
|
|
|
|
|
function isOnePointZero(n) {
|
|
return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
|
|
} // Check to see if string passed in is a percentage
|
|
|
|
|
|
function isPercentage(n) {
|
|
return typeof n === "string" && n.indexOf('%') != -1;
|
|
} // Force a hex value to have 2 characters
|
|
|
|
|
|
function pad2(c) {
|
|
return c.length == 1 ? '0' + c : '' + c;
|
|
} // Replace a decimal with it's percentage value
|
|
|
|
|
|
function convertToPercentage(n) {
|
|
if (n <= 1) {
|
|
n = n * 100 + "%";
|
|
}
|
|
|
|
return n;
|
|
} // Converts a decimal to a hex value
|
|
|
|
|
|
function convertDecimalToHex(d) {
|
|
return Math.round(parseFloat(d) * 255).toString(16);
|
|
} // Converts a hex value to a decimal
|
|
|
|
|
|
function convertHexToDecimal(h) {
|
|
return parseIntFromHex(h) / 255;
|
|
}
|
|
|
|
var matchers = function () {
|
|
// <http://www.w3.org/TR/css3-values/#integers>
|
|
var CSS_INTEGER = "[-\\+]?\\d+%?"; // <http://www.w3.org/TR/css3-values/#number-value>
|
|
|
|
var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?"; // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
|
|
|
|
var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")"; // Actual matching.
|
|
// Parentheses and commas are optional, but not required.
|
|
// Whitespace can take the place of commas or opening paren
|
|
|
|
var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
|
|
var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
|
|
return {
|
|
CSS_UNIT: new RegExp(CSS_UNIT),
|
|
rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
|
|
rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
|
|
hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
|
|
hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
|
|
hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
|
|
hsva: new RegExp("hsva" + PERMISSIVE_MATCH4),
|
|
hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
|
hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
|
hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
|
hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
|
|
};
|
|
}(); // `isValidCSSUnit`
|
|
// Take in a single string / number and check to see if it looks like a CSS unit
|
|
// (see `matchers` above for definition).
|
|
|
|
|
|
function isValidCSSUnit(color) {
|
|
return !!matchers.CSS_UNIT.exec(color);
|
|
} // `stringInputToObject`
|
|
// Permissive string parsing. Take in a number of formats, and output an object
|
|
// based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
|
|
|
|
|
|
function stringInputToObject(color) {
|
|
color = color.replace(trimLeft, '').replace(trimRight, '').toLowerCase();
|
|
var named = false;
|
|
|
|
if (names[color]) {
|
|
color = names[color];
|
|
named = true;
|
|
} else if (color == 'transparent') {
|
|
return {
|
|
r: 0,
|
|
g: 0,
|
|
b: 0,
|
|
a: 0,
|
|
format: "name"
|
|
};
|
|
} // Try to match string input using regular expressions.
|
|
// Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
|
|
// Just return an object and let the conversion functions handle that.
|
|
// This way the result will be the same whether the tinycolor is initialized with string or object.
|
|
|
|
|
|
var match;
|
|
|
|
if (match = matchers.rgb.exec(color)) {
|
|
return {
|
|
r: match[1],
|
|
g: match[2],
|
|
b: match[3]
|
|
};
|
|
}
|
|
|
|
if (match = matchers.rgba.exec(color)) {
|
|
return {
|
|
r: match[1],
|
|
g: match[2],
|
|
b: match[3],
|
|
a: match[4]
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hsl.exec(color)) {
|
|
return {
|
|
h: match[1],
|
|
s: match[2],
|
|
l: match[3]
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hsla.exec(color)) {
|
|
return {
|
|
h: match[1],
|
|
s: match[2],
|
|
l: match[3],
|
|
a: match[4]
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hsv.exec(color)) {
|
|
return {
|
|
h: match[1],
|
|
s: match[2],
|
|
v: match[3]
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hsva.exec(color)) {
|
|
return {
|
|
h: match[1],
|
|
s: match[2],
|
|
v: match[3],
|
|
a: match[4]
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hex8.exec(color)) {
|
|
return {
|
|
r: parseIntFromHex(match[1]),
|
|
g: parseIntFromHex(match[2]),
|
|
b: parseIntFromHex(match[3]),
|
|
a: convertHexToDecimal(match[4]),
|
|
format: named ? "name" : "hex8"
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hex6.exec(color)) {
|
|
return {
|
|
r: parseIntFromHex(match[1]),
|
|
g: parseIntFromHex(match[2]),
|
|
b: parseIntFromHex(match[3]),
|
|
format: named ? "name" : "hex"
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hex4.exec(color)) {
|
|
return {
|
|
r: parseIntFromHex(match[1] + '' + match[1]),
|
|
g: parseIntFromHex(match[2] + '' + match[2]),
|
|
b: parseIntFromHex(match[3] + '' + match[3]),
|
|
a: convertHexToDecimal(match[4] + '' + match[4]),
|
|
format: named ? "name" : "hex8"
|
|
};
|
|
}
|
|
|
|
if (match = matchers.hex3.exec(color)) {
|
|
return {
|
|
r: parseIntFromHex(match[1] + '' + match[1]),
|
|
g: parseIntFromHex(match[2] + '' + match[2]),
|
|
b: parseIntFromHex(match[3] + '' + match[3]),
|
|
format: named ? "name" : "hex"
|
|
};
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function validateWCAG2Parms(parms) {
|
|
// return valid WCAG2 parms for isReadable.
|
|
// If input parms are invalid, return {"level":"AA", "size":"small"}
|
|
var level, size;
|
|
parms = parms || {
|
|
"level": "AA",
|
|
"size": "small"
|
|
};
|
|
level = (parms.level || "AA").toUpperCase();
|
|
size = (parms.size || "small").toLowerCase();
|
|
|
|
if (level !== "AA" && level !== "AAA") {
|
|
level = "AA";
|
|
}
|
|
|
|
if (size !== "small" && size !== "large") {
|
|
size = "small";
|
|
}
|
|
|
|
return {
|
|
"level": level,
|
|
"size": size
|
|
};
|
|
}
|
|
/*// Node: Export function
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = tinycolor;
|
|
}
|
|
// AMD/requirejs: Define the module
|
|
else if (typeof define === 'function' && define.amd) {
|
|
define(function () {return tinycolor;});
|
|
}
|
|
// Browser: Expose to window
|
|
else {
|
|
window.tinycolor = tinycolor;
|
|
}*/
|
|
|
|
|
|
return tinycolor;
|
|
}(Math);
|
|
|
|
/* harmony default export */ __webpack_exports__["default"] = (tinycolor);
|
|
|
|
/***/ })
|
|
|
|
}]);
|
|
//# sourceMappingURL=addon-default-entry.js.map
|