Skip to content

Commit 50a2dd1

Browse files
committed
allow more than 2 buttons & button styles
1 parent 744308c commit 50a2dd1

File tree

4 files changed

+120
-70
lines changed

4 files changed

+120
-70
lines changed

src/components/prompt/prompt.jsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,30 @@ const messages = defineMessages({
3939
}
4040
});
4141

42+
const customButtonStyle = (button) => {
43+
// if class is manually specified, dont try to guess the intended style
44+
if (button.class) {
45+
switch (button.class) {
46+
case "ok":
47+
return styles.okButton;
48+
case "cancel":
49+
return styles.cancelButton;
50+
default:
51+
return styles.cancelButton;
52+
}
53+
}
54+
55+
// assume intended style from role
56+
if (button.role) {
57+
switch (button.role) {
58+
case "ok":
59+
return styles.okButton;
60+
case "close":
61+
return styles.cancelButton;
62+
}
63+
}
64+
return styles.cancelButton;
65+
};
4266
const PromptComponent = props => props.isCustom ? (
4367
<Modal
4468
className={styles.modalContent}
@@ -49,24 +73,23 @@ const PromptComponent = props => props.isCustom ? (
4973
boxRef={props.boxRef}
5074
styleContent={props.styleContent}
5175
styleOverlay={props.styleOverlay}
76+
scrollable={props.config.scrollable}
5277
>
5378
<Box className={styles.body}>
5479
<Box componentRef={props.customRef}>
5580
</Box>
56-
<Box className={styles.buttonRow}>
57-
<button
58-
className={styles.cancelButton}
59-
onClick={props.onCancel}
60-
>
61-
{props.closeTitle}
62-
</button>
63-
<button
64-
className={styles.okButton}
65-
onClick={props.onOk}
66-
>
67-
{props.enterTitle}
68-
</button>
69-
</Box>
81+
{(props.customButtons && props.customButtons.length > 0 ? <Box className={styles.buttonRow}>
82+
{/* slice then reverse to avoid mutating the array. reversing cause scratch modals put OK on the right & usually you define OK button first */}
83+
{props.customButtons.slice().reverse().map(button => (
84+
<button
85+
className={customButtonStyle(button)}
86+
style={button.style}
87+
onClick={() => props.onCustomButton(button)}
88+
>
89+
{button.name}
90+
</button>
91+
))}
92+
</Box> : null)}
7093
</Box>
7194
</Modal>
7295
) : (
@@ -252,8 +275,9 @@ PromptComponent.propTypes = {
252275

253276
/* custom modals */
254277
isCustom: PropTypes.bool,
255-
enterTitle: PropTypes.string,
256-
closeTitle: PropTypes.string,
278+
config: PropTypes.object,
279+
onCustomButton: PropTypes.func,
280+
customButtons: PropTypes.arrayOf(PropTypes.object),
257281
customRef: PropTypes.oneOfType([
258282
PropTypes.func,
259283
PropTypes.shape({ current: PropTypes.instanceOf(Element) })

src/containers/blocks.jsx

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,21 @@ class Blocks extends React.Component {
617617
p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud;
618618
this.setState(p);
619619
}
620-
handleCustomPrompt (config, styles, enterInfo, closeInfo) {
620+
621+
/**
622+
* @param {{title:string, scrollable:boolean?}} config The config for the modal
623+
* @param {{content:CSSStyleDeclaration?, overlay:CSSStyleDeclaration?}?} styles Sets styles on parts of the modal. If specified, at least one of the parts should have styles.
624+
* @param {Array<{
625+
* name:string,
626+
* role:"ok"|"close"|null,
627+
* class:"ok"|"cancel"|null,
628+
* style:CSSStyleDeclaration?,
629+
* dontClose:boolean?,
630+
* callback:function():void
631+
* }>?} buttons Buttons to place onto the modal. `role` makes the button callback run for other types of interactions.
632+
* @returns {Promise<HTMLElement>}
633+
*/
634+
handleCustomPrompt (config, styles, buttons) {
621635
return new Promise((resolve, reject) => {
622636
/* validate arguments */
623637
if (config && isObject(config)) {
@@ -631,18 +645,6 @@ class Blocks extends React.Component {
631645
if (styles && (!styles.content && !styles.overlay)) {
632646
return reject("Custom Modal -- If Param 2 is specified, specify CSS styles within either: 'content' or 'overlay'");
633647
}
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-
}
646648

647649
// 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
648650
const thisPromptId = uid();
@@ -656,7 +658,7 @@ class Blocks extends React.Component {
656658
this.setState({
657659
customPrompts: this.state.customPrompts.concat({
658660
id: thisPromptId,
659-
config, styles, enterInfo, closeInfo
661+
config, styles, buttons
660662
})
661663
});
662664
});
@@ -676,23 +678,36 @@ class Blocks extends React.Component {
676678
* and additional potentially conflicting variable names from the VM
677679
* to the variable validation prompt callback used in scratch-blocks.
678680
*/
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-
});
685-
}
686-
681+
handlePromptCallback (input, variableOptions) {
687682
this.state.prompt.callback(
688683
input,
689684
this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType),
690685
variableOptions);
691686
this.handlePromptClose();
692687
}
688+
handleCustomPromptButton(button, customPrompt) {
689+
button.callback();
690+
if (button.dontClose) return;
691+
this.setState({
692+
customPrompts: this.state.customPrompts.filter(prompt => prompt !== customPrompt)
693+
});
694+
}
695+
handleCustomPromptOk(customPrompt) {
696+
const okButton = (customPrompt.buttons || []).find(button => button.role === "ok");
697+
if (okButton) {
698+
okButton.callback();
699+
if (okButton.dontClose) return;
700+
return this.setState({
701+
customPrompts: this.state.customPrompts.filter(prompt => prompt !== customPrompt)
702+
});
703+
}
704+
}
693705
handlePromptClose (customPrompt) {
694706
if (customPrompt) {
695-
customPrompt.closeInfo.callback();
707+
const closeButton = (customPrompt.buttons || []).find(button => button.role === "close");
708+
if (closeButton) {
709+
closeButton.callback();
710+
}
696711
return this.setState({
697712
customPrompts: this.state.customPrompts.filter(prompt => prompt !== customPrompt)
698713
});
@@ -772,13 +787,14 @@ class Blocks extends React.Component {
772787
isCustom={true}
773788
vm={vm}
774789
customRef={this.customModalRefs.get(prompt.id)}
790+
onOk={() => this.handleCustomPromptOk(prompt)}
791+
onCancel={() => this.handlePromptClose(prompt)}
792+
config={prompt.config}
775793
title={prompt.config.title}
776794
styleContent={prompt.styles ? prompt.styles.content : null}
777795
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)}
796+
customButtons={prompt.buttons}
797+
onCustomButton={(button) => this.handleCustomPromptButton(button, prompt)}
782798
/>
783799
))}
784800
{extensionLibraryVisible ? (

src/containers/prompt.jsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Prompt extends React.Component {
1212
'handleOk',
1313
'handleScopeOptionSelection',
1414
'handleCancel',
15+
'handleCustomButton',
1516
'handleChange',
1617
'handleKeyPress',
1718
'handleCloudVariableOptionChange'
@@ -43,6 +44,9 @@ class Prompt extends React.Component {
4344
handleCancel () {
4445
this.props.onCancel();
4546
}
47+
handleCustomButton(button) {
48+
this.props.onCustomButton(button);
49+
}
4650
handleChange (e) {
4751
this.setState({inputValue: e.target.value});
4852
}
@@ -64,14 +68,15 @@ class Prompt extends React.Component {
6468
isCustom={this.props.isCustom}
6569
componentRef={this.props.componentRef}
6670
boxRef={this.props.boxRef}
67-
styleContent={this.props.styleContent}
68-
styleOverlay={this.props.styleOverlay}
69-
title={this.props.title}
70-
enterTitle={this.props.enterTitle}
71-
closeTitle={this.props.closeTitle}
7271
onOk={this.handleOk}
7372
onCancel={this.handleCancel}
7473
onKeyPress={this.handleKeyPress}
74+
styleContent={this.props.styleContent}
75+
styleOverlay={this.props.styleOverlay}
76+
title={this.props.title}
77+
config={this.props.config}
78+
customButtons={this.props.customButtons}
79+
onCustomButton={this.handleCustomButton}
7580
customRef={this.props.customRef}
7681
/>
7782
)
@@ -128,8 +133,9 @@ Prompt.propTypes = {
128133

129134
/* custom modals */
130135
isCustom: PropTypes.bool,
131-
enterTitle: PropTypes.string,
132-
closeTitle: PropTypes.string,
136+
config: PropTypes.object,
137+
onCustomButton: PropTypes.func,
138+
customButtons: PropTypes.arrayOf(PropTypes.object),
133139
customRef: PropTypes.oneOfType([
134140
PropTypes.func,
135141
PropTypes.shape({ current: PropTypes.instanceOf(Element) })

src/containers/sound-editor.jsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -509,18 +509,20 @@ class SoundEditor extends React.Component {
509509
const volumeParts = volumeDiv.children;
510510
let menu = await window.ScratchBlocks.customPrompt(
511511
{ title: "Modify Sound" }, { content: { width: "230px", height: "auto" } },
512-
{
513-
name: "Apply", callback: () => {
514-
audio.close();
515-
const pitch = pitchParts[1].value, volume = volumeParts[1].value;
516-
const truePitch = isNaN(Number(pitch)) ? 0 : Number(pitch);
517-
const trueVolume = isNaN(Number(volume)) ? 0 : Number(volume);
518-
this.handleEffect({
519-
pitch: truePitch * 10, volume: trueVolume
520-
});
521-
}
522-
},
523-
{ name: "Cancel", callback: () => audio.close() },
512+
[
513+
{
514+
name: "Apply", role: "ok", callback: () => {
515+
audio.close();
516+
const pitch = pitchParts[1].value, volume = volumeParts[1].value;
517+
const truePitch = isNaN(Number(pitch)) ? 0 : Number(pitch);
518+
const trueVolume = isNaN(Number(volume)) ? 0 : Number(volume);
519+
this.handleEffect({
520+
pitch: truePitch * 10, volume: trueVolume
521+
});
522+
}
523+
},
524+
{ name: "Cancel", role: "close", callback: () => audio.close() },
525+
],
524526
);
525527

526528
const modalHandler = () => {
@@ -631,14 +633,16 @@ class SoundEditor extends React.Component {
631633
let selectedForceRate = false;
632634
let menu = await window.ScratchBlocks.customPrompt(
633635
{ title: "Format Sound" }, { content: { width: "350px", height: "auto" } },
634-
{
635-
name: "Apply", callback: () => {
636-
const edits = { sampleRate: selectedSampleRate };
637-
if (selectedForceRate) edits.sampleRateEnforced = selectedSampleRate;
638-
this.handleEffect(edits);
639-
}
640-
},
641-
{ name: "Cancel", callback: () => {} },
636+
[
637+
{
638+
name: "Apply", role: "ok", callback: () => {
639+
const edits = { sampleRate: selectedSampleRate };
640+
if (selectedForceRate) edits.sampleRateEnforced = selectedSampleRate;
641+
this.handleEffect(edits);
642+
}
643+
},
644+
{ name: "Cancel", role: "close", callback: () => {} },
645+
],
642646
);
643647

644648
const modalHandler = () => {

0 commit comments

Comments
 (0)