mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-03-22 08:58:11 +00:00
Merge pull request #4635 from naturalcrit/refactor-tag-system
Refactor-tag-system
This commit is contained in:
@@ -16,6 +16,7 @@ const Combobox = createReactClass({
|
|||||||
suggestMethod : 'includes',
|
suggestMethod : 'includes',
|
||||||
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
||||||
},
|
},
|
||||||
|
valuePatterns: /.+/
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -74,6 +75,7 @@ const Combobox = createReactClass({
|
|||||||
type='text'
|
type='text'
|
||||||
onChange={(e)=>this.handleInput(e)}
|
onChange={(e)=>this.handleInput(e)}
|
||||||
value={this.state.value || ''}
|
value={this.state.value || ''}
|
||||||
|
pattern={this.props.valuePatterns}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
onBlur={(e)=>{
|
onBlur={(e)=>{
|
||||||
if(!e.target.checkValidity()){
|
if(!e.target.checkValidity()){
|
||||||
@@ -82,6 +84,12 @@ const Combobox = createReactClass({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e)=>{
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onEntry(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<i className='fas fa-caret-down'/>
|
<i className='fas fa-caret-down'/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import TagInput from '../tagInput/tagInput.jsx';
|
|||||||
import Themes from 'themes/themes.json';
|
import Themes from 'themes/themes.json';
|
||||||
import validations from './validations.js';
|
import validations from './validations.js';
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
|
||||||
|
|
||||||
import homebreweryThumbnail from '../../thumbnail.png';
|
import homebreweryThumbnail from '../../thumbnail.png';
|
||||||
|
|
||||||
const callIfExists = (val, fn, ...args)=>{
|
const callIfExists = (val, fn, ...args)=>{
|
||||||
@@ -33,7 +31,6 @@ const MetadataEditor = createReactClass({
|
|||||||
tags : [],
|
tags : [],
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : [],
|
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : 'en'
|
lang : 'en'
|
||||||
@@ -91,15 +88,6 @@ const MetadataEditor = createReactClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSystem : function(system, e){
|
|
||||||
if(e.target.checked){
|
|
||||||
this.props.metadata.systems.push(system);
|
|
||||||
} else {
|
|
||||||
this.props.metadata.systems = _.without(this.props.metadata.systems, system);
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.metadata);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleRenderer : function(renderer, e){
|
handleRenderer : function(renderer, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.renderer = renderer;
|
this.props.metadata.renderer = renderer;
|
||||||
@@ -155,18 +143,6 @@ const MetadataEditor = createReactClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSystems : function(){
|
|
||||||
return _.map(SYSTEMS, (val)=>{
|
|
||||||
return <label key={val}>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
checked={_.includes(this.props.metadata.systems, val)}
|
|
||||||
onChange={(e)=>this.handleSystem(val, e)} />
|
|
||||||
{val}
|
|
||||||
</label>;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPublish : function(){
|
renderPublish : function(){
|
||||||
if(this.props.metadata.published){
|
if(this.props.metadata.published){
|
||||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||||
@@ -304,7 +280,7 @@ const MetadataEditor = createReactClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderRenderOptions : function(){
|
renderRenderOptions : function(){
|
||||||
return <div className='field systems'>
|
return <div className='field renderers'>
|
||||||
<label>Renderer</label>
|
<label>Renderer</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<label key='legacy'>
|
<label key='legacy'>
|
||||||
@@ -363,19 +339,15 @@ const MetadataEditor = createReactClass({
|
|||||||
{this.renderThumbnail()}
|
{this.renderThumbnail()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
<TagInput
|
||||||
|
label='tags'
|
||||||
|
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
|
||||||
placeholder='add tag' unique={true}
|
placeholder='add tag' unique={true}
|
||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
|
smallText='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='field systems'>
|
|
||||||
<label>systems</label>
|
|
||||||
<div className='value'>
|
|
||||||
{this.renderSystems()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.renderLanguageDropdown()}
|
{this.renderLanguageDropdown()}
|
||||||
|
|
||||||
{this.renderThemeDropdown()}
|
{this.renderThemeDropdown()}
|
||||||
@@ -386,11 +358,13 @@ const MetadataEditor = createReactClass({
|
|||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderAuthors()}
|
||||||
|
|
||||||
<TagInput label='invited authors' valuePatterns={[/.+/]}
|
<TagInput
|
||||||
|
label='invited authors'
|
||||||
|
valuePatterns={/.+/}
|
||||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||||
placeholder='invite author' unique={true}
|
placeholder='invite author' unique={true}
|
||||||
values={this.props.metadata.invitedAuthors}
|
values={this.props.metadata.invitedAuthors}
|
||||||
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
smallText='Invited author usernames are case sensitive. After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.'
|
||||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,11 @@
|
|||||||
z-index : 200;
|
z-index : 200;
|
||||||
max-width : 150px;
|
max-width : 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.tags .tagInput-dropdown {
|
||||||
|
z-index : 201;
|
||||||
|
max-width : 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -129,7 +134,7 @@
|
|||||||
background-color : #AAAAAA;
|
background-color : #AAAAAA;
|
||||||
}
|
}
|
||||||
|
|
||||||
.systems.field .value {
|
.renderers.field .value {
|
||||||
label {
|
label {
|
||||||
display : inline-flex;
|
display : inline-flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
|
|||||||
210
client/homebrew/editor/tagInput/curatedTagSuggestionList.js
Normal file
210
client/homebrew/editor/tagInput/curatedTagSuggestionList.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
export default [
|
||||||
|
// ############################## Systems
|
||||||
|
// D&D
|
||||||
|
"system:D&D Original",
|
||||||
|
"system:D&D Basic",
|
||||||
|
"system:AD&D 1e",
|
||||||
|
"system:AD&D 2e",
|
||||||
|
"system:D&D 3e",
|
||||||
|
"system:D&D 3.5e",
|
||||||
|
"system:D&D 4e",
|
||||||
|
"system:D&D 5e",
|
||||||
|
"system:D&D 5e 2024",
|
||||||
|
"system:BD&D (B/X)",
|
||||||
|
"system:D&D Essentials",
|
||||||
|
|
||||||
|
// Other Famous RPGs
|
||||||
|
"system:Pathfinder 1e",
|
||||||
|
"system:Pathfinder 2e",
|
||||||
|
"system:Vampire: The Masquerade",
|
||||||
|
"system:Werewolf: The Apocalypse",
|
||||||
|
"system:Mage: The Ascension",
|
||||||
|
"system:Call of Cthulhu",
|
||||||
|
"system:Shadowrun",
|
||||||
|
"system:Star Wars RPG (D6/D20/Edge of the Empire)",
|
||||||
|
"system:Warhammer Fantasy Roleplay",
|
||||||
|
"system:Cyberpunk 2020",
|
||||||
|
"system:Blades in the Dark",
|
||||||
|
"system:Daggerheart",
|
||||||
|
"system:Draw Steel",
|
||||||
|
"system:Mutants and Masterminds",
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
"meta:V3",
|
||||||
|
"meta:Legacy",
|
||||||
|
"meta:Template",
|
||||||
|
"meta:Theme",
|
||||||
|
"meta:free",
|
||||||
|
"meta:Character Sheet",
|
||||||
|
"meta:Documentation",
|
||||||
|
"meta:NPC",
|
||||||
|
"meta:Guide",
|
||||||
|
"meta:Resource",
|
||||||
|
"meta:Notes",
|
||||||
|
"meta:Example",
|
||||||
|
|
||||||
|
// Book type
|
||||||
|
"type:Campaign",
|
||||||
|
"type:Campaign Setting",
|
||||||
|
"type:Adventure",
|
||||||
|
"type:One-Shot",
|
||||||
|
"type:Setting",
|
||||||
|
"type:World",
|
||||||
|
"type:Lore",
|
||||||
|
"type:History",
|
||||||
|
"type:Dungeon Master",
|
||||||
|
"type:Encounter Pack",
|
||||||
|
"type:Encounter",
|
||||||
|
"type:Session Notes",
|
||||||
|
"type:reference",
|
||||||
|
"type:Handbook",
|
||||||
|
"type:Manual",
|
||||||
|
"type:Manuals",
|
||||||
|
"type:Compendium",
|
||||||
|
"type:Bestiary",
|
||||||
|
|
||||||
|
// ###################################### RPG Keywords
|
||||||
|
|
||||||
|
// Classes / Subclasses / Archetypes
|
||||||
|
"Class",
|
||||||
|
"Subclass",
|
||||||
|
"Archetype",
|
||||||
|
"Martial",
|
||||||
|
"Half-Caster",
|
||||||
|
"Full Caster",
|
||||||
|
"Artificer",
|
||||||
|
"Barbarian",
|
||||||
|
"Bard",
|
||||||
|
"Cleric",
|
||||||
|
"Druid",
|
||||||
|
"Fighter",
|
||||||
|
"Monk",
|
||||||
|
"Paladin",
|
||||||
|
"Rogue",
|
||||||
|
"Sorcerer",
|
||||||
|
"Warlock",
|
||||||
|
"Wizard",
|
||||||
|
|
||||||
|
// Races / Species / Lineages
|
||||||
|
"Race",
|
||||||
|
"Ancestry",
|
||||||
|
"Lineage",
|
||||||
|
"Aasimar",
|
||||||
|
"Beastfolk",
|
||||||
|
"Dragonborn",
|
||||||
|
"Dwarf",
|
||||||
|
"Elf",
|
||||||
|
"Goblin",
|
||||||
|
"Half-Elf",
|
||||||
|
"Half-Orc",
|
||||||
|
"Human",
|
||||||
|
"Kobold",
|
||||||
|
"Lizardfolk",
|
||||||
|
"Lycan",
|
||||||
|
"Orc",
|
||||||
|
"Tiefling",
|
||||||
|
"Vampire",
|
||||||
|
"Yuan-Ti",
|
||||||
|
|
||||||
|
// Magic / Spells / Items
|
||||||
|
"Magic",
|
||||||
|
"Magic Item",
|
||||||
|
"Magic Items",
|
||||||
|
"Wondrous Item",
|
||||||
|
"Magic Weapon",
|
||||||
|
"Artifact",
|
||||||
|
"Spell",
|
||||||
|
"Spells",
|
||||||
|
"Cantrip",
|
||||||
|
"Cantrips",
|
||||||
|
"Eldritch",
|
||||||
|
"Eldritch Invocation",
|
||||||
|
"Invocation",
|
||||||
|
"Invocations",
|
||||||
|
"Pact boon",
|
||||||
|
"Pact Boon",
|
||||||
|
"Spellcaster",
|
||||||
|
"Spellblade",
|
||||||
|
"Magical Tattoos",
|
||||||
|
"Enchantment",
|
||||||
|
"Enchanted",
|
||||||
|
"Attunement",
|
||||||
|
"Requires Attunement",
|
||||||
|
"Rune",
|
||||||
|
"Runes",
|
||||||
|
"Wand",
|
||||||
|
"Rod",
|
||||||
|
"Scroll",
|
||||||
|
"Potion",
|
||||||
|
"Potions",
|
||||||
|
"Item",
|
||||||
|
"Items",
|
||||||
|
"Bag of Holding",
|
||||||
|
|
||||||
|
// Monsters / Creatures / Enemies
|
||||||
|
"Monster",
|
||||||
|
"Creatures",
|
||||||
|
"Creature",
|
||||||
|
"Beast",
|
||||||
|
"Beasts",
|
||||||
|
"Humanoid",
|
||||||
|
"Undead",
|
||||||
|
"Fiend",
|
||||||
|
"Aberration",
|
||||||
|
"Ooze",
|
||||||
|
"Giant",
|
||||||
|
"Dragon",
|
||||||
|
"Monstrosity",
|
||||||
|
"Demon",
|
||||||
|
"Devil",
|
||||||
|
"Elemental",
|
||||||
|
"Construct",
|
||||||
|
"Constructs",
|
||||||
|
"Boss",
|
||||||
|
"BBEG",
|
||||||
|
|
||||||
|
// ############################# Media / Pop Culture
|
||||||
|
"One Piece",
|
||||||
|
"Dragon Ball",
|
||||||
|
"Dragon Ball Z",
|
||||||
|
"Naruto",
|
||||||
|
"Jujutsu Kaisen",
|
||||||
|
"Fairy Tail",
|
||||||
|
"Final Fantasy",
|
||||||
|
"Kingdom Hearts",
|
||||||
|
"Elder Scrolls",
|
||||||
|
"Skyrim",
|
||||||
|
"WoW",
|
||||||
|
"World of Warcraft",
|
||||||
|
"Marvel Comics",
|
||||||
|
"DC Comics",
|
||||||
|
"Pokemon",
|
||||||
|
"League of Legends",
|
||||||
|
"Runeterra",
|
||||||
|
"Arcane",
|
||||||
|
"Yu-Gi-Oh",
|
||||||
|
"Minecraft",
|
||||||
|
"Don't Starve",
|
||||||
|
"Witcher",
|
||||||
|
"Witcher 3",
|
||||||
|
"Cyberpunk",
|
||||||
|
"Cyberpunk 2077",
|
||||||
|
"Fallout",
|
||||||
|
"Divinity Original Sin 2",
|
||||||
|
"Fullmetal Alchemist",
|
||||||
|
"Fullmetal Alchemist Brotherhood",
|
||||||
|
"Lobotomy Corporation",
|
||||||
|
"Bloodborne",
|
||||||
|
"Dragonlance",
|
||||||
|
"Shackled City Adventure Path",
|
||||||
|
"Baldurs Gate 3",
|
||||||
|
"Library of Ruina",
|
||||||
|
"Radiant Citadel",
|
||||||
|
"Ravenloft",
|
||||||
|
"Forgotten Realms",
|
||||||
|
"Exandria",
|
||||||
|
"Critical Role",
|
||||||
|
"Star Wars",
|
||||||
|
"SW5e",
|
||||||
|
"Star Wars 5e",
|
||||||
|
];
|
||||||
@@ -1,101 +1,210 @@
|
|||||||
import './tagInput.less';
|
import './tagInput.less';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import _ from 'lodash';
|
import Combobox from '../../../components/combobox.jsx';
|
||||||
|
|
||||||
const TagInput = ({ unique = true, values = [], ...props })=>{
|
import tagSuggestionList from './curatedTagSuggestionList.js';
|
||||||
const [tempInputText, setTempInputText] = useState('');
|
|
||||||
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
|
const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
|
||||||
|
const [tagList, setTagList] = useState(
|
||||||
|
values.map((value)=>({
|
||||||
|
value,
|
||||||
|
editing : false,
|
||||||
|
draft : '',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
handleChange(tagList.map((context)=>context.value));
|
const incoming = values || [];
|
||||||
|
const current = tagList.map((t)=>t.value);
|
||||||
|
|
||||||
|
const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]);
|
||||||
|
|
||||||
|
if(changed) {
|
||||||
|
setTagList(
|
||||||
|
incoming.map((value)=>({
|
||||||
|
value,
|
||||||
|
editing : false,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [values]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
onChange?.({
|
||||||
|
target : { value: tagList.map((t)=>t.value) },
|
||||||
|
});
|
||||||
}, [tagList]);
|
}, [tagList]);
|
||||||
|
|
||||||
const handleChange = (value)=>{
|
// substrings to be normalized to the first value on the array
|
||||||
props.onChange({
|
const duplicateGroups = [
|
||||||
target : { value }
|
['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 handleInputKeyDown = ({ evt, value, index, options = {} })=>{
|
const normalizeValue = (input)=>{
|
||||||
if(_.includes(['Enter', ','], evt.key)) {
|
const lowerInput = input.toLowerCase();
|
||||||
evt.preventDefault();
|
let normalizedTag = input;
|
||||||
submitTag(evt.target.value, value, index);
|
|
||||||
if(options.clear) {
|
for (const group of duplicateGroups) {
|
||||||
setTempInputText('');
|
for (const tag of group) {
|
||||||
|
if(!tag) continue;
|
||||||
|
|
||||||
|
const index = lowerInput.indexOf(tag.toLowerCase());
|
||||||
|
if(index !== -1) {
|
||||||
|
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, originalValue, index)=>{
|
const submitTag = (newValue, index = null)=>{
|
||||||
setTagList((prevContext)=>{
|
const trimmed = newValue?.trim();
|
||||||
// remove existing tag
|
if(!trimmed) return;
|
||||||
if(newValue === null){
|
if(!valuePatterns.test(trimmed)) return;
|
||||||
return [...prevContext].filter((context, i)=>i !== index);
|
|
||||||
|
const normalizedTag = normalizeValue(trimmed);
|
||||||
|
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
// add new tag
|
|
||||||
if(originalValue === null){
|
return [...prev, { value: normalizedTag, editing: false }];
|
||||||
return [...prevContext, { value: newValue, editing: false }];
|
|
||||||
}
|
|
||||||
// update existing tag
|
|
||||||
return prevContext.map((context, i)=>{
|
|
||||||
if(i === index) {
|
|
||||||
return { ...context, value: newValue, editing: false };
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeTag = (index)=>{
|
||||||
|
setTagList((prev)=>prev.filter((_, i)=>i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
const editTag = (index)=>{
|
const editTag = (index)=>{
|
||||||
setTagList((prevContext)=>{
|
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t)));
|
||||||
return prevContext.map((context, i)=>{
|
|
||||||
if(i === index) {
|
|
||||||
return { ...context, editing: true };
|
|
||||||
}
|
|
||||||
return { ...context, editing: false };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderReadTag = (context, index)=>{
|
const stopEditing = (index)=>{
|
||||||
return (
|
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t)));
|
||||||
<li key={index}
|
|
||||||
data-value={context.value}
|
|
||||||
className='tag'
|
|
||||||
onClick={()=>editTag(index)}>
|
|
||||||
{context.value}
|
|
||||||
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderWriteTag = (context, index)=>{
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input type='text'
|
<div className={classes} key={`tag-${tag}`} value={tag} data={tag} title={tag}>
|
||||||
key={index}
|
{tag}
|
||||||
defaultValue={context.value}
|
</div>
|
||||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='field'>
|
<div className='field tags'>
|
||||||
<label>{props.label}</label>
|
{label && <label>{label}</label>}
|
||||||
|
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<ul className='list'>
|
<ul className='list'>
|
||||||
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
{tagList.map((t, i)=>t.editing ? (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
type='text'
|
||||||
|
value={t.draft} // always use draft
|
||||||
|
pattern={valuePatterns.source}
|
||||||
|
onChange={(e)=>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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if(e.key === 'Escape') {
|
||||||
|
stopEditing(i);
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<li key={i} className='tag' onClick={()=>editTag(i)}>
|
||||||
|
{t.value}
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={(e)=>{
|
||||||
|
e.stopPropagation();
|
||||||
|
removeTag(i);
|
||||||
|
}}>
|
||||||
|
<i className='fa fa-times fa-fw' />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<input
|
<Combobox
|
||||||
type='text'
|
trigger='click'
|
||||||
className='value'
|
className='tagInput-dropdown'
|
||||||
placeholder={props.placeholder}
|
default=''
|
||||||
value={tempInputText}
|
placeholder={placeholder}
|
||||||
onChange={(e)=>setTempInputText(e.target.value)}
|
options={label === 'tags' ? suggestionOptions : []}
|
||||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
autoSuggest={
|
||||||
|
label === 'tags'
|
||||||
|
? {
|
||||||
|
suggestMethod : 'startsWith',
|
||||||
|
clearAutoSuggestOnClick : true,
|
||||||
|
filterOn : ['value', 'title'],
|
||||||
|
}
|
||||||
|
: { suggestMethod: 'includes', clearAutoSuggestOnClick: true, filterOn: [] }
|
||||||
|
}
|
||||||
|
valuePatterns={valuePatterns.source}
|
||||||
|
onSelect={(value)=>submitTag(value)}
|
||||||
|
onEntry={(e)=>{
|
||||||
|
if(e.key === 'Enter') {
|
||||||
|
console.log('submit');
|
||||||
|
e.preventDefault();
|
||||||
|
submitTag(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{smallText.length !== 0 && <small>{smallText}</small>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.list input {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagInput-dropdown {
|
||||||
|
.dropdown-options {
|
||||||
|
.item {
|
||||||
|
&.type {
|
||||||
|
background-color: #00800035;
|
||||||
|
}
|
||||||
|
&.group {
|
||||||
|
background-color: #50505035;
|
||||||
|
}
|
||||||
|
&.meta {
|
||||||
|
background-color: #00008035;
|
||||||
|
}
|
||||||
|
&.system {
|
||||||
|
background-color: #80000035;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1980
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
1980
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
File diff suppressed because it is too large
Load Diff
66
package-lock.json
generated
66
package-lock.json
generated
@@ -73,7 +73,7 @@
|
|||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.0.0",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.25.0",
|
"stylelint": "^16.25.0",
|
||||||
@@ -9953,27 +9953,6 @@
|
|||||||
"integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==",
|
"integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/livereload/node_modules/ws": {
|
|
||||||
"version": "7.5.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
|
||||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.3.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": "^5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
@@ -10601,6 +10580,22 @@
|
|||||||
"url": "https://opencollective.com/mongoose"
|
"url": "https://opencollective.com/mongoose"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mongoose/node_modules/gcp-metadata": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"gaxios": "^7.0.0",
|
||||||
|
"google-logging-utils": "^1.0.0",
|
||||||
|
"json-bigint": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mongoose/node_modules/mongodb": {
|
"node_modules/mongoose/node_modules/mongodb": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz",
|
||||||
@@ -15365,9 +15360,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "16.0.1",
|
"version": "16.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz",
|
||||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
"integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -15606,6 +15601,27 @@
|
|||||||
"integrity": "sha512-LhQ68uUnzHH0bwm/QiGA9JwqgadSDOwqB2AIs/LBsrOY6ScqVXKRN2slTCeKAhstDBJ/Of/Yxcjn0pnQmVlmtg==",
|
"integrity": "sha512-LhQ68uUnzHH0bwm/QiGA9JwqgadSDOwqB2AIs/LBsrOY6ScqVXKRN2slTCeKAhstDBJ/Of/Yxcjn0pnQmVlmtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "7.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user