colorpicker.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import $ from 'jquery';
  2. import clamp from 'mout/math/clamp';
  3. import bind from 'mout/function/bind';
  4. import { rgbstr2hex, hsb2hex, hex2hsb, hex2rgb, parseHex } from '../../utils/colors';
  5. const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
  6. const body = $('body');
  7. const MOUSEDOWN = 'mousedown touchstart MSPointerDown pointerdown';
  8. const MOUSEMOVE = 'mousemove touchmove MSPointerMove pointermove';
  9. const MOUSEUP = 'mouseup touchend MSPointerUp pointerup';
  10. const FOCUSIN = isFirefox ? 'focus' : 'focusin';
  11. export default class ColorpickerField {
  12. constructor(selector) {
  13. this.selector = selector;
  14. this.field = $(this.selector);
  15. this.options = Object.assign({}, this.field.data('grav-colorpicker'));
  16. this.built = false;
  17. this.attach();
  18. if (this.options.update) {
  19. this.field.on('change._grav_colorpicker', (event, field, hex, opacity) => {
  20. let backgroundColor = hex;
  21. let rgb = hex2rgb(hex);
  22. if (opacity < 1) {
  23. backgroundColor = 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + opacity + ')';
  24. }
  25. let target = field.closest(this.options.update);
  26. if (!target.length) {
  27. target = field.siblings(this.options.update);
  28. }
  29. if (!target.length) {
  30. target = field.parent('.g-colorpicker').find(this.options.update);
  31. }
  32. target.css({ backgroundColor });
  33. });
  34. }
  35. }
  36. attach() {
  37. body.on(FOCUSIN, this.selector, (event) => this.show(event, event.currentTarget));
  38. body.on(MOUSEDOWN, this.selector + ' .g-colorpicker, ' + this.selector + ' .g-colorpicker i', this.bound('iconClick'));
  39. body.on('keydown', this.selector, (event) => {
  40. switch (event.keyCode) {
  41. case 9: // tab
  42. this.hide();
  43. break;
  44. case 13: // enter
  45. case 27: // esc
  46. this.hide();
  47. event.currentTarget.blur();
  48. break;
  49. }
  50. return true;
  51. });
  52. // Update on keyup
  53. body.on('keyup', this.selector, (event) => {
  54. this.updateFromInput(true, event.currentTarget);
  55. return true;
  56. });
  57. // Update on paste
  58. body.on('paste', this.selector, (event) => {
  59. setTimeout(() => this.updateFromInput(true, event.currentTarget), 1);
  60. });
  61. }
  62. show(event, target) {
  63. target = $(target);
  64. if (!this.built) {
  65. this.build();
  66. }
  67. this.element = target;
  68. this.reposition();
  69. this.wrapper.addClass('cp-visible');
  70. this.updateFromInput();
  71. let mainContainer = $('#admin-main .content-wrapper').data('scrollbar').getViewElement();
  72. this.wrapper.on(MOUSEDOWN, '.cp-grid, .cp-slider, .cp-opacity-slider', this.bound('bodyDown'));
  73. body.on(MOUSEMOVE, this.bound('bodyMove'));
  74. body.on(MOUSEDOWN, this.bound('bodyClick'));
  75. body.on(MOUSEUP, this.bound('targetReset'));
  76. $(mainContainer).on('scroll', this.bound('reposition'));
  77. }
  78. hide() {
  79. if (!this.built) { return; }
  80. this.wrapper.removeClass('cp-visible');
  81. let mainContainer = $('#admin-main .content-wrapper').data('scrollbar').getViewElement();
  82. this.wrapper.undelegate(MOUSEDOWN, '.cp-grid, .cp-slider, .cp-opacity-slider', this.bound('bodyDown'));
  83. body.off(MOUSEMOVE, this.bound('bodyMove'));
  84. body.off(MOUSEDOWN, this.bound('bodyClick'));
  85. body.off(MOUSEUP, this.bound('targetReset'));
  86. $(mainContainer).off('scroll', this.bound('reposition'));
  87. }
  88. build() {
  89. this.wrapper = $('<div class="cp-wrapper cp-with-opacity cp-mode-hue" />');
  90. this.slider = $('<div class="cp-slider cp-sprite" />').appendTo(this.wrapper).append($('<div class="cp-picker" />'));
  91. this.opacitySlider = $('<div class="cp-opacity-slider cp-sprite" />').appendTo(this.wrapper).append($('<div class="cp-picker" />'));
  92. this.grid = $('<div class="cp-grid cp-sprite" />').appendTo(this.wrapper).append($('<div class="cp-grid-inner" />')).append($('<div class="cp-picker" />'));
  93. $('<div />').appendTo(this.grid.find('.cp-picker'));
  94. let tabs = $('<div class="cp-tabs" />').appendTo(this.wrapper);
  95. this.tabs = {
  96. hue: $('<div class="cp-tab-hue active" />').text('HUE').appendTo(tabs),
  97. brightness: $('<div class="cp-tab-brightness" />').text('BRI').appendTo(tabs),
  98. saturation: $('<div class="cp-tab-saturation" />').text('SAT').appendTo(tabs),
  99. wheel: $('<div class="cp-tab-wheel" />').text('WHEEL').appendTo(tabs),
  100. transparent: $('<div class="cp-tab-transp" />').text('TRANSPARENT').appendTo(tabs)
  101. };
  102. tabs.on(MOUSEDOWN, '> div', (event) => {
  103. let element = $(event.currentTarget);
  104. if (element.is(this.tabs.transparent)) {
  105. let sliderHeight = this.opacitySlider.height();
  106. this.opacity = 0;
  107. this.opacitySlider.find('.cp-picker').css({ 'top': clamp(sliderHeight - (sliderHeight * this.opacity), 0, sliderHeight) });
  108. this.move(this.opacitySlider, { manualOpacity: true });
  109. return;
  110. }
  111. let active = tabs.find('.active');
  112. let mode = active.attr('class').replace(/\s|active|cp-tab-/g, '');
  113. let newMode = element.attr('class').replace(/\s|active|cp-tab-/g, '');
  114. this.wrapper.removeClass('cp-mode-' + mode).addClass('cp-mode-' + newMode);
  115. active.removeClass('active');
  116. element.addClass('active');
  117. this.mode = newMode;
  118. this.updateFromInput();
  119. });
  120. this.wrapper.appendTo('.content-wrapper');
  121. this.built = true;
  122. this.mode = 'hue';
  123. }
  124. reposition() {
  125. let ct = $('.content-wrapper')[0];
  126. let offset = this.element[0].getBoundingClientRect();
  127. let ctOffset = ct.getBoundingClientRect();
  128. let delta = { x: 0, y: 0 };
  129. if (this.options.offset) {
  130. delta.x = this.options.offset.x || 0;
  131. delta.y = this.options.offset.y || 0;
  132. }
  133. this.wrapper.css({
  134. top: offset.top + offset.height + ct.scrollTop - ctOffset.top + delta.y,
  135. left: offset.left + ct.scrollLeft - ctOffset.left + delta.x
  136. });
  137. }
  138. iconClick(event) {
  139. if (this.wrapper && this.wrapper.hasClass('cp-visible')) { return true; }
  140. event && event.preventDefault();
  141. let input = $(event.currentTarget).find('input');
  142. setTimeout(() => input.focus(), 50);
  143. }
  144. bodyMove(event) {
  145. event && event.preventDefault();
  146. if (this.target) { this.move(this.target, event); }
  147. }
  148. bodyClick(event) {
  149. let target = $(event.target);
  150. if (!target.closest('.cp-wrapper').length && !target.is(this.selector)) {
  151. this.hide();
  152. }
  153. }
  154. bodyDown(event) {
  155. event && event.preventDefault();
  156. this.target = $(event.currentTarget);
  157. this.move(this.target, event, true);
  158. }
  159. targetReset(event) {
  160. event && event.preventDefault();
  161. this.target = null;
  162. }
  163. move(target, event) {
  164. let input = this.element;
  165. let picker = target.find('.cp-picker');
  166. let clientRect = target[0].getBoundingClientRect();
  167. let offsetX = clientRect.left + window.scrollX;
  168. let offsetY = clientRect.top + window.scrollY;
  169. let x = Math.round((event ? event.pageX : 0) - offsetX);
  170. let y = Math.round((event ? event.pageY : 0) - offsetY);
  171. let wx;
  172. let wy;
  173. let r;
  174. let phi;
  175. // Touch support
  176. let touchEvents = event.changedTouches || (event.originalEvent && event.originalEvent.changedTouches);
  177. if (event && touchEvents) {
  178. x = (touchEvents ? touchEvents[0].pageX : 0) - offsetX;
  179. y = (touchEvents ? touchEvents[0].pageY : 0) - offsetY;
  180. }
  181. if (event && event.manualOpacity) {
  182. y = clientRect.height;
  183. }
  184. // Constrain picker to its container
  185. if (x < 0) x = 0;
  186. if (y < 0) y = 0;
  187. if (x > clientRect.width) x = clientRect.width;
  188. if (y > clientRect.height) y = clientRect.height;
  189. // Constrain color wheel values to the wheel
  190. if (target.parent('.cp-mode-wheel').length && picker.parent('.cp-grid').length) {
  191. wx = 75 - x;
  192. wy = 75 - y;
  193. r = Math.sqrt(wx * wx + wy * wy);
  194. phi = Math.atan2(wy, wx);
  195. if (phi < 0) phi += Math.PI * 2;
  196. if (r > 75) {
  197. x = 75 - (75 * Math.cos(phi));
  198. y = 75 - (75 * Math.sin(phi));
  199. }
  200. x = Math.round(x);
  201. y = Math.round(y);
  202. }
  203. // Move the picker
  204. if (target.hasClass('cp-grid')) {
  205. picker.css({
  206. top: y,
  207. left: x
  208. });
  209. this.updateFromPicker(input, target);
  210. } else {
  211. picker.css({
  212. top: y
  213. });
  214. this.updateFromPicker(input, target);
  215. }
  216. }
  217. updateFromInput(dontFireEvent, element) {
  218. element = element ? $(element) : this.element;
  219. let value = element.val();
  220. let opacity = value.replace(/\s/g, '').match(/^rgba?\([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},(.+)\)/);
  221. let hex;
  222. let hsb;
  223. value = rgbstr2hex(value) || value;
  224. opacity = opacity ? clamp(opacity[1], 0, 1) : 1;
  225. if (!(hex = parseHex(value))) { hex = '#ffffff'; }
  226. hsb = hex2hsb(hex);
  227. if (this.built) {
  228. // opacity
  229. this.opacity = opacity;
  230. var sliderHeight = this.opacitySlider.height();
  231. this.opacitySlider.find('.cp-picker').css({ 'top': clamp(sliderHeight - (sliderHeight * this.opacity), 0, sliderHeight) });
  232. // bg color
  233. let gridHeight = this.grid.height();
  234. let gridWidth = this.grid.width();
  235. let r;
  236. let phi;
  237. let x;
  238. let y;
  239. sliderHeight = this.slider.height();
  240. switch (this.mode) {
  241. case 'wheel':
  242. // Set grid position
  243. r = clamp(Math.ceil(hsb.s * 0.75), 0, gridHeight / 2);
  244. phi = hsb.h * Math.PI / 180;
  245. x = clamp(75 - Math.cos(phi) * r, 0, gridWidth);
  246. y = clamp(75 - Math.sin(phi) * r, 0, gridHeight);
  247. this.grid.css({ backgroundColor: 'transparent' }).find('.cp-picker').css({
  248. top: y,
  249. left: x
  250. });
  251. // Set slider position
  252. y = 150 - (hsb.b / (100 / gridHeight));
  253. if (hex === '') y = 0;
  254. this.slider.find('.cp-picker').css({ top: y });
  255. // Update panel color
  256. this.slider.css({
  257. backgroundColor: hsb2hex({
  258. h: hsb.h,
  259. s: hsb.s,
  260. b: 100
  261. })
  262. });
  263. break;
  264. case 'saturation':
  265. // Set grid position
  266. x = clamp((5 * hsb.h) / 12, 0, 150);
  267. y = clamp(gridHeight - Math.ceil(hsb.b / (100 / gridHeight)), 0, gridHeight);
  268. this.grid.find('.cp-picker').css({
  269. top: y,
  270. left: x
  271. });
  272. // Set slider position
  273. y = clamp(sliderHeight - (hsb.s * (sliderHeight / 100)), 0, sliderHeight);
  274. this.slider.find('.cp-picker').css({ top: y });
  275. // Update UI
  276. this.slider.css({
  277. backgroundColor: hsb2hex({
  278. h: hsb.h,
  279. s: 100,
  280. b: hsb.b
  281. })
  282. });
  283. this.grid.find('.cp-grid-inner').css({ opacity: hsb.s / 100 });
  284. break;
  285. case 'brightness':
  286. // Set grid position
  287. x = clamp((5 * hsb.h) / 12, 0, 150);
  288. y = clamp(gridHeight - Math.ceil(hsb.s / (100 / gridHeight)), 0, gridHeight);
  289. this.grid.find('.cp-picker').css({
  290. top: y,
  291. left: x
  292. });
  293. // Set slider position
  294. y = clamp(sliderHeight - (hsb.b * (sliderHeight / 100)), 0, sliderHeight);
  295. this.slider.find('.cp-picker').css({ top: y });
  296. // Update UI
  297. this.slider.css({
  298. backgroundColor: hsb2hex({
  299. h: hsb.h,
  300. s: hsb.s,
  301. b: 100
  302. })
  303. });
  304. this.grid.find('.cp-grid-inner').css({ opacity: 1 - (hsb.b / 100) });
  305. break;
  306. case 'hue':
  307. default:
  308. // Set grid position
  309. x = clamp(Math.ceil(hsb.s / (100 / gridWidth)), 0, gridWidth);
  310. y = clamp(gridHeight - Math.ceil(hsb.b / (100 / gridHeight)), 0, gridHeight);
  311. this.grid.find('.cp-picker').css({
  312. top: y,
  313. left: x
  314. });
  315. // Set slider position
  316. y = clamp(sliderHeight - (hsb.h / (360 / sliderHeight)), 0, sliderHeight);
  317. this.slider.find('.cp-picker').css({ top: y });
  318. // Update panel color
  319. this.grid.css({
  320. backgroundColor: hsb2hex({
  321. h: hsb.h,
  322. s: 100,
  323. b: 100
  324. })
  325. });
  326. break;
  327. }
  328. }
  329. if (!dontFireEvent) { element.val(this.getValue(hex)); }
  330. (this.element || element).trigger('change._grav_colorpicker', [element, hex, opacity]);
  331. }
  332. updateFromPicker(input, target) {
  333. var getCoords = function(picker, container) {
  334. var left, top;
  335. if (!picker.length || !container) return null;
  336. left = picker[0].getBoundingClientRect().left;
  337. top = picker[0].getBoundingClientRect().top;
  338. return {
  339. x: left - container[0].getBoundingClientRect().left + (picker[0].offsetWidth / 2),
  340. y: top - container[0].getBoundingClientRect().top + (picker[0].offsetHeight / 2)
  341. };
  342. };
  343. let hex;
  344. let hue;
  345. let saturation;
  346. let brightness;
  347. let x;
  348. let y;
  349. let r;
  350. let phi;
  351. // Panel objects
  352. let grid = this.wrapper.find('.cp-grid');
  353. let slider = this.wrapper.find('.cp-slider');
  354. let opacitySlider = this.wrapper.find('.cp-opacity-slider');
  355. // Picker objects
  356. let gridPicker = grid.find('.cp-picker');
  357. let sliderPicker = slider.find('.cp-picker');
  358. let opacityPicker = opacitySlider.find('.cp-picker');
  359. // Picker positions
  360. let gridPos = getCoords(gridPicker, grid);
  361. let sliderPos = getCoords(sliderPicker, slider);
  362. let opacityPos = getCoords(opacityPicker, opacitySlider);
  363. // Sizes
  364. let gridWidth = grid[0].getBoundingClientRect().width;
  365. let gridHeight = grid[0].getBoundingClientRect().height;
  366. let sliderHeight = slider[0].getBoundingClientRect().height;
  367. let opacitySliderHeight = opacitySlider[0].getBoundingClientRect().height;
  368. let value = this.element.val();
  369. value = rgbstr2hex(value) || value;
  370. if (!(hex = parseHex(value))) { hex = '#ffffff'; }
  371. // Handle colors
  372. if (target.hasClass('cp-grid') || target.hasClass('cp-slider')) {
  373. // Determine HSB values
  374. switch (this.mode) {
  375. case 'wheel':
  376. // Calculate hue, saturation, and brightness
  377. x = (gridWidth / 2) - gridPos.x;
  378. y = (gridHeight / 2) - gridPos.y;
  379. r = Math.sqrt(x * x + y * y);
  380. phi = Math.atan2(y, x);
  381. if (phi < 0) phi += Math.PI * 2;
  382. if (r > 75) {
  383. r = 75;
  384. gridPos.x = 69 - (75 * Math.cos(phi));
  385. gridPos.y = 69 - (75 * Math.sin(phi));
  386. }
  387. saturation = clamp(r / 0.75, 0, 100);
  388. hue = clamp(phi * 180 / Math.PI, 0, 360);
  389. brightness = clamp(100 - Math.floor(sliderPos.y * (100 / sliderHeight)), 0, 100);
  390. hex = hsb2hex({
  391. h: hue,
  392. s: saturation,
  393. b: brightness
  394. });
  395. // Update UI
  396. slider.css({
  397. backgroundColor: hsb2hex({
  398. h: hue,
  399. s: saturation,
  400. b: 100
  401. })
  402. });
  403. break;
  404. case 'saturation':
  405. // Calculate hue, saturation, and brightness
  406. hue = clamp(parseInt(gridPos.x * (360 / gridWidth), 10), 0, 360);
  407. saturation = clamp(100 - Math.floor(sliderPos.y * (100 / sliderHeight)), 0, 100);
  408. brightness = clamp(100 - Math.floor(gridPos.y * (100 / gridHeight)), 0, 100);
  409. hex = hsb2hex({
  410. h: hue,
  411. s: saturation,
  412. b: brightness
  413. });
  414. // Update UI
  415. slider.css({
  416. backgroundColor: hsb2hex({
  417. h: hue,
  418. s: 100,
  419. b: brightness
  420. })
  421. });
  422. grid.find('.cp-grid-inner').css({ opacity: saturation / 100 });
  423. break;
  424. case 'brightness':
  425. // Calculate hue, saturation, and brightness
  426. hue = clamp(parseInt(gridPos.x * (360 / gridWidth), 10), 0, 360);
  427. saturation = clamp(100 - Math.floor(gridPos.y * (100 / gridHeight)), 0, 100);
  428. brightness = clamp(100 - Math.floor(sliderPos.y * (100 / sliderHeight)), 0, 100);
  429. hex = hsb2hex({
  430. h: hue,
  431. s: saturation,
  432. b: brightness
  433. });
  434. // Update UI
  435. slider.css({
  436. backgroundColor: hsb2hex({
  437. h: hue,
  438. s: saturation,
  439. b: 100
  440. })
  441. });
  442. grid.find('.cp-grid-inner').css({ opacity: 1 - (brightness / 100) });
  443. break;
  444. default:
  445. // Calculate hue, saturation, and brightness
  446. hue = clamp(360 - parseInt(sliderPos.y * (360 / sliderHeight), 10), 0, 360);
  447. saturation = clamp(Math.floor(gridPos.x * (100 / gridWidth)), 0, 100);
  448. brightness = clamp(100 - Math.floor(gridPos.y * (100 / gridHeight)), 0, 100);
  449. hex = hsb2hex({
  450. h: hue,
  451. s: saturation,
  452. b: brightness
  453. });
  454. // Update UI
  455. grid.css({
  456. backgroundColor: hsb2hex({
  457. h: hue,
  458. s: 100,
  459. b: 100
  460. })
  461. });
  462. break;
  463. }
  464. }
  465. // Handle opacity
  466. if (target.hasClass('cp-opacity-slider')) {
  467. this.opacity = parseFloat(1 - (opacityPos.y / opacitySliderHeight)).toFixed(2);
  468. }
  469. // Adjust case
  470. input.val(this.getValue(hex));
  471. // Handle change event
  472. this.element.trigger('change._grav_colorpicker', [this.element, hex, this.opacity]);
  473. }
  474. getValue(hex) {
  475. if (this.opacity === 1) { return hex; }
  476. let rgb = hex2rgb(hex);
  477. return 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + this.opacity + ')';
  478. }
  479. bound(name) {
  480. let bound = this._bound || (this._bound = {});
  481. return bound[name] || (bound[name] = bind(this[name], this));
  482. }
  483. }
  484. export let Instance = new ColorpickerField('[data-grav-colorpicker]');