您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

ColorPicker.tsx 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import React from "react";
  2. import { Popover } from "./Popover";
  3. import { isTransparent } from "../utils";
  4. import "./ColorPicker.scss";
  5. import { isArrowKey, KEYS } from "../keys";
  6. import { t, getLanguage } from "../i18n";
  7. import { isWritableElement } from "../utils";
  8. import colors from "../colors";
  9. const isValidColor = (color: string) => {
  10. const style = new Option().style;
  11. style.color = color;
  12. return !!style.color;
  13. };
  14. const getColor = (color: string): string | null => {
  15. if (isTransparent(color)) {
  16. return color;
  17. }
  18. return isValidColor(color)
  19. ? color
  20. : isValidColor(`#${color}`)
  21. ? `#${color}`
  22. : null;
  23. };
  24. // This is a narrow reimplementation of the awesome react-color Twitter component
  25. // https://github.com/casesandberg/react-color/blob/master/src/components/twitter/Twitter.js
  26. // Unfortunately, we can't detect keyboard layout in the browser. So this will
  27. // only work well for QWERTY but not AZERTY or others...
  28. const keyBindings = [
  29. ["1", "2", "3", "4", "5"],
  30. ["q", "w", "e", "r", "t"],
  31. ["a", "s", "d", "f", "g"],
  32. ].flat();
  33. const Picker = ({
  34. colors,
  35. color,
  36. onChange,
  37. onClose,
  38. label,
  39. showInput = true,
  40. type,
  41. }: {
  42. colors: string[];
  43. color: string | null;
  44. onChange: (color: string) => void;
  45. onClose: () => void;
  46. label: string;
  47. showInput: boolean;
  48. type: "canvasBackground" | "elementBackground" | "elementStroke";
  49. }) => {
  50. const firstItem = React.useRef<HTMLButtonElement>();
  51. const activeItem = React.useRef<HTMLButtonElement>();
  52. const gallery = React.useRef<HTMLDivElement>();
  53. const colorInput = React.useRef<HTMLInputElement>();
  54. React.useEffect(() => {
  55. // After the component is first mounted focus on first input
  56. if (activeItem.current) {
  57. activeItem.current.focus();
  58. } else if (colorInput.current) {
  59. colorInput.current.focus();
  60. } else if (gallery.current) {
  61. gallery.current.focus();
  62. }
  63. }, []);
  64. const handleKeyDown = (event: React.KeyboardEvent) => {
  65. if (event.key === KEYS.TAB) {
  66. const { activeElement } = document;
  67. if (event.shiftKey) {
  68. if (activeElement === firstItem.current) {
  69. colorInput.current?.focus();
  70. event.preventDefault();
  71. }
  72. } else if (activeElement === colorInput.current) {
  73. firstItem.current?.focus();
  74. event.preventDefault();
  75. }
  76. } else if (isArrowKey(event.key)) {
  77. const { activeElement } = document;
  78. const isRTL = getLanguage().rtl;
  79. const index = Array.prototype.indexOf.call(
  80. gallery!.current!.children,
  81. activeElement,
  82. );
  83. if (index !== -1) {
  84. const length = gallery!.current!.children.length - (showInput ? 1 : 0);
  85. const nextIndex =
  86. event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
  87. ? (index + 1) % length
  88. : event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
  89. ? (length + index - 1) % length
  90. : event.key === KEYS.ARROW_DOWN
  91. ? (index + 5) % length
  92. : event.key === KEYS.ARROW_UP
  93. ? (length + index - 5) % length
  94. : index;
  95. (gallery!.current!.children![nextIndex] as any).focus();
  96. }
  97. event.preventDefault();
  98. } else if (
  99. keyBindings.includes(event.key.toLowerCase()) &&
  100. !isWritableElement(event.target)
  101. ) {
  102. const index = keyBindings.indexOf(event.key.toLowerCase());
  103. (gallery!.current!.children![index] as any).focus();
  104. event.preventDefault();
  105. } else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
  106. event.preventDefault();
  107. onClose();
  108. }
  109. event.nativeEvent.stopImmediatePropagation();
  110. event.stopPropagation();
  111. };
  112. return (
  113. <div
  114. className={`color-picker color-picker-type-${type}`}
  115. role="dialog"
  116. aria-modal="true"
  117. aria-label={t("labels.colorPicker")}
  118. onKeyDown={handleKeyDown}
  119. >
  120. <div className="color-picker-triangle color-picker-triangle-shadow"></div>
  121. <div className="color-picker-triangle"></div>
  122. <div
  123. className="color-picker-content"
  124. ref={(el) => {
  125. if (el) {
  126. gallery.current = el;
  127. }
  128. }}
  129. tabIndex={0}
  130. >
  131. {colors.map((_color, i) => {
  132. const _colorWithoutHash = _color.replace("#", "");
  133. return (
  134. <button
  135. className="color-picker-swatch"
  136. onClick={(event) => {
  137. (event.currentTarget as HTMLButtonElement).focus();
  138. onChange(_color);
  139. }}
  140. title={`${t(`colors.${_colorWithoutHash}`)}${
  141. !isTransparent(_color) ? ` (${_color})` : ""
  142. } — ${keyBindings[i].toUpperCase()}`}
  143. aria-label={t(`colors.${_colorWithoutHash}`)}
  144. aria-keyshortcuts={keyBindings[i]}
  145. style={{ color: _color }}
  146. key={_color}
  147. ref={(el) => {
  148. if (el && i === 0) {
  149. firstItem.current = el;
  150. }
  151. if (el && _color === color) {
  152. activeItem.current = el;
  153. }
  154. }}
  155. onFocus={() => {
  156. onChange(_color);
  157. }}
  158. >
  159. {isTransparent(_color) ? (
  160. <div className="color-picker-transparent"></div>
  161. ) : undefined}
  162. <span className="color-picker-keybinding">{keyBindings[i]}</span>
  163. </button>
  164. );
  165. })}
  166. {showInput && (
  167. <ColorInput
  168. color={color}
  169. label={label}
  170. onChange={(color) => {
  171. onChange(color);
  172. }}
  173. ref={colorInput}
  174. />
  175. )}
  176. </div>
  177. </div>
  178. );
  179. };
  180. const ColorInput = React.forwardRef(
  181. (
  182. {
  183. color,
  184. onChange,
  185. label,
  186. }: {
  187. color: string | null;
  188. onChange: (color: string) => void;
  189. label: string;
  190. },
  191. ref,
  192. ) => {
  193. // console.log("ColorInput 0")
  194. if (document.readyState!="complete" && window.tmp_hooks){
  195. tmp_hooks.ColorInput_args={
  196. color,
  197. onChange,
  198. label,
  199. }
  200. }
  201. // console.log("dev ColorInput 0",window.indexLoadedTime,document.readyState)
  202. const [innerValue, setInnerValue] = React.useState(color);
  203. const inputRef = React.useRef(null);
  204. React.useEffect(() => {
  205. setInnerValue(color);
  206. }, [color]);
  207. React.useImperativeHandle(ref, () => inputRef.current);
  208. const changeColor = React.useCallback(
  209. (inputValue: string) => {
  210. const value = inputValue.toLowerCase();
  211. const color = getColor(value);
  212. if (color) {
  213. onChange(color);
  214. }
  215. // console.log("changeColor!")
  216. // window.tdx3 = {onChange}
  217. setInnerValue(value);
  218. },
  219. [onChange],
  220. );
  221. /*
  222. console.log("COLOR INPUT!")
  223. window.tdx = {
  224. changeColor,
  225. setInnerValue,
  226. innerValue,
  227. onChange,
  228. getColor,
  229. }
  230. */
  231. return (
  232. <label className="color-input-container">
  233. <div className="color-picker-hash">#</div>
  234. <input
  235. spellCheck={false}
  236. className="color-picker-input"
  237. aria-label={label}
  238. onChange={(event) => changeColor(event.target.value)}
  239. value={(innerValue || "").replace(/^#/, "")}
  240. onBlur={() => setInnerValue(color)}
  241. ref={inputRef}
  242. />
  243. </label>
  244. );
  245. },
  246. );
  247. window.ColorInput_orig = ColorInput
  248. export const ColorPicker = ({
  249. type,
  250. color,
  251. onChange,
  252. label,
  253. isActive,
  254. setActive,
  255. }: {
  256. type: "canvasBackground" | "elementBackground" | "elementStroke";
  257. color: string | null;
  258. onChange: (color: string) => void;
  259. label: string;
  260. isActive: boolean;
  261. setActive: (active: boolean) => void;
  262. }) => {
  263. const pickerButton = React.useRef<HTMLButtonElement>(null);
  264. return (
  265. <div>
  266. <div className="color-picker-control-container">
  267. <button
  268. className="color-picker-label-swatch"
  269. aria-label={label}
  270. style={color ? { "--swatch-color": color } : undefined}
  271. onClick={() => setActive(!isActive)}
  272. ref={pickerButton}
  273. />
  274. <ColorInput
  275. color={color}
  276. label={label}
  277. onChange={(color) => {
  278. onChange(color);
  279. }}
  280. />
  281. </div>
  282. <React.Suspense fallback="">
  283. {isActive ? (
  284. <Popover
  285. onCloseRequest={(event) =>
  286. event.target !== pickerButton.current && setActive(false)
  287. }
  288. >
  289. <Picker
  290. colors={colors[type]}
  291. color={color || null}
  292. onChange={(changedColor) => {
  293. onChange(changedColor);
  294. }}
  295. onClose={() => {
  296. setActive(false);
  297. pickerButton.current?.focus();
  298. }}
  299. label={label}
  300. showInput={false}
  301. type={type}
  302. />
  303. </Popover>
  304. ) : null}
  305. </React.Suspense>
  306. </div>
  307. );
  308. };