From e8e16f4d66c1daab22d110cc264a2f99c55963ce Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Tue, 17 Sep 2024 14:46:56 -0500 Subject: [PATCH 01/16] Initial commit: Rename component, set basic structure No actual functionality implemented yet, just renames the component from "StringArrayEditor" to "TagInput", for brevity at the possible cost of clarity. For now, the original StringArrayEditor is kept and named "TagInput-class.jsx" so that I can reference it as I work on the functional component. --- .../editor/metadataEditor/metadataEditor.jsx | 6 +++--- .../tagInput-class.jsx} | 6 +++--- client/homebrew/editor/tagInput/tagInput.jsx | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) rename client/homebrew/editor/{stringArrayEditor/stringArrayEditor.jsx => tagInput/tagInput-class.jsx} (97%) create mode 100644 client/homebrew/editor/tagInput/tagInput.jsx diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 0f1f6ad54..6ee607eab 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -6,7 +6,7 @@ const _ = require('lodash'); const request = require('../../utils/request-middleware.js'); const Nav = require('naturalcrit/nav/nav.jsx'); const Combobox = require('client/components/combobox.jsx'); -const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx'); +const TagInput = require('../tagInput/tagInput.jsx'); const Themes = require('themes/themes.json'); @@ -344,7 +344,7 @@ const MetadataEditor = createClass({ {this.renderThumbnail()} - this.handleFieldChange('tags', e)}/> @@ -368,7 +368,7 @@ const MetadataEditor = createClass({ {this.renderAuthors()} - !this.props.metadata.authors?.includes(v)]} placeholder='invite author' unique={true} values={this.props.metadata.invitedAuthors} diff --git a/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx b/client/homebrew/editor/tagInput/tagInput-class.jsx similarity index 97% rename from client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx rename to client/homebrew/editor/tagInput/tagInput-class.jsx index 8f06ae561..b7acf31d0 100644 --- a/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx +++ b/client/homebrew/editor/tagInput/tagInput-class.jsx @@ -2,8 +2,8 @@ const React = require('react'); const createClass = require('create-react-class'); const _ = require('lodash'); -const StringArrayEditor = createClass({ - displayName : 'StringArrayEditor', +const TagInput = createClass({ + displayName : 'TagInput', getDefaultProps : function() { return { label : '', @@ -146,4 +146,4 @@ const StringArrayEditor = createClass({ } }); -module.exports = StringArrayEditor; \ No newline at end of file +module.exports = TagInput; \ No newline at end of file diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx new file mode 100644 index 000000000..289432175 --- /dev/null +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -0,0 +1,21 @@ +const React = require('react'); +const { useState, useRef, useEffect } = React; +const _ = require('lodash'); + +const TagInput = ({unique, ...props})=>{ + + const [temporaryValue, setTemporaryValue] = useState(''); + + return ( +
+ +
+ ) +} + +module.exports = TagInput; \ No newline at end of file From d505e4e24c228d94467f5c99d1af90c973670343 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Tue, 17 Sep 2024 23:16:06 -0500 Subject: [PATCH 02/16] Render element for each value from props Take an array of values from props, load it into valueContext state with an "editing" boolean for each value. Then, when rendering the component, take each value in the valueContext array and create a div for each -- at this point, if the value is "being edited", it returns a div with text "editing". If not being edited, it returns a div with the value as text. Nothing is being edited at this point since that functionality doesn't exist yet. --- .../editor/metadataEditor/metadataEditor.jsx | 7 +++-- client/homebrew/editor/tagInput/tagInput.jsx | 28 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 6ee607eab..97b4df2ea 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -347,7 +347,8 @@ const MetadataEditor = createClass({ this.handleFieldChange('tags', e)}/> + // onChange={(e)=>this.handleFieldChange('tags', e)} + />
@@ -372,8 +373,10 @@ const MetadataEditor = createClass({ validators={[(v)=>!this.props.metadata.authors?.includes(v)]} placeholder='invite author' unique={true} values={this.props.metadata.invitedAuthors} + values={['cat', 'dog', 'fish']} 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.']} - onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/> + // onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} + />
diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 289432175..9391b9c16 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,19 +1,29 @@ const React = require('react'); -const { useState, useRef, useEffect } = React; +const { useState } = React; const _ = require('lodash'); -const TagInput = ({unique, ...props})=>{ +const TagInput = ({unique = true, values = [], ...props})=>{ const [temporaryValue, setTemporaryValue] = useState(''); + const [valueContext, setValueContext] = useState(values.map((value)=>({ value: value, editing : false }))); + + const tagElement = (value)=>{ + return ( +
{value}
+ ) + } return ( -
- +
+ + + {Object.values(valueContext).map((context, i)=>{ return context.editing ? tagElement('editing') : tagElement(context.value) })} + + setTemporaryValue(e.target.value)} />
) } From d5c5b4315beee23ba511a069d6d25779d64f3299 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Tue, 17 Sep 2024 23:28:56 -0500 Subject: [PATCH 03/16] Render tags as "write" or "read" Tags are now either "readTag" or "writeTag", with the former being a div with the tag value and the latter a text input with the value. Minor class name change in LESS. --- .../editor/metadataEditor/metadataEditor.less | 2 +- client/homebrew/editor/tagInput/tagInput.jsx | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less index 27ebd88c2..914af9c4e 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.less +++ b/client/homebrew/editor/metadataEditor/metadataEditor.less @@ -270,7 +270,7 @@ &:last-child { border-radius : 0 0.5em 0.5em 0; } } - .badge { + .tag { padding : 0.3em; margin : 2px; font-size : 0.9em; diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 9391b9c16..bd3f53db4 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -7,9 +7,15 @@ const TagInput = ({unique = true, values = [], ...props})=>{ const [temporaryValue, setTemporaryValue] = useState(''); const [valueContext, setValueContext] = useState(values.map((value)=>({ value: value, editing : false }))); - const tagElement = (value)=>{ + const readTag = (value)=>{ return ( -
{value}
+
{value}
+ ) + } + + const writeTag = (value)=>{ + return ( + ) } @@ -17,13 +23,16 @@ const TagInput = ({unique = true, values = [], ...props})=>{
- {Object.values(valueContext).map((context, i)=>{ return context.editing ? tagElement('editing') : tagElement(context.value) })} +
- setTemporaryValue(e.target.value)} /> + {Object.values(valueContext).map((context, i)=>{ return context.editing ? writeTag(context.value) : readTag(context.value) })} + + setTemporaryValue(e.target.value)} /> +
) } From 36aa4ea508e35a9220f747a2e52f2db56c045681 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Tue, 17 Sep 2024 23:50:59 -0500 Subject: [PATCH 04/16] Add click handler for readTags to open text input Clicking on a readTag now converts that tag to a text input, and maintains the tag value. It also closes any other open text inputs amongst the tags (but leaves the "new tag" input open). --- client/homebrew/editor/tagInput/tagInput.jsx | 21 ++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index bd3f53db4..8063b4ab2 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -7,13 +7,26 @@ const TagInput = ({unique = true, values = [], ...props})=>{ const [temporaryValue, setTemporaryValue] = useState(''); const [valueContext, setValueContext] = useState(values.map((value)=>({ value: value, editing : false }))); - const readTag = (value)=>{ + + const editTag = (evt)=>{ + setValueContext(valueContext.map((context)=>{ + context.editing = context.value === evt.target.dataset.value ? true : false; + return context; + })) + } + + const renderReadTag = (value)=>{ return ( -
{value}
+
editTag(evt)}> + {value} +
) } - const writeTag = (value)=>{ + const renderWriteTag = (value)=>{ return ( ) @@ -25,7 +38,7 @@ const TagInput = ({unique = true, values = [], ...props})=>{
- {Object.values(valueContext).map((context, i)=>{ return context.editing ? writeTag(context.value) : readTag(context.value) })} + {Object.values(valueContext).map((context, i)=>{ return context.editing ? renderWriteTag(context.value) : renderReadTag(context.value) })} Date: Wed, 18 Sep 2024 21:00:24 -0500 Subject: [PATCH 05/16] add editing of input functionality Currently uses uncontrolled inputs with a `defaultValue` attribute set to the values passed in via props. The input can then be edited, and when `Enter` is pressed, it updates the stored value state. Later, this can be updated to be trigger with `Tab` or clicking outside the active input element. --- client/homebrew/editor/tagInput/tagInput.jsx | 81 ++++++++++++-------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 8063b4ab2..cfcb2be42 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,53 +1,74 @@ const React = require('react'); const { useState } = React; -const _ = require('lodash'); - -const TagInput = ({unique = true, values = [], ...props})=>{ +const TagInput = ({ unique = true, values = [], ...props }) => { const [temporaryValue, setTemporaryValue] = useState(''); - const [valueContext, setValueContext] = useState(values.map((value)=>({ value: value, editing : false }))); + const [valueContext, setValueContext] = useState(values.map((value) => ({ value, editing: false }))); + const handleInputKeyDown = (evt, value) => { + if (evt.key === 'Enter') { + submitTag(evt.target.value, value); + } + }; - const editTag = (evt)=>{ - setValueContext(valueContext.map((context)=>{ - context.editing = context.value === evt.target.dataset.value ? true : false; - return context; - })) - } + const submitTag = (newValue, originalValue) => { + setValueContext((prevContext) => { + return prevContext.map((context) => { + if (context.value === originalValue) { + return { ...context, value: newValue, editing: false }; + } + return context; + }); + }); + }; - const renderReadTag = (value)=>{ + const editTag = (valueToEdit) => { + setValueContext((prevContext) => { + return prevContext.map((context) => { + if (context.value === valueToEdit) { + return { ...context, editing: true }; + } + return { ...context, editing: false }; + }); + }); + }; + + const renderReadTag = (context) => { return ( -
editTag(evt)}> - {value} +
editTag(context.value)}> + {context.value}
- ) - } + ); + }; - const renderWriteTag = (value)=>{ + const renderWriteTag = (context) => { return ( - - ) - } + handleInputKeyDown(evt, context.value)} + autoFocus + /> + ); + }; return (
-
- - {Object.values(valueContext).map((context, i)=>{ return context.editing ? renderWriteTag(context.value) : renderReadTag(context.value) })} + {valueContext.map((context) => { return context.editing ? renderWriteTag(context) : renderReadTag(context); })} setTemporaryValue(e.target.value)} /> + onChange={(e) => setTemporaryValue(e.target.value)} />
- ) -} + ); +}; -module.exports = TagInput; \ No newline at end of file +module.exports = TagInput; From d1686c4c8f7005786af1cc3c29333636405b5186 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 18 Sep 2024 21:50:04 -0500 Subject: [PATCH 06/16] Add in handlers for TagInput value changes Now brew metadata is actually updated and preserved across reloads to match updated tag values. useEffect calls the props.onChange event from the parent component on every change to the valueContext state of this component (right now, after hitting Enter in a tag input). --- .../editor/metadataEditor/metadataEditor.jsx | 5 ++--- client/homebrew/editor/tagInput/tagInput.jsx | 14 +++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 97b4df2ea..7df2e44b6 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -347,7 +347,7 @@ const MetadataEditor = createClass({ this.handleFieldChange('tags', e)} + onChange={(e)=>this.handleFieldChange('tags', e)} />
@@ -373,9 +373,8 @@ const MetadataEditor = createClass({ validators={[(v)=>!this.props.metadata.authors?.includes(v)]} placeholder='invite author' unique={true} values={this.props.metadata.invitedAuthors} - values={['cat', 'dog', 'fish']} 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.']} - // onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} + onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} />
diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index cfcb2be42..22760896a 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,16 +1,28 @@ const React = require('react'); -const { useState } = React; +const { useState, useEffect } = React; const TagInput = ({ unique = true, values = [], ...props }) => { const [temporaryValue, setTemporaryValue] = useState(''); const [valueContext, setValueContext] = useState(values.map((value) => ({ value, editing: false }))); + useEffect(()=>{ + handleChange(valueContext.map((context)=>context.value)) + }, [valueContext]) + + const handleChange = (value)=>{ + props.onChange({ + target : { value } + }) + }; + const handleInputKeyDown = (evt, value) => { if (evt.key === 'Enter') { submitTag(evt.target.value, value); } }; + + const submitTag = (newValue, originalValue) => { setValueContext((prevContext) => { return prevContext.map((context) => { From 70a3cb9ef9a1f1ae745107bb47cb259c245f74c0 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 18 Sep 2024 22:46:00 -0500 Subject: [PATCH 07/16] Add method for adding new tags Component now accepts new tags entered in the always-present input field. Entering a value and hitting Enter submits the tag, and it appears as a new tag. Updated the tag list keys to be unique (via `index`). To-Do: empty 'new tag' input after submitting. --- client/homebrew/editor/tagInput/tagInput.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 22760896a..f42e2f62f 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,5 +1,6 @@ const React = require('react'); const { useState, useEffect } = React; +const _ = require('lodash'); const TagInput = ({ unique = true, values = [], ...props }) => { const [temporaryValue, setTemporaryValue] = useState(''); @@ -25,6 +26,11 @@ const TagInput = ({ unique = true, values = [], ...props }) => { const submitTag = (newValue, originalValue) => { setValueContext((prevContext) => { + // add new tag + if(originalValue === null){ + return [...prevContext, { value: newValue, editing: false }] + } + // update existing tag return prevContext.map((context) => { if (context.value === originalValue) { return { ...context, value: newValue, editing: false }; @@ -45,9 +51,9 @@ const TagInput = ({ unique = true, values = [], ...props }) => { }); }; - const renderReadTag = (context) => { + const renderReadTag = (context, index) => { return ( -
editTag(context.value)}> @@ -56,10 +62,10 @@ const TagInput = ({ unique = true, values = [], ...props }) => { ); }; - const renderWriteTag = (context) => { + const renderWriteTag = (context, index) => { return ( handleInputKeyDown(evt, context.value)} autoFocus @@ -71,13 +77,14 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
- {valueContext.map((context) => { return context.editing ? renderWriteTag(context) : renderReadTag(context); })} + {valueContext.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} setTemporaryValue(e.target.value)} /> + onChange={(e) => setTemporaryValue(e.target.value)} + onKeyDown={(evt) => handleInputKeyDown(evt, null)} />
); From c65210b3ed6899e6ed131187a91b042d32fb2d35 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 18 Sep 2024 23:13:46 -0500 Subject: [PATCH 08/16] Add 'remove' button and method New button that triggers `submitTag()` method directly (rather than throw onKeyDown event) and passes `null` as the newValue. New `if` condition checks for null on newValue and if true, removes the tag that matches the originalValue. This *does* currently delete all duplicate tags if they match the one you are deleting. Not sure when you'd ever want duplicate tags, but regardless i'll likely switch this to work via Index, not value. --- client/homebrew/editor/tagInput/tagInput.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index f42e2f62f..03678961d 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -26,6 +26,11 @@ const TagInput = ({ unique = true, values = [], ...props }) => { const submitTag = (newValue, originalValue) => { setValueContext((prevContext) => { + // remove existing tag + if(newValue === null){ + console.log('remove'); + return [...prevContext].filter((context)=>context.value !== originalValue); + } // add new tag if(originalValue === null){ return [...prevContext, { value: newValue, editing: false }] @@ -58,6 +63,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => { className='tag' onClick={() => editTag(context.value)}> {context.value} +
); }; From c1288ce4bb79ef7b45085113f7cc1f889cd97e6b Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 18 Sep 2024 23:24:10 -0500 Subject: [PATCH 09/16] Use index to find and remove tags Fixes issue in last commit, so removing a tag that has duplicate value of other tags only removes the correct one, not the others as well. --- client/homebrew/editor/tagInput/tagInput.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 03678961d..f72ec17cf 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -24,12 +24,12 @@ const TagInput = ({ unique = true, values = [], ...props }) => { - const submitTag = (newValue, originalValue) => { + const submitTag = (newValue, originalValue, index) => { setValueContext((prevContext) => { // remove existing tag if(newValue === null){ console.log('remove'); - return [...prevContext].filter((context)=>context.value !== originalValue); + return [...prevContext].filter((context, i)=>i !== index); } // add new tag if(originalValue === null){ @@ -63,7 +63,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => { className='tag' onClick={() => editTag(context.value)}> {context.value} - +
); }; From a54adc1e4b9756dd9efc23b4c114516d5ebd697f Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 18 Sep 2024 23:39:26 -0500 Subject: [PATCH 10/16] Set new tag input to clear itself after submission Now whenever a new tag is submitted, the input element is cleared and ready for the next tag. Whitespace cleanup. --- client/homebrew/editor/tagInput/tagInput.jsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index f72ec17cf..625e89c36 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -16,19 +16,17 @@ const TagInput = ({ unique = true, values = [], ...props }) => { }) }; - const handleInputKeyDown = (evt, value) => { + const handleInputKeyDown = (evt, value, clear = false) => { if (evt.key === 'Enter') { submitTag(evt.target.value, value); - } + if(clear){ setTemporaryValue(''); } + }; }; - - const submitTag = (newValue, originalValue, index) => { setValueContext((prevContext) => { // remove existing tag if(newValue === null){ - console.log('remove'); return [...prevContext].filter((context, i)=>i !== index); } // add new tag @@ -90,7 +88,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => { placeholder={props.placeholder} value={temporaryValue} onChange={(e) => setTemporaryValue(e.target.value)} - onKeyDown={(evt) => handleInputKeyDown(evt, null)} /> + onKeyDown={(evt) => handleInputKeyDown(evt, null, true)} />
); From 5b4a7c168fc3985d1f15103fd0dcc477d8e4d314 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 18 Sep 2024 23:54:12 -0500 Subject: [PATCH 11/16] Add comma to "submit" buttons Now comma (`,`) submits a tag, like `Enter` --- client/homebrew/editor/tagInput/tagInput.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 625e89c36..dee7baf94 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -17,7 +17,8 @@ const TagInput = ({ unique = true, values = [], ...props }) => { }; const handleInputKeyDown = (evt, value, clear = false) => { - if (evt.key === 'Enter') { + if (_.includes(['Enter', ','], evt.key)) { + evt.preventDefault(); submitTag(evt.target.value, value); if(clear){ setTemporaryValue(''); } }; @@ -88,7 +89,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => { placeholder={props.placeholder} value={temporaryValue} onChange={(e) => setTemporaryValue(e.target.value)} - onKeyDown={(evt) => handleInputKeyDown(evt, null, true)} /> + onKeyDown={(evt) =>handleInputKeyDown(evt, null, true)} />
); From 7ea1696065e16a2e174a3edfafdcb2ea8f4cc62b Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 19 Sep 2024 10:40:09 -0500 Subject: [PATCH 12/16] Adjust html structure to handle tags as list Begin work on setting a better html structure for the component. Create a .less file for the component, which I may not actually use. --- client/homebrew/editor/tagInput/tagInput.jsx | 11 +++++++---- client/homebrew/editor/tagInput/tagInput.less | 0 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 client/homebrew/editor/tagInput/tagInput.less diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 625e89c36..8619df6cf 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,3 +1,4 @@ +require('./tagInput.less'); const React = require('react'); const { useState, useEffect } = React; const _ = require('lodash'); @@ -56,13 +57,13 @@ const TagInput = ({ unique = true, values = [], ...props }) => { const renderReadTag = (context, index) => { return ( -
editTag(context.value)}> {context.value} -
+ ); }; @@ -80,8 +81,10 @@ const TagInput = ({ unique = true, values = [], ...props }) => { return (
-
- {valueContext.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} +
+
    + {valueContext.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} +
Date: Thu, 19 Sep 2024 15:48:47 -0500 Subject: [PATCH 13/16] Fix multiple duplicate tags updating at once Fixes an issue where tags with duplicate values would all update to the same value after editing just one. Also an adjustment to the parameters that are passed to handleInputKeyDown-- they are now one object. This helps handle an "options" object where more optional features can be turned on and off. --- client/homebrew/editor/tagInput/tagInput.jsx | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 387e09b81..2c636b0c8 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -17,12 +17,14 @@ const TagInput = ({ unique = true, values = [], ...props }) => { }) }; - const handleInputKeyDown = (evt, value, clear = false) => { + const handleInputKeyDown = ({ evt, value, index, options = {} }) => { if (_.includes(['Enter', ','], evt.key)) { evt.preventDefault(); - submitTag(evt.target.value, value); - if(clear){ setTemporaryValue(''); } - }; + submitTag(evt.target.value, value, index); + if (options.clear) { + setTemporaryValue(''); + } + } }; const submitTag = (newValue, originalValue, index) => { @@ -36,8 +38,8 @@ const TagInput = ({ unique = true, values = [], ...props }) => { return [...prevContext, { value: newValue, editing: false }] } // update existing tag - return prevContext.map((context) => { - if (context.value === originalValue) { + return prevContext.map((context, i) => { + if (i === index) { return { ...context, value: newValue, editing: false }; } return context; @@ -45,10 +47,10 @@ const TagInput = ({ unique = true, values = [], ...props }) => { }); }; - const editTag = (valueToEdit) => { + const editTag = (index) => { setValueContext((prevContext) => { - return prevContext.map((context) => { - if (context.value === valueToEdit) { + return prevContext.map((context, i) => { + if (i === index) { return { ...context, editing: true }; } return { ...context, editing: false }; @@ -61,7 +63,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
  • editTag(context.value)}> + onClick={() => editTag(index)}> {context.value}
  • @@ -73,7 +75,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => { handleInputKeyDown(evt, context.value)} + onKeyDown={(evt) => handleInputKeyDown({evt, value: context.value, index: index})} autoFocus /> ); @@ -87,12 +89,14 @@ const TagInput = ({ unique = true, values = [], ...props }) => { {valueContext.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} - setTemporaryValue(e.target.value)} - onKeyDown={(evt) =>handleInputKeyDown(evt, null, true)} /> + onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })} + />
    ); From 433f016c25a6c69dfd3ef30d5e58263a2a6348c7 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 19 Sep 2024 15:57:40 -0500 Subject: [PATCH 14/16] rename tag container class to unify with fields --- client/homebrew/editor/tagInput/tagInput.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 2c636b0c8..8161ffe51 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -84,7 +84,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => { return (
    -
    +
      {valueContext.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
    From 5af45f16b0c5a08cb481151b643504a5806ad449 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Mon, 23 Sep 2024 14:54:24 -0500 Subject: [PATCH 15/16] remove tagInput-class This file was just the old StringArrayEditor that I kept around for easy reference. Can be deleted now. --- .../editor/tagInput/tagInput-class.jsx | 149 ------------------ 1 file changed, 149 deletions(-) delete mode 100644 client/homebrew/editor/tagInput/tagInput-class.jsx diff --git a/client/homebrew/editor/tagInput/tagInput-class.jsx b/client/homebrew/editor/tagInput/tagInput-class.jsx deleted file mode 100644 index 85b8c6a3f..000000000 --- a/client/homebrew/editor/tagInput/tagInput-class.jsx +++ /dev/null @@ -1,149 +0,0 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); - -const TagInput = createClass({ - displayName : 'TagInput', - getDefaultProps : function() { - return { - label : '', - values : [], - valuePatterns : null, - validators : [], - placeholder : '', - notes : [], - unique : false, - cannotEdit : [], - onChange : ()=>{} - }; - }, - - getInitialState : function() { - return { - valueContext : !!this.props.values ? this.props.values.map((value)=>({ - value, - editing : false - })) : [], - temporaryValue : '', - updateValue : '' - }; - }, - - componentDidUpdate : function(prevProps) { - if(!_.eq(this.props.values, prevProps.values)) { - this.setState({ - valueContext : this.props.values ? this.props.values.map((newValue)=>({ - value : newValue, - editing : this.state.valueContext.find(({ value })=>value === newValue)?.editing || false - })) : [] - }); - } - }, - - handleChange : function(value) { - this.props.onChange({ - target : { - value - } - }); - }, - - addValue : function(value){ - this.handleChange(_.uniq([...this.props.values, value])); - this.setState({ - temporaryValue : '' - }); - }, - - removeValue : function(index){ - this.handleChange(this.props.values.filter((_, i)=>i !== index)); - }, - - updateValue : function(value, index){ - const valueContext = this.state.valueContext; - valueContext[index].value = value; - valueContext[index].editing = false; - this.handleChange(valueContext.map((context)=>context.value)); - this.setState({ valueContext, updateValue: '' }); - }, - - editValue : function(index){ - if(!!this.props.cannotEdit && this.props.cannotEdit.includes(this.props.values[index])) { - return; - } - const valueContext = this.state.valueContext.map((context, i)=>{ - context.editing = index === i; - return context; - }); - this.setState({ valueContext, updateValue: this.props.values[index] }); - }, - - valueIsValid : function(value, index) { - const values = _.clone(this.props.values); - if(index !== undefined) { - values.splice(index, 1); - } - const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern)); - const uniqueIfSet = !this.props.unique || !values.includes(value); - const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value)); - return matchesPatterns && uniqueIfSet && passesValidators; - }, - - handleValueInputKeyDown : function(event, index) { - if(event.key === 'Enter') { - if(this.valueIsValid(event.target.value, index)) { - if(index !== undefined) { - this.updateValue(event.target.value, index); - } else { - this.addValue(event.target.value); - } - } - } else if(event.key === 'Escape') { - this.closeEditInput(index); - } - }, - - closeEditInput : function(index) { - const valueContext = this.state.valueContext; - valueContext[index].editing = false; - this.setState({ valueContext, updateValue: '' }); - }, - - render : function() { - const valueElements = Object.values(this.state.valueContext).map((context, i)=>context.editing - ? -
    - this.handleValueInputKeyDown(e, i)} - onChange={(e)=>this.setState({ updateValue: e.target.value })}/> - {
    { e.stopPropagation(); this.closeEditInput(i); }}>
    } - {this.valueIsValid(this.state.updateValue, i) ?
    { e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}>
    : null} -
    -
    - :
    this.editValue(i)}>{context.value} - {!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null :
    { e.stopPropagation(); this.removeValue(i); }}>
    } -
    - ); - - return
    - -
    -
    - {valueElements} -
    - this.handleValueInputKeyDown(e)} - onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/> - {this.valueIsValid(this.state.temporaryValue) ?
    { e.stopPropagation(); this.addValue(this.state.temporaryValue); }}>
    : null} -
    -
    - - {this.props.notes ? this.props.notes.map((n, index)=>

    {n}

    ) : null} -
    -
    ; - } -}); - -module.exports = TagInput; \ No newline at end of file From a96ff6ecb3a6c40fd223091ee4d99dc477b1dc05 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Mon, 23 Sep 2024 21:05:37 -0500 Subject: [PATCH 16/16] Variable name changes for clarity Followed suggestions on the PR. --- client/homebrew/editor/tagInput/tagInput.jsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 8161ffe51..816541167 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -4,12 +4,12 @@ const { useState, useEffect } = React; const _ = require('lodash'); const TagInput = ({ unique = true, values = [], ...props }) => { - const [temporaryValue, setTemporaryValue] = useState(''); - const [valueContext, setValueContext] = useState(values.map((value) => ({ value, editing: false }))); + const [tempInputText, setTempInputText] = useState(''); + const [tagList, setTagList] = useState(values.map((value) => ({ value, editing: false }))); useEffect(()=>{ - handleChange(valueContext.map((context)=>context.value)) - }, [valueContext]) + handleChange(tagList.map((context)=>context.value)) + }, [tagList]) const handleChange = (value)=>{ props.onChange({ @@ -22,13 +22,13 @@ const TagInput = ({ unique = true, values = [], ...props }) => { evt.preventDefault(); submitTag(evt.target.value, value, index); if (options.clear) { - setTemporaryValue(''); + setTempInputText(''); } } }; const submitTag = (newValue, originalValue, index) => { - setValueContext((prevContext) => { + setTagList((prevContext) => { // remove existing tag if(newValue === null){ return [...prevContext].filter((context, i)=>i !== index); @@ -48,7 +48,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => { }; const editTag = (index) => { - setValueContext((prevContext) => { + setTagList((prevContext) => { return prevContext.map((context, i) => { if (i === index) { return { ...context, editing: true }; @@ -86,15 +86,15 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
      - {valueContext.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} + {tagList.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
    setTemporaryValue(e.target.value)} + value={tempInputText} + onChange={(e) => setTempInputText(e.target.value)} onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })} />