PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-setup-wizard / step-appearance / _src / step-appearance.js

step-appearance.js in Booking Calendar 11.9, at includes/page-setup-wizard/step-appearance/_src/step-appearance.js

430 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Drive the Setup Wizard Appearance controls and request-local live preview.
3 *
4 * @package Booking Calendar
5 */
6 ( function ( window, document ) {
7 'use strict';
8
9 var module_config = window.wpbc_setup_wizard_appearance || { i18n: {} };
10
11 /**
12 * Create one Appearance step adapter for the shared wizard shell.
13 *
14 * @param {Object} config Server-owned preset and translation configuration.
15 * @return {Object} Step adapter.
16 */
17 function create_appearance_adapter( config ) {
18 var shell_api = null;
19 var editor = null;
20 var preview_frame_request = 0;
21
22 /**
23 * Return one translated string with a safe fallback.
24 *
25 * @param {string} key Translation key.
26 * @param {string} fallback Fallback text.
27 * @return {string} Translation or fallback.
28 */
29 function get_message( key, fallback ) {
30 return config.i18n && config.i18n[ key ] ? String( config.i18n[ key ] ) : fallback;
31 }
32
33 /**
34 * Return the current scalar Appearance values.
35 *
36 * @return {Object} Current values.
37 */
38 function get_values() {
39 var style = editor.node.querySelector( '[data-wpbc-appearance-style]:checked' );
40 var time_mode = editor.node.querySelector( '[data-wpbc-appearance-time-mode]:checked' );
41
42 return {
43 booking_form_style: style ? String( style.value || '' ) : '',
44 booking_form_accent_color: String( editor.accent_value.value || '' ).toUpperCase(),
45 booking_skin: String( editor.skin.value || '' ),
46 booking_timeslot_picker: time_mode ? String( time_mode.value || '' ) : ''
47 };
48 }
49
50 /**
51 * Update the selected presentation for style and time radios.
52 *
53 * @return {void}
54 */
55 function render_selected_controls() {
56 Array.prototype.slice.call( editor.node.querySelectorAll( '[data-wpbc-appearance-style]' ) ).forEach( function ( input ) {
57 input.closest( '.wpbc_setup_wizard__appearance-style-card' ).classList.toggle( 'is-selected', input.checked );
58 } );
59 Array.prototype.slice.call( editor.node.querySelectorAll( '[data-wpbc-appearance-time-mode]' ) ).forEach( function ( input ) {
60 input.closest( 'label' ).classList.toggle( 'is-selected', input.checked );
61 } );
62 }
63
64 /**
65 * Synchronize preset swatches and the Coloris field presentation.
66 *
67 * @param {string} color Canonical six-digit hexadecimal color.
68 * @return {void}
69 */
70 function render_accent_controls( color ) {
71 var coloris_field;
72
73 editor.accent_swatches.forEach( function ( swatch ) {
74 var is_selected = String( swatch.dataset.wpbcAppearanceAccentSwatch || '' ).toUpperCase() === color;
75
76 swatch.classList.toggle( 'is-selected', is_selected );
77 swatch.setAttribute( 'aria-pressed', is_selected ? 'true' : 'false' );
78 } );
79
80 coloris_field = editor.accent_value.closest( '.clr-field' );
81 if ( coloris_field && /^#[0-9A-F]{6}$/.test( color ) ) {
82 coloris_field.style.color = color;
83 }
84 }
85
86 /**
87 * Normalize the canonical Coloris accent field.
88 *
89 * @param {string} color Candidate six-digit hexadecimal color.
90 * @return {boolean} Whether the color is valid and was normalized.
91 */
92 function set_accent_color( color ) {
93 color = String( color || '' ).toUpperCase();
94 if ( ! /^#[0-9A-F]{6}$/.test( color ) ) {
95 render_accent_controls( '' );
96 return false;
97 }
98
99 editor.accent_value.value = color;
100 render_accent_controls( color );
101 shell_api.clear_field_error( 'booking_form_accent_color' );
102
103 return true;
104 }
105
106 /**
107 * Return the server-authorized Form Style preset registry.
108 *
109 * @return {Object} Presets keyed by style identifier.
110 */
111 function get_form_style_presets() {
112 return config.form_style_presets && 'object' === typeof config.form_style_presets
113 ? config.form_style_presets
114 : {};
115 }
116
117 /**
118 * Return every CSS variable that may be applied by a Form Style preset.
119 *
120 * @return {string[]} CSS custom-property names.
121 */
122 function get_form_style_css_var_names() {
123 return Array.isArray( config.form_style_css_var_names ) ? config.form_style_css_var_names : [];
124 }
125
126 /**
127 * Return relative luminance for a six-digit hexadecimal color.
128 *
129 * @param {string} color Valid six-digit hexadecimal color.
130 * @return {number} WCAG relative luminance.
131 */
132 function get_color_luminance( color ) {
133 var hex = String( color || '#000000' ).replace( '#', '' );
134 var channels = [ 0, 1, 2 ].map( function ( index ) {
135 var channel = parseInt( hex.substr( index * 2, 2 ), 16 ) / 255;
136 return channel <= 0.03928 ? channel / 12.92 : Math.pow( ( channel + 0.055 ) / 1.055, 2.4 );
137 } );
138
139 return ( 0.2126 * channels[ 0 ] ) + ( 0.7152 * channels[ 1 ] ) + ( 0.0722 * channels[ 2 ] );
140 }
141
142 /**
143 * Mix one hexadecimal color toward a target color.
144 *
145 * @param {string} color Source color.
146 * @param {string} target Target color.
147 * @param {number} amount Target mix ratio from zero to one.
148 * @return {string} Mixed six-digit hexadecimal color.
149 */
150 function mix_colors( color, target, amount ) {
151 var source = String( color || '#000000' ).replace( '#', '' );
152 var destination = String( target || '#000000' ).replace( '#', '' );
153 var channels = [];
154 var index;
155
156 for ( index = 0; index < 3; index++ ) {
157 channels.push( Math.round( parseInt( source.substr( index * 2, 2 ), 16 ) + ( ( parseInt( destination.substr( index * 2, 2 ), 16 ) - parseInt( source.substr( index * 2, 2 ), 16 ) ) * amount ) ) );
158 }
159
160 return '#' + channels.map( function ( channel ) {
161 return ( '0' + channel.toString( 16 ) ).slice( -2 );
162 } ).join( '' );
163 }
164
165 /**
166 * Add the selected accent to one preset CSS-variable map.
167 *
168 * @param {Object} css_vars Preset CSS variables.
169 * @param {string} accent Valid six-digit hexadecimal accent.
170 * @return {Object} CSS variables with accent overlays.
171 */
172 function apply_accent_to_css_vars( css_vars, accent ) {
173 var contrast = get_color_luminance( accent ) > 0.18 ? '#000000' : '#ffffff';
174 var hover = mix_colors( accent, '#ffffff' === contrast ? '#000000' : '#ffffff', 0.10 );
175 var accent_overlay = {
176 '--wpbc_form-accent-color': accent,
177 '--wpbc_form-accent-hover-color': hover,
178 '--wpbc_form-accent-contrast-color': contrast,
179 '--wpbc_form-field-focus-border-color': accent,
180 '--wpbc_form-field-focus-shadow-color': accent,
181 '--wpbc_form-choice-checked-border-color': accent,
182 '--wpbc_form-choice-checked-color': accent,
183 '--wpbc_form-choice-focus-color': accent,
184 '--wpbc_form-button-background-color': accent,
185 '--wpbc_form-button-background-color-alt': accent,
186 '--wpbc_form-button-border-color': accent,
187 '--wpbc_form-button-text-color': contrast,
188 '--wpbc_form-button-text-color-alt': contrast,
189 '--wpbc_form-button-hover-background-color': hover,
190 '--wpbc_form-button-hover-border-color': hover,
191 '--wpbc_form-button-hover-text-color': contrast,
192 '--wpbc_form-button-light-hover-border-color': accent,
193 '--wpbc_form-button-primary-hover-border-color': hover,
194 '--wpbc_form-page-break-color': accent
195 };
196
197 return Object.assign( {}, css_vars, accent_overlay );
198 }
199
200 /**
201 * Apply style classes, preset variables, and accent variables in place.
202 *
203 * @return {void}
204 */
205 function apply_form_style_to_preview() {
206 var values = get_values();
207 var presets = get_form_style_presets();
208 var preset = presets[ values.booking_form_style ] || presets.light_bordered || {};
209 var css_vars = preset.css_vars && 'object' === typeof preset.css_vars ? preset.css_vars : {};
210 var css_var_names = get_form_style_css_var_names();
211 var targets = Array.prototype.slice.call( editor.preview_root.querySelectorAll( '.wpbc_container.wpbc_form, .wpbc_bfb_form, .wpbc_bfb__form_preview_section_container' ) );
212
213 if ( ! /^#[0-9A-F]{6}$/.test( values.booking_form_accent_color ) ) {
214 return;
215 }
216
217 css_vars = apply_accent_to_css_vars( css_vars, values.booking_form_accent_color );
218 targets.forEach( function ( target ) {
219 target.classList.remove( 'wpbc_theme_dark_1', 'wpbc_bfb_form_appearance_custom' );
220 css_var_names.forEach( function ( variable_name ) {
221 target.style.removeProperty( variable_name );
222 } );
223 Object.keys( css_vars ).forEach( function ( variable_name ) {
224 if ( '' !== String( css_vars[ variable_name ] || '' ) ) {
225 target.style.setProperty( variable_name, css_vars[ variable_name ] );
226 }
227 } );
228 if ( preset.theme_class ) {
229 target.classList.add( String( preset.theme_class ) );
230 }
231 } );
232 }
233
234 /**
235 * Apply the selected server-resolved calendar skin without rebuilding markup.
236 *
237 * @return {void}
238 */
239 function apply_calendar_skin_to_preview() {
240 var option = editor.skin.options[ editor.skin.selectedIndex ];
241 var skin_url = option ? String( option.dataset.wpbcCalendarSkinUrl || '' ) : '';
242
243 if ( skin_url && 'function' === typeof window.wpbc__calendar__change_skin && document.getElementById( 'wpbc-calendar-skin-css' ) ) {
244 window.wpbc__calendar__change_skin( skin_url );
245 }
246 }
247
248 /**
249 * Switch the existing time controls between select and time-picker views.
250 *
251 * @return {void}
252 */
253 function apply_time_mode_to_preview() {
254 var is_enabled = 'On' === get_values().booking_timeslot_picker;
255 var time_selectors = 'select[name^="rangetime"], select[name^="starttime"], select[name^="endtime"], select[name^="durationtime"]';
256
257 if ( window._wpbc && 'function' === typeof window._wpbc.set_other_param ) {
258 window._wpbc.set_other_param( 'is_enabled_booking_timeslot_picker', is_enabled );
259 }
260
261 if ( is_enabled ) {
262 if ( window._wpbc && 'function' === typeof window.wpbc_hook__init_timeselector ) {
263 window.wpbc_hook__init_timeselector();
264 }
265 return;
266 }
267 if ( 'function' === typeof window.wpbc_hook__destroy_timeselector ) {
268 window.wpbc_hook__destroy_timeselector( editor.preview_root );
269 return;
270 }
271
272 Array.prototype.slice.call( editor.preview_root.querySelectorAll( '.wpbc_times_selector' ) ).forEach( function ( selector ) {
273 selector.remove();
274 } );
275 Array.prototype.slice.call( editor.preview_root.querySelectorAll( time_selectors ) ).forEach( function ( select ) {
276 select.style.display = '';
277 } );
278 }
279
280 /**
281 * Coalesce continuous Coloris input into one animation-frame update.
282 *
283 * @return {void}
284 */
285 function schedule_form_preview_update() {
286 if ( preview_frame_request ) {
287 window.cancelAnimationFrame( preview_frame_request );
288 }
289 preview_frame_request = window.requestAnimationFrame( function () {
290 preview_frame_request = 0;
291 apply_form_style_to_preview();
292 } );
293 }
294
295 /**
296 * Handle accent shortcut activation.
297 *
298 * @param {MouseEvent} event Click event.
299 * @return {void}
300 */
301 function handle_click( event ) {
302 var accent_swatch = event.target.closest( '[data-wpbc-appearance-accent-swatch]' );
303
304 if ( accent_swatch && set_accent_color( accent_swatch.dataset.wpbcAppearanceAccentSwatch ) ) {
305 schedule_form_preview_update();
306 }
307 }
308
309 /**
310 * Handle canonical select and radio changes without a network request.
311 *
312 * @param {Event} event Change event.
313 * @return {void}
314 */
315 function handle_change( event ) {
316 if ( event.target.matches( '[data-wpbc-appearance-style]' ) ) {
317 shell_api.clear_field_error( 'booking_form_style' );
318 render_selected_controls();
319 apply_form_style_to_preview();
320 } else if ( event.target.matches( '[data-wpbc-appearance-time-mode]' ) ) {
321 shell_api.clear_field_error( 'booking_timeslot_picker' );
322 render_selected_controls();
323 apply_time_mode_to_preview();
324 } else if ( event.target.matches( '[data-wpbc-appearance-preview-field]' ) ) {
325 shell_api.clear_field_error( 'booking_skin' );
326 apply_calendar_skin_to_preview();
327 }
328 }
329
330 /**
331 * Validate and apply continuous accent input.
332 *
333 * @param {Event} event Input event.
334 * @return {void}
335 */
336 function handle_accent_input( event ) {
337 if ( set_accent_color( event.target.value ) ) {
338 schedule_form_preview_update();
339 }
340 }
341
342 /**
343 * Initialize this module against shell-provided APIs.
344 *
345 * @param {Object} registered_shell_api Shared wizard adapter services.
346 * @return {void}
347 */
348 function initialize( registered_shell_api ) {
349 var editor_node;
350
351 shell_api = registered_shell_api;
352 editor_node = shell_api.root.querySelector( '[data-wpbc-appearance-editor]' );
353 if ( ! editor_node ) {
354 return;
355 }
356
357 editor = {
358 node: editor_node,
359 accent_value: editor_node.querySelector( '[data-wpbc-appearance-accent-value]' ),
360 accent_swatches: Array.prototype.slice.call( editor_node.querySelectorAll( '[data-wpbc-appearance-accent-swatch]' ) ),
361 skin: editor_node.querySelector( '[name="booking_skin"]' ),
362 preview_root: editor_node.querySelector( '[data-wpbc-appearance-inline-preview]' )
363 };
364
365 if ( ! editor.accent_value || ! editor.skin || ! editor.preview_root ) {
366 return;
367 }
368
369 editor.node.addEventListener( 'click', handle_click );
370 editor.node.addEventListener( 'change', handle_change );
371 editor.accent_value.addEventListener( 'input', handle_accent_input );
372 if ( 'function' === typeof window.Coloris ) {
373 window.Coloris( {
374 el: '.wpbc_setup_wizard__appearance-coloris',
375 alpha: false,
376 format: 'hex',
377 themeMode: 'auto',
378 onChange: function ( color, current_input ) {
379 if ( current_input === editor.accent_value && set_accent_color( color ) ) {
380 schedule_form_preview_update();
381 }
382 }
383 } );
384 }
385
386 render_selected_controls();
387 set_accent_color( editor.accent_value.value );
388 apply_form_style_to_preview();
389 apply_calendar_skin_to_preview();
390 apply_time_mode_to_preview();
391 }
392
393 /**
394 * Validate required fields before forward navigation.
395 *
396 * @return {HTMLElement|null} First invalid control, or null.
397 */
398 function validate() {
399 var values = get_values();
400 if ( ! values.booking_form_style ) {
401 return shell_api.set_field_error( 'booking_form_style', get_message( 'style_required', 'Choose a starting style.' ) );
402 }
403 if ( ! /^#[0-9A-F]{6}$/.test( values.booking_form_accent_color ) ) {
404 shell_api.set_field_error( 'booking_form_accent_color', get_message( 'accent_required', 'Choose a valid accent color.' ) );
405 return editor.accent_value;
406 }
407 if ( ! values.booking_skin ) {
408 return shell_api.set_field_error( 'booking_skin', get_message( 'skin_required', 'Choose a calendar skin.' ) );
409 }
410 if ( ! values.booking_timeslot_picker ) {
411 return shell_api.set_field_error( 'booking_timeslot_picker', get_message( 'time_required', 'Choose how time slots are displayed.' ) );
412 }
413 return null;
414 }
415
416 return {
417 initialize: initialize,
418 sync: function () {},
419 validate: validate
420 };
421 }
422
423 window.wpbc_setup_editor_modules = window.wpbc_setup_editor_modules || {};
424 window.wpbc_setup_editor_modules.appearance = { create: create_appearance_adapter };
425
426 if ( window.wpbc_setup_wizard_api && 'function' === typeof window.wpbc_setup_wizard_api.register_step_adapter ) {
427 window.wpbc_setup_wizard_api.register_step_adapter( create_appearance_adapter( module_config ) );
428 }
429 }( window, document ) );
430