From 301f183a2fa0b6c1b4269ae86df1b3ca03e68c0f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sun, 25 Feb 2024 22:34:03 +0000 Subject: [PATCH 01/14] wip: something is kinda working --- webui/package.json | 1 - webui/src/App.scss | 1 - webui/src/Components/TextInputField.tsx | 305 ++++++++++++++--------- webui/src/Controls/ButtonStyleConfig.tsx | 4 +- webui/src/scss/_controls.scss | 20 +- 5 files changed, 209 insertions(+), 122 deletions(-) diff --git a/webui/package.json b/webui/package.json index af7cc00d6a..9d405cd68c 100644 --- a/webui/package.json +++ b/webui/package.json @@ -59,7 +59,6 @@ "sanitize-html": "^2.11.0", "sass": "^1.71.0", "socket.io-client": "^4.7.4", - "tributejs": "^5.1.3", "typescript": "~5.3.3", "use-deep-compare": "^1.2.1", "usehooks-ts": "^2.14.0", diff --git a/webui/src/App.scss b/webui/src/App.scss index 181e040cde..f521ef485d 100644 --- a/webui/src/App.scss +++ b/webui/src/App.scss @@ -1,7 +1,6 @@ @import 'scss/variables'; @import '@coreui/coreui/scss/coreui'; -@import 'tributejs/src/tribute'; @import 'scss/react-time-picker'; @import 'scss/layout'; diff --git a/webui/src/Components/TextInputField.tsx b/webui/src/Components/TextInputField.tsx index c8e92af74b..84fe48d615 100644 --- a/webui/src/Components/TextInputField.tsx +++ b/webui/src/Components/TextInputField.tsx @@ -1,7 +1,10 @@ -import Tribute from 'tributejs' -import React, { useEffect, useMemo, useState, useCallback, useContext, ChangeEvent } from 'react' +import React, { useEffect, useMemo, useState, useCallback, useContext, ChangeEvent, useRef } from 'react' import { CInput } from '@coreui/react' import { VariableDefinitionsContext } from '../util.js' +import Select, { OptionProps, components as SelectComponents, createFilter } from 'react-select' +import { MenuPortalContext } from './DropdownInputField.js' +import { DropdownChoiceId } from '@companion-module/base' +import { observer } from 'mobx-react-lite' interface TextInputFieldProps { regex?: string @@ -17,13 +20,7 @@ interface TextInputFieldProps { useLocationVariables?: boolean } -interface TributeSuggestion { - key: string - value: string - label: string -} - -export function TextInputField({ +export const TextInputField = observer(function TextInputField({ regex, required, tooltip, @@ -36,71 +33,8 @@ export function TextInputField({ useVariables, useLocationVariables, }: TextInputFieldProps) { - const variableDefinitionsContext = useContext(VariableDefinitionsContext) - const [tmpValue, setTmpValue] = useState(null) - const tribute = useMemo(() => { - // Create it once, then we attach and detach whenever the ref changes - // @ts-expect-error Tribute import is broken - return new Tribute({ - values: [], - trigger: '$(', - - // function called on select that returns the content to insert - selectTemplate: (item: any) => `$(${item.original.value})`, - - // template for displaying item in menu - menuItemTemplate: (item: any) => - `${item.original.value}${item.original.label}`, - }) - }, []) - - useEffect(() => { - // Update the suggestions list in tribute whenever anything changes - const suggestions: TributeSuggestion[] = [] - if (useVariables) { - for (const [connectionLabel, variables] of Object.entries(variableDefinitionsContext)) { - for (const [name, va] of Object.entries(variables || {})) { - if (!va) continue - const variableId = `${connectionLabel}:${name}` - suggestions.push({ - key: variableId + ')', - value: variableId, - label: va.label, - }) - } - } - } - - if (useLocationVariables) { - suggestions.push( - { - key: 'this:page)', - value: 'this:page', - label: 'This page', - }, - { - key: 'this:column)', - value: 'this:column', - label: 'This column', - }, - { - key: 'this:row)', - value: 'this:row', - label: 'This row', - }, - { - key: 'this:page_name)', - value: 'this:page_name', - label: 'This page name', - } - ) - } - - tribute.append(0, suggestions, true) - }, [variableDefinitionsContext, tribute, useVariables, useLocationVariables]) - // Compile the regex (and cache) const compiledRegex = useMemo(() => { if (regex) { @@ -143,55 +77,202 @@ export function TextInputField({ setValid?.(isValueValid(value)) }, [isValueValid, value, setValid]) - const doOnChange = useCallback( - (e: React.ChangeEvent) => { + const storeValue = useCallback( + (value: string) => { // const newValue = decode(e.currentTarget.value, { scope: 'strict' }) - setTmpValue(e.currentTarget.value) - setValue(e.currentTarget.value) - setValid?.(isValueValid(e.currentTarget.value)) + setTmpValue(value) + setValue(value) + setValid?.(isValueValid(value)) }, [setValue, setValid, isValueValid] ) - - const [, setupTributePrevious] = useState< - [HTMLInputElement | null, ((e: React.ChangeEvent) => void) | null] - >([null, null]) - const setupTribute = useCallback( - (ref: HTMLInputElement) => { - // we need to detach, so need to track the value manually - setupTributePrevious(([oldRef, oldDoOnChange]) => { - if (oldRef) { - tribute.detach(oldRef) - if (oldDoOnChange) { - // @ts-expect-error - oldRef.removeEventListener('tribute-replaced', oldDoOnChange) - } - } - if (ref) { - tribute.attach(ref) - // @ts-expect-error - ref.addEventListener('tribute-replaced', doOnChange) - } - return [ref, doOnChange] - }) - }, - [tribute, doOnChange] + const doOnChange = useCallback( + (e: React.ChangeEvent) => storeValue(e.currentTarget.value), + [storeValue] ) + let isPickerOpen = false + let searchValue = '' + + const innerRef = useRef(null) + if (innerRef.current) { + console.log('cursor', innerRef.current.selectionStart) + if (innerRef.current.selectionStart != null && innerRef.current.selectionStart === innerRef.current.selectionEnd) { + const lastOpen = FindVariableStartIndexFromCursor(value, innerRef.current.selectionStart) + isPickerOpen = lastOpen !== -1 + console.log('open', FindVariableStartIndexFromCursor(value, innerRef.current.selectionStart)) + + searchValue = value.slice(lastOpen + 2, innerRef.current.selectionStart) + console.log('search', searchValue) + } + } + + const valueRef = useRef() + valueRef.current = value + + const onVariableSelect = useCallback((variable: DropdownChoiceInt | null) => { + const oldValue = valueRef.current + if (!variable || !oldValue || !innerRef.current) return + + if (!innerRef.current.selectionStart) return // Nothing selected + + const openIndex = FindVariableStartIndexFromCursor(oldValue, innerRef.current.selectionStart) + if (openIndex === -1) return + + storeValue(oldValue.slice(0, openIndex) + `$(${variable.value})` + oldValue.slice(innerRef.current.selectionStart)) + }, []) + + // const [variableSearchOpen, setVariableSearchOpen] = useState(false) + + const onFocusChange = useCallback(() => { + console.log('focus change') + }, []) + // Render the input const extraStyle = style || {} return ( - setTmpValue(value ?? '')} - onBlur={() => setTmpValue(null)} - placeholder={placeholder} + <> + setTmpValue(value ?? '')} + onBlur={() => setTmpValue(null)} + placeholder={placeholder} + onFocusCapture={onFocusChange} + onBlurCapture={onFocusChange} + /> +

aa

+ {useVariables && ( + + )} + + ) +}) + +interface DropdownChoiceInt { + value: string + label: DropdownChoiceId +} + +interface VariablesSelectProps { + isOpen: boolean + searchValue: string + onVariableSelect: (newValue: DropdownChoiceInt | null) => void + useLocationVariables: boolean +} + +function VariablesSelect({ isOpen, searchValue, onVariableSelect, useLocationVariables }: VariablesSelectProps) { + const variableDefinitionsContext = useContext(VariableDefinitionsContext) + const menuPortal = useContext(MenuPortalContext) + + const options = useMemo(() => { + // Update the suggestions list in tribute whenever anything changes + const suggestions: DropdownChoiceInt[] = [] + for (const [connectionLabel, variables] of Object.entries(variableDefinitionsContext)) { + for (const [name, va] of Object.entries(variables || {})) { + if (!va) continue + const variableId = `${connectionLabel}:${name}` + suggestions.push({ + // key: variableId + ')', + value: variableId, + label: va.label, + }) + } + } + + if (useLocationVariables) { + suggestions.push( + { + // key: 'this:page)', + value: 'this:page', + label: 'This page', + }, + { + // key: 'this:column)', + value: 'this:column', + label: 'This column', + }, + { + // key: 'this:row)', + value: 'this:row', + label: 'This row', + }, + { + // key: 'this:page_name)', + value: 'this:page_name', + label: 'This page name', + } + ) + } + + return suggestions + }, [variableDefinitionsContext, useLocationVariables]) + + const valueOption: DropdownChoiceInt = { + value: searchValue, + label: searchValue, + } + console.log('s', searchValue) + + return ( + { } const CustomControl = (props: ControlProps) => { - // const { data } = props - // const tempContext2 = useContext(tempContext) - return ( {props.children} @@ -366,12 +357,10 @@ const CustomValueContainer = (props: ValueContainerProps) => - {/* */} )} diff --git a/webui/src/scss/_controls.scss b/webui/src/scss/_controls.scss index 351704567b..30235fddc5 100644 --- a/webui/src/scss/_controls.scss +++ b/webui/src/scss/_controls.scss @@ -28,7 +28,6 @@ label.disabled { } /* Style for autocomplete of variable */ -.tribute-container li span, .variable-suggestion-option span { display: block; @@ -44,13 +43,20 @@ label.disabled { } } -.variables-text-input { - & > div { - padding: 0 !important; - } +.variable-select-root { + display: flex; + flex-grow: 1; + + .variables-text-input { + flex-grow: 1; - .form-control { - border: none; + & > div { + padding: 0 !important; + } + + .form-control { + border: none; + } } } From bdc7b18b1b7ee732f3453294fa2671e5128e944a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sun, 25 Feb 2024 23:48:19 +0000 Subject: [PATCH 06/14] wip: tidying --- webui/src/Components/TextInputField.tsx | 73 +++++++++---------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/webui/src/Components/TextInputField.tsx b/webui/src/Components/TextInputField.tsx index 0d30878ae9..6102efb962 100644 --- a/webui/src/Components/TextInputField.tsx +++ b/webui/src/Components/TextInputField.tsx @@ -1,13 +1,7 @@ -import React, { useEffect, useMemo, useState, useCallback, useContext, ChangeEvent, useRef, memo } from 'react' +import React, { useEffect, useMemo, useState, useCallback, useContext, useRef } from 'react' import { CInput } from '@coreui/react' import { VariableDefinitionsContext } from '../util.js' -import Select, { - ControlProps, - OptionProps, - components as SelectComponents, - ValueContainerProps, - createFilter, -} from 'react-select' +import Select, { ControlProps, OptionProps, components as SelectComponents, ValueContainerProps } from 'react-select' import { MenuPortalContext } from './DropdownInputField.js' import { DropdownChoiceId } from '@companion-module/base' import { observer } from 'mobx-react-lite' @@ -143,18 +137,23 @@ export const TextInputField = observer(function TextInputField({ // console.log('focus change') // }, []) + const extraStyle = useMemo( + () => ({ color: !isValueValid(showValue) ? 'red' : undefined, ...style }), + [isValueValid, showValue, style] + ) + const tmpVal = { value: showValue, setValue: doOnChange, setTmpValue: setTmpValue, setCursorPosition: setCursorPosition, - extraStyle: { color: !isValueValid(tmpValue ?? value) ? 'red' : undefined, ...style }, // TODO - memo + extraStyle: extraStyle, } // Render the input return ( <> - + {useVariables ? ( setTmpValue(value ?? '')} @@ -175,7 +174,7 @@ export const TextInputField = observer(function TextInputField({ placeholder={placeholder} /> )} - + ) }) @@ -202,10 +201,8 @@ function VariablesSelect({ isOpen, searchValue, onVariableSelect, useLocationVar for (const [connectionLabel, variables] of Object.entries(variableDefinitionsContext)) { for (const [name, va] of Object.entries(variables || {})) { if (!va) continue - const variableId = `${connectionLabel}:${name}` suggestions.push({ - // key: variableId + ')', - value: variableId, + value: `${connectionLabel}:${name}`, label: va.label, }) } @@ -214,22 +211,18 @@ function VariablesSelect({ isOpen, searchValue, onVariableSelect, useLocationVar if (useLocationVariables) { suggestions.push( { - // key: 'this:page)', value: 'this:page', label: 'This page', }, { - // key: 'this:column)', value: 'this:column', label: 'This column', }, { - // key: 'this:row)', value: 'this:row', label: 'This row', }, { - // key: 'this:page_name)', value: 'this:page_name', label: 'This page name', } @@ -242,7 +235,6 @@ function VariablesSelect({ isOpen, searchValue, onVariableSelect, useLocationVar return ( { - // // e.preventDefault() - // }} - /> + +