catchup-library-web 1.20.32 → 1.20.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4903,21 +4903,29 @@ var FillInTheBlanksActivityMaterialContent = ({
4903
4903
  const [selectedOption, setSelectedOption] = (0, import_react18.useState)(null);
4904
4904
  const [draggedOption, setDraggedOption] = (0, import_react18.useState)(null);
4905
4905
  const [dropTargetIndex, setDropTargetIndex] = (0, import_react18.useState)(null);
4906
+ const [draggedElement, setDraggedElement] = (0, import_react18.useState)(
4907
+ null
4908
+ );
4909
+ const dragElementRef = (0, import_react18.useRef)(null);
4910
+ const [touchPosition, setTouchPosition] = (0, import_react18.useState)({
4911
+ x: 0,
4912
+ y: 0
4913
+ });
4906
4914
  (0, import_react18.useEffect)(() => {
4907
4915
  setShuffleOptionList(shuffleArray(optionList));
4908
- }, []);
4916
+ }, [optionList]);
4909
4917
  (0, import_react18.useEffect)(() => {
4910
4918
  if (!showCorrectAnswer) return;
4911
4919
  const foundAnswer = answer.data.find(
4912
4920
  (answerData) => answerData.type === "FILL_IN_THE_BLANKS"
4913
4921
  );
4914
- if (foundAnswer.answerMap.length === 0) return;
4922
+ if (!foundAnswer || foundAnswer.answerMap.length === 0) return;
4915
4923
  if (Object.keys(materialMap).length === 0) return;
4916
4924
  foundAnswer.answerMap = Object.keys(materialMap).map(
4917
4925
  (materialMapKey) => JSON.parse(materialMap[materialMapKey])[0]
4918
4926
  );
4919
4927
  onChange(answer, 0, JSON.parse(materialMap[0])[0]);
4920
- }, [showCorrectAnswer]);
4928
+ }, [showCorrectAnswer, answer, materialMap, onChange]);
4921
4929
  const retrieveAnswerMap = () => {
4922
4930
  const foundIndex = answer.data.findIndex(
4923
4931
  (answerData) => answerData.type === "FILL_IN_THE_BLANKS"
@@ -4937,36 +4945,89 @@ var FillInTheBlanksActivityMaterialContent = ({
4937
4945
  const checkAnswerProvided = (answerMap2, option) => {
4938
4946
  return Object.keys(answerMap2).findIndex((key) => answerMap2[key] === option) !== -1;
4939
4947
  };
4940
- const handleSelectOption = (option) => {
4941
- setSelectedOption(option);
4948
+ const handleMouseDown = (e, option) => {
4949
+ e.preventDefault();
4950
+ setDraggedOption(option);
4951
+ setSelectedOption(null);
4942
4952
  };
4943
- const handleDragStart = (option) => {
4953
+ const handleMouseUp = () => {
4954
+ if (dropTargetIndex !== null && draggedOption !== null) {
4955
+ onChange(answer, dropTargetIndex, draggedOption);
4956
+ }
4957
+ setDraggedOption(null);
4958
+ setDropTargetIndex(null);
4959
+ };
4960
+ const handleTouchStart = (e, option, element) => {
4961
+ const touch = e.touches[0];
4944
4962
  setDraggedOption(option);
4963
+ setDraggedElement(element);
4964
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
4965
+ setSelectedOption(null);
4966
+ };
4967
+ const handleTouchMove = (e) => {
4968
+ if (!draggedOption) return;
4969
+ const touch = e.touches[0];
4970
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
4971
+ const elementUnder = document.elementFromPoint(
4972
+ touch.clientX,
4973
+ touch.clientY
4974
+ );
4975
+ const dropZone = elementUnder == null ? void 0 : elementUnder.closest("[data-drop-zone]");
4976
+ if (dropZone) {
4977
+ const dropIndex = dropZone.getAttribute("data-drop-zone");
4978
+ setDropTargetIndex(dropIndex);
4979
+ } else {
4980
+ setDropTargetIndex(null);
4981
+ }
4945
4982
  };
4946
- const handleDragEnd = () => {
4983
+ const handleTouchEnd = () => {
4947
4984
  if (dropTargetIndex !== null && draggedOption !== null) {
4948
4985
  onChange(answer, dropTargetIndex, draggedOption);
4949
4986
  }
4950
4987
  setDraggedOption(null);
4951
4988
  setDropTargetIndex(null);
4989
+ setDraggedElement(null);
4952
4990
  };
4953
- const handleDropZoneEnter = (index) => {
4954
- setDropTargetIndex(index);
4991
+ const handleSelectOption = (option) => {
4992
+ setSelectedOption(option);
4993
+ setDraggedOption(null);
4955
4994
  };
4956
- const handleDropZoneDrop = (index) => {
4995
+ const handleDropZoneClick = (index) => {
4957
4996
  if (selectedOption !== null) {
4958
4997
  onChange(answer, index, selectedOption);
4959
4998
  setSelectedOption(null);
4960
- } else if (draggedOption !== null) {
4961
- onChange(answer, index, draggedOption);
4962
- setDraggedOption(null);
4963
4999
  }
4964
- setDropTargetIndex(null);
4965
5000
  };
4966
5001
  const answerMap = retrieveAnswerMap();
4967
5002
  return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-row flex-wrap items-center", children: [
4968
5003
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "hidden md:block", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "font-semibold text-xl opacity-60", children: i18n_default.t("please_select_fill_in_the_blanks_text") }) }),
4969
5004
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "hidden md:contents", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(DividerLine_default, {}) }),
5005
+ draggedOption && touchPosition.x > 0 && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5006
+ "div",
5007
+ {
5008
+ className: "fixed pointer-events-none z-50 opacity-80",
5009
+ style: {
5010
+ left: `${touchPosition.x}px`,
5011
+ top: `${touchPosition.y}px`,
5012
+ transform: "translate(-50%, -50%)"
5013
+ },
5014
+ children: contentMap.type === "TEXT" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "border-catchup-blue border-2 px-2 rounded-catchup-xlarge bg-white shadow-lg", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "italic whitespace-pre-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5015
+ InputWithSpecialExpression_default,
5016
+ {
5017
+ value: draggedOption,
5018
+ showSpecialOnly: false
5019
+ }
5020
+ ) }) }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge bg-white shadow-lg", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5021
+ ShowMaterialMediaByContentType_default,
5022
+ {
5023
+ contentType: contentMap.type,
5024
+ src: draggedOption,
5025
+ canFullScreen: false
5026
+ },
5027
+ uniqueValue
5028
+ ) })
5029
+ }
5030
+ ),
4970
5031
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "w-full flex flex-row flex-wrap gap-x-2 gap-y-2 my-2", children: shuffleOptionList.map(
4971
5032
  (option, index) => checkAnswerProvided(answerMap, option) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "opacity-30", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
4972
5033
  ShowMaterialMediaByContentType_default,
@@ -4979,16 +5040,17 @@ var FillInTheBlanksActivityMaterialContent = ({
4979
5040
  ) }, index) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
4980
5041
  "div",
4981
5042
  {
4982
- draggable: true,
4983
- onDragStart: () => handleDragStart(option),
4984
- onDragEnd: handleDragEnd,
4985
- className: `${draggedOption === option ? "opacity-40" : "opacity-100"} transition-opacity duration-200`,
5043
+ ref: draggedOption === option ? dragElementRef : null,
5044
+ className: `${draggedOption === option ? "opacity-40" : selectedOption === option ? "ring-2 ring-blue-500" : "opacity-100"} transition-all duration-200`,
5045
+ onMouseDown: (e) => handleMouseDown(e, option),
5046
+ onTouchStart: (e) => handleTouchStart(e, option, e.currentTarget),
5047
+ onTouchMove: handleTouchMove,
5048
+ onTouchEnd: handleTouchEnd,
4986
5049
  children: contentMap.type === "TEXT" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
4987
5050
  "div",
4988
5051
  {
4989
- className: "border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none touch-none",
5052
+ className: "border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none",
4990
5053
  onClick: () => handleSelectOption(option),
4991
- onTouchEnd: () => handleSelectOption(option),
4992
5054
  children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "italic whitespace-pre-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
4993
5055
  InputWithSpecialExpression_default,
4994
5056
  {
@@ -5000,9 +5062,8 @@ var FillInTheBlanksActivityMaterialContent = ({
5000
5062
  ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5001
5063
  "div",
5002
5064
  {
5003
- className: "border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none touch-none",
5065
+ className: "border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none",
5004
5066
  onClick: () => handleSelectOption(option),
5005
- onTouchEnd: () => handleSelectOption(option),
5006
5067
  children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5007
5068
  ShowMaterialMediaByContentType_default,
5008
5069
  {
@@ -5018,7 +5079,7 @@ var FillInTheBlanksActivityMaterialContent = ({
5018
5079
  index
5019
5080
  )
5020
5081
  ) }),
5021
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "w-full flex flex-row flex-wrap", children: Object.keys(answerMap).map((materialKey, index) => {
5082
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "w-full flex flex-row flex-wrap", onMouseUp: handleMouseUp, children: Object.keys(answerMap).map((materialKey, index) => {
5022
5083
  const learnerAnswerState = checkAnswerState(
5023
5084
  JSON.parse(materialMap[materialKey]),
5024
5085
  answerMap[materialKey]
@@ -5026,21 +5087,11 @@ var FillInTheBlanksActivityMaterialContent = ({
5026
5087
  return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "w-full md:w-1/2", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "mx-2", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5027
5088
  "div",
5028
5089
  {
5029
- onDragOver: (e) => {
5030
- e.preventDefault();
5031
- handleDropZoneEnter(materialKey);
5032
- },
5033
- onDragLeave: () => setDropTargetIndex(null),
5034
- onDrop: (e) => {
5035
- e.preventDefault();
5036
- handleDropZoneDrop(materialKey);
5037
- },
5038
- onClick: () => {
5039
- if (selectedOption !== null) {
5040
- handleDropZoneDrop(materialKey);
5041
- }
5042
- },
5043
- className: `${dropTargetIndex === materialKey ? "ring-2 ring-blue-400" : ""}`,
5090
+ "data-drop-zone": materialKey,
5091
+ onMouseEnter: () => draggedOption && setDropTargetIndex(materialKey),
5092
+ onMouseLeave: () => setDropTargetIndex(null),
5093
+ onClick: () => handleDropZoneClick(materialKey),
5094
+ className: `${dropTargetIndex === materialKey ? "ring-2 ring-blue-400 bg-blue-50" : ""} transition-all duration-200 rounded-lg`,
5044
5095
  children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "w-full flex flex-row my-2 gap-x-2", children: [
5045
5096
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "my-auto", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: "text-xl", children: [
5046
5097
  parseFloat(materialKey) + 1,
@@ -5050,9 +5101,10 @@ var FillInTheBlanksActivityMaterialContent = ({
5050
5101
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5051
5102
  "div",
5052
5103
  {
5053
- className: `w-full min-h-[44px] border rounded-lg ${answerMap[materialKey] ? "border-catchup-blue-400 px-2" : "bg-catchup-gray-50 border-catchup-gray-200 border-dashed py-2 px-4"}`,
5054
- onClick: () => {
5104
+ className: `w-full min-h-[44px] border rounded-lg ${answerMap[materialKey] ? "border-catchup-blue-400 px-2 cursor-pointer" : "bg-catchup-gray-50 border-catchup-gray-200 border-dashed py-2 px-4"}`,
5105
+ onClick: (e) => {
5055
5106
  if (answerMap[materialKey]) {
5107
+ e.stopPropagation();
5056
5108
  onChange(answer, materialKey, "");
5057
5109
  }
5058
5110
  },
@@ -5062,7 +5114,7 @@ var FillInTheBlanksActivityMaterialContent = ({
5062
5114
  value: answerMap[materialKey],
5063
5115
  showSpecialOnly: false
5064
5116
  }
5065
- ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "text-gray-400 italic" })
5117
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "text-gray-400 italic", children: i18n_default.t("please_drop_here") })
5066
5118
  }
5067
5119
  ) }),
5068
5120
  learnerAnswerState === "CORRECT" ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "absolute -top-[10px] right-4 bg-catchup-white", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
@@ -5090,7 +5142,8 @@ var FillInTheBlanksActivityMaterialContent = ({
5090
5142
  "div",
5091
5143
  {
5092
5144
  className: "flex-1 cursor-pointer",
5093
- onClick: () => {
5145
+ onClick: (e) => {
5146
+ e.stopPropagation();
5094
5147
  onChange(answer, materialKey, "");
5095
5148
  },
5096
5149
  children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
package/dist/index.mjs CHANGED
@@ -4644,7 +4644,7 @@ var DropdownActivityContent_default = DropdownActivityContent;
4644
4644
 
4645
4645
  // src/components/activities/material-contents/FillInTheBlanksActivityMaterialContent.tsx
4646
4646
  import { InlineMath as InlineMath4 } from "react-katex";
4647
- import { useState as useState17, useEffect as useEffect8 } from "react";
4647
+ import { useState as useState17, useEffect as useEffect8, useRef as useRef4 } from "react";
4648
4648
 
4649
4649
  // src/components/texts/InputWithSpecialExpression.tsx
4650
4650
  import { InlineMath as InlineMath3 } from "react-katex";
@@ -4687,21 +4687,29 @@ var FillInTheBlanksActivityMaterialContent = ({
4687
4687
  const [selectedOption, setSelectedOption] = useState17(null);
4688
4688
  const [draggedOption, setDraggedOption] = useState17(null);
4689
4689
  const [dropTargetIndex, setDropTargetIndex] = useState17(null);
4690
+ const [draggedElement, setDraggedElement] = useState17(
4691
+ null
4692
+ );
4693
+ const dragElementRef = useRef4(null);
4694
+ const [touchPosition, setTouchPosition] = useState17({
4695
+ x: 0,
4696
+ y: 0
4697
+ });
4690
4698
  useEffect8(() => {
4691
4699
  setShuffleOptionList(shuffleArray(optionList));
4692
- }, []);
4700
+ }, [optionList]);
4693
4701
  useEffect8(() => {
4694
4702
  if (!showCorrectAnswer) return;
4695
4703
  const foundAnswer = answer.data.find(
4696
4704
  (answerData) => answerData.type === "FILL_IN_THE_BLANKS"
4697
4705
  );
4698
- if (foundAnswer.answerMap.length === 0) return;
4706
+ if (!foundAnswer || foundAnswer.answerMap.length === 0) return;
4699
4707
  if (Object.keys(materialMap).length === 0) return;
4700
4708
  foundAnswer.answerMap = Object.keys(materialMap).map(
4701
4709
  (materialMapKey) => JSON.parse(materialMap[materialMapKey])[0]
4702
4710
  );
4703
4711
  onChange(answer, 0, JSON.parse(materialMap[0])[0]);
4704
- }, [showCorrectAnswer]);
4712
+ }, [showCorrectAnswer, answer, materialMap, onChange]);
4705
4713
  const retrieveAnswerMap = () => {
4706
4714
  const foundIndex = answer.data.findIndex(
4707
4715
  (answerData) => answerData.type === "FILL_IN_THE_BLANKS"
@@ -4721,36 +4729,89 @@ var FillInTheBlanksActivityMaterialContent = ({
4721
4729
  const checkAnswerProvided = (answerMap2, option) => {
4722
4730
  return Object.keys(answerMap2).findIndex((key) => answerMap2[key] === option) !== -1;
4723
4731
  };
4724
- const handleSelectOption = (option) => {
4725
- setSelectedOption(option);
4732
+ const handleMouseDown = (e, option) => {
4733
+ e.preventDefault();
4734
+ setDraggedOption(option);
4735
+ setSelectedOption(null);
4736
+ };
4737
+ const handleMouseUp = () => {
4738
+ if (dropTargetIndex !== null && draggedOption !== null) {
4739
+ onChange(answer, dropTargetIndex, draggedOption);
4740
+ }
4741
+ setDraggedOption(null);
4742
+ setDropTargetIndex(null);
4726
4743
  };
4727
- const handleDragStart = (option) => {
4744
+ const handleTouchStart = (e, option, element) => {
4745
+ const touch = e.touches[0];
4728
4746
  setDraggedOption(option);
4747
+ setDraggedElement(element);
4748
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
4749
+ setSelectedOption(null);
4729
4750
  };
4730
- const handleDragEnd = () => {
4751
+ const handleTouchMove = (e) => {
4752
+ if (!draggedOption) return;
4753
+ const touch = e.touches[0];
4754
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
4755
+ const elementUnder = document.elementFromPoint(
4756
+ touch.clientX,
4757
+ touch.clientY
4758
+ );
4759
+ const dropZone = elementUnder == null ? void 0 : elementUnder.closest("[data-drop-zone]");
4760
+ if (dropZone) {
4761
+ const dropIndex = dropZone.getAttribute("data-drop-zone");
4762
+ setDropTargetIndex(dropIndex);
4763
+ } else {
4764
+ setDropTargetIndex(null);
4765
+ }
4766
+ };
4767
+ const handleTouchEnd = () => {
4731
4768
  if (dropTargetIndex !== null && draggedOption !== null) {
4732
4769
  onChange(answer, dropTargetIndex, draggedOption);
4733
4770
  }
4734
4771
  setDraggedOption(null);
4735
4772
  setDropTargetIndex(null);
4773
+ setDraggedElement(null);
4736
4774
  };
4737
- const handleDropZoneEnter = (index) => {
4738
- setDropTargetIndex(index);
4775
+ const handleSelectOption = (option) => {
4776
+ setSelectedOption(option);
4777
+ setDraggedOption(null);
4739
4778
  };
4740
- const handleDropZoneDrop = (index) => {
4779
+ const handleDropZoneClick = (index) => {
4741
4780
  if (selectedOption !== null) {
4742
4781
  onChange(answer, index, selectedOption);
4743
4782
  setSelectedOption(null);
4744
- } else if (draggedOption !== null) {
4745
- onChange(answer, index, draggedOption);
4746
- setDraggedOption(null);
4747
4783
  }
4748
- setDropTargetIndex(null);
4749
4784
  };
4750
4785
  const answerMap = retrieveAnswerMap();
4751
4786
  return /* @__PURE__ */ jsxs17("div", { className: "flex flex-row flex-wrap items-center", children: [
4752
4787
  /* @__PURE__ */ jsx28("div", { className: "hidden md:block", children: /* @__PURE__ */ jsx28("span", { className: "font-semibold text-xl opacity-60", children: i18n_default.t("please_select_fill_in_the_blanks_text") }) }),
4753
4788
  /* @__PURE__ */ jsx28("div", { className: "hidden md:contents", children: /* @__PURE__ */ jsx28(DividerLine_default, {}) }),
4789
+ draggedOption && touchPosition.x > 0 && /* @__PURE__ */ jsx28(
4790
+ "div",
4791
+ {
4792
+ className: "fixed pointer-events-none z-50 opacity-80",
4793
+ style: {
4794
+ left: `${touchPosition.x}px`,
4795
+ top: `${touchPosition.y}px`,
4796
+ transform: "translate(-50%, -50%)"
4797
+ },
4798
+ children: contentMap.type === "TEXT" ? /* @__PURE__ */ jsx28("div", { className: "border-catchup-blue border-2 px-2 rounded-catchup-xlarge bg-white shadow-lg", children: /* @__PURE__ */ jsx28("p", { className: "italic whitespace-pre-wrap", children: /* @__PURE__ */ jsx28(
4799
+ InputWithSpecialExpression_default,
4800
+ {
4801
+ value: draggedOption,
4802
+ showSpecialOnly: false
4803
+ }
4804
+ ) }) }) : /* @__PURE__ */ jsx28("div", { className: "border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge bg-white shadow-lg", children: /* @__PURE__ */ jsx28(
4805
+ ShowMaterialMediaByContentType_default,
4806
+ {
4807
+ contentType: contentMap.type,
4808
+ src: draggedOption,
4809
+ canFullScreen: false
4810
+ },
4811
+ uniqueValue
4812
+ ) })
4813
+ }
4814
+ ),
4754
4815
  /* @__PURE__ */ jsx28("div", { className: "w-full flex flex-row flex-wrap gap-x-2 gap-y-2 my-2", children: shuffleOptionList.map(
4755
4816
  (option, index) => checkAnswerProvided(answerMap, option) ? /* @__PURE__ */ jsx28("div", { className: "opacity-30", children: /* @__PURE__ */ jsx28(
4756
4817
  ShowMaterialMediaByContentType_default,
@@ -4763,16 +4824,17 @@ var FillInTheBlanksActivityMaterialContent = ({
4763
4824
  ) }, index) : /* @__PURE__ */ jsx28(
4764
4825
  "div",
4765
4826
  {
4766
- draggable: true,
4767
- onDragStart: () => handleDragStart(option),
4768
- onDragEnd: handleDragEnd,
4769
- className: `${draggedOption === option ? "opacity-40" : "opacity-100"} transition-opacity duration-200`,
4827
+ ref: draggedOption === option ? dragElementRef : null,
4828
+ className: `${draggedOption === option ? "opacity-40" : selectedOption === option ? "ring-2 ring-blue-500" : "opacity-100"} transition-all duration-200`,
4829
+ onMouseDown: (e) => handleMouseDown(e, option),
4830
+ onTouchStart: (e) => handleTouchStart(e, option, e.currentTarget),
4831
+ onTouchMove: handleTouchMove,
4832
+ onTouchEnd: handleTouchEnd,
4770
4833
  children: contentMap.type === "TEXT" ? /* @__PURE__ */ jsx28(
4771
4834
  "div",
4772
4835
  {
4773
- className: "border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none touch-none",
4836
+ className: "border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none",
4774
4837
  onClick: () => handleSelectOption(option),
4775
- onTouchEnd: () => handleSelectOption(option),
4776
4838
  children: /* @__PURE__ */ jsx28("p", { className: "italic whitespace-pre-wrap", children: /* @__PURE__ */ jsx28(
4777
4839
  InputWithSpecialExpression_default,
4778
4840
  {
@@ -4784,9 +4846,8 @@ var FillInTheBlanksActivityMaterialContent = ({
4784
4846
  ) : /* @__PURE__ */ jsx28(
4785
4847
  "div",
4786
4848
  {
4787
- className: "border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none touch-none",
4849
+ className: "border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none",
4788
4850
  onClick: () => handleSelectOption(option),
4789
- onTouchEnd: () => handleSelectOption(option),
4790
4851
  children: /* @__PURE__ */ jsx28(
4791
4852
  ShowMaterialMediaByContentType_default,
4792
4853
  {
@@ -4802,7 +4863,7 @@ var FillInTheBlanksActivityMaterialContent = ({
4802
4863
  index
4803
4864
  )
4804
4865
  ) }),
4805
- /* @__PURE__ */ jsx28("div", { className: "w-full flex flex-row flex-wrap", children: Object.keys(answerMap).map((materialKey, index) => {
4866
+ /* @__PURE__ */ jsx28("div", { className: "w-full flex flex-row flex-wrap", onMouseUp: handleMouseUp, children: Object.keys(answerMap).map((materialKey, index) => {
4806
4867
  const learnerAnswerState = checkAnswerState(
4807
4868
  JSON.parse(materialMap[materialKey]),
4808
4869
  answerMap[materialKey]
@@ -4810,21 +4871,11 @@ var FillInTheBlanksActivityMaterialContent = ({
4810
4871
  return /* @__PURE__ */ jsx28("div", { className: "w-full md:w-1/2", children: /* @__PURE__ */ jsx28("div", { className: "mx-2", children: /* @__PURE__ */ jsx28(
4811
4872
  "div",
4812
4873
  {
4813
- onDragOver: (e) => {
4814
- e.preventDefault();
4815
- handleDropZoneEnter(materialKey);
4816
- },
4817
- onDragLeave: () => setDropTargetIndex(null),
4818
- onDrop: (e) => {
4819
- e.preventDefault();
4820
- handleDropZoneDrop(materialKey);
4821
- },
4822
- onClick: () => {
4823
- if (selectedOption !== null) {
4824
- handleDropZoneDrop(materialKey);
4825
- }
4826
- },
4827
- className: `${dropTargetIndex === materialKey ? "ring-2 ring-blue-400" : ""}`,
4874
+ "data-drop-zone": materialKey,
4875
+ onMouseEnter: () => draggedOption && setDropTargetIndex(materialKey),
4876
+ onMouseLeave: () => setDropTargetIndex(null),
4877
+ onClick: () => handleDropZoneClick(materialKey),
4878
+ className: `${dropTargetIndex === materialKey ? "ring-2 ring-blue-400 bg-blue-50" : ""} transition-all duration-200 rounded-lg`,
4828
4879
  children: /* @__PURE__ */ jsxs17("div", { className: "w-full flex flex-row my-2 gap-x-2", children: [
4829
4880
  /* @__PURE__ */ jsx28("div", { className: "my-auto", children: /* @__PURE__ */ jsxs17("p", { className: "text-xl", children: [
4830
4881
  parseFloat(materialKey) + 1,
@@ -4834,9 +4885,10 @@ var FillInTheBlanksActivityMaterialContent = ({
4834
4885
  /* @__PURE__ */ jsx28("div", { className: "flex-1", children: /* @__PURE__ */ jsx28(
4835
4886
  "div",
4836
4887
  {
4837
- className: `w-full min-h-[44px] border rounded-lg ${answerMap[materialKey] ? "border-catchup-blue-400 px-2" : "bg-catchup-gray-50 border-catchup-gray-200 border-dashed py-2 px-4"}`,
4838
- onClick: () => {
4888
+ className: `w-full min-h-[44px] border rounded-lg ${answerMap[materialKey] ? "border-catchup-blue-400 px-2 cursor-pointer" : "bg-catchup-gray-50 border-catchup-gray-200 border-dashed py-2 px-4"}`,
4889
+ onClick: (e) => {
4839
4890
  if (answerMap[materialKey]) {
4891
+ e.stopPropagation();
4840
4892
  onChange(answer, materialKey, "");
4841
4893
  }
4842
4894
  },
@@ -4846,7 +4898,7 @@ var FillInTheBlanksActivityMaterialContent = ({
4846
4898
  value: answerMap[materialKey],
4847
4899
  showSpecialOnly: false
4848
4900
  }
4849
- ) : /* @__PURE__ */ jsx28("p", { className: "text-gray-400 italic" })
4901
+ ) : /* @__PURE__ */ jsx28("p", { className: "text-gray-400 italic", children: i18n_default.t("please_drop_here") })
4850
4902
  }
4851
4903
  ) }),
4852
4904
  learnerAnswerState === "CORRECT" ? /* @__PURE__ */ jsx28("div", { className: "absolute -top-[10px] right-4 bg-catchup-white", children: /* @__PURE__ */ jsx28(
@@ -4874,7 +4926,8 @@ var FillInTheBlanksActivityMaterialContent = ({
4874
4926
  "div",
4875
4927
  {
4876
4928
  className: "flex-1 cursor-pointer",
4877
- onClick: () => {
4929
+ onClick: (e) => {
4930
+ e.stopPropagation();
4878
4931
  onChange(answer, materialKey, "");
4879
4932
  },
4880
4933
  children: /* @__PURE__ */ jsx28(
@@ -5006,7 +5059,7 @@ var FillInTheBlanksActivityContent = ({
5006
5059
  var FillInTheBlanksActivityContent_default = FillInTheBlanksActivityContent;
5007
5060
 
5008
5061
  // src/components/activities/material-contents/GroupingActivityMaterialContent.tsx
5009
- import { useEffect as useEffect10, useRef as useRef5, useState as useState19 } from "react";
5062
+ import { useEffect as useEffect10, useRef as useRef6, useState as useState19 } from "react";
5010
5063
  import { useDrop as useDrop2 } from "react-dnd";
5011
5064
  import { InlineMath as InlineMath5 } from "react-katex";
5012
5065
 
@@ -5044,7 +5097,7 @@ var DraggableItem = ({
5044
5097
  var DraggableItem_default = DraggableItem;
5045
5098
 
5046
5099
  // src/components/dnds/DroppableItem.tsx
5047
- import { useRef as useRef4 } from "react";
5100
+ import { useRef as useRef5 } from "react";
5048
5101
  import { useDrop } from "react-dnd";
5049
5102
  import { jsx as jsx31 } from "react/jsx-runtime";
5050
5103
  var DroppableItem = ({
@@ -5055,7 +5108,7 @@ var DroppableItem = ({
5055
5108
  target,
5056
5109
  setTarget
5057
5110
  }) => {
5058
- const ref = useRef4(null);
5111
+ const ref = useRef5(null);
5059
5112
  const [, drop] = useDrop({
5060
5113
  accept: type,
5061
5114
  hover() {
@@ -5094,7 +5147,7 @@ var GroupingActivityMaterialContent = ({
5094
5147
  canDrop: monitor.canDrop()
5095
5148
  })
5096
5149
  });
5097
- const ref = useRef5(null);
5150
+ const ref = useRef6(null);
5098
5151
  useEffect10(() => {
5099
5152
  const shuffleArray2 = (array) => {
5100
5153
  if (!isShuffled) {
@@ -5374,7 +5427,7 @@ var GroupingActivityContent = ({
5374
5427
  var GroupingActivityContent_default = GroupingActivityContent;
5375
5428
 
5376
5429
  // src/components/activities/material-contents/MatchingActivityMaterialContent.tsx
5377
- import { useEffect as useEffect11, useRef as useRef6, useState as useState20 } from "react";
5430
+ import { useEffect as useEffect11, useRef as useRef7, useState as useState20 } from "react";
5378
5431
  import { useDrop as useDrop3 } from "react-dnd";
5379
5432
  import { InlineMath as InlineMath6 } from "react-katex";
5380
5433
  import { Fragment as Fragment5, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
@@ -5401,7 +5454,7 @@ var MatchingActivityMaterialContent = ({
5401
5454
  canDrop: monitor.canDrop()
5402
5455
  })
5403
5456
  });
5404
- const itemsRef = useRef6(null);
5457
+ const itemsRef = useRef7(null);
5405
5458
  useEffect11(() => {
5406
5459
  const shuffleArray2 = (array) => {
5407
5460
  if (!isShuffled) {
@@ -6196,7 +6249,7 @@ var useScreenSize = () => {
6196
6249
  var useScreenSize_default = useScreenSize;
6197
6250
 
6198
6251
  // src/components/dnds/DraggableDroppableItem.tsx
6199
- import { useRef as useRef7 } from "react";
6252
+ import { useRef as useRef8 } from "react";
6200
6253
  import { useDrag as useDrag2, useDrop as useDrop4 } from "react-dnd";
6201
6254
  import { jsx as jsx42 } from "react/jsx-runtime";
6202
6255
  var DraggableDroppableItem = ({
@@ -6208,7 +6261,7 @@ var DraggableDroppableItem = ({
6208
6261
  target,
6209
6262
  setTarget
6210
6263
  }) => {
6211
- const ref = useRef7(null);
6264
+ const ref = useRef8(null);
6212
6265
  const [, drop] = useDrop4({
6213
6266
  accept: type,
6214
6267
  hover() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catchup-library-web",
3
- "version": "1.20.32",
3
+ "version": "1.20.33",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,5 +1,5 @@
1
1
  import { InlineMath } from "react-katex";
2
- import { useState, useEffect } from "react";
2
+ import { useState, useEffect, useRef } from "react";
3
3
  import BaseImage from "../../images/BaseImage";
4
4
  import { shuffleArray } from "../../../utilization/AppUtilization";
5
5
  import ShowMaterialMediaByContentType from "./ShowMaterialMediaByContentType";
@@ -20,37 +20,48 @@ const FillInTheBlanksActivityMaterialContent = ({
20
20
  isPreview,
21
21
  showCorrectAnswer,
22
22
  }: IFillInTheBlanksActivityMaterialProps) => {
23
- const [shuffleOptionList, setShuffleOptionList] = useState([]);
24
- const [selectedOption, setSelectedOption] = useState(null);
25
- const [draggedOption, setDraggedOption] = useState(null);
26
- const [dropTargetIndex, setDropTargetIndex] = useState(null);
23
+ const [shuffleOptionList, setShuffleOptionList] = useState<any[]>([]);
24
+ const [selectedOption, setSelectedOption] = useState<any>(null);
25
+ const [draggedOption, setDraggedOption] = useState<any>(null);
26
+ const [dropTargetIndex, setDropTargetIndex] = useState<string | null>(null);
27
+ const [draggedElement, setDraggedElement] = useState<HTMLElement | null>(
28
+ null
29
+ );
30
+ const dragElementRef = useRef<HTMLDivElement>(null);
31
+ const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>({
32
+ x: 0,
33
+ y: 0,
34
+ });
27
35
 
28
36
  useEffect(() => {
29
37
  setShuffleOptionList(shuffleArray(optionList));
30
- }, []);
38
+ }, [optionList]);
31
39
 
32
40
  useEffect(() => {
33
41
  if (!showCorrectAnswer) return;
34
42
  const foundAnswer = answer.data.find(
35
43
  (answerData: any) => answerData.type === "FILL_IN_THE_BLANKS"
36
44
  );
37
- if (foundAnswer.answerMap.length === 0) return;
45
+ if (!foundAnswer || foundAnswer.answerMap.length === 0) return;
38
46
  if (Object.keys(materialMap).length === 0) return;
39
47
  foundAnswer.answerMap = Object.keys(materialMap).map(
40
48
  (materialMapKey) => JSON.parse(materialMap[materialMapKey])[0]
41
49
  );
42
50
 
43
51
  onChange(answer, 0, JSON.parse(materialMap[0])[0]);
44
- }, [showCorrectAnswer]);
52
+ }, [showCorrectAnswer, answer, materialMap, onChange]);
45
53
 
46
- const retrieveAnswerMap = () => {
54
+ const retrieveAnswerMap = (): Record<string, any> => {
47
55
  const foundIndex = answer.data.findIndex(
48
56
  (answerData: any) => answerData.type === "FILL_IN_THE_BLANKS"
49
57
  );
50
58
  return answer.data[foundIndex].answerMap;
51
59
  };
52
60
 
53
- const checkAnswerState = (correctAnswerList: any, learnerAnswer: string) => {
61
+ const checkAnswerState = (
62
+ correctAnswerList: any[],
63
+ learnerAnswer: string
64
+ ): string | null => {
54
65
  if (!isPreview) return null;
55
66
  const foundIndex = correctAnswerList.findIndex(
56
67
  (correctAnswer: string) => correctAnswer === learnerAnswer
@@ -61,42 +72,85 @@ const FillInTheBlanksActivityMaterialContent = ({
61
72
  return "INCORRECT";
62
73
  };
63
74
 
64
- const checkAnswerProvided = (answerMap: any, option: string) => {
75
+ const checkAnswerProvided = (
76
+ answerMap: Record<string, any>,
77
+ option: string
78
+ ): boolean => {
65
79
  return (
66
80
  Object.keys(answerMap).findIndex((key) => answerMap[key] === option) !==
67
81
  -1
68
82
  );
69
83
  };
70
84
 
71
- const handleSelectOption = (option: any) => {
72
- setSelectedOption(option);
85
+ // Mouse drag handlers
86
+ const handleMouseDown = (e: React.MouseEvent, option: any): void => {
87
+ e.preventDefault();
88
+ setDraggedOption(option);
89
+ setSelectedOption(null);
90
+ };
91
+
92
+ const handleMouseUp = (): void => {
93
+ if (dropTargetIndex !== null && draggedOption !== null) {
94
+ onChange(answer, dropTargetIndex, draggedOption);
95
+ }
96
+ setDraggedOption(null);
97
+ setDropTargetIndex(null);
73
98
  };
74
99
 
75
- const handleDragStart = (option: any) => {
100
+ // Touch drag handlers
101
+ const handleTouchStart = (
102
+ e: React.TouchEvent,
103
+ option: any,
104
+ element: HTMLElement
105
+ ): void => {
106
+ const touch = e.touches[0];
76
107
  setDraggedOption(option);
108
+ setDraggedElement(element);
109
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
110
+ setSelectedOption(null);
77
111
  };
78
112
 
79
- const handleDragEnd = () => {
113
+ const handleTouchMove = (e: React.TouchEvent): void => {
114
+ if (!draggedOption) return;
115
+
116
+ const touch = e.touches[0];
117
+ setTouchPosition({ x: touch.clientX, y: touch.clientY });
118
+
119
+ // Find the element under the touch point
120
+ const elementUnder = document.elementFromPoint(
121
+ touch.clientX,
122
+ touch.clientY
123
+ );
124
+ const dropZone = elementUnder?.closest("[data-drop-zone]");
125
+
126
+ if (dropZone) {
127
+ const dropIndex = dropZone.getAttribute("data-drop-zone");
128
+ setDropTargetIndex(dropIndex);
129
+ } else {
130
+ setDropTargetIndex(null);
131
+ }
132
+ };
133
+
134
+ const handleTouchEnd = (): void => {
80
135
  if (dropTargetIndex !== null && draggedOption !== null) {
81
136
  onChange(answer, dropTargetIndex, draggedOption);
82
137
  }
83
138
  setDraggedOption(null);
84
139
  setDropTargetIndex(null);
140
+ setDraggedElement(null);
85
141
  };
86
142
 
87
- const handleDropZoneEnter = (index: any) => {
88
- setDropTargetIndex(index);
143
+ // Click/tap to select (for easier mobile interaction)
144
+ const handleSelectOption = (option: any): void => {
145
+ setSelectedOption(option);
146
+ setDraggedOption(null);
89
147
  };
90
148
 
91
- const handleDropZoneDrop = (index: any) => {
149
+ const handleDropZoneClick = (index: string): void => {
92
150
  if (selectedOption !== null) {
93
151
  onChange(answer, index, selectedOption);
94
152
  setSelectedOption(null);
95
- } else if (draggedOption !== null) {
96
- onChange(answer, index, draggedOption);
97
- setDraggedOption(null);
98
153
  }
99
- setDropTargetIndex(null);
100
154
  };
101
155
 
102
156
  const answerMap = retrieveAnswerMap();
@@ -112,6 +166,38 @@ const FillInTheBlanksActivityMaterialContent = ({
112
166
  <DividerLine />
113
167
  </div>
114
168
 
169
+ {/* Floating drag preview for touch */}
170
+ {draggedOption && touchPosition.x > 0 && (
171
+ <div
172
+ className="fixed pointer-events-none z-50 opacity-80"
173
+ style={{
174
+ left: `${touchPosition.x}px`,
175
+ top: `${touchPosition.y}px`,
176
+ transform: "translate(-50%, -50%)",
177
+ }}
178
+ >
179
+ {contentMap.type === "TEXT" ? (
180
+ <div className="border-catchup-blue border-2 px-2 rounded-catchup-xlarge bg-white shadow-lg">
181
+ <p className="italic whitespace-pre-wrap">
182
+ <InputWithSpecialExpression
183
+ value={draggedOption}
184
+ showSpecialOnly={false}
185
+ />
186
+ </p>
187
+ </div>
188
+ ) : (
189
+ <div className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge bg-white shadow-lg">
190
+ <ShowMaterialMediaByContentType
191
+ key={uniqueValue}
192
+ contentType={contentMap.type}
193
+ src={draggedOption}
194
+ canFullScreen={false}
195
+ />
196
+ </div>
197
+ )}
198
+ </div>
199
+ )}
200
+
115
201
  <div className="w-full flex flex-row flex-wrap gap-x-2 gap-y-2 my-2">
116
202
  {shuffleOptionList.map((option, index) =>
117
203
  checkAnswerProvided(answerMap, option) ? (
@@ -126,18 +212,23 @@ const FillInTheBlanksActivityMaterialContent = ({
126
212
  ) : (
127
213
  <div
128
214
  key={index}
129
- draggable
130
- onDragStart={() => handleDragStart(option)}
131
- onDragEnd={handleDragEnd}
215
+ ref={draggedOption === option ? dragElementRef : null}
132
216
  className={`${
133
- draggedOption === option ? "opacity-40" : "opacity-100"
134
- } transition-opacity duration-200`}
217
+ draggedOption === option
218
+ ? "opacity-40"
219
+ : selectedOption === option
220
+ ? "ring-2 ring-blue-500"
221
+ : "opacity-100"
222
+ } transition-all duration-200`}
223
+ onMouseDown={(e) => handleMouseDown(e, option)}
224
+ onTouchStart={(e) => handleTouchStart(e, option, e.currentTarget)}
225
+ onTouchMove={handleTouchMove}
226
+ onTouchEnd={handleTouchEnd}
135
227
  >
136
228
  {contentMap.type === "TEXT" ? (
137
229
  <div
138
- className="border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none touch-none"
230
+ className="border-catchup-blue border-2 px-2 rounded-catchup-xlarge cursor-pointer select-none"
139
231
  onClick={() => handleSelectOption(option)}
140
- onTouchEnd={() => handleSelectOption(option)}
141
232
  >
142
233
  <p className="italic whitespace-pre-wrap">
143
234
  <InputWithSpecialExpression
@@ -148,9 +239,8 @@ const FillInTheBlanksActivityMaterialContent = ({
148
239
  </div>
149
240
  ) : (
150
241
  <div
151
- className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none touch-none"
242
+ className="border-catchup-blue border-2 px-2 py-1 rounded-catchup-xlarge cursor-pointer select-none"
152
243
  onClick={() => handleSelectOption(option)}
153
- onTouchEnd={() => handleSelectOption(option)}
154
244
  >
155
245
  <ShowMaterialMediaByContentType
156
246
  key={`${uniqueValue}-${index}`}
@@ -164,7 +254,7 @@ const FillInTheBlanksActivityMaterialContent = ({
164
254
  )
165
255
  )}
166
256
  </div>
167
- <div className="w-full flex flex-row flex-wrap">
257
+ <div className="w-full flex flex-row flex-wrap" onMouseUp={handleMouseUp}>
168
258
  {Object.keys(answerMap).map((materialKey, index) => {
169
259
  const learnerAnswerState = checkAnswerState(
170
260
  JSON.parse(materialMap[materialKey]),
@@ -174,25 +264,17 @@ const FillInTheBlanksActivityMaterialContent = ({
174
264
  <div key={index} className="w-full md:w-1/2">
175
265
  <div className="mx-2">
176
266
  <div
177
- onDragOver={(e) => {
178
- e.preventDefault();
179
- handleDropZoneEnter(materialKey);
180
- }}
181
- onDragLeave={() => setDropTargetIndex(null)}
182
- onDrop={(e) => {
183
- e.preventDefault();
184
- handleDropZoneDrop(materialKey);
185
- }}
186
- onClick={() => {
187
- if (selectedOption !== null) {
188
- handleDropZoneDrop(materialKey);
189
- }
190
- }}
267
+ data-drop-zone={materialKey}
268
+ onMouseEnter={() =>
269
+ draggedOption && setDropTargetIndex(materialKey)
270
+ }
271
+ onMouseLeave={() => setDropTargetIndex(null)}
272
+ onClick={() => handleDropZoneClick(materialKey)}
191
273
  className={`${
192
274
  dropTargetIndex === materialKey
193
- ? "ring-2 ring-blue-400"
275
+ ? "ring-2 ring-blue-400 bg-blue-50"
194
276
  : ""
195
- }`}
277
+ } transition-all duration-200 rounded-lg`}
196
278
  >
197
279
  <div className="w-full flex flex-row my-2 gap-x-2">
198
280
  <div className="my-auto">
@@ -206,11 +288,12 @@ const FillInTheBlanksActivityMaterialContent = ({
206
288
  <div
207
289
  className={`w-full min-h-[44px] border rounded-lg ${
208
290
  answerMap[materialKey]
209
- ? "border-catchup-blue-400 px-2"
291
+ ? "border-catchup-blue-400 px-2 cursor-pointer"
210
292
  : "bg-catchup-gray-50 border-catchup-gray-200 border-dashed py-2 px-4"
211
293
  }`}
212
- onClick={() => {
294
+ onClick={(e) => {
213
295
  if (answerMap[materialKey]) {
296
+ e.stopPropagation();
214
297
  onChange(answer, materialKey, "");
215
298
  }
216
299
  }}
@@ -221,7 +304,9 @@ const FillInTheBlanksActivityMaterialContent = ({
221
304
  showSpecialOnly={false}
222
305
  />
223
306
  ) : (
224
- <p className="text-gray-400 italic"></p>
307
+ <p className="text-gray-400 italic">
308
+ {i18n.t("please_drop_here")}
309
+ </p>
225
310
  )}
226
311
  </div>
227
312
  </div>
@@ -263,7 +348,8 @@ const FillInTheBlanksActivityMaterialContent = ({
263
348
  ) : (
264
349
  <div
265
350
  className="flex-1 cursor-pointer"
266
- onClick={() => {
351
+ onClick={(e) => {
352
+ e.stopPropagation();
267
353
  onChange(answer, materialKey, "");
268
354
  }}
269
355
  >