跟踪多个文本输入的值以确认它们在 JavaScript/React 文字游戏中都是正确的最佳方法

Best way to track values of multiple text inputs to confirm they are all correct in a JavaScript/React word game

我正在为我的投资组合制作一个密码文字游戏(破译名言)。我遇到了最后一个主要障碍:确认拼图已解决。

我没有声望指向 post 真正有助于澄清的图片,但想想财富之轮,其中每个白框都是一个单独的输入。我没有将它们附加到任何状态,因为每个谜题很容易有超过 100 个输入。我一直在通过 className 操纵它们以相应地自动填充输入(即,如果玩家在一个输入中输入 'e',则所有类似的输入都将填充 'e')

我以为我已经完成确认,但很快意识到我的解决方案有多么缺陷。我正在创建一个 pendingSolution 状态,它是一个字符串,并试图通过输入中的 OnChange 函数适当地填充它。每次值更改时我都会检查它是否与解决方案字符串匹配,但事实证明我没有正确设置 pendingSolution 字符串的状态来更新玩家在谜题中的进度。我不确定这是否是最好的方法,所以我正在寻找一些帮助来修复我的 updatePendingSolution 函数,或者寻求有关解决此问题的更好方法的建议。

示例:

这些函数用于处理输入的 OnChange 以及自动填充最多三个随机字母作为 onClick 提示。

const handleChange = (e) => {
     let letter = document.getElementsByClassName(e.target.className)
     for (let i=0; i < letter.length; i++) {
          letter[i].value = e.target.value
          letter[i].style.color = "darkblue"
     }
     setPendingSolution(pendingSolution.split('').map((char, i) => char.replace(/\*/, updatePendingSolution(char, e.target.value, i))).join(''))    
}

const handleClick = () => {
     const rand = Math.floor(Math.random() * 26)
     if(solution === pendingSolution) return
     if (randLetters.includes(rand)) {
          handleClick()
     } else {
          setRandLetters([...randLetters, rand])
          let letter = document.getElementsByClassName(`char_input ${shuffledAlphabet[rand].toLowerCase()}`)
          if (letter.length === 0) {
               letter = document.getElementsByClassName(`char_input ${shuffledAlphabet[rand]}`)
               if(letter.length === 0) handleClick()    
          } else {
               if (letter[0].value.toUpperCase() === alphabet[rand]) {
                    handleClick()
               } else {
                    for (let i=0; i < letter.length; i++) {
                         letter[i].value = alphabet[rand]
                         letter[i].disabled = true
                         letter[i].style.color = 'green';
                         setPendingSolution(pendingSolution.split('').map((char, i) => char.replace('*', updatePendingSolution(char, alphabet[rand], i))).join(''))
                    }
               }
          }
          setHints([...hints, alphabet[rand]])               
     }      
}

这是我需要帮助的部分。这是我正在研究的函数,每当玩家使用提示或更改输入时更新 pendingSolution 状态。

useEffect(() => {
        if (solution && pendingSolution) {
            if (solution === pendingSolution) {
                alert("You won!!!")
            }
        }
    }, [pendingSolution])

const updatePendingSolution = (char, value, i) => {
     if (solution[i].match(value.toUpperCase())) {
          return value.toUpperCase()
     } else if (solution[i].match(value.toLowerCase())) {
          return value.toLowerCase() 
     } else return char   
}

这是呈现引号和名称组件的代码:

function EncryptedText({ divName, onChange, words } ) {
    return (
        <div className={divName}>{words.split(" ").map((word, i) => {
            return (
                <div className="word_div" key={i}> {word.split("").map((char, j) => {
                    return (
                        <div key={j} className="char_container">
                            {char.match(/[A-Za-z]/) ?
                                char.match(/[A-Z]/) ?
                                    <div>
                                        <p className="puzzle_p uppercase"><input className={`char_input ${char.toLowerCase()}`} maxLength="1" onChange={onChange} type="text" /></p>
                                    </div>
                                    :<div>
                                        <p className="puzzle_p lowercase"><input className={`char_input ${char}`} maxLength="1" onChange={onChange} type="text" /></p>
                                    </div>
                                : <p className="puzzle_p">{char}</p>
                            }
                            <p className="puzzle_p">{char}</p>
                        </div>
                    )
                })}</div>
            )
        })}</div>
    )
}

export default EncryptedText

虽然这是一种完全不同的方法,也没有用 React 实现,但 OP 可能会从中得到一些启发。

主要思想是提供一种 'wheel-of-fortune' 组件。

这样的组件会处理所有用户交互的状态,并且“知道”何时解决。

为此,它会清理传入的所有文本并将其拆分为单词。然后将每个单词拆分为字符,并为每个字符创建一个对应的 input 元素。单词是分组到列表中的字符,句子/引语/引文由此类列表组成。

简化任何其他任务的诀窍是引入...

  1. 基于弱引用的映射,其中每个输入元素都是其对应有效字母字符的键。
  2. 一个映射,其中针对每个有效的小写字母字符 store/aggregate 一个输入元素列表,每个元素都与该字符相关。
  3. 任何创建的输入元素的引用数组。

...并且任何组件在内部都将以上三个作为参考。

然后只需要处理组件根节点上的任何 input 事件(事件委托)。

对于每个事件目标(input 元素),确实通过节点引用查找正确的字符。然后比较查找字符和节点值的小写值。如果它们相等,则有一个匹配项,并且可以继续通过小写字符查找其他匹配的输入元素。对于每个其他匹配元素,将自动填充该元素的相关正确字符大小写。

当然总是需要检查组件是否已解决。这是通过将所有当前元素值 in/to 连接成一个字符串并将其与原始传递文本的净化版本进行比较来完成的。

下面提供的组件实现具有一个 response 承诺,可以由第三方代码使用。承诺在内部通过前一段描述的过程得到解决。

function isComponentSolved(match, placeholders) {
  const currentValue = placeholders
    .reduce((value, node) => value + node.value, '');

  return (match === currentValue);
}

function handleCharacterMatchUI(node, className) {
  const itemNode = node.closest('li');
  const { classList } = itemNode;

  classList.add(className);
  setTimeout((() => classList.remove(className)), 300);
}
function handleNextPlaceholderFocus(target, placeholders) {
  let idx = placeholders
    .findIndex(node => node === target);

  let nextNode = placeholders[++idx];

  while (nextNode) {
    if (nextNode.disabled) {

      nextNode = placeholders[++idx];
    } else {
      nextNode.focus();
      nextNode = null;
    }
  }
  if (idx >= placeholders.length) {
    idx = 0;
    nextNode = placeholders[idx];

    while (nextNode !== target) {
      if (nextNode.disabled) {

        nextNode = placeholders[++idx];
      } else {
        nextNode.focus();
        nextNode = target;
      }
    }
  }
}

function handleInputFromBoundComponentData({ target }) {
  const {
    settle,
    match,
    placeholders,
    charByNodeMap,
    nodesByCharMap,
  } = this;

  const value = target.value.toLowerCase();
  const char = charByNodeMap.get(target).toLowerCase();

  if (value === char) {
    nodesByCharMap
      .get(char)
      .forEach(node => {

        node.disabled = true;
        node.value = charByNodeMap.get(node);

        handleCharacterMatchUI(node, 'match');
      });
      handleNextPlaceholderFocus(target, placeholders);
  } else {
    handleCharacterMatchUI(target, 'mismatch');
  }

  if (isComponentSolved(match, placeholders)) {
    // resolve the component's
    // `response` promise with
    // the 'solved' payload.
    settle('solved');
  }
}

function createPlaceholderNode(char, charByNodeMap, nodesByCharMap) {
  const node = document.createElement('input');

  // test for Letter only character
  // using unicode property escapes.
  if ((/\p{L}/u).test(char)) {
    let nodeList = nodesByCharMap.get(char);

    if (!nodeList) {
      nodeList = [];
      nodesByCharMap.set(char.toLowerCase(), nodeList);
    }
    nodeList.push(node);

    charByNodeMap.set(node, char);
  } else {
    // non Letter character.
    node.disabled = true;
    node.value = char;
  }
  node.type = 'text';

  return node;
}

function createWheelOfFortuneComponent(root) {
  let response = null; // a promise for any valid component.

  const text = (root.dataset.wheelOfFortune ?? '')
    .trim().replace((/\s+/g), ' '); // text normalization.

  if (text !== '') {
    const charByNodeMap = new WeakMap;
    const nodesByCharMap = new Map;

    const placeholders = [];
    const wordRootList = text
      .split(/\s/)

      .map(word => word
        .split('')

        .reduce((listNode, char) => {
          const itemNode = document.createElement('li');
          const phNode =
            createPlaceholderNode(char, charByNodeMap, nodesByCharMap);

          placeholders.push(phNode);

          itemNode.appendChild(phNode);
          listNode.appendChild(itemNode);

          return listNode;
        }, document.createElement('ol'))
      );

    let settle;
    response = new Promise(resolve => {
      settle = resolve;
    });

    root.addEventListener(
      'input',
      handleInputFromBoundComponentData.bind({
        settle,
        match: text.replace((/\s/g), ''),
        placeholders,
        charByNodeMap,
        nodesByCharMap,
      })
    );
    wordRootList
      .forEach(wordRoot => root.appendChild(wordRoot));
  }
  root.dataset.wheelOfFortune = '';

  return {
    root,
    response,
  };
}

async function handleComponentResponseAsync({ root, response }) {
  if (response !== null) {
    const result = await response;

    root.classList.add(result);
  }
}
function app() {
  [...document.querySelectorAll('[data-wheel-of-fortune]')]
    .map(createWheelOfFortuneComponent)
    .forEach(handleComponentResponseAsync)
}
app();
[data-wheel-of-fortune] {
  margin: 8px 0;
  padding: 0 0 3px 0;
}
[data-wheel-of-fortune].solved {
  outline: 2px solid green;
}
[data-wheel-of-fortune].failed {
  outline: 2px solid red;
}
[data-wheel-of-fortune] ol,
[data-wheel-of-fortune] ul {
list-style-type: none;
  display: inline-block;
  margin: 0 4px;
  padding: 0;
  position: relative;
  top: 3px;
}
[data-wheel-of-fortune] ol::after,
[data-wheel-of-fortune] ul::after {
  clear: both;
  content: '';
}
[data-wheel-of-fortune] li {
  position: relative;
  float: left;
  margin: 0;
  padding: 0;
}
[data-wheel-of-fortune] li [type="text"] {
  width: 11px;
  margin: 0;
  padding: 0;
  text-align: center;
}
[data-wheel-of-fortune] li::after {
  z-index: 1;
  position: absolute;
  display: block;
  content: '';
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  transition-property: opacity;
  transition-duration: .3s;
  opacity: 0;
}
[data-wheel-of-fortune] li.match::after {
  background-color: #c2ef2f;
  opacity: .5;
}
[data-wheel-of-fortune] li.mismatch::after {
  background-color: #fb5100;
  opacity: .5;
}
<article data-wheel-of-fortune="The journey of a thousand miles begins with one step."></article>

<article data-wheel-of-fortune="Great minds discuss ideas; average minds discuss events; small minds discuss people."></article>

<article data-wheel-of-fortune="Thank you for your help!"></article>