@@ -21,6 +21,7 @@ import DragConstants from '../lib/drag-constants';
21
21
import defineDynamicBlock from '../lib/define-dynamic-block' ;
22
22
import AddonHooks from '../addons/hooks' ;
23
23
import LoadScratchBlocksHOC from '../lib/tw-load-scratch-blocks-hoc.jsx' ;
24
+ import uid from "../lib/uid.js" ;
24
25
25
26
import { connect } from 'react-redux' ;
26
27
import { updateToolbox } from '../reducers/toolbox' ;
@@ -76,6 +77,7 @@ const addFunctionListener = (object, property, callback) => {
76
77
return result ;
77
78
} ;
78
79
} ;
80
+ const isObject = ( value ) => value && typeof value === 'object' && ! Array . isArray ( value ) ;
79
81
80
82
const DroppableBlocks = DropAreaHOC ( [
81
83
DragConstants . BACKPACK_CODE
@@ -143,10 +145,12 @@ class Blocks extends React.Component {
143
145
this . ScratchBlocks . recordSoundCallback = this . handleOpenSoundRecorder ;
144
146
145
147
this . state = {
146
- prompt : null
148
+ prompt : null ,
149
+ customPrompts : [ ] ,
147
150
} ;
148
151
this . onTargetsUpdate = debounce ( this . onTargetsUpdate , 100 ) ;
149
152
this . toolboxUpdateQueue = [ ] ;
153
+ this . customModalRefs = new Map ( ) ;
150
154
}
151
155
componentDidMount ( ) {
152
156
this . props . vm . setCompilerOptions ( {
@@ -236,6 +240,8 @@ class Blocks extends React.Component {
236
240
shouldComponentUpdate ( nextProps , nextState ) {
237
241
return (
238
242
this . state . prompt !== nextState . prompt ||
243
+ this . state . customPrompts !== nextState . customPrompts ||
244
+ ( nextState . customPrompts && this . state . customPrompts . length !== nextState . customPrompts . length ) ||
239
245
this . props . isVisible !== nextProps . isVisible ||
240
246
this . _renderedToolboxXML !== nextProps . toolboxXML ||
241
247
this . props . extensionLibraryVisible !== nextProps . extensionLibraryVisible ||
@@ -611,46 +617,49 @@ class Blocks extends React.Component {
611
617
p . prompt . showCloudOption = ( optVarType === this . ScratchBlocks . SCALAR_VARIABLE_TYPE ) && this . props . canUseCloud ;
612
618
this . setState ( p ) ;
613
619
}
614
- handleCustomPrompt ( title , scale , enterInfo , closeInfo ) {
615
- const isObject = ( value ) => typeof value === 'object' && ! Array . isArray ( value ) ;
616
- let needsExit = false ;
617
- const exitFunc = ( message ) => {
618
- needsExit = true ;
619
- console . error ( message ) ;
620
- } ;
620
+ handleCustomPrompt ( config , styles , enterInfo , closeInfo ) {
621
+ return new Promise ( ( resolve , reject ) => {
622
+ /* validate arguments */
623
+ if ( config && isObject ( config ) ) {
624
+ if ( ! config . title ) return reject ( "Custom Modal -- Missing 'title' (string) property in Param 1" ) ;
625
+ } else {
626
+ return reject ( "Custom Modal -- Param 1 must be an object with at least properties: 'title' (string)" ) ;
627
+ }
628
+ if ( styles && ! isObject ( styles ) ) {
629
+ return reject ( "Custom Modal -- Param 2 must be an object" ) ;
630
+ }
631
+ if ( styles && ( ! styles . content && ! styles . overlay ) ) {
632
+ return reject ( "Custom Modal -- If Param 2 is specified, specify CSS styles within either: 'content' or 'overlay'" ) ;
633
+ }
634
+ if ( isObject ( enterInfo ) ) {
635
+ if ( ! enterInfo . name || ! enterInfo . callback ) return reject ( "Custom Modal -- Missing name/callback property in Param 3" ) ;
636
+ if ( enterInfo . callback && typeof enterInfo . callback !== 'function' ) return reject ( "Custom Modal -- callback property in Param 3 must be a function" ) ;
637
+ } else {
638
+ return reject ( "Custom Modal -- Param 3 must be a object with properties: 'name' (string) and 'callback' (function)" ) ;
639
+ }
640
+ if ( isObject ( closeInfo ) ) {
641
+ if ( ! closeInfo . name || ! closeInfo . callback ) return reject ( "Custom Modal -- Missing name/callback property in Param 4" ) ;
642
+ if ( closeInfo . callback && typeof closeInfo . callback !== 'function' ) return reject ( "Custom Modal -- callback property in Param 4 must be a function" ) ;
643
+ } else {
644
+ return reject ( "Custom Modal -- Param 4 must be a object with properties: 'name' (string) and 'callback' (function)" ) ;
645
+ }
621
646
622
- /* validate arguments */
623
- if ( isObject ( scale ) ) {
624
- if ( ! scale . width || ! scale . height ) exitFunc ( "Custom Modal -- Missing width/height number property in Param 2" ) ;
625
- } else {
626
- exitFunc ( "Custom Modal -- Param 2 must be a object with 'width' and 'height' number properties" ) ;
627
- }
628
- if ( isObject ( enterInfo ) ) {
629
- if ( ! enterInfo . name || ! enterInfo . callback ) exitFunc ( "Custom Modal -- Missing name/callback property in Param 3" ) ;
630
- if ( enterInfo . callback && typeof enterInfo . callback !== 'function' ) exitFunc ( "Custom Modal -- callback property in Param 3 must be a function" ) ;
631
- } else {
632
- exitFunc ( "Custom Modal -- Param 3 must be a object with properties: 'name' (string) and 'callback' (function)" ) ;
633
- }
634
- if ( isObject ( closeInfo ) ) {
635
- if ( ! closeInfo . name || ! closeInfo . callback ) exitFunc ( "Custom Modal -- Missing name/callback property in Param 4" ) ;
636
- if ( closeInfo . callback && typeof closeInfo . callback !== 'function' ) exitFunc ( "Custom Modal -- callback property in Param 4 must be a function" ) ;
637
- } else {
638
- exitFunc ( "Custom Modal -- Param 4 must be a object with properties: 'name' (string) and 'callback' (function)" ) ;
639
- }
640
- if ( needsExit ) return ;
641
-
642
- this . setState ( { prompt : {
643
- isCustom : true ,
644
- title, enterInfo, closeInfo
645
- } } ) ;
646
-
647
- const modal = document . querySelector ( `div[class="ReactModalPortal"]` ) ;
648
- if ( modal ) {
649
- const inner = modal . firstChild . firstChild ;
650
- inner . style . width = typeof scale . width === 'number' ? `${ scale . width } px` : scale . width ;
651
- inner . style . height = typeof scale . height === 'number' ? `${ scale . height } px` : scale . height ;
652
- return inner . querySelector ( `div[class*="prompt_body_"] div` ) ;
653
- }
647
+ // create the callback for when the node is created. an HTML element (or modal) with ref={functionHere} will run the function with the HTMLElement as 1st arg
648
+ const thisPromptId = uid ( ) ;
649
+ this . customModalRefs . set ( thisPromptId , ( node ) => {
650
+ resolve ( node ) ;
651
+ } )
652
+
653
+ // Setting state with this info will cause blocks.jsx to re-render, rendering the modal before any code after setState can run.
654
+ // However, the callback & ref are not be usable until slightly later. this is why ref is set to a callback above.
655
+ // This is one of many reasons why React is pretty stupid.
656
+ this . setState ( {
657
+ customPrompts : this . state . customPrompts . concat ( {
658
+ id : thisPromptId ,
659
+ config, styles, enterInfo, closeInfo
660
+ } )
661
+ } ) ;
662
+ } ) ;
654
663
}
655
664
handleConnectionModalStart ( extensionId ) {
656
665
this . props . onOpenConnectionModal ( extensionId ) ;
@@ -667,20 +676,28 @@ class Blocks extends React.Component {
667
676
* and additional potentially conflicting variable names from the VM
668
677
* to the variable validation prompt callback used in scratch-blocks.
669
678
*/
670
- handlePromptCallback ( input , variableOptions ) {
671
- if ( this . state . prompt . isCustom ) {
672
- this . state . prompt . enterInfo . callback ( ) ;
673
- this . setState ( { prompt : null } ) ;
674
- return ;
679
+ handlePromptCallback ( input , variableOptions , customPrompt ) {
680
+ if ( customPrompt ) {
681
+ customPrompt . enterInfo . callback ( ) ;
682
+ return this . setState ( {
683
+ customPrompts : this . state . customPrompts . filter ( prompt => prompt !== customPrompt )
684
+ } ) ;
675
685
}
686
+
676
687
this . state . prompt . callback (
677
688
input ,
678
689
this . props . vm . runtime . getAllVarNamesOfType ( this . state . prompt . varType ) ,
679
690
variableOptions ) ;
680
691
this . handlePromptClose ( ) ;
681
692
}
682
- handlePromptClose ( ) {
683
- if ( this . state . prompt . isCustom ) this . state . prompt . closeInfo . callback ( ) ;
693
+ handlePromptClose ( customPrompt ) {
694
+ if ( customPrompt ) {
695
+ customPrompt . closeInfo . callback ( ) ;
696
+ return this . setState ( {
697
+ customPrompts : this . state . customPrompts . filter ( prompt => prompt !== customPrompt )
698
+ } ) ;
699
+ }
700
+
684
701
this . setState ( { prompt : null } ) ;
685
702
}
686
703
handleCustomProceduresClose ( data ) {
@@ -736,17 +753,7 @@ class Blocks extends React.Component {
736
753
onDrop = { this . handleDrop }
737
754
{ ...props }
738
755
/>
739
- { this . state . prompt ? this . state . prompt . isCustom ? (
740
- < Prompt
741
- isCustom = { this . state . prompt . isCustom }
742
- title = { this . state . prompt . title }
743
- enterTitle = { this . state . prompt . enterInfo . name }
744
- closeTitle = { this . state . prompt . closeInfo . name }
745
- vm = { vm }
746
- onCancel = { this . handlePromptClose }
747
- onOk = { this . handlePromptCallback }
748
- />
749
- ) : (
756
+ { this . state . prompt ? (
750
757
< Prompt
751
758
defaultValue = { this . state . prompt . defaultValue }
752
759
isStage = { vm . runtime . getEditingTarget ( ) . isStage }
@@ -760,6 +767,20 @@ class Blocks extends React.Component {
760
767
onOk = { this . handlePromptCallback }
761
768
/>
762
769
) : null }
770
+ { this . state . customPrompts . map ( prompt => (
771
+ < Prompt
772
+ isCustom = { true }
773
+ vm = { vm }
774
+ customRef = { this . customModalRefs . get ( prompt . id ) }
775
+ title = { prompt . config . title }
776
+ styleContent = { prompt . styles ? prompt . styles . content : null }
777
+ styleOverlay = { prompt . styles ? prompt . styles . overlay : null }
778
+ enterTitle = { prompt . enterInfo . name }
779
+ closeTitle = { prompt . closeInfo . name }
780
+ onCancel = { ( ) => this . handlePromptClose ( prompt ) }
781
+ onOk = { ( ) => this . handlePromptCallback ( null , null , prompt ) }
782
+ />
783
+ ) ) }
763
784
{ extensionLibraryVisible ? (
764
785
< ExtensionLibrary
765
786
vm = { vm }
0 commit comments