New source found from dndbeyond.com
This commit is contained in:
@@ -14,13 +14,20 @@ import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.sv
|
|||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export type AccordionVariant =
|
||||||
|
| "default"
|
||||||
|
| "text"
|
||||||
|
| "paper"
|
||||||
|
| "minimal"
|
||||||
|
| "solid";
|
||||||
|
|
||||||
export interface AccordionProps extends HTMLAttributes<HTMLElement> {
|
export interface AccordionProps extends HTMLAttributes<HTMLElement> {
|
||||||
variant?: "default" | "text" | "paper";
|
variant?: AccordionVariant;
|
||||||
summary: ReactNode;
|
summary: ReactNode;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
size?: "small" | "medium";
|
size?: "xx-small" | "x-small" | "small" | "medium";
|
||||||
forceShow?: boolean;
|
forceShow?: boolean;
|
||||||
useTheme?: boolean;
|
themed?: boolean;
|
||||||
resetOpen?: boolean;
|
resetOpen?: boolean;
|
||||||
override?: boolean | null;
|
override?: boolean | null;
|
||||||
handleIsOpen?: (id: string, isOpen: boolean) => void;
|
handleIsOpen?: (id: string, isOpen: boolean) => void;
|
||||||
@@ -40,8 +47,7 @@ export const Accordion: FC<AccordionProps> = ({
|
|||||||
variant = "default",
|
variant = "default",
|
||||||
children,
|
children,
|
||||||
forceShow = false,
|
forceShow = false,
|
||||||
// style,
|
themed,
|
||||||
useTheme,
|
|
||||||
resetOpen,
|
resetOpen,
|
||||||
handleIsOpen,
|
handleIsOpen,
|
||||||
override = null,
|
override = null,
|
||||||
@@ -101,7 +107,7 @@ export const Accordion: FC<AccordionProps> = ({
|
|||||||
styles.details,
|
styles.details,
|
||||||
styles[size],
|
styles[size],
|
||||||
styles[variant],
|
styles[variant],
|
||||||
useTheme && styles.theme,
|
themed && styles.themed,
|
||||||
showAlert && styles.alert,
|
showAlert && styles.alert,
|
||||||
className,
|
className,
|
||||||
])}
|
])}
|
||||||
@@ -113,6 +119,7 @@ export const Accordion: FC<AccordionProps> = ({
|
|||||||
className={styles.summary}
|
className={styles.summary}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
ref={summaryRef}
|
ref={summaryRef}
|
||||||
|
data-testid="accordion-summary"
|
||||||
>
|
>
|
||||||
{summaryImage && (
|
{summaryImage && (
|
||||||
<img
|
<img
|
||||||
@@ -123,7 +130,7 @@ export const Accordion: FC<AccordionProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={styles.heading}>
|
<div className={styles.heading}>
|
||||||
<div>
|
<div className={styles.summaryContent}>
|
||||||
{summary}
|
{summary}
|
||||||
{summaryMetaItems && (
|
{summaryMetaItems && (
|
||||||
<div className={styles.metaItems}>
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FC } from "react";
|
import Color from "color";
|
||||||
|
import { CSSProperties, FC } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button as TtuiButton,
|
Button as TtuiButton,
|
||||||
@@ -33,7 +34,14 @@ export const Button: FC<ButtonProps> = ({
|
|||||||
const isCustomSize = size === "xx-small";
|
const isCustomSize = size === "xx-small";
|
||||||
const isCustomVariant = variant === "builder" || variant === "builder-text";
|
const isCustomVariant = variant === "builder" || variant === "builder-text";
|
||||||
const isCustomColor = color === "builder-green";
|
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 (
|
return (
|
||||||
<TtuiButton
|
<TtuiButton
|
||||||
@@ -53,6 +61,7 @@ export const Button: FC<ButtonProps> = ({
|
|||||||
variant={!isCustomVariant ? variant : undefined}
|
variant={!isCustomVariant ? variant : undefined}
|
||||||
// Children are included in props
|
// Children are included in props
|
||||||
color={!isCustomColor ? color : undefined}
|
color={!isCustomColor ? color : undefined}
|
||||||
|
style={overrideStyles}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ interface CheckboxProps extends TtuiCheckboxProps {
|
|||||||
themed?: boolean;
|
themed?: boolean;
|
||||||
darkMode?: boolean;
|
darkMode?: boolean;
|
||||||
variant?: "default" | "builder" | "sidebar";
|
variant?: "default" | "builder" | "sidebar";
|
||||||
onClick?: (isEnabled: boolean) => void;
|
onChangeConfirm?: (
|
||||||
onChangePromise?: (
|
|
||||||
newIsEnabled: boolean,
|
newIsEnabled: boolean,
|
||||||
accept: () => void,
|
accept: () => void,
|
||||||
reject: () => void
|
reject: () => void
|
||||||
@@ -26,51 +25,52 @@ export const Checkbox: FC<CheckboxProps> = ({
|
|||||||
variant = "default",
|
variant = "default",
|
||||||
darkMode,
|
darkMode,
|
||||||
checked = false,
|
checked = false,
|
||||||
onChangePromise,
|
onChangeConfirm,
|
||||||
onClick,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [isChecked, setIsChecked] = useState(checked);
|
const [isChecked, setIsChecked] = useState(checked);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsChecked(checked);
|
|
||||||
}, [checked]);
|
|
||||||
|
|
||||||
const handleChange = (evt: ChangeEvent<HTMLInputElement>): void => {
|
const handleChange = (evt: ChangeEvent<HTMLInputElement>): void => {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
evt.nativeEvent.stopImmediatePropagation();
|
evt.nativeEvent.stopImmediatePropagation();
|
||||||
|
|
||||||
const newCheckedState = !isChecked;
|
const newCheckedState = evt.target.checked;
|
||||||
|
|
||||||
// promise-based logic
|
// promise-based logic
|
||||||
if (onChangePromise) {
|
if (onChangeConfirm) {
|
||||||
onChangePromise(
|
onChangeConfirm(
|
||||||
newCheckedState,
|
newCheckedState,
|
||||||
() => {
|
() => {
|
||||||
// Promise accepted, update the state
|
// Promise accepted, update the state
|
||||||
setIsChecked(newCheckedState);
|
setIsChecked(newCheckedState);
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
// Promise rejected, do nothing
|
// Promise rejected, reset the checkbox to its previous state
|
||||||
|
setIsChecked(!newCheckedState);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Update the state immediately if no promise handling - only IF onClick is provided
|
// Update the state immediately if no promise handling - only IF onClick is provided
|
||||||
if (onClick) {
|
if (onChange) {
|
||||||
onClick(newCheckedState);
|
onChange(evt);
|
||||||
setIsChecked(newCheckedState);
|
setIsChecked(newCheckedState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsChecked(checked);
|
||||||
|
}, [checked]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TtuiCheckbox
|
<TtuiCheckbox
|
||||||
className={clsx([
|
className={clsx([
|
||||||
|
styles.checkbox,
|
||||||
|
styles[variant],
|
||||||
themed && styles.themed,
|
themed && styles.themed,
|
||||||
darkMode && styles[darkMode ? "darkMode" : "lightMode"],
|
|
||||||
className,
|
className,
|
||||||
variant === "default" && styles.default,
|
|
||||||
variant === "builder" && styles.builder,
|
|
||||||
])}
|
])}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
aria-pressed={isChecked}
|
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 { FC, useEffect } from "react";
|
||||||
|
|
||||||
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
|
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 { Button, ButtonProps } from "../Button";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|||||||
@@ -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 { FC, HTMLAttributes } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
characterActions,
|
characterActions,
|
||||||
ChoiceUtils,
|
ChoiceUtils,
|
||||||
|
ContainerManager,
|
||||||
} from "@dndbeyond/character-rules-engine";
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
|
import { useDispatch } from "~/hooks/useDispatch";
|
||||||
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
|
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
|
||||||
import { CharClass, Choice, ClassDefinitionContract, Feat } from "~/types";
|
import { CharClass, Choice, ClassDefinitionContract, Feat } from "~/types";
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ export interface FeatureChoiceProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
charClass?: CharClass;
|
charClass?: CharClass;
|
||||||
featsData: Feat[];
|
featsData: Feat[];
|
||||||
subclassData?: ClassDefinitionContract[];
|
subclassData?: ClassDefinitionContract[];
|
||||||
|
itemShoppe?: ContainerManager | null;
|
||||||
onChoiceChange: (
|
onChoiceChange: (
|
||||||
choiceId: string,
|
choiceId: string,
|
||||||
type: number,
|
type: number,
|
||||||
@@ -33,6 +35,7 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
|
|||||||
choice,
|
choice,
|
||||||
featsData,
|
featsData,
|
||||||
subclassData,
|
subclassData,
|
||||||
|
itemShoppe,
|
||||||
onChoiceChange,
|
onChoiceChange,
|
||||||
className,
|
className,
|
||||||
collapseDescription,
|
collapseDescription,
|
||||||
@@ -69,13 +72,14 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { description, options, featSubChoices } =
|
const { description, options, featSubChoices, introDescription } =
|
||||||
ChoiceUtils.getSortedChoiceOptionsForFeaturesInfo(
|
ChoiceUtils.getSortedChoiceOptionsForFeaturesInfo(
|
||||||
choice,
|
choice,
|
||||||
featsData,
|
featsData,
|
||||||
featLookup,
|
featLookup,
|
||||||
charClass || null,
|
charClass || null,
|
||||||
subclassData || [],
|
subclassData || [],
|
||||||
|
itemShoppe || null,
|
||||||
preferences,
|
preferences,
|
||||||
prerequisiteData,
|
prerequisiteData,
|
||||||
ruleData,
|
ruleData,
|
||||||
@@ -90,6 +94,7 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
|
|||||||
options={options}
|
options={options}
|
||||||
onChange={handleChoiceChange}
|
onChange={handleChoiceChange}
|
||||||
description={description || ""}
|
description={description || ""}
|
||||||
|
descriptionIntro={introDescription || ""}
|
||||||
choiceInfo={choiceInfo}
|
choiceInfo={choiceInfo}
|
||||||
classId={charClass && classUtils.getId(charClass)}
|
classId={charClass && classUtils.getId(charClass)}
|
||||||
showBackgroundProficiencyOptions={true}
|
showBackgroundProficiencyOptions={true}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
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 { useSelector } from "react-redux";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -9,13 +9,17 @@ import {
|
|||||||
Choice,
|
Choice,
|
||||||
ChoiceUtils,
|
ChoiceUtils,
|
||||||
ClassDefinitionContract,
|
ClassDefinitionContract,
|
||||||
|
ClassFeature,
|
||||||
|
ClassFeatureUtils,
|
||||||
ClassUtils,
|
ClassUtils,
|
||||||
Constants,
|
Constants,
|
||||||
|
ContainerManager,
|
||||||
Feat,
|
Feat,
|
||||||
FeatUtils,
|
FeatUtils,
|
||||||
} from "@dndbeyond/character-rules-engine";
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
|
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 { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||||
import { AppLoggerUtils } from "~/tools/js/Shared/utils";
|
import { AppLoggerUtils } from "~/tools/js/Shared/utils";
|
||||||
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
|
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
|
||||||
@@ -35,7 +39,9 @@ export interface FeatureChoicesProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
) => void;
|
) => void;
|
||||||
collapseDescription?: boolean;
|
collapseDescription?: boolean;
|
||||||
charClass?: CharClass;
|
charClass?: CharClass;
|
||||||
|
classFeature?: ClassFeature;
|
||||||
shouldFetch?: boolean; // Optional prop to control fetching behavior
|
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> = ({
|
export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
||||||
@@ -45,7 +51,9 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
|||||||
onChoiceChange,
|
onChoiceChange,
|
||||||
collapseDescription,
|
collapseDescription,
|
||||||
charClass,
|
charClass,
|
||||||
|
classFeature,
|
||||||
shouldFetch = true, // Default to true if not provided
|
shouldFetch = true, // Default to true if not provided
|
||||||
|
itemShoppe = null,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||||
@@ -56,14 +64,20 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
|||||||
apiCreatorSelectors.makeLoadAvailableSubclasses
|
apiCreatorSelectors.makeLoadAvailableSubclasses
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||||
|
|
||||||
const [subclassData, setSubclassData] = useState<
|
const [subclassData, setSubclassData] = useState<
|
||||||
Array<ClassDefinitionContract>
|
Array<ClassDefinitionContract>
|
||||||
>([]);
|
>([]);
|
||||||
const [featsData, setFeatsData] = useState<Array<Feat>>([]);
|
const [featsData, setFeatsData] = useState<Array<Feat>>([]);
|
||||||
|
const [localItemShoppe, setLocalItemShoppe] =
|
||||||
|
useState<ContainerManager | null>(itemShoppe);
|
||||||
const [featLoadingStatus, setFeatLoadingStatus] =
|
const [featLoadingStatus, setFeatLoadingStatus] =
|
||||||
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
|
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
|
||||||
const [subclassLoadingStatus, setSubclassLoadingStatus] =
|
const [subclassLoadingStatus, setSubclassLoadingStatus] =
|
||||||
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
|
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
|
||||||
|
const [itemsLoadingStatus, setItemsLoadingStatus] =
|
||||||
|
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
|
||||||
|
|
||||||
const hasFeatChoice = choices.some(
|
const hasFeatChoice = choices.some(
|
||||||
(choice) =>
|
(choice) =>
|
||||||
@@ -77,15 +91,23 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
|||||||
Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION
|
Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasItemMappings = classFeature
|
||||||
|
? ClassFeatureUtils.getHasItemMappings(classFeature)
|
||||||
|
: false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasFeatChoice &&
|
hasFeatChoice &&
|
||||||
shouldFetch &&
|
shouldFetch &&
|
||||||
featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
|
featLoadingStatus !== DataLoadingStatusEnum.LOADED
|
||||||
) {
|
) {
|
||||||
setFeatLoadingStatus(DataLoadingStatusEnum.LOADING);
|
setFeatLoadingStatus(DataLoadingStatusEnum.LOADING);
|
||||||
|
|
||||||
loadAvailableFeats()
|
loadAvailableFeats({
|
||||||
|
signal: abortController.signal,
|
||||||
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
let featsData: Array<Feat> = [];
|
let featsData: Array<Feat> = [];
|
||||||
|
|
||||||
@@ -99,17 +121,22 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
|||||||
setFeatsData(featsData);
|
setFeatsData(featsData);
|
||||||
setFeatLoadingStatus(DataLoadingStatusEnum.LOADED);
|
setFeatLoadingStatus(DataLoadingStatusEnum.LOADED);
|
||||||
})
|
})
|
||||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
.catch((e) => {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
|
AppLoggerUtils.handleAdhocApiError(e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
charClass &&
|
charClass &&
|
||||||
hasSubclassChoice &&
|
hasSubclassChoice &&
|
||||||
shouldFetch &&
|
shouldFetch &&
|
||||||
subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
|
subclassLoadingStatus !== DataLoadingStatusEnum.LOADED
|
||||||
) {
|
) {
|
||||||
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING);
|
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING);
|
||||||
loadAvailableSubclasses(ClassUtils.getId(charClass))
|
loadAvailableSubclasses(ClassUtils.getId(charClass), {
|
||||||
|
signal: abortController.signal,
|
||||||
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
let subclassData: Array<ClassDefinitionContract> = [];
|
let subclassData: Array<ClassDefinitionContract> = [];
|
||||||
|
|
||||||
@@ -121,17 +148,55 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
|||||||
setSubclassData(subclassData);
|
setSubclassData(subclassData);
|
||||||
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED);
|
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,
|
hasFeatChoice,
|
||||||
hasSubclassChoice,
|
hasSubclassChoice,
|
||||||
|
hasItemMappings,
|
||||||
subclassLoadingStatus,
|
subclassLoadingStatus,
|
||||||
featLoadingStatus,
|
featLoadingStatus,
|
||||||
|
itemsLoadingStatus,
|
||||||
charClass,
|
charClass,
|
||||||
loadAvailableFeats,
|
loadAvailableFeats,
|
||||||
loadAvailableSubclasses,
|
loadAvailableSubclasses,
|
||||||
shouldFetch,
|
shouldFetch,
|
||||||
|
inventoryManager,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isDataLoaded = (): boolean => {
|
const isDataLoaded = (): boolean => {
|
||||||
@@ -145,6 +210,13 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (itemShoppe !== null) {
|
||||||
|
return true;
|
||||||
|
} else if (hasItemMappings) {
|
||||||
|
if (itemsLoadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -163,6 +235,7 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
|
|||||||
charClass={charClass}
|
charClass={charClass}
|
||||||
choice={choice}
|
choice={choice}
|
||||||
featsData={featsData}
|
featsData={featsData}
|
||||||
|
itemShoppe={localItemShoppe}
|
||||||
subclassData={subclassData}
|
subclassData={subclassData}
|
||||||
className={styles.choice}
|
className={styles.choice}
|
||||||
key={ChoiceUtils.getId(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 { FC } from "react";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
@@ -16,12 +17,14 @@ export interface HelperTextAccordionProps
|
|||||||
|
|
||||||
export const HelperTextAccordion: FC<HelperTextAccordionProps> = ({
|
export const HelperTextAccordion: FC<HelperTextAccordionProps> = ({
|
||||||
builderHelperText,
|
builderHelperText,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{builderHelperText.map((helperText, idx) => (
|
{builderHelperText.map((helperText, idx) => (
|
||||||
<Accordion
|
<Accordion
|
||||||
|
className={clsx([styles.accordion, className])}
|
||||||
{...props}
|
{...props}
|
||||||
key={uuidv4()}
|
key={uuidv4()}
|
||||||
summary={
|
summary={
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FC, HTMLAttributes } from "react";
|
import type { FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
import { InfoItem as TtuiInfoItem } from "@dndbeyond/ttui/components/InfoItem";
|
|
||||||
|
|
||||||
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
|
|
||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
interface InfoItemProps extends HTMLAttributes<HTMLElement> {
|
interface InfoItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
label: string;
|
label: string;
|
||||||
color?: "primary"; // Not needed here, but fixes ts error
|
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is an attribute for a given item, spell, or other entity.
|
* 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> = ({
|
export const InfoItem: FC<InfoItemProps> = ({
|
||||||
className,
|
className,
|
||||||
onClick,
|
inline = true,
|
||||||
|
label,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => (
|
||||||
const handleClick = useUnpropagatedClick(onClick);
|
<div
|
||||||
|
className={clsx([styles.item, inline && styles.inline, className])}
|
||||||
return (
|
role="listitem"
|
||||||
<TtuiInfoItem
|
|
||||||
className={clsx([styles.item, className])}
|
|
||||||
onClick={onClick ? handleClick : undefined}
|
|
||||||
{...props}
|
{...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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FC, HTMLAttributes } from "react";
|
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 { 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 config from "~/config";
|
||||||
import useUser from "~/hooks/useUser";
|
import useUser from "~/hooks/useUser";
|
||||||
@@ -13,9 +13,40 @@ import styles from "./styles.module.css";
|
|||||||
|
|
||||||
const BASE_PATHNAME = config.basePathname;
|
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> {}
|
interface LayoutProps extends HTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
|
export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const isVttView = searchParams.get("view") === "vtt";
|
||||||
const isDev = process.env.NODE_ENV === "development";
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
const matchSheet = useMatch(`${BASE_PATHNAME}/:characterId/`);
|
const matchSheet = useMatch(`${BASE_PATHNAME}/:characterId/`);
|
||||||
@@ -23,20 +54,33 @@ export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
|
|||||||
// Don't show the navigation in production
|
// Don't show the navigation in production
|
||||||
if (!isDev) return <>{children}</>;
|
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 (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{/* TODO: fetch navItems */}
|
{!isVttView &&
|
||||||
<div className={styles.siteStyles}>
|
createPortal(
|
||||||
<Sitebar user={user as any} navItems={[]} />
|
<NavigationMenu user={navigationUser} />,
|
||||||
{/* TODO: fetch sources */}
|
document.getElementById("header-wrapper")!
|
||||||
<MegaMenu enablePartyWizard={true} sources={[]} />
|
|
||||||
</div>
|
|
||||||
<div className={clsx(["container", styles.devContainer])}>{children}</div>
|
|
||||||
{!matchSheet && (
|
|
||||||
<div className={styles.siteStyles}>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<div className={clsx(["container", styles.devContainer])}>{children}</div>
|
||||||
|
{!isVttView &&
|
||||||
|
(!matchSheet ||
|
||||||
|
matchSheet.pathname === "/characters/builder" ||
|
||||||
|
matchSheet.pathname === "/characters/premade") &&
|
||||||
|
createPortal(<Footer />, document.getElementById("footer-wrapper")!)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ interface LinkProps
|
|||||||
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick"> {
|
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick"> {
|
||||||
onClick?: Function;
|
onClick?: Function;
|
||||||
useTheme?: boolean;
|
useTheme?: boolean;
|
||||||
useRouter?: boolean;
|
userouter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Link: FC<LinkProps> = ({
|
export const Link: FC<LinkProps> = ({
|
||||||
@@ -15,7 +15,7 @@ export const Link: FC<LinkProps> = ({
|
|||||||
href,
|
href,
|
||||||
onClick,
|
onClick,
|
||||||
useTheme,
|
useTheme,
|
||||||
useRouter,
|
userouter,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
//TODO - refactor to handle stop propagation on the oustide of this component (about 7 or so files to change)
|
//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 (
|
return (
|
||||||
<RouterLink
|
<RouterLink
|
||||||
className={className}
|
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 { MaxCharactersMessageText } from "../MaxCharactersMessageText";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
interface MaxCharactersDialogProps extends HTMLAttributes<HTMLDialogElement> {
|
interface MaxCharactersDialogProps
|
||||||
|
extends Omit<DialogProps, "onClose" | "open"> {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
useMyCharactersLink?: boolean;
|
useMyCharactersLink?: boolean;
|
||||||
|
hasLockedCharacters?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MaxCharactersDialog: React.FC<MaxCharactersDialogProps> = ({
|
export const MaxCharactersDialog: React.FC<MaxCharactersDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
useMyCharactersLink,
|
useMyCharactersLink,
|
||||||
|
hasLockedCharacters,
|
||||||
|
className,
|
||||||
...props
|
...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 (
|
return (
|
||||||
<dialog
|
<Dialog
|
||||||
className={styles.maxCharactersDialog}
|
className={clsx([styles.dialog, className])}
|
||||||
ref={dialogRef}
|
open={open}
|
||||||
onClick={handleClickBackdrop}
|
onClose={onClose}
|
||||||
|
modal
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<header>
|
<header className={styles.header}>
|
||||||
<h2 className={styles.dialogTitle}>Your character slots are full</h2>
|
<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>
|
</header>
|
||||||
<MaxCharactersMessageText useMyCharactersLink={useMyCharactersLink} />
|
<div className={styles.content}>
|
||||||
<footer className={styles.dialogFooter}>
|
<MaxCharactersMessageText
|
||||||
<Button href="/store/subscribe" variant="outline">
|
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
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose}>Close</Button>
|
)}
|
||||||
|
<Button
|
||||||
|
className={styles.cardButton}
|
||||||
|
color="info"
|
||||||
|
size="x-small"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ interface MaxCharactersMessageTextProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
includeTitle?: boolean;
|
includeTitle?: boolean;
|
||||||
useLinks?: boolean;
|
useLinks?: boolean;
|
||||||
useMyCharactersLink?: boolean;
|
useMyCharactersLink?: boolean;
|
||||||
|
hasLockedCharacters?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
|
export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
|
||||||
includeTitle,
|
includeTitle,
|
||||||
useLinks,
|
useLinks,
|
||||||
useMyCharactersLink,
|
useMyCharactersLink,
|
||||||
|
hasLockedCharacters,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const subscribe = useLinks ? (
|
const subscribe = useLinks ? (
|
||||||
@@ -31,6 +33,21 @@ export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
|
|||||||
"My Characters"
|
"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 (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{includeTitle && (
|
{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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,21 +7,23 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { Dialog } from "@dndbeyond/ttui/components/Dialog";
|
|
||||||
import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible";
|
import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible";
|
||||||
|
|
||||||
|
import { Dialog } from "~/components/Dialog";
|
||||||
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a popover component to handle dialogs that are dependent on a location from a trigger element.
|
* This is a popover component to handle dialogs that are dependent on a location from a trigger
|
||||||
* It uses the useIsVisible hook to handle the visibility of the dialog. The position of the dialog
|
* element. It uses the useIsVisible hook to handle the visibility of the dialog. The position of
|
||||||
* is determined by the position prop, which can be set to topLeft, topRight, left, right, bottomLeft, or bottomRight.
|
* the dialog is determined by the position prop, which can be set to topLeft, topRight, left,
|
||||||
* The maxWidth prop can be used to set the maximum width of the dialog. The trigger prop is the element that will trigger
|
* right, bottomLeft, or bottomRight. The maxWidth prop can be used to set the maximum width of the
|
||||||
* the dialog to open.The popover component also handles the positioning of the dialog based on the trigger element. The
|
* dialog. The trigger prop is the element that will trigger the dialog to open.The popover
|
||||||
* handleToggle function toggles the visibility of the dialog, and the handleClose function closes the dialog. The popover
|
* component also handles the positioning of the dialog based on the trigger element. The
|
||||||
* component also adds an event listener to the cancel button in the dialog to close the dialog when clicked.
|
* 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.
|
* In order for a cancel button to work, it must have a `data-cancel` attribute.
|
||||||
**/
|
**/
|
||||||
@@ -36,7 +38,7 @@ interface PopoverProps {
|
|||||||
| "bottomLeft"
|
| "bottomLeft"
|
||||||
| "bottomRight"
|
| "bottomRight"
|
||||||
| "bottom";
|
| "bottom";
|
||||||
trigger: ReactElement;
|
trigger: ReactElement<{ onClick?: (e: MouseEvent) => void }>;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
variant?: "default" | "menu";
|
variant?: "default" | "menu";
|
||||||
"data-testid"?: string;
|
"data-testid"?: string;
|
||||||
@@ -85,9 +87,12 @@ export const Popover: FC<PopoverProps> = ({
|
|||||||
// Check if children is a ReactElement
|
// Check if children is a ReactElement
|
||||||
if (React.isValidElement(children)) {
|
if (React.isValidElement(children)) {
|
||||||
// Create a copy of the ReactElement and attach handleClose function to it
|
// Create a copy of the ReactElement and attach handleClose function to it
|
||||||
return cloneElement(children as ReactElement, {
|
return cloneElement(
|
||||||
|
children as ReactElement<{ handleClose: (e: MouseEvent) => void }>,
|
||||||
|
{
|
||||||
handleClose,
|
handleClose,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// If not just return the children - means its a primitive type
|
// If not just return the children - means its a primitive type
|
||||||
else {
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,18 +1,25 @@
|
|||||||
import clsx from "clsx";
|
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 { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||||
|
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
export interface TabFilterProps extends HTMLAttributes<HTMLDivElement> {
|
interface TabFilterOption {
|
||||||
filters:
|
|
||||||
| {
|
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
badge?: ReactNode;
|
badge?: ReactNode;
|
||||||
}[];
|
}
|
||||||
|
export interface TabFilterProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
filters: Array<TabFilterOption>;
|
||||||
showAllTab?: boolean;
|
showAllTab?: boolean;
|
||||||
sharedChildren?: ReactNode;
|
sharedChildren?: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -30,6 +37,11 @@ export const TabFilter: FC<TabFilterProps> = ({
|
|||||||
const activeIndex = showAllTab ? activeFilter - 1 : activeFilter;
|
const activeIndex = showAllTab ? activeFilter - 1 : activeFilter;
|
||||||
const nonEmptyFilters = filters.filter((f) => f.label !== "");
|
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) => {
|
const getDefaultLabel = (label: ReactNode) => {
|
||||||
if (typeof label === "string") return label;
|
if (typeof label === "string") return label;
|
||||||
return uuidv4();
|
return uuidv4();
|
||||||
@@ -87,7 +99,7 @@ export const TabFilter: FC<TabFilterProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.content}>
|
<div className={styles.content} data-scrollable-container="true">
|
||||||
{sharedChildren}
|
{sharedChildren}
|
||||||
{!showAllTab || activeFilter !== 0
|
{!showAllTab || activeFilter !== 0
|
||||||
? nonEmptyFilters[activeIndex]?.content
|
? nonEmptyFilters[activeIndex]?.content
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
import clsx from "clsx";
|
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 ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
|
||||||
|
|
||||||
import { Popover } from "../Popover";
|
import { Popover } from "../Popover";
|
||||||
import styles from "./styles.module.css";
|
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 {
|
interface TabItem {
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
@@ -15,44 +34,79 @@ interface TabItem {
|
|||||||
|
|
||||||
interface TabListProps extends HTMLAttributes<HTMLElement> {
|
interface TabListProps extends HTMLAttributes<HTMLElement> {
|
||||||
tabs: Array<TabItem | null>;
|
tabs: Array<TabItem | null>;
|
||||||
variant?: "default" | "collapse" | "toggle";
|
variant?: "default" | "collapse" | "fullwidth";
|
||||||
defaultActiveId?: string;
|
defaultActiveId?: string;
|
||||||
maxNavItemsShow?: number;
|
maxNavItemsShow?: number;
|
||||||
|
onChangeTab?: (id: string) => void;
|
||||||
|
themed?: boolean;
|
||||||
|
forceShow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabList: FC<TabListProps> = ({
|
export const TabList: FC<TabListProps> = ({
|
||||||
|
className,
|
||||||
tabs,
|
tabs,
|
||||||
defaultActiveId,
|
defaultActiveId,
|
||||||
maxNavItemsShow = 7,
|
maxNavItemsShow = 7,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
|
onChangeTab,
|
||||||
|
themed,
|
||||||
|
forceShow,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState(
|
const tabListRef = useRef<HTMLDivElement>(null!);
|
||||||
defaultActiveId ? defaultActiveId : tabs[0]?.id
|
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) => {
|
const handleTabClick = (id: string) => {
|
||||||
if (id === activeTab && variant === "collapse") {
|
if (id === activeTab && variant === "collapse") {
|
||||||
setIsTabVisible(!isTabVisible);
|
setIsTabVisible(!isTabVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === activeTab && variant === "toggle") {
|
|
||||||
setActiveTab("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id !== activeTab) {
|
if (id !== activeTab) {
|
||||||
setActiveTab(id);
|
setActiveTab(id);
|
||||||
setIsTabVisible(true);
|
setIsTabVisible(true);
|
||||||
|
|
||||||
|
if (onChangeTab) {
|
||||||
|
onChangeTab(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const tabsWithContent = tabs.filter((tab) => tab);
|
|
||||||
const visibleTabs = tabsWithContent.slice(0, maxNavItemsShow);
|
useEffect(
|
||||||
const hiddenTabs = tabsWithContent.slice(maxNavItemsShow);
|
() => {
|
||||||
const selectedTab = tabs.find((tab) => tab?.id === activeTab);
|
// 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 (
|
return (
|
||||||
<div {...props}>
|
<div
|
||||||
|
className={clsx([
|
||||||
|
styles.tabList,
|
||||||
|
styles[variant],
|
||||||
|
themed && styles.themed,
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
ref={tabListRef}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<menu className={styles.tabs}>
|
<menu className={styles.tabs}>
|
||||||
{/* List of visible tabs */}
|
{/* List of visible tabs */}
|
||||||
{visibleTabs.map((tab: TabItem) => (
|
{visibleTabs.map((tab: TabItem) => (
|
||||||
@@ -62,10 +116,13 @@ export const TabList: FC<TabListProps> = ({
|
|||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={activeTab === tab.id}
|
aria-checked={activeTab === tab.id}
|
||||||
onClick={() => handleTabClick(tab.id)}
|
onClick={() => handleTabClick(tab.id)}
|
||||||
|
data-testid={tab.id}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
{variant !== "default" && (
|
{isToggleable && (
|
||||||
<ChevronDown className={!isTabVisible ? styles.closed : ""} />
|
<ChevronDown
|
||||||
|
className={clsx([!isTabVisible && styles.closed])}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface TextareaProps
|
|||||||
value?: string;
|
value?: string;
|
||||||
onInputBlur: (textValue: string) => void;
|
onInputBlur: (textValue: string) => void;
|
||||||
onInputKeyUp: () => void;
|
onInputKeyUp: () => void;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState, type FC, type HTMLAttributes } from "react";
|
import { useState, type FC, type HTMLAttributes } from "react";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
|
||||||
|
|
||||||
export interface ToggleProps
|
export interface ToggleProps
|
||||||
extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
|
extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
|
||||||
|
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
color?: "primary" | "secondary" | "warning" | "error" | "info" | "success" | "themed";
|
color?: "primary" | "secondary" | "warning" | "error" | "info" | "success" | "themed";
|
||||||
onClick?: (isEnabled: boolean) => void;
|
onClick?: (isEnabled: boolean) => void;
|
||||||
@@ -26,11 +23,9 @@ export const Toggle: FC<ToggleProps> = ({
|
|||||||
onChangePromise,
|
onChangePromise,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
|
||||||
const [enabled, setEnabled] = useState(checked);
|
const [enabled, setEnabled] = useState(checked);
|
||||||
|
|
||||||
const handleClick = (evt: React.MouseEvent): void => {
|
const handleClick = (evt: React.MouseEvent): void => {
|
||||||
if (!isReadonly) {
|
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
evt.nativeEvent.stopImmediatePropagation();
|
evt.nativeEvent.stopImmediatePropagation();
|
||||||
|
|
||||||
@@ -46,7 +41,6 @@ export const Toggle: FC<ToggleProps> = ({
|
|||||||
onClick(!enabled);
|
onClick(!enabled);
|
||||||
setEnabled(!enabled);
|
setEnabled(!enabled);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
RuleDataUtils,
|
RuleDataUtils,
|
||||||
} from "@dndbeyond/character-rules-engine";
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
import { Select } from "~/components/Select";
|
||||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
import XpBar from "~/tools/js/smartComponents/XpBar";
|
import XpBar from "~/tools/js/smartComponents/XpBar";
|
||||||
import { Select } from "~/tools/js/smartComponents/legacy";
|
|
||||||
|
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
@@ -144,15 +144,22 @@ export const XpManager: FC<XpManagerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={clsx([styles.controls, styles.withDivider])}>
|
<div className={clsx([styles.controls, styles.withDivider])}>
|
||||||
<div className={styles.control}>
|
<div className={styles.control}>
|
||||||
<label id="set-level" className={styles.label}>
|
<label
|
||||||
|
id="set-level-label"
|
||||||
|
htmlFor="set-levelButton"
|
||||||
|
className={styles.label}
|
||||||
|
>
|
||||||
Set Level
|
Set Level
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
aria-labelledby="set-level"
|
className={styles.levelSelect}
|
||||||
|
aria-labelledby="set-level-label"
|
||||||
options={levelOptions}
|
options={levelOptions}
|
||||||
onChange={handleChooseLevel}
|
onChange={handleChooseLevel}
|
||||||
value={levelChosen}
|
value={levelChosen}
|
||||||
placeholder={"--"}
|
placeholder={"--"}
|
||||||
|
name="set-level"
|
||||||
|
searchThreshold={null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx([styles.control, styles.setXp])}>
|
<div className={clsx([styles.control, styles.setXp])}>
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ export const SourceCategoryDescription = {
|
|||||||
export const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif"];
|
export const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif"];
|
||||||
export const FILE_SIZE_3MB = 3 * 1024 * 1024;
|
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
|
* Export constants from the rules engine to reduce the number of places where
|
||||||
* they're being imported.
|
* they're being imported.
|
||||||
@@ -232,8 +237,6 @@ export const NoteTypeEnum = Constants.NoteTypeEnum;
|
|||||||
export const NotificationTypeEnum = Constants.NotificationTypeEnum;
|
export const NotificationTypeEnum = Constants.NotificationTypeEnum;
|
||||||
export const PoundsInTon = Constants.POUNDS_IN_TON;
|
export const PoundsInTon = Constants.POUNDS_IN_TON;
|
||||||
export const PropertyList = Constants.PROPERTY_LIST;
|
export const PropertyList = Constants.PROPERTY_LIST;
|
||||||
export const PartyInventorySharingStateEnum =
|
|
||||||
Constants.PartyInventorySharingStateEnum;
|
|
||||||
export const PreferenceAbilityScoreDisplayTypeEnum =
|
export const PreferenceAbilityScoreDisplayTypeEnum =
|
||||||
Constants.PreferenceAbilityScoreDisplayTypeEnum;
|
Constants.PreferenceAbilityScoreDisplayTypeEnum;
|
||||||
export const PreferenceEncumbranceTypeEnum =
|
export const PreferenceEncumbranceTypeEnum =
|
||||||
@@ -243,6 +246,7 @@ export const PreferencePrivacyTypeEnum = Constants.PreferencePrivacyTypeEnum;
|
|||||||
export const PreferenceProgressionTypeEnum =
|
export const PreferenceProgressionTypeEnum =
|
||||||
Constants.PreferenceProgressionTypeEnum;
|
Constants.PreferenceProgressionTypeEnum;
|
||||||
export const PreferenceSharingTypeEnum = Constants.PreferenceSharingTypeEnum;
|
export const PreferenceSharingTypeEnum = Constants.PreferenceSharingTypeEnum;
|
||||||
|
export const PreferenceLongRestTypeEnum = Constants.PreferenceLongRestTypeEnum;
|
||||||
export const PrerequisiteSubTypeEnum = Constants.PrerequisiteSubTypeEnum;
|
export const PrerequisiteSubTypeEnum = Constants.PrerequisiteSubTypeEnum;
|
||||||
export const PrerequisiteTypeEnum = Constants.PrerequisiteTypeEnum;
|
export const PrerequisiteTypeEnum = Constants.PrerequisiteTypeEnum;
|
||||||
export const ProficiencyAdjustmentTypeEnum =
|
export const ProficiencyAdjustmentTypeEnum =
|
||||||
|
|||||||
@@ -62,16 +62,11 @@ export const CharacterThemeProvider = ({ children }) => {
|
|||||||
__html: `
|
__html: `
|
||||||
:root {
|
:root {
|
||||||
--theme-color: ${themeColor};
|
--theme-color: ${themeColor};
|
||||||
|
--theme-color-muted: color-mix(in srgb, var(--theme-color), var(--theme-background-solid) 30%);
|
||||||
--theme-background: ${backgroundColor};
|
--theme-background: ${backgroundColor};
|
||||||
--theme-background-solid: ${solidBackground};
|
--theme-background-solid: ${solidBackground};
|
||||||
--theme-contrast: var(--ttui_grey-${isDarkMode ? 50 : 900});
|
--theme-contrast: var(--ttui_grey-${isDarkMode ? 50 : 900});
|
||||||
|
|
||||||
--theme-transparent: color-mix(in srgb, var(--theme-color), transparent 60%);
|
--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);" : ""
|
|
||||||
}
|
|
||||||
}`,
|
}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 { toCamelCase } from "~/helpers/casing";
|
||||||
import { getFeatureFlagsFromCharacterService } from "~/helpers/characterServiceApi";
|
import { getFeatureFlagsFromCharacterService } from "~/helpers/characterServiceApi";
|
||||||
|
|
||||||
const featureFlagsToGet = [
|
const featureFlagsToGet = [
|
||||||
"release-gate-ims",
|
|
||||||
"release-gate-gfs-blessings-ui",
|
|
||||||
"release-gate-character-sheet-tour",
|
"release-gate-character-sheet-tour",
|
||||||
"release-gate-premade-characters",
|
"circuit-breaker.datadog-rum-characters-app",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const defaultFlags = {
|
export const defaultFlags = {
|
||||||
imsFlag: false,
|
|
||||||
gfsBlessingsUiFlag: false,
|
|
||||||
characterSheetTourFlag: false,
|
characterSheetTourFlag: false,
|
||||||
premadeCharactersFlag: false,
|
datadogRumCharactersAppFlag: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FeatureFlags = typeof defaultFlags;
|
export type FeatureFlags = typeof defaultFlags;
|
||||||
@@ -25,7 +28,7 @@ export interface FeatureFlagContextType extends FeatureFlags {
|
|||||||
|
|
||||||
export const FeatureFlagContext = createContext<FeatureFlagContextType>(null!);
|
export const FeatureFlagContext = createContext<FeatureFlagContextType>(null!);
|
||||||
|
|
||||||
interface ProviderProps {
|
interface ProviderProps extends PropsWithChildren {
|
||||||
defaultOverrides?: Partial<FeatureFlags>;
|
defaultOverrides?: Partial<FeatureFlags>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,12 +47,12 @@ export const FeatureFlagProvider: FC<ProviderProps> = ({
|
|||||||
const formattedFlag = flag
|
const formattedFlag = flag
|
||||||
// Remove `character-app` from the flag name
|
// Remove `character-app` from the flag name
|
||||||
.replace(/character-app\./i, "")
|
.replace(/character-app\./i, "")
|
||||||
|
// Remove `circuit-breaker` from the flag name
|
||||||
|
.replace(/circuit-breaker\./i, "")
|
||||||
// Remove `release-gate` from the flag name
|
// Remove `release-gate` from the flag name
|
||||||
.replace(/release-gate\W/i, "")
|
.replace(/release-gate\W/i, "")
|
||||||
// Remove `character-sheet` from flag name if not the tour flag
|
// Remove `character-sheet` from flag name if not the tour flag
|
||||||
.replace(/character-sheet\W(?!tour)/i, "")
|
.replace(/character-sheet\W(?!tour)/i, "");
|
||||||
// Change `2d` to `tactical` since var can't start with number
|
|
||||||
.replace(/2d/i, "tactical");
|
|
||||||
|
|
||||||
const key = toCamelCase(`${formattedFlag}Flag`);
|
const key = toCamelCase(`${formattedFlag}Flag`);
|
||||||
return { ...acc, [key]: flags[flag] };
|
return { ...acc, [key]: flags[flag] };
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { createContext, FC, useContext, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
FC,
|
||||||
|
PropsWithChildren,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
export interface FiltersContextType {
|
export interface FiltersContextType {
|
||||||
showItemTypes: boolean;
|
showItemTypes: boolean;
|
||||||
@@ -13,7 +19,7 @@ export interface FiltersContextType {
|
|||||||
|
|
||||||
export const FiltersContext = createContext<FiltersContextType>(null!);
|
export const FiltersContext = createContext<FiltersContextType>(null!);
|
||||||
|
|
||||||
export const FiltersProvider: FC = ({ children }) => {
|
export const FiltersProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [showItemTypes, setShowItemTypes] = useState(true);
|
const [showItemTypes, setShowItemTypes] = useState(true);
|
||||||
const [showItemSourceCategories, setShowItemSourceCategories] =
|
const [showItemSourceCategories, setShowItemSourceCategories] =
|
||||||
useState(true);
|
useState(true);
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { isEqual } from "lodash";
|
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 { useSelector } from "react-redux";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +46,7 @@ export interface PaneInfo {
|
|||||||
) => void;
|
) => void;
|
||||||
paneHistoryPrevious: () => void;
|
paneHistoryPrevious: () => void;
|
||||||
paneHistoryNext: () => void;
|
paneHistoryNext: () => void;
|
||||||
|
resetActivePane: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarContextType {
|
export interface SidebarContextType {
|
||||||
@@ -48,7 +56,7 @@ export interface SidebarContextType {
|
|||||||
|
|
||||||
export const SidebarContext = createContext<SidebarContextType>(null!);
|
export const SidebarContext = createContext<SidebarContextType>(null!);
|
||||||
|
|
||||||
export const SidebarProvider: FC = ({ children }) => {
|
export const SidebarProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const isMobile = useSelector(appEnvSelectors.getIsMobile);
|
const isMobile = useSelector(appEnvSelectors.getIsMobile);
|
||||||
const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
|
const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
|
||||||
|
|
||||||
@@ -64,6 +72,8 @@ export const SidebarProvider: FC = ({ children }) => {
|
|||||||
|
|
||||||
const width: number = 340;
|
const width: number = 340;
|
||||||
|
|
||||||
|
const resetActivePane = () => setActivePane(null);
|
||||||
|
|
||||||
//Create a new pane history
|
//Create a new pane history
|
||||||
const paneHistoryStart = (
|
const paneHistoryStart = (
|
||||||
type: PaneComponentEnum,
|
type: PaneComponentEnum,
|
||||||
@@ -161,6 +171,7 @@ export const SidebarProvider: FC = ({ children }) => {
|
|||||||
paneHistoryPush,
|
paneHistoryPush,
|
||||||
paneHistoryPrevious,
|
paneHistoryPrevious,
|
||||||
paneHistoryNext,
|
paneHistoryNext,
|
||||||
|
resetActivePane,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import createCache from "@emotion/cache";
|
import createCache from "@emotion/cache";
|
||||||
import { CacheProvider } from "@emotion/react";
|
import { CacheProvider } from "@emotion/react";
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
|
||||||
import { useState, createContext, useEffect, useMemo } from "react";
|
import { useState, createContext, useEffect, useMemo } from "react";
|
||||||
import { prefixer } from "stylis";
|
import { prefixer } from "stylis";
|
||||||
|
|
||||||
import { getTheme } from "~/theme";
|
|
||||||
|
|
||||||
type PaletteOpt = "light" | "dark";
|
type PaletteOpt = "light" | "dark";
|
||||||
|
|
||||||
interface Manager {
|
interface Manager {
|
||||||
@@ -77,12 +74,9 @@ export const ThemeManagerProvider = ({
|
|||||||
/**
|
/**
|
||||||
* End the hack
|
* End the hack
|
||||||
*/
|
*/
|
||||||
const theme = getTheme(manager.lightOrDark, manager.primary);
|
|
||||||
return (
|
return (
|
||||||
<ThemeManager.Provider value={{ manager, setManager }}>
|
<ThemeManager.Provider value={{ manager, setManager }}>
|
||||||
<CacheProvider value={hack__cache}>
|
<CacheProvider value={hack__cache}>{children}</CacheProvider>
|
||||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
|
||||||
</CacheProvider>
|
|
||||||
</ThemeManager.Provider>
|
</ThemeManager.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
PreferenceEncumbranceTypeEnum,
|
PreferenceEncumbranceTypeEnum,
|
||||||
PreferenceHitPointTypeEnum,
|
PreferenceHitPointTypeEnum,
|
||||||
PreferencePrivacyTypeEnum,
|
PreferencePrivacyTypeEnum,
|
||||||
|
PreferenceLongRestTypeEnum,
|
||||||
PreferenceProgressionTypeEnum,
|
PreferenceProgressionTypeEnum,
|
||||||
PreferenceSharingTypeEnum,
|
PreferenceSharingTypeEnum,
|
||||||
SenseTypeEnum,
|
SenseTypeEnum,
|
||||||
@@ -26,6 +27,7 @@ export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
|
|||||||
const progressionType = PreferenceProgressionTypeEnum.MILESTONE;
|
const progressionType = PreferenceProgressionTypeEnum.MILESTONE;
|
||||||
const sharingType = PreferenceSharingTypeEnum.LIMITED;
|
const sharingType = PreferenceSharingTypeEnum.LIMITED;
|
||||||
const privacyType = PreferencePrivacyTypeEnum.CAMPAIGN_ONLY;
|
const privacyType = PreferencePrivacyTypeEnum.CAMPAIGN_ONLY;
|
||||||
|
const longRestType = PreferenceLongRestTypeEnum.HALF;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abilityScoreDisplayType,
|
abilityScoreDisplayType,
|
||||||
@@ -38,6 +40,7 @@ export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
|
|||||||
enforceMulticlassRules: true,
|
enforceMulticlassRules: true,
|
||||||
hitPointType,
|
hitPointType,
|
||||||
ignoreCoinWeight: true,
|
ignoreCoinWeight: true,
|
||||||
|
longRestType,
|
||||||
primaryMovement,
|
primaryMovement,
|
||||||
primarySense,
|
primarySense,
|
||||||
privacyType,
|
privacyType,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
import { AbilityManager } from "@dndbeyond/character-rules-engine/es";
|
||||||
AbilityManager,
|
|
||||||
FeaturesManager,
|
|
||||||
} from "@dndbeyond/character-rules-engine/es";
|
|
||||||
|
|
||||||
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
|
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
|
||||||
|
|
||||||
@@ -18,7 +15,7 @@ export function useAbilities() {
|
|||||||
|
|
||||||
setAbilities(abilities);
|
setAbilities(abilities);
|
||||||
}
|
}
|
||||||
return FeaturesManager.subscribeToUpdates({ onUpdate });
|
return AbilityManager.subscribeToUpdates({ onUpdate });
|
||||||
}, [attributesManager, setAbilities]);
|
}, [attributesManager, setAbilities]);
|
||||||
|
|
||||||
return abilities;
|
return abilities;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ApiAdapterUtils as apiAdapterUtils,
|
ApiAdapterUtils as apiAdapterUtils,
|
||||||
BackgroundUtils as backgroundUtils,
|
BackgroundUtils as backgroundUtils,
|
||||||
CampaignUtils as campaignUtils,
|
CampaignUtils as campaignUtils,
|
||||||
CampaignSettingUtils as campaignSettingUtils,
|
|
||||||
CharacterUtils as characterUtils,
|
CharacterUtils as characterUtils,
|
||||||
ChoiceUtils as choiceUtils,
|
ChoiceUtils as choiceUtils,
|
||||||
ClassFeatureUtils as classFeatureUtils,
|
ClassFeatureUtils as classFeatureUtils,
|
||||||
@@ -124,7 +123,6 @@ export const useCharacterEngine = () => ({
|
|||||||
baseFeats: useSelector(s.getBaseFeats),
|
baseFeats: useSelector(s.getBaseFeats),
|
||||||
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
|
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
|
||||||
campaign: useSelector(s.getCampaign),
|
campaign: useSelector(s.getCampaign),
|
||||||
campaignSettings: useSelector(sds.getCampaignSettings),
|
|
||||||
carryingCapacity: useSelector(s.getCarryCapacity),
|
carryingCapacity: useSelector(s.getCarryCapacity),
|
||||||
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
|
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
|
||||||
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
|
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
|
||||||
@@ -151,7 +149,7 @@ export const useCharacterEngine = () => ({
|
|||||||
classSpells: useSelector(s.getClassSpells),
|
classSpells: useSelector(s.getClassSpells),
|
||||||
classSpellInfo: useSelector(s.getClassSpellInfoLookup),
|
classSpellInfo: useSelector(s.getClassSpellInfoLookup),
|
||||||
classSpellLists: useSelector(s.getClassSpellLists),
|
classSpellLists: useSelector(s.getClassSpellLists),
|
||||||
classSpellListSpells: useSelector(s.getClassSpellListSpellsLookup),
|
classSpellListSpellsLookup: useSelector(s.getClassSpellListSpellsLookup),
|
||||||
classes: useSelector(s.getClasses),
|
classes: useSelector(s.getClasses),
|
||||||
classesModifiers: useSelector(s.getClassesModifiers),
|
classesModifiers: useSelector(s.getClassesModifiers),
|
||||||
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
|
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
|
||||||
@@ -170,6 +168,7 @@ export const useCharacterEngine = () => ({
|
|||||||
creatureOwnerData: useSelector(s.getCreatureOwnerData),
|
creatureOwnerData: useSelector(s.getCreatureOwnerData),
|
||||||
creatureRules: useSelector(s.getCreatureRules),
|
creatureRules: useSelector(s.getCreatureRules),
|
||||||
creatures: useSelector(s.getCreatures),
|
creatures: useSelector(s.getCreatures),
|
||||||
|
currencies: useSelector(s.getCurrencies),
|
||||||
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
|
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
|
||||||
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
|
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
|
||||||
currentLevel: useSelector(s.getCurrentLevel),
|
currentLevel: useSelector(s.getCurrentLevel),
|
||||||
@@ -235,6 +234,7 @@ export const useCharacterEngine = () => ({
|
|||||||
inventoryContainers: useSelector(s.getInventoryContainers),
|
inventoryContainers: useSelector(s.getInventoryContainers),
|
||||||
inventoryLookup: useSelector(s.getInventoryLookup),
|
inventoryLookup: useSelector(s.getInventoryLookup),
|
||||||
inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
|
inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
|
||||||
|
itemPlans: useSelector(s.getAvailableItemPlans),
|
||||||
isSheetReady: useSelector(s.isCharacterSheetReady),
|
isSheetReady: useSelector(s.isCharacterSheetReady),
|
||||||
isDead: useSelector(s.isDead),
|
isDead: useSelector(s.isDead),
|
||||||
isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
|
isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
|
||||||
@@ -252,7 +252,9 @@ export const useCharacterEngine = () => ({
|
|||||||
levelSpells: useSelector(s.getLevelSpells),
|
levelSpells: useSelector(s.getLevelSpells),
|
||||||
lifestyle: useSelector(s.getLifestyle),
|
lifestyle: useSelector(s.getLifestyle),
|
||||||
loadSpecies: useSelector(api.makeLoadAvailableRaces),
|
loadSpecies: useSelector(api.makeLoadAvailableRaces),
|
||||||
|
longRestText: useSelector(sds.getLongRestText),
|
||||||
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
|
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
|
||||||
|
maxReplicatedItemsCount: useSelector(s.getMaxReplicatedItemsCount),
|
||||||
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
|
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
|
||||||
miscModifiers: useSelector(s.getMiscModifiers),
|
miscModifiers: useSelector(s.getMiscModifiers),
|
||||||
modifierData: useSelector(s.getModifierData),
|
modifierData: useSelector(s.getModifierData),
|
||||||
@@ -279,7 +281,7 @@ export const useCharacterEngine = () => ({
|
|||||||
partyInfo: useSelector(sds.getPartyInfo),
|
partyInfo: useSelector(sds.getPartyInfo),
|
||||||
partyInventory: useSelector(s.getPartyInventory),
|
partyInventory: useSelector(s.getPartyInventory),
|
||||||
partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
|
partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
|
||||||
partyInventoryItem: useSelector(s.getPartyInventoryLookup),
|
partyInventoryLookup: useSelector(s.getPartyInventoryLookup),
|
||||||
passiveInsight: useSelector(s.getPassiveInsight),
|
passiveInsight: useSelector(s.getPassiveInsight),
|
||||||
passiveInvestigation: useSelector(s.getPassiveInvestigation),
|
passiveInvestigation: useSelector(s.getPassiveInvestigation),
|
||||||
passivePerception: useSelector(s.getPassivePerception),
|
passivePerception: useSelector(s.getPassivePerception),
|
||||||
@@ -294,6 +296,7 @@ export const useCharacterEngine = () => ({
|
|||||||
playerName: useSelector(s.getUsername),
|
playerName: useSelector(s.getUsername),
|
||||||
preferences: useSelector(s.getCharacterPreferences),
|
preferences: useSelector(s.getCharacterPreferences),
|
||||||
prerequisiteData: useSelector(s.getPrerequisiteData),
|
prerequisiteData: useSelector(s.getPrerequisiteData),
|
||||||
|
premadeInfo: useSelector(char.getPremadeInfo),
|
||||||
processedInitiative: useSelector(s.getProcessedInitiative),
|
processedInitiative: useSelector(s.getProcessedInitiative),
|
||||||
proficiency: useSelector(s.getProficiencyLookup),
|
proficiency: useSelector(s.getProficiencyLookup),
|
||||||
proficiencyBonus: useSelector(s.getProficiencyBonus),
|
proficiencyBonus: useSelector(s.getProficiencyBonus),
|
||||||
@@ -370,7 +373,6 @@ export const useCharacterEngine = () => ({
|
|||||||
apiAdapterUtils,
|
apiAdapterUtils,
|
||||||
backgroundUtils,
|
backgroundUtils,
|
||||||
campaignUtils,
|
campaignUtils,
|
||||||
campaignSettingUtils,
|
|
||||||
characterUtils,
|
characterUtils,
|
||||||
choiceUtils,
|
choiceUtils,
|
||||||
classFeatureUtils,
|
classFeatureUtils,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useCallback, useState } from "react";
|
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 { characterSelectors } from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
import { useDispatch } from "~/hooks/useDispatch";
|
||||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
|
};
|
||||||
@@ -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,17 +1,18 @@
|
|||||||
import { HTMLAttributes, useState } from "react";
|
import { HTMLAttributes, JSX, useState } from "react";
|
||||||
import styles from '../../styles/errors.module.css';
|
|
||||||
|
import styles from "../../styles/errors.module.css";
|
||||||
|
|
||||||
export interface ErrorHandlerOptions {
|
export interface ErrorHandlerOptions {
|
||||||
initialState: boolean;
|
initialState: boolean;
|
||||||
errMsg: string;
|
errMsg: string;
|
||||||
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
|
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface ErrorHandler {
|
export interface ErrorHandler {
|
||||||
showError: boolean;
|
showError: boolean;
|
||||||
setShowError: (value: boolean) => void;
|
setShowError: (value: boolean) => void;
|
||||||
ErrorMessage: () => JSX.Element;
|
ErrorMessage: () => JSX.Element;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useErrorHandling = (
|
export const useErrorHandling = (
|
||||||
initialState: boolean,
|
initialState: boolean,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { HTMLAttributes } from "react";
|
import { HTMLAttributes, JSX } from "react";
|
||||||
|
|
||||||
import { useErrorHandling } from "./useErrorHandling";
|
import { useErrorHandling } from "./useErrorHandling";
|
||||||
|
|
||||||
export interface MaxLengthErrorHandler {
|
export interface MaxLengthErrorHandler {
|
||||||
handleMaxLengthErrorMsg: (value: string) => void;
|
handleMaxLengthErrorMsg: (value: string) => void;
|
||||||
hideError: () => void;
|
hideError: () => void;
|
||||||
MaxLengthErrorMessage: () => JSX.Element;
|
MaxLengthErrorMessage: () => JSX.Element;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useMaxLengthErrorHandling = (
|
export const useMaxLengthErrorHandling = (
|
||||||
initialState: boolean,
|
initialState: boolean,
|
||||||
@@ -15,11 +16,11 @@ export const useMaxLengthErrorHandling = (
|
|||||||
): MaxLengthErrorHandler => {
|
): MaxLengthErrorHandler => {
|
||||||
errMsg ||= `The max length is ${maxLength} characters.`;
|
errMsg ||= `The max length is ${maxLength} characters.`;
|
||||||
|
|
||||||
const {
|
const { showError, setShowError, ErrorMessage } = useErrorHandling(
|
||||||
showError,
|
initialState,
|
||||||
setShowError,
|
errMsg,
|
||||||
ErrorMessage,
|
errorMessageAttributes
|
||||||
} = useErrorHandling(initialState, errMsg, errorMessageAttributes);
|
);
|
||||||
|
|
||||||
const handleMaxLengthErrorMsg = (value: string): void => {
|
const handleMaxLengthErrorMsg = (value: string): void => {
|
||||||
if (!maxLength) {
|
if (!maxLength) {
|
||||||
@@ -35,15 +36,14 @@ export const useMaxLengthErrorHandling = (
|
|||||||
|
|
||||||
const hideError = () => {
|
const hideError = () => {
|
||||||
setShowError(false);
|
setShowError(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const MaxLengthErrorMessage = (): JSX.Element => showError && errMsg
|
const MaxLengthErrorMessage = (): JSX.Element =>
|
||||||
? (<ErrorMessage />)
|
showError && errMsg ? <ErrorMessage /> : <></>;
|
||||||
: <></>;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleMaxLengthErrorMsg,
|
handleMaxLengthErrorMsg,
|
||||||
MaxLengthErrorMessage,
|
MaxLengthErrorMessage,
|
||||||
hideError
|
hideError,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
import { ExtraManager } from "@dndbeyond/character-rules-engine/es";
|
||||||
ExtraManager,
|
|
||||||
FeaturesManager,
|
|
||||||
} from "@dndbeyond/character-rules-engine/es";
|
|
||||||
|
|
||||||
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
|
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
|
||||||
|
|
||||||
@@ -19,7 +16,7 @@ export function useExtras() {
|
|||||||
const onUpdate = () => {
|
const onUpdate = () => {
|
||||||
setExtras(extrasManager.getCharacterExtraManagers());
|
setExtras(extrasManager.getCharacterExtraManagers());
|
||||||
};
|
};
|
||||||
return FeaturesManager.subscribeToUpdates({ onUpdate });
|
return ExtraManager.subscribeToUpdates({ onUpdate });
|
||||||
}, [extrasManager]);
|
}, [extrasManager]);
|
||||||
|
|
||||||
return extras;
|
return extras;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiAdapterUtils,
|
ApiAdapterUtils,
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
syncTransactionActions,
|
syncTransactionActions,
|
||||||
} from "@dndbeyond/character-rules-engine";
|
} from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
|
import { useDispatch } from "~/hooks/useDispatch";
|
||||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||||
|
|
||||||
export interface ExportPdfData {
|
export interface ExportPdfData {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface AppUser extends User {
|
|||||||
roles: Array<string>;
|
roles: Array<string>;
|
||||||
subscription: string;
|
subscription: string;
|
||||||
subscriptionTier: string;
|
subscriptionTier: string;
|
||||||
|
avatarUrl?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* AppUserState has three states:
|
* AppUserState has three states:
|
||||||
|
|||||||
+26
-19
@@ -1,18 +1,22 @@
|
|||||||
import ReactDOM from "react-dom";
|
import "core-js/stable";
|
||||||
import { QueryClient, QueryClientProvider } from "react-query";
|
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
import { FontProvider } from "@dndbeyond/ftui-components";
|
|
||||||
import "@dndbeyond/ttui/styles/palette.css";
|
import "@dndbeyond/ttui/styles/palette.css";
|
||||||
|
import "@dndbeyond/ttui/styles/typography.css";
|
||||||
import wayOfLight from "@dndbeyond/ttui/themes/wayOfLight";
|
import wayOfLight from "@dndbeyond/ttui/themes/wayOfLight";
|
||||||
|
|
||||||
import { Layout } from "./components/Layout";
|
import { Layout } from "./components/Layout";
|
||||||
import { AuthProvider } from "./contexts/Authentication";
|
import config from "./config";
|
||||||
import { FeatureFlagProvider } from "./contexts/FeatureFlag";
|
import { AuthProvider, useAuth } from "./contexts/Authentication";
|
||||||
|
import { FeatureFlagProvider, useFeatureFlags } from "./contexts/FeatureFlag";
|
||||||
import { FiltersProvider } from "./contexts/Filters";
|
import { FiltersProvider } from "./contexts/Filters";
|
||||||
import { HeadContextProvider } from "./contexts/Head";
|
import { HeadContextProvider } from "./contexts/Head";
|
||||||
import { ThemeManagerProvider } from "./contexts/ThemeManager";
|
import { ThemeManagerProvider } from "./contexts/ThemeManager";
|
||||||
import { reportWebVitals } from "./helpers/reportWebVitals";
|
import { reportWebVitals } from "./helpers/reportWebVitals";
|
||||||
|
import { useDatadogRum } from "./hooks/useDatadogRum";
|
||||||
import { Routes } from "./routes";
|
import { Routes } from "./routes";
|
||||||
import "./styles/global.css";
|
import "./styles/global.css";
|
||||||
import "./styles/index.scss";
|
import "./styles/index.scss";
|
||||||
@@ -24,18 +28,19 @@ const queryClient = new QueryClient({
|
|||||||
defaultOptions: { queries: { staleTime: 60 * 60 * 1000 } },
|
defaultOptions: { queries: { staleTime: 60 * 60 * 1000 } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const fonts = [
|
const DatadogGate = () => {
|
||||||
{
|
const { datadogRumCharactersAppFlag } = useFeatureFlags();
|
||||||
family: "Tiamat Condensed SC",
|
return datadogRumCharactersAppFlag ? <DatadogBootstrap /> : null;
|
||||||
type: "woff2",
|
};
|
||||||
url: "https://www.dndbeyond.com/fonts/tiamatcondensedsc-regular-webfont.woff2",
|
|
||||||
},
|
const DatadogBootstrap = () => {
|
||||||
{
|
const user = useAuth();
|
||||||
family: "MrsEavesSmallCaps",
|
useDatadogRum(user ?? null);
|
||||||
type: "truetype",
|
|
||||||
url: "https://www.dndbeyond.com/content/skins/waterdeep/fonts/MrsEavesSmallCaps.ttf",
|
return null;
|
||||||
},
|
};
|
||||||
];
|
|
||||||
|
const environment = config.environment;
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<>
|
<>
|
||||||
@@ -43,10 +48,10 @@ const App = () => (
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeManagerProvider manager={{ lightOrDark: "light" }}>
|
<ThemeManagerProvider manager={{ lightOrDark: "light" }}>
|
||||||
<FontProvider fonts={fonts} />
|
|
||||||
<HeadContextProvider>
|
<HeadContextProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<FeatureFlagProvider>
|
<FeatureFlagProvider>
|
||||||
|
{environment !== "development" && <DatadogGate />}
|
||||||
<FiltersProvider>
|
<FiltersProvider>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes />
|
<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
|
// If you want to start measuring performance in your app, pass a function
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
|||||||
-13
@@ -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
@@ -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
@@ -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');
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.array.flat-map');
|
|
||||||
module.exports = require('../../modules/_core').Array.flatMap;
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.array.includes');
|
|
||||||
module.exports = require('../../modules/_core').Array.includes;
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.object.entries');
|
|
||||||
module.exports = require('../../modules/_core').Object.entries;
|
|
||||||
ddb_main/node_modules/@babel/polyfill/node_modules/core-js/fn/object/get-own-property-descriptors.js
Generated
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.object.get-own-property-descriptors');
|
|
||||||
module.exports = require('../../modules/_core').Object.getOwnPropertyDescriptors;
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.object.values');
|
|
||||||
module.exports = require('../../modules/_core').Object.values;
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
require('../../modules/es6.promise');
|
|
||||||
require('../../modules/es7.promise.finally');
|
|
||||||
module.exports = require('../../modules/_core').Promise['finally'];
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.string.pad-end');
|
|
||||||
module.exports = require('../../modules/_core').String.padEnd;
|
|
||||||
Generated
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.string.pad-start');
|
|
||||||
module.exports = require('../../modules/_core').String.padStart;
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.string.trim-right');
|
|
||||||
module.exports = require('../../modules/_core').String.trimRight;
|
|
||||||
Generated
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.string.trim-left');
|
|
||||||
module.exports = require('../../modules/_core').String.trimLeft;
|
|
||||||
Generated
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../../modules/es7.symbol.async-iterator');
|
|
||||||
module.exports = require('../../modules/_wks-ext').f('asyncIterator');
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
require('../modules/es7.global');
|
|
||||||
module.exports = require('../modules/_core').global;
|
|
||||||
Generated
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
module.exports = function (it) {
|
|
||||||
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
|
|
||||||
return it;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-5
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
var core = module.exports = { version: '2.6.12' };
|
|
||||||
if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef
|
|
||||||
Generated
Vendored
-20
@@ -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);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Generated
Vendored
-4
@@ -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;
|
|
||||||
});
|
|
||||||
Generated
Vendored
-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) : {};
|
|
||||||
};
|
|
||||||
Generated
Vendored
-62
@@ -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;
|
|
||||||
Generated
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
module.exports = function (exec) {
|
|
||||||
try {
|
|
||||||
return !!exec();
|
|
||||||
} catch (e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Generated
Vendored
-6
@@ -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
|
|
||||||
Generated
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
var hasOwnProperty = {}.hasOwnProperty;
|
|
||||||
module.exports = function (it, key) {
|
|
||||||
return hasOwnProperty.call(it, key);
|
|
||||||
};
|
|
||||||
Generated
Vendored
-8
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-3
@@ -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;
|
|
||||||
});
|
|
||||||
Generated
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
module.exports = function (it) {
|
|
||||||
return typeof it === 'object' ? it !== null : typeof it === 'function';
|
|
||||||
};
|
|
||||||
Generated
Vendored
-16
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-8
@@ -1,8 +0,0 @@
|
|||||||
module.exports = function (bitmap, value) {
|
|
||||||
return {
|
|
||||||
enumerable: !(bitmap & 1),
|
|
||||||
configurable: !(bitmap & 2),
|
|
||||||
writable: !(bitmap & 4),
|
|
||||||
value: value
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Generated
Vendored
-12
@@ -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");
|
|
||||||
};
|
|
||||||
Generated
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
// https://github.com/tc39/proposal-global
|
|
||||||
var $export = require('./_export');
|
|
||||||
|
|
||||||
$export($export.G, { global: require('./_global') });
|
|
||||||
Generated
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
module.exports = function (it) {
|
|
||||||
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
|
|
||||||
return it;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-5
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-7
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-8
@@ -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);
|
|
||||||
};
|
|
||||||
Generated
Vendored
-5
@@ -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;
|
|
||||||
};
|
|
||||||
-5
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-26
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-15
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-23
@@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Generated
Vendored
-44
@@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Generated
Vendored
-28
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-16
@@ -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;
|
|
||||||
};
|
|
||||||
Generated
Vendored
-6
@@ -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);
|
|
||||||
};
|
|
||||||
-25
@@ -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;
|
|
||||||
};
|
|
||||||
-23
@@ -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;
|
|
||||||
};
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
var toString = {}.toString;
|
|
||||||
|
|
||||||
module.exports = function (it) {
|
|
||||||
return toString.call(it).slice(8, -1);
|
|
||||||
};
|
|
||||||
Generated
Vendored
-144
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Generated
Vendored
-85
@@ -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
|
|
||||||
};
|
|
||||||
Generated
Vendored
-85
@@ -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
Reference in New Issue
Block a user