Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,9 @@ function saveThemeJson(cfg: pxt.TargetBundle, localDir?: boolean, packaged?: boo
if (cfg.appTheme?.pxtJsonOptions?.length) {
for (const option of cfg.appTheme.pxtJsonOptions) {
targetStrings[`{id:setting}${option.label}`] = option.label;
if (option.type === "input" && option.placeholder) {
targetStrings[`{id:setting}${option.placeholder}`] = option.placeholder;
}
}
}

Expand Down
16 changes: 14 additions & 2 deletions localtypings/pxtarget.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,12 +634,24 @@ declare namespace pxt {
markdown?: string;
}

interface PxtJsonOption {
interface PxtJsonOptionBase {
label: string;
property: string;
type: "checkbox" | "input";
}

interface PxtJsonCheckboxOption extends PxtJsonOptionBase {
type: "checkbox";
}

interface PxtJsonInputOption extends PxtJsonOptionBase {
type: "input";
placeholder?: string;
maxLength?: number;
filter?: string;
}

type PxtJsonOption = PxtJsonCheckboxOption | PxtJsonInputOption;

interface AssetConfig {
[index: string /*pxt.AssetType*/]: (boolean | { label?: string; iconClass?: string });
}
Expand Down
108 changes: 98 additions & 10 deletions react-common/components/controls/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface InputProps extends ControlProps {
preserveValueOnBlur?: boolean;
options?: pxt.Map<string>;
filter?: string;
maxLength?: number;

validator?: (value: string, prevValue: string) => string;
onChange?: (newValue: string) => void;
Expand Down Expand Up @@ -74,17 +75,21 @@ export const Input = (props: InputProps) => {
preserveValueOnBlur = true,
options,
validator,
maxLength,
} = props;

const [value, setValue] = React.useState(initialValue || "");
const [expanded, setExpanded] = React.useState(false);
const [filter] = React.useState(props.filter ? new RegExp(props.filter) : undefined);
const filter = React.useMemo(() => (props.filter ? new RegExp(props.filter, "g") : undefined), [props.filter]);
const optionValues = React.useMemo(() => (options ? Object.values(options) : []), [options]);
const isComposingRef = React.useRef(false);
const lastCommittedValueRef = React.useRef(initialValue || "");

let container: HTMLDivElement;

React.useEffect(() => {
setValue(initialValue || "");
lastCommittedValueRef.current = initialValue || "";
}, [initialValue]);

const handleContainerRef = (ref: HTMLDivElement) => {
Expand All @@ -102,20 +107,90 @@ export const Input = (props: InputProps) => {
}
}

const changeHandler = (e: React.ChangeEvent<any>) => {
let newValue = (e.target as any).value;
if (newValue && filter) {
newValue = newValue.match(filter)?.join("") || "";
const sanitizeValue = (val: string) => {
let res = val;
if (res && filter) {
filter.lastIndex = 0;
res = res.match(filter)?.join("") || "";
}
return res;
Comment thread
Copilot marked this conversation as resolved.
Resolved
};

const applyValue = (newValue: string) => {
if (!readOnly && (value !== newValue)) {
setValue(newValue);
}
if (onChange) {
onChange(newValue);
if (lastCommittedValueRef.current !== newValue) {
lastCommittedValueRef.current = newValue;
Comment thread
Copilot marked this conversation as resolved.
Resolved
if (onChange) {
onChange(newValue);
}
}
}
};

const processInput = (input: HTMLInputElement) => {
const rawValue = input.value;
const caret = input.selectionStart ?? rawValue.length;
let newCaretPos = caret;
if (filter) {
filter.lastIndex = 0;
const rawBeforeCaret = rawValue.slice(0, caret);
const cleanBeforeCaret = rawBeforeCaret.match(filter)?.join("") || "";
newCaretPos = cleanBeforeCaret.length;
}

const sanitized = sanitizeValue(rawValue);
if (input.value !== sanitized) {
input.value = sanitized;
if (input.setSelectionRange) {
input.setSelectionRange(newCaretPos, newCaretPos);
}
}
applyValue(sanitized);
};

const changeHandler = (e: React.ChangeEvent<any>) => {
const input = e.target as HTMLInputElement;
if (isComposingRef.current) {
setValue(input.value);
return;
}
Comment thread
Copilot marked this conversation as resolved.
Resolved
processInput(input);
};

const pasteHandler = (e: React.ClipboardEvent<HTMLInputElement>) => {
Comment thread
THEb0nny marked this conversation as resolved.
Resolved
if (!filter || readOnly || disabled) return;
e.preventDefault();
Comment thread
Copilot marked this conversation as resolved.
Resolved
const input = e.currentTarget;
const text = e.clipboardData.getData("text");
Comment thread
Copilot marked this conversation as resolved.
Resolved
filter.lastIndex = 0;
const cleanPaste = text.match(filter)?.join("") || "";
Comment thread
THEb0nny marked this conversation as resolved.
Resolved
const selStart = input.selectionStart ?? value.length;
const selEnd = input.selectionEnd ?? value.length;
const prefix = value.slice(0, selStart);
const suffix = value.slice(selEnd);
const maxInsertLen = maxLength !== undefined ? Math.max(0, maxLength - prefix.length - suffix.length) : cleanPaste.length;
const inserted = cleanPaste.slice(0, maxInsertLen);
Comment thread
Copilot marked this conversation as resolved.
Resolved
const newValue = prefix + inserted + suffix;
const newPos = selStart + inserted.length;
input.value = newValue;
Comment thread
Copilot marked this conversation as resolved.
Resolved
if (input.setSelectionRange) {
input.setSelectionRange(newPos, newPos);
}
applyValue(newValue);
};

const compositionStartHandler = () => {
isComposingRef.current = true;
};

const compositionEndHandler = (e: React.CompositionEvent<any>) => {
isComposingRef.current = false;
processInput(e.target as HTMLInputElement);
};

const keyDownHandler = (e: React.KeyboardEvent) => {
if (isComposingRef.current) return;
const charCode = (typeof e.which == "number") ? e.which : e.keyCode;
if (charCode === /*enter*/13 || props.treatSpaceAsEnter && charCode === /*space*/32) {
let validatedValue = value;
Expand Down Expand Up @@ -171,16 +246,24 @@ export const Input = (props: InputProps) => {
}

const blurHandler = () => {
let validatedValue = value;
let currentValue = value;
if (isComposingRef.current) {
isComposingRef.current = false;
currentValue = sanitizeValue(value);
applyValue(currentValue);
}
let validatedValue = currentValue;
if (validator) {
validatedValue = validator(value, initialValue || "");
validatedValue = validator(currentValue, initialValue || "");
setValue(validatedValue);
lastCommittedValueRef.current = validatedValue;
}
if (onBlur) {
onBlur(validatedValue);
}
if (!preserveValueOnBlur) {
setValue("");
lastCommittedValueRef.current = "";
}
}

Expand All @@ -195,6 +278,7 @@ export const Input = (props: InputProps) => {

const value = options[option];
setValue(value);
lastCommittedValueRef.current = value;
if (onOptionSelected) {
onOptionSelected(value);
}
Expand Down Expand Up @@ -237,13 +321,17 @@ export const Input = (props: InputProps) => {
autoFocus={autoFocus}
onClick={clickHandler}
onChange={changeHandler}
onPaste={pasteHandler}
onCompositionStart={compositionStartHandler}
onCompositionEnd={compositionEndHandler}
onKeyDown={keyDownHandler}
onBlur={blurHandler}
onFocus={focusHandler}
autoComplete={autoComplete ? "" : "off"}
autoCorrect={autoComplete ? "" : "off"}
autoCapitalize={autoComplete ? "" : "off"}
spellCheck={autoComplete}
maxLength={maxLength}
Comment thread
Copilot marked this conversation as resolved.
Resolved
disabled={disabled}
ref={handleInputRef} />
{icon && (onIconClick
Expand Down
3 changes: 2 additions & 1 deletion teachertool/src/components/CriteriaInstanceDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ const InlineInputSegment: React.FC<InlineInputSegmentProps> = ({
preserveValueOnBlur={true}
placeholder={numeric ? "0" : param.name}
title={tooltip}
filter={numeric && !paramOptions ? "[0-9]{1,2}" : undefined}
filter={numeric && !paramOptions ? "[0-9]" : undefined}
maxLength={numeric && !paramOptions ? 2 : undefined}
/>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/pxtjson.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,12 @@ export class Editor extends srceditor.Editor {
id={option.property}
label={pxt.Util.rlf(`{id:setting}${option.label}`)}
ariaLabel={option.label}
placeholder={option.placeholder ? pxt.Util.rlf(`{id:setting}${option.placeholder}`) : undefined}
Comment thread
THEb0nny marked this conversation as resolved.
Resolved
initialValue={(c as any)?.[option.property] || ""}
onChange={value => this.applyPropertyInput(option, value)}
autoComplete={false}
maxLength={option.maxLength}
filter={option.filter}
Comment thread
THEb0nny marked this conversation as resolved.
Resolved
/>
) : undefined
)}
Expand Down