mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-16 11:38:38 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b22c839d3c | |||
| d44cc1e27e |
@@ -24,7 +24,6 @@ interface LinkSurveyWrapperProps {
|
||||
IS_FORMBRICKS_CLOUD: boolean;
|
||||
publicDomain: string;
|
||||
isBrandingEnabled: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export const LinkSurveyWrapper = ({
|
||||
@@ -42,7 +41,6 @@ export const LinkSurveyWrapper = ({
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
publicDomain,
|
||||
isBrandingEnabled,
|
||||
dir = "auto",
|
||||
}: LinkSurveyWrapperProps) => {
|
||||
//for embedded survey strip away all surrounding css
|
||||
const [isBackgroundLoaded, setIsBackgroundLoaded] = useState(false);
|
||||
@@ -82,11 +80,11 @@ export const LinkSurveyWrapper = ({
|
||||
onBackgroundLoaded={handleBackgroundLoaded}>
|
||||
<div className="flex max-h-dvh min-h-dvh items-center justify-center overflow-clip">
|
||||
{!styling.isLogoHidden && (project.logo?.url || styling.logo?.url) && (
|
||||
<ClientLogo projectLogo={project.logo} surveyLogo={styling.logo} dir={dir} />
|
||||
<ClientLogo projectLogo={project.logo} surveyLogo={styling.logo} />
|
||||
)}
|
||||
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
|
||||
{isPreview && (
|
||||
<div className="fixed top-0 left-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
<div />
|
||||
Survey Preview 👀
|
||||
<ResetProgressButton onClick={handleResetSurvey} />
|
||||
|
||||
@@ -10,7 +10,6 @@ import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { CustomScriptsInjector } from "@/modules/survey/link/components/custom-scripts-injector";
|
||||
import { LinkSurveyWrapper } from "@/modules/survey/link/components/link-survey-wrapper";
|
||||
import { getPrefillValue } from "@/modules/survey/link/lib/prefill";
|
||||
import { isRTLLanguage } from "@/modules/survey/link/lib/utils";
|
||||
import { SurveyInline } from "@/modules/ui/components/survey";
|
||||
|
||||
interface SurveyClientWrapperProps {
|
||||
@@ -117,11 +116,6 @@ export const SurveyClientWrapper = ({
|
||||
}
|
||||
setResponseData({});
|
||||
};
|
||||
// Determine text direction based on language code for logo positioning only
|
||||
// which checks both language code and survey content. This is only for logo UI positioning.
|
||||
const logoDir = useMemo(() => {
|
||||
return isRTLLanguage(survey, languageCode) ? "rtl" : "auto";
|
||||
}, [languageCode, survey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -146,8 +140,7 @@ export const SurveyClientWrapper = ({
|
||||
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
|
||||
IMPRINT_URL={IMPRINT_URL}
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
dir={logoDir}>
|
||||
isBrandingEnabled={project.linkSurveyBranding}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
environmentId={survey.environmentId}
|
||||
|
||||
@@ -1,188 +1,85 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getElementsFromSurveyBlocks, getWebAppLocale, isRTL, isRTLLanguage } from "./utils";
|
||||
|
||||
const createMockSurvey = (languages: TSurvey["languages"] = []): TSurvey =>
|
||||
({
|
||||
id: "survey-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test",
|
||||
type: "link",
|
||||
environmentId: "env-1",
|
||||
createdBy: null,
|
||||
status: "draft",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Welcome" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
questions: [],
|
||||
blocks: [],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
variables: [],
|
||||
styling: null,
|
||||
segment: null,
|
||||
languages,
|
||||
displayPercentage: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
projectOverwrites: null,
|
||||
surveyClosedMessage: null,
|
||||
followUps: [],
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
showLanguageSwitch: null,
|
||||
recaptcha: null,
|
||||
isBackButtonHidden: false,
|
||||
isCaptureIpEnabled: false,
|
||||
slug: null,
|
||||
metadata: {},
|
||||
}) as TSurvey;
|
||||
import { getWebAppLocale } from "./utils";
|
||||
|
||||
describe("getWebAppLocale", () => {
|
||||
test("maps language codes and handles defaults", () => {
|
||||
expect(getWebAppLocale("en", createMockSurvey())).toBe("en-US");
|
||||
expect(getWebAppLocale("de", createMockSurvey())).toBe("de-DE");
|
||||
const surveyWithLang = createMockSurvey([
|
||||
{
|
||||
language: {
|
||||
id: "l1",
|
||||
code: "de",
|
||||
alias: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "p1",
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
expect(getWebAppLocale("default", surveyWithLang)).toBe("de-DE");
|
||||
expect(getWebAppLocale("xx", createMockSurvey())).toBe("en-US");
|
||||
});
|
||||
|
||||
test("returns en-US when default requested but no default language", () => {
|
||||
const surveyNoDefault = createMockSurvey([
|
||||
{
|
||||
language: { id: "l1", code: "de", alias: null, createdAt: new Date(), updatedAt: new Date(), projectId: "p1" },
|
||||
default: false,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
expect(getWebAppLocale("default", surveyNoDefault)).toBe("en-US");
|
||||
});
|
||||
|
||||
test("matches base language code for variants", () => {
|
||||
expect(getWebAppLocale("pt-PT", createMockSurvey())).toBe("pt-PT");
|
||||
expect(getWebAppLocale("es-MX", createMockSurvey())).toBe("es-ES");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRTL", () => {
|
||||
test("detects RTL characters", () => {
|
||||
expect(isRTL("مرحبا")).toBe(true);
|
||||
expect(isRTL("שלום")).toBe(true);
|
||||
expect(isRTL("Hello")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRTLLanguage", () => {
|
||||
const createJsSurvey = (
|
||||
languages: TJsEnvironmentStateSurvey["languages"] = [],
|
||||
blocks: TSurveyBlock[] = []
|
||||
): TJsEnvironmentStateSurvey =>
|
||||
({
|
||||
id: "s1",
|
||||
const createMockSurvey = (languages: TSurvey["languages"] = []): TSurvey => {
|
||||
return {
|
||||
id: "survey-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test",
|
||||
name: "Test Survey",
|
||||
type: "link",
|
||||
environmentId: "env-1",
|
||||
createdBy: null,
|
||||
status: "draft",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Welcome" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
blocks,
|
||||
questions: [],
|
||||
blocks: [],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
variables: [],
|
||||
styling: null,
|
||||
segment: null,
|
||||
languages,
|
||||
}) as unknown as TJsEnvironmentStateSurvey;
|
||||
displayPercentage: null,
|
||||
isVerifyEmailEnabled: false,
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
singleUse: null,
|
||||
pin: null,
|
||||
projectOverwrites: null,
|
||||
surveyClosedMessage: null,
|
||||
followUps: [],
|
||||
delay: 0,
|
||||
autoComplete: null,
|
||||
showLanguageSwitch: null,
|
||||
recaptcha: null,
|
||||
isBackButtonHidden: false,
|
||||
isCaptureIpEnabled: false,
|
||||
slug: null,
|
||||
metadata: {},
|
||||
} as TSurvey;
|
||||
};
|
||||
|
||||
test("checks language codes when multi-language enabled", () => {
|
||||
const survey = createJsSurvey([
|
||||
test("maps language codes to web app locales", () => {
|
||||
const survey = createMockSurvey();
|
||||
expect(getWebAppLocale("en", survey)).toBe("en-US");
|
||||
expect(getWebAppLocale("de", survey)).toBe("de-DE");
|
||||
expect(getWebAppLocale("pt-BR", survey)).toBe("pt-BR");
|
||||
});
|
||||
|
||||
test("handles 'default' languageCode by finding default language in survey", () => {
|
||||
const survey = createMockSurvey([
|
||||
{
|
||||
language: {
|
||||
id: "l1",
|
||||
code: "ar",
|
||||
id: "lang1",
|
||||
code: "de",
|
||||
alias: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "p1",
|
||||
projectId: "proj1",
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
expect(isRTLLanguage(survey, "ar")).toBe(true);
|
||||
expect(isRTLLanguage(survey, "en")).toBe(false);
|
||||
|
||||
expect(getWebAppLocale("default", survey)).toBe("de-DE");
|
||||
});
|
||||
|
||||
test("checks content when no languages configured", () => {
|
||||
const element = {
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "مرحبا" },
|
||||
required: false,
|
||||
} as unknown as TSurveyElement;
|
||||
const block = { id: "b1", name: "Block", elements: [element] } as TSurveyBlock;
|
||||
expect(isRTLLanguage(createJsSurvey([], [block]), "default")).toBe(true);
|
||||
});
|
||||
|
||||
test("checks welcomeCard headline when enabled and no languages", () => {
|
||||
const survey = {
|
||||
...createJsSurvey([], []),
|
||||
welcomeCard: { enabled: true, headline: { default: "مرحبا" } },
|
||||
} as unknown as TJsEnvironmentStateSurvey;
|
||||
expect(isRTLLanguage(survey, "default")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when no languages and no headlines found", () => {
|
||||
const element = { id: "q1", type: TSurveyElementTypeEnum.OpenText, headline: {}, required: false };
|
||||
const block = { id: "b1", name: "Block", elements: [element] } as TSurveyBlock;
|
||||
expect(isRTLLanguage(createJsSurvey([], [block]), "default")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getElementsFromSurveyBlocks", () => {
|
||||
test("extracts elements from blocks", () => {
|
||||
const el1 = {
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: false,
|
||||
} as unknown as TSurveyElement;
|
||||
const el2 = {
|
||||
id: "q2",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q2" },
|
||||
required: false,
|
||||
} as unknown as TSurveyElement;
|
||||
const block = { id: "b1", name: "Block", elements: [el1, el2] } as TSurveyBlock;
|
||||
const result = getElementsFromSurveyBlocks([block]);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe("q1");
|
||||
test("falls back to en-US when language is not supported", () => {
|
||||
const survey = createMockSurvey();
|
||||
expect(getWebAppLocale("default", survey)).toBe("en-US");
|
||||
expect(getWebAppLocale("xx", survey)).toBe("en-US");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,60 +1,5 @@
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export function isRTL(text: string): boolean {
|
||||
const rtlCharRegex = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||
return rtlCharRegex.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of RTL language codes
|
||||
*/
|
||||
const RTL_LANGUAGES = ["ar", "ar-SA", "ar-EG", "ar-AE", "ar-MA", "he", "fa", "ur"];
|
||||
|
||||
/**
|
||||
* Returns true if the language code represents an RTL language.
|
||||
* @param survey The survey to test
|
||||
* @param languageCode The language code to test (e.g., "ar", "ar-SA", "he")
|
||||
*/
|
||||
export function isRTLLanguage(survey: TJsEnvironmentStateSurvey, languageCode: string): boolean {
|
||||
if (survey.languages.length === 0) {
|
||||
if (survey.welcomeCard.enabled) {
|
||||
const welcomeCardHeadline = survey.welcomeCard.headline?.[languageCode];
|
||||
if (welcomeCardHeadline) {
|
||||
return isRTL(welcomeCardHeadline);
|
||||
}
|
||||
}
|
||||
|
||||
const questions = getElementsFromSurveyBlocks(survey.blocks);
|
||||
for (const question of questions) {
|
||||
const questionHeadline = question.headline[languageCode];
|
||||
|
||||
// the first non-empty question headline is the survey direction
|
||||
if (questionHeadline) {
|
||||
return isRTL(questionHeadline);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
const code =
|
||||
languageCode === "default"
|
||||
? survey.languages.find((language) => language.default)?.language.code
|
||||
: languageCode;
|
||||
const baseCode = code?.split("-")[0].toLowerCase() ?? "en";
|
||||
return RTL_LANGUAGES.some((rtl) => rtl.toLowerCase().startsWith(baseCode));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a flat array of elements from the survey's blocks structure.
|
||||
* @param blocks The blocks array
|
||||
* @returns An array of TSurveyElement (pure elements without block-level properties)
|
||||
*/
|
||||
export const getElementsFromSurveyBlocks = (blocks: TSurveyBlock[]): TSurveyElement[] =>
|
||||
blocks.flatMap((block) => block.elements);
|
||||
|
||||
/**
|
||||
* Maps survey language codes to web app locale codes.
|
||||
* Falls back to "en-US" if the language is not available in web app locales.
|
||||
|
||||
@@ -13,7 +13,6 @@ interface ClientLogoProps {
|
||||
projectLogo: Project["logo"] | null;
|
||||
surveyLogo?: TLogo | null;
|
||||
previewSurvey?: boolean;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export const ClientLogo = ({
|
||||
@@ -21,23 +20,13 @@ export const ClientLogo = ({
|
||||
projectLogo,
|
||||
surveyLogo,
|
||||
previewSurvey = false,
|
||||
dir = "auto",
|
||||
}: ClientLogoProps) => {
|
||||
const { t } = useTranslation();
|
||||
const logoToUse = surveyLogo?.url ? surveyLogo : projectLogo;
|
||||
|
||||
let positionClasses = "";
|
||||
if (!previewSurvey) {
|
||||
if (dir === "rtl") {
|
||||
positionClasses = "top-3 right-3 md:top-7 md:right-7";
|
||||
} else {
|
||||
positionClasses = "top-3 left-3 md:top-7 md:left-7";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(positionClasses, "group absolute z-0 rounded-lg")}
|
||||
className={cn(previewSurvey ? "" : "top-3 left-3 md:top-7 md:left-7", "group absolute z-0 rounded-lg")}
|
||||
style={{ backgroundColor: logoToUse?.bgColor }}>
|
||||
{previewSurvey && environmentId && (
|
||||
<Link
|
||||
|
||||
@@ -304,7 +304,7 @@ function ListVariant({
|
||||
aria-invalid={Boolean(errorMessage)}
|
||||
/>
|
||||
<span
|
||||
className={cn("mx-3", optionLabelClassName)}
|
||||
className={cn("mr-3 ml-3", optionLabelClassName)}
|
||||
style={{ fontSize: "var(--fb-option-font-size)" }}>
|
||||
{option.label}
|
||||
</span>
|
||||
@@ -336,7 +336,7 @@ function ListVariant({
|
||||
aria-invalid={Boolean(errorMessage)}
|
||||
/>
|
||||
<span
|
||||
className={cn("mx-3 grow", optionLabelClassName)}
|
||||
className={cn("mr-3 ml-3 grow", optionLabelClassName)}
|
||||
style={{ fontSize: "var(--fb-option-font-size)" }}>
|
||||
{otherOptionLabel}
|
||||
</span>
|
||||
@@ -385,7 +385,7 @@ function ListVariant({
|
||||
aria-invalid={Boolean(errorMessage)}
|
||||
/>
|
||||
<span
|
||||
className={cn("mx-3", optionLabelClassName)}
|
||||
className={cn("mr-3 ml-3", optionLabelClassName)}
|
||||
style={{ fontSize: "var(--fb-option-font-size)" }}>
|
||||
{option.label}
|
||||
</span>
|
||||
|
||||
@@ -60,6 +60,26 @@ interface RankingItemProps {
|
||||
dir?: TextDirection;
|
||||
}
|
||||
|
||||
function getTopButtonRadiusClass(isFirst: boolean, dir?: TextDirection): string {
|
||||
if (isFirst) {
|
||||
return "cursor-not-allowed opacity-30";
|
||||
}
|
||||
if (dir === "rtl") {
|
||||
return "rounded-tl-md";
|
||||
}
|
||||
return "rounded-tr-md";
|
||||
}
|
||||
|
||||
function getBottomButtonRadiusClass(isLast: boolean, dir?: TextDirection): string {
|
||||
if (isLast) {
|
||||
return "cursor-not-allowed opacity-30";
|
||||
}
|
||||
if (dir === "rtl") {
|
||||
return "rounded-bl-md";
|
||||
}
|
||||
return "rounded-br-md";
|
||||
}
|
||||
|
||||
function RankingItem({
|
||||
item,
|
||||
rankedIds,
|
||||
@@ -74,11 +94,21 @@ function RankingItem({
|
||||
const isLast = isRanked && rankIndex === rankedIds.length - 1;
|
||||
const displayNumber = isRanked ? rankIndex + 1 : undefined;
|
||||
|
||||
// RTL-aware padding class
|
||||
const paddingClass = dir === "rtl" ? "pr-3" : "pl-3";
|
||||
|
||||
// RTL-aware border class for control buttons
|
||||
const borderClass = dir === "rtl" ? "border-r" : "border-l";
|
||||
|
||||
// RTL-aware border radius classes for control buttons
|
||||
const topButtonRadiusClass = getTopButtonRadiusClass(isFirst, dir);
|
||||
const bottomButtonRadiusClass = getBottomButtonRadiusClass(isLast, dir);
|
||||
|
||||
return (
|
||||
<div
|
||||
dir={dir}
|
||||
className={cn(
|
||||
"rounded-option flex h-12 cursor-pointer items-center border px-3 transition-all",
|
||||
"rounded-option flex h-12 cursor-pointer items-center border transition-all",
|
||||
paddingClass,
|
||||
"bg-option-bg border-option-border",
|
||||
"hover:bg-option-hover-bg focus-within:border-brand focus-within:bg-option-selected-bg focus-within:shadow-sm",
|
||||
isRanked && "bg-option-selected-bg border-brand",
|
||||
@@ -108,14 +138,16 @@ function RankingItem({
|
||||
)}>
|
||||
{displayNumber}
|
||||
</span>
|
||||
<span className="font-option text-option font-option-weight text-option-label shrink grow text-start">
|
||||
<span
|
||||
className="font-option text-option font-option-weight text-option-label shrink grow text-start"
|
||||
dir={dir}>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Up/Down buttons for ranked items */}
|
||||
{isRanked ? (
|
||||
<div className={cn("border-option-border -mx-3 flex h-full grow-0 flex-col")} dir={dir}>
|
||||
<div className={cn("border-option-border flex h-full grow-0 flex-col", borderClass)}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={isFirst ? -1 : 0}
|
||||
@@ -125,7 +157,10 @@ function RankingItem({
|
||||
}}
|
||||
disabled={isFirst || disabled}
|
||||
aria-label={`Move ${item.label} up`}
|
||||
className={cn("flex flex-1 items-center justify-center px-2 transition-colors")}>
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-center px-2 transition-colors",
|
||||
topButtonRadiusClass
|
||||
)}>
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
@@ -138,7 +173,8 @@ function RankingItem({
|
||||
disabled={isLast || disabled}
|
||||
aria-label={`Move ${item.label} down`}
|
||||
className={cn(
|
||||
"border-option-border flex flex-1 items-center justify-center border-t px-2 transition-colors"
|
||||
"border-option-border flex flex-1 items-center justify-center border-t px-2 transition-colors",
|
||||
bottomButtonRadiusClass
|
||||
)}>
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -225,6 +261,7 @@ function Ranking({
|
||||
{/* Ranking Options */}
|
||||
<div className="relative">
|
||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||
|
||||
<fieldset className="w-full" dir={dir}>
|
||||
<legend className="sr-only">Ranking options</legend>
|
||||
<div className="space-y-2" ref={parent as React.Ref<HTMLDivElement>}>
|
||||
|
||||
@@ -244,7 +244,6 @@ function SingleSelect({
|
||||
return (
|
||||
<label
|
||||
key={option.id}
|
||||
dir={dir}
|
||||
htmlFor={optionId}
|
||||
className={cn(getOptionContainerClassName(isSelected), isSelected && "z-10")}>
|
||||
<span className="flex items-center">
|
||||
@@ -255,7 +254,7 @@ function SingleSelect({
|
||||
aria-required={required}
|
||||
/>
|
||||
<span
|
||||
className={cn("mx-3 grow", optionLabelClassName)}
|
||||
className={cn("mr-3 ml-3 grow", optionLabelClassName)}
|
||||
style={{ fontSize: "var(--fb-option-font-size)" }}>
|
||||
{option.label}
|
||||
</span>
|
||||
@@ -266,7 +265,6 @@ function SingleSelect({
|
||||
{hasOtherOption && otherOptionId ? (
|
||||
<label
|
||||
htmlFor={`${inputId}-${otherOptionId}`}
|
||||
dir={dir}
|
||||
className={cn(getOptionContainerClassName(isOtherSelected), isOtherSelected && "z-10")}>
|
||||
<span className="flex items-center">
|
||||
<RadioGroupItem
|
||||
@@ -306,7 +304,6 @@ function SingleSelect({
|
||||
<label
|
||||
key={option.id}
|
||||
htmlFor={optionId}
|
||||
dir={dir}
|
||||
className={cn(getOptionContainerClassName(isSelected), isSelected && "z-10")}>
|
||||
<span className="flex items-center">
|
||||
<RadioGroupItem
|
||||
@@ -316,7 +313,7 @@ function SingleSelect({
|
||||
aria-required={required}
|
||||
/>
|
||||
<span
|
||||
className={cn("mx-3 grow", optionLabelClassName)}
|
||||
className={cn("mr-3 ml-3 grow", optionLabelClassName)}
|
||||
style={{ fontSize: "var(--fb-option-font-size)" }}>
|
||||
{option.label}
|
||||
</span>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { DateElement as SurveyUIDateElement } from "@formbricks/survey-ui";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyDateElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
|
||||
@@ -17,8 +16,6 @@ interface DateElementProps {
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
autoFocusEnabled: boolean;
|
||||
currentElementId: string;
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function DateElement({
|
||||
@@ -29,8 +26,6 @@ export function DateElement({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentElementId,
|
||||
surveyLanguages,
|
||||
dir = "auto",
|
||||
}: Readonly<DateElementProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
@@ -83,12 +78,7 @@ export function DateElement({
|
||||
required={element.required}
|
||||
requiredLabel={t("common.required")}
|
||||
errorMessage={errorMessage}
|
||||
locale={
|
||||
languageCode === "default"
|
||||
? surveyLanguages.find((language) => language.default)?.language.code
|
||||
: languageCode
|
||||
}
|
||||
dir={dir}
|
||||
locale={languageCode}
|
||||
imageUrl={element.imageUrl}
|
||||
videoUrl={element.videoUrl}
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,6 @@ interface MatrixElementProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
currentElementId: string;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function MatrixElement({
|
||||
@@ -26,7 +25,6 @@ export function MatrixElement({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentElementId,
|
||||
dir = "auto",
|
||||
}: Readonly<MatrixElementProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
@@ -144,7 +142,6 @@ export function MatrixElement({
|
||||
return (
|
||||
<form key={element.id} onSubmit={handleSubmit} className="w-full">
|
||||
<Matrix
|
||||
dir={dir}
|
||||
elementId={element.id}
|
||||
inputId={element.id}
|
||||
headline={getLocalizedValue(element.headline, languageCode)}
|
||||
|
||||
@@ -16,7 +16,6 @@ interface RankingElementProps {
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
autoFocusEnabled: boolean;
|
||||
currentElementId: string;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
}
|
||||
|
||||
export function RankingElement({
|
||||
@@ -27,7 +26,6 @@ export function RankingElement({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentElementId,
|
||||
dir = "auto",
|
||||
}: Readonly<RankingElementProps>) {
|
||||
const { t } = useTranslation();
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
@@ -130,7 +128,6 @@ export function RankingElement({
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="w-full">
|
||||
<Ranking
|
||||
dir={dir}
|
||||
elementId={element.id}
|
||||
inputId={element.id}
|
||||
headline={getLocalizedValue(element.headline, languageCode)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type TSurveyMatrixElement,
|
||||
type TSurveyRankingElement,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { ElementConditional } from "@/components/general/element-conditional";
|
||||
@@ -17,6 +16,41 @@ import { ScrollableContainer } from "@/components/wrappers/scrollable-container"
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Safely calls requestSubmit on a form element with fallback for browsers
|
||||
* that don't support it (e.g., Mobile Safari 15.5)
|
||||
*/
|
||||
const safeRequestSubmit = (form: HTMLFormElement | null | undefined): void => {
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if requestSubmit is available
|
||||
if (typeof form.requestSubmit === "function") {
|
||||
try {
|
||||
form.requestSubmit();
|
||||
} catch (error) {
|
||||
// Fallback if requestSubmit throws an error
|
||||
console.warn("[Formbricks] form.requestSubmit() failed, using fallback:", error);
|
||||
dispatchSubmitEvent(form);
|
||||
}
|
||||
} else {
|
||||
// Fallback for browsers that don't support requestSubmit
|
||||
dispatchSubmitEvent(form);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fallback method to trigger form validation by dispatching a submit event
|
||||
*/
|
||||
const dispatchSubmitEvent = (form: HTMLFormElement): void => {
|
||||
const submitEvent = new Event("submit", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
form.dispatchEvent(submitEvent);
|
||||
};
|
||||
|
||||
interface BlockConditionalProps {
|
||||
block: TSurveyBlock;
|
||||
value: TResponseData;
|
||||
@@ -37,7 +71,6 @@ interface BlockConditionalProps {
|
||||
onOpenExternalURL?: (url: string) => void | Promise<void>;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
fullSizeCards: boolean;
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
}
|
||||
|
||||
export function BlockConditional({
|
||||
@@ -60,8 +93,7 @@ export function BlockConditional({
|
||||
onOpenExternalURL,
|
||||
dir,
|
||||
fullSizeCards,
|
||||
surveyLanguages,
|
||||
}: Readonly<BlockConditionalProps>) {
|
||||
}: BlockConditionalProps) {
|
||||
// Track the current element being filled (for TTC tracking)
|
||||
const [currentElementId, setCurrentElementId] = useState(block.elements[0]?.id);
|
||||
|
||||
@@ -144,7 +176,7 @@ export function BlockConditional({
|
||||
response.length < rankingElement.choices.length);
|
||||
|
||||
if (hasIncompleteRanking) {
|
||||
form.requestSubmit();
|
||||
safeRequestSubmit(form);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -177,7 +209,7 @@ export function BlockConditional({
|
||||
element.type === TSurveyElementTypeEnum.ContactInfo
|
||||
) {
|
||||
if (!form.checkValidity()) {
|
||||
form.requestSubmit();
|
||||
safeRequestSubmit(form);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -194,14 +226,14 @@ export function BlockConditional({
|
||||
response &&
|
||||
hasUnansweredRows(response, element)
|
||||
) {
|
||||
form.requestSubmit();
|
||||
safeRequestSubmit(form);
|
||||
return false;
|
||||
}
|
||||
|
||||
// For other element types, check if required fields are empty
|
||||
// CTA elements should not block navigation even if marked required (as they are informational)
|
||||
if (element.type !== TSurveyElementTypeEnum.CTA && element.required && isEmptyResponse(response)) {
|
||||
form.requestSubmit();
|
||||
safeRequestSubmit(form);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -232,9 +264,7 @@ export function BlockConditional({
|
||||
// Call each form's submit method to trigger TTC calculation
|
||||
block.elements.forEach((element) => {
|
||||
const form = elementFormRefs.current.get(element.id);
|
||||
if (form) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
safeRequestSubmit(form);
|
||||
});
|
||||
|
||||
// Collect TTC from the ref (populated synchronously by form submissions)
|
||||
@@ -293,7 +323,6 @@ export function BlockConditional({
|
||||
return (
|
||||
<div key={element.id}>
|
||||
<ElementConditional
|
||||
surveyLanguages={surveyLanguages}
|
||||
element={element}
|
||||
value={value[element.id]}
|
||||
onChange={(responseData) => handleElementChange(element.id, responseData)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { type TResponseData, type TResponseDataValue, type TResponseTtc } from "
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import { type TSurveyElement, type TSurveyElementChoice } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { AddressElement } from "@/components/elements/address-element";
|
||||
import { CalElement } from "@/components/elements/cal-element";
|
||||
import { ConsentElement } from "@/components/elements/consent-element";
|
||||
@@ -37,7 +36,6 @@ interface ElementConditionalProps {
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
formRef?: (ref: HTMLFormElement | null) => void; // Callback to expose the form element
|
||||
onTtcCollect?: (elementId: string, ttc: number) => void; // Callback to collect TTC synchronously
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
}
|
||||
|
||||
export function ElementConditional({
|
||||
@@ -55,8 +53,7 @@ export function ElementConditional({
|
||||
dir,
|
||||
formRef,
|
||||
onTtcCollect,
|
||||
surveyLanguages,
|
||||
}: Readonly<ElementConditionalProps>) {
|
||||
}: ElementConditionalProps) {
|
||||
// Ref to the container div, used to find and expose the form element inside
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -227,8 +224,6 @@ export function ElementConditional({
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
surveyLanguages={surveyLanguages}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.PictureSelection:
|
||||
@@ -285,7 +280,6 @@ export function ElementConditional({
|
||||
ttc={ttc}
|
||||
setTtc={wrappedSetTtc}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Address:
|
||||
@@ -305,7 +299,6 @@ export function ElementConditional({
|
||||
case TSurveyElementTypeEnum.Ranking:
|
||||
return (
|
||||
<RankingElement
|
||||
dir={dir}
|
||||
element={element}
|
||||
value={Array.isArray(value) ? getResponseValueForRankingElement(value, element.choices) : []}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { mixColor } from "@/lib/color";
|
||||
import { getI18nLanguage } from "@/lib/i18n-utils";
|
||||
import i18n from "@/lib/i18n.config";
|
||||
import { useClickOutside } from "@/lib/use-click-outside-hook";
|
||||
import { cn, isRTLLanguage } from "@/lib/utils";
|
||||
import { checkIfSurveyIsRTL, cn } from "@/lib/utils";
|
||||
|
||||
interface LanguageSwitchProps {
|
||||
survey: TJsEnvironmentStateSurvey;
|
||||
@@ -59,7 +59,7 @@ export function LanguageSwitch({
|
||||
handleI18nLanguage(calculatedLanguageCode);
|
||||
|
||||
if (setDir) {
|
||||
const calculateDir = isRTLLanguage(survey, calculatedLanguageCode) ? "rtl" : "auto";
|
||||
const calculateDir = checkIfSurveyIsRTL(survey, calculatedLanguageCode) ? "rtl" : "auto";
|
||||
setDir?.(calculateDir);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { isRTLLanguage } from "@/lib/utils";
|
||||
import { checkIfSurveyIsRTL } from "@/lib/utils";
|
||||
import { SurveyContainer } from "../wrappers/survey-container";
|
||||
import { Survey } from "./survey";
|
||||
|
||||
@@ -8,11 +8,12 @@ export function RenderSurvey(props: SurveyContainerProps) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const onFinishedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isRTL = isRTLLanguage(props.survey, props.languageCode);
|
||||
|
||||
const isRTL = checkIfSurveyIsRTL(props.survey, props.languageCode);
|
||||
const [dir, setDir] = useState<"ltr" | "rtl" | "auto">(isRTL ? "rtl" : "auto");
|
||||
|
||||
useEffect(() => {
|
||||
const isRTL = isRTLLanguage(props.survey, props.languageCode);
|
||||
const isRTL = checkIfSurveyIsRTL(props.survey, props.languageCode);
|
||||
setDir(isRTL ? "rtl" : "auto");
|
||||
}, [props.languageCode, props.survey]);
|
||||
|
||||
|
||||
@@ -801,7 +801,6 @@ export function Survey({
|
||||
return (
|
||||
Boolean(block) && (
|
||||
<BlockConditional
|
||||
surveyLanguages={localSurvey.languages}
|
||||
key={block.id}
|
||||
surveyId={localSurvey.id}
|
||||
block={{
|
||||
|
||||
@@ -30,7 +30,7 @@ interface WelcomeCardProps {
|
||||
|
||||
function TimerIcon() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mr-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@@ -177,16 +177,18 @@ export function WelcomeCard({
|
||||
</div>
|
||||
{timeToFinish && !showResponseCount ? (
|
||||
<div
|
||||
className="text-subheading my-4 flex items-center space-x-1"
|
||||
className="text-subheading my-4 flex items-center"
|
||||
data-testid="fb__surveys__welcome-card__time-display">
|
||||
<TimerIcon />
|
||||
<p className="pt-1 text-xs">
|
||||
<span>{calculateTimeToComplete()} </span>
|
||||
<span>
|
||||
{calculateTimeToComplete()}{" "}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
|
||||
<div className="text-subheading my-4 flex items-center space-x-1">
|
||||
<div className="text-subheading my-4 flex items-center">
|
||||
<UsersIcon />
|
||||
<p className="pt-1 text-xs">
|
||||
<span data-testid="fb__surveys__welcome-card__response-count">
|
||||
@@ -196,10 +198,12 @@ export function WelcomeCard({
|
||||
</div>
|
||||
) : null}
|
||||
{timeToFinish && showResponseCount ? (
|
||||
<div className="text-subheading my-4 flex items-center space-x-1">
|
||||
<div className="text-subheading my-4 flex items-center">
|
||||
<TimerIcon />
|
||||
<p className="pt-1 text-xs" data-testid="fb__surveys__welcome-card__info-text-test">
|
||||
<span>{calculateTimeToComplete()} </span>
|
||||
<span>
|
||||
{calculateTimeToComplete()}{" "}
|
||||
</span>
|
||||
<span data-testid="fb__surveys__welcome-card__response-count">
|
||||
{responseCount && responseCount > 3
|
||||
? `⋅ ${t("common.people_responded", { count: responseCount })}`
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
getMimeType,
|
||||
getShuffledChoicesIds,
|
||||
getShuffledRowIndices,
|
||||
isRTL,
|
||||
isRTLLanguage,
|
||||
} from "./utils";
|
||||
|
||||
// Mock crypto.getRandomValues for deterministic shuffle tests
|
||||
@@ -329,83 +327,3 @@ describe("findBlockByElementId", () => {
|
||||
expect(block).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRTL", () => {
|
||||
test("returns true for RTL text", () => {
|
||||
expect(isRTL("مرحبا")).toBe(true);
|
||||
expect(isRTL("שלום")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for LTR text", () => {
|
||||
expect(isRTL("Hello")).toBe(false);
|
||||
expect(isRTL("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRTLLanguage", () => {
|
||||
test("returns true for RTL language codes when multi-language enabled", () => {
|
||||
const survey: TJsEnvironmentStateSurvey = {
|
||||
...baseMockSurvey,
|
||||
languages: [
|
||||
{
|
||||
language: {
|
||||
id: "l1",
|
||||
code: "ar",
|
||||
alias: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "p1",
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
} as TJsEnvironmentStateSurvey;
|
||||
expect(isRTLLanguage(survey, "ar")).toBe(true);
|
||||
expect(isRTLLanguage(survey, "he")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for LTR language codes", () => {
|
||||
const survey: TJsEnvironmentStateSurvey = {
|
||||
...baseMockSurvey,
|
||||
languages: [
|
||||
{
|
||||
language: {
|
||||
id: "l1",
|
||||
code: "en",
|
||||
alias: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "p1",
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
} as TJsEnvironmentStateSurvey;
|
||||
expect(isRTLLanguage(survey, "en")).toBe(false);
|
||||
});
|
||||
|
||||
test("checks survey content when no languages configured", () => {
|
||||
const survey: TJsEnvironmentStateSurvey = {
|
||||
...baseMockSurvey,
|
||||
blocks: [
|
||||
{
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "مرحبا" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as TJsEnvironmentStateSurvey;
|
||||
expect(isRTLLanguage(survey, "default")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,43 +222,26 @@ export function isRTL(text: string): boolean {
|
||||
return rtlCharRegex.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of RTL language codes
|
||||
*/
|
||||
const RTL_LANGUAGES = ["ar", "ar-SA", "ar-EG", "ar-AE", "ar-MA", "he", "fa", "ur"];
|
||||
|
||||
/**
|
||||
* Returns true if the language code represents an RTL language.
|
||||
* @param languageCode The language code to test (e.g., "ar", "ar-SA", "he")
|
||||
*/
|
||||
export function isRTLLanguage(survey: TJsEnvironmentStateSurvey, languageCode: string): boolean {
|
||||
if (survey.languages.length === 0) {
|
||||
if (survey.welcomeCard.enabled) {
|
||||
const welcomeCardHeadline = survey.welcomeCard.headline?.[languageCode];
|
||||
if (welcomeCardHeadline) {
|
||||
return isRTL(welcomeCardHeadline);
|
||||
}
|
||||
export const checkIfSurveyIsRTL = (survey: TJsEnvironmentStateSurvey, languageCode: string): boolean => {
|
||||
if (survey.welcomeCard.enabled) {
|
||||
const welcomeCardHeadline = survey.welcomeCard.headline?.[languageCode];
|
||||
if (welcomeCardHeadline) {
|
||||
return isRTL(welcomeCardHeadline);
|
||||
}
|
||||
|
||||
const questions = getElementsFromSurveyBlocks(survey.blocks);
|
||||
for (const question of questions) {
|
||||
const questionHeadline = question.headline[languageCode];
|
||||
|
||||
// the first non-empty question headline is the survey direction
|
||||
if (questionHeadline) {
|
||||
return isRTL(questionHeadline);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
const code =
|
||||
languageCode === "default"
|
||||
? survey.languages.find((language) => language.default)?.language.code
|
||||
: languageCode;
|
||||
const baseCode = code?.split("-")[0].toLowerCase() ?? "en";
|
||||
return RTL_LANGUAGES.some((rtl) => rtl.toLowerCase().startsWith(baseCode));
|
||||
}
|
||||
}
|
||||
|
||||
const questions = getElementsFromSurveyBlocks(survey.blocks);
|
||||
for (const question of questions) {
|
||||
const questionHeadline = question.headline[languageCode];
|
||||
|
||||
// the first non-empty question headline is the survey direction
|
||||
if (questionHeadline) {
|
||||
return isRTL(questionHeadline);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Derives a flat array of elements from the survey's blocks structure.
|
||||
|
||||
Generated
+1
@@ -10249,6 +10249,7 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
|
||||
|
||||
tarn@3.0.2:
|
||||
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
|
||||
|
||||
Reference in New Issue
Block a user