diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /browser/components/pocket/content/panels/js/components/TagPicker | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/pocket/content/panels/js/components/TagPicker')
-rw-r--r-- | browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx | 208 | ||||
-rw-r--r-- | browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss | 141 |
2 files changed, 349 insertions, 0 deletions
diff --git a/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx new file mode 100644 index 0000000000..9c1f658a80 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.jsx @@ -0,0 +1,208 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useEffect } from "react"; +import panelMessaging from "../../messages"; + +function TagPicker(props) { + const [tags, setTags] = useState(props.tags); // New tag group to store + const [allTags, setAllTags] = useState([]); // All tags ever used (in no particular order) + const [recentTags, setRecentTags] = useState([]); // Most recently used tags + const [duplicateTag, setDuplicateTag] = useState(null); + const [inputValue, setInputValue] = useState(""); + + // Status can be success, waiting, or error. + const [{ tagInputStatus, tagInputErrorMessage }, setTagInputStatus] = + useState({ + tagInputStatus: "", + tagInputErrorMessage: "", + }); + + let handleKeyDown = e => { + const enterKey = e.keyCode === 13; + const commaKey = e.keyCode === 188; + const tabKey = inputValue && e.keyCode === 9; + + // Submit tags on enter with no input. + // Enter tag on comma, tab, or enter with input. + // Tab to next element with no input. + if (commaKey || enterKey || tabKey) { + e.preventDefault(); + if (inputValue) { + addTag(inputValue.trim()); + setInputValue(``); // Clear out input + } else if (enterKey) { + submitTags(); + } + } + }; + + let addTag = tagToAdd => { + if (!tagToAdd?.length) { + return; + } + + let newDuplicateTag = tags.find(item => item === tagToAdd); + + if (!newDuplicateTag) { + setTags([...tags, tagToAdd]); + } else { + setDuplicateTag(newDuplicateTag); + + setTimeout(() => { + setDuplicateTag(null); + }, 1000); + } + }; + + let removeTag = index => { + let updatedTags = tags.slice(0); // Shallow copied array + updatedTags.splice(index, 1); + setTags(updatedTags); + }; + + let submitTags = () => { + let tagsToSubmit = []; + + if (tags?.length) { + tagsToSubmit = tags; + } + + // Capture tags that have been typed in but not explicitly added to the tag collection + if (inputValue?.trim().length) { + tagsToSubmit.push(inputValue.trim()); + } + + if (!props.itemUrl || !tagsToSubmit?.length) { + return; + } + + setTagInputStatus({ + tagInputStatus: "waiting", + tagInputErrorMessage: "", + }); + panelMessaging.sendMessage( + "PKT_addTags", + { + url: props.itemUrl, + tags: tagsToSubmit, + }, + function (resp) { + const { data } = resp; + + if (data.status === "success") { + setTagInputStatus({ + tagInputStatus: "success", + tagInputErrorMessage: "", + }); + } else if (data.status === "error") { + setTagInputStatus({ + tagInputStatus: "error", + tagInputErrorMessage: data.error.message, + }); + } + } + ); + }; + + useEffect(() => { + panelMessaging.sendMessage("PKT_getTags", {}, resp => { + setAllTags(resp?.data?.tags); + }); + }, []); + + useEffect(() => { + panelMessaging.sendMessage("PKT_getRecentTags", {}, resp => { + setRecentTags(resp?.data?.recentTags); + }); + }, []); + + return ( + <div className="stp_tag_picker"> + {!tagInputStatus && ( + <> + <h3 + className="header_small" + data-l10n-id="pocket-panel-signup-add-tags" + /> + <div className="stp_tag_picker_tags"> + {tags.map((tag, i) => ( + <div + className={`stp_tag_picker_tag${ + duplicateTag === tag ? ` stp_tag_picker_tag_duplicate` : `` + }`} + > + <button + onClick={() => removeTag(i)} + className={`stp_tag_picker_tag_remove`} + > + X + </button> + {tag} + </div> + ))} + <div className="stp_tag_picker_input_wrapper"> + <input + className="stp_tag_picker_input" + type="text" + list="tag-list" + value={inputValue} + onChange={e => setInputValue(e.target.value)} + onKeyDown={e => handleKeyDown(e)} + maxlength="25" + /> + <datalist id="tag-list"> + {allTags + .sort((a, b) => a.search(inputValue) - b.search(inputValue)) + .map(item => ( + <option key={item} value={item} /> + ))} + </datalist> + <button + className="stp_tag_picker_button" + disabled={!inputValue?.length && !tags.length} + data-l10n-id="pocket-panel-saved-save-tags" + onClick={() => submitTags()} + /> + </div> + </div> + <div className="recent_tags"> + {recentTags + .slice(0, 3) + .filter(recentTag => { + return !tags.find(item => item === recentTag); + }) + .map(tag => ( + <div className="stp_tag_picker_tag"> + <button + className="stp_tag_picker_tag_remove" + onClick={() => addTag(tag)} + > + + {tag} + </button> + </div> + ))} + </div> + </> + )} + {tagInputStatus === "waiting" && ( + <h3 + className="header_large" + data-l10n-id="pocket-panel-saved-processing-tags" + /> + )} + {tagInputStatus === "success" && ( + <h3 + className="header_large" + data-l10n-id="pocket-panel-saved-tags-saved" + /> + )} + {tagInputStatus === "error" && ( + <h3 className="header_small">{tagInputErrorMessage}</h3> + )} + </div> + ); +} + +export default TagPicker; diff --git a/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss new file mode 100644 index 0000000000..215307d079 --- /dev/null +++ b/browser/components/pocket/content/panels/js/components/TagPicker/TagPicker.scss @@ -0,0 +1,141 @@ +.stp_tag_picker { + .stp_tag_picker_tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 8px; + border: 1px solid #8F8F9D; + border-radius: 4px; + font-style: normal; + font-weight: normal; + font-size: 1rem; + line-height: 1.2rem; + color: #15141A; + margin-bottom: 10px; + } + + .stp_tag_picker_tag { + background: #F0F0F4; + border-radius: 4px; + color: #15141A; + display: inline-block; + font-size: 0.85rem; + line-height: 1rem; + font-style: normal; + font-weight: 600; + padding: 0 8px; + transition: background-color 200ms ease-in-out; + + @include theme_dark { + background: #2B2A33; + color: #FBFBFB; + } + } + + .recent_tags .stp_tag_picker_tag { + margin-inline-end: 5px; + } + + .stp_tag_picker_tag_remove { + padding-top: 5px; + padding-bottom: 5px; + padding-inline-end: 5px; + color: #5B5B66; + font-weight: 400; + + &:hover { + color: #3E3E44; + } + + &:focus { + color: #3E3E44; + outline: 2px solid #0060df; + outline-offset: -4px; + } + + @include theme_dark { + color: #8F8F9D; + + &:hover { + color: #fff; + } + + &:focus { + outline: 2px solid #00DDFF; + } + } + } + + .stp_tag_picker_tag_duplicate { + background-color: #bbb; + + @include theme_dark { + background-color: #666; + } + } + + .stp_tag_picker_input_wrapper { + display: flex; + flex-grow: 1; + } + + .stp_tag_picker_input { + flex-grow: 1; + border: 1px solid #8F8F9D; + padding: 0 6px; + border-start-start-radius: 4px; + border-end-start-radius: 4px; + + &:focus { + border: 1px solid #0060DF; + outline: 1px solid #0060DF; + } + + @include theme_dark { + background: none; + color: #FBFBFB; + + &:focus { + border: 1px solid #00DDFF; + outline: 1px solid #00DDFF; + } + } + } + + .stp_tag_picker_button { + font-size: 0.95rem; + line-height: 1.1rem; + padding: 4px 6px; + background-color: #F0F0F4; + border: 1px solid #8F8F9D; + border-inline-start: none; + border-start-end-radius: 4px; + border-end-end-radius: 4px; + &:disabled { + color: #8F8F9D; + } + &:hover:enabled { + background-color: #DADADF; + } + &:focus:enabled { + border: 1px solid #0060DF; + outline: 1px solid #0060DF; + } + + @include theme_dark { + background-color: #2B2A33; + color: #FBFBFB; + &:disabled { + color: #666; + } + &:hover:enabled { + background-color: #53535d; + } + &:focus:enabled { + border: 1px solid #00DDFF; + outline: 1px solid #00DDFF; + } + } + } +} |