diff --git a/NEWS.md b/NEWS.md
index 5190bf50e5..b8ca7ccb89 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -20,6 +20,7 @@
- YouTube spout now supports following playlists. ([#1260](https://github.com/fossar/selfoss/pull/1260))
- Confirmation is now required when leaving the setting page with unsaved source changes. ([#1300](https://github.com/fossar/selfoss/pull/1300))
- Add link from settings page to individual sources. ([#1329](https://github.com/fossar/selfoss/pull/1329))
+- Tag colour can be now changed using keyboard. ([#1335](https://github.com/fossar/selfoss/pull/1335))
- Translations into several new languages were added:
- English (United Kingdom): `en-GB`
- French (Canada): `fr-CA`
diff --git a/README.md b/README.md
index cc466a9fac..d1221e00a2 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,6 @@ Special thanks to the great programmers of these libraries used by selfoss:
* [PHP Universal Feed Generator](https://github.com/ajaxray/FeedWriter)
* [Elphin IcoFileLoader](https://github.com/lordelph/icofileloader)
* [jQuery hotkeys](https://github.com/tzuryby/jquery.hotkeys)
-* [Spectrum Colorpicker](https://github.com/bgrins/spectrum)
* [Graby](https://github.com/j0k3r/graby)
* [FullTextRSS filters](http://help.fivefilters.org/customer/portal/articles/223153-site-patterns)
diff --git a/assets/js/helpers/color.js b/assets/js/helpers/color.js
new file mode 100644
index 0000000000..a52db53f8c
--- /dev/null
+++ b/assets/js/helpers/color.js
@@ -0,0 +1,20 @@
+/**
+ * Get dark OR bright color depending the color contrast.
+ *
+ * @param string hexColor color (hex) value
+ * @param string darkColor dark color value
+ * @param string brightColor bright color value
+ *
+ * @return string dark OR bright color value
+ *
+ * @see https://24ways.org/2010/calculating-color-contrast/
+ */
+export function colorByBrightness(hexColor, darkColor = '#555', brightColor = '#EEE') {
+ // Strip hash sign.
+ const color = hexColor.substr(1);
+ const r = parseInt(color.substr(0, 2), 16);
+ const g = parseInt(color.substr(2, 2), 16);
+ const b = parseInt(color.substr(4, 2), 16);
+ const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
+ return yiq >= 128 ? darkColor : brightColor;
+}
diff --git a/assets/js/icons.js b/assets/js/icons.js
index 6ea266f4e2..58f696116e 100644
--- a/assets/js/icons.js
+++ b/assets/js/icons.js
@@ -11,6 +11,7 @@ import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons/faS
import { faTimesCircle } from '@fortawesome/free-regular-svg-icons/faTimesCircle';
import { faArrowAltCircleDown } from '@fortawesome/free-solid-svg-icons/faArrowAltCircleDown';
import { faArrowRight } from '@fortawesome/free-solid-svg-icons/faArrowRight';
+import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck';
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons/faCheckCircle';
import { faCaretDown } from '@fortawesome/free-solid-svg-icons/faCaretDown';
import { faCaretRight } from '@fortawesome/free-solid-svg-icons/faCaretRight';
@@ -54,6 +55,7 @@ export {
faArrowRight as next,
faCaretDown as arrowExpanded,
faCaretRight as arrowCollapsed,
+ faCheck as check,
faCheckCircle as markRead,
faCloudUploadAlt as settings,
faCog as menu,
diff --git a/assets/js/index.js b/assets/js/index.js
index 45ce375cbb..96c03021b9 100644
--- a/assets/js/index.js
+++ b/assets/js/index.js
@@ -1,6 +1,5 @@
import 'regenerator-runtime/runtime';
import './jquery';
-import 'spectrum-colorpicker';
import selfoss from './selfoss-base';
import './selfoss-shares';
import './selfoss-db-online';
diff --git a/assets/js/templates/ColorChooser.jsx b/assets/js/templates/ColorChooser.jsx
index 213bb5b162..93806b6fc7 100644
--- a/assets/js/templates/ColorChooser.jsx
+++ b/assets/js/templates/ColorChooser.jsx
@@ -1,33 +1,116 @@
-import React from 'react';
+import React, { useContext, useMemo } from 'react';
+import { useFloating, autoUpdate, flip, offset, shift } from '@floating-ui/react-dom';
import PropTypes from 'prop-types';
+import { Button as MenuButton, Wrapper as MenuWrapper, Menu, MenuItem } from 'react-aria-menubutton';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { colorByBrightness } from '../helpers/color';
+import { LocalizationContext } from '../helpers/i18n';
+import * as icons from '../icons';
+
+const palette = ['#ffccc9', '#ffce93', '#fffc9e', '#ffffc7', '#9aff99', '#96fffb', '#cdffff', '#cbcefb', '#fffe65', '#cfcfcf', '#fd6864', '#fe996b', '#fcff2f', '#67fd9a', '#38fff8', '#68fdff', '#9698ed', '#c0c0c0', '#fe0000', '#f8a102', '#ffcc67', '#f8ff00', '#34ff34', '#68cbd0', '#34cdf9', '#6665cd', '#9b9b9b', '#cb0000', '#f56b00', '#ffcb2f', '#ffc702', '#32cb00', '#00d2cb', '#3166ff', '#6434fc', '#656565', '#9a0000', '#ce6301', '#cd9934', '#999903', '#009901', '#329a9d', '#3531ff', '#6200c9', '#343434', '#680100', '#963400', '#986536', '#646809', '#036400', '#34696d', '#00009b', '#303498', '#000000', '#330001', '#643403', '#663234', '#343300', '#013300', '#003532', '#010066', '#340096'];
+
+function ColorButton({tag, color}) {
+ const style = useMemo(
+ () => ({
+ backgroundColor: color,
+ color: colorByBrightness(color),
+ }),
+ [color]
+ );
+
+ const selected = color === tag.color;
+
+ return (
+
+ );
+}
+
+ColorButton.propTypes = {
+ tag: PropTypes.object.isRequired,
+ color: PropTypes.string.isRequired,
+};
+
+const preventDefault = (event) => {
+ event.preventDefault();
+ // Prevent closing navigation on mobile.
+ event.stopPropagation();
+};
export default function ColorChooser({tag, onChange}) {
- const colorChooser = React.useRef(null);
-
- React.useLayoutEffect(() => {
- // init colorpicker
- const picker = colorChooser.current;
- $(picker).spectrum({
- showPaletteOnly: true,
- color: tag.color,
- palette: [
- ['#ffccc9', '#ffce93', '#fffc9e', '#ffffc7', '#9aff99', '#96fffb', '#cdffff', '#cbcefb', '#fffe65', '#cfcfcf', '#fd6864', '#fe996b', '#fcff2f', '#67fd9a', '#38fff8', '#68fdff', '#9698ed', '#c0c0c0', '#fe0000', '#f8a102', '#ffcc67', '#f8ff00', '#34ff34', '#68cbd0', '#34cdf9', '#6665cd', '#9b9b9b', '#cb0000', '#f56b00', '#ffcb2f', '#ffc702', '#32cb00', '#00d2cb', '#3166ff', '#6434fc', '#656565', '#9a0000', '#ce6301', '#cd9934', '#999903', '#009901', '#329a9d', '#3531ff', '#6200c9', '#343434', '#680100', '#963400', '#986536', '#646809', '#036400', '#34696d', '#00009b', '#303498', '#000000', '#330001', '#643403', '#663234', '#343300', '#013300', '#003532', '#010066', '#340096']
- ],
- change: onChange,
- });
-
- return () => {
- $(picker).spectrum('destroy');
- };
- }, [onChange, tag.color]);
-
- const style = React.useMemo(
+ const style = useMemo(
() => ({ backgroundColor: tag.color }),
[tag.color]
);
+ const {
+ x: menuX,
+ y: menuY,
+ reference: buttonRef,
+ floating: floatingRef,
+ strategy: positionStrategy,
+ } = useFloating({
+ placement: 'right-start',
+ strategy: 'fixed',
+ middleware: [
+ offset({ mainAxis: 16 }),
+ shift(),
+ flip(),
+ ],
+ whileElementsMounted: autoUpdate,
+ });
+
+ const _ = useContext(LocalizationContext);
+
return (
-
+
+
+
+ {_('tag_change_color_button_title')}
+
+
+
+
);
}
diff --git a/assets/js/templates/NavTags.jsx b/assets/js/templates/NavTags.jsx
index e538c02d31..e24f7ee36b 100644
--- a/assets/js/templates/NavTags.jsx
+++ b/assets/js/templates/NavTags.jsx
@@ -20,7 +20,7 @@ function Tag({ tag, active, collapseNav }) {
(color) => {
updateTag(
tagName,
- color.toHexString()
+ color
).then(() => {
selfoss.entriesPage?.reload();
}).catch((error) => {
diff --git a/assets/locale/cs.json b/assets/locale/cs.json
index ab20057fdf..b985ac13a5 100644
--- a/assets/locale/cs.json
+++ b/assets/locale/cs.json
@@ -50,6 +50,7 @@
"lang_source_last_post": "Poslední příspěvek spatřen",
"lang_source_refresh": "Obnovit tento zdroj",
"lang_sources_leaving_unsaved_prompt": "Na stránce jsou neuložené změny. Opravdu chcete opustit nastavení?",
+ "lang_tag_change_color_button_title": "Změnit barvu",
"lang_no_entries": "Žádný záznam",
"lang_more": "Více",
"lang_login": "Přihlásit",
diff --git a/assets/locale/en.json b/assets/locale/en.json
index 950f719e27..a48618b6df 100644
--- a/assets/locale/en.json
+++ b/assets/locale/en.json
@@ -62,6 +62,7 @@
"lang_source_last_post": "Last post seen",
"lang_source_refresh": "Refresh this source",
"lang_sources_leaving_unsaved_prompt": "Leave settings with unsaved source changes?",
+ "lang_tag_change_color_button_title": "Change color",
"lang_no_entries": "No entries found",
"lang_more": "More",
"lang_login": "Log in",
diff --git a/assets/package-lock.json b/assets/package-lock.json
index ef377870f5..cd616bff30 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -6,6 +6,7 @@
"": {
"dependencies": {
"@fancyapps/fancybox": "^3.1.20",
+ "@floating-ui/react-dom": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^6.0.0",
"@fortawesome/free-regular-svg-icons": "^6.0.0",
@@ -30,7 +31,6 @@
"react-router-dom": "^5.2.0",
"reset-css": "^5.0.1",
"rooks": "^5.0.2",
- "spectrum-colorpicker": "^1.8.1",
"tinykeys": "^1.1.1",
"unreset-css": "^1.0.1",
"use-state-with-deps": "^1.1.1"
@@ -213,6 +213,32 @@
"jquery": ">=1.9.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.0.tgz",
+ "integrity": "sha512-W7+i5Suhhvv97WDTW//KqUA43f/2a4abprM1rWqtLM9lIlJ29tbFI8h232SvqunXon0WmKNEKVjbOsgBhTnbLw=="
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.0.tgz",
+ "integrity": "sha512-PS75dnMg4GdWjDFOiOs15cDzYJpukRNHqQn0ugrBlsrpk2n+y8bwZ24XrsdLSL7kxshmxxr2nTNycLnmRIvV7g==",
+ "dependencies": {
+ "@floating-ui/core": "^0.7.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.0.tgz",
+ "integrity": "sha512-mpYGykTqwtBYT+ZTQQ2OfZ6wXJNuUgmqqD9ooCgbMRgvul6InFOTtWYvtujps439hmOFiVPm4PoBkEEn5imidg==",
+ "dependencies": {
+ "@floating-ui/dom": "^0.5.0",
+ "use-isomorphic-layout-effect": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz",
@@ -6065,11 +6091,6 @@
"specificity": "bin/specificity"
}
},
- "node_modules/spectrum-colorpicker": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/spectrum-colorpicker/-/spectrum-colorpicker-1.8.1.tgz",
- "integrity": "sha512-x1picQ5giVso71ESII7jZ3+ZFdit8WthNkzwJqLNdPDPzrltKUQGpTohWyPfSAID+bK1zGdO6bDbSh1S6GoLYA=="
- },
"node_modules/stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@@ -6628,6 +6649,19 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-state-with-deps": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/use-state-with-deps/-/use-state-with-deps-1.1.1.tgz",
@@ -6892,6 +6926,28 @@
"integrity": "sha512-rcEtu8t+WnmqIDV/Wfm1yvy/nDdwc7YV25j9HLxGC2/WOsUhk9rcWg2nB8g1BrjRt9zaoADdjHTU6ILYTJzBBg==",
"requires": {}
},
+ "@floating-ui/core": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.0.tgz",
+ "integrity": "sha512-W7+i5Suhhvv97WDTW//KqUA43f/2a4abprM1rWqtLM9lIlJ29tbFI8h232SvqunXon0WmKNEKVjbOsgBhTnbLw=="
+ },
+ "@floating-ui/dom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.0.tgz",
+ "integrity": "sha512-PS75dnMg4GdWjDFOiOs15cDzYJpukRNHqQn0ugrBlsrpk2n+y8bwZ24XrsdLSL7kxshmxxr2nTNycLnmRIvV7g==",
+ "requires": {
+ "@floating-ui/core": "^0.7.0"
+ }
+ },
+ "@floating-ui/react-dom": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.0.tgz",
+ "integrity": "sha512-mpYGykTqwtBYT+ZTQQ2OfZ6wXJNuUgmqqD9ooCgbMRgvul6InFOTtWYvtujps439hmOFiVPm4PoBkEEn5imidg==",
+ "requires": {
+ "@floating-ui/dom": "^0.5.0",
+ "use-isomorphic-layout-effect": "^1.1.1"
+ }
+ },
"@fortawesome/fontawesome-common-types": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz",
@@ -11078,11 +11134,6 @@
"integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==",
"dev": true
},
- "spectrum-colorpicker": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/spectrum-colorpicker/-/spectrum-colorpicker-1.8.1.tgz",
- "integrity": "sha512-x1picQ5giVso71ESII7jZ3+ZFdit8WthNkzwJqLNdPDPzrltKUQGpTohWyPfSAID+bK1zGdO6bDbSh1S6GoLYA=="
- },
"stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@@ -11522,6 +11573,12 @@
"punycode": "^2.1.0"
}
},
+ "use-isomorphic-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+ "requires": {}
+ },
"use-state-with-deps": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/use-state-with-deps/-/use-state-with-deps-1.1.1.tgz",
diff --git a/assets/package.json b/assets/package.json
index 3d2a88e943..855317c0f0 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -1,6 +1,7 @@
{
"dependencies": {
"@fancyapps/fancybox": "^3.1.20",
+ "@floating-ui/react-dom": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^6.0.0",
"@fortawesome/free-regular-svg-icons": "^6.0.0",
@@ -25,7 +26,6 @@
"react-router-dom": "^5.2.0",
"reset-css": "^5.0.1",
"rooks": "^5.0.2",
- "spectrum-colorpicker": "^1.8.1",
"tinykeys": "^1.1.1",
"unreset-css": "^1.0.1",
"use-state-with-deps": "^1.1.1"
diff --git a/assets/styles/color-chooser.scss b/assets/styles/color-chooser.scss
new file mode 100644
index 0000000000..a161507e24
--- /dev/null
+++ b/assets/styles/color-chooser.scss
@@ -0,0 +1,48 @@
+.color-chooser-button {
+ display: block;
+
+ /* Span the whole wrapper */
+ height: 100%;
+ width: 100%;
+}
+
+.color {
+ .popup-menu {
+ width: 15em;
+ display: flex;
+ flex-flow: wrap;
+
+ /* Popper does not like margins */
+ margin: 0;
+ }
+
+ .popup-menu-item {
+ width: 1.3em;
+ height: 1.3em;
+ display: flex;
+ margin: 0.2em;
+ border: 1px solid black;
+ padding: 0;
+ border-radius: 0.2em;
+ flex-shrink: 0; /* preserve aspect ratio */
+
+ svg {
+ margin: auto;
+ display: block;
+ }
+ }
+}
+
+@media screen and (max-width: 1024px) {
+ .color {
+ .popup-menu {
+ max-width: 26em;
+ width: 100%;
+ }
+
+ .popup-menu-item {
+ width: 2em;
+ height: 2em;
+ }
+ }
+}
diff --git a/assets/styles/main.scss b/assets/styles/main.scss
index 0b40ba6f17..993422c216 100644
--- a/assets/styles/main.scss
+++ b/assets/styles/main.scss
@@ -1,8 +1,8 @@
@use 'sass:color';
@import 'npm:@fancyapps/fancybox/dist/jquery.fancybox.css';
-@import 'npm:spectrum-colorpicker/spectrum.css';
@import 'npm:reset-css/sass/reset';
+@import 'mixins/visually-hidden';
/* base */
@@ -22,6 +22,8 @@ $text-color: black;
--background-color: white;
}
+@import 'color-chooser';
+
html,
body {
height: 100%;
@@ -61,6 +63,10 @@ button::-moz-focus-inner {
@import 'popup-menu';
+.visually-hidden {
+ @include visually-hidden;
+}
+
#js-loading-message {
margin: 1em;
text-align: center;
diff --git a/assets/styles/mixins/visually-hidden.scss b/assets/styles/mixins/visually-hidden.scss
new file mode 100644
index 0000000000..2f6b91d044
--- /dev/null
+++ b/assets/styles/mixins/visually-hidden.scss
@@ -0,0 +1,18 @@
+// stylelint-disable declaration-no-important
+
+// Hide content visually while keeping it accessible to assistive technologies
+//
+// See: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/
+// See: https://kittygiraudel.com/2016/10/13/css-hide-and-seek/
+
+@mixin visually-hidden() {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important; // Fix for https://github.com/twbs/bootstrap/issues/25686
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}