From 65d7051819e8112194240f958458852716ce8e6f Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Fri, 22 Jan 2021 17:38:23 +0100 Subject: [PATCH] Allowing the user to easily edit the channel name mode --- shared/js/tree/Channel.ts | 48 +++++++---- .../channel-edit/ControllerProperties.ts | 38 ++++++++- .../js/ui/modal/channel-edit/Definitions.ts | 9 +- shared/js/ui/modal/channel-edit/Renderer.scss | 20 +++++ shared/js/ui/modal/channel-edit/Renderer.tsx | 82 ++++++++++++++++--- shared/js/ui/react-elements/InputField.tsx | 10 ++- 6 files changed, 177 insertions(+), 30 deletions(-) diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index 115eb30d..bfd39a13 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -114,54 +114,72 @@ 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; + } } } @@ -177,7 +195,7 @@ export class ChannelEntry extends ChannelTreeEntry { readonly events: Registry; - parsed_channel_name: ParsedChannelName; + parsed_channel_name: ChannelNameParser; private _family_index: number = 0; @@ -206,7 +224,7 @@ export class ChannelEntry extends ChannelTreeEntry { 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 { 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); diff --git a/shared/js/ui/modal/channel-edit/ControllerProperties.ts b/shared/js/ui/modal/channel-edit/ControllerProperties.ts index 722904fe..a57ccf40 100644 --- a/shared/js/ui/modal/channel-edit/ControllerProperties.ts +++ b/shared/js/ui/modal/channel-edit/ControllerProperties.ts @@ -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 =

(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) => { diff --git a/shared/js/ui/modal/channel-edit/Definitions.ts b/shared/js/ui/modal/channel-edit/Definitions.ts index 6ae893f7..c0f12afa 100644 --- a/shared/js/ui/modal/channel-edit/Definitions.ts +++ b/shared/js/ui/modal/channel-edit/Definitions.ts @@ -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": { diff --git a/shared/js/ui/modal/channel-edit/Renderer.scss b/shared/js/ui/modal/channel-edit/Renderer.scss index bf348106..f96f7418 100644 --- a/shared/js/ui/modal/channel-edit/Renderer.scss +++ b/shared/js/ui/modal/channel-edit/Renderer.scss @@ -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; diff --git a/shared/js/ui/modal/channel-edit/Renderer.tsx b/shared/js/ui/modal/channel-edit/Renderer.tsx index 61836f73..716bd0a6 100644 --- a/shared/js/ui/modal/channel-edit/Renderer.tsx +++ b/shared/js/ui/modal/channel-edit/Renderer.tsx @@ -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(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(); + + 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 ( - setPropertyValue(value, true)} - onChange={value => setPropertyValue(value)} - isInvalid={!valid} - /> +

+ + setValue(value, true)} + onChange={value => setValue(value, false)} + isInvalid={!valid} + maxLength={propertyValue?.maxNameLength} + /> +
); }); diff --git a/shared/js/ui/react-elements/InputField.tsx b/shared/js/ui/react-elements/InputField.tsx index 4497b117..ae3c9a1e 100644 --- a/shared/js/ui/react-elements/InputField.tsx +++ b/shared/js/ui/react-elements/InputField.tsx @@ -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 this.props.onInput(event.currentTarget.value))} onKeyDown={e => this.onKeyDown(e)} + maxLength={this.props.maxLength} /> } {this.props.suffix ? {this.props.suffix} : undefined} @@ -399,6 +401,7 @@ export const ControlledSelect = (props: { export interface SelectProperties { type?: "flat" | "boxed"; + refSelect?: React.RefObject, 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 { - private refSelect = React.createRef(); + private refSelect; constructor(props) { super(props); + this.refSelect = this.props.refSelect || React.createRef(); + this.state = { isInvalid: false, invalidMessage: "" @@ -453,6 +460,7 @@ export class Select extends React.Component value={this.props.value} defaultValue={this.props.defaultValue} disabled={disabled} + title={this.props.title} onFocus={this.props.onFocus} onBlur={this.props.onBlur}