/* This file is part of GNU Taler (C) 2022 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ import { css } from "@linaria/core"; import { Fragment, h, JSX, VNode } from "preact"; import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "preact/hooks"; // eslint-disable-next-line import/extensions import { theme } from "../style.js"; import { FormControlContext, useFormControl } from "./FormControl.js"; const rootStyle = css` color: ${theme.palette.text.primary}; line-height: 1.4375em; box-sizing: border-box; position: relative; cursor: text; display: inline-flex; align-items: center; `; const rootDisabledStyle = css` color: ${theme.palette.text.disabled}; cursor: default; `; const rootMultilineStyle = css` padding: 4px 0 5px; `; const fullWidthStyle = css` width: "100%"; `; export function InputBaseRoot({ class: _class, disabled, error, multiline, focused, fullWidth, startAdornment, endAdornment, children, }: any): VNode { const fcs = useFormControl({}); return (
{children}
); } const componentStyle = css` font: inherit; letter-spacing: inherit; color: currentColor; border: 0px; box-sizing: content-box; background: none; height: 1.4375em; margin: 0px; -webkit-tap-highlight-color: transparent; display: block; min-width: 0px; width: 100%; animation-name: "auto-fill-cancel"; animation-duration: 10ms; @keyframes auto-fill { from { display: block; } } @keyframes auto-fill-cancel { from { display: block; } } &::placeholder { color: "currentColor"; opacity: ${theme.palette.mode === "light" ? 0.42 : 0.5}; transition: ${theme.transitions.create("opacity", { duration: theme.transitions.duration.shorter, })}; } &:not(focus)::placeholder { opacity: 0; } &:focus::placeholder { opacity: ${theme.palette.mode === "light" ? 0.42 : 0.5}; } &:focus { outline: 0; } &:invalid { box-shadow: none; } &::-webkit-search-decoration { -webkit-appearance: none; } &:-webkit-autofill { animation-duration: 5000s; animation-name: auto-fill; } textarea { height: "auto"; resize: "none"; padding: 0px; padding-top: 0px; } `; const componentDisabledStyle = css` opacity: 1; --webkit-text-fill-color: ${theme.palette.text.disabled}; `; const componentSmallStyle = css` padding-top: 1px; `; const componentMultilineStyle = css` height: auto; resize: none; padding: 0px; padding-top: 0px; `; const searchStyle = css` -moz-appearance: textfield; -webkit-appearance: textfield; `; export function InputBaseComponent({ disabled, size, multiline, type, class: _class, startAdornment, endAdornment, ...props }: any): VNode { return ( {startAdornment} {endAdornment} ); } export function InputBase({ Root = InputBaseRoot, Input, onChange, onInput, name, placeholder, readOnly, onKeyUp, onKeyDown, rows, type = "text", value, maxRows, minRows, onClick, ...props }: any): VNode { const fcs = useFormControl(props); // const [focused, setFocused] = useState(false); useLayoutEffect(() => { if (value && value !== "") { fcs.onFilled(); } else { fcs.onEmpty(); } }, [value, fcs]); const handleFocus = (event: JSX.TargetedFocusEvent): void => { // Fix a bug with IE11 where the focus/blur events are triggered // while the component is disabled. if (fcs.disabled) { event.stopPropagation(); return; } // if (onFocus) { // onFocus(event); // } // if (inputPropsProp.onFocus) { // inputPropsProp.onFocus(event); // } fcs.onFocus(); }; const handleBlur = (): void => { // if (onBlur) { // onBlur(event); // } // if (inputPropsProp.onBlur) { // inputPropsProp.onBlur(event); // } fcs.onBlur(); }; const handleChange = ( event: JSX.TargetedEvent, ): void => { // if (inputPropsProp.onChange) { // inputPropsProp.onChange(event, ...args); // } // Perform in the willUpdate if (onChange) { onChange(event.currentTarget.value); } }; const handleInput = ( event: JSX.TargetedEvent, ): void => { // if (inputPropsProp.onChange) { // inputPropsProp.onChange(event, ...args); // } // Perform in the willUpdate if (onInput) { event.currentTarget.value = onInput(event.currentTarget.value); } }; const handleClick = ( event: JSX.TargetedMouseEvent, ): void => { // if (inputRef.current && event.currentTarget === event.target) { // inputRef.current.focus(); // } if (onClick) { onClick(event.currentTarget.value); } }; const rowsProps = { minRows: rows ? rows : minRows, maxRows: rows ? rows : maxRows, }; if (props.multiline) { Input = TextareaAutoSize; } return ( ); } const shadowStyle = css` visibility: hidden; position: absolute; overflow: hidden; height: 0px; top: 0px; left: 0px; transform: translateZ(0); `; function ownerDocument(node: Node | null | undefined): Document { return (node && node.ownerDocument) || document; } function ownerWindow(node: Node | null | undefined): Window { const doc = ownerDocument(node); return doc.defaultView || window; } function getStyleValue( computedStyle: CSSStyleDeclaration, property: any, ): number { return parseInt(computedStyle[property], 10) || 0; } function debounce(func: any, wait = 166): any { let timeout: any; function debounced(...args: any[]): void { const later = () => { func.apply({}, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); } debounced.clear = () => { clearTimeout(timeout); }; return debounced; } export function TextareaAutoSize({ // disabled, // size, onChange, onInput, value, multiline, focused, disabled, error, minRows = 1, maxRows, style, type, class: _class, ...props }: any): VNode { // const { onChange, maxRows, minRows = 1, style, value, ...other } = props; const { current: isControlled } = useRef(value != null); const inputRef = useRef(null); // const handleRef = useForkRef(ref, inputRef); const shadowRef = useRef(null); const renders = useRef(0); const [state, setState] = useState<{ outerHeightStyle: any; overflow: any }>({ outerHeightStyle: undefined, overflow: undefined, }); const syncHeight = useCallback(() => { const input = inputRef.current; const inputShallow = shadowRef.current; if (!input || !inputShallow) return; const containerWindow = ownerWindow(input); const computedStyle = containerWindow.getComputedStyle(input); // If input's width is shrunk and it's not visible, don't sync height. if (computedStyle.width === "0px") { return; } inputShallow.style.width = computedStyle.width; inputShallow.value = input.value || props.placeholder || "x"; if (inputShallow.value.slice(-1) === "\n") { // Certain fonts which overflow the line height will cause the textarea // to report a different scrollHeight depending on whether the last line // is empty. Make it non-empty to avoid this issue. inputShallow.value += " "; } const boxSizing: string = computedStyle["box-sizing" as any]; const padding = getStyleValue(computedStyle, "padding-bottom") + getStyleValue(computedStyle, "padding-top"); const border = getStyleValue(computedStyle, "border-bottom-width") + getStyleValue(computedStyle, "border-top-width"); // The height of the inner content const innerHeight = inputShallow.scrollHeight; // Measure height of a textarea with a single row inputShallow.value = "x"; const singleRowHeight = inputShallow.scrollHeight; // The height of the outer content let outerHeight = innerHeight; if (minRows) { outerHeight = Math.max(Number(minRows) * singleRowHeight, outerHeight); } if (maxRows) { outerHeight = Math.min(Number(maxRows) * singleRowHeight, outerHeight); } outerHeight = Math.max(outerHeight, singleRowHeight); // Take the box sizing into account for applying this value as a style. const outerHeightStyle = outerHeight + (boxSizing === "border-box" ? padding + border : 0); const overflow = Math.abs(outerHeight - innerHeight) <= 1; setState((prevState) => { // Need a large enough difference to update the height. // This prevents infinite rendering loop. if ( renders.current < 20 && ((outerHeightStyle > 0 && Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) || prevState.overflow !== overflow) ) { renders.current += 1; return { overflow, outerHeightStyle, }; } return prevState; }); }, [maxRows, minRows, props.placeholder]); useLayoutEffect(() => { const handleResize = debounce(() => { renders.current = 0; syncHeight(); }); const containerWindow = ownerWindow(inputRef.current); containerWindow.addEventListener("resize", handleResize); let resizeObserver: any; if (typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(inputRef.current); } return () => { handleResize.clear(); containerWindow.removeEventListener("resize", handleResize); if (resizeObserver) { resizeObserver.disconnect(); } }; }, [syncHeight]); useLayoutEffect(() => { syncHeight(); }); useLayoutEffect(() => { renders.current = 0; }, [value]); const handleChange = (event: any): void => { renders.current = 0; if (!isControlled) { syncHeight(); } if (onChange) { onChange(event.target.value); } }; const handleInput = (event: any): void => { renders.current = 0; if (!isControlled) { syncHeight(); } if (onInput) { event.currentTarget.value = onInput(event.currentTarget.value); } }; return (