Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
10 KiB
292 lines
10 KiB
import { ColorHsla, ColorRgba, hexaToHsla, hslaToRgba, rgbaToHexa as rgbaToHexa, rgbaToHsla } from "../helpers/color"; |
|
import attachGrabListeners from "../helpers/dom/attachGrabListeners"; |
|
import { clamp } from "../helpers/number"; |
|
import InputField, { InputState } from "./inputField"; |
|
|
|
export type ColorPickerColor = { |
|
hsl: string; |
|
rgb: string; |
|
hex: string; |
|
hsla: string; |
|
rgba: string; |
|
hexa: string; |
|
rgbaArray: ColorRgba; |
|
}; |
|
|
|
export default class ColorPicker { |
|
private static BASE_CLASS = 'color-picker'; |
|
public container: HTMLElement; |
|
|
|
private boxRect: DOMRect; |
|
//private boxDraggerRect: DOMRect; |
|
private hueRect: DOMRect; |
|
//private hueDraggerRect: DOMRect; |
|
|
|
private hue = 0; |
|
private saturation = 100; |
|
private lightness = 50; |
|
private alpha = 1; |
|
private elements: { |
|
box: SVGSVGElement, |
|
boxDragger: SVGSVGElement, |
|
sliders: HTMLElement, |
|
hue: SVGSVGElement, |
|
hueDragger: SVGSVGElement, |
|
saturation: SVGLinearGradientElement, |
|
} = {} as any; |
|
private hexInputField: InputField; |
|
private rgbInputField: InputField; |
|
public onChange: (color: ReturnType<ColorPicker['getCurrentColor']>) => void; |
|
|
|
constructor() { |
|
this.container = document.createElement('div'); |
|
this.container.classList.add(ColorPicker.BASE_CLASS); |
|
|
|
const html = ` |
|
<svg class="${ColorPicker.BASE_CLASS + '-box'}" viewBox="0 0 380 198"> |
|
<defs> |
|
<linearGradient id="color-picker-saturation" x1="0%" y1="0%" x2="100%" y2="0%"> |
|
<stop offset="0%" stop-color="#fff"></stop> |
|
<stop offset="100%" stop-color="hsl(0,100%,50%)"></stop> |
|
</linearGradient> |
|
<linearGradient id="color-picker-brightness" x1="0%" y1="0%" x2="0%" y2="100%"> |
|
<stop offset="0%" stop-color="rgba(0,0,0,0)"></stop> |
|
<stop offset="100%" stop-color="#000"></stop> |
|
</linearGradient> |
|
<pattern id="color-picker-pattern" width="100%" height="100%"> |
|
<rect x="0" y="0" width="100%" height="100%" fill="url(#color-picker-saturation)"></rect> |
|
<rect x="0" y="0" width="100%" height="100%" fill="url(#color-picker-brightness)"></rect> |
|
</pattern> |
|
</defs> |
|
<rect rx="10" ry="10" x="0" y="0" width="380" height="198" fill="url(#color-picker-pattern)"></rect> |
|
<svg class="${ColorPicker.BASE_CLASS + '-dragger'} ${ColorPicker.BASE_CLASS + '-box-dragger'}" x="0" y="0"> |
|
<circle r="11" fill="inherit" stroke="#fff" stroke-width="2"></circle> |
|
</svg> |
|
</svg> |
|
<div class="${ColorPicker.BASE_CLASS + '-sliders'}"> |
|
<svg class="${ColorPicker.BASE_CLASS + '-color-slider'}" viewBox="0 0 380 24"> |
|
<defs> |
|
<linearGradient id="hue" x1="100%" y1="0%" x2="0%" y2="0%"> |
|
<stop offset="0%" stop-color="#f00"></stop> |
|
<stop offset="16.666%" stop-color="#f0f"></stop> |
|
<stop offset="33.333%" stop-color="#00f"></stop> |
|
<stop offset="50%" stop-color="#0ff"></stop> |
|
<stop offset="66.666%" stop-color="#0f0"></stop> |
|
<stop offset="83.333%" stop-color="#ff0"></stop> |
|
<stop offset="100%" stop-color="#f00"></stop> |
|
</linearGradient> |
|
</defs> |
|
<rect rx="4" ry="4" x="0" y="9" width="380" height="8" fill="url(#hue)"></rect> |
|
<svg class="${ColorPicker.BASE_CLASS + '-dragger'} ${ColorPicker.BASE_CLASS + '-color-slider-dragger'}" x="0" y="13"> |
|
<circle r="11" fill="inherit" stroke="#fff" stroke-width="2"></circle> |
|
</svg> |
|
</svg> |
|
</div> |
|
`; |
|
|
|
this.container.innerHTML = html; |
|
|
|
this.elements.box = this.container.firstElementChild as any; |
|
this.elements.boxDragger = this.elements.box.lastElementChild as any; |
|
this.elements.saturation = this.elements.box.firstElementChild.firstElementChild as any; |
|
|
|
this.elements.sliders = this.elements.box.nextElementSibling as any; |
|
|
|
this.elements.hue = this.elements.sliders.firstElementChild as any; |
|
this.elements.hueDragger = this.elements.hue.lastElementChild as any; |
|
|
|
this.hexInputField = new InputField({plainText: true, label: 'Appearance.Color.Hex'}); |
|
this.rgbInputField = new InputField({plainText: true, label: 'Appearance.Color.RGB'}); |
|
|
|
const inputs = document.createElement('div'); |
|
inputs.className = ColorPicker.BASE_CLASS + '-inputs'; |
|
inputs.append(this.hexInputField.container, this.rgbInputField.container); |
|
this.container.append(inputs); |
|
|
|
this.hexInputField.input.addEventListener('input', () => { |
|
let value = this.hexInputField.value.replace(/#/g, '').slice(0, 6); |
|
|
|
const match = value.match(/([a-fA-F\d]+)/); |
|
const valid = match && match[0].length === value.length && [/* 3, 4, */6].includes(value.length); |
|
this.hexInputField.setState(valid ? InputState.Neutral : InputState.Error); |
|
|
|
value = '#' + value; |
|
this.hexInputField.setValueSilently(value); |
|
|
|
if(valid) { |
|
this.setColor(value, false, true); |
|
} |
|
}); |
|
|
|
// patched https://stackoverflow.com/a/34029238/6758968 |
|
const rgbRegExp = /^(?:rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(?:\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(?:([01]?\d\d?|2[0-4]\d|25[0-5])\)?)$/; |
|
this.rgbInputField.input.addEventListener('input', () => { |
|
const match = this.rgbInputField.value.match(rgbRegExp); |
|
this.rgbInputField.setState(match ? InputState.Neutral : InputState.Error); |
|
|
|
if(match) { |
|
this.setColor(rgbaToHsla(+match[1], +match[2], +match[3]), true, false); |
|
} |
|
}); |
|
|
|
this.attachBoxListeners(); |
|
this.attachHueListeners(); |
|
} |
|
|
|
private onGrabStart = () => { |
|
document.documentElement.style.cursor = this.elements.boxDragger.style.cursor = 'grabbing'; |
|
}; |
|
|
|
private onGrabEnd = () => { |
|
document.documentElement.style.cursor = this.elements.boxDragger.style.cursor = ''; |
|
}; |
|
|
|
private attachBoxListeners() { |
|
attachGrabListeners(this.elements.box as any, () => { |
|
this.onGrabStart(); |
|
this.boxRect = this.elements.box.getBoundingClientRect(); |
|
//this.boxDraggerRect = this.elements.boxDragger.getBoundingClientRect(); |
|
}, (pos) => { |
|
this.saturationHandler(pos.x, pos.y); |
|
}, () => { |
|
this.onGrabEnd(); |
|
}); |
|
} |
|
|
|
private attachHueListeners() { |
|
attachGrabListeners(this.elements.hue as any, () => { |
|
this.onGrabStart(); |
|
this.hueRect = this.elements.hue.getBoundingClientRect(); |
|
//this.hueDraggerRect = this.elements.hueDragger.getBoundingClientRect(); |
|
}, (pos) => { |
|
this.hueHandler(pos.x); |
|
}, () => { |
|
this.onGrabEnd(); |
|
}); |
|
} |
|
|
|
public setColor(color: ColorHsla | string, updateHexInput = true, updateRgbInput = true) { |
|
if(color === undefined) { // * set to red |
|
color = { |
|
h: 0, |
|
s: 100, |
|
l: 50, |
|
a: 1 |
|
}; |
|
} else if(typeof(color) === 'string') { |
|
if(color[0] === '#') { |
|
color = hexaToHsla(color); |
|
} else { |
|
const rgb = color.match(/[.?\d]+/g); |
|
color = rgbaToHsla(+rgb[0], +rgb[1], +rgb[2], rgb[3] === undefined ? 1 : +rgb[3]); |
|
} |
|
} |
|
|
|
// Set box |
|
this.boxRect = this.elements.box.getBoundingClientRect(); |
|
|
|
const boxX = this.boxRect.width / 100 * color.s; |
|
const percentY = 100 - (color.l / (100 - color.s / 2)) * 100; |
|
const boxY = this.boxRect.height / 100 * percentY; |
|
|
|
this.saturationHandler(this.boxRect.left + boxX, this.boxRect.top + boxY, false); |
|
|
|
// Set hue |
|
this.hueRect = this.elements.hue.getBoundingClientRect(); |
|
|
|
const percentHue = color.h / 360; |
|
const hueX = this.hueRect.left + this.hueRect.width * percentHue; |
|
|
|
this.hueHandler(hueX, false); |
|
|
|
// Set values |
|
this.hue = color.h; |
|
this.saturation = color.s; |
|
this.lightness = color.l; |
|
this.alpha = color.a; |
|
|
|
this.updatePicker(updateHexInput, updateRgbInput); |
|
}; |
|
|
|
public getCurrentColor(): ColorPickerColor { |
|
const rgbaArray = hslaToRgba(this.hue, this.saturation, this.lightness, this.alpha); |
|
const hexa = rgbaToHexa(rgbaArray); |
|
const hex = hexa.slice(0, -2); |
|
|
|
return { |
|
hsl: `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`, |
|
rgb: `rgb(${rgbaArray[0]}, ${rgbaArray[1]}, ${rgbaArray[2]})`, |
|
hex: hex, |
|
hsla: `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha})`, |
|
rgba: `rgba(${rgbaArray[0]}, ${rgbaArray[1]}, ${rgbaArray[2]}, ${rgbaArray[3]})`, |
|
hexa: hexa, |
|
rgbaArray: rgbaArray |
|
}; |
|
} |
|
|
|
public updatePicker(updateHexInput = true, updateRgbInput = true) { |
|
const color = this.getCurrentColor(); |
|
this.elements.boxDragger.setAttributeNS(null, 'fill', color.hex); |
|
|
|
if(updateHexInput) { |
|
this.hexInputField.setValueSilently(color.hex); |
|
this.hexInputField.setState(InputState.Neutral); |
|
} |
|
|
|
if(updateRgbInput) { |
|
this.rgbInputField.setValueSilently(color.rgbaArray.slice(0, -1).join(', ')); |
|
this.rgbInputField.setState(InputState.Neutral); |
|
} |
|
|
|
if(this.onChange) { |
|
this.onChange(color); |
|
} |
|
} |
|
|
|
private hueHandler(pageX: number, update = true) { |
|
const eventX = clamp(pageX - this.hueRect.left, 0, this.hueRect.width); |
|
|
|
const percents = eventX / this.hueRect.width; |
|
this.hue = Math.round(360 * percents); |
|
|
|
const hsla = `hsla(${this.hue}, 100%, 50%, ${this.alpha})`; |
|
|
|
this.elements.hueDragger.setAttributeNS(null, 'x', (percents * 100) + '%'); |
|
this.elements.hueDragger.setAttributeNS(null, 'fill', hsla); |
|
|
|
this.elements.saturation.lastElementChild.setAttributeNS(null, 'stop-color', hsla); |
|
|
|
if(update) { |
|
this.updatePicker(); |
|
} |
|
} |
|
|
|
private saturationHandler(pageX: number, pageY: number, update = true) { |
|
const maxX = this.boxRect.width; |
|
const maxY = this.boxRect.height; |
|
|
|
const eventX = clamp(pageX - this.boxRect.left, 0, maxX); |
|
const eventY = clamp(pageY - this.boxRect.top, 0, maxY); |
|
|
|
const posX = eventX / maxX * 100; |
|
const posY = eventY / maxY * 100; |
|
|
|
const boxDragger = this.elements.boxDragger; |
|
boxDragger.setAttributeNS(null, 'x', posX + '%'); |
|
boxDragger.setAttributeNS(null, 'y', posY + '%'); |
|
|
|
const saturation = clamp(posX, 0, 100); |
|
|
|
const lightnessX = 100 - saturation / 2; |
|
const lightnessY = 100 - clamp(posY, 0, 100); |
|
|
|
const lightness = clamp(lightnessY / 100 * lightnessX, 0, 100); |
|
|
|
this.saturation = saturation; |
|
this.lightness = lightness; |
|
|
|
if(update) { |
|
this.updatePicker(); |
|
} |
|
}; |
|
}
|
|
|