mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-08 23:59:38 -06:00
feat: Full RTL support for Multi-Language (#2727)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -49,7 +49,7 @@ How to deliver a specific language depends on the survey type (app or link surve
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Click on the **Edit Languages** button, to add a new language to your survey
|
||||
2. Click on the **Edit languages** button, to add a new language to your survey
|
||||
|
||||
<MdxImage
|
||||
src={SurveyLanguageSettings}
|
||||
@@ -58,7 +58,7 @@ How to deliver a specific language depends on the survey type (app or link surve
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add Language** button to add the language to your product.
|
||||
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add language** button to add the language to your product.
|
||||
|
||||
<MdxImage
|
||||
src={AddLanguages}
|
||||
|
||||
@@ -177,7 +177,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
|
||||
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
|
||||
<div className=" text-amber-800">
|
||||
<p className="text-xs font-semibold">
|
||||
Your ${option.id} is not yet connected to Formbricks.
|
||||
Your {option.id} is not yet connected to Formbricks.
|
||||
</p>
|
||||
<p className="text-xs font-normal">
|
||||
<Link
|
||||
|
||||
@@ -168,13 +168,13 @@ export const QuestionCard = ({
|
||||
className="flex-1 rounded-r-lg border border-slate-200">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className={cn(open ? "" : " ", "flex cursor-pointer justify-between p-4 hover:bg-slate-50")}>
|
||||
className={cn(open ? "" : " ", "flex cursor-pointer justify-between gap-4 p-4 hover:bg-slate-50")}>
|
||||
<div>
|
||||
<div className="inline-flex">
|
||||
<div className="flex grow">
|
||||
<div className="-ml-0.5 mr-3 h-6 min-w-[1.5rem] text-slate-400">
|
||||
{QUESTIONS_ICON_MAP[question.type]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="grow" dir="auto">
|
||||
<p className="text-sm font-semibold">
|
||||
{recallToHeadline(
|
||||
question.headline,
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@formbricks/ui/DropdownMenu";
|
||||
|
||||
interface QuestionDropdownProps {
|
||||
interface QuestionMenuProps {
|
||||
questionIdx: number;
|
||||
lastQuestion: boolean;
|
||||
duplicateQuestion: (questionIdx: number) => void;
|
||||
@@ -39,7 +39,7 @@ export const QuestionMenu = ({
|
||||
question,
|
||||
updateQuestion,
|
||||
addQuestion,
|
||||
}: QuestionDropdownProps) => {
|
||||
}: QuestionMenuProps) => {
|
||||
const [logicWarningModal, setLogicWarningModal] = useState(false);
|
||||
const [changeToType, setChangeToType] = useState(question.type);
|
||||
|
||||
|
||||
@@ -70,11 +70,8 @@ export const SelectQuestionChoice = ({
|
||||
return (
|
||||
<div className="flex w-full gap-2" ref={setNodeRef} style={style}>
|
||||
{/* drag handle */}
|
||||
<div
|
||||
className={cn("flex items-center", choice.id === "other" && "invisible")}
|
||||
{...listeners}
|
||||
{...attributes}>
|
||||
<GripVerticalIcon className="mt-3 h-4 w-4 cursor-move text-slate-400" />
|
||||
<div className={cn("mt-6", choice.id === "other" && "invisible")} {...listeners} {...attributes}>
|
||||
<GripVerticalIcon className="h-4 w-4 cursor-move text-slate-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full space-x-2">
|
||||
|
||||
@@ -61,6 +61,7 @@ export const UpdateQuestionId = ({
|
||||
onChange={(e) => {
|
||||
setCurrentValue(e.target.value);
|
||||
}}
|
||||
dir="auto"
|
||||
disabled={localSurvey.status !== "draft" && !question.isDraft}
|
||||
className={`h-10 ${isInputInvalid ? "border-red-300 focus:border-red-300" : ""}`}
|
||||
/>
|
||||
|
||||
@@ -243,7 +243,7 @@ export const BasicSegmentSettings = ({
|
||||
handleUpdateSegment();
|
||||
}}
|
||||
disabled={isSaveDisabled}>
|
||||
Save Changes
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export const MultipleChoiceSummary = ({
|
||||
.filter((otherValue) => otherValue.value !== "")
|
||||
.slice(0, visibleOtherResponses)
|
||||
.map((otherValue, idx) => (
|
||||
<div key={idx}>
|
||||
<div key={idx} dir="auto">
|
||||
{surveyType === "link" && (
|
||||
<div
|
||||
key={idx}
|
||||
|
||||
@@ -69,7 +69,7 @@ export const OpenTextSummary = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
|
||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold" dir="auto">
|
||||
{response.value}
|
||||
</div>
|
||||
<div className="px-4 text-slate-500 md:px-6">
|
||||
|
||||
@@ -191,20 +191,20 @@ test.describe("Multi Language Survey Create", async () => {
|
||||
//add a new language
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await page.getByRole("link", { name: "Survey Languages" }).click();
|
||||
await page.getByRole("button", { name: "Edit Languages" }).click();
|
||||
await page.getByRole("button", { name: "Add Language" }).click();
|
||||
await page.getByRole("button", { name: "Edit languages" }).click();
|
||||
await page.getByRole("button", { name: "Add language" }).click();
|
||||
await page.getByRole("button", { name: "Select" }).click();
|
||||
await page.getByPlaceholder("Search items").click();
|
||||
await page.getByPlaceholder("Search items").fill("Eng");
|
||||
await page.getByText("English").click();
|
||||
await page.getByRole("button", { name: "Save Changes" }).click();
|
||||
await page.getByRole("button", { name: "Edit Languages" }).click();
|
||||
await page.getByRole("button", { name: "Add Language" }).click();
|
||||
await page.getByRole("button", { name: "Save changes" }).click();
|
||||
await page.getByRole("button", { name: "Edit languages" }).click();
|
||||
await page.getByRole("button", { name: "Add language" }).click();
|
||||
await page.getByRole("button", { name: "Select" }).click();
|
||||
await page.getByRole("textbox", { name: "Search items" }).click();
|
||||
await page.getByRole("textbox", { name: "Search items" }).fill("German");
|
||||
await page.getByText("German").nth(1).click();
|
||||
await page.getByRole("button", { name: "Save Changes" }).click();
|
||||
await page.getByRole("button", { name: "Save changes" }).click();
|
||||
await page.waitForTimeout(2000);
|
||||
await page.getByRole("link", { name: "Surveys" }).click();
|
||||
await page.getByRole("button", { name: "Start from scratch Create a" }).click();
|
||||
|
||||
@@ -231,7 +231,7 @@ export function SegmentSettings({
|
||||
}}
|
||||
type="submit"
|
||||
variant="darkCTA">
|
||||
Save Changes
|
||||
Save changes
|
||||
</Button>
|
||||
|
||||
{isDeleteSegmentModalOpen ? (
|
||||
|
||||
@@ -144,8 +144,8 @@ export function EditLanguage({ product, environmentId }: EditLanguageProps) {
|
||||
|
||||
const AddLanguageButton: React.FC<{ onClick: () => void }> = ({ onClick }) =>
|
||||
isEditing && languages.length === product.languages.length ? (
|
||||
<Button onClick={onClick} size="sm" variant="secondary">
|
||||
<PlusIcon /> Add Language
|
||||
<Button onClick={onClick} size="sm" variant="secondary" StartIcon={PlusIcon}>
|
||||
Add language
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
@@ -236,7 +236,7 @@ const EditSaveButtons: React.FC<{
|
||||
isEditing ? (
|
||||
<div className="flex gap-4">
|
||||
<Button onClick={onSave} size="sm" variant="darkCTA">
|
||||
Save Changes
|
||||
Save changes
|
||||
</Button>
|
||||
<Button onClick={onCancel} size="sm" variant="minimal">
|
||||
Cancel
|
||||
@@ -244,6 +244,6 @@ const EditSaveButtons: React.FC<{
|
||||
</div>
|
||||
) : (
|
||||
<Button className="w-fit" onClick={onEdit} size="sm" variant="darkCTA">
|
||||
Edit Languages
|
||||
Edit languages
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -667,7 +667,7 @@ const checkForI18n = (response: TResponse, id: string, survey: TSurvey, language
|
||||
(typeof response.data[id] === "string"
|
||||
? ([response.data[id]] as string[])
|
||||
: (response.data[id] as string[])
|
||||
).forEach((data) => {
|
||||
)?.forEach((data) => {
|
||||
choiceValues.push(
|
||||
getLocalizedValue(
|
||||
question.choices.find((choice) => choice.label[languageCode] === data)?.label,
|
||||
|
||||
@@ -9,6 +9,7 @@ interface BackButtonProps {
|
||||
export const BackButton = ({ onClick, backButtonLabel, tabIndex = 2 }: BackButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
dir="auto"
|
||||
tabIndex={tabIndex}
|
||||
type={"button"}
|
||||
className={cn(
|
||||
|
||||
@@ -30,6 +30,7 @@ export const SubmitButton = ({
|
||||
|
||||
return (
|
||||
<button
|
||||
dir="auto"
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
tabIndex={tabIndex}
|
||||
|
||||
@@ -13,7 +13,9 @@ export const Headline = ({
|
||||
}: HeadlineProps) => {
|
||||
return (
|
||||
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
|
||||
<div className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}>
|
||||
<div
|
||||
className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}
|
||||
dir="auto">
|
||||
{headline}
|
||||
{!required && (
|
||||
<span
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface HtmlBodyProps {
|
||||
htmlString: string | undefined;
|
||||
htmlString?: string;
|
||||
questionId: string;
|
||||
}
|
||||
|
||||
@@ -22,7 +23,9 @@ export const HtmlBody = ({ htmlString, questionId }: HtmlBodyProps) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor={questionId}
|
||||
className="fb-htmlbody break-words" // styles are in global.css
|
||||
dangerouslySetInnerHTML={{ __html: safeHtml }}></label>
|
||||
className={cn("fb-htmlbody break-words")} // styles are in global.css
|
||||
dangerouslySetInnerHTML={{ __html: safeHtml }}
|
||||
dir="auto"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
export const Subheader = ({ subheader, questionId }: { subheader?: string; questionId: string }) => {
|
||||
interface SubheaderProps {
|
||||
subheader?: string;
|
||||
questionId: string;
|
||||
}
|
||||
|
||||
export const Subheader = ({ subheader, questionId }: SubheaderProps) => {
|
||||
return (
|
||||
<p htmlFor={questionId} className="text-subheading block break-words text-sm font-normal leading-5">
|
||||
<p
|
||||
htmlFor={questionId}
|
||||
className="text-subheading block break-words text-sm font-normal leading-5"
|
||||
dir="auto">
|
||||
{subheader}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -147,6 +147,7 @@ export const AddressQuestion = ({
|
||||
{inputConfig.map(({ name, placeholder, required }, index) => (
|
||||
<input
|
||||
ref={index === 0 ? addressTextRef : null}
|
||||
dir="auto"
|
||||
key={index}
|
||||
name={name}
|
||||
autoComplete={name}
|
||||
|
||||
@@ -72,6 +72,7 @@ export const CTAQuestion = ({
|
||||
<div className="flex w-full justify-end">
|
||||
{!question.required && (
|
||||
<button
|
||||
dir="auto"
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
||||
@@ -66,6 +66,7 @@ export const ConsentQuestion = ({
|
||||
/>
|
||||
<div className="bg-survey-bg sticky -bottom-2 z-10 w-full px-1 py-1">
|
||||
<label
|
||||
dir="auto"
|
||||
tabIndex={1}
|
||||
id={`${question.id}-label`}
|
||||
onKeyDown={(e) => {
|
||||
@@ -94,7 +95,7 @@ export const ConsentQuestion = ({
|
||||
aria-labelledby={`${question.id}-label`}
|
||||
required={question.required}
|
||||
/>
|
||||
<span id={`${question.id}-label`} className="ml-3 font-medium">
|
||||
<span id={`${question.id}-label`} className="ml-3 mr-3 font-medium">
|
||||
{getLocalizedValue(question.label, languageCode)}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -84,7 +84,7 @@ export const MatrixQuestion = ({
|
||||
const columnsHeaders = useMemo(
|
||||
() =>
|
||||
question.columns.map((column, index) => (
|
||||
<th key={index} className="text-heading max-w-40 break-words px-4 py-2 font-normal">
|
||||
<th key={index} className="text-heading max-w-40 break-words px-4 py-2 font-normal" dir="auto">
|
||||
{getLocalizedValue(column, languageCode)}
|
||||
</th>
|
||||
)),
|
||||
@@ -117,7 +117,7 @@ export const MatrixQuestion = ({
|
||||
{question.rows.map((row, rowIndex) => (
|
||||
// Table rows
|
||||
<tr className={`${rowIndex % 2 === 0 ? "bg-input-bg" : ""}`}>
|
||||
<td className="text-heading rounded-l-custom max-w-40 break-words px-4 py-2">
|
||||
<td className="text-heading rounded-l-custom max-w-40 break-words px-4 py-2" dir="auto">
|
||||
{getLocalizedValue(row, languageCode)}
|
||||
</td>
|
||||
{question.columns.map((column, columnIndex) => (
|
||||
@@ -139,10 +139,12 @@ export const MatrixQuestion = ({
|
||||
getLocalizedValue(row, languageCode)
|
||||
);
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
dir="auto">
|
||||
<div className="flex items-center justify-center p-2">
|
||||
{/* radio input */}
|
||||
<input
|
||||
dir="auto"
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
id={`${row}-${column}`}
|
||||
|
||||
@@ -181,7 +181,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
}
|
||||
}}
|
||||
autoFocus={idx === 0 && !isInIframe}>
|
||||
<span className="flex items-center text-sm">
|
||||
<span className="flex items-center text-sm" dir="auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={choice.id}
|
||||
@@ -207,7 +207,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
: question.required
|
||||
}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
<span id={`${choice.id}-label`} className="ml-3 mr-3 grow font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -231,7 +231,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
document.getElementById(otherOption.id)?.focus();
|
||||
}
|
||||
}}>
|
||||
<span className="flex items-center text-sm">
|
||||
<span className="flex items-center text-sm" dir="auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
@@ -250,13 +250,14 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 mr-3 grow font-medium">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
dir="auto"
|
||||
id={`${otherOption.id}-label`}
|
||||
name={question.id}
|
||||
tabIndex={questionChoices.length + 1}
|
||||
|
||||
@@ -128,6 +128,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
if (!choice || choice.id === "other") return;
|
||||
return (
|
||||
<label
|
||||
dir="auto"
|
||||
tabIndex={idx + 1}
|
||||
key={choice.id}
|
||||
className={cn(
|
||||
@@ -152,6 +153,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
id={choice.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(choice.label, languageCode)}
|
||||
dir="auto"
|
||||
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={() => {
|
||||
@@ -161,7 +163,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
checked={value === getLocalizedValue(choice.label, languageCode)}
|
||||
required={question.required && idx === 0}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
<span id={`${choice.id}-label`} className="ml-3 mr-3 grow font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -170,6 +172,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
})}
|
||||
{otherOption && (
|
||||
<label
|
||||
dir="auto"
|
||||
tabIndex={questionChoices.length + 1}
|
||||
className={cn(
|
||||
value === getLocalizedValue(otherOption.label, languageCode)
|
||||
@@ -187,6 +190,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
}}>
|
||||
<span className="flex items-center text-sm">
|
||||
<input
|
||||
dir="auto"
|
||||
type="radio"
|
||||
id={otherOption.id}
|
||||
tabIndex={-1}
|
||||
@@ -200,7 +204,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
}}
|
||||
checked={otherSelected}
|
||||
/>
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
|
||||
<span id={`${otherOption.id}-label`} className="ml-3 mr-3 grow font-medium" dir="auto">
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -209,6 +213,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
ref={otherSpecify}
|
||||
tabIndex={questionChoices.length + 1}
|
||||
id={`${otherOption.id}-label`}
|
||||
dir="auto"
|
||||
name={question.id}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -122,8 +122,8 @@ export const NPSQuestion = ({
|
||||
})}
|
||||
</div>
|
||||
<div className="text-subheading mt-2 flex justify-between px-1.5 text-xs leading-6">
|
||||
<p>{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p>{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
<p dir="auto">{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p dir="auto">{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +99,7 @@ export const OpenTextQuestion = ({
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir="auto"
|
||||
step={"any"}
|
||||
required={question.required}
|
||||
value={value ? (value as string) : ""}
|
||||
@@ -118,6 +119,7 @@ export const OpenTextQuestion = ({
|
||||
aria-label="textarea"
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir="auto"
|
||||
required={question.required}
|
||||
value={value as string}
|
||||
type={question.inputType}
|
||||
|
||||
@@ -206,8 +206,12 @@ export const RatingQuestion = ({
|
||||
))}
|
||||
</div>
|
||||
<div className="text-subheading mt-4 flex justify-between px-1.5 text-xs leading-6">
|
||||
<p className="w-1/2 text-left">{getLocalizedValue(question.lowerLabel, languageCode)}</p>
|
||||
<p className="w-1/2 text-right">{getLocalizedValue(question.upperLabel, languageCode)}</p>
|
||||
<p className="w-1/2 text-left" dir="auto">
|
||||
{getLocalizedValue(question.lowerLabel, languageCode)}
|
||||
</p>
|
||||
<p className="w-1/2 text-right" dir="auto">
|
||||
{getLocalizedValue(question.upperLabel, languageCode)}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
/* need to use !important because in packages/ui/components/editor/stylesEditorFrontend.css the color is defined for some classes */
|
||||
color: var(--fb-subheading-color) !important;
|
||||
}
|
||||
|
||||
/* without this, it wont override the color */
|
||||
p.fb-editor-paragraph {
|
||||
color: var(--fb-subheading-color) !important;
|
||||
@@ -106,8 +107,8 @@ p.fb-editor-paragraph {
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none !important; /* Internet Explorer 10+ */
|
||||
scrollbar-width: thin !important; /* Firefox */
|
||||
-ms-overflow-style: none !important; /* Internet Explorer 10+ */
|
||||
scrollbar-width: thin !important; /* Firefox */
|
||||
scrollbar-color: transparent transparent !important; /* Firefox */
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
@@ -118,4 +119,4 @@ p.fb-editor-paragraph {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ interface AddressResponseProps {
|
||||
|
||||
export const AddressResponse = ({ value }: AddressResponseProps) => {
|
||||
return (
|
||||
<div className="my-1 font-semibold text-slate-700">
|
||||
<div className="my-1 font-semibold text-slate-700" dir="auto">
|
||||
{value.map(
|
||||
(item, index) =>
|
||||
item && (
|
||||
|
||||
@@ -63,6 +63,7 @@ const editorConfig = {
|
||||
|
||||
export const Editor = (props: TextEditorProps) => {
|
||||
const editable = props.editable ?? true;
|
||||
|
||||
return (
|
||||
<div className="editor cursor-text rounded-md">
|
||||
<LexicalComposer initialConfig={{ ...editorConfig, editable }}>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const exampleTheme = {
|
||||
rtl: "fb-editor-rtl",
|
||||
ltr: "fb-editor-ltr",
|
||||
placeholder: "fb-editor-placeholder",
|
||||
paragraph: "fb-editor-paragraph",
|
||||
heading: {
|
||||
|
||||
@@ -53,3 +53,11 @@
|
||||
.fb-editor-nested-listitem {
|
||||
list-style-type: none !important;
|
||||
}
|
||||
|
||||
.fb-editor-rtl {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.fb-editor-ltr {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ export const QuestionFormInput = ({
|
||||
className,
|
||||
attributeClasses,
|
||||
}: QuestionFormInputProps) => {
|
||||
const defaultLanguageCode =
|
||||
localSurvey.languages.filter((lang) => lang.default)[0]?.language.code ?? "default";
|
||||
const usedLanguageCode = selectedLanguageCode === defaultLanguageCode ? "default" : selectedLanguageCode;
|
||||
|
||||
const question: TSurveyQuestion = localSurvey.questions[questionIdx];
|
||||
const isChoice = id.includes("choice");
|
||||
const isMatrixLabelRow = id.includes("row");
|
||||
@@ -135,18 +139,18 @@ export const QuestionFormInput = ({
|
||||
const [showRecallItemSelect, setShowRecallItemSelect] = useState(false);
|
||||
const [showFallbackInput, setShowFallbackInput] = useState(false);
|
||||
const [recallItems, setRecallItems] = useState<TSurveyRecallItem[]>(
|
||||
getLocalizedValue(text, selectedLanguageCode).includes("#recall:")
|
||||
getLocalizedValue(text, usedLanguageCode).includes("#recall:")
|
||||
? getRecallItems(
|
||||
getLocalizedValue(text, selectedLanguageCode),
|
||||
getLocalizedValue(text, usedLanguageCode),
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
usedLanguageCode,
|
||||
attributeClasses
|
||||
)
|
||||
: []
|
||||
);
|
||||
const [fallbacks, setFallbacks] = useState<{ [type: string]: string }>(
|
||||
getLocalizedValue(text, selectedLanguageCode).includes("/fallback:")
|
||||
? getFallbackValues(getLocalizedValue(text, selectedLanguageCode))
|
||||
getLocalizedValue(text, usedLanguageCode).includes("/fallback:")
|
||||
? getFallbackValues(getLocalizedValue(text, usedLanguageCode))
|
||||
: {}
|
||||
);
|
||||
|
||||
@@ -161,6 +165,19 @@ export const QuestionFormInput = ({
|
||||
// Hook to synchronize the horizontal scroll position of highlightContainerRef and inputRef.
|
||||
useSyncScroll(highlightContainerRef, inputRef);
|
||||
|
||||
useEffect(() => {
|
||||
setRecallItems(
|
||||
getLocalizedValue(text, usedLanguageCode).includes("#recall:")
|
||||
? getRecallItems(
|
||||
getLocalizedValue(text, usedLanguageCode),
|
||||
localSurvey,
|
||||
usedLanguageCode,
|
||||
attributeClasses
|
||||
)
|
||||
: []
|
||||
);
|
||||
}, [usedLanguageCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id === "headline" || id === "subheader") {
|
||||
checkForRecallSymbol();
|
||||
@@ -187,8 +204,8 @@ export const QuestionFormInput = ({
|
||||
// Constructs an array of JSX elements representing segmented parts of text, interspersed with special formatted spans for recall headlines.
|
||||
const processInput = (): JSX.Element[] => {
|
||||
const parts: JSX.Element[] = [];
|
||||
let remainingText = recallToHeadline(text, localSurvey, false, selectedLanguageCode, attributeClasses)[
|
||||
selectedLanguageCode
|
||||
let remainingText = recallToHeadline(text, localSurvey, false, usedLanguageCode, attributeClasses)[
|
||||
usedLanguageCode
|
||||
];
|
||||
filterRecallItems(remainingText);
|
||||
recallItemLabels.forEach((label) => {
|
||||
@@ -221,7 +238,7 @@ export const QuestionFormInput = ({
|
||||
return parts;
|
||||
};
|
||||
setRenderedText(processInput());
|
||||
}, [text]);
|
||||
}, [text, recallItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fallbackInputRef.current) {
|
||||
@@ -235,7 +252,7 @@ export const QuestionFormInput = ({
|
||||
|
||||
const checkForRecallSymbol = () => {
|
||||
const pattern = /(^|\s)@(\s|$)/;
|
||||
if (pattern.test(getLocalizedValue(text, selectedLanguageCode))) {
|
||||
if (pattern.test(getLocalizedValue(text, usedLanguageCode))) {
|
||||
setShowRecallItemSelect(true);
|
||||
} else {
|
||||
setShowRecallItemSelect(false);
|
||||
@@ -262,16 +279,16 @@ export const QuestionFormInput = ({
|
||||
}
|
||||
setShowRecallItemSelect(false);
|
||||
let modifiedHeadlineWithId = { ...getElementTextBasedOnType() };
|
||||
modifiedHeadlineWithId[selectedLanguageCode] = getLocalizedValue(
|
||||
modifiedHeadlineWithId[usedLanguageCode] = getLocalizedValue(
|
||||
modifiedHeadlineWithId,
|
||||
selectedLanguageCode
|
||||
usedLanguageCode
|
||||
).replace(/(?<=^|\s)@(?=\s|$)/g, `#recall:${recallItem.id}/fallback:# `);
|
||||
handleUpdate(getLocalizedValue(modifiedHeadlineWithId, selectedLanguageCode));
|
||||
handleUpdate(getLocalizedValue(modifiedHeadlineWithId, usedLanguageCode));
|
||||
const modifiedHeadlineWithName = recallToHeadline(
|
||||
modifiedHeadlineWithId,
|
||||
localSurvey,
|
||||
false,
|
||||
selectedLanguageCode,
|
||||
usedLanguageCode,
|
||||
attributeClasses
|
||||
);
|
||||
setText(modifiedHeadlineWithName);
|
||||
@@ -287,15 +304,15 @@ export const QuestionFormInput = ({
|
||||
} else {
|
||||
const recallItemToRemove = recallItem.label.slice(0, -1);
|
||||
const newText = { ...text };
|
||||
newText[selectedLanguageCode] = text[selectedLanguageCode].replace(`@${recallItemToRemove}`, "");
|
||||
newText[usedLanguageCode] = text[usedLanguageCode].replace(`@${recallItemToRemove}`, "");
|
||||
setText(newText);
|
||||
handleUpdate(text[selectedLanguageCode].replace(`@${recallItemToRemove}`, ""));
|
||||
handleUpdate(text[usedLanguageCode].replace(`@${recallItemToRemove}`, ""));
|
||||
let updatedFallback = { ...fallbacks };
|
||||
delete updatedFallback[recallItem.id];
|
||||
setFallbacks(updatedFallback);
|
||||
setRecallItems(includedRecallItems);
|
||||
}
|
||||
});
|
||||
setRecallItems(includedRecallItems);
|
||||
};
|
||||
|
||||
const addFallback = () => {
|
||||
@@ -303,7 +320,7 @@ export const QuestionFormInput = ({
|
||||
filteredRecallItems.forEach((recallQuestion) => {
|
||||
if (recallQuestion) {
|
||||
const recallInfo = findRecallInfoById(
|
||||
getLocalizedValue(headlineWithFallback, selectedLanguageCode),
|
||||
getLocalizedValue(headlineWithFallback, usedLanguageCode),
|
||||
recallQuestion!.id
|
||||
);
|
||||
if (recallInfo) {
|
||||
@@ -312,11 +329,11 @@ export const QuestionFormInput = ({
|
||||
let updatedFallback = { ...fallbacks };
|
||||
updatedFallback[recallQuestion.id] = fallBackValue;
|
||||
setFallbacks(updatedFallback);
|
||||
headlineWithFallback[selectedLanguageCode] = getLocalizedValue(
|
||||
headlineWithFallback[usedLanguageCode] = getLocalizedValue(
|
||||
headlineWithFallback,
|
||||
selectedLanguageCode
|
||||
usedLanguageCode
|
||||
).replace(recallInfo, `#recall:${recallQuestion?.id}/fallback:${fallBackValue}#`);
|
||||
handleUpdate(getLocalizedValue(headlineWithFallback, selectedLanguageCode));
|
||||
handleUpdate(getLocalizedValue(headlineWithFallback, usedLanguageCode));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -347,7 +364,7 @@ export const QuestionFormInput = ({
|
||||
const createUpdatedText = (updatedText: string): TI18nString => {
|
||||
return {
|
||||
...getElementTextBasedOnType(),
|
||||
[selectedLanguageCode]: updatedText,
|
||||
[usedLanguageCode]: updatedText,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -424,10 +441,11 @@ export const QuestionFormInput = ({
|
||||
<div
|
||||
id="wrapper"
|
||||
ref={highlightContainerRef}
|
||||
className="no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ">
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${localSurvey.languages?.length > 1 ? "pr-24" : ""}`}
|
||||
dir="auto">
|
||||
{renderedText}
|
||||
</div>
|
||||
{getLocalizedValue(getElementTextBasedOnType(), selectedLanguageCode).includes("recall:") && (
|
||||
{getLocalizedValue(getElementTextBasedOnType(), usedLanguageCode).includes("recall:") && (
|
||||
<button
|
||||
className="fixed right-14 hidden items-center rounded-b-lg bg-slate-100 px-2.5 py-1 text-xs hover:bg-slate-200 group-hover:flex"
|
||||
onClick={(e) => {
|
||||
@@ -439,7 +457,8 @@ export const QuestionFormInput = ({
|
||||
</button>
|
||||
)}
|
||||
<Input
|
||||
key={`${questionId}-${id}-${selectedLanguageCode}`}
|
||||
key={`${questionId}-${id}-${usedLanguageCode}`}
|
||||
dir="auto"
|
||||
className={`absolute top-0 text-black caret-black ${localSurvey.languages?.length > 1 ? "pr-24" : ""} ${className}`}
|
||||
placeholder={placeholder ? placeholder : getPlaceHolderById(id)}
|
||||
id={id}
|
||||
@@ -447,8 +466,8 @@ export const QuestionFormInput = ({
|
||||
aria-label={label}
|
||||
autoComplete={showRecallItemSelect ? "off" : "on"}
|
||||
value={
|
||||
recallToHeadline(text, localSurvey, false, selectedLanguageCode, attributeClasses)[
|
||||
selectedLanguageCode
|
||||
recallToHeadline(text, localSurvey, false, usedLanguageCode, attributeClasses)[
|
||||
usedLanguageCode
|
||||
]
|
||||
}
|
||||
ref={inputRef}
|
||||
@@ -456,30 +475,24 @@ export const QuestionFormInput = ({
|
||||
onChange={(e) => {
|
||||
let translatedText = {
|
||||
...getElementTextBasedOnType(),
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
[usedLanguageCode]: e.target.value,
|
||||
};
|
||||
setText(
|
||||
recallToHeadline(
|
||||
translatedText,
|
||||
localSurvey,
|
||||
false,
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
)
|
||||
recallToHeadline(translatedText, localSurvey, false, usedLanguageCode, attributeClasses)
|
||||
);
|
||||
handleUpdate(headlineToRecall(e.target.value, recallItems, fallbacks));
|
||||
}}
|
||||
maxLength={maxLength ?? undefined}
|
||||
isInvalid={
|
||||
isInvalid &&
|
||||
text[selectedLanguageCode]?.trim() === "" &&
|
||||
text[usedLanguageCode]?.trim() === "" &&
|
||||
localSurvey.languages?.length > 1 &&
|
||||
isTranslationIncomplete
|
||||
}
|
||||
/>
|
||||
{enabledLanguages.length > 1 && (
|
||||
<LanguageIndicator
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
selectedLanguageCode={usedLanguageCode}
|
||||
surveyLanguages={enabledLanguages}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
@@ -520,19 +533,19 @@ export const QuestionFormInput = ({
|
||||
addRecallItem={addRecallItem}
|
||||
setShowRecallItemSelect={setShowRecallItemSelect}
|
||||
recallItems={recallItems}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
selectedLanguageCode={usedLanguageCode}
|
||||
hiddenFields={localSurvey.hiddenFields}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedLanguageCode !== "default" && value && typeof value["default"] !== undefined && (
|
||||
{usedLanguageCode !== "default" && value && typeof value["default"] !== undefined && (
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
<strong>Translate:</strong>{" "}
|
||||
{recallToHeadline(value, localSurvey, false, "default", attributeClasses)["default"]}
|
||||
</div>
|
||||
)}
|
||||
{selectedLanguageCode === "default" && localSurvey.languages?.length > 1 && isTranslationIncomplete && (
|
||||
{usedLanguageCode === "default" && localSurvey.languages?.length > 1 && isTranslationIncomplete && (
|
||||
<div className="mt-1 text-xs text-red-400">Contains Incomplete translations</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -157,7 +157,7 @@ export const SingleResponseCardBody = ({
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
{renderResponse(question.type, response.data[question.id], question)}
|
||||
<div dir="auto">{renderResponse(question.type, response.data[question.id], question)}</div>
|
||||
</div>
|
||||
) : (
|
||||
<QuestionSkip
|
||||
|
||||
Reference in New Issue
Block a user