color.es6.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. /**
  2. * @file
  3. * Attaches the behaviors for the Color module.
  4. */
  5. (function($, Drupal) {
  6. /**
  7. * Displays farbtastic color selector and initialize color administration UI.
  8. *
  9. * @type {Drupal~behavior}
  10. *
  11. * @prop {Drupal~behaviorAttach} attach
  12. * Attach color selection behavior to relevant context.
  13. */
  14. Drupal.behaviors.color = {
  15. attach(context, settings) {
  16. let i;
  17. let j;
  18. let colors;
  19. // This behavior attaches by ID, so is only valid once on a page.
  20. const form = $(context)
  21. .find('#system-theme-settings .color-form')
  22. .once('color');
  23. if (form.length === 0) {
  24. return;
  25. }
  26. const inputs = [];
  27. const hooks = [];
  28. const locks = [];
  29. let focused = null;
  30. // Add Farbtastic.
  31. $('<div class="color-placeholder"></div>')
  32. .once('color')
  33. .prependTo(form);
  34. const farb = $.farbtastic('.color-placeholder');
  35. // Decode reference colors to HSL.
  36. const reference = settings.color.reference;
  37. Object.keys(reference || {}).forEach(color => {
  38. reference[color] = farb.RGBToHSL(farb.unpack(reference[color]));
  39. });
  40. // Build a preview.
  41. const height = [];
  42. const width = [];
  43. /**
  44. * Renders the preview.
  45. */
  46. function preview() {
  47. Drupal.color.callback(context, settings, form, farb, height, width);
  48. }
  49. /**
  50. * Resets the color scheme selector.
  51. */
  52. function resetScheme() {
  53. form.find('#edit-scheme').each(function() {
  54. this.selectedIndex = this.options.length - 1;
  55. });
  56. }
  57. /**
  58. * Shifts a given color, using a reference pair (ref in HSL).
  59. *
  60. * This algorithm ensures relative ordering on the saturation and
  61. * luminance axes is preserved, and performs a simple hue shift.
  62. *
  63. * It is also symmetrical. If: shiftColor(c, a, b) === d, then
  64. * shiftColor(d, b, a) === c.
  65. *
  66. * @function Drupal.color~shiftColor
  67. *
  68. * @param {string} given
  69. * A hex color code to shift.
  70. * @param {Array.<number>} ref1
  71. * First HSL color reference.
  72. * @param {Array.<number>} ref2
  73. * Second HSL color reference.
  74. *
  75. * @return {string}
  76. * A hex color, shifted.
  77. */
  78. function shiftColor(given, ref1, ref2) {
  79. let d;
  80. // Convert to HSL.
  81. given = farb.RGBToHSL(farb.unpack(given));
  82. // Hue: apply delta.
  83. given[0] += ref2[0] - ref1[0];
  84. // Saturation: interpolate.
  85. if (ref1[1] === 0 || ref2[1] === 0) {
  86. given[1] = ref2[1];
  87. } else {
  88. d = ref1[1] / ref2[1];
  89. if (d > 1) {
  90. given[1] /= d;
  91. } else {
  92. given[1] = 1 - (1 - given[1]) * d;
  93. }
  94. }
  95. // Luminance: interpolate.
  96. if (ref1[2] === 0 || ref2[2] === 0) {
  97. given[2] = ref2[2];
  98. } else {
  99. d = ref1[2] / ref2[2];
  100. if (d > 1) {
  101. given[2] /= d;
  102. } else {
  103. given[2] = 1 - (1 - given[2]) * d;
  104. }
  105. }
  106. return farb.pack(farb.HSLToRGB(given));
  107. }
  108. /**
  109. * Callback for Farbtastic when a new color is chosen.
  110. *
  111. * @param {HTMLElement} input
  112. * The input element where the color is chosen.
  113. * @param {string} color
  114. * The color that was chosen through the input.
  115. * @param {bool} propagate
  116. * Whether or not to propagate the color to a locked pair value
  117. * @param {bool} colorScheme
  118. * Flag to indicate if the user is using a color scheme when changing
  119. * the color.
  120. */
  121. function callback(input, color, propagate, colorScheme) {
  122. let matched;
  123. // Set background/foreground colors.
  124. $(input).css({
  125. backgroundColor: color,
  126. color: farb.RGBToHSL(farb.unpack(color))[2] > 0.5 ? '#000' : '#fff',
  127. });
  128. // Change input value.
  129. if ($(input).val() && $(input).val() !== color) {
  130. $(input).val(color);
  131. // Update locked values.
  132. if (propagate) {
  133. i = input.i;
  134. for (j = i + 1; ; ++j) {
  135. if (!locks[j - 1] || $(locks[j - 1]).is('.is-unlocked')) {
  136. break;
  137. }
  138. matched = shiftColor(
  139. color,
  140. reference[input.key],
  141. reference[inputs[j].key],
  142. );
  143. callback(inputs[j], matched, false);
  144. }
  145. for (j = i - 1; ; --j) {
  146. if (!locks[j] || $(locks[j]).is('.is-unlocked')) {
  147. break;
  148. }
  149. matched = shiftColor(
  150. color,
  151. reference[input.key],
  152. reference[inputs[j].key],
  153. );
  154. callback(inputs[j], matched, false);
  155. }
  156. // Update preview.
  157. preview();
  158. }
  159. // Reset colorScheme selector.
  160. if (!colorScheme) {
  161. resetScheme();
  162. }
  163. }
  164. }
  165. // Loop through all defined gradients.
  166. Object.keys(settings.gradients || {}).forEach(i => {
  167. // Add element to display the gradient.
  168. $('.color-preview')
  169. .once('color')
  170. .append(`<div id="gradient-${i}"></div>`);
  171. const gradient = $(`.color-preview #gradient-${i}`);
  172. // Add height of current gradient to the list (divided by 10).
  173. height.push(parseInt(gradient.css('height'), 10) / 10);
  174. // Add width of current gradient to the list (divided by 10).
  175. width.push(parseInt(gradient.css('width'), 10) / 10);
  176. // Add rows (or columns for horizontal gradients).
  177. // Each gradient line should have a height (or width for horizontal
  178. // gradients) of 10px (because we divided the height/width by 10
  179. // above).
  180. for (
  181. j = 0;
  182. j <
  183. (settings.gradients[i].direction === 'vertical'
  184. ? height[i]
  185. : width[i]);
  186. ++j
  187. ) {
  188. gradient.append('<div class="gradient-line"></div>');
  189. }
  190. });
  191. // Set up colorScheme selector.
  192. form.find('#edit-scheme').on('change', function() {
  193. const schemes = settings.color.schemes;
  194. const colorScheme = this.options[this.selectedIndex].value;
  195. if (colorScheme !== '' && schemes[colorScheme]) {
  196. // Get colors of active scheme.
  197. colors = schemes[colorScheme];
  198. Object.keys(colors || {}).forEach(fieldName => {
  199. callback(
  200. $(`#edit-palette-${fieldName}`),
  201. colors[fieldName],
  202. false,
  203. true,
  204. );
  205. });
  206. preview();
  207. }
  208. });
  209. /**
  210. * Focuses Farbtastic on a particular field.
  211. *
  212. * @param {jQuery.Event} e
  213. * The focus event on the field.
  214. */
  215. function focus(e) {
  216. const input = e.target;
  217. // Remove old bindings.
  218. if (focused) {
  219. $(focused)
  220. .off('keyup', farb.updateValue)
  221. .off('keyup', preview)
  222. .off('keyup', resetScheme)
  223. .parent()
  224. .removeClass('item-selected');
  225. }
  226. // Add new bindings.
  227. focused = input;
  228. farb.linkTo(color => {
  229. callback(input, color, true, false);
  230. });
  231. farb.setColor(input.value);
  232. $(focused)
  233. .on('keyup', farb.updateValue)
  234. .on('keyup', preview)
  235. .on('keyup', resetScheme)
  236. .parent()
  237. .addClass('item-selected');
  238. }
  239. // Initialize color fields.
  240. form
  241. .find('.js-color-palette input.form-text')
  242. .each(function() {
  243. // Extract palette field name.
  244. this.key = this.id.substring(13);
  245. // Link to color picker temporarily to initialize.
  246. farb
  247. .linkTo(() => {})
  248. .setColor('#000')
  249. .linkTo(this);
  250. // Add lock.
  251. const i = inputs.length;
  252. if (inputs.length) {
  253. let toggleClick = true;
  254. const lock = $(
  255. `<button class="color-palette__lock">${Drupal.t(
  256. 'Unlock',
  257. )}</button>`,
  258. ).on('click', function(e) {
  259. e.preventDefault();
  260. if (toggleClick) {
  261. $(this)
  262. .addClass('is-unlocked')
  263. .html(Drupal.t('Lock'));
  264. $(hooks[i - 1]).attr(
  265. 'class',
  266. locks[i - 2] && $(locks[i - 2]).is(':not(.is-unlocked)')
  267. ? 'color-palette__hook is-up'
  268. : 'color-palette__hook',
  269. );
  270. $(hooks[i]).attr(
  271. 'class',
  272. locks[i] && $(locks[i]).is(':not(.is-unlocked)')
  273. ? 'color-palette__hook is-down'
  274. : 'color-palette__hook',
  275. );
  276. } else {
  277. $(this)
  278. .removeClass('is-unlocked')
  279. .html(Drupal.t('Unlock'));
  280. $(hooks[i - 1]).attr(
  281. 'class',
  282. locks[i - 2] && $(locks[i - 2]).is(':not(.is-unlocked)')
  283. ? 'color-palette__hook is-both'
  284. : 'color-palette__hook is-down',
  285. );
  286. $(hooks[i]).attr(
  287. 'class',
  288. locks[i] && $(locks[i]).is(':not(.is-unlocked)')
  289. ? 'color-palette__hook is-both'
  290. : 'color-palette__hook is-up',
  291. );
  292. }
  293. toggleClick = !toggleClick;
  294. });
  295. $(this).after(lock);
  296. locks.push(lock);
  297. }
  298. // Add hook.
  299. const hook = $('<div class="color-palette__hook"></div>');
  300. $(this).after(hook);
  301. hooks.push(hook);
  302. $(this)
  303. .parent()
  304. .find('.color-palette__lock')
  305. .trigger('click');
  306. this.i = i;
  307. inputs.push(this);
  308. })
  309. .on('focus', focus);
  310. form.find('.js-color-palette label');
  311. // Focus first color.
  312. inputs[0].focus();
  313. // Render preview.
  314. preview();
  315. },
  316. };
  317. })(jQuery, Drupal);