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: {}
|
notify_description_changed: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ParsedChannelName {
|
export type ChannelNameAlignment = "center" | "right" | "left" | "normal" | "repetitive";
|
||||||
|
export class ChannelNameParser {
|
||||||
readonly originalName: string;
|
readonly originalName: string;
|
||||||
alignment: "center" | "right" | "left" | "normal" | "repetitive";
|
alignment: ChannelNameAlignment;
|
||||||
text: string; /* does not contain any alignment codes */
|
text: string; /* does not contain any alignment codes */
|
||||||
|
uniqueId: string;
|
||||||
|
|
||||||
constructor(name: string, hasParentChannel: boolean) {
|
constructor(name: string, hasParentChannel: boolean) {
|
||||||
this.originalName = name;
|
this.originalName = name;
|
||||||
this.parse(hasParentChannel);
|
this.parse(hasParentChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parse(has_parent_channel: boolean) {
|
private parse(hasParentChannel: boolean) {
|
||||||
this.alignment = "normal";
|
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(']');
|
let end = this.originalName.indexOf(']');
|
||||||
if(end === -1) break parse_type;
|
if(end === -1) {
|
||||||
|
break parseType;
|
||||||
|
}
|
||||||
|
|
||||||
let options = this.originalName.substr(1, end - 1);
|
let options = this.originalName.substr(1, end - 1);
|
||||||
if(options.indexOf("spacer") === -1) break parse_type;
|
const spacerIndex = options.indexOf("spacer");
|
||||||
options = options.substr(0, 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";
|
options = "l";
|
||||||
else if(options.length > 1)
|
} else if(options.length > 1) {
|
||||||
options = options[0];
|
options = options[0];
|
||||||
|
}
|
||||||
|
|
||||||
switch (options) {
|
switch (options) {
|
||||||
case "r":
|
case "r":
|
||||||
this.alignment = "right";
|
this.alignment = "right";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "l":
|
case "l":
|
||||||
this.alignment = "left";
|
this.alignment = "left";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "c":
|
case "c":
|
||||||
this.alignment = "center";
|
this.alignment = "center";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "*":
|
case "*":
|
||||||
this.alignment = "repetitive";
|
this.alignment = "repetitive";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break parse_type;
|
break parseType;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.text = this.originalName.substr(end + 1);
|
this.text = this.originalName.substr(end + 1);
|
||||||
}
|
}
|
||||||
if(!this.text && this.alignment === "normal")
|
|
||||||
|
if(!this.text && this.alignment === "normal") {
|
||||||
this.text = this.originalName;
|
this.text = this.originalName;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
|
@ -177,7 +195,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
|
|
||||||
readonly events: Registry<ChannelEvents>;
|
readonly events: Registry<ChannelEvents>;
|
||||||
|
|
||||||
parsed_channel_name: ParsedChannelName;
|
parsed_channel_name: ChannelNameParser;
|
||||||
|
|
||||||
private _family_index: number = 0;
|
private _family_index: number = 0;
|
||||||
|
|
||||||
|
@ -206,7 +224,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
this.properties = new ChannelProperties();
|
this.properties = new ChannelProperties();
|
||||||
this.channelId = channelId;
|
this.channelId = channelId;
|
||||||
this.properties.channel_name = channelName;
|
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"]) => {
|
this.clientPropertyChangedListener = (event: ClientEvents["notify_properties_updated"]) => {
|
||||||
if("client_nickname" in event.updated_properties || "client_talk_power" in event.updated_properties) {
|
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(hasUpdate) {
|
||||||
if(key == "channel_name") {
|
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") {
|
} else if(key == "channel_order") {
|
||||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||||
this.channelTree.moveChannel(this, order, this.parent, false);
|
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 {ChannelEditableProperty} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||||
import {ChannelTree} from "tc-shared/tree/ChannelTree";
|
import {ChannelTree} from "tc-shared/tree/ChannelTree";
|
||||||
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
|
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["phoneticName"] = SimplePropertyProvider("channel_name_phonetic", "");
|
||||||
ChannelPropertyProviders["icon"] = {
|
ChannelPropertyProviders["icon"] = {
|
||||||
provider: async (properties, _channel, _parentChannel, channelTree) => {
|
provider: async (properties, _channel, _parentChannel, channelTree) => {
|
||||||
|
|
|
@ -32,7 +32,14 @@ export type ChannelEditPermissionsState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ChannelEditableProperty {
|
export interface ChannelEditableProperty {
|
||||||
"name": string,
|
"name": {
|
||||||
|
rawName: string,
|
||||||
|
parsedName?: string,
|
||||||
|
parsedAlignment?: "center" | "right" | "left" | "normal" | "repetitive",
|
||||||
|
maxNameLength?: number,
|
||||||
|
hasParent?: boolean,
|
||||||
|
spacerUniqueId?: string
|
||||||
|
},
|
||||||
"phoneticName": string,
|
"phoneticName": string,
|
||||||
|
|
||||||
"icon": {
|
"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 {
|
.buttons {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useContext, useEffect, useRef, useState} from "react";
|
import {useContext, useEffect, useRef, useState} from "react";
|
||||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
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 {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
|
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
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 {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||||
import {RadioButton} from "tc-shared/ui/react-elements/RadioButton";
|
import {RadioButton} from "tc-shared/ui/react-elements/RadioButton";
|
||||||
import {Slider} from "tc-shared/ui/react-elements/Slider";
|
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 {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
import {getIconManager} from "tc-shared/file/Icons";
|
import {getIconManager} from "tc-shared/file/Icons";
|
||||||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
import {ChannelNameAlignment, ChannelNameParser} from "tc-shared/tree/Channel";
|
||||||
|
|
||||||
const cssStyle = require("./Renderer.scss");
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
@ -122,6 +122,10 @@ function useValidationState<T extends keyof ChannelEditableProperty>(property: T
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ChannelNameType = (props: { selected: ChannelNameAlignment }) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const ChannelName = React.memo(() => {
|
const ChannelName = React.memo(() => {
|
||||||
const modalType = useContext(ModalTypeContext);
|
const modalType = useContext(ModalTypeContext);
|
||||||
|
|
||||||
|
@ -129,16 +133,72 @@ const ChannelName = React.memo(() => {
|
||||||
const editable = usePropertyPermission("name", modalType === "channel-create");
|
const editable = usePropertyPermission("name", modalType === "channel-create");
|
||||||
const valid = useValidationState("name");
|
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 (
|
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
|
<BoxedInputField
|
||||||
className={cssStyle.input}
|
className={cssStyle.input}
|
||||||
disabled={!editable || propertyState !== "normal"}
|
disabled={!editable || propertyState !== "normal"}
|
||||||
value={propertyValue || ""}
|
value={(propertyValue?.hasParent ? propertyValue?.rawName : propertyValue?.parsedName) || ""}
|
||||||
placeholder={propertyState === "normal" ? tr("Channel name") : tr("loading")}
|
placeholder={propertyState === "normal" ? tr("Channel name") : tr("loading")}
|
||||||
onInput={value => setPropertyValue(value, true)}
|
onInput={value => setValue(value, true)}
|
||||||
onChange={value => setPropertyValue(value)}
|
onChange={value => setValue(value, false)}
|
||||||
isInvalid={!valid}
|
isInvalid={!valid}
|
||||||
|
maxLength={propertyValue?.maxNameLength}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface BoxedInputFieldProperties {
|
||||||
isInvalid?: boolean;
|
isInvalid?: boolean;
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
|
maxLength?: number,
|
||||||
|
|
||||||
size?: "normal" | "large" | "small";
|
size?: "normal" | "large" | "small";
|
||||||
type?: "text" | "password" | "number";
|
type?: "text" | "password" | "number";
|
||||||
|
@ -86,6 +87,7 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
||||||
disabled={this.state.disabled || this.props.disabled}
|
disabled={this.state.disabled || this.props.disabled}
|
||||||
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
|
||||||
onKeyDown={e => this.onKeyDown(e)}
|
onKeyDown={e => this.onKeyDown(e)}
|
||||||
|
maxLength={this.props.maxLength}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{this.props.suffix ? <a key={"suffix"} className={cssStyle.suffix}>{this.props.suffix}</a> : undefined}
|
{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 {
|
export interface SelectProperties {
|
||||||
type?: "flat" | "boxed";
|
type?: "flat" | "boxed";
|
||||||
|
refSelect?: React.RefObject<HTMLSelectElement>,
|
||||||
|
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
@ -416,6 +419,8 @@ export interface SelectProperties {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
|
||||||
|
title?: string,
|
||||||
|
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
|
|
||||||
|
@ -430,11 +435,13 @@ export interface SelectFieldState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Select extends React.Component<SelectProperties, SelectFieldState> {
|
export class Select extends React.Component<SelectProperties, SelectFieldState> {
|
||||||
private refSelect = React.createRef<HTMLSelectElement>();
|
private refSelect;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.refSelect = this.props.refSelect || React.createRef<HTMLSelectElement>();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
invalidMessage: ""
|
invalidMessage: ""
|
||||||
|
@ -453,6 +460,7 @@ export class Select extends React.Component<SelectProperties, SelectFieldState>
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
defaultValue={this.props.defaultValue}
|
defaultValue={this.props.defaultValue}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
title={this.props.title}
|
||||||
|
|
||||||
onFocus={this.props.onFocus}
|
onFocus={this.props.onFocus}
|
||||||
onBlur={this.props.onBlur}
|
onBlur={this.props.onBlur}
|
||||||
|
|
Loading…
Reference in New Issue