0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-22 06:48:11 +00:00

proper tooltips

This commit is contained in:
Víctor Losada Hernández
2026-02-19 14:04:54 +01:00
parent 904548521e
commit ba4b30228a
6 changed files with 160 additions and 129 deletions

View File

@@ -11,6 +11,7 @@ const Combobox = createReactClass({
trigger : 'hover',
default : '',
placeholder : '',
tooltip: '',
autoSuggest : {
clearAutoSuggestOnClick : true,
suggestMethod : 'includes',
@@ -70,7 +71,8 @@ const Combobox = createReactClass({
return (
<div className='dropdown-input item'
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
{...(this.props.tooltip ? { 'data-tooltip-bottom': this.props.tooltip } : {})}>
<input
type='text'
onChange={(e)=>this.handleInput(e)}

View File

@@ -256,7 +256,7 @@ const MetadataEditor = createReactClass({
return <div className='field language'>
<label>language</label>
<div className='value'>
<div className='value' data-tooltip-right='Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.'>
<Combobox trigger='click'
className='language-dropdown'
default={this.props.metadata.lang || ''}
@@ -273,7 +273,6 @@ const MetadataEditor = createReactClass({
filterOn : ['value', 'detail', 'title']
}}
/>
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
</div>
</div>;
@@ -339,14 +338,20 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()}
</div>
<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}
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)}
/>
<div className="field tags">
<label>Tags</label>
<div className="value" >
<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}
values={this.props.metadata.tags}
onChange={(e)=>this.handleFieldChange('tags', e)}
tooltip='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
/>
</div>
</div>
{this.renderLanguageDropdown()}
@@ -358,15 +363,22 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()}
<TagInput
label='invited authors'
valuePatterns={/.+/}
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true}
values={this.props.metadata.invitedAuthors}
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)}
/>
<div className="field tags">
<label>Invited authors</label>
<div className="value">
<TagInput
label='invited authors'
valuePatterns={/.+/}
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true}
tooltip={`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.`}
values={this.props.metadata.invitedAuthors}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
/>
</div>
</div>
<h2>Privacy</h2>

View File

@@ -44,8 +44,6 @@
gap : 10px;
}
.field {
position : relative;
display : flex;
@@ -62,6 +60,9 @@
& > .value {
flex : 1 1 auto;
width : 50px;
&[data-tooltip-right] {
max-width:380px;
}
&:invalid { background : #FFB9B9; }
small {
display : block;
@@ -110,7 +111,7 @@
}
}
&.language .language-dropdown {
&.language .value {
z-index : 200;
max-width : 150px;
}

View File

@@ -1,71 +1,71 @@
import './tagInput.less';
import React, { useState, useEffect } 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 = ({tooltip, label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => {
const [tagList, setTagList] = useState(
values.map((value)=>({
values.map((value) => ({
value,
editing : false,
draft : '',
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,
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'],
["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) {
if (index !== -1) {
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
break;
}
}
}
if(normalizedTag.includes(':')) {
const [rawType, rawValue = ''] = normalizedTag.split(':');
if (normalizedTag.includes(":")) {
const [rawType, rawValue = ""] = normalizedTag.split(":");
const tagType = rawType.trim().toLowerCase();
const tagValue = rawValue.trim();
if(tagValue.length > 0) {
if (tagValue.length > 0) {
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
}
//trims spaces around colon and capitalizes the first word after the colon
@@ -75,56 +75,56 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
return normalizedTag;
};
const submitTag = (newValue, index = null)=>{
const submitTag = (newValue, index = null) => {
const trimmed = newValue?.trim();
if(!trimmed) return;
if(!valuePatterns.test(trimmed)) return;
if (!trimmed) return;
if (!valuePatterns.test(trimmed)) return;
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));
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: 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)=>(i === index ? { ...t, editing: true, draft: t.value } : t)));
const editTag = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: true, draft: t.value } : t)));
};
const stopEditing = (index)=>{
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t)));
const stopEditing = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: false, draft: "" } : t)));
};
const suggestionOptions = tagSuggestionList.map((tag)=>{
const tagType = tag.split(':');
const suggestionOptions = tagSuggestionList.map((tag) => {
const tagType = tag.split(":");
let classes = 'item';
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 (
@@ -135,26 +135,54 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
});
return (
<div className='field tags'>
{label && <label>{label}</label>}
<div className='value'>
<ul className='list'>
{tagList.map((t, i)=>t.editing ? (
<div className="tagInputWrap">
<Combobox
trigger="click"
className="tagInput-dropdown"
default=""
placeholder={placeholder}
options={label === "tags" ? suggestionOptions : []}
tooltip={tooltip}
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") {
e.preventDefault();
submitTag(e.target.value);
}
}}
/>
<ul className="list">
{tagList.map((t, i) =>
t.editing ? (
<input
key={i}
type='text'
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') {
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)),
setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, draft: "" } : tag)),
);
}
if(e.key === 'Escape') {
if (e.key === "Escape") {
stopEditing(i);
e.target.blur();
}
@@ -162,47 +190,20 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
autoFocus
/>
) : (
<li key={i} className='tag' onClick={()=>editTag(i)}>
<li key={i} className="tag" onClick={() => editTag(i)}>
{t.value}
<button
type='button'
onClick={(e)=>{
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(i);
}}>
<i className='fa fa-times fa-fw' />
<i className="fa fa-times fa-fw" />
</button>
</li>
),
)}
</ul>
<Combobox
trigger='click'
className='tagInput-dropdown'
default=''
placeholder={placeholder}
options={label === 'tags' ? suggestionOptions : []}
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') {
e.preventDefault();
submitTag(e.target.value);
}
}}
/>
{smallText.length !== 0 && <small>{smallText}</small>}
</div>
)}
</ul>
</div>
);
};

View File

@@ -1,5 +1,11 @@
.tags {
.tagInputWrap {
display:grid;
grid-template-columns: 200px 3fr;
gap:10px;
}
.list input {
border-radius: 5px;
}