---
# System prepended metadata

title: Untitled

---

4
```json=
[
    {
      "colPos": "0",
      "desc": "校正儀器名稱",
      "fieldId": "201101717432791-00",
      "fieldSeq": "490835839",
      "height": "500",
      "isBKM": false,
      "isBold": true,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "0",
      "totalMergedCells": 8,
      "type": "label",
      "width": "500"
  },
  {
      "colPos": "0",
      "desc": "Dresser Down Force  Load Cell",
      "fieldId": "202081212523403-00",
      "fieldSeq": "490835983",
      "height": "500",
      "isBKM": false,
      "isBold": false,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "1",
      "totalMergedCells": 3,
      "type": "label",
      "width": "500"
  },
  {
      "colPos": "3",
      "desc": "[text](na)",
      "fieldId": "202081212523403-01",
      "fieldSeq": "490456892",
      "height": "500",
      "inChLoc": "",
      "isAutoPMStartTime": false,
      "isAutoToolId": false,
      "isBarcode": false,
      "isChanged": false,
      "isJig": true,
      "isNone": true,
      "length": "",
      "naRfCol": "",
      "partNum": "",
      "rowPos": "1",
      "totalMergedCells": 2,
      "type": "text",
      "width": "500"
  },
  {
      "colPos": "5",
      "fieldId": "202081212523403-02",
      "fieldSeq": "490835985",
      "fileUrl": "https://pmc-partsmiddle-central.gw.f12gpaas.tsmc.com.tw/PartsMiddle/rs/EpmmPmForm/getImageFile?formNo\u003dM-CCM-04-03-029-906\u0026fileName\u003d20230709155558.png",
      "filename": "Dresser Jig 20230607.png",
      "height": "500",
      "isChanged": false,
      "rowPos": "1",
      "totalMergedCells": 3,
      "type": "image",
      "width": "1150"
  },
  {
      "colPos": "0",
      "desc": "1. Check Downforce 是否符合規格",
      "fieldId": "2011017171720606-00",
      "fieldSeq": "490835835",
      "height": "500",
      "isBKM": false,
      "isBold": true,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "2",
      "totalMergedCells": 8,
      "type": "label",
      "width": "500"
  },
  {
      "colPos": "0",
      "desc": "Pol-A (Spec:18.0~22.0N)",
      "fieldId": "201101914532881-00",
      "fieldSeq": "490835843",
      "height": "500",
      "isBKM": false,
      "isBold": true,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "3",
      "totalMergedCells": 1,
      "type": "label",
      "width": "500"
  },{
      "ECSVID": "",
      "ECSVName": "",
      "autoFillName": "",
      "autoFillType": "",
      "colPos": "1",
      "decimal": "",
      "description": "dresser down force u1_Pressure_1",
      "fieldId": "201101914532881-01",
      "fieldSeq": "490835844_1",
      "fillInType": "",
      "height": "500",
      "isAutoCopyByName": false,
      "isAutoFillECSV": false,
      "isChanged": false,
      "isECSVID": true,
      "isECSVName": false,
      "isNone": true,
      "maxValue": "22",
      "minValue": "18",
      "muliply": "",
      "rowPos": "3",
      "source": "",
      "timing": "",
      "totalMergedCells": 1,
      "type": "numeric",
      "width": "500"
  },
  {
      "colPos": "2",
      "desc": "Pol-B (Spec:13.0~17.0N)",
      "fieldId": "201101914532881-02",
      "fieldSeq": "490835845",
      "height": "500",
      "isBKM": false,
      "isBold": false,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "3",
      "totalMergedCells": 1,
      "type": "label",
      "width": "500"
  },{
      "ECSVID": "",
      "ECSVName": "",
      "autoFillName": "",
      "autoFillType": "",
      "colPos": "3",
      "decimal": "",
      "description": "dresser down force u1_Pressure_2",
      "fieldId": "201101914532881-01",
      "fieldSeq": "490835844_2",
      "fillInType": "",
      "height": "500",
      "isAutoCopyByName": false,
      "isAutoFillECSV": false,
      "isChanged": false,
      "isECSVID": true,
      "isECSVName": false,
      "isNone": true,
      "maxValue": "17",
      "minValue": "13",
      "muliply": "",
      "rowPos": "3",
      "source": "",
      "timing": "",
      "totalMergedCells": 1,
      "type": "numeric",
      "width": "500"
  },
  {
      "colPos": "4",
      "desc": "Pol-C (Spec:18.0~22.0N)",
      "fieldId": "201101914532881-04",
      "fieldSeq": "490835847",
      "height": "500",
      "isBKM": false,
      "isBold": false,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "3",
      "totalMergedCells": 1,
      "type": "label",
      "width": "500"
  },{
      "ECSVID": "",
      "ECSVName": "",
      "autoFillName": "",
      "autoFillType": "",
      "colPos": "5",
      "decimal": "",
      "description": "dresser down force u2_Pressure_1",
      "fieldId": "201101914532881-05",
      "fieldSeq": "490835848",
      "fillInType": "",
      "height": "500",
      "isAutoCopyByName": false,
      "isAutoFillECSV": false,
      "isChanged": false,
      "isECSVID": true,
      "isECSVName": false,
      "isNone": true,
      "maxValue": "22",
      "minValue": "18",
      "muliply": "",
      "rowPos": "3",
      "source": "",
      "timing": "",
      "totalMergedCells": 1,
      "type": "numeric",
      "width": "500"
  },
  {
      "colPos": "6",
      "desc": "Pol-D (Spec:13.0~17.0N)",
      "fieldId": "201101914532881-06",
      "fieldSeq": "490835849",
      "height": "500",
      "isBKM": false,
      "isBold": false,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "3",
      "totalMergedCells": 1,
      "type": "label",
      "width": "500"
  },{
      "ECSVID": "",
      "ECSVName": "",
      "autoFillName": "",
      "autoFillType": "",
      "colPos": "7",
      "decimal": "",
      "description": "dresser down force u2_Pressure_2",
      "fieldId": "201101914532881-07",
      "fieldSeq": "490835850",
      "fillInType": "",
      "height": "500",
      "isAutoCopyByName": false,
      "isAutoFillECSV": false,
      "isChanged": false,
      "isECSVID": true,
      "isECSVName": false,
      "isNone": true,
      "maxValue": "17",
      "minValue": "13",
      "muliply": "",
      "rowPos": "3",
      "source": "",
      "timing": "",
      "totalMergedCells": 1,
      "type": "numeric",
      "width": "500"
  },
  {
      "colPos": "0",
      "desc": "7.3  檢查清潔 Polish-ABCD FAC drain 管 \n※於2,4,6,8,10,12月份執行",
      "fieldId": "2020812122047399-00",
      "fieldSeq": "490835968",
      "height": "500",
      "isBKM": false,
      "isBold": false,
      "isChanged": false,
      "isLink": false,
      "isPM50": false,
      "isTB": false,
      "rowPos": "4",
      "totalMergedCells": 4,
      "type": "label",
      "width": "500"
  },{
      "colPos": "4",
      "fieldId": "2020812122047399-01",
      "fieldSeq": "490835969",
      "height": "500",
      "isChanged": false,
      "layout": "horizontal",
      "radioButtonGroup":[ {
      "desc": "執行",
      "sequence": "0"},
      {
      "desc": "不需執行",
      "sequence": "1"
      }
    ],
      "rowPos": "4",
      "selectedRadioBtn": "",
      "template": "none",
      "totalMergedCells": 4,
      "type": "radioButton",
      "width": "500"
  }
  ,{
    "colPos": "1",
    "fieldId": "2020812122047399-01",
    "fieldSeq": "8888888888",
    "height": "500",
    "isChanged": false,
    "layout": "horizontal",
    "radioButtonGroup":[ {
    "desc": "V",
    "sequence": "0"},
    {
    "desc": "X",
    "sequence": "1"
    },
    {
    "desc": "NA",
    "sequence": "2"
    }
  ],
    "rowPos": "5",
    "selectedRadioBtn": "",
    "template": "none",
    "totalMergedCells": 4,
    "type": "radioButton",
    "width": "500"
}
,{
    "colPos": "1",
    "fieldId": "2020812122047399-01",
    "fieldSeq": "99999999999",
    "height": "500",
    "isChanged": false,
    "layout": "horizontal",
    "radioButtonGroup":[ {
    "desc": "V",
    "sequence": "0"},
    {
    "desc": "X",
    "sequence": "1"
    },
    {
    "desc": "NA",
    "sequence": "2"
    },
    {
    "desc": "NAAAA",
    "sequence": "3"
    }
  ],
    "rowPos": "6",
    "selectedRadioBtn": "",
    "template": "none",
    "totalMergedCells": 4,
    "type": "radioButton",
    "width": "500"
}
]
```


```typescript=
import { useRef, useState,  useEffect} from 'react';
import { useHistory } from 'react-router-dom';
import { Workbook, WorkbookInstance } from "@fortune-sheet/react";
import "@fortune-sheet/react/dist/index.css";
import _formJsonData from "./data/FormJsonData.json";
import { Util } from './model/Util';
import { FieldSrc } from './model/FieldSrc'
import { LabelSrc } from './model/LabelSrc';
import { TextSrc } from './model/TextSrc';
import { NumericSrc } from './model/Numeric';
import { RadioButtonSrc } from './model/RadioButton';
import { ImageSrc } from './model/ImageSrc';
import "./style.css";


import { Alert,  Button, Dialog } from '@mui/material';
import { LocaleTW, LocaleEn } from './locale/formEditorLocale';


type ArrayElement = FieldSrc | LabelSrc | TextSrc | NumericSrc | RadioButtonSrc | ImageSrc;

const useConsoleLog = false;
const isShiftRightWhenLoading = true;
// -----------------------
export default function FormLinker() {
  const ref = useRef<WorkbookInstance>(null);
  const history = useHistory();

  const [language, setLanguage] = useState("TW");
  const [locale, setLocale] = useState(language === "TW" ? new LocaleTW : new LocaleEn);

  const [lastRow, setLastRow] = useState(-1);
  const [lastCol, setLastCol] = useState(-1);


  const [showRightSideBlock, setShowRightSideBlock] = useState(false);
  const [alertBack, setAlertBack] = useState(false);
  const [alertRefresh, setAlertRefresh] = useState(false);

  // 一開始存取localStorage 存的” linkageData”，當作呈現在左側表格的資料
  // 此資料為在FormEditor(/edit-form)中，按下[SAVE]後所存的資料
  const [importData, setImportData] = useState(localStorage.getItem("linkageData"));

  const [exportJson, setExportJson] = useState(updateLastExportDataPosition());
  const [isLoading, setIsLoading] = useState(true);
 
 
  // ------------------------------ [Custom Toolbar Items]----------------------------


  const customToolbarItems_Linkage = [
    { // 返回上一頁
      key: "BACK",
      tooltip: "Back",
      icon: Util.ICON_LEFT,
      onClick: async () => {
        setAlertBack(true);
      }
    },
    { // 重新整理 (回到上次儲存的Data)
      key: "REFRESH",
      tooltip: "Refresh",
      icon: Util.ICON_REFRESH,
      onClick: async () => {
        setAlertRefresh(true);
      }
    },
    { // 編輯Auto NA 設定
      key: "EDIT",
      tooltip: "Edit",
      icon: Util.ICON_EDIT,
      onClick: async () => {

        let rowRange = ref.current?.getSelection()?.at(0)?.row;
        let colRange = ref.current?.getSelection()?.at(0)?.column;

        if (rowRange && colRange) {
          // 限定只有radioButton才可以編輯
          let type = checkType(Number(rowRange[0]), Number(colRange[0]));
          if (type === Util.C_CELL_TYPE_RADIOBUTTON) {
            handleAutoNA(Number(rowRange[0]), Number(colRange[0]));
          }

        }

      }
    },
    { // 存成json格式
      key: "SAVE",
      tooltip: "Save",
      icon: Util.ICON_SAVE,
      onClick: async () => {
        handleSaveData();
      }
    },
  ];



  // ------------------------------------------------------------------

  
  function updateLastExportDataPosition() {
    /**修正目前所有欄位的位置 
     * 比對importData的fieldSeq, 去更新lastExportData的[row, col]
     * 包括本cell與linkedCell
    */
    let lastExportData = localStorage.getItem("linkedCellSettings");
    if ( lastExportData && importData) {
      let linkedCellData = JSON.parse(lastExportData);
      
      let allCellsData = JSON.parse(structuredClone(importData));

      
      for( let index = 0; index < linkedCellData.length; index++ ) {
        let curCell = linkedCellData[index];
       
        // Find the matching object in allCellsData
        let cell = allCellsData.filter((cell: { fieldSeq: string }) => cell.fieldSeq === curCell.fieldSeq);
        if ( cell && cell[0]) {
          // 本cell
          linkedCellData[index].row = Number(cell[0].rowPos); 
          linkedCellData[index].col = Number(cell[0].colPos);


          if (linkedCellData[index]['linkedCell'] && linkedCellData[index]['linkedCell'][0]){

            for( let seq = 0; seq < linkedCellData[index]['linkedCell'].length; seq++ ) {
              let obj = linkedCellData[index]['linkedCell'][seq];

              // Find the matching object in allCellsData
              let matchCell =  allCellsData.filter((cell: { fieldSeq: string }) => cell.fieldSeq === obj.seq);
              if ( obj && matchCell && matchCell[0] ) {
                obj.r = Number(matchCell[0].rowPos);
                obj.c = Number(matchCell[0].colPos);
              }
              else { // 之前linked的Cell被刪除或改成其他type
                linkedCellData[index]['linkedCell'].splice(seq, 1); // 刪除之前linked的Cell
              }
            } // for
          } 
        }
      }

      return linkedCellData;
    }

    return [];
  }




  // --------------------[LOAD]---------------------------------------------
  const LoadData = async () => {

    if (importData) {
      
      let jsonData = JSON.parse(importData);
      loadDataFromJson(jsonData);
      
    }
    else {
      loadDataFromJson(_formJsonData);
    }

  }

  useEffect(() => {

    LoadData().then(() => setIsLoading(false))
      .then(() => setLinkedCellSettings(structuredClone(exportJson)))
      .then(() => initFontBackgroundColor());

  }, [])

  // ------------------------------------------------------------------------


  // -------------------------[Font/Background Color]-------------------------
  function setCellColor(row: number, col: number, color: string) {
    ref.current?.setCellFormat(row, col, "bg", color);
  }

  function setCellFontColor(row: number, col: number, color: string) {
    ref.current?.setCellFormat(row, col, "fc", color);
  }

  function initFontBackgroundColor() {
    /** Highlight radioButton cell 
     * radioButton: 背景淺藍，字黑色
     * 其餘: 灰色
     * */

    let allLinkedCells = exportJson? exportJson: linkedCellSettings;

    const sheet = ref.current?.getAllSheets();
    if (sheet) {

      // find last non-null row & col
      const pos = findLastNonNullRowAndColumn(sheet[0].data); // pos[0]: row, pos[1]: col 
      setLastRow(pos[0]);
      setLastCol(pos[1]);

      let borderRow = pos[0] + 1;
      let borderCol = pos[1] + 1;

      if (typeof borderRow === "undefined" || typeof borderCol === "undefined") return;
      // traverse all cell, set font/background color
      for (let row = 0; row < borderRow; row++) {
        for (let col = 0; col < borderCol; col++) {
          setCellColor(row, col, "white");

          let type = checkType(row, col);
          switch (type) {
            case Util.C_CELL_TYPE_RADIOBUTTON:
                let cell = allLinkedCells.filter((cell: { row: number; col: number; }) => cell.row === row && cell.col === col);
                // 已有設定連動的cell 設為藍色
              setCellColor(row, col, Util.BACKGROUND_COLOR_LIGHT_BLUE); // 尚未設定連動的為淺藍

              if (cell && cell.length > 0) {
                for( let index = 0; index < cell.length; index++ ) {
                  if ( cell[index].linkedCell.length > 0 ) {
                    setCellColor(row, col, Util.BACKGROUND_COLOR_BLUE); // 設定連動的為BLUE
                    break;
                  }
                }
              } // if 
              // else setCellColor(row, col, Util.BACKGROUND_COLOR_LIGHT_BLUE); // 尚未設定連動的為淺藍
              break;

            default:
              setCellFontColor(row, col, "#DCDCDC");
          } // switch
        }
      }
    } // if
  }

  function setOperableCellColor(op: string, linkedCellRow: number, linkedCellCol: number) {
    /** 
     * [op]
     * "on":  Highlight可進行連動的欄位(text, numeric, radioBtn) : 背景淺藍， 字黑色
     *        目前進行設定的radioBtn Cell: 黃色
     * "off": 還原cell顯示: 
     *          RadioBtn: 
     *              已有設定連動: 藍色
     *              尚未設定連動: 淺藍
     *          Others:
     *            背景白色，字灰色
     * */

    let color = op === "on" ? Util.BACKGROUND_COLOR_LIGHT_BLUE : "white";
    let fontColor = op === "on" ? "black" : Util.BACKGROUND_COLOR_GREY;
    for (let row = 0; row <= lastRow; row++) {
      for (let col = 0; col <= lastCol; col++) {

        let type = checkType(row, col);
        switch (type) {
          case Util.C_CELL_TYPE_TEXTINPUT:
            setCellColor(row, col, color);
            setCellFontColor(row, col, fontColor);
            break;

          case Util.C_CELL_TYPE_NUMINPUT:
            setCellColor(row, col, color);
            setCellFontColor(row, col, fontColor);
            break;

          case Util.C_CELL_TYPE_RADIOBUTTON:
            if (op === "on") setCellColor(row, col, color);
            else {
              const cell = linkedCellSettings.filter((cell: { row: number; col: number; }) => cell.row === row && cell.col === col);

             if (cell && cell.length > 0) {
                // 已有設定連動的cell: 藍色，
                setCellColor(row, col, Util.BACKGROUND_COLOR_BLUE);
              }
              else {
                // 尚未設定連動: 淺藍
                setCellColor(row, col, Util.BACKGROUND_COLOR_LIGHT_BLUE);
              }
            } // else 
            break;

          default:
          // setCellColor(row, col, "#DCDCDC");
        } // switch
      }
    }

    if (linkedCellRow !== -1 && linkedCellCol !== -1) {
      // 目前進行設定的radioBtn Cell: 黃色
      setCellColor(linkedCellRow, linkedCellCol, Util.BACKGROUND_COLOR_YELLOW);
    } // if 
  }
  // ------------------------------------------------------------------------


  // 目前進行設定的radioBtn Cell
  const [linkedCell, setLinkedCell] = useState(new RadioButtonSrc());
  const [curRow, setCurRow] = useState(-1);
  const [curCol, setCurCol] = useState(-1);

  // 儲存每個選項對應的欄位設定
  let lastLinkedCellData = localStorage.getItem("linkedCellSettings");
  const [linkedCellSettings, setLinkedCellSettings] = useState(lastLinkedCellData ? JSON.parse(lastLinkedCellData) :
    [{ fieldSeq: "", row: -1, col: -1, index: -1, linkedCell: [{ r: -1, c: -1, seq: "" }] }]);

  function loading() {
    return (
      <div>
        <div className="loading-container">
          <div className="loading-text">
            <span>L</span>
            <span>O</span>
            <span>A</span>
            <span>D</span>
            <span>I</span>
            <span>N</span>
            <span>G</span>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className='container' >

      {/* 左分欄 */}
      <div className="leftside block" style={{ width: '70%' }}>
        {isLoading ? <div>{loading()}</div> : <></>}


        <Workbook ref={ref}
          data={[{
            name: 'Linkage',
            config: {
              rowReadOnly: {},
              colReadOnly: {
                0: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1, 10: 1, 11: 1, 12: 1, 13: 1, 14: 1, 15: 1
              }
            }
          }]}
          showSheetTabs={false}
          toolbarItems={[]}
          customToolbarItems={customToolbarItems_Linkage}
          showFormulaBar={false}
        />
      </div>

      { /** 右分欄 */}
      <div className="rightside block" style={{ left: '70%' }}>
        <div className='rightside-title'> <h2>[設定欄位連動]</h2></div>
        {showRightSideBlock ? <div>{linkHints()}</div> : <div>{startHints()}</div>}

        {showRightSideBlock && linkedCell && typeof (linkedCell.radioButtonGroup) !== "undefined" && linkedCell.radioButtonGroup.length !== 0 ? (
        Array.from({ length: linkedCell.radioButtonGroup.length }).map((_, index) => {
            const boxId = `${linkedCell.rowPos}-${linkedCell.colPos}-${index}`;
            return (
              <div key={index} className="box" id={boxId}>
                <div className="box-header">
                  {linkedCell.radioButtonGroup ? linkedCell.radioButtonGroup[index].desc : null}
                </div>
                <div className="box-content">
                  {linkedCellSettings
                    .filter((cellSetting: { row: number; col: number; index: number; }) => cellSetting.row === Number(linkedCell.rowPos) && cellSetting.col === Number(linkedCell.colPos) && cellSetting.index === index)
                    .map((cellSetting: { linkedCell: any[]; }) => cellSetting.linkedCell.map((linkedCellItem) => (
                      <div key={`${linkedCellItem.r}-${linkedCellItem.c}`} className="linked-cell">
                        {getLabel(linkedCellItem.r, linkedCellItem.c)}
                      </div>
                    )))
                  }
                </div>

                <div className="box-buttons">
                  <Button variant="outlined" onClick={() => handleAddLinkedCell(index)}>Add</Button>
                  <Button variant="outlined" onClick={() => handleDeleteLinkedCell(index)}>Delete</Button>
                </div>
              </div>
            );
          })
        )
          :
          null

        }
      </div>

      { /** 分割線 */}
      <div
        className="divider"
        style={{ left: '70%' }}
      />
      {
        alertBack ?
          <>
            <Dialog open={alertBack}>
              <Alert variant="outlined" severity="warning">
                {locale.Back_Alert}
                <Button variant="outlined" onClick={handleCancelBack}>{locale.ConfigBaseRender_btnCancel}</Button>
                <Button variant="outlined" onClick={handleBack}>{locale.YES}</Button>
              </Alert>
            </Dialog>

          </> :
          <></>
      }
      {
        alertRefresh ?
          <>
            <Dialog open={alertRefresh}>
              <Alert variant="outlined" severity="warning">
                {locale.Refresh_Alert}
                <Button variant="outlined" onClick={handleCancelRefresh}>{locale.ConfigBaseRender_btnCancel}</Button>
                <Button variant="outlined" onClick={handleRefresh}>{locale.YES}</Button>
              </Alert>
            </Dialog>


          </> :
          <></>
      }
    </div>
  );

  // ----------------------------------------

  function showEditBlock() {
    setShowRightSideBlock(true);
  }

  function hideEditBlock() {
    setShowRightSideBlock(false);
  }


  function handleAutoNA(row: number, col: number) {
    if (useConsoleLog) console.log("Start handleAutoNA [+]");
    setCurRow(row);
    setCurCol(col);
    showEditBlock();
    setOperableCellColor("on", Number(row), Number(col));

    // 設定目前進行設定的radioBtn Cell -> linkedCell
    let cell = getCellData(row, col);
    let curCell = cell.at(0);
    if (cell && curCell && curCell.radioButtonGroup) {
      setLinkedCell(structuredClone(curCell));
    } // if 

    if (useConsoleLog) console.log("End handleAutoNA [-]");
  }

  function isLinkedCellExist(row: number, col: number) {
    if ( row === -1 && col === -1 ) return false;
    return true;
  }

  function getLabel(row: number, col: number) {
    if ( isLinkedCellExist(row, col) ) return '[' + getLabelName( row, col)+ ']';
    else return '';
  }

  function getLabelName(row: number, col: number) {
    /** 取得欄名列號 e.g. [3, 7] -> H4 */


    let label = "";
    const A_CODE = 65; // ASCII code for 'A'
    const colLetter = A_CODE + col;
    label = String.fromCharCode(colLetter) + (row + 1).toString();
    return label;
  }


  function handleAddLinkedCell(index: number) {
    /**加入欲連動的欄位([row, col]) 
     * index為目前radio button的第幾個選項，(從0開始)
    */

    // 取得所選取的欄位的[row, col]
    let rowRange = ref.current?.getSelection()?.at(0)?.row;
    let colRange = ref.current?.getSelection()?.at(0)?.column;


    if (rowRange && colRange) {
      let row = rowRange[0];
      let col = colRange[0];


      // 只能連動text, numeric, radioBtn
      let type = checkType(row, col);
      if (type != Util.C_CELL_TYPE_NUMINPUT &&
        type != Util.C_CELL_TYPE_RADIOBUTTON &&
        type != Util.C_CELL_TYPE_TEXTINPUT) {
        return;
      } // if

      // 自己這一格不用連動
      if (row === curRow && col === curCol) return;


      let curCellSetting = linkedCellSettings.find((cell: { row: number; col: number; index: number; }) => cell.row === curRow && cell.col === curCol && cell.index === index);
      if (curCellSetting) { // update existing setting

        setLinkedCellSettings((prevState: any[]) => prevState.map(setting => {
          if (setting === curCellSetting) {
            // 避免重複加入相同[row, col]
            if (!setting.linkedCell.some((linked: { r: number; c: number; }) => linked.r === row && linked.c === col)) {
              let cell = getCellData(row, col);
              let curCell = cell.at(0);
              if (cell && curCell) {
                return {
                  ...setting,
                  linkedCell: [
                    ...setting.linkedCell,
                    { r: row, c: col, seq: curCell.fieldSeq }
                  ]
                };
              } // if 
            }
          }
          return setting;
        }));
      }
      else { // create new setting
        let cell = getCellData(curRow, curCol);
        let curCell = cell.at(0)

        let addCell = getCellData(row, col).at(0);
        if (cell && curCell && addCell) {
          setLinkedCellSettings((prevState: any) => [
            ...prevState,
            {
              fieldSeq: curCell?.fieldSeq || "",
              row: curRow,
              col: curCol,
              index: index,
              linkedCell: [{ r: row, c: col, seq: addCell?.fieldSeq || "" }]
            }
          ]);
        } // if 
      } // else 
    } // if 
  }


  function handleDeleteLinkedCell(index: number) {
    /**取消該欄位([row, col])連動 
     * index為目前radio button的第幾個選項，(從0開始)
    */

    let rowRange = ref.current?.getSelection()?.at(0)?.row;
    let colRange = ref.current?.getSelection()?.at(0)?.column;

    if (rowRange && colRange) {
      let row = rowRange[0];
      let col = colRange[0];


      setLinkedCellSettings((prevState: any[]) => {
        // 找到目前進行設定的radioBtn Cell (with相同的row, col, index)
        let settingIndex = prevState.findIndex(cell => cell.row === curRow && cell.col === curCol && cell.index === index);
        if (settingIndex !== -1) {
          // 將欲刪除[row, col]以外的所有cell存入newLinkedCell中
          let newLinkedCell = prevState[settingIndex].linkedCell.filter((cell: { r: number; c: number; }) => !(cell.r === row && cell.c === col));


          if (newLinkedCell.length === 0) {
            // 若newLinkedCell是空的, 則刪除所有設定
            return [...prevState.slice(0, settingIndex), ...prevState.slice(settingIndex + 1)];
          }
          else {
            // 將舊的linkedCell更新為newLinkedCell
            return [
              ...prevState.slice(0, settingIndex),
              {
                ...prevState[settingIndex],
                linkedCell: newLinkedCell
              },
              ...prevState.slice(settingIndex + 1)
            ];
          }
        }

        return prevState;
      });
    }
  }

  function handleSaveData() {
    hideEditBlock();
    setOperableCellColor("off", -1, -1);

    setExportJson(structuredClone(linkedCellSettings));

    localStorage.setItem("linkedCellSettings", JSON.stringify(structuredClone(linkedCellSettings)));
    console.log(linkedCellSettings);
  }


  function startHints() {
    return (
      <div className="icon-text">
        <div>
          請於左側選擇要設定連動的欄位
        </div>

        <div>
          再按左上角的
          {Util.ICON_EDIT_MINI}
          圖示，進行設定。
        </div>


        <br /><br />
        <h2>[儲存連動設定]</h2>
        <div>按下左上角
          {Util.ICON_SAVE_MINI}
          圖示，即儲存欄位連動設定
        </div>



        <br /><br />
        <h2>[重新整理]</h2>
        <div>按下左上角
          {Util.ICON_REFRESH_MINI}
          圖示，即回到上次儲存的連動資料
        </div>
        <div style={{ color: "red" }}>注意: 未儲存的資料將會遺失</div>

      </div>
    );
  }

  function linkHints() {
    return (
      <div className="icon-text">
        <div>
          請先於左側選擇要連動的欄位
        </div>

        <div>
          按下[ADD]，加入該選項連動，
        </div>

        <div>
          按下[Delete]，取消該選項連動
        </div>

      </div>
    );
  }


  function handleBack() {
    setAlertBack(false);
    history.push("/");
  }

  function handleCancelBack() {
    setAlertBack(false);
  }



  function handleRefresh() {
    if (useConsoleLog) console.log('Start handleRefresh [+]');
    hideEditBlock();
    setAlertRefresh(false);

    // clear all cells, unmerge cells
    for (let row = 0; row <= lastRow; row++) {
      for (let col = 0; col <= lastCol; col++) {
        ref.current?.clearCell(row, col);
        ref.current?.setCellFormat(Number(row), Number(col), "fc", "black");
        ref.current?.setCellFormat(Number(row), Number(col), "bg", 'white');
      }
    }


    let rowRange = [0, lastRow];
    let colRange = [0, lastCol];
    ref.current?.cancelMerge([{ row: rowRange, column: colRange }]);

    resetLinkedCellSettings();

    // reLoad data, set font/background Color
    LoadData().then(() => initFontBackgroundColor());

    if (useConsoleLog) console.log('End handleRefresh [-]');
  }

  function handleCancelRefresh() {
    setAlertRefresh(false);
  }


  function resetLinkedCellSettings() {
    /**將linkedCellSettings清空，並更新為上次儲存的連動資料 ( exportJson ) */


    if (linkedCellSettings != null) {
      while (linkedCellSettings.length > 0) {
        linkedCellSettings.pop();
      }
    } // if 

    if (exportJson) {
      for (let index = 0; index < exportJson.length; index++) {
        linkedCellSettings.push(exportJson[index]);
      }
    }

  }

  function checkType(row: number, col: number) {
    let cell: any;
    if (importData) {
      cell = JSON.parse(importData).filter((data: { rowPos: string; colPos: string; }) => data.rowPos === row.toString() && data.colPos === col.toString());
    } // if 
    else {
      cell = _formJsonData.filter(data => data.rowPos === row.toString() && data.colPos === col.toString());
    } // else 

    return cell.at(0)?.type;
  }

  function getCellData(row: number, col: number) {
    let cell: any;
    if (importData) {
      cell = JSON.parse(importData).filter((data: { rowPos: string; colPos: string; }) => data.rowPos === row.toString() && data.colPos === col.toString());
    } // if 
    else {
      cell = _formJsonData.filter(data => data.rowPos === row.toString() && data.colPos === col.toString());
    } // else 

    return cell;
  }

  // ----------------------------------------
  // Load data from json file
  async function loadDataFromJson(jsonData: any) {

    // for shiftRight
    let lastCol = -1;
    let lastRow = -1;

    for (let i = 0; i < jsonData.length; i++) {

      let data = jsonData[i];
      let curCol = Number(data.colPos);

      let dataInput;
      switch (data.type) {
        case Util.C_CELL_TYPE_LABEL:
          dataInput = (data as LabelSrc);
          break;

        case Util.C_CELL_TYPE_TEXTINPUT:
          dataInput = (data as TextSrc);
          break;

        case Util.C_CELL_TYPE_NUMINPUT:
          dataInput = (data as NumericSrc);
          break;

        case Util.C_CELL_TYPE_RADIOBUTTON:
          dataInput = (data as RadioButtonSrc);
          break;

        case Util.C_CELL_TYPE_IMAGE:
          dataInput = (data as ImageSrc);
          break;


        default:
          ;
      }

      if (data.rowPos && data.colPos) {
        if (isShiftRightWhenLoading) {
          if (lastRow !== Number(data.rowPos)) { // diff row
            lastCol = -1;
          } // if 
          else if (lastCol != -1 && curCol <= lastCol) {
            data.colPos = (lastCol + 1).toString();
          }
        }

        
        if ( typeof dataInput !== "undefined" ) showDataInCell(data.type, dataInput, Number(data.rowPos), Number(data.colPos));

        let rowRange = [Number(data.rowPos), Number(data.rowPos)];
        let colRange = [Number(data.colPos), Number(data.colPos) + Number(data.totalMergedCells) - 1];

        // console.log(rowRange, colRange);
        showMergeCell(rowRange, colRange);
      } // if 

      lastCol = Number(data.colPos) + Number(data.totalMergedCells) - 1;
      lastRow = Number(data.rowPos);

    } // end for
  }

  function showMergeCell(rowRange: number[], colRange: number[]) {
    ref.current?.mergeCells([{ row: [rowRange[0], rowRange[0]], column: colRange }], "merge-all");
  }

  function showDataInCell(type: string, data: ArrayElement, row: Number, col: Number) {
    if (useConsoleLog) console.log('Start showDataInCell [+]');
    // console.log(data)
    switch (type) {
      case Util.C_CELL_TYPE_LABEL:
        let data_label = (data as LabelSrc);
        ref.current?.setCellValue(Number(row), Number(col), data_label?.desc);
        for (let seq = 0; seq < data_label.totalMergedCells; seq++) {

          if (data_label?.isBold) ref.current?.setCellFormat(Number(row), Number(col) + seq, "bl", 1);
          else ref.current?.setCellFormat(Number(row), Number(col) + seq, "bl", 0);

          if (data_label?.isTB || data_label?.isBKM) ref.current?.setCellFormat(Number(row), Number(col) + seq, "bg", 'yellow');
          else ref.current?.setCellFormat(Number(row), Number(col) + seq, "bg", 'white');

          if (data_label?.isPM50) ref.current?.setCellFormat(Number(row), Number(col) + seq, "fc", "blue");
          else ref.current?.setCellFormat(Number(row), Number(col) + seq, "fc", "black");
        }

        break;
      case Util.C_CELL_TYPE_TEXTINPUT:
        let data_text = (data as TextSrc);
        let outputDescText = generateOutputDesc(data_text, Util.C_CELL_TYPE_TEXTINPUT);
        ref.current?.setCellValue(Number(row), Number(col), outputDescText);
        break;
      case Util.C_CELL_TYPE_NUMINPUT:
        let data_numeric = (data as NumericSrc);
        let outputDescNumeric = generateOutputDesc(data_numeric, Util.C_CELL_TYPE_NUMINPUT);
        ref.current?.setCellValue(Number(row), Number(col), outputDescNumeric ? outputDescNumeric : '');
        break;
      case Util.C_CELL_TYPE_RADIOBUTTON:
        let data_radioBtn = (data as RadioButtonSrc);
        let outputDescRadioBtn = generateOutputDesc(data_radioBtn, Util.C_CELL_TYPE_RADIOBUTTON);
        ref.current?.setCellValue(Number(row), Number(col), outputDescRadioBtn ? outputDescRadioBtn : '');
        break;
      case Util.C_CELL_TYPE_IMAGE:
        let data_image = (data as ImageSrc);
        let outputDescImage = generateOutputDesc(data_image, type);
        ref.current?.setCellValue(Number(row), Number(col), outputDescImage ? outputDescImage : '');
        break;
    }

    if (useConsoleLog) console.log('End showDataInCell [-]');
  }

  function generateOutputDesc(fieldData: FieldSrc, type: string) {
    if (useConsoleLog) console.log('Start generateOutputDesc [+]');
    let outputDesc = '';
    switch (type) {
      case Util.C_CELL_TYPE_TEXTINPUT:
        let fieldData_text = (fieldData as TextSrc);
        outputDesc = "[text](";
        if (fieldData_text.isNone) outputDesc += 'na';
        else if (fieldData_text.isBarcode) outputDesc += 'autoFillin';
        else if (fieldData_text.isAutoToolId) outputDesc += 'toolId';
        else if (fieldData_text.isAutoPMStartTime) outputDesc += 'actlStartTime';
        else if (fieldData_text.isJig) outputDesc += 'jig';
        outputDesc += ')';
        break;
      case Util.C_CELL_TYPE_NUMINPUT:
        let fieldData_numeric = (fieldData as NumericSrc);
        outputDesc = "[NUMERIC][" + (fieldData_numeric?.minValue || 'NAN') + ',' + (fieldData_numeric?.maxValue || 'NAN') + ']';
        if (fieldData_numeric?.isAutoFillECSV) {
          outputDesc += '(autoFillIn:';

          if (fieldData_numeric?.source === 'RPA') {
            outputDesc += 'RPA';
          }
          else {
            // console.log('fieldData_numeric?.source:', fieldData_numeric?.source);
            if (fieldData_numeric?.source === 'PMS-SV') outputDesc += 'PMS-SV';
            else if (fieldData_numeric?.source === 'TOOL-SV') outputDesc += 'Tool-SV';
            else if (fieldData_numeric?.source === 'TOOL-EC') outputDesc += 'Tool-EC';

            if (fieldData_numeric?.isECSVID) outputDesc += 'ID';
            else outputDesc += 'NAME';
          }
          outputDesc += ')';
        }
        break;
      case Util.C_CELL_TYPE_RADIOBUTTON:
        // ☐
        let fieldData_radioBtn = (fieldData as RadioButtonSrc);
        if (fieldData_radioBtn.radioButtonGroup) {
          for (let index = 0; index < fieldData_radioBtn.radioButtonGroup.length; index++) {
            outputDesc += ('☐ ' + fieldData_radioBtn.radioButtonGroup[index].desc + '\n');
          } // for
        }
        break;
      case Util.C_CELL_TYPE_IMAGE:
        let fieldData_image = (fieldData as ImageSrc);
        if (fieldData_image.filename) outputDesc = fieldData_image.filename;

        break;
    }

    if (useConsoleLog) console.log('End generateOutputDesc [-]');
    return outputDesc;
  }

  function findLastNonNullRowAndColumn(data: any[] | undefined): [number, number] {
    if (data) {
      let lastRow = data.length - 1;
      let lastColumn = data[0].length - 1;

      // Find last non-null row
      for (let i = lastRow; i >= 0; i--) {
        if (data[i].some((value: null) => value !== null)) {
          lastRow = i;
          break;
        }
      }

      // Find last non-null column
      for (let j = lastColumn; j >= 0; j--) {
        if (data.some((row) => row[j] !== null)) {
          lastColumn = j;
          break;
        }
      }

      return [lastRow, lastColumn];
    }
    else return [-1, -1];

  }
}
```

style.css
```css=
.radio-group {
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
}


.radio-group input[type="radio"] {
  margin-right: 5px;
}



.vertical-btn {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100px;
  width: 100px;
}

.radio-group>* {
  margin-right: 10px;
  /* add some space between buttons */
}

.section {
  display: -webkit-flex;
  display: flex;
}


.image-display {
  height: max-content;
  width: 20rem;
  background-color: #f1f1f1;
  padding: 10px;

}

.component {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1;
  height: 100%;
}

/* ---------------------------- */
.container {
  position: relative;
  height: 100%;
}

.divider {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  height: 100%;
  background-color: #000;
  /* cursor: col-resize; */
  z-index: 1;
}

.block {
  position: absolute;
  top: 0;
  bottom: 0;
  overflow: auto;
}

.leftside {
  left: 0;
  background-color: #ddd;
}

.rightside {
  right: 0;
  background-color: #eee;
}


/* BOX */

.box-content {
  background-color: #ffffff;
  border: 1px solid #dcdcdc;
  padding: 10px;
  margin: 10px;
  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
  
}


.box-header {
  margin: 10px;
}


.box-buttons{
  margin: 10px;
}


.linked-cell{
  display: inline-block;
  text-indent:5px;
}

.rightside-title {
  text-align: center;
  align-items: center;
}

.icon-text {
  text-align: center;
}



/* Loading Page */
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500&display=swap');

* {
  box-sizing: border-box;
}


loading_body {
  width: fit-content;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-image: radial-gradient( circle farthest-corner at 10% 20%,  rgba(0,152,155,1) 0.1%, rgba(0,94,120,1) 94.2% );
  background-size: 100%;
  font-family: 'Montserrat', sans-serif;
  overflow: hidden;
}

.loading-container { width: 100%; max-width: 520px; text-align: center; color: #fff; position: relative; margin: 0 32px; }

.loading-container:before { content: ''; position: absolute; width: 100%; height: 3px; background-color: #fff; bottom: 0; left: 0; border-radius: 10px; animation: movingLine 2.4s infinite ease-in-out; }

@keyframes movingLine {
  0% {
    opacity: 0;
    width: 0;
  }

  33.3%, 66% {
    opacity: 0.8;
    width: 100%;
  }
  
  85% {
    width: 0;
    left: initial;
    right: 0;
    opacity: 1;
  }

  100% {
   opacity: 0;
   width: 0;
  }
}

.loading-container { width:max-content; text-align: center; color: #fff; position: fixed; margin: 0 32px; }
.loading-container:before { content: ''; position: absolute; width: max-content; height: 3px; background-color: #fff; bottom: 0; left: 0; border-radius: 10px; animation: movingLine 2.4s infinite ease-in-out; }

.loading-text { font-size: 5vw; line-height: 64px; letter-spacing: 10px; margin-bottom: 32px; display: flex; justify-content: space-evenly; }
.loading-text span { animation: moveLetters 2.4s infinite ease-in-out; transform: translatex(0); position: relative; display: inline-block; opacity: 0; text-shadow: 0px 2px 10px rgba(46, 74, 81, 0.3); } 

@for $i from 1 through 7 {
  .loading-text span:nth-child(#{$i}) {
    animation-delay: $i * 0.1s;
  }
}

@keyframes moveLetters {
  0% {
    transform: translateX(-15vw);
    opacity: 0;
  }
  
  33.3%, 66% {
    transform: translateX(0);
    opacity: 1;
  }
  
  100% {
    transform: translateX(15vw);
    opacity: 0;
  }
}



.socials {
  position: fixed;
  bottom: 16px;
  right: 16px;
  display: flex;
  align-items: center;
}

.social-link {
  color: #fff;
  display: flex;
  align-items: center;
  cursor: pointer;
  text-decoration: none;
  margin-right: 12px;
}
```

App.tsx
```typescript=
import RenderEditForm from './components/FormEditor';
import FormLinker from './components/FormLinker'

import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';


export default function App() {
  localStorage.clear();
  console.log("CLEAR LocalStorage");

  return (
    <>
      <Router>
        <Switch>
          <Route path="/edit-autoNA"><FormLinker /></Route>
          <Route path="/edit-form"><RenderEditForm /></Route>
          <Route path="/">
            <Redirect to="/edit-form" />
          </Route>
        </Switch>
      </Router>
    </>
  );
}

```


```typescript=
import { FieldSrc } from "./FieldSrc";
import { Util } from "./Util";


export class LabelSrc extends FieldSrc{

    isTB?: boolean;
    isBKM?: boolean;
    isBold?: boolean;
    isPM50?: boolean;
    isLink?: boolean;

    fontWeight?: string; // no use
    desc?: string;


    // add props property
    static props: LabelSrc[] = [];

    constructor( ){
        super();

        this.isTB = false;
        this.isBKM = false;

        this.colPos = '';
        this.rowPos = '';
        this.type = Util.C_CELL_TYPE_LABEL;
        this.fontWeight = '';
        this.isPM50 = false;
        this.isLink = false;
        this.desc = '';
        this.isBold = false;

        // add this instance to the props array
        LabelSrc.props.push(this);
    }



    public setProps( data: Partial<LabelSrc> ){ // props: Partial<LabelSrc>
        let getData = LabelSrc.props.find((lastData) => Number(lastData?.rowPos) === Number(data.rowPos) &&  Number(lastData?.colPos) ===  Number(data.colPos) );


        function padTo2Digits(num: number) {
            return num.toString().padStart(2, '0');
        }

        function formatDate(date: Date) {
            return (
              [
                date.getFullYear(),
                padTo2Digits(date.getMonth() + 1),
                padTo2Digits(date.getDate()),
                padTo2Digits(date.getHours()),
                padTo2Digits(date.getMinutes()),
                padTo2Digits(date.getSeconds()),
              ].join('')
            );
          }        
        
        if ( getData ) {
            getData.type = Util.C_CELL_TYPE_LABEL;
            getData.fieldId = data.fieldId?data.fieldId:getData.fieldId;
            getData.fieldSeq = data.fieldSeq?data.fieldSeq:getData.fieldSeq;
            getData.width = data.width?data.width:getData.width;
            getData.colPos = data.colPos?data.colPos:getData.colPos;
            getData.rowPos = data.rowPos?data.rowPos:getData.rowPos;

            getData.fontWeight = data.fontWeight?data.fontWeight:getData.fontWeight;
            getData.desc = data.desc?data.desc:getData.desc;



            getData.isChanged = typeof(data.isChanged)==="undefined"?getData.isChanged:data.isChanged;
            getData.isTB = typeof(data.isTB)==="undefined"?getData.isTB:data.isTB;
            getData.isBKM = typeof(data.isBKM)==="undefined"?getData.isBKM:data.isBKM;
            getData.isPM50 = typeof(data.isPM50)==="undefined"?getData.isPM50:data.isPM50;
            getData.isLink = typeof(data.isLink)==="undefined"?getData.isLink:data.isLink;
            getData.isBold = typeof(data.isBold)==="undefined"?getData.isBold:data.isBold;

            getData.totalMergedCells = data.totalMergedCells?data.totalMergedCells:getData.totalMergedCells;


        } // if 
        else { // First time to create LabelSrc
            const now = formatDate(new Date());


            this.fieldId = data.fieldId?data.fieldId:now+data.rowPos+'-'+data.colPos;
            this.fieldSeq = data.fieldSeq?data.fieldSeq:this.fieldId;

            this.isChanged = data.isChanged;
            this.isTB = data.isTB?data.isTB:false;
            this.isBKM = data.isBKM?data.isBKM:false;
            this.colPos = data.colPos;
            this.rowPos = data.rowPos;
            this.type = Util.C_CELL_TYPE_LABEL;

            this.fontWeight = data.fontWeight?data.fontWeight:'';
            this.isPM50 = data.isPM50?data.isPM50:false;
            this.isLink = data.isLink?data.isLink:false;
            this.desc = data.desc?data.desc:'';
            this.isBold = data.isBold?data.isBold:false;


            this.totalMergedCells = data.totalMergedCells?data.totalMergedCells:1;
            this.width = data.width?data.width: "300";
        }
    }


    public getFieldDataById(fieldId: string) {
        return LabelSrc.props.find((data) => data?.fieldId === fieldId);
    }

    public getFieldDataByPos(row: Number, col: Number) {
        return LabelSrc.props.find((data) => Number(data?.rowPos) === row && Number(data?.colPos) === col);
    }

}
```

```typescript=
import { FieldSrc } from "./FieldSrc";
import { Util } from "./Util";


export class NumericSrc extends FieldSrc{

    decimal?: string;
    maxValue?: string;
    minValue?: string;

    description?: string;
    isNone?: boolean;
    isAutoFillECSV?: boolean;
    isAutoCopyByName?: boolean;

    // 自動填值 EC/SV
    source?: string;
    fillInType?: string; // 一次性/多次性
    timing?: string;
    muliply?: string;
    
    isECSVID?: boolean;
    isECSVName?: boolean;
    ECSVID?: string;
    ECSVName?: string;

    // 自動填本次/前次值
    autoFillType?: string;
    autoFillName?: string;

    // add props property
    static props: NumericSrc[] = [];

    constructor( ){
        super();

        this.colPos = '';
        this.rowPos = '';
        this.type = Util.C_CELL_TYPE_NUMINPUT;

        this.decimal = '';
        this.maxValue = '';
        this.minValue = '';

        this.description = '';
        this.isNone = false; // default
        this.isAutoFillECSV = false;
        this.isAutoCopyByName = false;

        this.source = '';
        this.fillInType = '';
        this.timing = '';
        this.muliply = '';
        this.ECSVID = '';
        this.ECSVName = '';
        this.isECSVID = true; // default
        this.isECSVName = false;
        this.autoFillType = '';
        this.autoFillName = '';

        // add this instance to the props array
        NumericSrc.props.push(this);
    }

  
    public setProps( data: Partial<NumericSrc> ){ // props: Partial<LabelSrc>
        let getData = NumericSrc.props.find((lastData) => Number(lastData?.rowPos) === Number(data.rowPos) &&  Number(lastData?.colPos) ===  Number(data.colPos) );
        
        function padTo2Digits(num: number) {
            return num.toString().padStart(2, '0');
        }

        function formatDate(date: Date) {
            return (
              [
                date.getFullYear(),
                padTo2Digits(date.getMonth() + 1),
                padTo2Digits(date.getDate()),
                padTo2Digits(date.getHours()),
                padTo2Digits(date.getMinutes()),
                padTo2Digits(date.getSeconds()),
              ].join('')
            );
        }        
        
        if ( getData ) {
            getData.type = Util.C_CELL_TYPE_NUMINPUT;
            getData.type = Util.C_CELL_TYPE_NUMINPUT;
            getData.fieldId = data.fieldId?data.fieldId:getData.fieldId;
            getData.fieldSeq = data.fieldSeq?data.fieldSeq:getData.fieldSeq;
           
            getData.width = data.width?data.width:getData.width;
            getData.colPos = data.colPos?data.colPos:getData.colPos;
            getData.rowPos = data.rowPos?data.rowPos:getData.rowPos;

            getData.isChanged = typeof(data.isChanged)==="undefined"?getData.isChanged:data.isChanged;

            getData.decimal = data.decimal?data.decimal:getData.decimal;
            getData.maxValue = data.maxValue?data.maxValue:getData.maxValue;
            getData.minValue = data.minValue?data.minValue:getData.minValue;

            getData.description = data.description?data.description:getData.description;

            getData.isNone = typeof(data.isNone)==="undefined"?getData.isNone:data.isNone;
            getData.isAutoFillECSV = typeof(data.isAutoFillECSV)==="undefined"?getData.isAutoFillECSV:data.isAutoFillECSV;
            getData.isAutoCopyByName = typeof(data.isAutoCopyByName)==="undefined"?getData.isAutoCopyByName:data.isAutoCopyByName;

            getData.source = data.source?data.source:getData.source;
            getData.fillInType = (getData.source && ( getData.source==='PMS-SV' || getData.source==='RPA'))?"once"
                                  :data.fillInType?data.fillInType:getData.fillInType;


            getData.timing = (getData.source && getData.source==='RPA')?"During PM"
            :data.timing?data.timing:getData.timing;

            getData.muliply = data.muliply?data.muliply:getData.muliply;
            getData.ECSVID = data.ECSVID?data.ECSVID:getData.ECSVID;
            getData.ECSVName = data.ECSVName?data.ECSVName:getData.ECSVName;
            getData.isECSVID = typeof(data.isECSVID)==="undefined"?getData.isECSVID:data.isECSVID;
            getData.isECSVName = typeof(data.isECSVName)==="undefined"?getData.isECSVName:data.isECSVName;
            getData.autoFillType = data.autoFillType?data.autoFillType:getData.autoFillType;
            getData.autoFillName = data.autoFillName?data.autoFillName:getData.autoFillName;


            getData.totalMergedCells = data.totalMergedCells?data.totalMergedCells:getData.totalMergedCells;

        } // if 
        else { // First time to create NumericSrc
            const now = formatDate(new Date());
            this.fieldId = data.fieldId?data.fieldId:now+data.rowPos+'-'+data.colPos;
            this.fieldSeq = data.fieldSeq?data.fieldSeq:this.fieldId;
            this.isChanged = data.isChanged;

            this.colPos = data.colPos;
            this.rowPos = data.rowPos;
            this.type = Util.C_CELL_TYPE_NUMINPUT;
            this.decimal = data.decimal?data.decimal:'';
            this.maxValue = data.maxValue?data.maxValue:'';
            this.minValue = data.minValue?data.minValue:'';

            this.description = data.description?data.description:'';
            this.isNone = data.isNone? data.isNone:(data.isAutoFillECSV||data.isAutoCopyByName)?false:true;
            this.isAutoFillECSV = data.isAutoFillECSV?data.isAutoFillECSV:false;
            this.isAutoCopyByName = data.isAutoCopyByName?data.isAutoCopyByName:false;

            this.source = data.source?data.source:'';
            this.fillInType = (this.source && ( this.source==='PMS-SV' || this.source==='RPA'))?"once"
            :data.fillInType?data.fillInType:this.fillInType;
                        this.timing = (this.source && this.source==='RPA')?"During PM"
            :data.timing?data.timing:'';

            this.muliply = data.muliply?data.muliply:'';
            this.ECSVID = data.ECSVID?data.ECSVID:'';
            this.ECSVName = data.ECSVName?data.ECSVName:'';
            this.isECSVID = data.isECSVID?data.isECSVID:(data.isECSVName)?false:true; // default
            this.isECSVName = data.isECSVName?data.isECSVName:false;

            this.totalMergedCells = data.totalMergedCells?data.totalMergedCells:1;
            this.width = data.width?data.width:"300";
        } // else 
    }

    public getFieldDataById(fieldId: string) {
        return NumericSrc.props.find((data) => data?.fieldId === fieldId);
    }

    public getFieldDataByPos(row: Number, col: Number) {
        return NumericSrc.props.find((data) => Number(data?.rowPos) === row && Number(data?.colPos) === col);
    }
 
}
```

```typescript=
import { FieldSrc } from "./FieldSrc";
import { Util } from "./Util";

interface RadioButton {
    sequence: string;
    desc: string;
}

export class RadioButtonSrc extends FieldSrc{

    layout?: string; // horizontal or vertical
    template?: string; // none, v_x, v_x_na
    radioButtonGroup?: RadioButton[];
    selectedRadioBtn?: string;

    // add props property
    static props: RadioButtonSrc[] = [];

    constructor( ){
        super();

        this.width = '';
        this.colPos = '';
        this.rowPos = '';
        this.type = Util.C_CELL_TYPE_RADIOBUTTON;

        this.layout = '';
        this.template = '';

        this.radioButtonGroup = [];
        this.selectedRadioBtn = '';
        // add this instance to the props array
        RadioButtonSrc.props.push(this);
    }

  
    public setProps( data: Partial<RadioButtonSrc> ){ // props: Partial<LabelSrc>
        let getData = RadioButtonSrc.props.find((lastData) => Number(lastData?.rowPos) === Number(data.rowPos) &&  Number(lastData?.colPos) ===  Number(data.colPos) );
        
        function padTo2Digits(num: number) {
            return num.toString().padStart(2, '0');
        }

        function formatDate(date: Date) {
            return (
              [
                date.getFullYear(),
                padTo2Digits(date.getMonth() + 1),
                padTo2Digits(date.getDate()),
                padTo2Digits(date.getHours()),
                padTo2Digits(date.getMinutes()),
                padTo2Digits(date.getSeconds()),
              ].join('')
            );
        }        
        
        if ( getData ) {
            getData.type = Util.C_CELL_TYPE_RADIOBUTTON;
            getData.fieldId = data.fieldId?data.fieldId:getData.fieldId;
            getData.fieldSeq = data.fieldSeq?data.fieldSeq:getData.fieldSeq;
            
            getData.width = data.width?data.width:getData.width;
            getData.colPos = data.colPos?data.colPos:getData.colPos;
            getData.rowPos = data.rowPos?data.rowPos:getData.rowPos;

            getData.isChanged = typeof(data.isChanged)==="undefined"?getData.isChanged:data.isChanged;

            getData.layout = data.layout?data.layout:getData.layout; 
            getData.template = data.template?data.template:getData.template; 
            getData.radioButtonGroup = data.radioButtonGroup?data.radioButtonGroup:getData.radioButtonGroup;
            getData.selectedRadioBtn = data.selectedRadioBtn?data.selectedRadioBtn:getData.selectedRadioBtn;
            getData.totalMergedCells = data.totalMergedCells?data.totalMergedCells:getData.totalMergedCells; 
        } // if 
        else { // First time to create RadioButtonSrc
            const now = formatDate(new Date());

            this.fieldId = data.fieldId?data.fieldId:now+data.rowPos+'-'+data.colPos;
            this.fieldSeq = data.fieldSeq?data.fieldSeq:this.fieldId;
            this.isChanged = data.isChanged?data.isChanged:false;

            this.colPos = data.colPos;
            this.rowPos = data.rowPos;
            this.type = Util.C_CELL_TYPE_RADIOBUTTON;

            this.layout = data.layout?data.layout:'horizontal'; // default : horizontal
            this.template = data.template?data.template:'none'; // default : none

            this.radioButtonGroup = data.radioButtonGroup;
            this.selectedRadioBtn = data.selectedRadioBtn?data.selectedRadioBtn:'';
            this.width = data.width?data.width:"300";

            this.totalMergedCells = data.totalMergedCells?data.totalMergedCells:1;            
        } // else 
    }

    public getFieldDataById(fieldId: string) {
        return RadioButtonSrc.props.find((data) => data?.fieldId === fieldId);
    }

    public getFieldDataByPos(row: Number, col: Number) {
        return RadioButtonSrc.props.find((data) => Number(data?.rowPos) === row && Number(data?.colPos) === col);
    }
      
}

```


```typescript=
import { FieldSrc } from "./FieldSrc";
import { Util } from "./Util";


export class TextSrc extends FieldSrc{

    length?: string;

    // frontend custom
    isNone?: boolean;
    isBarcode?: boolean;
    isAutoToolId?: boolean;
    isAutoPMStartTime?: boolean;
    isJig?: boolean;
    desc?: string;

    // [Barcode field]
    naRfCol?: string;
    partNum?: string;
    inChLoc?: string;


    // add props property
    static props: TextSrc[] = [];

    constructor( ){
        super();
        
        this.type = Util.C_CELL_TYPE_TEXTINPUT;
        this.length = '';

        this.isNone = false;
        this.isBarcode = false;
        this.isAutoToolId = false;
        this.isAutoPMStartTime = false;
        this.isJig = false;

        this.desc = ''; 

        // [Barcode field]
        this.naRfCol = '';
        this.partNum = '';
        this.inChLoc = '';

        // add this instance to the props array
        TextSrc.props.push(this);
    }
    
    public setProps( data: Partial<TextSrc> ){ 
        let getData = TextSrc.props.find((lastData) => Number(lastData?.rowPos) === Number(data.rowPos) &&  Number(lastData?.colPos) ===  Number(data.colPos) );
        
        function padTo2Digits(num: number) {
            return num.toString().padStart(2, '0');
        }

        function formatDate(date: Date) {
            return (
              [
                date.getFullYear(),
                padTo2Digits(date.getMonth() + 1),
                padTo2Digits(date.getDate()),
                padTo2Digits(date.getHours()),
                padTo2Digits(date.getMinutes()),
                padTo2Digits(date.getSeconds()),
              ].join('')
            );
          }        
        
        if ( getData ) {
            getData.type = Util.C_CELL_TYPE_TEXTINPUT;
            getData.fieldId = data.fieldId?data.fieldId:getData.fieldId;
            getData.fieldSeq = data.fieldSeq?data.fieldSeq:getData.fieldSeq;

            getData.width = data.width?data.width:getData.width;
            getData.colPos = data.colPos?data.colPos:getData.colPos;
            getData.rowPos = data.rowPos?data.rowPos:getData.rowPos;

            getData.isChanged = typeof(data.isChanged)==="undefined"?getData.isChanged:data.isChanged;
            getData.isNone = typeof(data.isNone)==="undefined"?getData.isNone:data.isNone;
            getData.isBarcode = typeof(data.isBarcode)==="undefined"?getData.isBarcode:data.isBarcode;
            getData.isAutoToolId = typeof(data.isAutoToolId)==="undefined"?getData.isAutoToolId:data.isAutoToolId;
            getData.isAutoPMStartTime = typeof(data.isAutoPMStartTime)==="undefined"?getData.isAutoPMStartTime:data.isAutoPMStartTime;
            getData.isJig = typeof(data.isJig)==="undefined"?getData.isJig:data.isJig;


            getData.desc = data.desc?data.desc:getData.desc;


            getData.naRfCol = data.naRfCol?data.naRfCol:getData.naRfCol;
            getData.partNum = data.partNum?data.partNum:getData.partNum;
            getData.inChLoc = data.inChLoc?data.inChLoc:getData.inChLoc;


            getData.totalMergedCells =  data.totalMergedCells?data.totalMergedCells:getData.totalMergedCells;
            getData.length = data.length?data.length:getData.length;
        } // if 
        else { // First time to create TextSrc
            const now = formatDate(new Date());

            this.fieldId = data.fieldId?data.fieldId:now+data.rowPos+'-'+data.colPos;
            this.fieldSeq = data.fieldSeq?data.fieldSeq:this.fieldId;
            this.isChanged = data.isChanged?data.isChanged:false;
            
            this.colPos = data.colPos;
            this.rowPos = data.rowPos;
            this.type = Util.C_CELL_TYPE_TEXTINPUT;


            // [default value]
            this.isNone = data.isNone? data.isNone: (data.isBarcode||data.isAutoToolId||data.isAutoPMStartTime||data.isJig)?false: true;
            this.isBarcode = data.isBarcode? data.isBarcode: false;
            this.isAutoToolId = data.isAutoToolId? data.isAutoToolId: false;
            this.isAutoPMStartTime = data.isAutoPMStartTime? data.isAutoPMStartTime: false;
            this.isJig = data.isJig? data.isJig: false;
            this.desc = data.desc?data.desc:'[text](na)'; // default value


            // [Barcode field]
            this.naRfCol = data.naRfCol?data.naRfCol:'';
            this.partNum = data.partNum?data.partNum:'';
            this.inChLoc = data.inChLoc?data.inChLoc:'';

            this.totalMergedCells = data.totalMergedCells?data.totalMergedCells:1;
            this.width = data.width?data.width:"300";
            this.length = data.length?data.length:'';
        } // else 
    }

    public getFieldDataById(fieldId: string) {
        return TextSrc.props.find((data) => data?.fieldId === fieldId);
    }

    public getFieldDataByPos(row: Number, col: Number) {
        return TextSrc.props.find((data) => Number(data?.rowPos) === row && Number(data?.colPos) === col);
    }
}
```