From 7777c400a562add33bd1d3a80f3fe7559f742825 Mon Sep 17 00:00:00 2001 From: Olivier Gagnon Date: Mon, 3 May 2021 19:46:04 -0400 Subject: [PATCH] Make coding contract title click-to-copy --- src/CodingContracts.ts | 61 +++++++---------------- src/ui/React/CodingContractPopup.tsx | 61 +++++++++++++++++++++++ src/ui/React/CopyableText.tsx | 29 +++++++++-- src/ui/React/PopupButton.tsx | 72 ++++++++++++++++++++++++++++ src/ui/React/PopupCloseButton.tsx | 35 ++++++-------- 5 files changed, 188 insertions(+), 70 deletions(-) create mode 100644 src/ui/React/CodingContractPopup.tsx create mode 100644 src/ui/React/PopupButton.tsx diff --git a/src/CodingContracts.ts b/src/CodingContracts.ts index 3b93250b8..82b5bc5ba 100644 --- a/src/CodingContracts.ts +++ b/src/CodingContracts.ts @@ -14,8 +14,10 @@ import { } from "../utils/JSONReviver"; import { KEY } from "../utils/helpers/keyCodes"; import { createElement } from "../utils/uiHelpers/createElement"; -import { createPopup } from "../utils/uiHelpers/createPopup"; +import { createPopup, removePopup } from "./ui/React/createPopup"; import { removeElementById } from "../utils/uiHelpers/removeElementById"; +import { CodingContractPopup } from "./ui/React/CodingContractPopup"; + /* tslint:disable:no-magic-numbers completed-docs max-classes-per-file no-console */ @@ -171,57 +173,26 @@ export class CodingContract { * Creates a popup to prompt the player to solve the problem */ async prompt(): Promise { - // tslint:disable-next-line - return new Promise((resolve) => { - const contractType: CodingContractType = CodingContractTypes[this.type]; - const popupId = `coding-contract-prompt-popup-${this.fn}`; - const title: HTMLElement = createElement("h1", { - innerHTML: this.type, - }); - const txt: HTMLElement = createElement("p", { - innerHTML: ["You are attempting to solve a Coding Contract. You have", - `${this.getMaxNumTries() - this.tries} tries remaining,`, - "after which the contract will self-destruct.

", - `${contractType.desc(this.data).replace(/\n/g, "
")}`].join(" "), - }); - let solveBtn: HTMLElement; - const cancelBtn = createElement("a", { - class: "a-link-button", - clickListener: () => { + const popupId = `coding-contract-prompt-popup-${this.fn}`; + return new Promise((resolve, reject) => { + let popup = new CodingContractPopup({ + c: this, + popupId: popupId, + onClose: () => { resolve(CodingContractResult.Cancelled); - removeElementById(popupId); + removePopup(popupId); }, - innerText: "Cancel", - }); - const answerInput = createElement("input", { - onkeydown: (e: any) => { - if (e.keyCode === KEY.ENTER && answerInput.value !== "") { - e.preventDefault(); - solveBtn.click(); - } else if (e.keyCode === KEY.ESC) { - e.preventDefault(); - cancelBtn.click(); - } - }, - placeholder: "Enter Solution here", - width: "50%", - }) as HTMLInputElement; - solveBtn = createElement("a", { - class: "a-link-button", - clickListener: () => { - const answer: string = answerInput.value; - if (this.isSolution(answer)) { + onAttempt: (val: string) => { + console.log(`top; ${val}`); + if (this.isSolution(val)) { resolve(CodingContractResult.Success); } else { resolve(CodingContractResult.Failure); } - removeElementById(popupId); - }, - innerText: "Solve", + removePopup(popupId); + } }); - const lineBreak: HTMLElement = createElement("br"); - createPopup(popupId, [title, lineBreak, txt, lineBreak, lineBreak, answerInput, solveBtn, cancelBtn]); - answerInput.focus(); + createPopup(popupId, CodingContractPopup, popup.props); }); } diff --git a/src/ui/React/CodingContractPopup.tsx b/src/ui/React/CodingContractPopup.tsx new file mode 100644 index 000000000..6371b9eb5 --- /dev/null +++ b/src/ui/React/CodingContractPopup.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import { KEY } from "../../../utils/helpers/keyCodes"; + +import { CodingContract, CodingContractType, CodingContractTypes } from "../../CodingContracts"; +import { ClickableTag, CopyableText } from "./CopyableText"; +import { PopupCloseButton } from "./PopupCloseButton"; + +type IProps = { + c: CodingContract; + popupId: string; + onClose: () => void; + onAttempt: (answer: string) => void; +} + +type IState = { + answer: string; +} + +export class CodingContractPopup extends React.Component{ + constructor(props: IProps) { + super(props); + this.state = { answer: ''}; + this.setAnswer = this.setAnswer.bind(this); + this.onInputKeydown = this.onInputKeydown.bind(this); + } + + setAnswer(event: any) { + this.setState({ answer: event.target.value }); + } + + onInputKeydown(e:any){ + if (e.keyCode === KEY.ENTER && e.target.value !== "") { + e.preventDefault(); + this.props.onAttempt(this.state.answer); + } else if (e.keyCode === KEY.ESC) { + e.preventDefault(); + this.props.onClose(); + } + } + + render(): React.ReactNode { + const contractType: CodingContractType = CodingContractTypes[this.props.c.type]; + let description = []; + for (const [i, value] of contractType.desc(this.props.c.data).split('\n').entries()) + description.push({value}
); + return ( +
+ +

+

You are attempting to solve a Coding Contract. You have {this.props.c.getMaxNumTries() - this.props.c.tries} tries remaining, after which the contract will self-destruct.

+
+

{description}

+
+ + this.props.onAttempt(this.state.answer)} text={"Solve"} /> + +
+ ) + } +} \ No newline at end of file diff --git a/src/ui/React/CopyableText.tsx b/src/ui/React/CopyableText.tsx index 26ea3dc1a..f31b5d8f9 100644 --- a/src/ui/React/CopyableText.tsx +++ b/src/ui/React/CopyableText.tsx @@ -1,7 +1,13 @@ import * as React from "react"; +export enum ClickableTag{ + Tag_span, + Tag_h1 +} + type IProps = { value: string; + tag: ClickableTag; } type IState = { @@ -9,6 +15,11 @@ type IState = { } export class CopyableText extends React.Component { + public static defaultProps = { + //Default span to prevent destroying current clickables + tag: ClickableTag.Tag_span + }; + constructor(props: IProps) { super(props); @@ -53,9 +64,19 @@ export class CopyableText extends React.Component { render(): React.ReactNode { - return ( - {this.props.value} - Copied! - ); + switch (this.props.tag) { + case ClickableTag.Tag_h1: + return ( +

+ {this.props.value} + Copied! +

) + case ClickableTag.Tag_span: + return ( + + {this.props.value} + Copied! + ) + } } } \ No newline at end of file diff --git a/src/ui/React/PopupButton.tsx b/src/ui/React/PopupButton.tsx new file mode 100644 index 000000000..6a6bd4868 --- /dev/null +++ b/src/ui/React/PopupButton.tsx @@ -0,0 +1,72 @@ +/** + * Basic button for popup dialog boxes + * It creates an event handler such that pressing Esc will perform the click handler. + * + * Should only be used in other React components, otherwise it may not be properly + * unmounted + */ +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +import { KEY } from "../../../utils/helpers/keyCodes"; +import { removeElement } from "../../../utils/uiHelpers/removeElement"; + +export interface IPopupButtonProps { + class?: string; + popup: HTMLElement | string; + style?: object; + text: string; + onClose?: () => void; +} + +export class PopupButton extends React.Component { + constructor(props: IPopupButtonProps) { + super(props); + this.handleClick = this.handleClick.bind(this); + this.keyListener = this.keyListener.bind(this); + } + + componentDidMount() { + document.addEventListener("keydown", this.keyListener); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.keyListener); + } + + handleClick() { + if(this.props.onClose) + this.props.onClose(); + //We might be able to remove this? + //Clickhandler from the props will override this anyhow. + let popup: HTMLElement | null; + if (typeof this.props.popup === "string") { + popup = document.getElementById(this.props.popup); + } else { + popup = this.props.popup; + } + // TODO Check if this is okay? This is essentially calling to unmount a parent component + if (popup instanceof HTMLElement) { + ReactDOM.unmountComponentAtNode(popup); // Removes everything inside the wrapper container + removeElement(popup); // Removes the wrapper container + } + } + + keyListener(e: KeyboardEvent) { + //This doesn't really make sense, a button doesnt have to listen to escape IMO + //Too affraid to remove it since im not sure what it will break.. But yuck.. + if (e.keyCode === KEY.ESC) { + this.handleClick(); + } + } + + render(): React.ReactNode { + const className = this.props.class ? this.props.class : "std-button"; + + return ( + + ) + } +} \ No newline at end of file diff --git a/src/ui/React/PopupCloseButton.tsx b/src/ui/React/PopupCloseButton.tsx index 8375fabec..8230ff5ae 100644 --- a/src/ui/React/PopupCloseButton.tsx +++ b/src/ui/React/PopupCloseButton.tsx @@ -8,33 +8,27 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; -import { KEY } from "../../../utils/helpers/keyCodes"; import { removeElement } from "../../../utils/uiHelpers/removeElement"; +import { IPopupButtonProps, PopupButton } from "./PopupButton"; -export interface IPopupCloseButtonProps { +export interface IPopupCloseButtonProps extends IPopupButtonProps { class?: string; popup: HTMLElement | string; style?: any; text: string; + onClose: () => void; } -export class PopupCloseButton extends React.Component { +export class PopupCloseButton extends PopupButton { constructor(props: IPopupCloseButtonProps) { super(props); this.closePopup = this.closePopup.bind(this); - this.keyListener = this.keyListener.bind(this); - } - - componentDidMount(): void { - document.addEventListener("keydown", this.keyListener); - } - - componentWillUnmount(): void { - document.removeEventListener("keydown", this.keyListener); } closePopup(): void { + if(this.props.onClose) + this.props.onClose(); let popup: HTMLElement | null; if (typeof this.props.popup === "string") { popup = document.getElementById(this.props.popup); @@ -42,24 +36,23 @@ export class PopupCloseButton extends React.Component + )