New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
+14 -7
View File
@@ -14,13 +14,20 @@ import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.sv
import styles from "./styles.module.css";
export type AccordionVariant =
| "default"
| "text"
| "paper"
| "minimal"
| "solid";
export interface AccordionProps extends HTMLAttributes<HTMLElement> {
variant?: "default" | "text" | "paper";
variant?: AccordionVariant;
summary: ReactNode;
description?: ReactNode;
size?: "small" | "medium";
size?: "xx-small" | "x-small" | "small" | "medium";
forceShow?: boolean;
useTheme?: boolean;
themed?: boolean;
resetOpen?: boolean;
override?: boolean | null;
handleIsOpen?: (id: string, isOpen: boolean) => void;
@@ -40,8 +47,7 @@ export const Accordion: FC<AccordionProps> = ({
variant = "default",
children,
forceShow = false,
// style,
useTheme,
themed,
resetOpen,
handleIsOpen,
override = null,
@@ -101,7 +107,7 @@ export const Accordion: FC<AccordionProps> = ({
styles.details,
styles[size],
styles[variant],
useTheme && styles.theme,
themed && styles.themed,
showAlert && styles.alert,
className,
])}
@@ -113,6 +119,7 @@ export const Accordion: FC<AccordionProps> = ({
className={styles.summary}
onClick={handleToggle}
ref={summaryRef}
data-testid="accordion-summary"
>
{summaryImage && (
<img
@@ -123,7 +130,7 @@ export const Accordion: FC<AccordionProps> = ({
/>
)}
<div className={styles.heading}>
<div>
<div className={styles.summaryContent}>
{summary}
{summaryMetaItems && (
<div className={styles.metaItems}>
@@ -0,0 +1,41 @@
import clsx from "clsx";
import { FC, HTMLAttributes, ReactNode } from "react";
import styles from "./styles.module.css";
export interface AccordionSummaryProps
extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
imageUrl?: string;
title: ReactNode;
description?: string | string[];
callout?: ReactNode;
}
export const AccordionSummary: FC<AccordionSummaryProps> = ({
callout,
className,
description,
imageUrl,
title,
...props
}) => {
const buildDescription = () => {
if (Array.isArray(description)) {
return description.join(" • ");
}
return description;
};
return (
<div className={clsx([styles.accordionSummary, className])} {...props}>
{imageUrl && <img className={styles.avatar} src={imageUrl} alt="" />}
<div className={styles.text}>
<div className={styles.title}>{title}</div>
{description && (
<p className={styles.description}>{buildDescription()}</p>
)}
</div>
{callout && <div className={styles.callout}>{callout}</div>}
</div>
);
};
+11 -2
View File
@@ -1,5 +1,6 @@
import clsx from "clsx";
import { FC } from "react";
import Color from "color";
import { CSSProperties, FC } from "react";
import {
Button as TtuiButton,
@@ -33,7 +34,14 @@ export const Button: FC<ButtonProps> = ({
const isCustomSize = size === "xx-small";
const isCustomVariant = variant === "builder" || variant === "builder-text";
const isCustomColor = color === "builder-green";
const { isDarkMode } = useCharacterTheme();
const { isDarkMode, themeColor } = useCharacterTheme();
let overrideStyles: CSSProperties = {};
if (isDarkMode && themed && variant === "solid" && size === "xx-small") {
overrideStyles = {
// Determine if the theme color is darker or lighter
color: Color(themeColor).isDark() ? "" : "black",
};
}
return (
<TtuiButton
@@ -53,6 +61,7 @@ export const Button: FC<ButtonProps> = ({
variant={!isCustomVariant ? variant : undefined}
// Children are included in props
color={!isCustomColor ? color : undefined}
style={overrideStyles}
{...props}
/>
);
+17 -17
View File
@@ -12,8 +12,7 @@ interface CheckboxProps extends TtuiCheckboxProps {
themed?: boolean;
darkMode?: boolean;
variant?: "default" | "builder" | "sidebar";
onClick?: (isEnabled: boolean) => void;
onChangePromise?: (
onChangeConfirm?: (
newIsEnabled: boolean,
accept: () => void,
reject: () => void
@@ -26,51 +25,52 @@ export const Checkbox: FC<CheckboxProps> = ({
variant = "default",
darkMode,
checked = false,
onChangePromise,
onClick,
onChangeConfirm,
onChange,
disabled,
...props
}) => {
const [isChecked, setIsChecked] = useState(checked);
useEffect(() => {
setIsChecked(checked);
}, [checked]);
const handleChange = (evt: ChangeEvent<HTMLInputElement>): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
const newCheckedState = !isChecked;
const newCheckedState = evt.target.checked;
// promise-based logic
if (onChangePromise) {
onChangePromise(
if (onChangeConfirm) {
onChangeConfirm(
newCheckedState,
() => {
// Promise accepted, update the state
setIsChecked(newCheckedState);
},
() => {
// Promise rejected, do nothing
// Promise rejected, reset the checkbox to its previous state
setIsChecked(!newCheckedState);
}
);
} else {
// Update the state immediately if no promise handling - only IF onClick is provided
if (onClick) {
onClick(newCheckedState);
if (onChange) {
onChange(evt);
setIsChecked(newCheckedState);
}
}
};
useEffect(() => {
setIsChecked(checked);
}, [checked]);
return (
<TtuiCheckbox
className={clsx([
styles.checkbox,
styles[variant],
themed && styles.themed,
darkMode && styles[darkMode ? "darkMode" : "lightMode"],
className,
variant === "default" && styles.default,
variant === "builder" && styles.builder,
])}
onChange={handleChange}
aria-pressed={isChecked}
@@ -0,0 +1,195 @@
import clsx from "clsx";
import { orderBy } from "lodash";
import { ChangeEvent, FC, HTMLAttributes, ReactNode } from "react";
import { Accordion, AccordionProps } from "../Accordion";
import { Checkbox } from "../Checkbox";
import styles from "./styles.module.css";
export interface CheckboxInfo {
description?: string;
disabled?: boolean;
initiallyEnabled: boolean;
label: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeConfirm?: (
newIsEnabled: boolean,
accept: () => void,
reject: () => void
) => void;
sortOrder?: number;
value?: string | number;
}
interface CheckboxGroupProps
extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
accordionDefaultOpen?: boolean;
accordionSummary?: string;
checkboxes: Array<CheckboxInfo>;
disabled?: boolean;
description?: ReactNode;
name: string;
onToggleAll?: (e: ChangeEvent<HTMLInputElement>) => void;
showAccordion?: boolean;
themed?: boolean;
toggleAllLabel?: string;
title: ReactNode;
variant?: "default" | "inline" | "collapse";
}
export const CheckboxGroup: FC<CheckboxGroupProps> = ({
accordionSummary = "Options",
accordionDefaultOpen,
checkboxes,
className,
disabled,
description,
name,
onToggleAll,
showAccordion,
themed,
title,
toggleAllLabel = "Enable All Options",
variant = "default",
...props
}) => {
const isInlineVariant = variant === "inline";
const isCollapseVariant = variant === "collapse";
const sortedCheckboxes = orderBy(checkboxes, "sortOrder");
const allChecksEnabled = sortedCheckboxes.every(
(checkbox) => checkbox.initiallyEnabled
);
// Determine whether to render a div or an Accordion
const ContentWrapper = showAccordion ? Accordion : "div";
// Set props specific to Accordion if needed
const accordionProps = showAccordion
? {
summary: accordionSummary,
forceShow: accordionDefaultOpen,
variant: "text",
}
: {};
const checkboxList = sortedCheckboxes.map((checkbox, i) => {
const id = `${name}-${i}`;
return (
<div
key={i}
className={styles.checkboxOption}
data-testid="form-checkbox-group"
>
<Checkbox
className={styles.checkbox}
id={id}
name={name}
checked={checkbox.initiallyEnabled}
disabled={disabled || checkbox.disabled}
themed={themed}
onChange={checkbox.onChange}
onChangeConfirm={checkbox.onChangeConfirm}
value={checkbox.value}
/>
<div className={styles.optionContent}>
<label className={styles.label} htmlFor={id}>
{checkbox.label}
</label>
{checkbox.description && (
<div
className={styles.description}
dangerouslySetInnerHTML={{ __html: checkbox.description }}
/>
)}
</div>
</div>
);
});
/** Handle Collapse Variant separately since it's structure is different */
if (isCollapseVariant)
return (
<Accordion
className={clsx([
styles.checkboxGroup,
styles.collapse,
themed && styles.themed,
className,
])}
summary={<p className={styles.checkboxGroupTitle}>{title}</p>}
variant="text"
forceShow={accordionDefaultOpen}
{...props}
>
{description && (
<div className={styles.checkboxGroupDescription}>{description}</div>
)}
{onToggleAll && (
<Checkbox
className={clsx([styles.checkbox, styles.toggleAll])}
id={`${name}-toggle-all`}
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onChange={onToggleAll}
themed={themed}
label={toggleAllLabel}
/>
)}
{checkboxList}
</Accordion>
);
return (
<div
className={clsx([
styles.checkboxGroup,
styles[variant],
themed && styles.themed,
className,
])}
{...props}
>
<header>
<div className={styles.checkboxGroupHeader}>
<p className={styles.checkboxGroupTitle}>{title}</p>
{isInlineVariant && checkboxes.length < 2 && (
<Checkbox
className={styles.checkbox}
id={`${name}-0`}
name={name}
checked={checkboxes[0].initiallyEnabled}
disabled={disabled || checkboxes[0].disabled}
themed={themed}
onChange={checkboxes[0].onChange}
onChangeConfirm={checkboxes[0].onChangeConfirm}
value={checkboxes[0].value}
/>
)}
</div>
{description && (
<div className={styles.checkboxGroupDescription}>{description}</div>
)}
</header>
{!isInlineVariant && onToggleAll && (
<Checkbox
className={clsx([styles.checkbox, styles.toggleAll])}
id={`${name}-toggle-all`}
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onChange={onToggleAll}
themed={themed}
label={toggleAllLabel}
/>
)}
{!isInlineVariant && (
<ContentWrapper
className={styles.checkboxOptions}
{...(accordionProps as AccordionProps)}
>
{checkboxList}
</ContentWrapper>
)}
</div>
);
};
@@ -2,7 +2,8 @@ import clsx from "clsx";
import { FC, useEffect } from "react";
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
import { Dialog, DialogProps } from "@dndbeyond/ttui/components/Dialog";
import { Dialog, DialogProps } from "~/components/Dialog";
import { Button, ButtonProps } from "../Button";
import styles from "./styles.module.css";
+21
View File
@@ -0,0 +1,21 @@
import clsx from "clsx";
import { FC } from "react";
import {
Dialog as TtuiDialog,
DialogProps as TtuiDialogProps,
} from "@dndbeyond/ttui/components/Dialog";
import styles from "./styles.module.css";
/**
* A wrapper around the TTUI Dialog component to apply custom styles. This is necessary because the
* transform property conflicts with styles for the select component. Using this component to
* remove those styles resolves the issue.
*/
export interface DialogProps extends TtuiDialogProps {}
export const Dialog: FC<DialogProps> = ({ className, ...props }) => {
return <TtuiDialog className={clsx(styles.dialog, className)} {...props} />;
};
@@ -1,12 +1,13 @@
import { FC, HTMLAttributes } from "react";
import { useDispatch } from "react-redux";
import {
characterActions,
ChoiceUtils,
ContainerManager,
} from "@dndbeyond/character-rules-engine";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useDispatch } from "~/hooks/useDispatch";
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
import { CharClass, Choice, ClassDefinitionContract, Feat } from "~/types";
@@ -15,6 +16,7 @@ export interface FeatureChoiceProps extends HTMLAttributes<HTMLDivElement> {
charClass?: CharClass;
featsData: Feat[];
subclassData?: ClassDefinitionContract[];
itemShoppe?: ContainerManager | null;
onChoiceChange: (
choiceId: string,
type: number,
@@ -33,6 +35,7 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
choice,
featsData,
subclassData,
itemShoppe,
onChoiceChange,
className,
collapseDescription,
@@ -69,13 +72,14 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
}
};
const { description, options, featSubChoices } =
const { description, options, featSubChoices, introDescription } =
ChoiceUtils.getSortedChoiceOptionsForFeaturesInfo(
choice,
featsData,
featLookup,
charClass || null,
subclassData || [],
itemShoppe || null,
preferences,
prerequisiteData,
ruleData,
@@ -90,6 +94,7 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
options={options}
onChange={handleChoiceChange}
description={description || ""}
descriptionIntro={introDescription || ""}
choiceInfo={choiceInfo}
classId={charClass && classUtils.getId(charClass)}
showBackgroundProficiencyOptions={true}
@@ -1,5 +1,5 @@
import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState } from "react";
import { FC, HTMLAttributes, useContext, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
@@ -9,13 +9,17 @@ import {
Choice,
ChoiceUtils,
ClassDefinitionContract,
ClassFeature,
ClassFeatureUtils,
ClassUtils,
Constants,
ContainerManager,
Feat,
FeatUtils,
} from "@dndbeyond/character-rules-engine";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import { InventoryManagerContext } from "~/tools/js/Shared/managers/InventoryManagerContext";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { AppLoggerUtils } from "~/tools/js/Shared/utils";
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
@@ -35,7 +39,9 @@ export interface FeatureChoicesProps extends HTMLAttributes<HTMLDivElement> {
) => void;
collapseDescription?: boolean;
charClass?: CharClass;
classFeature?: ClassFeature;
shouldFetch?: boolean; // Optional prop to control fetching behavior
itemShoppe?: ContainerManager | null; // Optional prop for cases where the itemShoppe is fetched from the parent component (only used if the Feature Choices component is rendered multiple times in the same parent - e.g. in the Item Plans Manage Pane)
}
export const FeatureChoices: FC<FeatureChoicesProps> = ({
@@ -45,7 +51,9 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
onChoiceChange,
collapseDescription,
charClass,
classFeature,
shouldFetch = true, // Default to true if not provided
itemShoppe = null,
...props
}) => {
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
@@ -56,14 +64,20 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
apiCreatorSelectors.makeLoadAvailableSubclasses
);
const { inventoryManager } = useContext(InventoryManagerContext);
const [subclassData, setSubclassData] = useState<
Array<ClassDefinitionContract>
>([]);
const [featsData, setFeatsData] = useState<Array<Feat>>([]);
const [localItemShoppe, setLocalItemShoppe] =
useState<ContainerManager | null>(itemShoppe);
const [featLoadingStatus, setFeatLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const [subclassLoadingStatus, setSubclassLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const [itemsLoadingStatus, setItemsLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const hasFeatChoice = choices.some(
(choice) =>
@@ -77,15 +91,23 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION
);
const hasItemMappings = classFeature
? ClassFeatureUtils.getHasItemMappings(classFeature)
: false;
useEffect(() => {
const abortController = new AbortController();
if (
hasFeatChoice &&
shouldFetch &&
featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
featLoadingStatus !== DataLoadingStatusEnum.LOADED
) {
setFeatLoadingStatus(DataLoadingStatusEnum.LOADING);
loadAvailableFeats()
loadAvailableFeats({
signal: abortController.signal,
})
.then((response) => {
let featsData: Array<Feat> = [];
@@ -99,17 +121,22 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
setFeatsData(featsData);
setFeatLoadingStatus(DataLoadingStatusEnum.LOADED);
})
.catch(AppLoggerUtils.handleAdhocApiError);
.catch((e) => {
if (abortController.signal.aborted) return;
AppLoggerUtils.handleAdhocApiError(e);
});
}
if (
charClass &&
hasSubclassChoice &&
shouldFetch &&
subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
subclassLoadingStatus !== DataLoadingStatusEnum.LOADED
) {
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING);
loadAvailableSubclasses(ClassUtils.getId(charClass))
loadAvailableSubclasses(ClassUtils.getId(charClass), {
signal: abortController.signal,
})
.then((response) => {
let subclassData: Array<ClassDefinitionContract> = [];
@@ -121,17 +148,55 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
setSubclassData(subclassData);
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED);
})
.catch(AppLoggerUtils.handleAdhocApiError);
.catch((e) => {
if (abortController.signal.aborted) return;
AppLoggerUtils.handleAdhocApiError(e);
});
}
if (
localItemShoppe === null &&
hasItemMappings &&
shouldFetch &&
itemsLoadingStatus !== DataLoadingStatusEnum.LOADED
) {
if (inventoryManager) {
setItemsLoadingStatus(DataLoadingStatusEnum.LOADING);
inventoryManager
.getInventoryShoppe({
onSuccess: (shoppeContainer: ContainerManager) => {
setLocalItemShoppe(shoppeContainer);
setItemsLoadingStatus(DataLoadingStatusEnum.LOADED);
},
additionalApiConfig: {
signal: abortController.signal,
},
})
.catch((e) => {
if (abortController.signal.aborted) return;
AppLoggerUtils.handleAdhocApiError(e);
});
} else {
setItemsLoadingStatus(DataLoadingStatusEnum.LOADED);
}
}
return () => {
abortController.abort();
};
}, [
hasFeatChoice,
hasSubclassChoice,
hasItemMappings,
subclassLoadingStatus,
featLoadingStatus,
itemsLoadingStatus,
charClass,
loadAvailableFeats,
loadAvailableSubclasses,
shouldFetch,
inventoryManager,
]);
const isDataLoaded = (): boolean => {
@@ -145,6 +210,13 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
return false;
}
}
if (itemShoppe !== null) {
return true;
} else if (hasItemMappings) {
if (itemsLoadingStatus !== DataLoadingStatusEnum.LOADED) {
return false;
}
}
return true;
};
@@ -163,6 +235,7 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
charClass={charClass}
choice={choice}
featsData={featsData}
itemShoppe={localItemShoppe}
subclassData={subclassData}
className={styles.choice}
key={ChoiceUtils.getId(choice)}
@@ -0,0 +1,98 @@
import { StepType, TourContext, TourProvider } from "@reactour/tour";
import clsx from "clsx";
import { Dispatch, FC, HTMLAttributes, SetStateAction } from "react";
import QuestionIcon from "@dndbeyond/fontawesome-cache/svgs/solid/question.svg";
import { Button } from "../Button";
import styles from "./styles.module.css";
export interface GuidedTourProps extends HTMLAttributes<HTMLDivElement> {
afterOpen?: (target: Element | null) => void;
beforeClose?: (target: Element | null) => void;
cookieName?: string;
setStep?: SetStateAction<any>;
showOnFirstLoad?: boolean;
steps?: StepType[];
step?: number;
}
export const GuidedTour: FC<GuidedTourProps> = ({
afterOpen,
beforeClose,
children,
className,
cookieName = "ddbGuidedTour",
setStep,
showOnFirstLoad,
step = 0,
steps = [],
...props
}) => {
const handleBeforeClose = () => {
const html = document.querySelector("html");
if (html) html.style.overflowY = "visible";
if (beforeClose) beforeClose(null);
};
const handleAfterOpen = () => {
const html = document.querySelector("html");
if (html) html.style.overflowY = "hidden";
if (afterOpen) afterOpen(null);
};
const handleLoad = (setIsOpen: Dispatch<SetStateAction<boolean>>) => {
if (showOnFirstLoad) {
const viewedTour = localStorage.getItem(cookieName);
if (!viewedTour) {
setIsOpen(true);
localStorage.setItem(cookieName, "true");
}
}
};
const PrevButton = ({ Button, ...props }) => (
<Button {...props} kind="prev" hideArrow={step === 0} />
);
const NextButton = ({ Button, ...props }) => (
<Button
{...props}
kind="next"
hideArrow={step === (steps?.length || 0) - 1}
/>
);
return (
<div className={clsx([styles.guidedTour, className])} {...props}>
<TourProvider
steps={steps}
currentStep={step}
setCurrentStep={setStep}
afterOpen={handleAfterOpen}
beforeClose={handleBeforeClose}
showDots={false}
prevButton={PrevButton}
nextButton={NextButton}
>
<TourContext.Consumer>
{({ setIsOpen }) => (
<div onLoad={() => handleLoad(setIsOpen)}>
{children}
<Button
className={styles.tourButton}
onClick={() => setIsOpen(true)}
color="info"
size="small"
aria-label="Open guided tour"
>
<QuestionIcon />
</Button>
</div>
)}
</TourContext.Consumer>
</TourProvider>
</div>
);
};
@@ -0,0 +1,41 @@
import { useTour } from "@reactour/tour";
import clsx from "clsx";
import { FC, HTMLAttributes, ReactNode } from "react";
import { Button } from "../Button";
import styles from "./styles.module.css";
export interface GuidedTourStepProps
extends Omit<HTMLAttributes<HTMLDivElement>, "content"> {
title: string;
content: ReactNode;
showClose?: boolean;
}
export const GuidedTourStep: FC<GuidedTourStepProps> = ({
className,
content,
showClose,
title,
...props
}) => {
const { setIsOpen } = useTour();
return (
<div className={clsx([styles.guidedTourStep, className])} {...props}>
<p className={styles.title}>{title}</p>
<div className={styles.content}>{content}</div>
{showClose && (
<Button
className={styles.closeButton}
size="small"
variant="outline"
themed
onClick={() => setIsOpen(false)}
>
Start Playing!
</Button>
)}
</div>
);
};
@@ -1,3 +1,4 @@
import clsx from "clsx";
import { FC } from "react";
import { v4 as uuidv4 } from "uuid";
@@ -16,12 +17,14 @@ export interface HelperTextAccordionProps
export const HelperTextAccordion: FC<HelperTextAccordionProps> = ({
builderHelperText,
className,
...props
}) => {
return (
<>
{builderHelperText.map((helperText, idx) => (
<Accordion
className={clsx([styles.accordion, className])}
{...props}
key={uuidv4()}
summary={
+17 -22
View File
@@ -1,35 +1,30 @@
import clsx from "clsx";
import { FC, HTMLAttributes } from "react";
import { InfoItem as TtuiInfoItem } from "@dndbeyond/ttui/components/InfoItem";
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
import type { FC, HTMLAttributes } from "react";
import styles from "./styles.module.css";
interface InfoItemProps extends HTMLAttributes<HTMLElement> {
interface InfoItemProps extends HTMLAttributes<HTMLDivElement> {
label: string;
color?: "primary"; // Not needed here, but fixes ts error
inline?: boolean;
}
/**
* This component is an attribute for a given item, spell, or other entity.
* The InfoItemList component is a customized version of the InfoItem component from
* the @dndbeyond/ttui library.
**/
*/
export const InfoItem: FC<InfoItemProps> = ({
className,
onClick,
inline = true,
label,
children,
...props
}) => {
const handleClick = useUnpropagatedClick(onClick);
return (
<TtuiInfoItem
className={clsx([styles.item, className])}
onClick={onClick ? handleClick : undefined}
{...props}
/>
);
};
}) => (
<div
className={clsx([styles.item, inline && styles.inline, className])}
role="listitem"
{...props}
>
<p className={styles.label}>{label}</p>
<p className={styles.value}>{children}</p>
</div>
);
@@ -0,0 +1,107 @@
import clsx from "clsx";
import { FC, HTMLAttributes, MouseEvent } from "react";
import {
AccessUtils,
CampaignUtils,
FormatUtils,
Infusion,
InfusionUtils,
RuleDataUtils,
Constants,
} from "@dndbeyond/character-rules-engine/es";
import { MarketplaceCta } from "~/components/MarketplaceCta";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import Snippet from "../../tools/js/smartComponents/Snippet";
import styles from "./styles.module.css";
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
infusion: Infusion;
onClick?: (infusion: Infusion) => void;
}
export const InfusionPreview: FC<Props> = ({
infusion,
onClick,
className,
...props
}) => {
const { ruleData, snippetData, proficiencyBonus, partyInfo } =
useCharacterEngine();
const itemMappingId = InfusionUtils.getInventoryMappingId(infusion);
const partyRestrictions = partyInfo
? CampaignUtils.getPartyInventoryRestrictions(partyInfo)
: [];
const isInfusedItemInParty = partyRestrictions.some(
(restriction) =>
restriction.type === Constants.PartyRestrictionTypeEnum.INFUSION &&
restriction.inventoryMappingId === itemMappingId
);
const handleClick = (evt: MouseEvent): void => {
if (onClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onClick(infusion);
}
};
//if you have access to the infusion via entitlments or the infused item is in the party - you can see the details, othwise show marketplace cta
const isAccessible =
AccessUtils.isAccessible(InfusionUtils.getAccessType(infusion)) ||
isInfusedItemInParty;
const getSourceNames = (): string | null => {
const sources = InfusionUtils.getSources(infusion);
let sourceNames: Array<string> = [];
if (sources.length > 0) {
sources.forEach((source) => {
let sourceInfo = RuleDataUtils.getSourceDataInfo(
source.sourceId,
ruleData
);
if (sourceInfo !== null && sourceInfo.description !== null) {
sourceNames.push(sourceInfo.description);
}
});
}
return sourceNames.length > 0
? FormatUtils.renderNonOxfordCommaList(sourceNames)
: null;
};
return (
<div
className={clsx([onClick && styles.isInteractive, className])}
onClick={handleClick}
{...props}
>
<div className={styles.name}>
Infusion: {InfusionUtils.getName(infusion)}
</div>
<div className={styles.snippet}>
{isAccessible ? (
<Snippet
snippetData={snippetData}
classLevel={
InfusionUtils.getSelectedModifierData(infusion)?.value ?? null
}
proficiencyBonus={proficiencyBonus}
>
{InfusionUtils.getSnippet(infusion)}
</Snippet>
) : (
<MarketplaceCta
sourceName={getSourceNames()}
description="To unlock this infusion, check out the Marketplace to view purchase options."
/>
)}
</div>
</div>
);
};
+58 -14
View File
@@ -1,10 +1,10 @@
import clsx from "clsx";
import { FC, HTMLAttributes } from "react";
import { useMatch } from "react-router-dom";
import { createPortal } from "react-dom";
import { useMatch, useSearchParams } from "react-router-dom";
import { NavigationMenu } from "@dndbeyond/navigation-menu";
import { Footer } from "@dndbeyond/ttui/components/Footer";
import { MegaMenu } from "@dndbeyond/ttui/components/MegaMenu";
import { Sitebar } from "@dndbeyond/ttui/components/Sitebar";
import config from "~/config";
import useUser from "~/hooks/useUser";
@@ -13,9 +13,40 @@ import styles from "./styles.module.css";
const BASE_PATHNAME = config.basePathname;
const NOTIFICATIONS_CACHE_KEY = "dndbNotificationsCache";
const seedNotificationsCache = () => {
try {
const existing = localStorage.getItem(NOTIFICATIONS_CACHE_KEY);
const parsed = existing ? JSON.parse(existing) : null;
const isValid =
parsed &&
typeof parsed.expiresAtMs === "number" &&
Date.now() < parsed.expiresAtMs;
if (!isValid) {
localStorage.setItem(
NOTIFICATIONS_CACHE_KEY,
JSON.stringify({
expiresAtMs: Date.now() + 60 * 60 * 1000,
counts: {
unreadNotifications: null,
unreadMessages: null,
unreadReports: null,
unreadAppeals: null,
},
})
);
}
} catch {
// localStorage unavailable
}
};
interface LayoutProps extends HTMLAttributes<HTMLDivElement> {}
export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const isDev = process.env.NODE_ENV === "development";
const user = useUser();
const matchSheet = useMatch(`${BASE_PATHNAME}/:characterId/`);
@@ -23,20 +54,33 @@ export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
// Don't show the navigation in production
if (!isDev) return <>{children}</>;
// Seed the notifications cache so NavigationMenu skips the JWT fetch in local
// dev, where the auth service is not proxied.
seedNotificationsCache();
const navigationUser =
user && user.avatarUrl
? {
displayName: user.displayName,
avatarUrl: user.avatarUrl,
subscriptionTier: user.subscriptionTier as "Master",
roles: user.roles,
}
: undefined;
return (
<div {...props}>
{/* TODO: fetch navItems */}
<div className={styles.siteStyles}>
<Sitebar user={user as any} navItems={[]} />
{/* TODO: fetch sources */}
<MegaMenu enablePartyWizard={true} sources={[]} />
</div>
{!isVttView &&
createPortal(
<NavigationMenu user={navigationUser} />,
document.getElementById("header-wrapper")!
)}
<div className={clsx(["container", styles.devContainer])}>{children}</div>
{!matchSheet && (
<div className={styles.siteStyles}>
<Footer />
</div>
)}
{!isVttView &&
(!matchSheet ||
matchSheet.pathname === "/characters/builder" ||
matchSheet.pathname === "/characters/premade") &&
createPortal(<Footer />, document.getElementById("footer-wrapper")!)}
</div>
);
};
+3 -3
View File
@@ -6,7 +6,7 @@ interface LinkProps
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick"> {
onClick?: Function;
useTheme?: boolean;
useRouter?: boolean;
userouter?: boolean;
}
export const Link: FC<LinkProps> = ({
@@ -15,7 +15,7 @@ export const Link: FC<LinkProps> = ({
href,
onClick,
useTheme,
useRouter,
userouter,
...props
}) => {
//TODO - refactor to handle stop propagation on the oustide of this component (about 7 or so files to change)
@@ -27,7 +27,7 @@ export const Link: FC<LinkProps> = ({
}
};
if (useRouter)
if (userouter)
return (
<RouterLink
className={className}
@@ -0,0 +1,53 @@
import { clsx } from "clsx";
import { FC, HTMLAttributes } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement> {
sourceName?: string | null;
sourceNames?: Array<string>;
url?: string;
description?: string | null;
className?: string;
}
export const MarketplaceCta: FC<Props> = ({
sourceName = null,
sourceNames,
url = "/marketplace",
description = null,
className,
...props
}) => {
const allSourceNames: Array<string> = [];
if (sourceName) {
allSourceNames.push(sourceName);
}
if (sourceNames) {
allSourceNames.push(...sourceNames);
}
return (
<div className={clsx([styles.cta, className])} {...props}>
<a href={url} target="_blank" className={styles.link} rel="noreferrer">
<div className={styles.content}>
{allSourceNames.length > 0 && (
<div className={styles.header}>
<div className={styles.headerIntro}>Available in</div>
<div className={styles.headerTitle}>
{FormatUtils.renderNonOxfordCommaList(allSourceNames)}
</div>
</div>
)}
{description && (
<div className={styles.description}>{description}</div>
)}
<div className={styles.button}>Go To Marketplace</div>
</div>
</a>
</div>
);
};
@@ -1,65 +1,74 @@
import { HTMLAttributes, useEffect, useRef } from "react";
import clsx from "clsx";
import { Button } from "@dndbeyond/ttui/components/Button";
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
import { Button } from "../Button";
import { Dialog, DialogProps } from "../Dialog";
import { MaxCharactersMessageText } from "../MaxCharactersMessageText";
import styles from "./styles.module.css";
interface MaxCharactersDialogProps extends HTMLAttributes<HTMLDialogElement> {
interface MaxCharactersDialogProps
extends Omit<DialogProps, "onClose" | "open"> {
open: boolean;
onClose: () => void;
useMyCharactersLink?: boolean;
hasLockedCharacters?: boolean;
}
export const MaxCharactersDialog: React.FC<MaxCharactersDialogProps> = ({
open,
onClose,
useMyCharactersLink,
hasLockedCharacters,
className,
...props
}) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const handleClickBackdrop = (e) => {
const rect = dialogRef.current?.getBoundingClientRect();
const isClickInDialog =
rect &&
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width;
if (!isClickInDialog) (dialogRef.current as any)?.close();
};
useEffect(() => {
if (open) {
// Hacky workaround since showModal() isn't
// supported by TypeScript <4.8.3
(dialogRef.current as any)?.showModal();
} else {
// Hacky workaround since close() isn't
// supported by TypeScript <4.8.3
(dialogRef.current as any)?.close();
}
}, [onClose, open]);
return (
<dialog
className={styles.maxCharactersDialog}
ref={dialogRef}
onClick={handleClickBackdrop}
<Dialog
className={clsx([styles.dialog, className])}
open={open}
onClose={onClose}
modal
{...props}
>
<header>
<h2 className={styles.dialogTitle}>Your character slots are full</h2>
</header>
<MaxCharactersMessageText useMyCharactersLink={useMyCharactersLink} />
<footer className={styles.dialogFooter}>
<Button href="/store/subscribe" variant="outline">
Subscribe
<header className={styles.header}>
<h2 className={styles.title}>Your character slots are full</h2>
<Button
className={styles.closeButton}
variant="tool"
onClick={onClose}
aria-label="Close Modal"
>
<CloseIcon />
</Button>
</header>
<div className={styles.content}>
<MaxCharactersMessageText
hasLockedCharacters={hasLockedCharacters}
useMyCharactersLink={useMyCharactersLink}
/>
</div>
<footer className={styles.footer}>
{!hasLockedCharacters && (
<Button
className={styles.cardButton}
href="/store/subscribe"
color="info"
size="x-small"
variant="outline"
>
Subscribe
</Button>
)}
<Button
className={styles.cardButton}
color="info"
size="x-small"
onClick={onClose}
>
Close
</Button>
<Button onClick={onClose}>Close</Button>
</footer>
</dialog>
</Dialog>
);
};
@@ -6,12 +6,14 @@ interface MaxCharactersMessageTextProps extends HTMLAttributes<HTMLDivElement> {
includeTitle?: boolean;
useLinks?: boolean;
useMyCharactersLink?: boolean;
hasLockedCharacters?: boolean;
}
export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
includeTitle,
useLinks,
useMyCharactersLink,
hasLockedCharacters,
...props
}) => {
const subscribe = useLinks ? (
@@ -31,6 +33,21 @@ export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
"My Characters"
);
if (hasLockedCharacters) {
return (
<div {...props}>
<p className={styles.messageSubtext}>
Your character slots are full. To create a new character, select which
characters to keep using the options below, or{" "}
<a className={styles.link} href="/store/subscribe">
subscribe
</a>{" "}
to unlock additional slots.
</p>
</div>
);
}
return (
<div {...props}>
{includeTitle && (
@@ -0,0 +1,18 @@
import { HTMLAttributes, FC, ReactNode } from "react";
import styles from "./styles.module.css";
export interface NoResultsFoundProps extends HTMLAttributes<HTMLDivElement> {
message?: ReactNode;
}
export const NoResultsFound: FC<NoResultsFoundProps> = ({
message = "No results found",
...props
}) => {
return (
<p className={styles.noResults} {...props}>
{message}
</p>
);
};
+17 -12
View File
@@ -7,21 +7,23 @@ import React, {
useEffect,
} from "react";
import { Dialog } from "@dndbeyond/ttui/components/Dialog";
import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible";
import { Dialog } from "~/components/Dialog";
import { useCharacterTheme } from "~/contexts/CharacterTheme";
import styles from "./styles.module.css";
/**
* This is a popover component to handle dialogs that are dependent on a location from a trigger element.
* It uses the useIsVisible hook to handle the visibility of the dialog. The position of the dialog
* is determined by the position prop, which can be set to topLeft, topRight, left, right, bottomLeft, or bottomRight.
* The maxWidth prop can be used to set the maximum width of the dialog. The trigger prop is the element that will trigger
* the dialog to open.The popover component also handles the positioning of the dialog based on the trigger element. The
* handleToggle function toggles the visibility of the dialog, and the handleClose function closes the dialog. The popover
* component also adds an event listener to the cancel button in the dialog to close the dialog when clicked.
* This is a popover component to handle dialogs that are dependent on a location from a trigger
* element. It uses the useIsVisible hook to handle the visibility of the dialog. The position of
* the dialog is determined by the position prop, which can be set to topLeft, topRight, left,
* right, bottomLeft, or bottomRight. The maxWidth prop can be used to set the maximum width of the
* dialog. The trigger prop is the element that will trigger the dialog to open.The popover
* component also handles the positioning of the dialog based on the trigger element. The
* handleToggle function toggles the visibility of the dialog, and the handleClose function closes
* the dialog. The popover component also adds an event listener to the cancel button in the dialog
* to close the dialog when clicked.
*
* In order for a cancel button to work, it must have a `data-cancel` attribute.
**/
@@ -36,7 +38,7 @@ interface PopoverProps {
| "bottomLeft"
| "bottomRight"
| "bottom";
trigger: ReactElement;
trigger: ReactElement<{ onClick?: (e: MouseEvent) => void }>;
maxWidth?: number;
variant?: "default" | "menu";
"data-testid"?: string;
@@ -85,9 +87,12 @@ export const Popover: FC<PopoverProps> = ({
// Check if children is a ReactElement
if (React.isValidElement(children)) {
// Create a copy of the ReactElement and attach handleClose function to it
return cloneElement(children as ReactElement, {
handleClose,
});
return cloneElement(
children as ReactElement<{ handleClose: (e: MouseEvent) => void }>,
{
handleClose,
}
);
}
// If not just return the children - means its a primitive type
else {
@@ -0,0 +1,34 @@
import { FC } from "react";
import { PreferencePrivacyTypeEnum } from "~/constants";
import { RadioGroup, RadioGroupProps } from "../RadioGroup";
export interface PrivacyTypeRadioProps
extends Omit<RadioGroupProps, "name" | "title" | "options"> {}
export const PrivacyTypeRadio: FC<PrivacyTypeRadioProps> = (props) => (
<RadioGroup
title="Character Privacy"
name="privacyType"
options={[
{
value: PreferencePrivacyTypeEnum.CAMPAIGN_ONLY,
label: "Campaign Only",
description:
"Only others within a Campaign you have joined can view your Characters.",
},
{
value: PreferencePrivacyTypeEnum.PUBLIC,
label: "Public",
description: "Anyone with a shared link can view your Characters.",
},
{
value: PreferencePrivacyTypeEnum.PRIVATE,
label: "Private",
description: "Only you can view your Characters.",
},
]}
{...props}
/>
);
@@ -0,0 +1,117 @@
import clsx from "clsx";
import { ChangeEvent, FC, HTMLAttributes, useRef, useState } from "react";
import styles from "./styles.module.css";
export interface RadioOption {
label: string;
value: string | number;
description?: string;
disabled?: boolean;
}
export interface RadioGroupProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
name: string;
initialValue?: string | number;
title: string;
options: RadioOption[];
description?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeConfirm?: (
e: ChangeEvent<HTMLInputElement>,
newValue: string | number,
oldValue: string | number,
accept: () => void,
reject: () => void
) => void;
disabled?: boolean;
themed?: boolean;
}
export const RadioGroup: FC<RadioGroupProps> = ({
className,
name,
initialValue,
title,
options,
description,
disabled,
onChange,
onChangeConfirm,
themed,
...props
}) => {
const radioGroupRef = useRef<HTMLDivElement>(null!);
const [selectedValue, setSelectedValue] = useState(
initialValue || options[0]?.value
);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue =
typeof selectedValue === "number"
? Number(e.target.value)
: e.target.value;
if (onChangeConfirm) {
onChangeConfirm(
e,
newValue,
selectedValue,
() => {
if (onChange) onChange(e);
setSelectedValue(newValue);
},
() => {
// noop
}
);
} else {
if (onChange) onChange(e);
setSelectedValue(newValue);
}
};
return (
<div
className={clsx([styles.radioGroup, themed && styles.themed, className])}
ref={radioGroupRef}
{...props}
>
<legend>{title}</legend>
{description && <p className={styles.subtitle}>{description}</p>}
<div className={styles.options}>
{options?.map((option) => {
const id = `${name}_${option.value}`;
const checked = String(selectedValue) === String(option.value);
return (
<div
className={styles.option}
key={id}
data-testid="radio-option-container"
>
<input
type="radio"
name={name}
id={id}
onChange={handleChange}
value={option.value}
aria-checked={checked}
disabled={disabled || option.disabled}
/>
<div className={styles.content}>
<label className={styles.label} htmlFor={id}>
{option.label}
</label>
{option.description && (
<p className={styles.description}>{option.description}</p>
)}
</div>
</div>
);
})}
</div>
</div>
);
};
+545
View File
@@ -0,0 +1,545 @@
import clsx from "clsx";
import groupBy from "lodash/groupBy";
import {
ChangeEvent,
CSSProperties,
FC,
SelectHTMLAttributes,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
FeatureChoiceOption,
HtmlSelectOption,
HtmlSelectOptionGroup,
} from "@dndbeyond/character-rules-engine";
import Check from "@dndbeyond/fontawesome-cache/svgs/regular/check.svg";
import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible";
import styles from "./styles.module.css";
export interface SelectOption {
value: string | number;
label: string;
groupLabel?: string;
index: number;
}
export interface SelectProps
extends Omit<
SelectHTMLAttributes<HTMLDivElement>,
"value" | "onChange" | "name" | "size"
> {
options: (FeatureChoiceOption | HtmlSelectOption | HtmlSelectOptionGroup)[];
value: string | number | null;
onChange?: (value: string | number) => void;
onChangeConfirm?: (
newValue: string,
oldValue: string,
accept: () => void,
reject: () => void
) => void;
placeholder?: string;
searchThreshold?: number | null;
searchPlaceholder?: string;
isHighlighted?: boolean;
name: string;
optionsWidth?: "medium" | "large" | "x-large";
hidePlaceholderOption?: boolean;
size?: "small" | "medium";
resetAfterChoice?: boolean;
}
export const Select: FC<SelectProps> = ({
className,
placeholder = "-- Choose an Option --",
name,
onChange,
onChangeConfirm,
options,
value,
disabled,
searchThreshold = 10,
searchPlaceholder = "Search...",
isHighlighted,
optionsWidth = "medium",
hidePlaceholderOption,
size = "medium",
resetAfterChoice,
...props
}) => {
const buttonRef = useRef<HTMLButtonElement>(null!);
const searchRef = useRef<HTMLInputElement>(null!);
const optionsRef = useRef<HTMLDivElement>(null!);
const optionsListRef = useRef<HTMLDivElement>(null!);
const isSearchActive = useRef(false);
const { ref: selectRef, isVisible, setIsVisible } = useIsVisible(false);
const [internalValue, setInternalValue] = useState(value || "");
const [query, setQuery] = useState("");
const [focused, setFocused] = useState(0);
const [buttonPosition, setButtonPosition] = useState<DOMRect | null>(null);
const getOptions = () => {
let counter = 1;
const optionArr: SelectOption[] = [];
options.forEach((option: FeatureChoiceOption | HtmlSelectOptionGroup) => {
if ("optGroupLabel" in option) {
option.options.forEach((opt: HtmlSelectOption) => {
optionArr.push({
label: opt.label || "",
value: opt.value,
groupLabel: option.optGroupLabel,
index: counter,
});
counter++;
});
} else {
optionArr.push({
label: option.label || "",
value: option.value,
index: counter,
});
counter++;
}
});
return optionArr;
};
const [results, setResults] = useState(getOptions());
const currentSelection = results.find(
(option) => String(option.value) === String(internalValue)
);
const scrollIntoView = useCallback(() => {
if (!optionsRef.current) return;
const optionList = optionsRef.current.querySelector(
`.${styles.optionList}`
);
if (!optionList) return;
let targetElement: Element | null = null;
// Look for the focused option first (including placeholder at index 0)
if (focused >= 0) {
targetElement = optionList.querySelector(`[data-index="${focused}"]`);
}
// If no focused option, look for the currently selected option
else {
targetElement = optionList.querySelector(`[aria-selected="true"]`);
}
// Scroll the target element into view within the dropdown container
if (targetElement) {
targetElement.scrollIntoView({
behavior: "auto",
block: "nearest",
inline: "nearest",
});
}
}, [focused]);
const buildOptionsHtml = (options: SelectOption[]) => [
...options.map((option) => (
<label
className={clsx([
styles.option,
focused === option.index && styles.focused,
])}
aria-selected={
(value === null && query === "" && option.index === 0) ||
String(internalValue) === String(option.value)
}
key={option.value}
data-index={option.index}
onMouseEnter={() => setFocused(option.index)}
>
<Check className={styles.check} aria-label={option.label} />
<input
type="radio"
name={name}
value={option.value}
onClick={() => handleClickOption(option.value)}
onChange={handleChange}
checked={value === option.value}
/>
{option.label}
</label>
)),
];
const getOptionsList = () => {
// Group items by groupLabel
const grouped = groupBy(results, "groupLabel");
// Turn grouped object into an array with groups and values
const entries = Object.entries(grouped);
// Map over grouped items to render
return entries.map(([groupName, options]) => {
// If there is a valid groupName
if (groupName !== "undefined") {
return (
<div className={styles.optionGroup} key={groupName}>
<label className={styles.groupLabel}>{groupName}</label>
{buildOptionsHtml(options)}
</div>
);
}
// If the groupName is "undefined"
return buildOptionsHtml(options);
});
};
const getOptionsPosition = (): CSSProperties | undefined => {
const maxHeight = 300; // Maximum height of the options dropdown
// If the button exists, set styles based on its position
if (buttonPosition && optionsListRef.current) {
// This logic checks to see if the options should be above or below the button
const buttonBottom = buttonPosition.y + buttonPosition.height;
// Get search height to account for that in the height calculation
const searchHeight = searchRef.current?.scrollHeight || 0;
// Get the height of the options list
const optionsListHeight = optionsListRef.current.scrollHeight;
// Calculate the height of the options list (capped at maxHeight)
const optionsHeight =
optionsListHeight + searchHeight > maxHeight
? maxHeight - searchHeight
: optionsListHeight + searchHeight;
// Determine if there is enoughy room below the button to show the entire options list
const isEnoughRoomBelow =
buttonBottom + optionsHeight < window.innerHeight;
const selectStyle = selectRef.current?.style;
selectStyle?.setProperty("--options-width", `${buttonPosition.width}px`);
selectStyle?.setProperty(
"--options-top",
isEnoughRoomBelow
? `${buttonBottom}px`
: `${buttonPosition.y - optionsHeight + 8}px`
);
selectStyle?.setProperty("--options-height", `${optionsHeight}px`);
}
return undefined;
};
const handleToggleVisibility = () => {
const isHidden = isVisible;
setIsVisible(!isHidden);
if (!isHidden) {
// Get the position of the button when it is clicked
if (buttonRef.current)
setButtonPosition(buttonRef.current.getBoundingClientRect());
// Reset state and scroll to selected option
setFocused(0);
setQuery("");
setResults(getOptions());
searchRef.current?.focus();
// Use setTimeout to ensure the DOM is updated before scrolling
setTimeout(() => {
scrollIntoView();
}, 0);
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (onChangeConfirm) {
// If a confirm modal is needed, use that handler
onChangeConfirm(
e.target.value,
String(internalValue),
() => {
if (onChange) {
onChange(e.target.value);
}
setInternalValue(e.target.value);
},
() => {}
);
}
// If no confirm modal is needed, use the normal handler
else {
if (onChange) onChange?.(e.target.value);
setInternalValue(e.target.value);
}
// Reset state
handleReset();
};
const handleClickOption = (value: string | number) => {
if (value === currentSelection?.value) {
handleReset();
}
};
const handleReset = () => {
if (resetAfterChoice) setInternalValue("");
setIsVisible(false);
setQuery("");
setResults(getOptions());
};
const handleSearch = (e) => {
// Reset results to default
if (!e.target.value) {
setResults(getOptions());
} else {
const filteredOptions = getOptions()
.filter((opt) =>
opt.label.toLowerCase().includes(e.target.value.toLowerCase())
)
.map((opt, index) => ({
...opt,
index,
}));
setResults(filteredOptions);
}
setQuery(e.target.value);
};
const handleKeypress = (e) => {
// Get the currently focused element in the DOM
const activeElement = document.activeElement as HTMLElement;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
if (isVisible) {
// Go to the next option
if (focused < results.length) {
setFocused(focused + 1);
}
// If there are no more options, go back to the top (placeholder at index 0)
else {
setFocused(0);
}
}
// If the button is focused and the dropdown is closed, open it
else if (activeElement === buttonRef.current) {
setIsVisible(true);
setFocused(currentSelection?.index || 0);
}
scrollIntoView();
break;
case "ArrowUp":
e.preventDefault();
if (isVisible) {
// Go to the previous option
if (focused > 0) {
setFocused(focused - 1);
}
// If there are no previous options, go back to the bottom
else {
setFocused(results.length);
}
}
// If the button is focused and the dropdown is closed, open it
else if (activeElement === buttonRef.current) {
setIsVisible(true);
setFocused(currentSelection?.index || results.length);
}
scrollIntoView();
break;
case "Enter":
e.preventDefault();
// If the dropdown is open, select the focused option
if (isVisible) {
if (focused === 0) {
// Handle the placeholder option (empty value)
handleChange({
target: { value: "" },
} as ChangeEvent<HTMLInputElement>);
buttonRef.current.focus();
} else {
const focusedOption = results.find((opt) => opt.index === focused);
if (focusedOption) {
handleChange({
target: { value: focusedOption.value.toString() },
} as ChangeEvent<HTMLInputElement>);
buttonRef.current.focus();
}
}
}
// If the button is focused and the dropdown is closed, open it
else if (activeElement === buttonRef.current) {
handleToggleVisibility();
}
scrollIntoView();
break;
case "Escape":
handleReset();
break;
case "Tab":
if (isVisible) {
// If the search is focused when tab is pressed go to item
if (document.activeElement === searchRef.current) {
e.preventDefault();
setFocused(currentSelection?.index || 0);
scrollIntoView();
searchRef.current.blur();
} else {
setFocused(0);
searchRef.current?.focus();
handleToggleVisibility();
}
}
break;
default:
break;
}
};
useEffect(() => {
const selectElement = selectRef.current;
if (!selectElement) return;
selectElement.addEventListener("keydown", handleKeypress);
return () => {
selectElement.removeEventListener("keydown", handleKeypress);
};
});
useEffect(
() => {
// keep the search active is searchThreshold is set and results are more than the threshold - this prevents the search from going away if the search results are less than the threshold after a query is entered
isSearchActive.current =
!!searchThreshold &&
searchThreshold >= 0 &&
results.length > searchThreshold;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
// If the select is visible, prevent the page from scrolling. This prevents us from having to
// recalculate the position of the option group on scroll.
document.body.style.overflowY = isVisible ? "hidden" : "unset";
getOptionsPosition();
// Scroll to the selected item when the dropdown becomes visible
if (isVisible) {
setTimeout(() => {
scrollIntoView();
}, 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible, scrollIntoView]);
const optionsKey = useMemo(
() => options.map((o) => ("value" in o ? o.value : o.optGroupLabel)).join(","),
// eslint-disable-next-line react-hooks/exhaustive-deps
[options]
);
useEffect(() => {
if (!isSearchActive.current) {
setResults(getOptions());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [optionsKey]);
useEffect(() => {
if (currentSelection) {
setFocused(currentSelection.index);
}
}, [currentSelection]);
useEffect(() => {
setInternalValue(value || "");
}, [value]);
return (
<div
ref={selectRef}
className={clsx([
styles.select,
isHighlighted && styles.highlighted,
className,
])}
onClick={(e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}}
{...props}
>
<button
className={clsx([styles.button, styles[size]])}
id={`${name}Button`}
onClick={(e) => handleToggleVisibility()}
role="combobox"
aria-haspopup="listbox"
aria-controls={`${name}Dropdown`}
aria-labelledby={`${name}Label`}
aria-expanded={isVisible}
tabIndex={0}
disabled={disabled || false}
ref={buttonRef}
>
<span className={styles.buttonText}>
{currentSelection ? currentSelection?.label : placeholder}
</span>
<ChevronDown />
</button>
<div
ref={optionsRef}
className={clsx([styles.options, styles[optionsWidth]])}
role="group"
id={`${name}Dropdown`}
>
{isSearchActive.current && (
<input
className={styles.search}
type="search"
value={query}
onChange={handleSearch}
placeholder={searchPlaceholder}
ref={searchRef}
/>
)}
<div ref={optionsListRef} className={styles.optionList} role="menu">
{!!searchThreshold && query.length > 0 && results.length < 1 && (
<label className={styles.notFound}>
No results found for <strong>{query}</strong>
</label>
)}
{!query && !hidePlaceholderOption && (
<label
className={clsx([styles.option, focused === 0 && styles.focused])}
aria-selected={!internalValue && query === ""}
data-index={0}
onMouseEnter={() => setFocused(0)}
>
<Check className={styles.check} />
<input
type="radio"
name={name}
value={""}
onClick={() => handleClickOption("")}
onChange={handleChange}
checked={value === ""}
/>
{placeholder}
</label>
)}
{getOptionsList()}
</div>
</div>
</div>
);
};
+20 -8
View File
@@ -1,18 +1,25 @@
import clsx from "clsx";
import { FC, Fragment, HTMLAttributes, ReactNode, useState } from "react";
import {
FC,
Fragment,
HTMLAttributes,
ReactNode,
useEffect,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid";
import { useCharacterTheme } from "~/contexts/CharacterTheme";
import styles from "./styles.module.css";
interface TabFilterOption {
label: ReactNode;
content: ReactNode;
badge?: ReactNode;
}
export interface TabFilterProps extends HTMLAttributes<HTMLDivElement> {
filters:
| {
label: ReactNode;
content: ReactNode;
badge?: ReactNode;
}[];
filters: Array<TabFilterOption>;
showAllTab?: boolean;
sharedChildren?: ReactNode;
}
@@ -30,6 +37,11 @@ export const TabFilter: FC<TabFilterProps> = ({
const activeIndex = showAllTab ? activeFilter - 1 : activeFilter;
const nonEmptyFilters = filters.filter((f) => f.label !== "");
// Reset the active filter to the first tab when the filter options change
useEffect(() => {
setActiveFilter(0);
}, [filters.length]);
const getDefaultLabel = (label: ReactNode) => {
if (typeof label === "string") return label;
return uuidv4();
@@ -87,7 +99,7 @@ export const TabFilter: FC<TabFilterProps> = ({
);
})}
</div>
<div className={styles.content}>
<div className={styles.content} data-scrollable-container="true">
{sharedChildren}
{!showAllTab || activeFilter !== 0
? nonEmptyFilters[activeIndex]?.content
+73 -16
View File
@@ -1,11 +1,30 @@
import clsx from "clsx";
import { FC, HTMLAttributes, ReactNode, useState } from "react";
import {
FC,
HTMLAttributes,
ReactNode,
useEffect,
useRef,
useState,
} from "react";
import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
import { Popover } from "../Popover";
import styles from "./styles.module.css";
/**
* This component renders a set of tabs with associated content panels.
* It supports different variants for tab behavior, including default,
* collapse, and toggle modes. Tabs can be limited in number of visible
* items, with overflow handled via a "More" dropdown menu.
*
** Variants:
* - default: Standard tab behavior where clicking a tab shows its content.
* - collapse: Always has a selected tab, but clicking it again collapses the content.
* - fullwidth: Tabs stretch to fill the container width.
*/
interface TabItem {
label: ReactNode;
content: ReactNode;
@@ -15,44 +34,79 @@ interface TabItem {
interface TabListProps extends HTMLAttributes<HTMLElement> {
tabs: Array<TabItem | null>;
variant?: "default" | "collapse" | "toggle";
variant?: "default" | "collapse" | "fullwidth";
defaultActiveId?: string;
maxNavItemsShow?: number;
onChangeTab?: (id: string) => void;
themed?: boolean;
forceShow?: boolean;
}
export const TabList: FC<TabListProps> = ({
className,
tabs,
defaultActiveId,
maxNavItemsShow = 7,
variant = "default",
onChangeTab,
themed,
forceShow,
...props
}) => {
const [activeTab, setActiveTab] = useState(
defaultActiveId ? defaultActiveId : tabs[0]?.id
const tabListRef = useRef<HTMLDivElement>(null!);
const [activeTab, setActiveTab] = useState(defaultActiveId || tabs[0]?.id);
const [isTabVisible, setIsTabVisible] = useState(
forceShow ?? activeTab !== ""
);
const [isTabVisible, setIsTabVisible] = useState(activeTab !== "");
const tabsWithContent = tabs.filter((tab) => tab);
const visibleTabs = tabsWithContent.slice(0, maxNavItemsShow);
const hiddenTabs = tabsWithContent.slice(maxNavItemsShow);
const selectedTab = tabs.find((tab) => tab?.id === activeTab);
const isToggleable = variant === "collapse";
const handleTabClick = (id: string) => {
if (id === activeTab && variant === "collapse") {
setIsTabVisible(!isTabVisible);
}
if (id === activeTab && variant === "toggle") {
setActiveTab("");
}
if (id !== activeTab) {
setActiveTab(id);
setIsTabVisible(true);
if (onChangeTab) {
onChangeTab(id);
}
}
};
const tabsWithContent = tabs.filter((tab) => tab);
const visibleTabs = tabsWithContent.slice(0, maxNavItemsShow);
const hiddenTabs = tabsWithContent.slice(maxNavItemsShow);
const selectedTab = tabs.find((tab) => tab?.id === activeTab);
useEffect(
() => {
// Update CSS variable for number of tabs
const tabListStyles = tabListRef.current?.style;
if (tabListStyles) {
tabListStyles.setProperty("--tab-count", tabs.length.toString());
tabListStyles.setProperty(
"--tab-active-position",
tabs.indexOf(selectedTab as TabItem).toString()
);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[tabs.length, selectedTab]
);
return (
<div {...props}>
<div
className={clsx([
styles.tabList,
styles[variant],
themed && styles.themed,
className,
])}
ref={tabListRef}
{...props}
>
<menu className={styles.tabs}>
{/* List of visible tabs */}
{visibleTabs.map((tab: TabItem) => (
@@ -62,10 +116,13 @@ export const TabList: FC<TabListProps> = ({
role="radio"
aria-checked={activeTab === tab.id}
onClick={() => handleTabClick(tab.id)}
data-testid={tab.id}
>
{tab.label}
{variant !== "default" && (
<ChevronDown className={!isTabVisible ? styles.closed : ""} />
{isToggleable && (
<ChevronDown
className={clsx([!isTabVisible && styles.closed])}
/>
)}
</button>
</li>
@@ -15,6 +15,7 @@ export interface TextareaProps
value?: string;
onInputBlur: (textValue: string) => void;
onInputKeyUp: () => void;
placeholder?: string;
}
/**
+13 -19
View File
@@ -1,12 +1,9 @@
import clsx from "clsx";
import { useState, type FC, type HTMLAttributes } from "react";
import styles from "./styles.module.css";
import { useSelector } from "react-redux";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
export interface ToggleProps
extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
checked?: boolean;
color?: "primary" | "secondary" | "warning" | "error" | "info" | "success" | "themed";
onClick?: (isEnabled: boolean) => void;
@@ -26,26 +23,23 @@ export const Toggle: FC<ToggleProps> = ({
onChangePromise,
...props
}) => {
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const [enabled, setEnabled] = useState(checked);
const handleClick = (evt: React.MouseEvent): void => {
if (!isReadonly) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
if (onChangePromise) {
onChangePromise(
!enabled,
() => {
setEnabled(!enabled);
},
() => {}
);
} else if (onClick) {
onClick(!enabled);
setEnabled(!enabled);
}
if (onChangePromise) {
onChangePromise(
!enabled,
() => {
setEnabled(!enabled);
},
() => {}
);
} else if (onClick) {
onClick(!enabled);
setEnabled(!enabled);
}
};
+10 -3
View File
@@ -9,9 +9,9 @@ import {
RuleDataUtils,
} from "@dndbeyond/character-rules-engine";
import { Select } from "~/components/Select";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import XpBar from "~/tools/js/smartComponents/XpBar";
import { Select } from "~/tools/js/smartComponents/legacy";
import { Button } from "../Button";
import styles from "./styles.module.css";
@@ -144,15 +144,22 @@ export const XpManager: FC<XpManagerProps> = ({
</div>
<div className={clsx([styles.controls, styles.withDivider])}>
<div className={styles.control}>
<label id="set-level" className={styles.label}>
<label
id="set-level-label"
htmlFor="set-levelButton"
className={styles.label}
>
Set Level
</label>
<Select
aria-labelledby="set-level"
className={styles.levelSelect}
aria-labelledby="set-level-label"
options={levelOptions}
onChange={handleChooseLevel}
value={levelChosen}
placeholder={"--"}
name="set-level"
searchThreshold={null}
/>
</div>
<div className={clsx([styles.control, styles.setXp])}>
+6 -2
View File
@@ -77,6 +77,11 @@ export const SourceCategoryDescription = {
export const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif"];
export const FILE_SIZE_3MB = 3 * 1024 * 1024;
export const KETCH_RETRY_CONFIG = {
maxRetries: 10,
retryInterval: 500,
} as const;
/**
* Export constants from the rules engine to reduce the number of places where
* they're being imported.
@@ -232,8 +237,6 @@ export const NoteTypeEnum = Constants.NoteTypeEnum;
export const NotificationTypeEnum = Constants.NotificationTypeEnum;
export const PoundsInTon = Constants.POUNDS_IN_TON;
export const PropertyList = Constants.PROPERTY_LIST;
export const PartyInventorySharingStateEnum =
Constants.PartyInventorySharingStateEnum;
export const PreferenceAbilityScoreDisplayTypeEnum =
Constants.PreferenceAbilityScoreDisplayTypeEnum;
export const PreferenceEncumbranceTypeEnum =
@@ -243,6 +246,7 @@ export const PreferencePrivacyTypeEnum = Constants.PreferencePrivacyTypeEnum;
export const PreferenceProgressionTypeEnum =
Constants.PreferenceProgressionTypeEnum;
export const PreferenceSharingTypeEnum = Constants.PreferenceSharingTypeEnum;
export const PreferenceLongRestTypeEnum = Constants.PreferenceLongRestTypeEnum;
export const PrerequisiteSubTypeEnum = Constants.PrerequisiteSubTypeEnum;
export const PrerequisiteTypeEnum = Constants.PrerequisiteTypeEnum;
export const ProficiencyAdjustmentTypeEnum =
@@ -62,16 +62,11 @@ export const CharacterThemeProvider = ({ children }) => {
__html: `
:root {
--theme-color: ${themeColor};
--theme-color-muted: color-mix(in srgb, var(--theme-color), var(--theme-background-solid) 30%);
--theme-background: ${backgroundColor};
--theme-background-solid: ${solidBackground};
--theme-contrast: var(--ttui_grey-${isDarkMode ? 50 : 900});
--theme-transparent: color-mix(in srgb, var(--theme-color), transparent 60%);
/* Update the character muted color for readability on dark mode */
${
isDarkMode ? "--character-muted-color: var(--ttui_grey-400);" : ""
}
}`,
}}
/>
+14 -11
View File
@@ -1,20 +1,23 @@
import { FC, createContext, useContext, useEffect, useState } from "react";
import {
FC,
PropsWithChildren,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { toCamelCase } from "~/helpers/casing";
import { getFeatureFlagsFromCharacterService } from "~/helpers/characterServiceApi";
const featureFlagsToGet = [
"release-gate-ims",
"release-gate-gfs-blessings-ui",
"release-gate-character-sheet-tour",
"release-gate-premade-characters",
"circuit-breaker.datadog-rum-characters-app",
];
export const defaultFlags = {
imsFlag: false,
gfsBlessingsUiFlag: false,
characterSheetTourFlag: false,
premadeCharactersFlag: false,
datadogRumCharactersAppFlag: false,
};
export type FeatureFlags = typeof defaultFlags;
@@ -25,7 +28,7 @@ export interface FeatureFlagContextType extends FeatureFlags {
export const FeatureFlagContext = createContext<FeatureFlagContextType>(null!);
interface ProviderProps {
interface ProviderProps extends PropsWithChildren {
defaultOverrides?: Partial<FeatureFlags>;
}
@@ -44,12 +47,12 @@ export const FeatureFlagProvider: FC<ProviderProps> = ({
const formattedFlag = flag
// Remove `character-app` from the flag name
.replace(/character-app\./i, "")
// Remove `circuit-breaker` from the flag name
.replace(/circuit-breaker\./i, "")
// Remove `release-gate` from the flag name
.replace(/release-gate\W/i, "")
// Remove `character-sheet` from flag name if not the tour flag
.replace(/character-sheet\W(?!tour)/i, "")
// Change `2d` to `tactical` since var can't start with number
.replace(/2d/i, "tactical");
.replace(/character-sheet\W(?!tour)/i, "");
const key = toCamelCase(`${formattedFlag}Flag`);
return { ...acc, [key]: flags[flag] };
+8 -2
View File
@@ -1,4 +1,10 @@
import { createContext, FC, useContext, useState } from "react";
import {
createContext,
FC,
PropsWithChildren,
useContext,
useState,
} from "react";
export interface FiltersContextType {
showItemTypes: boolean;
@@ -13,7 +19,7 @@ export interface FiltersContextType {
export const FiltersContext = createContext<FiltersContextType>(null!);
export const FiltersProvider: FC = ({ children }) => {
export const FiltersProvider: FC<PropsWithChildren> = ({ children }) => {
const [showItemTypes, setShowItemTypes] = useState(true);
const [showItemSourceCategories, setShowItemSourceCategories] =
useState(true);
+13 -2
View File
@@ -1,5 +1,12 @@
import { isEqual } from "lodash";
import { createContext, FC, useContext, useEffect, useState } from "react";
import {
createContext,
FC,
PropsWithChildren,
useContext,
useEffect,
useState,
} from "react";
import { useSelector } from "react-redux";
import {
@@ -39,6 +46,7 @@ export interface PaneInfo {
) => void;
paneHistoryPrevious: () => void;
paneHistoryNext: () => void;
resetActivePane: () => void;
}
export interface SidebarContextType {
@@ -48,7 +56,7 @@ export interface SidebarContextType {
export const SidebarContext = createContext<SidebarContextType>(null!);
export const SidebarProvider: FC = ({ children }) => {
export const SidebarProvider: FC<PropsWithChildren> = ({ children }) => {
const isMobile = useSelector(appEnvSelectors.getIsMobile);
const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
@@ -64,6 +72,8 @@ export const SidebarProvider: FC = ({ children }) => {
const width: number = 340;
const resetActivePane = () => setActivePane(null);
//Create a new pane history
const paneHistoryStart = (
type: PaneComponentEnum,
@@ -161,6 +171,7 @@ export const SidebarProvider: FC = ({ children }) => {
paneHistoryPush,
paneHistoryPrevious,
paneHistoryNext,
resetActivePane,
},
}}
>
@@ -1,11 +1,8 @@
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { ThemeProvider } from "@mui/material/styles";
import { useState, createContext, useEffect, useMemo } from "react";
import { prefixer } from "stylis";
import { getTheme } from "~/theme";
type PaletteOpt = "light" | "dark";
interface Manager {
@@ -77,12 +74,9 @@ export const ThemeManagerProvider = ({
/**
* End the hack
*/
const theme = getTheme(manager.lightOrDark, manager.primary);
return (
<ThemeManager.Provider value={{ manager, setManager }}>
<CacheProvider value={hack__cache}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</CacheProvider>
<CacheProvider value={hack__cache}>{children}</CacheProvider>
</ThemeManager.Provider>
);
};
@@ -4,6 +4,7 @@ import {
PreferenceEncumbranceTypeEnum,
PreferenceHitPointTypeEnum,
PreferencePrivacyTypeEnum,
PreferenceLongRestTypeEnum,
PreferenceProgressionTypeEnum,
PreferenceSharingTypeEnum,
SenseTypeEnum,
@@ -26,6 +27,7 @@ export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
const progressionType = PreferenceProgressionTypeEnum.MILESTONE;
const sharingType = PreferenceSharingTypeEnum.LIMITED;
const privacyType = PreferencePrivacyTypeEnum.CAMPAIGN_ONLY;
const longRestType = PreferenceLongRestTypeEnum.HALF;
return {
abilityScoreDisplayType,
@@ -38,6 +40,7 @@ export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
enforceMulticlassRules: true,
hitPointType,
ignoreCoinWeight: true,
longRestType,
primaryMovement,
primarySense,
privacyType,
+66
View File
@@ -0,0 +1,66 @@
import {
SortByPropMap,
getSearchableTerms,
} from "~/state/selectors/characterUtils";
import { SortOrderEnum } from "~/types";
import { logListingSearchChanged } from "./analytics";
import { byProp } from "./sortUtils";
type KeyValuePair = { key: string; value: string | Array<string> };
export const keyValuePairsToQueryString = (
keyValuePairs: Array<KeyValuePair>
) =>
keyValuePairs
.map(({ key, value }) =>
Array.isArray(value)
? value.map((v) => `${key}=${v}`).join("&")
: `${key}=${value}`
)
.join("&");
export const queryListToMap = (queries) => {
return queries.reduce((queryMap, { key, value }) => {
queryMap[key] = value;
return queryMap;
}, {});
};
export const matchesSearchTerm =
(lowerSearch: string) => (searchableTerm: string) =>
searchableTerm.toLowerCase().includes(lowerSearch);
export const applyQueriesClientSide = (queryMap) => (json: any) => {
let data = json.data || [];
const { search, sortBy, sortOrder = SortOrderEnum.Ascending } = queryMap;
if (search) {
const matchesSearch = matchesSearchTerm(search.toLowerCase());
data = data.filter((character) =>
getSearchableTerms(character).some(matchesSearch)
);
// Logging this here will duplicate the log if a search term is used and then the sort is changed.
// If this was logged in MyCharactersListing.handleSearchChanged, then it wouldn't have the context of all data and would miss matches in some scenarios.
// This function is debounced to prevent spamming the logs as the user types.
logListingSearchChanged(data, matchesSearch);
}
const sortByPropSelector = SortByPropMap[sortBy];
if (sortByPropSelector) {
data = data.sort(byProp(sortByPropSelector));
if (sortOrder === SortOrderEnum.Descending) {
data.reverse();
}
}
return {
...json,
data,
};
};
+2 -5
View File
@@ -1,9 +1,6 @@
import { useContext, useEffect, useState } from "react";
import {
AbilityManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { AbilityManager } from "@dndbeyond/character-rules-engine/es";
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
@@ -18,7 +15,7 @@ export function useAbilities() {
setAbilities(abilities);
}
return FeaturesManager.subscribeToUpdates({ onUpdate });
return AbilityManager.subscribeToUpdates({ onUpdate });
}, [attributesManager, setAbilities]);
return abilities;
+7 -5
View File
@@ -8,7 +8,6 @@ import {
ApiAdapterUtils as apiAdapterUtils,
BackgroundUtils as backgroundUtils,
CampaignUtils as campaignUtils,
CampaignSettingUtils as campaignSettingUtils,
CharacterUtils as characterUtils,
ChoiceUtils as choiceUtils,
ClassFeatureUtils as classFeatureUtils,
@@ -124,7 +123,6 @@ export const useCharacterEngine = () => ({
baseFeats: useSelector(s.getBaseFeats),
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
campaign: useSelector(s.getCampaign),
campaignSettings: useSelector(sds.getCampaignSettings),
carryingCapacity: useSelector(s.getCarryCapacity),
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
@@ -151,7 +149,7 @@ export const useCharacterEngine = () => ({
classSpells: useSelector(s.getClassSpells),
classSpellInfo: useSelector(s.getClassSpellInfoLookup),
classSpellLists: useSelector(s.getClassSpellLists),
classSpellListSpells: useSelector(s.getClassSpellListSpellsLookup),
classSpellListSpellsLookup: useSelector(s.getClassSpellListSpellsLookup),
classes: useSelector(s.getClasses),
classesModifiers: useSelector(s.getClassesModifiers),
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
@@ -170,6 +168,7 @@ export const useCharacterEngine = () => ({
creatureOwnerData: useSelector(s.getCreatureOwnerData),
creatureRules: useSelector(s.getCreatureRules),
creatures: useSelector(s.getCreatures),
currencies: useSelector(s.getCurrencies),
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
currentLevel: useSelector(s.getCurrentLevel),
@@ -235,6 +234,7 @@ export const useCharacterEngine = () => ({
inventoryContainers: useSelector(s.getInventoryContainers),
inventoryLookup: useSelector(s.getInventoryLookup),
inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
itemPlans: useSelector(s.getAvailableItemPlans),
isSheetReady: useSelector(s.isCharacterSheetReady),
isDead: useSelector(s.isDead),
isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
@@ -252,7 +252,9 @@ export const useCharacterEngine = () => ({
levelSpells: useSelector(s.getLevelSpells),
lifestyle: useSelector(s.getLifestyle),
loadSpecies: useSelector(api.makeLoadAvailableRaces),
longRestText: useSelector(sds.getLongRestText),
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
maxReplicatedItemsCount: useSelector(s.getMaxReplicatedItemsCount),
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
miscModifiers: useSelector(s.getMiscModifiers),
modifierData: useSelector(s.getModifierData),
@@ -279,7 +281,7 @@ export const useCharacterEngine = () => ({
partyInfo: useSelector(sds.getPartyInfo),
partyInventory: useSelector(s.getPartyInventory),
partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
partyInventoryItem: useSelector(s.getPartyInventoryLookup),
partyInventoryLookup: useSelector(s.getPartyInventoryLookup),
passiveInsight: useSelector(s.getPassiveInsight),
passiveInvestigation: useSelector(s.getPassiveInvestigation),
passivePerception: useSelector(s.getPassivePerception),
@@ -294,6 +296,7 @@ export const useCharacterEngine = () => ({
playerName: useSelector(s.getUsername),
preferences: useSelector(s.getCharacterPreferences),
prerequisiteData: useSelector(s.getPrerequisiteData),
premadeInfo: useSelector(char.getPremadeInfo),
processedInitiative: useSelector(s.getProcessedInitiative),
proficiency: useSelector(s.getProficiencyLookup),
proficiencyBonus: useSelector(s.getProficiencyBonus),
@@ -370,7 +373,6 @@ export const useCharacterEngine = () => ({
apiAdapterUtils,
backgroundUtils,
campaignUtils,
campaignSettingUtils,
characterUtils,
choiceUtils,
classFeatureUtils,
+2 -1
View File
@@ -1,8 +1,9 @@
import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { characterSelectors } from "@dndbeyond/character-rules-engine/es";
import { useDispatch } from "~/hooks/useDispatch";
import { toastMessageActions } from "~/tools/js/Shared/actions";
import {
+59
View File
@@ -0,0 +1,59 @@
import { useCallback } from "react";
import { KETCH_RETRY_CONFIG } from "~/constants";
import { KetchConsentResponse } from "~/types";
export const useConsent = () => {
const setupAnalyticsConsentListener = useCallback(
(onConsentChange: (hasConsent: boolean) => void) => {
let retries = 0;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
if (process.env.NODE_ENV === "development") {
onConsentChange(true);
return () => {
// noop cleanup for dev
};
}
const trySetup = () => {
if ("window" in globalThis && window.ketch) {
try {
window.ketch(
"on",
"consent",
(consentData: KetchConsentResponse) => {
onConsentChange(Boolean(consentData.purposes?.analytics));
}
);
} catch (error) {
console.error("Failed to setup Ketch consent listener:", error);
onConsentChange(false);
}
} else if (retries < KETCH_RETRY_CONFIG.maxRetries) {
retries++;
timeoutId = setTimeout(trySetup, KETCH_RETRY_CONFIG.retryInterval);
} else {
console.warn(
`Failed to initialize consent management after ${KETCH_RETRY_CONFIG.maxRetries} retries`
);
onConsentChange(false);
}
};
trySetup();
// Return cleanup function to cancel pending retries
return () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
};
},
[]
);
return {
setupAnalyticsConsentListener,
};
};
+91
View File
@@ -0,0 +1,91 @@
import { datadogRum } from "@datadog/browser-rum";
import { useEffect, useState } from "react";
import type { User } from "@dndbeyond/authentication-lib-js";
import config from "~/config";
import { useConsent } from "~/hooks/useConsent";
const isDndbeyondUrl = (url: string): boolean => {
try {
const { protocol, hostname } = new URL(url);
return (
protocol === "https:" &&
(hostname === "dndbeyond.com" || hostname.endsWith(".dndbeyond.com"))
);
} catch {
return false;
}
};
export const useDatadogRum = (user: User | null) => {
const [hasConsent, setHasConsent] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const environment = config.environment;
const version = config.version.replace(/@dndbeyond\/character-app@/, ""); // example: 1.70.116
const { setupAnalyticsConsentListener } = useConsent();
useEffect(() => {
const unsubscribe = setupAnalyticsConsentListener(setHasConsent);
return () => {
unsubscribe();
};
}, [setupAnalyticsConsentListener]);
useEffect(() => {
if (datadogRum.getInternalContext()) {
return;
}
try {
datadogRum.init({
applicationId: "2cfa3248-a632-4f5b-bf9e-aafa18dc69ae", // Not a secret, safe to include in client code
clientToken: "pub940bbbc614a4ae3ab0c872fdad597554", // Not a secret, safe to include in client code
site: "datadoghq.com",
service: "characters-app",
env: environment === "production" ? "live" : environment,
version: `${version}`,
sessionSampleRate: environment === "production" ? 10 : 100,
sessionReplaySampleRate: environment === "production" ? 1 : 10,
traceContextInjection: "sampled",
traceSampleRate: 10,
trackingConsent: "not-granted", // Start with no consent, will update when consent status is known
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: "mask-user-input",
allowedTracingUrls: [isDndbeyondUrl],
});
setIsInitialized(true);
} catch (error) {
console.warn("Datadog RUM failed to initialize:", error);
}
}, [environment]);
useEffect(() => {
if (!isInitialized) {
return;
}
if (!user) {
datadogRum.clearUser();
return;
}
datadogRum.setUser({
id: `${user.id ?? ""}`,
name: `${user.name ?? ""}`,
sub_tier: `${user.subscriptionTier ?? ""}`,
});
}, [user, isInitialized]);
useEffect(() => {
if (!isInitialized) {
return;
}
datadogRum.setTrackingConsent(hasConsent ? "granted" : "not-granted");
}, [hasConsent, isInitialized]);
};
+7
View File
@@ -0,0 +1,7 @@
import { useDispatch as useReactReduxDispatch } from "react-redux";
import { AnyAction, Dispatch } from "redux";
// TODO: We are typing useDispatch this way to work around issues with UnknownAction, the type that was introduced in Redux 5.
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
export const useDispatch =
useReactReduxDispatch.withTypes<Dispatch<AnyAction>>();
@@ -1,33 +1,34 @@
import { HTMLAttributes, useState } from "react";
import styles from '../../styles/errors.module.css';
import { HTMLAttributes, JSX, useState } from "react";
import styles from "../../styles/errors.module.css";
export interface ErrorHandlerOptions {
initialState: boolean;
errMsg: string;
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
};
initialState: boolean;
errMsg: string;
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
}
export interface ErrorHandler {
showError: boolean;
setShowError: (value: boolean) => void;
ErrorMessage: () => JSX.Element;
};
showError: boolean;
setShowError: (value: boolean) => void;
ErrorMessage: () => JSX.Element;
}
export const useErrorHandling = (
initialState: boolean,
errMsg: string,
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
initialState: boolean,
errMsg: string,
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
): ErrorHandler => {
const [showError, setShowError] = useState(initialState);
const ErrorMessage = (): JSX.Element => (
<div className={styles.inputError} {...errorMessageAttributes}>
{errMsg}
</div>
);
const [showError, setShowError] = useState(initialState);
const ErrorMessage = (): JSX.Element => (
<div className={styles.inputError} {...errorMessageAttributes}>
{errMsg}
</div>
);
return {
showError,
setShowError,
ErrorMessage,
};
return {
showError,
setShowError,
ErrorMessage,
};
};
@@ -1,49 +1,49 @@
import { HTMLAttributes } from "react";
import { HTMLAttributes, JSX } from "react";
import { useErrorHandling } from "./useErrorHandling";
export interface MaxLengthErrorHandler {
handleMaxLengthErrorMsg: (value: string) => void;
hideError: () => void;
MaxLengthErrorMessage: () => JSX.Element;
};
handleMaxLengthErrorMsg: (value: string) => void;
hideError: () => void;
MaxLengthErrorMessage: () => JSX.Element;
}
export const useMaxLengthErrorHandling = (
initialState: boolean,
maxLength: number | null,
errMsg: string = "",
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
initialState: boolean,
maxLength: number | null,
errMsg: string = "",
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
): MaxLengthErrorHandler => {
errMsg ||= `The max length is ${maxLength} characters.`;
errMsg ||= `The max length is ${maxLength} characters.`;
const {
showError,
setShowError,
ErrorMessage,
} = useErrorHandling(initialState, errMsg, errorMessageAttributes);
const { showError, setShowError, ErrorMessage } = useErrorHandling(
initialState,
errMsg,
errorMessageAttributes
);
const handleMaxLengthErrorMsg = (value: string): void => {
if (!maxLength) {
// Skip if maxLength is 0 or not set.
return;
}
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
if (isTooLong !== showError) {
setShowError(isTooLong);
}
};
const hideError = () => {
setShowError(false);
const handleMaxLengthErrorMsg = (value: string): void => {
if (!maxLength) {
// Skip if maxLength is 0 or not set.
return;
}
const MaxLengthErrorMessage = (): JSX.Element => showError && errMsg
? (<ErrorMessage />)
: <></>;
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
if (isTooLong !== showError) {
setShowError(isTooLong);
}
};
return {
handleMaxLengthErrorMsg,
MaxLengthErrorMessage,
hideError
};
}
const hideError = () => {
setShowError(false);
};
const MaxLengthErrorMessage = (): JSX.Element =>
showError && errMsg ? <ErrorMessage /> : <></>;
return {
handleMaxLengthErrorMsg,
MaxLengthErrorMessage,
hideError,
};
};
+2 -5
View File
@@ -1,9 +1,6 @@
import { useContext, useEffect, useState } from "react";
import {
ExtraManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { ExtraManager } from "@dndbeyond/character-rules-engine/es";
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
@@ -19,7 +16,7 @@ export function useExtras() {
const onUpdate = () => {
setExtras(extrasManager.getCharacterExtraManagers());
};
return FeaturesManager.subscribeToUpdates({ onUpdate });
return ExtraManager.subscribeToUpdates({ onUpdate });
}, [extrasManager]);
return extras;
+2 -1
View File
@@ -1,5 +1,5 @@
import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import {
ApiAdapterUtils,
@@ -9,6 +9,7 @@ import {
syncTransactionActions,
} from "@dndbeyond/character-rules-engine";
import { useDispatch } from "~/hooks/useDispatch";
import { toastMessageActions } from "~/tools/js/Shared/actions";
export interface ExportPdfData {
+1
View File
@@ -11,6 +11,7 @@ export interface AppUser extends User {
roles: Array<string>;
subscription: string;
subscriptionTier: string;
avatarUrl?: string;
}
/**
* AppUserState has three states:
+26 -19
View File
@@ -1,18 +1,22 @@
import ReactDOM from "react-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import "core-js/stable";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { FontProvider } from "@dndbeyond/ftui-components";
import "@dndbeyond/ttui/styles/palette.css";
import "@dndbeyond/ttui/styles/typography.css";
import wayOfLight from "@dndbeyond/ttui/themes/wayOfLight";
import { Layout } from "./components/Layout";
import { AuthProvider } from "./contexts/Authentication";
import { FeatureFlagProvider } from "./contexts/FeatureFlag";
import config from "./config";
import { AuthProvider, useAuth } from "./contexts/Authentication";
import { FeatureFlagProvider, useFeatureFlags } from "./contexts/FeatureFlag";
import { FiltersProvider } from "./contexts/Filters";
import { HeadContextProvider } from "./contexts/Head";
import { ThemeManagerProvider } from "./contexts/ThemeManager";
import { reportWebVitals } from "./helpers/reportWebVitals";
import { useDatadogRum } from "./hooks/useDatadogRum";
import { Routes } from "./routes";
import "./styles/global.css";
import "./styles/index.scss";
@@ -24,18 +28,19 @@ const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 60 * 60 * 1000 } },
});
const fonts = [
{
family: "Tiamat Condensed SC",
type: "woff2",
url: "https://www.dndbeyond.com/fonts/tiamatcondensedsc-regular-webfont.woff2",
},
{
family: "MrsEavesSmallCaps",
type: "truetype",
url: "https://www.dndbeyond.com/content/skins/waterdeep/fonts/MrsEavesSmallCaps.ttf",
},
];
const DatadogGate = () => {
const { datadogRumCharactersAppFlag } = useFeatureFlags();
return datadogRumCharactersAppFlag ? <DatadogBootstrap /> : null;
};
const DatadogBootstrap = () => {
const user = useAuth();
useDatadogRum(user ?? null);
return null;
};
const environment = config.environment;
const App = () => (
<>
@@ -43,10 +48,10 @@ const App = () => (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<ThemeManagerProvider manager={{ lightOrDark: "light" }}>
<FontProvider fonts={fonts} />
<HeadContextProvider>
<AuthProvider>
<FeatureFlagProvider>
{environment !== "development" && <DatadogGate />}
<FiltersProvider>
<Layout>
<Routes />
@@ -61,7 +66,9 @@ const App = () => (
</>
);
ReactDOM.render(<App />, document.getElementById("character-tools-target"));
const root = createRoot(document.getElementById("character-tools-target")!);
root.render(<App />);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
-13
View File
@@ -1,13 +0,0 @@
"use strict";
require("./noConflict");
var _global = _interopRequireDefault(require("core-js/library/fn/global"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
if (_global["default"]._babelPolyfill && typeof console !== "undefined" && console.warn) {
console.warn("@babel/polyfill is loaded more than once on this page. This is probably not desirable/intended " + "and may have consequences if different versions of the polyfills are applied sequentially. " + "If you do need to load the polyfill more than once, use @babel/polyfill/noConflict " + "instead to bypass the warning.");
}
_global["default"]._babelPolyfill = true;
-29
View File
@@ -1,29 +0,0 @@
"use strict";
require("core-js/es6");
require("core-js/fn/array/includes");
require("core-js/fn/array/flat-map");
require("core-js/fn/string/pad-start");
require("core-js/fn/string/pad-end");
require("core-js/fn/string/trim-start");
require("core-js/fn/string/trim-end");
require("core-js/fn/symbol/async-iterator");
require("core-js/fn/object/get-own-property-descriptors");
require("core-js/fn/object/values");
require("core-js/fn/object/entries");
require("core-js/fn/promise/finally");
require("core-js/web");
require("regenerator-runtime/runtime");
-139
View File
@@ -1,139 +0,0 @@
require('../modules/es6.symbol');
require('../modules/es6.object.create');
require('../modules/es6.object.define-property');
require('../modules/es6.object.define-properties');
require('../modules/es6.object.get-own-property-descriptor');
require('../modules/es6.object.get-prototype-of');
require('../modules/es6.object.keys');
require('../modules/es6.object.get-own-property-names');
require('../modules/es6.object.freeze');
require('../modules/es6.object.seal');
require('../modules/es6.object.prevent-extensions');
require('../modules/es6.object.is-frozen');
require('../modules/es6.object.is-sealed');
require('../modules/es6.object.is-extensible');
require('../modules/es6.object.assign');
require('../modules/es6.object.is');
require('../modules/es6.object.set-prototype-of');
require('../modules/es6.object.to-string');
require('../modules/es6.function.bind');
require('../modules/es6.function.name');
require('../modules/es6.function.has-instance');
require('../modules/es6.parse-int');
require('../modules/es6.parse-float');
require('../modules/es6.number.constructor');
require('../modules/es6.number.to-fixed');
require('../modules/es6.number.to-precision');
require('../modules/es6.number.epsilon');
require('../modules/es6.number.is-finite');
require('../modules/es6.number.is-integer');
require('../modules/es6.number.is-nan');
require('../modules/es6.number.is-safe-integer');
require('../modules/es6.number.max-safe-integer');
require('../modules/es6.number.min-safe-integer');
require('../modules/es6.number.parse-float');
require('../modules/es6.number.parse-int');
require('../modules/es6.math.acosh');
require('../modules/es6.math.asinh');
require('../modules/es6.math.atanh');
require('../modules/es6.math.cbrt');
require('../modules/es6.math.clz32');
require('../modules/es6.math.cosh');
require('../modules/es6.math.expm1');
require('../modules/es6.math.fround');
require('../modules/es6.math.hypot');
require('../modules/es6.math.imul');
require('../modules/es6.math.log10');
require('../modules/es6.math.log1p');
require('../modules/es6.math.log2');
require('../modules/es6.math.sign');
require('../modules/es6.math.sinh');
require('../modules/es6.math.tanh');
require('../modules/es6.math.trunc');
require('../modules/es6.string.from-code-point');
require('../modules/es6.string.raw');
require('../modules/es6.string.trim');
require('../modules/es6.string.iterator');
require('../modules/es6.string.code-point-at');
require('../modules/es6.string.ends-with');
require('../modules/es6.string.includes');
require('../modules/es6.string.repeat');
require('../modules/es6.string.starts-with');
require('../modules/es6.string.anchor');
require('../modules/es6.string.big');
require('../modules/es6.string.blink');
require('../modules/es6.string.bold');
require('../modules/es6.string.fixed');
require('../modules/es6.string.fontcolor');
require('../modules/es6.string.fontsize');
require('../modules/es6.string.italics');
require('../modules/es6.string.link');
require('../modules/es6.string.small');
require('../modules/es6.string.strike');
require('../modules/es6.string.sub');
require('../modules/es6.string.sup');
require('../modules/es6.date.now');
require('../modules/es6.date.to-json');
require('../modules/es6.date.to-iso-string');
require('../modules/es6.date.to-string');
require('../modules/es6.date.to-primitive');
require('../modules/es6.array.is-array');
require('../modules/es6.array.from');
require('../modules/es6.array.of');
require('../modules/es6.array.join');
require('../modules/es6.array.slice');
require('../modules/es6.array.sort');
require('../modules/es6.array.for-each');
require('../modules/es6.array.map');
require('../modules/es6.array.filter');
require('../modules/es6.array.some');
require('../modules/es6.array.every');
require('../modules/es6.array.reduce');
require('../modules/es6.array.reduce-right');
require('../modules/es6.array.index-of');
require('../modules/es6.array.last-index-of');
require('../modules/es6.array.copy-within');
require('../modules/es6.array.fill');
require('../modules/es6.array.find');
require('../modules/es6.array.find-index');
require('../modules/es6.array.species');
require('../modules/es6.array.iterator');
require('../modules/es6.regexp.constructor');
require('../modules/es6.regexp.exec');
require('../modules/es6.regexp.to-string');
require('../modules/es6.regexp.flags');
require('../modules/es6.regexp.match');
require('../modules/es6.regexp.replace');
require('../modules/es6.regexp.search');
require('../modules/es6.regexp.split');
require('../modules/es6.promise');
require('../modules/es6.map');
require('../modules/es6.set');
require('../modules/es6.weak-map');
require('../modules/es6.weak-set');
require('../modules/es6.typed.array-buffer');
require('../modules/es6.typed.data-view');
require('../modules/es6.typed.int8-array');
require('../modules/es6.typed.uint8-array');
require('../modules/es6.typed.uint8-clamped-array');
require('../modules/es6.typed.int16-array');
require('../modules/es6.typed.uint16-array');
require('../modules/es6.typed.int32-array');
require('../modules/es6.typed.uint32-array');
require('../modules/es6.typed.float32-array');
require('../modules/es6.typed.float64-array');
require('../modules/es6.reflect.apply');
require('../modules/es6.reflect.construct');
require('../modules/es6.reflect.define-property');
require('../modules/es6.reflect.delete-property');
require('../modules/es6.reflect.enumerate');
require('../modules/es6.reflect.get');
require('../modules/es6.reflect.get-own-property-descriptor');
require('../modules/es6.reflect.get-prototype-of');
require('../modules/es6.reflect.has');
require('../modules/es6.reflect.is-extensible');
require('../modules/es6.reflect.own-keys');
require('../modules/es6.reflect.prevent-extensions');
require('../modules/es6.reflect.set');
require('../modules/es6.reflect.set-prototype-of');
module.exports = require('../modules/_core');
@@ -1,2 +0,0 @@
require('../../modules/es7.array.flat-map');
module.exports = require('../../modules/_core').Array.flatMap;
@@ -1,2 +0,0 @@
require('../../modules/es7.array.includes');
module.exports = require('../../modules/_core').Array.includes;
@@ -1,2 +0,0 @@
require('../../modules/es7.object.entries');
module.exports = require('../../modules/_core').Object.entries;
@@ -1,2 +0,0 @@
require('../../modules/es7.object.get-own-property-descriptors');
module.exports = require('../../modules/_core').Object.getOwnPropertyDescriptors;
@@ -1,2 +0,0 @@
require('../../modules/es7.object.values');
module.exports = require('../../modules/_core').Object.values;
@@ -1,4 +0,0 @@
'use strict';
require('../../modules/es6.promise');
require('../../modules/es7.promise.finally');
module.exports = require('../../modules/_core').Promise['finally'];
@@ -1,2 +0,0 @@
require('../../modules/es7.string.pad-end');
module.exports = require('../../modules/_core').String.padEnd;
@@ -1,2 +0,0 @@
require('../../modules/es7.string.pad-start');
module.exports = require('../../modules/_core').String.padStart;
@@ -1,2 +0,0 @@
require('../../modules/es7.string.trim-right');
module.exports = require('../../modules/_core').String.trimRight;
@@ -1,2 +0,0 @@
require('../../modules/es7.string.trim-left');
module.exports = require('../../modules/_core').String.trimLeft;
@@ -1,2 +0,0 @@
require('../../modules/es7.symbol.async-iterator');
module.exports = require('../../modules/_wks-ext').f('asyncIterator');
@@ -1,2 +0,0 @@
require('../modules/es7.global');
module.exports = require('../modules/_core').global;
@@ -1,4 +0,0 @@
module.exports = function (it) {
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
return it;
};
@@ -1,5 +0,0 @@
var isObject = require('./_is-object');
module.exports = function (it) {
if (!isObject(it)) throw TypeError(it + ' is not an object!');
return it;
};
@@ -1,2 +0,0 @@
var core = module.exports = { version: '2.6.12' };
if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef
@@ -1,20 +0,0 @@
// optional / simple context binding
var aFunction = require('./_a-function');
module.exports = function (fn, that, length) {
aFunction(fn);
if (that === undefined) return fn;
switch (length) {
case 1: return function (a) {
return fn.call(that, a);
};
case 2: return function (a, b) {
return fn.call(that, a, b);
};
case 3: return function (a, b, c) {
return fn.call(that, a, b, c);
};
}
return function (/* ...args */) {
return fn.apply(that, arguments);
};
};
@@ -1,4 +0,0 @@
// Thank's IE8 for his funny defineProperty
module.exports = !require('./_fails')(function () {
return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7;
});
@@ -1,7 +0,0 @@
var isObject = require('./_is-object');
var document = require('./_global').document;
// typeof document.createElement is 'object' in old IE
var is = isObject(document) && isObject(document.createElement);
module.exports = function (it) {
return is ? document.createElement(it) : {};
};
@@ -1,62 +0,0 @@
var global = require('./_global');
var core = require('./_core');
var ctx = require('./_ctx');
var hide = require('./_hide');
var has = require('./_has');
var PROTOTYPE = 'prototype';
var $export = function (type, name, source) {
var IS_FORCED = type & $export.F;
var IS_GLOBAL = type & $export.G;
var IS_STATIC = type & $export.S;
var IS_PROTO = type & $export.P;
var IS_BIND = type & $export.B;
var IS_WRAP = type & $export.W;
var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
var expProto = exports[PROTOTYPE];
var target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
var key, own, out;
if (IS_GLOBAL) source = name;
for (key in source) {
// contains in native
own = !IS_FORCED && target && target[key] !== undefined;
if (own && has(exports, key)) continue;
// export native or passed
out = own ? target[key] : source[key];
// prevent global pollution for namespaces
exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key]
// bind timers to global for call from export context
: IS_BIND && own ? ctx(out, global)
// wrap global constructors for prevent change them in library
: IS_WRAP && target[key] == out ? (function (C) {
var F = function (a, b, c) {
if (this instanceof C) {
switch (arguments.length) {
case 0: return new C();
case 1: return new C(a);
case 2: return new C(a, b);
} return new C(a, b, c);
} return C.apply(this, arguments);
};
F[PROTOTYPE] = C[PROTOTYPE];
return F;
// make static versions for prototype methods
})(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
// export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
if (IS_PROTO) {
(exports.virtual || (exports.virtual = {}))[key] = out;
// export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
if (type & $export.R && expProto && !expProto[key]) hide(expProto, key, out);
}
}
};
// type bitmap
$export.F = 1; // forced
$export.G = 2; // global
$export.S = 4; // static
$export.P = 8; // proto
$export.B = 16; // bind
$export.W = 32; // wrap
$export.U = 64; // safe
$export.R = 128; // real proto method for `library`
module.exports = $export;
@@ -1,7 +0,0 @@
module.exports = function (exec) {
try {
return !!exec();
} catch (e) {
return true;
}
};
@@ -1,6 +0,0 @@
// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
var global = module.exports = typeof window != 'undefined' && window.Math == Math
? window : typeof self != 'undefined' && self.Math == Math ? self
// eslint-disable-next-line no-new-func
: Function('return this')();
if (typeof __g == 'number') __g = global; // eslint-disable-line no-undef
@@ -1,4 +0,0 @@
var hasOwnProperty = {}.hasOwnProperty;
module.exports = function (it, key) {
return hasOwnProperty.call(it, key);
};
@@ -1,8 +0,0 @@
var dP = require('./_object-dp');
var createDesc = require('./_property-desc');
module.exports = require('./_descriptors') ? function (object, key, value) {
return dP.f(object, key, createDesc(1, value));
} : function (object, key, value) {
object[key] = value;
return object;
};
@@ -1,3 +0,0 @@
module.exports = !require('./_descriptors') && !require('./_fails')(function () {
return Object.defineProperty(require('./_dom-create')('div'), 'a', { get: function () { return 7; } }).a != 7;
});
@@ -1,3 +0,0 @@
module.exports = function (it) {
return typeof it === 'object' ? it !== null : typeof it === 'function';
};
@@ -1,16 +0,0 @@
var anObject = require('./_an-object');
var IE8_DOM_DEFINE = require('./_ie8-dom-define');
var toPrimitive = require('./_to-primitive');
var dP = Object.defineProperty;
exports.f = require('./_descriptors') ? Object.defineProperty : function defineProperty(O, P, Attributes) {
anObject(O);
P = toPrimitive(P, true);
anObject(Attributes);
if (IE8_DOM_DEFINE) try {
return dP(O, P, Attributes);
} catch (e) { /* empty */ }
if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported!');
if ('value' in Attributes) O[P] = Attributes.value;
return O;
};
@@ -1,8 +0,0 @@
module.exports = function (bitmap, value) {
return {
enumerable: !(bitmap & 1),
configurable: !(bitmap & 2),
writable: !(bitmap & 4),
value: value
};
};
@@ -1,12 +0,0 @@
// 7.1.1 ToPrimitive(input [, PreferredType])
var isObject = require('./_is-object');
// instead of the ES6 spec version, we didn't implement @@toPrimitive case
// and the second argument - flag - preferred type is a string
module.exports = function (it, S) {
if (!isObject(it)) return it;
var fn, val;
if (S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
if (typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it))) return val;
if (!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
throw TypeError("Can't convert object to primitive value");
};
@@ -1,4 +0,0 @@
// https://github.com/tc39/proposal-global
var $export = require('./_export');
$export($export.G, { global: require('./_global') });
@@ -1,4 +0,0 @@
module.exports = function (it) {
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
return it;
};
@@ -1,5 +0,0 @@
var cof = require('./_cof');
module.exports = function (it, msg) {
if (typeof it != 'number' && cof(it) != 'Number') throw TypeError(msg);
return +it;
};
@@ -1,7 +0,0 @@
// 22.1.3.31 Array.prototype[@@unscopables]
var UNSCOPABLES = require('./_wks')('unscopables');
var ArrayProto = Array.prototype;
if (ArrayProto[UNSCOPABLES] == undefined) require('./_hide')(ArrayProto, UNSCOPABLES, {});
module.exports = function (key) {
ArrayProto[UNSCOPABLES][key] = true;
};
@@ -1,8 +0,0 @@
'use strict';
var at = require('./_string-at')(true);
// `AdvanceStringIndex` abstract operation
// https://tc39.github.io/ecma262/#sec-advancestringindex
module.exports = function (S, index, unicode) {
return index + (unicode ? at(S, index).length : 1);
};
@@ -1,5 +0,0 @@
module.exports = function (it, Constructor, name, forbiddenField) {
if (!(it instanceof Constructor) || (forbiddenField !== undefined && forbiddenField in it)) {
throw TypeError(name + ': incorrect invocation!');
} return it;
};
@@ -1,5 +0,0 @@
var isObject = require('./_is-object');
module.exports = function (it) {
if (!isObject(it)) throw TypeError(it + ' is not an object!');
return it;
};
@@ -1,26 +0,0 @@
// 22.1.3.3 Array.prototype.copyWithin(target, start, end = this.length)
'use strict';
var toObject = require('./_to-object');
var toAbsoluteIndex = require('./_to-absolute-index');
var toLength = require('./_to-length');
module.exports = [].copyWithin || function copyWithin(target /* = 0 */, start /* = 0, end = @length */) {
var O = toObject(this);
var len = toLength(O.length);
var to = toAbsoluteIndex(target, len);
var from = toAbsoluteIndex(start, len);
var end = arguments.length > 2 ? arguments[2] : undefined;
var count = Math.min((end === undefined ? len : toAbsoluteIndex(end, len)) - from, len - to);
var inc = 1;
if (from < to && to < from + count) {
inc = -1;
from += count - 1;
to += count - 1;
}
while (count-- > 0) {
if (from in O) O[to] = O[from];
else delete O[to];
to += inc;
from += inc;
} return O;
};
@@ -1,15 +0,0 @@
// 22.1.3.6 Array.prototype.fill(value, start = 0, end = this.length)
'use strict';
var toObject = require('./_to-object');
var toAbsoluteIndex = require('./_to-absolute-index');
var toLength = require('./_to-length');
module.exports = function fill(value /* , start = 0, end = @length */) {
var O = toObject(this);
var length = toLength(O.length);
var aLen = arguments.length;
var index = toAbsoluteIndex(aLen > 1 ? arguments[1] : undefined, length);
var end = aLen > 2 ? arguments[2] : undefined;
var endPos = end === undefined ? length : toAbsoluteIndex(end, length);
while (endPos > index) O[index++] = value;
return O;
};
@@ -1,23 +0,0 @@
// false -> Array#indexOf
// true -> Array#includes
var toIObject = require('./_to-iobject');
var toLength = require('./_to-length');
var toAbsoluteIndex = require('./_to-absolute-index');
module.exports = function (IS_INCLUDES) {
return function ($this, el, fromIndex) {
var O = toIObject($this);
var length = toLength(O.length);
var index = toAbsoluteIndex(fromIndex, length);
var value;
// Array#includes uses SameValueZero equality algorithm
// eslint-disable-next-line no-self-compare
if (IS_INCLUDES && el != el) while (length > index) {
value = O[index++];
// eslint-disable-next-line no-self-compare
if (value != value) return true;
// Array#indexOf ignores holes, Array#includes - not
} else for (;length > index; index++) if (IS_INCLUDES || index in O) {
if (O[index] === el) return IS_INCLUDES || index || 0;
} return !IS_INCLUDES && -1;
};
};
@@ -1,44 +0,0 @@
// 0 -> Array#forEach
// 1 -> Array#map
// 2 -> Array#filter
// 3 -> Array#some
// 4 -> Array#every
// 5 -> Array#find
// 6 -> Array#findIndex
var ctx = require('./_ctx');
var IObject = require('./_iobject');
var toObject = require('./_to-object');
var toLength = require('./_to-length');
var asc = require('./_array-species-create');
module.exports = function (TYPE, $create) {
var IS_MAP = TYPE == 1;
var IS_FILTER = TYPE == 2;
var IS_SOME = TYPE == 3;
var IS_EVERY = TYPE == 4;
var IS_FIND_INDEX = TYPE == 6;
var NO_HOLES = TYPE == 5 || IS_FIND_INDEX;
var create = $create || asc;
return function ($this, callbackfn, that) {
var O = toObject($this);
var self = IObject(O);
var f = ctx(callbackfn, that, 3);
var length = toLength(self.length);
var index = 0;
var result = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined;
var val, res;
for (;length > index; index++) if (NO_HOLES || index in self) {
val = self[index];
res = f(val, index, O);
if (TYPE) {
if (IS_MAP) result[index] = res; // map
else if (res) switch (TYPE) {
case 3: return true; // some
case 5: return val; // find
case 6: return index; // findIndex
case 2: result.push(val); // filter
} else if (IS_EVERY) return false; // every
}
}
return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : result;
};
};
@@ -1,28 +0,0 @@
var aFunction = require('./_a-function');
var toObject = require('./_to-object');
var IObject = require('./_iobject');
var toLength = require('./_to-length');
module.exports = function (that, callbackfn, aLen, memo, isRight) {
aFunction(callbackfn);
var O = toObject(that);
var self = IObject(O);
var length = toLength(O.length);
var index = isRight ? length - 1 : 0;
var i = isRight ? -1 : 1;
if (aLen < 2) for (;;) {
if (index in self) {
memo = self[index];
index += i;
break;
}
index += i;
if (isRight ? index < 0 : length <= index) {
throw TypeError('Reduce of empty array with no initial value');
}
}
for (;isRight ? index >= 0 : length > index; index += i) if (index in self) {
memo = callbackfn(memo, self[index], index, O);
}
return memo;
};
@@ -1,16 +0,0 @@
var isObject = require('./_is-object');
var isArray = require('./_is-array');
var SPECIES = require('./_wks')('species');
module.exports = function (original) {
var C;
if (isArray(original)) {
C = original.constructor;
// cross-realm fallback
if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined;
if (isObject(C)) {
C = C[SPECIES];
if (C === null) C = undefined;
}
} return C === undefined ? Array : C;
};
@@ -1,6 +0,0 @@
// 9.4.2.3 ArraySpeciesCreate(originalArray, length)
var speciesConstructor = require('./_array-species-constructor');
module.exports = function (original, length) {
return new (speciesConstructor(original))(length);
};
@@ -1,25 +0,0 @@
'use strict';
var aFunction = require('./_a-function');
var isObject = require('./_is-object');
var invoke = require('./_invoke');
var arraySlice = [].slice;
var factories = {};
var construct = function (F, len, args) {
if (!(len in factories)) {
for (var n = [], i = 0; i < len; i++) n[i] = 'a[' + i + ']';
// eslint-disable-next-line no-new-func
factories[len] = Function('F,a', 'return new F(' + n.join(',') + ')');
} return factories[len](F, args);
};
module.exports = Function.bind || function bind(that /* , ...args */) {
var fn = aFunction(this);
var partArgs = arraySlice.call(arguments, 1);
var bound = function (/* args... */) {
var args = partArgs.concat(arraySlice.call(arguments));
return this instanceof bound ? construct(fn, args.length, args) : invoke(fn, args, that);
};
if (isObject(fn.prototype)) bound.prototype = fn.prototype;
return bound;
};
@@ -1,23 +0,0 @@
// getting tag from 19.1.3.6 Object.prototype.toString()
var cof = require('./_cof');
var TAG = require('./_wks')('toStringTag');
// ES3 wrong here
var ARG = cof(function () { return arguments; }()) == 'Arguments';
// fallback for IE11 Script Access Denied error
var tryGet = function (it, key) {
try {
return it[key];
} catch (e) { /* empty */ }
};
module.exports = function (it) {
var O, T, B;
return it === undefined ? 'Undefined' : it === null ? 'Null'
// @@toStringTag case
: typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T
// builtinTag case
: ARG ? cof(O)
// ES3 arguments fallback
: (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B;
};
@@ -1,5 +0,0 @@
var toString = {}.toString;
module.exports = function (it) {
return toString.call(it).slice(8, -1);
};
@@ -1,144 +0,0 @@
'use strict';
var dP = require('./_object-dp').f;
var create = require('./_object-create');
var redefineAll = require('./_redefine-all');
var ctx = require('./_ctx');
var anInstance = require('./_an-instance');
var forOf = require('./_for-of');
var $iterDefine = require('./_iter-define');
var step = require('./_iter-step');
var setSpecies = require('./_set-species');
var DESCRIPTORS = require('./_descriptors');
var fastKey = require('./_meta').fastKey;
var validate = require('./_validate-collection');
var SIZE = DESCRIPTORS ? '_s' : 'size';
var getEntry = function (that, key) {
// fast case
var index = fastKey(key);
var entry;
if (index !== 'F') return that._i[index];
// frozen object case
for (entry = that._f; entry; entry = entry.n) {
if (entry.k == key) return entry;
}
};
module.exports = {
getConstructor: function (wrapper, NAME, IS_MAP, ADDER) {
var C = wrapper(function (that, iterable) {
anInstance(that, C, NAME, '_i');
that._t = NAME; // collection type
that._i = create(null); // index
that._f = undefined; // first entry
that._l = undefined; // last entry
that[SIZE] = 0; // size
if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
});
redefineAll(C.prototype, {
// 23.1.3.1 Map.prototype.clear()
// 23.2.3.2 Set.prototype.clear()
clear: function clear() {
for (var that = validate(this, NAME), data = that._i, entry = that._f; entry; entry = entry.n) {
entry.r = true;
if (entry.p) entry.p = entry.p.n = undefined;
delete data[entry.i];
}
that._f = that._l = undefined;
that[SIZE] = 0;
},
// 23.1.3.3 Map.prototype.delete(key)
// 23.2.3.4 Set.prototype.delete(value)
'delete': function (key) {
var that = validate(this, NAME);
var entry = getEntry(that, key);
if (entry) {
var next = entry.n;
var prev = entry.p;
delete that._i[entry.i];
entry.r = true;
if (prev) prev.n = next;
if (next) next.p = prev;
if (that._f == entry) that._f = next;
if (that._l == entry) that._l = prev;
that[SIZE]--;
} return !!entry;
},
// 23.2.3.6 Set.prototype.forEach(callbackfn, thisArg = undefined)
// 23.1.3.5 Map.prototype.forEach(callbackfn, thisArg = undefined)
forEach: function forEach(callbackfn /* , that = undefined */) {
validate(this, NAME);
var f = ctx(callbackfn, arguments.length > 1 ? arguments[1] : undefined, 3);
var entry;
while (entry = entry ? entry.n : this._f) {
f(entry.v, entry.k, this);
// revert to the last existing entry
while (entry && entry.r) entry = entry.p;
}
},
// 23.1.3.7 Map.prototype.has(key)
// 23.2.3.7 Set.prototype.has(value)
has: function has(key) {
return !!getEntry(validate(this, NAME), key);
}
});
if (DESCRIPTORS) dP(C.prototype, 'size', {
get: function () {
return validate(this, NAME)[SIZE];
}
});
return C;
},
def: function (that, key, value) {
var entry = getEntry(that, key);
var prev, index;
// change existing entry
if (entry) {
entry.v = value;
// create new entry
} else {
that._l = entry = {
i: index = fastKey(key, true), // <- index
k: key, // <- key
v: value, // <- value
p: prev = that._l, // <- previous entry
n: undefined, // <- next entry
r: false // <- removed
};
if (!that._f) that._f = entry;
if (prev) prev.n = entry;
that[SIZE]++;
// add to index
if (index !== 'F') that._i[index] = entry;
} return that;
},
getEntry: getEntry,
setStrong: function (C, NAME, IS_MAP) {
// add .keys, .values, .entries, [@@iterator]
// 23.1.3.4, 23.1.3.8, 23.1.3.11, 23.1.3.12, 23.2.3.5, 23.2.3.8, 23.2.3.10, 23.2.3.11
$iterDefine(C, NAME, function (iterated, kind) {
this._t = validate(iterated, NAME); // target
this._k = kind; // kind
this._l = undefined; // previous
}, function () {
var that = this;
var kind = that._k;
var entry = that._l;
// revert to the last existing entry
while (entry && entry.r) entry = entry.p;
// get next entry
if (!that._t || !(that._l = entry = entry ? entry.n : that._t._f)) {
// or finish the iteration
that._t = undefined;
return step(1);
}
// return step by kind
if (kind == 'keys') return step(0, entry.k);
if (kind == 'values') return step(0, entry.v);
return step(0, [entry.k, entry.v]);
}, IS_MAP ? 'entries' : 'values', !IS_MAP, true);
// add [@@species], 23.1.2.2, 23.2.2.2
setSpecies(NAME);
}
};
@@ -1,85 +0,0 @@
'use strict';
var redefineAll = require('./_redefine-all');
var getWeak = require('./_meta').getWeak;
var anObject = require('./_an-object');
var isObject = require('./_is-object');
var anInstance = require('./_an-instance');
var forOf = require('./_for-of');
var createArrayMethod = require('./_array-methods');
var $has = require('./_has');
var validate = require('./_validate-collection');
var arrayFind = createArrayMethod(5);
var arrayFindIndex = createArrayMethod(6);
var id = 0;
// fallback for uncaught frozen keys
var uncaughtFrozenStore = function (that) {
return that._l || (that._l = new UncaughtFrozenStore());
};
var UncaughtFrozenStore = function () {
this.a = [];
};
var findUncaughtFrozen = function (store, key) {
return arrayFind(store.a, function (it) {
return it[0] === key;
});
};
UncaughtFrozenStore.prototype = {
get: function (key) {
var entry = findUncaughtFrozen(this, key);
if (entry) return entry[1];
},
has: function (key) {
return !!findUncaughtFrozen(this, key);
},
set: function (key, value) {
var entry = findUncaughtFrozen(this, key);
if (entry) entry[1] = value;
else this.a.push([key, value]);
},
'delete': function (key) {
var index = arrayFindIndex(this.a, function (it) {
return it[0] === key;
});
if (~index) this.a.splice(index, 1);
return !!~index;
}
};
module.exports = {
getConstructor: function (wrapper, NAME, IS_MAP, ADDER) {
var C = wrapper(function (that, iterable) {
anInstance(that, C, NAME, '_i');
that._t = NAME; // collection type
that._i = id++; // collection id
that._l = undefined; // leak store for uncaught frozen objects
if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
});
redefineAll(C.prototype, {
// 23.3.3.2 WeakMap.prototype.delete(key)
// 23.4.3.3 WeakSet.prototype.delete(value)
'delete': function (key) {
if (!isObject(key)) return false;
var data = getWeak(key);
if (data === true) return uncaughtFrozenStore(validate(this, NAME))['delete'](key);
return data && $has(data, this._i) && delete data[this._i];
},
// 23.3.3.4 WeakMap.prototype.has(key)
// 23.4.3.4 WeakSet.prototype.has(value)
has: function has(key) {
if (!isObject(key)) return false;
var data = getWeak(key);
if (data === true) return uncaughtFrozenStore(validate(this, NAME)).has(key);
return data && $has(data, this._i);
}
});
return C;
},
def: function (that, key, value) {
var data = getWeak(anObject(key), true);
if (data === true) uncaughtFrozenStore(that).set(key, value);
else data[that._i] = value;
return that;
},
ufstore: uncaughtFrozenStore
};
@@ -1,85 +0,0 @@
'use strict';
var global = require('./_global');
var $export = require('./_export');
var redefine = require('./_redefine');
var redefineAll = require('./_redefine-all');
var meta = require('./_meta');
var forOf = require('./_for-of');
var anInstance = require('./_an-instance');
var isObject = require('./_is-object');
var fails = require('./_fails');
var $iterDetect = require('./_iter-detect');
var setToStringTag = require('./_set-to-string-tag');
var inheritIfRequired = require('./_inherit-if-required');
module.exports = function (NAME, wrapper, methods, common, IS_MAP, IS_WEAK) {
var Base = global[NAME];
var C = Base;
var ADDER = IS_MAP ? 'set' : 'add';
var proto = C && C.prototype;
var O = {};
var fixMethod = function (KEY) {
var fn = proto[KEY];
redefine(proto, KEY,
KEY == 'delete' ? function (a) {
return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
} : KEY == 'has' ? function has(a) {
return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
} : KEY == 'get' ? function get(a) {
return IS_WEAK && !isObject(a) ? undefined : fn.call(this, a === 0 ? 0 : a);
} : KEY == 'add' ? function add(a) { fn.call(this, a === 0 ? 0 : a); return this; }
: function set(a, b) { fn.call(this, a === 0 ? 0 : a, b); return this; }
);
};
if (typeof C != 'function' || !(IS_WEAK || proto.forEach && !fails(function () {
new C().entries().next();
}))) {
// create collection constructor
C = common.getConstructor(wrapper, NAME, IS_MAP, ADDER);
redefineAll(C.prototype, methods);
meta.NEED = true;
} else {
var instance = new C();
// early implementations not supports chaining
var HASNT_CHAINING = instance[ADDER](IS_WEAK ? {} : -0, 1) != instance;
// V8 ~ Chromium 40- weak-collections throws on primitives, but should return false
var THROWS_ON_PRIMITIVES = fails(function () { instance.has(1); });
// most early implementations doesn't supports iterables, most modern - not close it correctly
var ACCEPT_ITERABLES = $iterDetect(function (iter) { new C(iter); }); // eslint-disable-line no-new
// for early implementations -0 and +0 not the same
var BUGGY_ZERO = !IS_WEAK && fails(function () {
// V8 ~ Chromium 42- fails only with 5+ elements
var $instance = new C();
var index = 5;
while (index--) $instance[ADDER](index, index);
return !$instance.has(-0);
});
if (!ACCEPT_ITERABLES) {
C = wrapper(function (target, iterable) {
anInstance(target, C, NAME);
var that = inheritIfRequired(new Base(), target, C);
if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
return that;
});
C.prototype = proto;
proto.constructor = C;
}
if (THROWS_ON_PRIMITIVES || BUGGY_ZERO) {
fixMethod('delete');
fixMethod('has');
IS_MAP && fixMethod('get');
}
if (BUGGY_ZERO || HASNT_CHAINING) fixMethod(ADDER);
// weak collections should not contains .clear method
if (IS_WEAK && proto.clear) delete proto.clear;
}
setToStringTag(C, NAME);
O[NAME] = C;
$export($export.G + $export.W + $export.F * (C != Base), O);
if (!IS_WEAK) common.setStrong(C, NAME, IS_MAP);
return C;
};

Some files were not shown because too many files have changed in this diff Show More