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