TeaWeb/shared/js/ui/modal/css-editor/Renderer.tsx

420 lines
15 KiB
TypeScript
Raw Normal View History

import * as React from "react";
import {CssEditorEvents, CssEditorUserData, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions";
import {Registry} from "tc-shared/events";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {BoxedInputField, FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {useState} from "react";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
import {Button} from "tc-shared/ui/react-elements/Button";
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
const cssStyle = require("./Renderer.scss");
const CssVariableRenderer = React.memo((props: { events: Registry<CssEditorEvents>, variable: CssVariable, selected: boolean }) => {
const [ selected, setSelected ] = useState(props.selected);
const [ override, setOverride ] = useState(props.variable.overwriteValue);
const [ overrideColor, setOverrideColor ] = useState(props.variable.customValue);
props.events.reactUse("action_select_entry", event => setSelected(event.variable === props.variable));
props.events.reactUse("action_override_toggle", event => {
if(event.variableName !== props.variable.name)
return;
setOverride(event.enabled);
if(event.enabled)
setOverrideColor(event.value);
});
props.events.reactUse("action_change_override_value", event => {
if(event.variableName !== props.variable.name)
return;
setOverrideColor(event.value);
});
return (
<div
className={cssStyle.variable + " " + (selected ? cssStyle.selected : "")}
onClick={() => {
if(selected)
return;
props.events.fire("action_select_entry", { variable: props.variable })
}}
>
<div className={cssStyle.preview}>
<div
className={cssStyle.color}
style={{ backgroundColor: props.variable.defaultValue }}
/>
</div>
<div className={cssStyle.preview}>
<div
className={cssStyle.color}
style={{ backgroundColor: override ? overrideColor : undefined }}
/>
</div>
<a>{props.variable.name}</a>
</div>
);
});
const CssVariableListBodyRenderer = (props: { events: Registry<CssEditorEvents> }) => {
const [ variables, setVariables ] = useState<"loading" | CssVariable[]>(() => {
props.events.fire_async("query_css_variables");
return "loading";
});
const [ filter, setFilter ] = useState(undefined);
const [ selectedVariable, setSelectedVariable ] = useState(undefined);
props.events.reactUse("action_select_entry", event => setSelectedVariable(event.variable));
props.events.reactUse("query_css_variables", () => setVariables("loading"));
let content;
if(variables === "loading") {
content = (
<div className={cssStyle.overlay} key={"loading"}>
<a>
<Translatable>Loading</Translatable>&nbsp;
<LoadingDots />
</a>
</div>
);
} else {
content = [];
for(const variable of variables) {
if(filter && variable.name.toLowerCase().indexOf(filter) === -1)
continue;
content.push(<CssVariableRenderer
key={"variable-" + variable.name}
events={props.events}
variable={variable}
selected={selectedVariable === variable.name}
/>);
}
if(content.length === 0) {
content.push(
<div className={cssStyle.overlay} key={"no-match"}>
<a><Translatable>No variable matched your filter</Translatable></a>
</div>
);
}
}
props.events.reactUse("action_set_filter", event => setFilter(event.filter?.toLowerCase()));
props.events.reactUse("notify_css_variables", event => setVariables(event.variables));
return (
<div className={cssStyle.body} onKeyPress={event => {
if(variables === "loading")
return;
/* TODO: This isn't working since the div isn't focused properly yet */
let offset = 0;
if(event.key === "ArrowDown") {
offset = 1;
} else if(event.key === "ArrowUp") {
offset = -1;
}
if(offset !== 0) {
const selectIndex = variables.findIndex(e => e === selectedVariable);
if(selectIndex === -1)
return;
const variable = variables[selectIndex + offset];
if(!variable)
return;
props.events.fire("action_select_entry", { variable: variable });
}
}} tabIndex={0}>
{content}
</div>
);
};
const CssVariableListSearchRenderer = (props: { events: Registry<CssEditorEvents> }) => {
const [ isLoading, setLoading ] = useState(true);
props.events.reactUse("notify_css_variables", () => setLoading(false));
props.events.reactUse("query_css_variables", () => setLoading(true));
return (
<div className={cssStyle.search}>
<FlatInputField
label={<Translatable>Filter variables</Translatable>}
labelType={"floating"}
className={cssStyle.input}
onInput={text => props.events.fire("action_set_filter", { filter: text })}
disabled={isLoading}
/>
</div>
);
};
const CssVariableListRenderer = (props: { events: Registry<CssEditorEvents> }) => (
<div className={cssStyle.containerList}>
<div className={cssStyle.header}>
<a><Translatable>CSS Variable list</Translatable></a>
</div>
<div className={cssStyle.list} onKeyPress={event => console.error(event.key)}>
<CssVariableListBodyRenderer events={props.events} />
<CssVariableListSearchRenderer events={props.events} />
</div>
</div>
);
const SelectedVariableInfo = (props: { events: Registry<CssEditorEvents> }) => {
const [ selectedVariable, setSelectedVariable ] = useState<CssVariable>(undefined);
props.events.reactUse("action_select_entry", event => setSelectedVariable(event.variable));
return (<>
<div className={cssStyle.detail}>
<div className={cssStyle.title}>
<Translatable>Name</Translatable>
</div>
<div className={cssStyle.value}>
<BoxedInputField
editable={false}
value={selectedVariable ? selectedVariable.name : "-"}
/>
</div>
</div>
<div className={cssStyle.detail}>
<div className={cssStyle.title}>
<Translatable>Default Value</Translatable>
</div>
<div className={cssStyle.value}>
<BoxedInputField
editable={false}
value={selectedVariable ? selectedVariable.defaultValue : "-"}
/>
</div>
</div>
</>);
};
const OverrideVariableInfo = (props: { events: Registry<CssEditorEvents> }) => {
const [ selectedVariable, setSelectedVariable ] = useState<CssVariable>(undefined);
const [ overwriteValue, setOverwriteValue ] = useState<string>(undefined);
const [ overwriteEnabled, setOverwriteEnabled ] = useState(false);
props.events.reactUse("action_select_entry", event => {
setSelectedVariable(event.variable);
setOverwriteEnabled(event.variable?.overwriteValue);
setOverwriteValue(event.variable?.customValue);
});
props.events.reactUse("action_override_toggle", event => {
if(event.variableName !== selectedVariable?.name)
return;
selectedVariable.overwriteValue = event.enabled;
setOverwriteEnabled(event.enabled);
if(event.enabled)
setOverwriteValue(event.value);
});
props.events.reactUse("action_change_override_value", event => {
if(event.variableName !== selectedVariable?.name)
return;
setOverwriteValue(event.value);
});
return (<>
<div className={cssStyle.detail}>
<div className={cssStyle.title}>
<Translatable>Override Value</Translatable>
<Checkbox
value={overwriteEnabled}
disabled={!selectedVariable}
onChange={value => {
props.events.fire("action_override_toggle", {
variableName: selectedVariable.name,
value: typeof selectedVariable.customValue === "string" ? selectedVariable.customValue : selectedVariable.defaultValue,
enabled: value
});
}}
/>
</div>
<div className={cssStyle.value + " " + cssStyle.color}>
<BoxedInputField
className={cssStyle.input}
disabled={!overwriteEnabled}
value={overwriteValue || " "}
onInput={text => {
selectedVariable.customValue = text;
props.events.fire("action_change_override_value", { value: text, variableName: selectedVariable.name });
}}
/>
<CssVariableColorPicker events={props.events} selectedVariable={selectedVariable} />
</div>
</div>
</>);
};
const CssVariableColorPicker = (props: { events: Registry<CssEditorEvents>, selectedVariable: CssVariable }) => {
const [ overwriteValue, setOverwriteValue ] = useState<string>(undefined);
const [ overwriteEnabled, setOverwriteEnabled ] = useState(false);
props.events.reactUse("action_override_toggle", event => {
if(event.variableName !== props.selectedVariable?.name)
return;
props.selectedVariable.overwriteValue = event.enabled;
setOverwriteEnabled(event.enabled);
if(event.enabled)
setOverwriteValue(event.value);
});
props.events.reactUse("action_change_override_value", event => {
if(event.variableName !== props.selectedVariable?.name || 'cpInvoker' in event)
return;
setOverwriteValue(event.value);
});
let currentInput: string;
let inputTimeout: number;
return (
<label className={cssStyle.colorButton} >
<input
disabled={!overwriteEnabled}
type={"color"}
value={overwriteValue}
onChange={event => {
currentInput = event.target.value;
if(inputTimeout)
return;
inputTimeout = setTimeout(() => {
inputTimeout = undefined;
props.events.fire("action_change_override_value", { value: currentInput, variableName: props.selectedVariable.name });
}, 150);
}}
/>
<a className="rainbow-letter" style={{ borderBottomColor: overwriteValue }}>C</a>
</label>
)
};
const ControlButtons = (props: { events: Registry<CssEditorEvents> }) => {
return (
<div className={cssStyle.buttons}>
<Button
color={"blue"}
type={"normal"}
className={cssStyle.button}
onClick={() => props.events.fire("action_randomize")}
><Translatable>Randomize</Translatable></Button>
<Button
color={"red"}
type={"normal"}
className={cssStyle.button + " " + cssStyle.buttonReset}
onClick={() => props.events.fire("action_reset")}
><Translatable>Reset</Translatable></Button>
<Button
color={"blue"}
type={"normal"}
className={cssStyle.button}
onClick={() => props.events.fire("action_export")}
><Translatable>Export</Translatable></Button>
<Button
color={"green"}
type={"normal"}
className={cssStyle.button}
onClick={() => requestFileAsText().then(content => {
props.events.fire("action_import", { config: content })
})}
><Translatable>Import</Translatable></Button>
</div>
)
};
const CssVariableEditor = (props: { events: Registry<CssEditorEvents> }) => {
return (
<div className={cssStyle.containerEdit}>
<div className={cssStyle.header}>
<a><Translatable>Variable details</Translatable></a>
</div>
<SelectedVariableInfo events={props.events} />
<OverrideVariableInfo events={props.events} />
<ControlButtons events={props.events} />
</div>
)
};
const downloadTextAsFile = (text, name) => {
const element = document.createElement("a");
element.text = "download";
element.href = "data:test/plain;charset=utf-8," + encodeURIComponent(text);
element.download = name;
element.style.display = "none";
document.body.appendChild(element);
element.click();
element.remove();
};
const requestFileAsText = async (): Promise<string> => {
const element = document.createElement("input");
element.style.display = "none";
element.type = "file";
document.body.appendChild(element);
element.click();
await new Promise(resolve => {
element.onchange = resolve;
});
if(element.files.length !== 1)
return undefined;
const file = element.files[0];
element.remove();
return await file.text();
};
class PopoutConversationUI extends AbstractModal {
private readonly events: Registry<CssEditorEvents>;
private readonly userData: CssEditorUserData;
constructor(events: Registry<CssEditorEvents>, userData: CssEditorUserData) {
super();
this.userData = userData;
this.events = events;
this.events.on("notify_export_result", event => {
createInfoModal(tr("Config exported successfully"), tr("The config has been exported successfully.")).open();
downloadTextAsFile(event.config, "teaweb-style.json");
});
this.events.on("notify_import_result", event => {
if(event.success)
createInfoModal(tr("Config imported successfully"), tr("The config has been imported successfully.")).open();
else
createErrorModal(tr("Config imported failed"), tr("The config import has been failed.")).open();
})
}
renderBody() {
return (
<div className={cssStyle.container}>
<CssVariableListRenderer events={this.events} />
<CssVariableEditor events={this.events} />
</div>
);
}
title() {
return "CSS Variable editor";
}
}
export = PopoutConversationUI;