PluginProbe
Customify / trunk
Customify vtrunk
2.10.9 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.7.1 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.6.0 1.6.0.1 1.6.5 1.7.0 1.7.1 All 77 releases
customify / js / customizer / font-fields.js

font-fields.js in Customify trunk, at js/customizer/font-fields.js

620 lines 22.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /** @namespace customify */
2 window.customify = window.customify || parent.customify || {};
3
4 // This is for the Customizer Font control
5 (function ($, customify, wp) {
6
7 /**
8 * Expose the API publicly on window.customify.fontFields
9 *
10 * @namespace customify.fontFields
11 */
12 if ( typeof customify.fontFields === 'undefined' ) {
13 customify.fontFields = {}
14 }
15 _.extend( customify.fontFields, function () {
16 const wrapperSelector = '.font-options__wrapper',
17 valueHolderSelector = '.customify_font_values',
18 fontFamilySelector = '.customify_font_family',
19 fontVariantSelector = '.customify_font_weight',
20 fontHeadTitleSelector = '.font-options__head .font-options__font-title'
21
22 let familyPlaceholderText ,
23 variantAutoText // This is for the empty value.
24
25 const api = wp.customize
26
27 // We will use this to remember that we are self-updating the field from the subfields.
28 // We will save this info for each setting ID.
29 const updatingValue = {},
30 loadingValue = {}
31
32 const init = function () {
33 familyPlaceholderText = customify.l10n.fonts.familyPlaceholderText
34 variantAutoText = customify.l10n.fonts.variantAutoText
35
36 const $fontFamilyFields = $(fontFamilySelector)
37
38 // Add the Google Fonts opts to each control.
39 if (typeof api.settings['google_fonts_opts'] !== 'undefined') {
40 $fontFamilyFields.each(function (i, el) {
41 const googleOptionsPlaceholder = $(el).find('.google-fonts-opts-placeholder').first()
42 if (googleOptionsPlaceholder) {
43 // Replace the placeholder with the HTML for the Google fonts select options.
44 googleOptionsPlaceholder.replaceWith(api.settings['google_fonts_opts'])
45
46 // The active font family might be a Google font so we need to set the current value after we've added the options.
47 const activeFontFamily = $(el).data('active_font_family')
48 if (typeof activeFontFamily !== 'undefined') {
49 $(el).val(activeFontFamily)
50 }
51 }
52 })
53 }
54
55 // Initialize the select2 field for the font family
56 $fontFamilyFields.select2({
57 placeholder: familyPlaceholderText
58 })
59
60 // We only need to bind to the original select since select2 triggers the event for this one when changed.
61 $fontFamilyFields.on('change', function (event, who) {
62 const newFontFamily = event.target.value,
63 wrapper = $(event.target).closest(wrapperSelector),
64 settingID = $(wrapper.children(valueHolderSelector)).data('customize-setting-link'),
65 setting = api(settingID)
66
67 // Get the new font details
68 const newFontDetails = getFontDetails(newFontFamily)
69
70 // Update the font field head title (with the new font family name).
71 updateFontHeadTitle(newFontDetails, wrapper)
72
73 // Update the variant subfield with the new options given by the selected font family.
74 updateVariantField(newFontDetails, wrapper)
75
76 if (typeof who !== 'undefined' && who === 'customify') {
77 // The change was triggered programmatically by Customify.
78 // No need to self-update the value.
79 } else {
80 // Mark this input as touched by the user.
81 $(event.target).data('touched', true)
82
83 // Serialize subfield values and refresh the fonts in the preview window.
84 selfUpdateValue(wrapper, settingID)
85 }
86 })
87
88 // Handle the reverse value direction, when the customize setting is updated and the subfields need to update their values.
89 $fontFamilyFields.each(function (i, el) {
90 const wrapper = $(el).closest(wrapperSelector),
91 settingID = $(wrapper.children(valueHolderSelector)).data('customize-setting-link'),
92 setting = api(settingID)
93
94 setting.bind(function (newValue, oldValue) {
95 if (!updatingValue[this.id]) {
96 loadFontValue(wrapper, newValue, this.id)
97 }
98 })
99 })
100
101 // Initialize the select2 field for the font variant
102 initSubfield($(fontVariantSelector), true)
103
104 // Initialize all the regular selects in the font subfields
105 initSubfield($fontFamilyFields.parents(wrapperSelector).find('select').not('select[class*=\' select2\'],select[class^=\'select2\']'), false);
106
107 // Initialize the all the range fields in the font subfields
108 initSubfield($fontFamilyFields.parents(wrapperSelector).find('input[type=range]'), false);
109
110 handleFontPopupToggle()
111 }
112
113 const initSubfield = function (subfieldList, select2 = false, select2Placeholder = '') {
114 if (!subfieldList.length) {
115 return
116 }
117 // Mark these as not touched by the user.
118 $(subfieldList).data('touched', false)
119
120 $(subfieldList).on('input change', function (event, who) {
121 if (typeof who !== 'undefined' && who === 'customify') {
122 // The change was triggered programmatically by Customify.
123 // No need to self-update the value.
124 } else {
125 const wrapper = $(event.target).closest(wrapperSelector),
126 settingID = $(wrapper.children(valueHolderSelector)).data('customize-setting-link')
127
128 // Mark this input as touched by the user.
129 $(event.target).data('touched', true)
130
131 // Gather subfield values and trigger refresh of the fonts in the preview window.
132 selfUpdateValue(wrapper, settingID)
133 }
134 })
135
136 // If we've been instructed, initialize a select2.
137 if (true === select2) {
138 const select2Args = {}
139
140 if (!_.isEmpty(select2Placeholder)) {
141 select2Args['placeholder'] = select2Placeholder
142 }
143
144 $(subfieldList).select2(select2Args)
145 }
146 }
147
148 const handleFontPopupToggle = function () {
149 const $allFontCheckboxes = $('#customize-controls .js-font-option-toggle')
150
151 // Close all other font fields popups when opening a font field popup.
152 $allFontCheckboxes.on('click', function () {
153 const $checkbox = $(this)
154 if ($checkbox.prop('checked') === true) {
155 $allFontCheckboxes.not($checkbox).prop('checked', false)
156 }
157 })
158
159 // Make sure that all fonts popups are closed when backing away from a panel or section.
160 // @todo This doesn't catch backing with ESC key. For that we should hook on Customizer section and panel events ('collapsed').
161 $('#customize-controls .customize-panel-back, #customize-controls .customize-section-back').on('click', function() {
162 $allFontCheckboxes.prop('checked', false)
163 })
164 }
165
166 /**
167 * Update the title of the font field (the field head) with the new font family name.
168 *
169 * @param newFontDetails
170 * @param wrapper
171 */
172 const updateFontHeadTitle = function (newFontDetails, wrapper) {
173 const fontTitleElement = wrapper.find(fontHeadTitleSelector)
174
175 let fontFamilyDisplay = newFontDetails.family
176 if (typeof newFontDetails.family_display === 'string' && newFontDetails.family_display.length ) {
177 fontFamilyDisplay = newFontDetails.family_display
178 }
179
180 $(fontTitleElement).html(fontFamilyDisplay)
181 }
182
183 /**
184 * This function updates the data in font weight selector from the given <option> element
185 *
186 * @param newFontDetails
187 * @param wrapper
188 */
189 const updateVariantField = function (newFontDetails, wrapper) {
190 const variants = typeof newFontDetails.variants !== 'undefined' ? newFontDetails.variants : [],
191 fontVariantInput = wrapper.find(fontVariantSelector),
192 selectedVariant = fontVariantInput.val() ? fontVariantInput.val() : '',
193 newVariants = []
194
195 // We clear everything about this subfield.
196 fontVariantInput.val(null).empty()
197 if (fontVariantInput.hasClass("select2-hidden-accessible")) {
198 fontVariantInput.select2('destroy')
199 }
200
201 // Mark this input as not touched by the user.
202 fontVariantInput.data('touched', false)
203
204 if (typeof variants === 'undefined' || Object.keys(variants).length < 2) {
205 fontVariantInput.parent().hide()
206 fontVariantInput.parent().prev('label').hide()
207 // Mark this input as disabled.
208 fontVariantInput.data('disabled', true)
209 return
210 }
211
212 // Initialize the options with an empty one.
213 newVariants.push({
214 'id': '',
215 'text': variantAutoText
216 })
217
218 // we need to turn the data array into a specific form like [{id:"id", text:"Text"}]
219 $.each(variants, function (index, variant) {
220 let newVariant = {
221 'id': variant, // This is the option value.
222 'text': variant
223 }
224
225 // Leave the comparison loose.
226 if (selectedVariant == variant) {
227 newVariant.selected = true
228 }
229
230 newVariants.push(newVariant)
231 })
232
233 // Only reinitialize the select2.
234 // No need to rebind on change or on input since those are still bound to the original HTML element.
235 fontVariantInput.select2({
236 data: newVariants
237 })
238
239 fontVariantInput.parent().show()
240 fontVariantInput.parent().prev('label').show()
241 // Mark this input as enabled.
242 fontVariantInput.data('disabled', false)
243 }
244
245 /**
246 * Gather the value for our entire font field and save it in the setting.
247 */
248 const selfUpdateValue = function (wrapper, settingID) {
249 // If we are already self-updating this and we haven't finished, we need to stop here to prevent infinite loops
250 // This call might have come from a subfield detecting the change thus triggering a further selfUpdateValue()
251 if (true === updatingValue[settingID]) {
252 return
253 }
254
255 // If we are loading this setting value and haven't finished,
256 // there is no point in updating it as this would cause infinite loops.
257 if (true === loadingValue[settingID]) {
258 return
259 }
260
261 // Mark the fact that we are self-updating the field value
262 updatingValue[settingID] = true
263
264 const optionsList = wrapper.find('.font-options__options-list'),
265 inputs = optionsList.find('[data-value_entry]'),
266 setting = api(settingID),
267 oldValue = setting(),
268 newFontData = _.isEmpty(oldValue) ? {} : $.extend(true, {}, oldValue)
269
270 inputs.each(function (key, input) {
271 const $input = $(input)
272 const valueEntry = $input.data('value_entry')
273 let value = $input.val()
274
275 // We only pick up subfields values that have been touched by the user, that are enabled (visible) or values that are missing in the oldValue.
276 if (_.isUndefined(valueEntry) || $input.data('disabled') || (!$input.data('touched') && !_.isUndefined(newFontData[valueEntry]))) {
277 return
278 }
279
280 if ('font_family' === valueEntry) {
281 // Get the src of the selected option.
282 const src = $(input.options[input.selectedIndex]).data('src')
283
284 if (src) {
285 newFontData['src'] = src
286 } else {
287 delete newFontData['src']
288 }
289 }
290
291 if (!_.isUndefined(value) && !_.isNull(value) && value !== '') {
292 if (_.includes(['letter_spacing', 'line_height', 'font_size'], valueEntry)) {
293 // Standardize the value.
294 value = standardizeNumericalValue(value, input, false)
295 }
296
297 newFontData[valueEntry] = value
298 } else {
299 delete newFontData[valueEntry]
300 }
301 })
302
303 // We don't need to store font variants or subsets list in the value
304 // since we will get those from the global font details.
305 delete newFontData['variants']
306 delete newFontData['subsets']
307
308 // We need to make sure that we don't "use" any variants not supported by the new font (values passed over from the old value).
309 // Get the new font details
310 const newFontDetails = getFontDetails(newFontData['font_family'])
311 // Check the font variant
312 if (typeof newFontData['font_variant'] !== 'undefined' && typeof newFontDetails.variants !== 'undefined' && Object.keys(newFontDetails.variants).length > 0) {
313 // Make sure that the font_variant is a string, not a number.
314 newFontData['font_variant'] = String(newFontData['font_variant'])
315
316 if (!_.includes(newFontDetails.variants, newFontData['font_variant'])) {
317 // The new font doesn't have this variant. Nor should the value.
318 delete newFontData['font_variant']
319 }
320 } else {
321 // The new font has no variants. Nor should the value.
322 delete newFontData['font_variant']
323 }
324
325 // Update the Customizer setting value.
326 setting.set(newFontData)
327
328 // Finished with the field value self-updating.
329 updatingValue[settingID] = false
330 }
331
332 /**
333 * This function is a reverse of selfUpdateValue(), initializing the entire font field controls
334 * based on the setting value.
335 */
336 const loadFontValue = function (wrapper, value, settingID) {
337 // If we are already loading this setting value and haven't finished, there is no point in starting again.
338 if (true === loadingValue[settingID]) {
339 return
340 }
341
342 // Mark the fact that we are loading the field value
343 loadingValue[settingID] = true
344
345 const optionsList = $(wrapper).find('.font-options__options-list'),
346 inputs = optionsList.find('[data-value_entry]')
347
348 inputs.each(function (key, input) {
349 const $input = $(input)
350 const valueEntry = $input.data('value_entry')
351
352 // In the case of select2, only the original selects have the data field, thus excluding select2 created select DOM elements
353 if (typeof valueEntry === 'undefined' || valueEntry === '' || typeof value[valueEntry] === 'undefined') {
354 return
355 }
356
357 // We will do this only for numerical sub-fields.
358 if (_.includes(['letter_spacing', 'line_height', 'font_size'], valueEntry)) {
359 const subfieldValue = standardizeNumericalValue(value[valueEntry], input)
360
361 // Make sure that the unit and value_unit attributes are in place.
362 if (subfieldValue.unit !== '') {
363 $input.data('value_unit', subfieldValue.unit)
364 if (_.isEmpty($input.attr('unit'))) {
365 $input.attr('unit', subfieldValue.unit)
366 }
367 }
368
369 // If the field unit and value unit differ, we have some conversion to do.
370 // We will convert the received value to the appropriate unit declared by the input.
371 // We will use a guessed base size of 16px. Not an exact conversion, but it will have to do.
372 const baseSize = 16
373 const subfieldUnit = $input.attr('unit').trim().toLowerCase()
374 const subfieldValueUnit = $input.data('value_unit').trim().toLowerCase()
375 // The comparison is intentionally loose.
376 if (subfieldUnit != subfieldValueUnit) {
377 if (_.includes(['em', 'rem'], subfieldValueUnit) && 'px' === subfieldUnit) {
378 // We will have to multiply the value.
379 subfieldValue.value = round(subfieldValue.value * baseSize, customify.fonts.floatPrecision)
380 } else if (_.includes(['em', 'rem'], subfieldUnit) && 'px' === subfieldValueUnit) {
381 // We will have to divide the value.
382 subfieldValue.value = round(subfieldValue.value / baseSize, customify.fonts.floatPrecision)
383 }
384 }
385
386 // If this field has a min/max attribute we need to make sure that those attributes allow for the value we are trying to impose.
387 if ($input.attr('min') && $input.attr('min') > subfieldValue.value) {
388 $input.attr('min', subfieldValue.value)
389 }
390 if ($input.attr('max') && $input.attr('max') < subfieldValue.value) {
391 $input.attr('max', subfieldValue.value)
392 }
393
394 $input.val(subfieldValue.value)
395 } else {
396 $input.val(value[valueEntry])
397 }
398
399 // Mark this input as not touched by the user.
400 $input.data('touched', false)
401
402 $input.trigger('change', ['customify'])
403 })
404
405 // Finished with the field value loading.
406 loadingValue[settingID] = false
407 }
408
409 /**
410 * Given a value we will standardize it to an array with 'value' and 'unit'.
411 *
412 * This is a mirror logic of the server-side one from Customify_Fonts_Global::standardizeNumericalValue()
413 *
414 * @param value
415 * @param input Optional. The input this value was extracted from
416 * @param valueFirst Optional. Whether to give higher priority to value related data, or to input related one.
417 */
418 const standardizeNumericalValue = function (value, input = false, valueFirst = true) {
419 const standardValue = {value: false, unit: false}
420
421 if (_.includes(['','false',false], value)) {
422 return standardValue
423 }
424
425 if (!isNaN(value)) {
426 standardValue.value = value
427 } else if (typeof value.value !== 'undefined') {
428 standardValue.value = value.value
429 if (typeof value.unit !== 'undefined') {
430 standardValue.unit = value.unit
431 }
432 } else if (typeof value[0] !== 'undefined') {
433 standardValue.value = value[0]
434 if (typeof value[1] !== 'undefined') {
435 standardValue.unit = value[1]
436 }
437 } else if (typeof value === 'string') {
438 // We will get everything in front that is a valid part of a number (float including).
439 const matches = value.match(/^([\d.\-+]+)(.+)/i)
440 if (matches !== null && typeof matches[1] !== 'undefined') {
441 standardValue.value = matches[1]
442 if (!_.isEmpty(matches[2])) {
443 standardValue.unit = matches[2]
444 }
445 } else {
446 // If we could not extract anything useful we will trust the developer and leave it like that.
447 standardValue.value = value
448 }
449 }
450
451 if (false !== input && (false === standardValue.unit || _.isEmpty(standardValue.unit))) {
452 // If we are given an input, we will attempt to extract the unit from its attributes.
453 let fallbackInputUnit = ''
454 const $input = $(input)
455
456 if (valueFirst) {
457 if (!_.isEmpty($input.data('value_unit'))) {
458 fallbackInputUnit = $input.data('value_unit')
459 } else if (!_.isEmpty($input.attr('unit'))) {
460 fallbackInputUnit = $input.attr('unit')
461 }
462 } else {
463 if (!_.isEmpty($input.attr('unit'))) {
464 fallbackInputUnit = $input.attr('unit')
465 } else if (!_.isEmpty($input.data('value_unit'))) {
466 fallbackInputUnit = $input.data('value_unit')
467 }
468 }
469 standardValue.unit = fallbackInputUnit
470 }
471
472 // Make sure that if we have a numerical value, it is a float.
473 if (!isNaN(standardValue.value)) {
474 standardValue.value = parseFloat(standardValue.value);
475 }
476
477 return standardValue
478 }
479
480 const determineFontType = function (fontFamily) {
481 // The default is a standard font (aka no special loading or processing).
482 let fontType = 'system_font'
483
484 // We will follow a stack in the following order: third-party fonts, cloud fonts, theme fonts, Google fonts, standard fonts.
485 if (typeof customify.fonts.third_party_fonts[fontFamily] !== 'undefined') {
486 fontType = 'third_party_font'
487 } else if (typeof customify.fonts.cloud_fonts[fontFamily] !== 'undefined') {
488 fontType = 'cloud_font'
489 } else if (typeof customify.fonts.theme_fonts[fontFamily] !== 'undefined') {
490 fontType = 'theme_font'
491 } else if (typeof customify.fonts.google_fonts[fontFamily] !== 'undefined') {
492 fontType = 'google_font'
493 }
494
495 return fontType
496 }
497
498 const getFontDetails = function (fontFamily, fontType = false) {
499 if (false === fontType) {
500 // We will determine the font type based on font family.
501 fontType = determineFontType(fontFamily)
502 }
503
504 switch (fontType) {
505 case 'theme_font':
506 return customify.fonts.theme_fonts[fontFamily]
507 break
508 case 'cloud_font':
509 return customify.fonts.cloud_fonts[fontFamily]
510 break
511 case 'google_font':
512 return customify.fonts.google_fonts[fontFamily]
513 break
514 case 'system_font':
515 if (typeof customify.fonts.system_fonts[fontFamily] !== 'undefined') {
516 return customify.fonts.system_fonts[fontFamily]
517 }
518 break
519 case 'third_party_font':
520 if (typeof customify.fonts.third_party_fonts[fontFamily] !== 'undefined') {
521 return customify.fonts.third_party_fonts[fontFamily]
522 }
523 break
524 default:
525 }
526
527 return false
528 }
529
530 /**
531 * Will convert an array of CSS like variants into their FVD equivalents. Web Font Loader expects this format.
532 * @link https://github.com/typekit/fvd
533 */
534 const convertFontVariantToFVD = function (variant) {
535 variant = String(variant)
536
537 let fontStyle = 'n' // normal
538 if (-1 !== variant.indexOf('italic')) {
539 fontStyle = 'i'
540 variant = variant.replace('italic', '')
541 } else if (-1 !== variant.indexOf('oblique')) {
542 fontStyle = 'o'
543 variant = variant.replace('oblique', '')
544 }
545
546 let fontWeight
547
548 // The equivalence:
549 //
550 // 1: 100
551 // 2: 200
552 // 3: 300
553 // 4: 400 (default, also recognized as 'normal')
554 // 5: 500
555 // 6: 600
556 // 7: 700 (also recognized as 'bold')
557 // 8: 800
558 // 9: 900
559
560 switch (variant) {
561 case '100':
562 fontWeight = '1'
563 break
564 case '200':
565 fontWeight = '2'
566 break
567 case '300':
568 fontWeight = '3'
569 break
570 case '500':
571 fontWeight = '5'
572 break
573 case '600':
574 fontWeight = '6'
575 break
576 case '700':
577 case 'bold':
578 fontWeight = '7'
579 break
580 case '800':
581 fontWeight = '8'
582 break
583 case '900':
584 fontWeight = '9'
585 break
586 default:
587 fontWeight = '4'
588 break
589 }
590
591 return fontStyle + fontWeight
592 }
593
594 /**
595 * Round a number to a precision, specified in number of decimal places
596 *
597 * @param {number} number - The number to round
598 * @param {number} precision - The number of decimal places to round to:
599 * > 0 means decimals, < 0 means powers of 10
600 *
601 *
602 * @return {number} - The number, rounded
603 */
604 const round = function (number, precision) {
605 const factor = Math.pow(10, precision)
606 return Math.round(number * factor) / factor;
607 }
608
609 return {
610 init: init,
611 selfUpdateValue: selfUpdateValue,
612 standardizeNumericalValue: standardizeNumericalValue,
613 determineFontType: determineFontType,
614 getFontDetails: getFontDetails,
615 convertFontVariantToFVD: convertFontVariantToFVD,
616 round: round,
617 }
618 }() )
619 })(jQuery, customify, wp)
620