diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 2a0df4e76..9082633e5 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -339,9 +339,9 @@ const MetadataEditor = createReactClass({ {this.renderThumbnail()} - !this.props.metadata.authors?.includes(v)]} placeholder='invite author' unique={true} diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index f272a605c..9f7f71498 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,126 +1,130 @@ -import "./tagInput.less"; -import React, { useState, useEffect, useMemo } from "react"; -import Combobox from "../../../components/combobox.jsx"; +import './tagInput.less'; +import React, { useState, useEffect } from 'react'; +import Combobox from '../../../components/combobox.jsx'; -import tagSuggestionList from "./curatedTagSuggestionList.js"; +import tagSuggestionList from './curatedTagSuggestionList.js'; -const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => { +const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{ const [tagList, setTagList] = useState( - values.map((value) => ({ + values.map((value)=>({ value, - display: value.trim(), - editing: false, + editing : false, + draft : '', })), ); - useEffect(() => { + useEffect(()=>{ const incoming = values || []; - const current = tagList.map((t) => t.value); + const current = tagList.map((t)=>t.value); - const changed = incoming.length !== current.length || incoming.some((v, i) => v !== current[i]); + const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]); - if (changed) { + if(changed) { setTagList( - incoming.map((value) => ({ + incoming.map((value)=>({ value, - display: value.trim(), - editing: false, + editing : false, })), ); } }, [values]); - useEffect(() => { + useEffect(()=>{ onChange?.({ - target: { value: tagList.map((t) => t.value) }, + target : { value: tagList.map((t)=>t.value) }, }); }, [tagList]); + // substrings to be normalized to the first value on the array const duplicateGroups = [ - ["5e 2024", "5.5e", "5e'24", "5.24", "5e24", "5.5"], - ["5e", "5th Edition"], - ["Dungeons & Dragons", "Dungeons and Dragons", "Dungeons n dragons"], - ["D&D", "DnD", "dnd", "Dnd", "dnD", "d&d", "d&D", "D&d"], - ["P2e", "p2e", "P2E", "Pathfinder 2e"], - ["meta:", "Meta:", "META:"], - ["group:", "Group:", "GROUP:"], - ["type:", "Type:", "TYPE:"], - ["system:", "System:", "SYSTEM:"], + ['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'], + ['5e', '5th Edition'], + ['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'], + ['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'], + ['P2e', 'p2e', 'P2E', 'Pathfinder 2e'], ]; - const normalizeValue = (input) => { + const normalizeValue = (input)=>{ const lowerInput = input.toLowerCase(); + let normalizedTag = input; for (const group of duplicateGroups) { for (const tag of group) { - if (!tag) continue; + if(!tag) continue; const index = lowerInput.indexOf(tag.toLowerCase()); - if (index !== -1) { - return input.slice(0, index) + group[0] + input.slice(index + tag.length); + if(index !== -1) { + normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length); + break; } } } - return input; + if(normalizedTag.includes(':')) { + const [rawType, rawValue = ''] = normalizedTag.split(':'); + const tagType = rawType.trim().toLowerCase(); + const tagValue = rawValue.trim(); + + if(tagValue.length > 0) { + normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`; + } + //trims spaces around colon and capitalizes the first word after the colon + //this is preferred to users not understanding they can't put spaces in + } + + return normalizedTag; }; - const submitTag = (newValue, index = null) => { + const submitTag = (newValue, index = null)=>{ const trimmed = newValue?.trim(); - console.log(newValue, trimmed); - if (!trimmed) return; - console.log(valuePatterns.test(trimmed)); - if (!valuePatterns.test(trimmed)) return; + if(!trimmed) return; + if(!valuePatterns.test(trimmed)) return; - const canonical = normalizeValue(trimmed); + const normalizedTag = normalizeValue(trimmed); - setTagList((prev) => { - const existsIndex = prev.findIndex((t) => t.value.toLowerCase() === canonical.toLowerCase()); - - if (unique && existsIndex !== -1) return prev; - - if (index !== null) { - return prev.map((t, i) => - i === index ? { ...t, value: canonical, display: canonical, editing: false } : t, - ); + setTagList((prev)=>{ + const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase()); + if(unique && existsIndex !== -1) return prev; + if(index !== null) { + return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t)); } - return [...prev, { value: canonical, display: canonical, editing: false }]; + return [...prev, { value: normalizedTag, editing: false }]; }); }; - const removeTag = (index) => { - setTagList((prev) => prev.filter((_, i) => i !== index)); + const removeTag = (index)=>{ + setTagList((prev)=>prev.filter((_, i)=>i !== index)); }; - const editTag = (index) => { - setTagList((prev) => prev.map((t, i) => ({ ...t, editing: i === index }))); + const editTag = (index)=>{ + setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t))); }; - const suggestionOptions = tagSuggestionList.map((tag) => { - const tagType = tag.split(":"); + const stopEditing = (index)=>{ + setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t))); + }; - let classes = "item"; + const suggestionOptions = tagSuggestionList.map((tag)=>{ + const tagType = tag.split(':'); + + let classes = 'item'; switch (tagType[0]) { - case "type": - classes = "item type"; - break; - - case "group": - classes = "item group"; - break; - - case "meta": - classes = "item meta"; - break; - - case "system": - classes = "item system"; - break; - - default: - classes = "item"; - break; + case 'type': + classes = 'item type'; + break; + case 'group': + classes = 'item group'; + break; + case 'meta': + classes = 'item meta'; + break; + case 'system': + classes = 'item system'; + break; + default: + classes = 'item'; + break; } return ( @@ -131,68 +135,70 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde }); return ( -
+
{label && } -
-
    - {tagList.map((t, i) => - t.editing ? ( - { - const val = e.target.value; - setTagList((prev) => - prev.map((tag, idx) => (idx === i ? { ...tag, display: val } : tag)), +
    +
      + {tagList.map((t, i)=>t.editing ? ( + setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)), + ) + } + onKeyDown={(e)=>{ + if(e.key === 'Enter') { + e.preventDefault(); + submitTag(t.draft, i); // submit draft + setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)), ); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - submitTag(e.target.value, i); - } - }} - autoFocus - /> - ) : ( -
    • editTag(i)}> - {t.display} - -
    • - ), + } + if(e.key === 'Escape') { + stopEditing(i); + e.target.blur(); + } + }} + autoFocus + /> + ) : ( +
    • editTag(i)}> + {t.value} + +
    • + ), )}
    submitTag(value)} - onEntry={(e) => { - if (e.key === "Enter") { - console.log("submit"); + onSelect={(value)=>submitTag(value)} + onEntry={(e)=>{ + if(e.key === 'Enter') { + console.log('submit'); e.preventDefault(); submitTag(e.target.value); } diff --git a/client/homebrew/editor/tagInput/tagInput.less b/client/homebrew/editor/tagInput/tagInput.less index 392bd3edd..3165b3935 100644 --- a/client/homebrew/editor/tagInput/tagInput.less +++ b/client/homebrew/editor/tagInput/tagInput.less @@ -1,16 +1,20 @@ +.list input { + border-radius: 5px; +} + .tagInput-dropdown { .dropdown-options { .item { &.type { background-color: #00800035; } - &.group { + &.group { background-color: #50505035; } - &.meta { + &.meta { background-color: #00008035; } - &.system { + &.system { background-color: #80000035; } }