]> source.dussan.org Git - gitea.git/commitdiff
Replace coloris with vanilla-colorful (#30201)
authorsilverwind <me@silverwind.io>
Wed, 3 Apr 2024 09:15:06 +0000 (11:15 +0200)
committerGitHub <noreply@github.com>
Wed, 3 Apr 2024 09:15:06 +0000 (09:15 +0000)
Found [a better color
picker](https://github.com/web-padawan/vanilla-colorful) that [does not
rely](https://github.com/mdbassit/Coloris/issues/139) on
`querySelectorAll` or a global shared instance, and is also around a
third of the size of the previous one.

The popover is handled by tippy.js for which I introduced a new "bare"
theme and it uses a new sibling-based mechanism which should prove
useful later to create tippy popovers via HTML only.

<img width="846" alt="Screenshot 2024-03-31 at 04 03 38"
src="https://github.com/go-gitea/gitea/assets/115237/7639b911-a2d7-4f5c-bffd-a9d84561e747">

package-lock.json
package.json
web_src/css/features/colorpicker.css
web_src/css/modules/tippy.css
web_src/js/features/colorpicker.js
web_src/js/modules/tippy.js

index 21de79387f7f50ac4e1637e79ff4e3802ce40fa1..a5f7a09ed0868a1bdb3f697f78a863b76a33d7b6 100644 (file)
@@ -13,7 +13,6 @@
         "@github/relative-time-element": "4.4.0",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-        "@melloware/coloris": "0.23.0",
         "@primer/octicons": "19.9.0",
         "add-asset-webpack-plugin": "2.0.1",
         "ansi_up": "6.0.2",
@@ -54,6 +53,7 @@
         "toastify-js": "1.12.0",
         "tributejs": "5.1.3",
         "uint8-to-base64": "0.2.0",
+        "vanilla-colorful": "0.7.2",
         "vue": "3.4.21",
         "vue-bar-graph": "2.0.0",
         "vue-chartjs": "5.3.0",
         "@mcaptcha/core-glue": "^0.1.0-alpha-5"
       }
     },
-    "node_modules/@melloware/coloris": {
-      "version": "0.23.0",
-      "resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
-      "integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
-    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
         "builtins": "^1.0.3"
       }
     },
+    "node_modules/vanilla-colorful": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz",
+      "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
+    },
     "node_modules/vite": {
       "version": "5.2.6",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
index beea0e5d868cfd237b32408c99f12aa18e81921d..004ac9e2bf6483057c7be208ac9632c1506e7d90 100644 (file)
@@ -12,7 +12,6 @@
     "@github/relative-time-element": "4.4.0",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-    "@melloware/coloris": "0.23.0",
     "@primer/octicons": "19.9.0",
     "add-asset-webpack-plugin": "2.0.1",
     "ansi_up": "6.0.2",
@@ -53,6 +52,7 @@
     "toastify-js": "1.12.0",
     "tributejs": "5.1.3",
     "uint8-to-base64": "0.2.0",
+    "vanilla-colorful": "0.7.2",
     "vue": "3.4.21",
     "vue-bar-graph": "2.0.0",
     "vue-chartjs": "5.3.0",
index 0c651cfeb3b3ea72db95b6982327fa9a80251a31..b7436783df95eaf07ce8128e84fbf89701f6ad0a 100644 (file)
@@ -1,10 +1,6 @@
-/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
-   opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
-   based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
-
 .js-color-picker-input {
   display: flex;
-  flex-wrap: wrap;
+  position: relative;
 }
 
 .js-color-picker-input input {
   padding-left: 32px !important;
 }
 
-.clr-picker {
-  display: none;
-  flex-wrap: wrap;
-  position: absolute;
-  width: 200px;
-  z-index: 1002; /* above .ui.modal which has 1001 */
-  border-radius: var(--border-radius);
-  background-color: var(--color-menu);
-  justify-content: flex-end;
-  direction: ltr;
-  box-shadow: 0 5px 20px var(--color-shadow);
-  user-select: none;
-}
-
-.clr-picker.clr-open {
-  display: flex;
-}
-
-.clr-gradient {
-  position: relative;
-  width: 100%;
-  height: 100px;
-  border-radius: 3px 3px 0 0;
-  background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
-  cursor: pointer;
-}
-
-.clr-marker {
-  position: absolute;
-  width: 12px;
-  height: 12px;
-  margin: -6px 0 0 -6px;
-  border: 1px solid var(--color-white);
-  border-radius: 50%;
-  background-color: currentcolor;
-  cursor: pointer;
-}
-
-.clr-picker input[type="range"]::-webkit-slider-runnable-track {
-  width: 100%;
-  height: 16px;
-}
-
-.clr-picker input[type="range"]::-webkit-slider-thumb {
-  width: 16px;
-  height: 16px;
-  -webkit-appearance: none;
-}
-
-.clr-picker input[type="range"]::-moz-range-track {
-  width: 100%;
-  height: 16px;
-  border: 0;
-}
-
-.clr-picker input[type="range"]::-moz-range-thumb {
-  width: 16px;
-  height: 16px;
-  border: 0;
-}
-
-.clr-hue {
-  background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
-  position: relative;
-  width: calc(100% - 40px);
-  height: 10px;
-  margin: 10px 20px;
-  border-radius: 4px;
-}
-
-.clr-hue input[type="range"] {
-  position: absolute;
-  width: calc(100% + 32px);
-  margin: 0;
-  background-color: transparent;
-  opacity: 0;
-  cursor: pointer;
-  appearance: none;
-}
-
-.clr-hue div {
-  position: absolute;
-  width: 16px;
-  height: 16px;
-  left: 0;
-  top: 50%;
-  transform: translate(-50%, -50%);
-  border: 2px solid var(--color-white);
-  border-radius: 50%;
-  background-color: currentcolor;
-  box-shadow: 0 0 1px var(--color-shadow);
-  pointer-events: none;
-}
-
-.clr-field {
-  flex: 1;
-  position: relative;
-  color: transparent;
-}
-
-.clr-field button {
+.js-color-picker-input .preview-square {
   position: absolute;
   aspect-ratio: 1;
   height: 16px;
   left: 10px;
   top: 50%;
   transform: translateY(-50%);
-  margin: 0;
-  padding: 0;
-  border: 0;
-  color: inherit;
-  pointer-events: none;
   border-radius: 2px;
   background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
   background-position: 0 0, 4px 4px;
   background-size: 8px 8px;
 }
 
-.clr-field button::after {
+.js-color-picker-input .preview-square::after {
   content: "";
-  display: block;
   position: absolute;
   width: 100%;
   height: 100%;
-  left: 0;
-  top: 0;
   border-radius: inherit;
   background-color: currentcolor;
 }
 
-.clr-marker:focus {
-  outline: none;
+hex-color-picker {
+  width: 180px;
+  height: 120px;
 }
 
-.clr-keyboard-nav .clr-marker:focus,
-.clr-keyboard-nav .clr-hue input:focus + div,
-.clr-keyboard-nav .clr-alpha input:focus + div {
-  outline: none;
-  box-shadow: 0 0 2px 2px var(--color-white);
+hex-color-picker::part(hue-pointer),
+hex-color-picker::part(saturation-pointer) {
+  width: 22px;
+  height: 22px;
 }
 
-.clr-picker .clr-preview,
-.clr-picker .clr-clear,
-.clr-picker .clr-swatches,
-.clr-picker .clr-format,
-.clr-picker .clr-alpha,
-.clr-picker .clr-color {
-  display: none;
+hex-color-picker::part(hue) {
+  flex-basis: 16px;
 }
index 76d36b42930f343512b51cb4082497d356817a21..6ac7c37d934248ed1edd0a7471d390b1a690023d 100644 (file)
   z-index: 1;
 }
 
+/* bare theme, no styling at all, except box-shadow */
+.tippy-box[data-theme="bare"] {
+  border: none;
+  box-shadow: 0 6px 18px var(--color-shadow);
+}
+
+.tippy-box[data-theme="bare"] .tippy-content {
+  padding: 0;
+  background: transparent;
+}
+
 /* tooltip theme for text tooltips */
 
 .tippy-box[data-theme="tooltip"] {
index f342598e664a5814055a4c73fe3dc2f9d9a8e903..6d00d908c967822bea44a52109a5a0d5b8f78d27 100644 (file)
@@ -1,31 +1,66 @@
-export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
-  const inputEls = document.querySelectorAll(selector);
-  if (!inputEls.length) return;
+import {createTippy} from '../modules/tippy.js';
 
-  const [{coloris, init}] = await Promise.all([
-    import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
+export async function initColorPickers() {
+  const els = document.getElementsByClassName('js-color-picker-input');
+  if (!els.length) return;
+
+  await Promise.all([
+    import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
     import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
   ]);
 
-  init();
-  coloris({
-    el: selector,
-    alpha: false,
-    focusInput: true,
-    selectInput: false,
-    ...opts,
+  for (const el of els) {
+    initPicker(el);
+  }
+}
+
+function updateSquare(el, newValue) {
+  el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
+}
+
+function updatePicker(el, newValue) {
+  el.setAttribute('color', newValue);
+}
+
+function initPicker(el) {
+  const input = el.querySelector('input');
+
+  const square = document.createElement('div');
+  square.classList.add('preview-square');
+  updateSquare(square, input.value);
+  el.append(square);
+
+  const picker = document.createElement('hex-color-picker');
+  picker.addEventListener('color-changed', (e) => {
+    input.value = e.detail.value;
+    input.focus();
+    updateSquare(square, e.detail.value);
+  });
+
+  input.addEventListener('input', (e) => {
+    updateSquare(square, e.target.value);
+    updatePicker(picker, e.target.value);
+  });
+
+  createTippy(input, {
+    trigger: 'focus click',
+    theme: 'bare',
+    hideOnClick: true,
+    content: picker,
+    placement: 'bottom-start',
+    interactive: true,
+    onShow() {
+      updatePicker(picker, input.value);
+    },
   });
 
-  for (const inputEl of inputEls) {
-    const parent = inputEl.closest('.js-color-picker-input');
-    // prevent tabbing on the color preview `button` inside the input
-    parent.querySelector('button').tabIndex = -1;
-    // init precolors
-    for (const el of parent.querySelectorAll('.precolors .color')) {
-      el.addEventListener('click', (e) => {
-        inputEl.value = e.target.getAttribute('data-color-hex');
-        inputEl.dispatchEvent(new Event('input', {bubbles: true}));
-      });
-    }
+  // init precolors
+  for (const colorEl of el.querySelectorAll('.precolors .color')) {
+    colorEl.addEventListener('click', (e) => {
+      const newValue = e.target.getAttribute('data-color-hex');
+      input.value = newValue;
+      input.dispatchEvent(new Event('input', {bubbles: true}));
+      updateSquare(square, newValue);
+    });
   }
 }
index 220c9e551213d155426781909571b8bc2864d1a3..83b28e57454d60526a1ad14b5df96f7862f1cbd9 100644 (file)
@@ -3,11 +3,12 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
 import {formatDatetime} from '../utils/time.js';
 
 const visibleInstances = new Set();
+const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
 
 export function createTippy(target, opts = {}) {
   // the callback functions should be destructured from opts,
   // because we should use our own wrapper functions to handle them, do not let the user override them
-  const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
+  const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
 
   const instance = tippy(target, {
     appendTo: document.body,
@@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
       visibleInstances.add(instance);
       return onShow?.(instance);
     },
-    arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`,
+    arrow: arrow || (theme === 'bare' ? false : arrowSvg),
     role: role || 'menu', // HTML role attribute
-    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
+    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
     plugins: [followCursor],
     ...other,
   });