From 5692ba944dd6332e496dc66d08de0a82338f1da9 Mon Sep 17 00:00:00 2001 From: zzxming Date: Fri, 6 Dec 2024 14:46:17 +0800 Subject: [PATCH 1/6] feat: tooltip add click display --- src/utils/components/tooltip.ts | 63 +++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/utils/components/tooltip.ts b/src/utils/components/tooltip.ts index 7355547..1595f49 100644 --- a/src/utils/components/tooltip.ts +++ b/src/utils/components/tooltip.ts @@ -26,6 +26,7 @@ interface ToolTipOptions { msg?: string; delay?: number; content?: HTMLElement; + type?: 'hover' | 'click'; } const DISTANCE = 4; let tooltipContainer: HTMLElement; @@ -33,7 +34,7 @@ export interface TooltipInstance { destroy: () => void; }; export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}): TooltipInstance | null => { - const { msg = '', delay = 150, content, direction = 'bottom' } = options; + const { msg = '', delay = 150, content, direction = 'bottom', type = 'hover' } = options; const bem = createBEM('tooltip'); if (msg || content) { if (!tooltipContainer) { @@ -89,18 +90,60 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) }, delay); }; - const eventListeners = [target, tooltip]; + const hoverDisplay = () => { + const eventListeners = [target, tooltip]; + const show = () => { + for (const listener of eventListeners) { + listener.addEventListener('mouseenter', open); + listener.addEventListener('mouseleave', close); + } + }; + const hide = () => { + for (const listener of eventListeners) { + listener.removeEventListener('mouseenter', open); + listener.removeEventListener('mouseleave', close); + } + }; + return { + show, + hide, + destroy: () => {}, + }; + }; + const stopPropagation = (e: Event) => e.stopPropagation(); + const clickDisplay = () => { + // eslint-disable-next-line unicorn/consistent-function-scoping + const show = (e: MouseEvent) => { + stopPropagation(e); + open(); + document.removeEventListener('click', close); + document.addEventListener('click', close, { once: true }); + }; + return { + show: () => { + tooltip.addEventListener('click', stopPropagation); + target.addEventListener('click', show); + }, + hide: () => { + document.removeEventListener('click', close); + }, + destroy: () => { + tooltip.removeEventListener('click', stopPropagation); + target.removeEventListener('click', show); + }, + }; + }; + const displayMethods = { + hover: hoverDisplay, + click: clickDisplay, + }; - for (const listener of eventListeners) { - listener.addEventListener('mouseenter', open); - listener.addEventListener('mouseleave', close); - } + const { show, hide, destroy: destroyDisplay } = displayMethods[type](); + show(); const destroy = () => { - for (const listener of eventListeners) { - listener.removeEventListener('mouseenter', open); - listener.removeEventListener('mouseleave', close); - } + hide(); + destroyDisplay(); if (cleanup) cleanup(); tooltip.remove(); }; From 62b65081787fe0e3d2fd59cc0cc683a8d49f2771 Mon Sep 17 00:00:00 2001 From: zzxming Date: Fri, 6 Dec 2024 16:49:57 +0800 Subject: [PATCH 2/6] refactor: remove transitionend event compatible --- src/modules/table-scrollbar.ts | 6 +++--- src/utils/components/tooltip.ts | 3 +-- src/utils/utils.ts | 9 --------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/modules/table-scrollbar.ts b/src/modules/table-scrollbar.ts index 773c77e..31a70cd 100644 --- a/src/modules/table-scrollbar.ts +++ b/src/modules/table-scrollbar.ts @@ -1,7 +1,7 @@ import type TableUp from '..'; import type { TableMainFormat } from '../formats'; import Quill from 'quill'; -import { addScrollEvent, clearScrollEvent, debounce, handleIfTransitionend } from '../utils'; +import { addScrollEvent, clearScrollEvent, debounce } from '../utils'; export class Scrollbar { minSize: number = 20; @@ -184,7 +184,7 @@ export class Scrollbar { showScrollbar = debounce(() => { this.cursorLeave = false; this.scrollbar.classList.remove('transparent'); - handleIfTransitionend(this.scrollbar, 150, () => { + this.scrollbar.addEventListener('transitionend', () => { this.scrollbar.style.display = (this.isVertical ? this.sizeHeight : this.sizeWidth) ? 'block' : 'none'; }); }, 200); @@ -193,7 +193,7 @@ export class Scrollbar { hideScrollbar = debounce(() => { this.cursorLeave = true; this.scrollbar.classList.add('transparent'); - handleIfTransitionend(this.scrollbar, 150, () => { + this.scrollbar.addEventListener('transitionend', () => { this.scrollbar.style.display = this.cursorDown && (this.isVertical ? this.sizeHeight : this.sizeWidth) ? 'block' : 'none'; }); }, 200); diff --git a/src/utils/components/tooltip.ts b/src/utils/components/tooltip.ts index 1595f49..0a40ea4 100644 --- a/src/utils/components/tooltip.ts +++ b/src/utils/components/tooltip.ts @@ -7,7 +7,6 @@ import { shift, } from '@floating-ui/dom'; import { createBEM } from '../bem'; -import { handleIfTransitionend } from '../utils'; interface ToolTipOptions { direction?: @@ -85,8 +84,8 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) const close = () => { if (timer) clearTimeout(timer); timer = setTimeout(() => { + tooltip.addEventListener('transitionend', transitionendHandler, { once: true }); tooltip.classList.add('transparent'); - handleIfTransitionend(tooltip, 150, transitionendHandler, { once: true }); }, delay); }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 797a023..35d9570 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -137,12 +137,3 @@ export function clearScrollEvent(this: ScrollHandle) { } this.scrollHandler = []; } - -export const handleIfTransitionend = (domNode: HTMLElement, duration: number, handler: () => void, options?: boolean | AddEventListenerOptions, lastTimer?: ReturnType): ReturnType => { - if (lastTimer) clearTimeout(lastTimer); - domNode.addEventListener('transitionend', handler, options); - // handle remove when transition set none - return setTimeout(() => { - handler(); - }, duration); -}; From 65f54a2740e63543a9b77f97a8bdfaeb71c7cfa5 Mon Sep 17 00:00:00 2001 From: zzxming Date: Fri, 6 Dec 2024 17:33:13 +0800 Subject: [PATCH 3/6] style: no need prefer function scoping --- eslint.config.js | 1 + src/modules/table-scrollbar.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index c1cf91f..1342ea8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,6 +8,7 @@ export default factory({ { rules: { 'unicorn/prefer-dom-node-dataset': 'off', + 'unicorn/consistent-function-scoping': 'off', 'no-cond-assign': 'off', 'new-cap': 'off', }, diff --git a/src/modules/table-scrollbar.ts b/src/modules/table-scrollbar.ts index 31a70cd..5982f6e 100644 --- a/src/modules/table-scrollbar.ts +++ b/src/modules/table-scrollbar.ts @@ -115,7 +115,6 @@ export class Scrollbar { }); this.thumb.classList.add('ql-table-scrollbar-thumb'); - // eslint-disable-next-line unicorn/consistent-function-scoping const mouseMoveDocumentHandler = (e: MouseEvent) => { if (this.cursorDown === false) return; const prevPage = this.thumbState[this.propertyMap.axis]; @@ -180,7 +179,6 @@ export class Scrollbar { }); } - // eslint-disable-next-line unicorn/consistent-function-scoping showScrollbar = debounce(() => { this.cursorLeave = false; this.scrollbar.classList.remove('transparent'); @@ -189,7 +187,6 @@ export class Scrollbar { }); }, 200); - // eslint-disable-next-line unicorn/consistent-function-scoping hideScrollbar = debounce(() => { this.cursorLeave = true; this.scrollbar.classList.add('transparent'); From c6466ee52fa37f9b61fa3bdeb74d8f10d3943f04 Mon Sep 17 00:00:00 2001 From: zzxming Date: Fri, 6 Dec 2024 18:00:57 +0800 Subject: [PATCH 4/6] feat: custom color picker --- src/modules/table-menu/table-menu-common.ts | 70 +++++---- .../table-menu/table-menu-contextmenu.ts | 6 +- src/style/color-picker.less | 67 +++++++++ src/style/index.less | 13 ++ src/utils/color.ts | 135 +++++++++++++++++ src/utils/components/color-picker.ts | 136 ++++++++++++++++++ src/utils/components/index.ts | 1 + src/utils/components/tooltip.ts | 98 ++++++++----- 8 files changed, 465 insertions(+), 61 deletions(-) create mode 100644 src/style/color-picker.less create mode 100644 src/utils/color.ts create mode 100644 src/utils/components/color-picker.ts diff --git a/src/modules/table-menu/table-menu-common.ts b/src/modules/table-menu/table-menu-common.ts index 96d734d..ae99800 100644 --- a/src/modules/table-menu/table-menu-common.ts +++ b/src/modules/table-menu/table-menu-common.ts @@ -1,7 +1,7 @@ import type Quill from 'quill'; import type { TableUp } from '../..'; -import type { TableMenuOptions, ToolOption, TooltipInstance } from '../../utils'; -import { createTooltip, debounce, defaultColorMap, isArray, isFunction, randomId } from '../../utils'; +import type { TableMenuOptions, ToolOption, TooltipInstance, ToolTipOptions } from '../../utils'; +import { createColorPicker, createTooltip, debounce, defaultColorMap, isArray, isFunction, randomId } from '../../utils'; import { colorClassName, defaultTools, maxSaveColorCount, menuColorSelectClassName, usedColors } from './constants'; export type TableMenuOptionsInput = Partial>; @@ -10,6 +10,10 @@ export class TableMenuCommon { menu: HTMLElement | null = null; updateUsedColor: (this: any, color?: string) => void; colorItemClass = `color-${randomId()}`; + colorChooseTooltipOption: ToolTipOptions = { + direction: 'top', + }; + tooltipItem: TooltipInstance[] = []; constructor(public tableModule: TableUp, public quill: Quill, options: TableMenuOptionsInput) { @@ -92,11 +96,9 @@ export class TableMenuCommon { } item.appendChild(iconDom); - // color choose handler will trigger when the color input event if (isColorChoose && attrKey) { - const colorSelectWrapper = this.createColorChoose({ name, icon, handle, isColorChoose, key: attrKey, tip }); - const tooltipItem = createTooltip(item, { content: colorSelectWrapper, direction: 'top' }); - tooltipItem && this.tooltipItem.push(tooltipItem); + const tooltipItem = this.createColorChoose(item, { name, icon, handle, isColorChoose, key: attrKey, tip }); + this.tooltipItem.push(tooltipItem); item.classList.add(menuColorSelectClassName); } else { @@ -117,7 +119,7 @@ export class TableMenuCommon { return toolBox; }; - createColorChoose({ handle, key }: ToolOption) { + createColorChoose(item: HTMLElement, { handle, key }: ToolOption) { const colorSelectWrapper = document.createElement('div'); colorSelectWrapper.classList.add(colorClassName.selectWrapper); @@ -155,30 +157,25 @@ export class TableMenuCommon { clearColor.addEventListener('click', () => { handle(this.tableModule, this.getSelectedTds(), null); }); - const label = document.createElement('label'); - label.classList.add(colorClassName.btn, 'table-color-custom'); - const customColor = document.createElement('span'); + const customColor = document.createElement('div'); + customColor.classList.add(colorClassName.btn, 'table-color-custom'); customColor.textContent = this.tableModule.options.texts.custom; - const input = document.createElement('input'); - input.type = 'color'; - Object.assign(input.style, { - width: 0, - height: 0, - padding: 0, - border: 0, - outline: 'none', - opacity: 0, + const colorPicker = createColorPicker({ + onChange: (color) => { + handle(this.tableModule, this.getSelectedTds(), color); + this.updateUsedColor(color); + }, }); - input.addEventListener('input', () => { - handle(this.tableModule, this.getSelectedTds(), input.value); - this.updateUsedColor(input.value); - }, false); - label.appendChild(customColor); - label.appendChild(input); + const { hide: hideColorPicker, destroy: destroyColorPicker } = createTooltip(customColor, { + direction: 'right', + type: 'click', + content: colorPicker, + container: customColor, + })!; colorMapRow.appendChild(transparentColor); colorMapRow.appendChild(clearColor); - colorMapRow.appendChild(label); + colorMapRow.appendChild(customColor); colorSelectWrapper.appendChild(colorMapRow); if (usedColors.size > 0) { @@ -194,6 +191,8 @@ export class TableMenuCommon { } colorSelectWrapper.addEventListener('click', (e) => { + e.stopPropagation(); + hideColorPicker(); const item = e.target as HTMLElement; const color = item.style.backgroundColor; const selectedTds = this.getSelectedTds(); @@ -203,7 +202,21 @@ export class TableMenuCommon { this.updateUsedColor(color); } }); - return colorSelectWrapper; + + return createTooltip(item, { + content: colorSelectWrapper, + onClose(force) { + const isChild = colorSelectWrapper.contains(colorPicker); + if (force && isChild) { + hideColorPicker(); + } + return isChild; + }, + onDestroy() { + destroyColorPicker(); + }, + ...this.colorChooseTooltipOption, + })!; } getSelectedTds() { @@ -222,6 +235,9 @@ export class TableMenuCommon { hideTools() { this.menu && Object.assign(this.menu.style, { display: 'none' }); + for (const tooltip of this.tooltipItem) { + tooltip.hide(true); + } } destroy() { diff --git a/src/modules/table-menu/table-menu-contextmenu.ts b/src/modules/table-menu/table-menu-contextmenu.ts index 44d2493..ccf9213 100644 --- a/src/modules/table-menu/table-menu-contextmenu.ts +++ b/src/modules/table-menu/table-menu-contextmenu.ts @@ -1,12 +1,16 @@ import type Quill from 'quill'; import type TableUp from '../..'; -import type { TableMenuOptions } from '../../utils'; +import type { TableMenuOptions, ToolTipOptions } from '../../utils'; import { limitDomInViewPort } from '../../utils'; import { contextmenuClassName, menuColorSelectClassName } from './constants'; import { TableMenuCommon } from './table-menu-common'; type TableMenuOptionsInput = Partial>; export class TableMenuContextmenu extends TableMenuCommon { + colorChooseTooltipOption: ToolTipOptions = { + direction: 'right', + }; + constructor(public tableModule: TableUp, public quill: Quill, options: TableMenuOptionsInput) { super(tableModule, quill, options); diff --git a/src/style/color-picker.less b/src/style/color-picker.less new file mode 100644 index 0000000..4619f66 --- /dev/null +++ b/src/style/color-picker.less @@ -0,0 +1,67 @@ +@import './variables.less'; + +.@{namespace}-color-picker { + &__preview { + width: 20px; + height: 20px; + cursor: pointer; + border-radius: 6px; + } + + &__content { + .setCssVar(color-picker-bg-color, #ffffff); + + box-sizing: border-box; + background: .getCssVar(color-picker-bg-color) []; + border-radius: 6px; + box-shadow: 0 0 6px #b2b5b8; + width: 197px; + height: 166px; + padding: 8px; + } + + &__selector { + width: 150px; + height: 150px; + position: absolute; + } + + &__background { + width: 100%; + height: 100%; + background: linear-gradient(to top, #000 0%, rgba(0, 0, 0, 0) 100%), + linear-gradient(to right, #fff 0%, rgba(255, 255, 255, 0) 100%); + &-handle { + box-sizing: border-box; + position: absolute; + top: 0px; + left: 150px; + border-radius: 100%; + width: 10px; + height: 10px; + border: 1px solid #ffffff; + cursor: pointer; + transform: translate(-5px, -5px); + opacity: 0.85; + } + } + + &__hue { + width: 17px; + height: 150px; + margin-left: 164px; + position: absolute; + opacity: 0.85; + background: linear-gradient(0deg, red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red); + &-handle { + box-sizing: border-box; + position: absolute; + width: 21px; + transform: translate(-2px, -5px); + height: 10px; + border: 2px solid #ffffff; + opacity: 0.85; + cursor: pointer; + } + } +} diff --git a/src/style/index.less b/src/style/index.less index c244f42..7b7d3ec 100644 --- a/src/style/index.less +++ b/src/style/index.less @@ -425,3 +425,16 @@ @import './select-box.less'; @import './tooltip.less'; @import './dialog.less'; +@import './color-picker.less'; + +.@{namespace}-tooltip { + .@{namespace}-color-picker { + &__content { + .setCssVar(color-picker-bg-color, transparent); + + width: 181px; + padding: 8px 0px; + box-shadow: none; + } + } +} diff --git a/src/utils/color.ts b/src/utils/color.ts new file mode 100644 index 0000000..930b126 --- /dev/null +++ b/src/utils/color.ts @@ -0,0 +1,135 @@ +export interface HSB { + h: number; + s: number; + b: number; +}; +export interface RGB { + r: number; + g: number; + b: number; +}; +export function validateHSB(hsb: HSB) { + return { + h: Math.min(360, Math.max(0, hsb.h)), + s: Math.min(100, Math.max(0, hsb.s)), + b: Math.min(100, Math.max(0, hsb.b)), + }; +} +export function HEXtoRGB(hex: string) { + const hexValue = Number.parseInt(hex.includes('#') ? hex.slice(1) : hex, 16); + return { r: hexValue >> 16, g: (hexValue & 0x00_FF_00) >> 8, b: hexValue & 0x00_00_FF }; +} +export function RGBtoHSB(rgb: RGB) { + const hsb = { + h: 0, + s: 0, + b: 0, + }; + const min = Math.min(rgb.r, rgb.g, rgb.b); + const max = Math.max(rgb.r, rgb.g, rgb.b); + const delta = max - min; + + hsb.b = max; + hsb.s = max !== 0 ? (255 * delta) / max : 0; + + if (hsb.s !== 0) { + if (rgb.r === max) { + hsb.h = (rgb.g - rgb.b) / delta; + } + else if (rgb.g === max) { + hsb.h = 2 + (rgb.b - rgb.r) / delta; + } + else { + hsb.h = 4 + (rgb.r - rgb.g) / delta; + } + } + else { + hsb.h = -1; + } + + hsb.h *= 60; + + if (hsb.h < 0) { + hsb.h += 360; + } + + hsb.s *= 100 / 255; + hsb.b *= 100 / 255; + + return hsb; +} +export function HSBtoRGB(hsb: HSB) { + let rgb: RGB = { + r: 0, + g: 0, + b: 0, + }; + let h = Math.round(hsb.h); + const s = Math.round((hsb.s * 255) / 100); + const v = Math.round((hsb.b * 255) / 100); + + if (s === 0) { + rgb = { + r: v, + g: v, + b: v, + }; + } + else { + const t1 = v; + const t2 = ((255 - s) * v) / 255; + const t3 = ((t1 - t2) * (h % 60)) / 60; + + if (h === 360) h = 0; + + if (h < 60) { + rgb.r = t1; + rgb.b = t2; + rgb.g = t2 + t3; + } + else if (h < 120) { + rgb.g = t1; + rgb.b = t2; + rgb.r = t1 - t3; + } + else if (h < 180) { + rgb.g = t1; + rgb.r = t2; + rgb.b = t2 + t3; + } + else if (h < 240) { + rgb.b = t1; + rgb.r = t2; + rgb.g = t1 - t3; + } + else if (h < 300) { + rgb.b = t1; + rgb.g = t2; + rgb.r = t2 + t3; + } + else if (h < 360) { + rgb.r = t1; + rgb.g = t2; + rgb.b = t1 - t3; + } + else { + rgb.r = 0; + rgb.g = 0; + rgb.b = 0; + } + } + + return { r: Math.round(rgb.r), g: Math.round(rgb.g), b: Math.round(rgb.b) }; +} +export function RGBtoHEX(rgb: RGB) { + const hex = [rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16)]; + for (const key in hex) { + if (hex[key].length === 1) { + hex[key] = `0${hex[key]}`; + } + } + return hex.join(''); +}; +export function HSBtoHEX(hsb: HSB) { + return RGBtoHEX(HSBtoRGB(hsb)); +} diff --git a/src/utils/components/color-picker.ts b/src/utils/components/color-picker.ts new file mode 100644 index 0000000..8d3f4e2 --- /dev/null +++ b/src/utils/components/color-picker.ts @@ -0,0 +1,136 @@ +import type { HSB } from '../color'; +import { createBEM } from '../bem'; +import { HEXtoRGB, HSBtoHEX, HSBtoRGB, RGBtoHEX, RGBtoHSB, validateHSB } from '../color'; + +interface ColorPickerOptions { + color: string; + onChange: (color: string) => void; +}; +export const createColorPicker = (options: Partial = {}) => { + let hsbValue: HSB = RGBtoHSB(HEXtoRGB(options.color || '#ff0000')); + const bem = createBEM('color-picker'); + const root = document.createElement('div'); + root.classList.add(bem.b()); + + const content = document.createElement('div'); + content.classList.add(bem.be('content')); + root.appendChild(content); + + const colorSelector = document.createElement('div'); + colorSelector.classList.add(bem.be('selector')); + + const colorBackground = document.createElement('div'); + colorBackground.classList.add(bem.be('background')); + + const colorHandle = document.createElement('div'); + colorHandle.classList.add(bem.be('background-handle')); + + colorBackground.appendChild(colorHandle); + colorSelector.appendChild(colorBackground); + content.appendChild(colorSelector); + + const colorHue = document.createElement('div'); + colorHue.classList.add(bem.be('hue')); + + const colorHueHandle = document.createElement('div'); + colorHueHandle.classList.add(bem.be('hue-handle')); + + colorHue.appendChild(colorHueHandle); + content.appendChild(colorHue); + + let colorDragging = false; + let hueDragging = false; + function onDrag(event: MouseEvent) { + if (colorDragging) { + pickColor(event); + event.preventDefault(); + } + + if (hueDragging) { + pickHue(event); + event.preventDefault(); + } + } + function onColorSelectorDragEnd() { + document.removeEventListener('mousemove', onDrag); + document.removeEventListener('mouseup', onColorSelectorDragEnd); + colorDragging = false; + } + function onColorSelectorMousedown(e: MouseEvent) { + document.addEventListener('mousemove', onDrag); + document.addEventListener('mouseup', onColorSelectorDragEnd); + colorDragging = true; + pickColor(e); + } + colorSelector.addEventListener('mousedown', onColorSelectorMousedown); + function updateColorHandle() { + Object.assign(colorHandle.style, { + left: `${Math.floor((150 * hsbValue.s) / 100)}px`, + top: `${Math.floor((150 * (100 - hsbValue.b)) / 100)}px`, + }); + } + function pickColor(event: MouseEvent) { + event.preventDefault(); + const rect = colorSelector.getBoundingClientRect(); + const top = rect.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0); + const left = rect.left + document.body.scrollLeft; + const saturation = Math.floor((100 * Math.max(0, Math.min(150, event.pageX - left))) / 150); + const brightness = Math.floor((100 * (150 - Math.max(0, Math.min(150, event.pageY - top)))) / 150); + + hsbValue = validateHSB({ + h: hsbValue.h, + s: saturation, + b: brightness, + }); + + updateColorHandle(); + if (options.onChange) { + options.onChange(`#${HSBtoHEX(hsbValue)}`); + } + } + + function onColorHueDragEnd() { + document.removeEventListener('mousemove', onDrag); + document.removeEventListener('mouseup', onColorHueDragEnd); + hueDragging = false; + } + function onColorHueMousedown(event: MouseEvent) { + document.addEventListener('mousemove', onDrag); + document.addEventListener('mouseup', onColorHueDragEnd); + hueDragging = true; + pickHue(event); + } + colorHue.addEventListener('mousedown', onColorHueMousedown); + function updateHue() { + colorHueHandle.style.top = `${Math.floor(150 - (150 * hsbValue.h) / 360)}px`; + } + function updateColorSelector() { + colorSelector.style.backgroundColor = `#${RGBtoHEX(HSBtoRGB({ + h: hsbValue.h, + s: 100, + b: 100, + }))}`; + } + function pickHue(event: MouseEvent) { + event.preventDefault(); + const top = colorHue.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0); + + hsbValue = validateHSB({ + h: Math.floor((360 * (150 - Math.max(0, Math.min(150, event.pageY - top)))) / 150), + s: hsbValue.s, + b: hsbValue.b, + }); + + updateColorSelector(); + updateHue(); + + if (options.onChange) { + options.onChange(`#${HSBtoHEX(hsbValue)}`); + } + } + + updateColorHandle(); + updateColorSelector(); + updateHue(); + return root; +}; diff --git a/src/utils/components/index.ts b/src/utils/components/index.ts index c10570f..adcdb12 100644 --- a/src/utils/components/index.ts +++ b/src/utils/components/index.ts @@ -1,4 +1,5 @@ export * from './button'; +export * from './color-picker'; export * from './dialog'; export * from './input'; export * from './table'; diff --git a/src/utils/components/tooltip.ts b/src/utils/components/tooltip.ts index 0a40ea4..a705845 100644 --- a/src/utils/components/tooltip.ts +++ b/src/utils/components/tooltip.ts @@ -8,7 +8,7 @@ import { } from '@floating-ui/dom'; import { createBEM } from '../bem'; -interface ToolTipOptions { +export interface ToolTipOptions { direction?: | 'top' | 'top-start' @@ -25,21 +25,29 @@ interface ToolTipOptions { msg?: string; delay?: number; content?: HTMLElement; + container?: HTMLElement; type?: 'hover' | 'click'; + onOpen?: (force?: boolean) => boolean; + onClose?: (force?: boolean) => boolean; + closed?: () => void; + onDestroy?: () => void; } const DISTANCE = 4; let tooltipContainer: HTMLElement; export interface TooltipInstance { destroy: () => void; + show: (force?: boolean) => void; + hide: (force?: boolean) => void; }; export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}): TooltipInstance | null => { - const { msg = '', delay = 150, content, direction = 'bottom', type = 'hover' } = options; + const { msg = '', delay = 150, content, direction = 'bottom', type = 'hover', container, onOpen, onClose, closed, onDestroy } = options; const bem = createBEM('tooltip'); if (msg || content) { if (!tooltipContainer) { tooltipContainer = document.createElement('div'); document.body.appendChild(tooltipContainer); } + const appendTo = container || tooltipContainer; const tooltip = document.createElement('div'); tooltip.classList.add(bem.b(), 'hidden', 'transparent'); if (content) { @@ -48,7 +56,9 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) else if (msg) { tooltip.textContent = msg; } - let timer: ReturnType | null; + let showTimer: ReturnType | undefined; + let closeTimer: ReturnType | undefined; + let closeTransendTimer: ReturnType | undefined; let cleanup: () => void; const update = () => { if (cleanup) cleanup(); @@ -64,15 +74,23 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) }; const transitionendHandler = () => { tooltip.classList.add('hidden'); - if (tooltipContainer.contains(tooltip)) { - tooltipContainer.removeChild(tooltip); + if (appendTo.contains(tooltip)) { + appendTo.removeChild(tooltip); } if (cleanup) cleanup(); + if (closed) closed(); }; - const open = () => { - if (timer) clearTimeout(timer); - timer = setTimeout(() => { - tooltipContainer.appendChild(tooltip); + + const openTooltip = (force: boolean = false) => { + if (closeTimer) clearTimeout(closeTimer); + if (closeTransendTimer) clearTimeout(closeTransendTimer); + + showTimer = setTimeout(() => { + if (onOpen) { + const allow = onOpen(force); + if (!force && allow) return; + } + appendTo.appendChild(tooltip); tooltip.removeEventListener('transitionend', transitionendHandler); tooltip.classList.remove('hidden'); @@ -81,9 +99,14 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) tooltip.classList.remove('transparent'); }, delay); }; - const close = () => { - if (timer) clearTimeout(timer); - timer = setTimeout(() => { + const closeTooltip = (force: boolean = false) => { + if (showTimer) clearTimeout(showTimer); + + closeTimer = setTimeout(() => { + if (onClose) { + const allow = onClose(force); + if (!force && allow) return; + } tooltip.addEventListener('transitionend', transitionendHandler, { once: true }); tooltip.classList.add('transparent'); }, delay); @@ -91,44 +114,50 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) const hoverDisplay = () => { const eventListeners = [target, tooltip]; - const show = () => { + const close = closeTooltip.bind(this, false); + const open = openTooltip.bind(this, false); + const prepare = () => { for (const listener of eventListeners) { listener.addEventListener('mouseenter', open); listener.addEventListener('mouseleave', close); } }; - const hide = () => { - for (const listener of eventListeners) { - listener.removeEventListener('mouseenter', open); - listener.removeEventListener('mouseleave', close); - } - }; return { - show, - hide, - destroy: () => {}, + prepare, + show: openTooltip, + hide: closeTooltip, + destroy: () => { + for (const listener of eventListeners) { + listener.removeEventListener('mouseenter', open); + listener.removeEventListener('mouseleave', close); + } + }, }; }; - const stopPropagation = (e: Event) => e.stopPropagation(); const clickDisplay = () => { - // eslint-disable-next-line unicorn/consistent-function-scoping + const close = (e: MouseEvent) => { + e.stopPropagation(); + closeTooltip(false); + }; const show = (e: MouseEvent) => { - stopPropagation(e); - open(); + e.stopPropagation(); + openTooltip(); document.removeEventListener('click', close); document.addEventListener('click', close, { once: true }); }; return { - show: () => { - tooltip.addEventListener('click', stopPropagation); + prepare: () => { + tooltip.addEventListener('click', (e: Event) => e.stopPropagation()); target.addEventListener('click', show); }, - hide: () => { + show: openTooltip, + hide: (force: boolean = false) => { + closeTooltip(force); document.removeEventListener('click', close); }, destroy: () => { - tooltip.removeEventListener('click', stopPropagation); target.removeEventListener('click', show); + document.removeEventListener('click', close); }, }; }; @@ -137,16 +166,19 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) click: clickDisplay, }; - const { show, hide, destroy: destroyDisplay } = displayMethods[type](); - show(); + const { prepare, show, hide, destroy: destroyDisplay } = displayMethods[type](); + prepare(); const destroy = () => { - hide(); + hide(true); + if (onDestroy) onDestroy(); destroyDisplay(); if (cleanup) cleanup(); tooltip.remove(); }; return { + show, + hide, destroy, }; } From 34d3f65afc92ddd1f7dd5a1f56bb82966d9a1e45 Mon Sep 17 00:00:00 2001 From: zzxming Date: Sat, 7 Dec 2024 09:56:28 +0800 Subject: [PATCH 5/6] feat: color add alpha --- src/style/color-picker.less | 41 ++++++++++++++-- src/utils/color.ts | 43 ++++++++++------- src/utils/components/color-picker.ts | 70 +++++++++++++++++++++++++--- 3 files changed, 128 insertions(+), 26 deletions(-) diff --git a/src/style/color-picker.less b/src/style/color-picker.less index 4619f66..2f45318 100644 --- a/src/style/color-picker.less +++ b/src/style/color-picker.less @@ -16,7 +16,7 @@ border-radius: 6px; box-shadow: 0 0 6px #b2b5b8; width: 197px; - height: 166px; + height: 186px; padding: 8px; } @@ -47,7 +47,7 @@ } &__hue { - width: 17px; + width: 12px; height: 150px; margin-left: 164px; position: absolute; @@ -56,12 +56,45 @@ &-handle { box-sizing: border-box; position: absolute; - width: 21px; - transform: translate(-2px, -5px); + width: 16px; height: 10px; + transform: translate(-2px, -5px); border: 2px solid #ffffff; opacity: 0.85; cursor: pointer; } } + + &__alpha { + width: 150px; + height: 12px; + position: absolute; + margin-top: 158px; + background: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%); + background-size: 12px 12px; + background-position: + 0 0, + 6px 0, + 6px -6px, + 0 6px; + &-bg { + position: relative; + height: 100%; + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + } + &-handle { + box-sizing: border-box; + position: absolute; + opacity: 0.85; + cursor: pointer; + top: 0; + width: 10px; + height: 16px; + transform: translate(-5px, -2px); + border: 1px solid #ebeef5; + background-color: #ffffff; + box-shadow: 0 0 2px #0009; + } + } } diff --git a/src/utils/color.ts b/src/utils/color.ts index 930b126..e0560a8 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -2,28 +2,39 @@ export interface HSB { h: number; s: number; b: number; + a: number; }; export interface RGB { r: number; g: number; b: number; + a: number; }; -export function validateHSB(hsb: HSB) { +export const validateHSB = (hsb: HSB) => { return { h: Math.min(360, Math.max(0, hsb.h)), s: Math.min(100, Math.max(0, hsb.s)), b: Math.min(100, Math.max(0, hsb.b)), + a: hsb.a ? Math.min(1, Math.max(0, hsb.a)) : 1, }; -} -export function HEXtoRGB(hex: string) { - const hexValue = Number.parseInt(hex.includes('#') ? hex.slice(1) : hex, 16); - return { r: hexValue >> 16, g: (hexValue & 0x00_FF_00) >> 8, b: hexValue & 0x00_00_FF }; -} -export function RGBtoHSB(rgb: RGB) { +}; +export const HEXtoRGB = (hex: string) => { + let hexValue = Number.parseInt(hex.includes('#') ? hex.slice(1) : hex, 16); + let alpha = 1; + + if (hex.length === 8) { + alpha = (hexValue & 0xFF) / 255; + hexValue = hexValue >> 8; + } + + return { r: hexValue >> 16, g: (hexValue & 0x00_FF_00) >> 8, b: hexValue & 0x00_00_FF, a: alpha }; +}; +export const RGBtoHSB = (rgb: RGB) => { const hsb = { h: 0, s: 0, b: 0, + a: rgb.a || 1, }; const min = Math.min(rgb.r, rgb.g, rgb.b); const max = Math.max(rgb.r, rgb.g, rgb.b); @@ -57,12 +68,13 @@ export function RGBtoHSB(rgb: RGB) { hsb.b *= 100 / 255; return hsb; -} -export function HSBtoRGB(hsb: HSB) { +}; +export const HSBtoRGB = (hsb: HSB) => { let rgb: RGB = { r: 0, g: 0, b: 0, + a: hsb.a || 1, }; let h = Math.round(hsb.h); const s = Math.round((hsb.s * 255) / 100); @@ -73,6 +85,7 @@ export function HSBtoRGB(hsb: HSB) { r: v, g: v, b: v, + a: rgb.a, }; } else { @@ -119,10 +132,10 @@ export function HSBtoRGB(hsb: HSB) { } } - return { r: Math.round(rgb.r), g: Math.round(rgb.g), b: Math.round(rgb.b) }; -} -export function RGBtoHEX(rgb: RGB) { - const hex = [rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16)]; + return { r: Math.round(rgb.r), g: Math.round(rgb.g), b: Math.round(rgb.b), a: hsb.a || 1 }; +}; +export const RGBtoHEX = (rgb: RGB) => { + const hex = [rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16), Math.round((rgb.a || 1) * 255).toString(16)]; for (const key in hex) { if (hex[key].length === 1) { hex[key] = `0${hex[key]}`; @@ -130,6 +143,4 @@ export function RGBtoHEX(rgb: RGB) { } return hex.join(''); }; -export function HSBtoHEX(hsb: HSB) { - return RGBtoHEX(HSBtoRGB(hsb)); -} +export const HSBtoHEX = (hsb: HSB) => RGBtoHEX(HSBtoRGB(hsb)); diff --git a/src/utils/components/color-picker.ts b/src/utils/components/color-picker.ts index 8d3f4e2..ff19bbd 100644 --- a/src/utils/components/color-picker.ts +++ b/src/utils/components/color-picker.ts @@ -21,13 +21,23 @@ export const createColorPicker = (options: Partial = {}) => const colorBackground = document.createElement('div'); colorBackground.classList.add(bem.be('background')); + colorSelector.appendChild(colorBackground); const colorHandle = document.createElement('div'); colorHandle.classList.add(bem.be('background-handle')); - colorBackground.appendChild(colorHandle); - colorSelector.appendChild(colorBackground); - content.appendChild(colorSelector); + + const colorAlpha = document.createElement('div'); + colorAlpha.classList.add(bem.be('alpha')); + + const alphaBg = document.createElement('div'); + alphaBg.classList.add(bem.be('alpha-bg')); + + const alphaHandle = document.createElement('div'); + alphaHandle.classList.add(bem.be('alpha-handle')); + + colorAlpha.appendChild(alphaBg); + colorAlpha.appendChild(alphaHandle); const colorHue = document.createElement('div'); colorHue.classList.add(bem.be('hue')); @@ -37,18 +47,26 @@ export const createColorPicker = (options: Partial = {}) => colorHue.appendChild(colorHueHandle); content.appendChild(colorHue); + content.appendChild(colorSelector); + content.appendChild(colorAlpha); let colorDragging = false; let hueDragging = false; + let alphaDragging = false; function onDrag(event: MouseEvent) { if (colorDragging) { - pickColor(event); event.preventDefault(); + pickColor(event); } if (hueDragging) { + event.preventDefault(); pickHue(event); + } + + if (alphaDragging) { event.preventDefault(); + pickAlpha(event); } } function onColorSelectorDragEnd() { @@ -70,7 +88,6 @@ export const createColorPicker = (options: Partial = {}) => }); } function pickColor(event: MouseEvent) { - event.preventDefault(); const rect = colorSelector.getBoundingClientRect(); const top = rect.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0); const left = rect.left + document.body.scrollLeft; @@ -81,6 +98,7 @@ export const createColorPicker = (options: Partial = {}) => h: hsbValue.h, s: saturation, b: brightness, + a: hsbValue.a, }); updateColorHandle(); @@ -109,28 +127,68 @@ export const createColorPicker = (options: Partial = {}) => h: hsbValue.h, s: 100, b: 100, + a: 1, }))}`; } function pickHue(event: MouseEvent) { - event.preventDefault(); const top = colorHue.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0); hsbValue = validateHSB({ h: Math.floor((360 * (150 - Math.max(0, Math.min(150, event.pageY - top)))) / 150), s: hsbValue.s, b: hsbValue.b, + a: hsbValue.a, }); updateColorSelector(); updateHue(); + updateAlphaBg(); if (options.onChange) { options.onChange(`#${HSBtoHEX(hsbValue)}`); } } + function pickAlpha(event: MouseEvent) { + const { pageX } = event; + const rect = colorAlpha.getBoundingClientRect(); + let left = pageX - rect.left; + left = Math.max(10 / 2, left); + left = Math.min(left, rect.width - 10 / 2); + + hsbValue.a = Math.round(((left - 10 / 2) / (rect.width - 10)) * 100) / 100; + + updateAlphaBg(); + updateAlphaHandle(); + + if (options.onChange) { + options.onChange(`#${HSBtoHEX(hsbValue)}`); + } + } + function onColorAlphaDragEnd() { + document.removeEventListener('mousemove', onDrag); + document.removeEventListener('mouseup', onColorAlphaDragEnd); + alphaDragging = false; + } + function onColorAlphaMousedown(event: MouseEvent) { + document.addEventListener('mousemove', onDrag); + document.addEventListener('mouseup', onColorAlphaDragEnd); + alphaDragging = true; + pickAlpha(event); + } + colorAlpha.addEventListener('mousedown', onColorAlphaMousedown); + function updateAlphaHandle() { + alphaHandle.style.left = `${hsbValue.a * 100}%`; + } + function updateAlphaBg() { + const { r, g, b } = HSBtoRGB(hsbValue); + alphaBg.style.background = `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 0%, rgba(${r}, ${g}, ${b}, 1) 100%)`; + } + updateColorHandle(); updateColorSelector(); updateHue(); + updateAlphaHandle(); + updateAlphaBg(); return root; }; From 6a72e03b301aaa669061d66d3caccdb63017ef0c Mon Sep 17 00:00:00 2001 From: zzxming Date: Sat, 7 Dec 2024 09:57:32 +0800 Subject: [PATCH 6/6] fix: remove unnecessary this bind --- src/utils/components/tooltip.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/components/tooltip.ts b/src/utils/components/tooltip.ts index a705845..0248a41 100644 --- a/src/utils/components/tooltip.ts +++ b/src/utils/components/tooltip.ts @@ -114,8 +114,8 @@ export const createTooltip = (target: HTMLElement, options: ToolTipOptions = {}) const hoverDisplay = () => { const eventListeners = [target, tooltip]; - const close = closeTooltip.bind(this, false); - const open = openTooltip.bind(this, false); + const close = closeTooltip.bind(undefined, false); + const open = openTooltip.bind(undefined, false); const prepare = () => { for (const listener of eventListeners) { listener.addEventListener('mouseenter', open);