Skip to content

Commit f10c44f

Browse files
committed
small customPrompt rework & fix multiple prompts
1 parent 3f91fb7 commit f10c44f

File tree

7 files changed

+184
-63
lines changed

7 files changed

+184
-63
lines changed

src/components/box/box.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,12 @@ Box.propTypes = {
9999
/**
100100
* A callback function whose first parameter is the underlying dom elements.
101101
* This call back will be executed immediately after the component is mounted or unmounted
102+
* Can also be a ref of Element
102103
*/
103-
componentRef: PropTypes.func,
104+
componentRef: PropTypes.oneOfType([
105+
PropTypes.func,
106+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
107+
]),
104108
/** https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction */
105109
direction: PropTypes.oneOf([
106110
'row', 'row-reverse', 'column', 'column-reverse'

src/components/modal/modal.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import styles from './modal.css';
1616
const ModalComponent = props => (
1717
<ReactModal
1818
isOpen
19+
ref={props.componentRef}
1920
className={classNames(styles.modalContent, props.className, {
2021
[styles.fullScreen]: props.fullScreen
2122
})}
@@ -24,10 +25,12 @@ const ModalComponent = props => (
2425
[styles.scrollable]: props.scrollable
2526
})}
2627
onRequestClose={props.onRequestClose}
28+
style={{ content: props.styleContent, overlay: props.styleOverlay }}
2729
>
2830
<Box
2931
dir={props.isRtl ? 'rtl' : 'ltr'}
3032
direction="column"
33+
componentRef={props.boxRef}
3134
grow={1}
3235
>
3336
<div className={classNames(styles.header, props.headerClassName)}>
@@ -98,6 +101,16 @@ const ModalComponent = props => (
98101

99102
ModalComponent.propTypes = {
100103
children: PropTypes.node,
104+
componentRef: PropTypes.oneOfType([
105+
PropTypes.func,
106+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
107+
]),
108+
boxRef: PropTypes.oneOfType([
109+
PropTypes.func,
110+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
111+
]),
112+
styleContent: PropTypes.object,
113+
styleOverlay: PropTypes.object,
101114
className: PropTypes.string,
102115
contentLabel: PropTypes.oneOfType([
103116
PropTypes.string,

src/components/prompt/prompt.jsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Modal from '../../containers/modal.jsx';
99
import styles from './prompt.css';
1010
import { SCRATCH_MAX_CLOUD_VARIABLES } from '../../lib/tw-cloud-limits.js';
1111

12-
1312
const messages = defineMessages({
1413
forAllSpritesMessage: {
1514
defaultMessage: 'For all sprites',
@@ -46,9 +45,13 @@ const PromptComponent = props => props.isCustom ? (
4645
contentLabel={props.title}
4746
id="customModal"
4847
onRequestClose={props.onCancel}
48+
componentRef={props.ref}
49+
boxRef={props.boxRef}
50+
styleContent={props.styleContent}
51+
styleOverlay={props.styleOverlay}
4952
>
5053
<Box className={styles.body}>
51-
<Box>
54+
<Box componentRef={props.customRef}>
5255
</Box>
5356
<Box className={styles.buttonRow}>
5457
<button
@@ -72,6 +75,10 @@ const PromptComponent = props => props.isCustom ? (
7275
contentLabel={props.title}
7376
id="variableModal"
7477
onRequestClose={props.onCancel}
78+
componentRef={props.componentRef}
79+
boxRef={props.boxRef}
80+
styleContent={props.styleContent}
81+
styleOverlay={props.styleOverlay}
7582
>
7683
<Box className={styles.body}>
7784
<Box className={styles.label}>
@@ -232,11 +239,25 @@ PromptComponent.propTypes = {
232239
onScopeOptionSelection: PropTypes.func,
233240
showCloudOption: PropTypes.bool,
234241
showVariableOptions: PropTypes.bool,
242+
componentRef: PropTypes.oneOfType([
243+
PropTypes.func,
244+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
245+
]),
246+
boxRef: PropTypes.oneOfType([
247+
PropTypes.func,
248+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
249+
]),
250+
styleContent: PropTypes.object,
251+
styleOverlay: PropTypes.object,
235252

236253
/* custom modals */
237254
isCustom: PropTypes.bool,
238255
enterTitle: PropTypes.string,
239-
closeTitle: PropTypes.string
256+
closeTitle: PropTypes.string,
257+
customRef: PropTypes.oneOfType([
258+
PropTypes.func,
259+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
260+
]),
240261
};
241262

242263
export default PromptComponent;

src/containers/blocks.jsx

Lines changed: 79 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import DragConstants from '../lib/drag-constants';
2121
import defineDynamicBlock from '../lib/define-dynamic-block';
2222
import AddonHooks from '../addons/hooks';
2323
import LoadScratchBlocksHOC from '../lib/tw-load-scratch-blocks-hoc.jsx';
24+
import uid from "../lib/uid.js";
2425

2526
import {connect} from 'react-redux';
2627
import {updateToolbox} from '../reducers/toolbox';
@@ -76,6 +77,7 @@ const addFunctionListener = (object, property, callback) => {
7677
return result;
7778
};
7879
};
80+
const isObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
7981

8082
const DroppableBlocks = DropAreaHOC([
8183
DragConstants.BACKPACK_CODE
@@ -143,10 +145,12 @@ class Blocks extends React.Component {
143145
this.ScratchBlocks.recordSoundCallback = this.handleOpenSoundRecorder;
144146

145147
this.state = {
146-
prompt: null
148+
prompt: null,
149+
customPrompts: [],
147150
};
148151
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
149152
this.toolboxUpdateQueue = [];
153+
this.customModalRefs = new Map();
150154
}
151155
componentDidMount () {
152156
this.props.vm.setCompilerOptions({
@@ -236,6 +240,8 @@ class Blocks extends React.Component {
236240
shouldComponentUpdate (nextProps, nextState) {
237241
return (
238242
this.state.prompt !== nextState.prompt ||
243+
this.state.customPrompts !== nextState.customPrompts ||
244+
(nextState.customPrompts && this.state.customPrompts.length !== nextState.customPrompts.length) ||
239245
this.props.isVisible !== nextProps.isVisible ||
240246
this._renderedToolboxXML !== nextProps.toolboxXML ||
241247
this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible ||
@@ -611,46 +617,49 @@ class Blocks extends React.Component {
611617
p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud;
612618
this.setState(p);
613619
}
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+
}
621646

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+
});
654663
}
655664
handleConnectionModalStart (extensionId) {
656665
this.props.onOpenConnectionModal(extensionId);
@@ -667,20 +676,28 @@ class Blocks extends React.Component {
667676
* and additional potentially conflicting variable names from the VM
668677
* to the variable validation prompt callback used in scratch-blocks.
669678
*/
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+
});
675685
}
686+
676687
this.state.prompt.callback(
677688
input,
678689
this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType),
679690
variableOptions);
680691
this.handlePromptClose();
681692
}
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+
684701
this.setState({prompt: null});
685702
}
686703
handleCustomProceduresClose (data) {
@@ -736,17 +753,7 @@ class Blocks extends React.Component {
736753
onDrop={this.handleDrop}
737754
{...props}
738755
/>
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 ? (
750757
<Prompt
751758
defaultValue={this.state.prompt.defaultValue}
752759
isStage={vm.runtime.getEditingTarget().isStage}
@@ -760,6 +767,20 @@ class Blocks extends React.Component {
760767
onOk={this.handlePromptCallback}
761768
/>
762769
) : 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+
))}
763784
{extensionLibraryVisible ? (
764785
<ExtensionLibrary
765786
vm={vm}

src/containers/modal.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,19 @@ class Modal extends React.Component {
4848

4949
Modal.propTypes = {
5050
id: PropTypes.string.isRequired,
51+
componentRef: PropTypes.oneOfType([
52+
PropTypes.func,
53+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
54+
]),
55+
boxRef: PropTypes.oneOfType([
56+
PropTypes.func,
57+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
58+
]),
5159
isRtl: PropTypes.bool,
5260
onRequestClose: PropTypes.func,
5361
onRequestOpen: PropTypes.func,
62+
styleContent: PropTypes.object,
63+
styleOverlay: PropTypes.object,
5464
scrollable: PropTypes.bool
5565
};
5666

src/containers/prompt.jsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,17 @@ class Prompt extends React.Component {
6262
if (this.props.isCustom) return (
6363
<PromptComponent
6464
isCustom={this.props.isCustom}
65+
componentRef={this.props.componentRef}
66+
boxRef={this.props.boxRef}
67+
styleContent={this.props.styleContent}
68+
styleOverlay={this.props.styleOverlay}
6569
title={this.props.title}
6670
enterTitle={this.props.enterTitle}
6771
closeTitle={this.props.closeTitle}
6872
onOk={this.handleOk}
6973
onCancel={this.handleCancel}
7074
onKeyPress={this.handleKeyPress}
75+
customRef={this.props.customRef}
7176
/>
7277
)
7378
else return (
@@ -90,6 +95,10 @@ class Prompt extends React.Component {
9095
onKeyPress={this.handleKeyPress}
9196
onOk={this.handleOk}
9297
onScopeOptionSelection={this.handleScopeOptionSelection}
98+
componentRef={this.props.componentRef}
99+
styleContent={this.props.styleContent}
100+
styleOverlay={this.props.styleOverlay}
101+
boxRef={this.props.boxRef}
93102
/>
94103
);
95104
}
@@ -106,11 +115,25 @@ Prompt.propTypes = {
106115
showCloudOption: PropTypes.bool,
107116
showVariableOptions: PropTypes.bool,
108117
vm: PropTypes.instanceOf(VM),
118+
componentRef: PropTypes.oneOfType([
119+
PropTypes.func,
120+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
121+
]),
122+
boxRef: PropTypes.oneOfType([
123+
PropTypes.func,
124+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
125+
]),
126+
styleContent: PropTypes.object,
127+
styleOverlay: PropTypes.object,
109128

110129
/* custom modals */
111130
isCustom: PropTypes.bool,
112131
enterTitle: PropTypes.string,
113-
closeTitle: PropTypes.string
132+
closeTitle: PropTypes.string,
133+
customRef: PropTypes.oneOfType([
134+
PropTypes.func,
135+
PropTypes.shape({ current: PropTypes.instanceOf(Element) })
136+
]),
114137
};
115138

116139
export default Prompt;

0 commit comments

Comments
 (0)