Allowing the user to easily edit the channel name mode
parent
f80b9e9caa
commit
65d7051819
|
@ -114,55 +114,73 @@ export interface ChannelEvents extends ChannelTreeEntryEvents {
|
|||
notify_description_changed: {}
|
||||
}
|
||||
|
||||
export class ParsedChannelName {
|
||||
export type ChannelNameAlignment = "center" | "right" | "left" | "normal" | "repetitive";
|
||||
export class ChannelNameParser {
|
||||
readonly originalName: string;
|
||||
alignment: "center" | "right" | "left" | "normal" | "repetitive";
|
||||
alignment: ChannelNameAlignment;
|
||||
text: string; /* does not contain any alignment codes */
|
||||
uniqueId: string;
|
||||
|
||||
constructor(name: string, hasParentChannel: boolean) {
|
||||
this.originalName = name;
|
||||
this.parse(hasParentChannel);
|
||||
}
|
||||
|
||||
private parse(has_parent_channel: boolean) {
|
||||
private parse(hasParentChannel: boolean) {
|
||||
this.alignment = "normal";
|
||||
if(this.originalName.length < 3) {
|
||||
this.text = this.originalName;
|
||||
return;
|
||||
}
|
||||
|
||||
parse_type:
|
||||
if(!has_parent_channel && this.originalName.charAt(0) == '[') {
|
||||
|
||||
parseType:
|
||||
if(!hasParentChannel && this.originalName.charAt(0) == '[') {
|
||||
let end = this.originalName.indexOf(']');
|
||||
if(end === -1) break parse_type;
|
||||
if(end === -1) {
|
||||
break parseType;
|
||||
}
|
||||
|
||||
let options = this.originalName.substr(1, end - 1);
|
||||
if(options.indexOf("spacer") === -1) break parse_type;
|
||||
options = options.substr(0, options.indexOf("spacer"));
|
||||
const spacerIndex = options.indexOf("spacer");
|
||||
if(spacerIndex === -1) break parseType;
|
||||
this.uniqueId = options.substring(spacerIndex + 6);
|
||||
options = options.substr(0, spacerIndex);
|
||||
|
||||
if(options.length == 0)
|
||||
if(options.length == 0) {
|
||||
options = "l";
|
||||
else if(options.length > 1)
|
||||
} else if(options.length > 1) {
|
||||
options = options[0];
|
||||
}
|
||||
|
||||
switch (options) {
|
||||
case "r":
|
||||
this.alignment = "right";
|
||||
break;
|
||||
|
||||
case "l":
|
||||
this.alignment = "left";
|
||||
break;
|
||||
|
||||
case "c":
|
||||
this.alignment = "center";
|
||||
break;
|
||||
|
||||
case "*":
|
||||
this.alignment = "repetitive";
|
||||
break;
|
||||
|
||||
default:
|
||||
break parse_type;
|
||||
break parseType;
|
||||
}
|
||||
|
||||
this.text = this.originalName.substr(end + 1);
|
||||
}
|
||||
if(!this.text && this.alignment === "normal")
|
||||
|
||||
if(!this.text && this.alignment === "normal") {
|
||||
this.text = this.originalName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||
|
@ -177,7 +195,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
|
||||
readonly events: Registry<ChannelEvents>;
|
||||
|
||||
parsed_channel_name: ParsedChannelName;
|
||||
parsed_channel_name: ChannelNameParser;
|
||||
|
||||
private _family_index: number = 0;
|
||||
|
||||
|
@ -206,7 +224,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
this.properties = new ChannelProperties();
|
||||
this.channelId = channelId;
|
||||
this.properties.channel_name = channelName;
|
||||
this.parsed_channel_name = new ParsedChannelName(channelName, false);
|
||||
this.parsed_channel_name = new ChannelNameParser(channelName, false);
|
||||
|
||||
this.clientPropertyChangedListener = (event: ClientEvents["notify_properties_updated"]) => {
|
||||
if("client_nickname" in event.updated_properties || "client_talk_power" in event.updated_properties) {
|
||||
|
@ -627,7 +645,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
|
||||
if(hasUpdate) {
|
||||
if(key == "channel_name") {
|
||||
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
|
||||
this.parsed_channel_name = new ChannelNameParser(value, this.hasParent());
|
||||
} else if(key == "channel_order") {
|
||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||
this.channelTree.moveChannel(this, order, this.parent, false);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ChannelEntry, ChannelProperties, ChannelSidebarMode} from "tc-shared/tree/Channel";
|
||||
import {ChannelEntry, ChannelNameParser, ChannelProperties, ChannelSidebarMode} from "tc-shared/tree/Channel";
|
||||
import {ChannelEditableProperty} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||
import {ChannelTree} from "tc-shared/tree/ChannelTree";
|
||||
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
|
||||
|
@ -17,7 +17,41 @@ const SimplePropertyProvider = <P extends keyof ChannelProperties>(channelProper
|
|||
};
|
||||
}
|
||||
|
||||
ChannelPropertyProviders["name"] = SimplePropertyProvider("channel_name", "");
|
||||
ChannelPropertyProviders["name"] = {
|
||||
provider: async (properties, channel, parentChannel, channelTree) => {
|
||||
let spacerUniqueId = 0;
|
||||
const hasParent = !!(channel?.hasParent() || parentChannel);
|
||||
if(!hasParent) {
|
||||
const channels = channelTree.rootChannel();
|
||||
while(true) {
|
||||
let matchFound = false;
|
||||
for(const channel of channels) {
|
||||
if(channel.parsed_channel_name.uniqueId === spacerUniqueId.toString()) {
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!matchFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
spacerUniqueId++;
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = new ChannelNameParser(properties.channel_name, hasParent);
|
||||
return {
|
||||
rawName: properties.channel_name,
|
||||
spacerUniqueId: parsed.uniqueId || spacerUniqueId.toString(),
|
||||
hasParent,
|
||||
maxNameLength: 30 - (parsed.originalName.length - parsed.text.length),
|
||||
parsedAlignment: parsed.alignment,
|
||||
parsedName: parsed.text
|
||||
}
|
||||
},
|
||||
applier: (value, properties) => properties.channel_name = value.rawName
|
||||
}
|
||||
ChannelPropertyProviders["phoneticName"] = SimplePropertyProvider("channel_name_phonetic", "");
|
||||
ChannelPropertyProviders["icon"] = {
|
||||
provider: async (properties, _channel, _parentChannel, channelTree) => {
|
||||
|
|
|
@ -32,7 +32,14 @@ export type ChannelEditPermissionsState = {
|
|||
};
|
||||
|
||||
export interface ChannelEditableProperty {
|
||||
"name": string,
|
||||
"name": {
|
||||
rawName: string,
|
||||
parsedName?: string,
|
||||
parsedAlignment?: "center" | "right" | "left" | "normal" | "repetitive",
|
||||
maxNameLength?: number,
|
||||
hasParent?: boolean,
|
||||
spacerUniqueId?: string
|
||||
},
|
||||
"phoneticName": string,
|
||||
|
||||
"icon": {
|
||||
|
|
|
@ -40,6 +40,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
.channelName {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
min-width: 10em;
|
||||
|
||||
.select {
|
||||
margin-right: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
&.hasParent {
|
||||
.select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 1em;
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import * as React from "react";
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
|
@ -16,7 +15,7 @@ import {Switch} from "tc-shared/ui/react-elements/Switch";
|
|||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||
import {RadioButton} from "tc-shared/ui/react-elements/RadioButton";
|
||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
||||
|
@ -24,6 +23,7 @@ import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
|||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {getIconManager} from "tc-shared/file/Icons";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import {ChannelNameAlignment, ChannelNameParser} from "tc-shared/tree/Channel";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
|
@ -122,6 +122,10 @@ function useValidationState<T extends keyof ChannelEditableProperty>(property: T
|
|||
return valid;
|
||||
}
|
||||
|
||||
const ChannelNameType = (props: { selected: ChannelNameAlignment }) => {
|
||||
|
||||
}
|
||||
|
||||
const ChannelName = React.memo(() => {
|
||||
const modalType = useContext(ModalTypeContext);
|
||||
|
||||
|
@ -129,16 +133,72 @@ const ChannelName = React.memo(() => {
|
|||
const editable = usePropertyPermission("name", modalType === "channel-create");
|
||||
const valid = useValidationState("name");
|
||||
|
||||
const refSelect = useRef<HTMLSelectElement>();
|
||||
|
||||
const setValue = (text: string | undefined, localOnly: boolean) => {
|
||||
let rawName;
|
||||
switch(propertyValue.hasParent ? "normal" : refSelect.current.value) {
|
||||
case "center":
|
||||
rawName = "[cspacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
case "left":
|
||||
rawName = "[lspacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
case "right":
|
||||
rawName = "[rspacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
case "repetitive":
|
||||
rawName ="[*spacer" + propertyValue.spacerUniqueId + "]" + text;
|
||||
break;
|
||||
|
||||
default:
|
||||
case "normal":
|
||||
rawName = text;
|
||||
break;
|
||||
}
|
||||
|
||||
setPropertyValue({
|
||||
rawName,
|
||||
parsedName: text,
|
||||
|
||||
hasParent: propertyValue.hasParent,
|
||||
spacerUniqueId: propertyValue.spacerUniqueId,
|
||||
maxNameLength: propertyValue.maxNameLength,
|
||||
parsedAlignment: propertyValue.parsedAlignment
|
||||
}, localOnly);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={joinClassList(cssStyle.channelName, propertyValue?.hasParent && cssStyle.hasParent)}>
|
||||
<Select
|
||||
value={propertyValue?.parsedAlignment || "loading"}
|
||||
className={cssStyle.select}
|
||||
onChange={() => setValue(propertyValue.parsedName, false)}
|
||||
type={"boxed"}
|
||||
title={useTr("Channel name mode")}
|
||||
refSelect={refSelect}
|
||||
>
|
||||
<option value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
|
||||
<option value={"normal"}>{useTr("Normal Name")}</option>
|
||||
<option value={"center"}>{useTr("Centered")}</option>
|
||||
<option value={"left"}>{useTr("Left Aligned")}</option>
|
||||
<option value={"right"}>{useTr("Right Aligned")}</option>
|
||||
<option value={"repetitive"}>{useTr("Repetitive")}</option>
|
||||
</Select>
|
||||
<BoxedInputField
|
||||
className={cssStyle.input}
|
||||
disabled={!editable || propertyState !== "normal"}
|
||||
value={propertyValue || ""}
|
||||
value={(propertyValue?.hasParent ? propertyValue?.rawName : propertyValue?.parsedName) || ""}
|
||||
placeholder={propertyState === "normal" ? tr("Channel name") : tr("loading")}
|
||||
onInput={value => setPropertyValue(value, true)}
|
||||
onChange={value => setPropertyValue(value)}
|
||||
onInput={value => setValue(value, true)}
|
||||
onChange={value => setValue(value, false)}
|
||||
isInvalid={!valid}
|
||||
maxLength={propertyValue?.maxNameLength}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface BoxedInputFieldProperties {
|
|||
isInvalid?: boolean;
|
||||
|
||||
className?: string;
|
||||
maxLength?: number,
|
||||
|
||||
size?: "normal" | "large" | "small";
|
||||
type?: "text" | "password" | "number";
|
||||
|
@ -86,6 +87,7 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
|||
disabled={this.state.disabled || this.props.disabled}
|
||||
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
||||
onKeyDown={e => this.onKeyDown(e)}
|
||||
maxLength={this.props.maxLength}
|
||||
/>
|
||||
}
|
||||
{this.props.suffix ? <a key={"suffix"} className={cssStyle.suffix}>{this.props.suffix}</a> : undefined}
|
||||
|
@ -399,6 +401,7 @@ export const ControlledSelect = (props: {
|
|||
|
||||
export interface SelectProperties {
|
||||
type?: "flat" | "boxed";
|
||||
refSelect?: React.RefObject<HTMLSelectElement>,
|
||||
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
|
@ -416,6 +419,8 @@ export interface SelectProperties {
|
|||
disabled?: boolean;
|
||||
editable?: boolean;
|
||||
|
||||
title?: string,
|
||||
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
|
||||
|
@ -430,11 +435,13 @@ export interface SelectFieldState {
|
|||
}
|
||||
|
||||
export class Select extends React.Component<SelectProperties, SelectFieldState> {
|
||||
private refSelect = React.createRef<HTMLSelectElement>();
|
||||
private refSelect;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.refSelect = this.props.refSelect || React.createRef<HTMLSelectElement>();
|
||||
|
||||
this.state = {
|
||||
isInvalid: false,
|
||||
invalidMessage: ""
|
||||
|
@ -453,6 +460,7 @@ export class Select extends React.Component<SelectProperties, SelectFieldState>
|
|||
value={this.props.value}
|
||||
defaultValue={this.props.defaultValue}
|
||||
disabled={disabled}
|
||||
title={this.props.title}
|
||||
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
|
|
Loading…
Reference in New Issue