zhconv 0.4.1

Traditional, Simplified and regional Chinese variants converter powered by MediaWiki & OpenCC rulesets and the Aho-Corasick algorithm 中文简繁及地區詞轉換
Documentation
import { useState, useEffect } from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Box from "@mui/material/Box";
import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText";
import InputLabel from "@mui/material/InputLabel";
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Chip from "@mui/material/Chip";
import Dialog /*, { DialogProps }*/ from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import Grid from "@mui/material/Grid";

import CGroupCheckbox from "./CGroupCheckbox";

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
  PaperProps: {
    style: {
      maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
      width: 250,
    },
  },
  keepMounted: true,
};

function CGroupDialog({
  cgroups,
  open,
  onClose: handleClose,
  onSelect: handleSelect,
  selected,
}: {
  cgroups: string[];
  open: boolean;
  onClose: () => void;
  onSelect: (cgroups: string[]) => void;
  selected: string[];
}) {
  // Note: For performance, Checkboxes are memoized, resulting in captured states to be stale
  //       when get updated. We have to use transition functions for setState.
  //       We maintain a additional somewhat duplicate state here to avoid receive a complex
  //       onSelect callback from the upstream, for ergonomics.
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [_selected_, setSelected_] = useState([] as string[]);
  const handleCheck = (name: string, checked: boolean) => {
    setSelected_((prev: string[]) => {
      const set = new Set(prev);
      if (checked) {
        set.add(name);
      } else {
        set.delete(name);
      }
      handleSelect(Array.from(set));
      return Array.from(set);
    });
  };
  // this might trigger an extra unnecessary render. but it won't cause an actually trouble
  // eslint-disable-next-line react-hooks/set-state-in-effect
  useEffect(() => setSelected_(selected), [selected]);
  const handleClear = () => {
    handleSelect([]);
  };
  const handleInvert = () => {
    const set = new Set(selected);
    handleSelect(cgroups.filter((name) => !set.has(name)));
  };
  return (
    <Dialog
      open={open}
      onClose={handleClose}
      scroll="paper"
      aria-labelledby="cgroups-dialog-title"
      aria-describedby="cgroups-dialog-description"
      keepMounted
    >
      <DialogTitle id="cgroups-dialog-title">
        <Box display="flex" alignItems="center" gap={1}>
          <span>CGroups / 轉換組</span>
          <IconButton
            size="small"
            href="https://zh.wikipedia.org/wiki/WP:CGROUP"
            target="_blank"
            rel="noopener"
            color="primary"
            sx={{ p: 0.5 }}
          >
            <InfoOutlined fontSize="small" />
          </IconButton>
        </Box>
      </DialogTitle>
      <DialogContent dividers>
        {cgroups.map((name) => (
          <CGroupCheckbox
            key={name}
            name={name}
            checked={selected.indexOf(name) > -1}
            onCheck={handleCheck}
          />
          // <FormControlLabel key={name} control={<input type="checkbox" id={name} name={name} value={name} />} label={name} />
        ))}
        <FormHelperText>Press Ctrl + F to search</FormHelperText>
      </DialogContent>
      <DialogActions>
        <Grid container direction="row" justifyContent="space-between">
          <Grid>
            <Button onClick={handleClear} color="primary">
              Clear / 清空
            </Button>
            <Button onClick={handleInvert} color="primary">
              Invert / 反選
            </Button>
          </Grid>
          <Grid>
            <Button onClick={handleClose} color="secondary">
              Ok / 好
            </Button>
          </Grid>
        </Grid>
      </DialogActions>
    </Dialog>
  );
}

export default function CGroupSelect({
  cgroups,
  selected,
  onSelect: handleSelect,
  disabled,
}: {
  cgroups: string[] | null;
  selected: string[];
  onSelect: (selected: string[]) => void;
  disabled?: boolean;
}) {
  const loading = cgroups === null;
  const isDisabled = loading || disabled;
  const [dialogOpen, setDialogOpen] = useState(false);
  // const handleDelete = (name: string) => {
  //   const set = new Set(selected);
  //   set.delete(name);
  //   handleSelect(Array.from(set));
  // };
  return (
    <>
      <FormControl variant="standard" sx={{ m: 1 }}>
        <InputLabel id="cgroups-select-label" color="primary">CGroups / 轉換組</InputLabel>
        <Select
          labelId="cgroups-select-label"
          id="cgroups-select"
          multiple
          value={selected.length > 0 ? selected : ["placeholder"]}
          open={false}
          onOpen={() => setDialogOpen(true)}
          style={{ width: "100%" }}
          fullWidth={true}
          disabled={isDisabled}
          color="primary"
          // input={<Input id="select-multiple-chip" />}
          renderValue={(selected) => (
            <Box
              sx={{
                display: "flex",
                justifyContent: "center",
                flexWrap: "wrap",
                listStyle: "none",
                p: 0.5,
                m: 0,
              }}
            >
              {loading ? (
                <CircularProgress size={24} color="inherit" />
              ) : (selected as string[]).length === 0 ||
                (selected as string[])[0] === "placeholder" ? (
                <Chip
                  key="add more"
                  label="Select ... / 選擇 ..."
                  color="primary"
                  sx={{ m: 0.3, cursor: "pointer" }}
                  variant="outlined"
                />
              ) : (
                (selected as string[]).map((name) => (
                  <Chip
                    key={name}
                    label={name}
                    // the ondelete event seems to be shadowed anyway
                    // onDelete={(event) => {
                    //   event.preventDefault();
                    //   event.stopPropagation();
                    //   handleDelete(name);
                    // }}
                    sx={{ m: 0.3, cursor: "pointer" }}
                    variant="outlined"
                  />
                ))
              )}
            </Box>
          )}
          MenuProps={MenuProps}
        >
          {selected.map((name) => (
            <MenuItem key={name} value={name}>
              {name}
            </MenuItem>
          ))}
        </Select>
      </FormControl>
      <CGroupDialog
        cgroups={cgroups || []}
        selected={selected}
        onSelect={handleSelect}
        open={dialogOpen}
        onClose={() => setDialogOpen(false)}
      />
    </>
  );
}