diff --git a/css/menu.css b/css/menu.css new file mode 100644 index 0000000..e69de29 diff --git a/fbpatch.php b/fbpatch.php new file mode 100644 index 0000000..5f5a23a --- /dev/null +++ b/fbpatch.php @@ -0,0 +1,31 @@ + diff --git a/html/menu.html b/html/menu.html new file mode 100644 index 0000000..c8df7ed --- /dev/null +++ b/html/menu.html @@ -0,0 +1,24 @@ + + + + + +
diff --git a/js/calculations.js b/js/calculations.js new file mode 100644 index 0000000..f70a036 --- /dev/null +++ b/js/calculations.js @@ -0,0 +1,70 @@ + +function patch_form_calculation_CIPF() { + let form_calculation = document.querySelector('form.fb_form.multistep'); + if (form_calculation === null) + return; + + /* + * finds the input:checked in the element .calculate_field + * and trigger the event 'change' on it + * - this event 'change' is added by form builder in divi-form-calc-min.js : + * $(document).on( + * 'change', + * '.calculate_field input:not([type="hidden"]), .calculate_field select', + * function () { + * ... + * } + * ); + * + */ + function trigger_change(element) { + /* + * jquery version + * + let inputs = $(element).find('input:checked'); + inputs.trigger('change'); + */ + + /* + * js version + * + */ + let inputs = element.querySelectorAll('input:checked'); + // loop through inputs and trigger 'change' event on each + inputs.forEach(function(input) { + // Triggering 'change' event + let change_event = new Event('change', { + bubbles: true, + cancelable: true, + }); + input.dispatchEvent(change_event); + }); + } + + + // create an observer on form to check if child nodes are modified + const observer_form = new MutationObserver(wait_for_calculation_class); + observer_form.observe(form_calculation, { + subtree: true, + attributes: true, + }); + + // observe mutations to see if they include the addition of class .calculate_field + // if the class is added, call the function that might trigger the change event on it + function wait_for_calculation_class(mutationsList) { + mutationsList.forEach((mutation) => { + // check if class where added + if (mutation.type !== 'attributes') + return; + if (mutation.attributeName !== 'class') + return; + // check if added class is .calculate_field + let target = mutation.target; + if (target.classList.contains('calculate_field')) { + // If the class is added, trigger the 'change' event + trigger_change(target); + } + }); + } +} +patch_form_calculation_CIPF(); diff --git a/js/dates.js b/js/dates.js new file mode 100644 index 0000000..09d8e4e --- /dev/null +++ b/js/dates.js @@ -0,0 +1,145 @@ + +/* +* overriding the datepicker function to intercept the arguments +* -> https://stackoverflow.com/questions/26667720/how-to-get-the-selected-date-from-jquery-datepicker +* then create an false input that will mask the real one +* and put the original date format on the false element, +* while transforming the real one with the acf format 'Ymd' +* +* another solution would be to find the elements with the datepicker, and add the onSelect here +* +*/ +// store the original jQuery UI datepicker function +const original_datepicker = jQuery.fn.datepicker; +// override the datepicker function +jQuery.fn.datepicker = function(options) { + /* + * first creates the fake input + * + */ + create_fake_date_input(options, this); + + + /* + * then override the options to add action on select date : + * -> store the date in acf format in the hidden field value + * + */ + options.onSelect = function(date_text, inst) { + const acf_date_format = "yymmdd"; + const selected_date = jQuery(this).datepicker('getDate'); + const formated_date = jQuery.datepicker.formatDate(acf_date_format, selected_date); + + const fake_id = 'acf_date_fake_' + inst.id; + jQuery('#' + fake_id).val(date_text); + jQuery(this).val(formated_date); + } + + // call the original datepicker function with the updated option + return original_datepicker.call(this, options); +}; + + + + +/* +* creates the false element +* place it above the real one to hide it +* +*/ +async function create_fake_date_input(options, element) { + const fake_id = 'acf_date_fake_' + element.attr('id'); + if (jQuery('#' + fake_id).length !== 0) { + return; + } + + + const fake_name = 'acf_date_fake_for_' + element.attr('name'); + const original_date_string = await get_acf_date_from_rest(element); + const fake_value = original_date_string; + + + /* + * we position the hidden element right in top of the real datepicker one + * it might nor work in some situation, but so far it's good + * then we remove pointer events, so that clicking on it actually clicks on the real input + * + */ + let fake_style = ` + position: absolute; + top: 0px; + left: 0px; + pointer-events: none; + `; + const font_weight = element.css('font-weight'); + if (font_weight) { + fake_style += `font-weight: ${font_weight};`; + } + + + /* + * gives the parent element a defined position if needed + * then create the false input above the real one + * + */ + if (element.parent().css('position') === 'static') { + element.parent().css('position', 'relative'); + } + jQuery(``).insertAfter(element); +} + + + + +/* +* use the rest api to get the formated value for the date field +* +*/ +async function get_acf_date_from_rest(element) { + const pid = getUrlParameter('pid'); + + const prefix = "de_fb_"; + const name = element.attr('name') + let acf_name = name.substring(prefix.length); + + let date = element.val(); // default date, not formated, if fetch call fails + let fetch_data = null; + + try { + const response = await fetch(`/wp-json/wp/v2/posts/${pid}?_fields=acf.${acf_name}&acf_format=standard`); + fetch_data = await response.json(); + tmp_date = fetch_data.acf[acf_name]; + if (tmp_date) { + date = tmp_date; + } + } + catch (error) { + console.error('Error fetching ACF field:', error); + } + + return date; +} + + + + +/* +* https://stackoverflow.com/a/21903119/9497573 +* +*/ +function getUrlParameter(sParam) { + let sPageURL = window.location.search.substring(1), + sURLVariables = sPageURL.split('&'), + sParameterName, + i; + + for (i = 0; i < sURLVariables.length; i++) { + sParameterName = sURLVariables[i].split('='); + + if (sParameterName[0] === sParam) { + return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1]); + } + } + return false; +}; + diff --git a/js/modals.js b/js/modals.js new file mode 100644 index 0000000..53d6e81 --- /dev/null +++ b/js/modals.js @@ -0,0 +1,29 @@ +let modal_wrapper_CIPF = document.querySelector('#de-fb-modal-wrapper-'); + +// create an observer on first #de-fb-modal-wrapper- to check if child nodes are added +const observer_CIPF = new MutationObserver(wait_for_close_button_CIPF); +observer_CIPF.observe(modal_wrapper_CIPF, { + subtree: true, + childList: true, +}); + +// observe mutations to see if they include the creation of the button .modal-close +// if the button is created, add an eventListener to it +function wait_for_close_button_CIPF(mutationsList) { + mutationsList.forEach((mutation) => { + // check if nodes were added + if (mutation.type !== 'childList') + return; + // check if added nodes includes the button .modal-close + let modal_close = document.querySelector('#de-fb-modal-wrapper- .modal-close'); + if (modal_close !== null) { + modal_close.addEventListener("click", delete_modal_CIPF); + } + }); +} + +// when triggered, the .modal-close button will remove all childs from #de-fb-modal-wrapper- +function delete_modal_CIPF() { + modal_wrapper_CIPF.innerHTML = ''; +} + diff --git a/js/urls.js b/js/urls.js new file mode 100644 index 0000000..292a9ad --- /dev/null +++ b/js/urls.js @@ -0,0 +1,9 @@ +// https://stackoverflow.com/questions/12741517/how-to-make-url-validation-without-http-or-add-it-after-validation-passed/44848476#44848476 + +let old_url = jQuery.validator.methods.url; +jQuery.validator.addMethod( 'url', function(value, element) { + let url = old_url.bind(this); + return url(value, element) || url('http://' + value, element); + }, 'Please enter a valid URL' +); + diff --git a/menu/admin_menu.php b/menu/admin_menu.php new file mode 100644 index 0000000..a6a66c6 --- /dev/null +++ b/menu/admin_menu.php @@ -0,0 +1,38 @@ + diff --git a/menu/admin_menu_toggle.php b/menu/admin_menu_toggle.php new file mode 100644 index 0000000..cdff7eb --- /dev/null +++ b/menu/admin_menu_toggle.php @@ -0,0 +1,108 @@ +show menu'; + } + else if ($toggle === $toggle_menu['show']) { + $links[] = 'hide menu'; + } + return $links; +} +add_filter('plugin_action_links_fbpatch/fbpatch.php', __NAMESPACE__.'\add_link_to_plugin'); + + + + +/* +* handle the toggle menu when url is reached +* +*/ +function toggle_plugin_menu() { + $slug_toggle = Fbpatch::SLUG_TOOGLE_ADMIN_MENU; + $toggle_menu = Fbpatch::OPTION_TOGGLE_MENU; + + global $wp; + $current_slug = $wp->request; + if ($current_slug !== $slug_toggle['_name']) { + return; + } + + $show = null; + if (!isset($_GET)) { + $show = null; + } + else if (empty($_GET)) { + $show = null; + } + if (!isset($_GET[$slug_toggle['toggle']])) { + $show = null; + } + else if ($_GET[$slug_toggle['toggle']] === $slug_toggle['show']) { + $show = true; + } + else if ($_GET[$slug_toggle['toggle']] === $slug_toggle['hide']) { + $show = false; + } + + if ($show === true) { + update_option($toggle_menu['_name'], $toggle_menu['show']); + } + else if ($show === false) { + update_option($toggle_menu['_name'], $toggle_menu['hide']); + } + + $plugins_menu_url = admin_url('plugins.php'); + wp_redirect($plugins_menu_url, 301); + exit; +} +add_action('template_redirect', __NAMESPACE__.'\toggle_plugin_menu'); + + + + + +?> diff --git a/menu/menu_content.php b/menu/menu_content.php new file mode 100644 index 0000000..e0c9b5f --- /dev/null +++ b/menu/menu_content.php @@ -0,0 +1,104 @@ +"> +* +* +*/ +function patches_choice() { + $nonce = Fbpatch::NONCE; + if (!isset($_POST[$nonce['_name']])) { + \FBPATCH\redirect_menu_referer($_POST); + exit; + } + if (!wp_verify_nonce($_POST[$nonce['_name']], $nonce['_action'])) { + \FBPATCH\redirect_menu_referer($_POST); + exit; + } + + /* + * + * + [24-Mar-2024 12:24:08 UTC] -> _POST { + "action":"add_patches", + "nonce_name":"7eeb560dc0", + "_wp_http_referer":"\/wp-admin\/admin.php?page=fbpatch-plugin", + "hide_show":"on" + } + */ + $pathes_on = array(); + foreach($_POST as $key => $value) { + if ($value !== 'on') { + continue; + } + $pathes_on[] = $key; + } + Fbpatch::set_patches($pathes_on); + + \FBPATCH\redirect_menu_referer($_POST); +} +add_action('admin_post_'.Fbpatch::ADMIN_POST_PATCH_CHOICE, __NAMESPACE__.'\patches_choice'); + + + + +function redirect_menu_referer($post) { + if (!isset($post)) { + wp_redirect(admin_url(), 301); + exit; + } + if (is_null($post)) { + wp_redirect(admin_url(), 301); + exit; + } + if (empty($post)) { + wp_redirect(admin_url(), 301); + exit; + } + + if (!isset($post['_wp_http_referer'])) { + wp_redirect(admin_url(), 301); + exit; + } + + wp_redirect(home_url($post['_wp_http_referer']), 301); + exit; +} + + + + + +?> diff --git a/php/fbpatch_class.php b/php/fbpatch_class.php new file mode 100644 index 0000000..50d07c5 --- /dev/null +++ b/php/fbpatch_class.php @@ -0,0 +1,316 @@ +'toogle_admin_menu_url_fbpatch', 'toggle'=>'toggle', 'show'=>'show', 'hide'=>'hide']; + const OPTION_TOGGLE_MENU = ['_name'=>'toggle_admin_menu_option_fbpatch', 'show'=>'show', 'hide'=>'hide']; + const NONCE = ['_name'=>'nonce_name', '_action'=>'action_name']; + const ADMIN_POST_PATCH_CHOICE = 'add_patches'; + const ACF_DATE_CLASS = 'dfb_date'; + + /* + * get path an url from plugin root + * + */ + public static function root_path() { + return plugin_dir_path(__DIR__); + } + public static function root_url() { + return plugin_dir_url(__DIR__); + } + + + + + /* + * --------------------------------------------------------------------------- + * OPTIONS + * these functions are used to select which patch is applied + * + */ + + private static $_patches = [ + '_name'=>'fbpatch_list_of_patches', + 'dates' => ['checked'=>true, 'title'=>'dates', 'description'=>"gerer des dates pour acf dans n'importe quels formats (fonctionne bien pour divi, pas certain pour d'autres plugins)"], + 'calculations'=> ['checked'=>true, 'title'=>'calculations', 'description'=>"afficher le total des calculs dès l'ouverture des formulaires"], + 'hide_show' => ['checked'=>true, 'title'=>'masquer les offres', 'description'=>"permettre de masquer les offres en editant un formulaire, sans les supprimer"], + 'modals' => ['checked'=>false, 'title'=>'modals', 'description'=>"permettre plusieurs modals sur une meme page"], + 'urls' => ['checked'=>false, 'title'=>'urls', 'description'=>"permettre de rentrer des urls sans 'http://'"], + ]; + + private static function set_option_patches() { + /* + * get the list of patches in option + * create option if needed + * + */ + $raw_patches_option = get_option(self::$_patches['_name']); + if (false === $raw_patches_option) { + add_option(self::$_patches['_name'], '', '', 'no'); + } + $patches_option = unserialize($raw_patches_option); + if (empty($patches_option)) { + $patches_option = array(); + } + + /* + * if the option miss patches, add them + * + */ + foreach (self::$_patches as $patch => $data) { + if ($patch === '_name') { + continue; + } + if (isset($patches_option[$patch])) { + // updates the title and the description + $patches_option[$patch]['title'] = $data['title']; + $patches_option[$patch]['description'] = $data['description']; + } + else { + // add the option + $patches_option[$patch] = $data; + } + } + + /* + * if the option has additional patches, delete them + * + */ + foreach ($patches_option as $patch => $data) { + if (isset(self::$_patches[$patch])) { + continue; + } + unset($patches_option[$patch]); + } + + /* + * change the option list with the update patches + * + */ + ksort($patches_option); + $serialize_patches_option = serialize($patches_option); + update_option(self::$_patches['_name'], $serialize_patches_option); + } + public static function get_patches() { + self::set_option_patches(); + $patches = get_option(self::$_patches['_name']); + return unserialize($patches); + } + public static function set_patches($patches_on) { + /* + * loop through the option list and update occording to the received list + * + */ + $raw_patches = get_option(self::$_patches['_name']); + $patches_option = unserialize($raw_patches); + foreach($patches_option as $patch => $data) { + if (in_array($patch, $patches_on)) { + $patches_option[$patch]['checked'] = true; + } + else { + $patches_option[$patch]['checked'] = false; + } + } + + /* + * change the option list with the update patches + * + */ + ksort($patches_option); + $serialize_patches_option = serialize($patches_option); + update_option(self::$_patches['_name'], $serialize_patches_option); + } + + /* + * this function will include the files of the different patches if they are set in the options + * + */ + public static function init_hook() { + $patches = Fbpatch::get_patches(); + foreach($patches as $patch => $data) { + if ($data['checked'] === true) { + include_once(self::root_path() . '/php/patches/'.$patch.'.php'); + } + } + } + + + + + + + + + + /* + * --------------------------------------------------------------------------- + * HIDE SHOW + * to hide the chosen elements of the form without deleting the data in acf fields + * + */ + + private static $_post_id = 0; + private static $_has_elements_to_skip = false; + private static $_skip_elements = array(); + + public static function is_post_id($id) { + return self::$_post_id == $id; + } + public static function is_to_skip($key) { + return in_array($key, self::$_skip_elements); + } + + /* + * create an array of the elements to forget + * + */ + public static function set_post_elements_to_forget($id, $is_to_skip, $skip_array) { + self::$_post_id = $id; + self::$_has_elements_to_skip = $is_to_skip; + self::$_skip_elements = $skip_array; + } + + /* + * if there is elements to skip, add the filter + * + */ + public static function init_skip_hook() { + if (true === self::$_has_elements_to_skip) { + add_filter("update_post_metadata", __NAMESPACE__.'\filter_elements_to_skip', 10, 5); + } + } + + /* + * after insertion finished, removes filter and reset variables + * + */ + public static function end_skip_hook() { + if (true === self::$_has_elements_to_skip) { + remove_filter("update_post_metadata", __NAMESPACE__.'\filter_elements_to_skip'); + } + self::$_post_id = 0; + self::$_has_elements_to_skip = false; + self::$_skip_elements = array(); + } + + + + + + + + + + /* + * --------------------------------------------------------------------------- + * DATES + * stores the real dates in the database + * + */ + private static $dates_option = 'dfb_acf_dates_option'; + + /* + * stores acf dates by post_id + * + */ + public static function update_acf_date($post_id, $acf_key, $acf_date) { + if (empty($acf_date)) { + return; + } + $dates = get_option(self::$dates_option); + + /* + * if option does not exists, add it with its first value + * + */ + if ($dates === false) { + $dates[$post_id] = array($acf_key => $acf_date); + add_option(self::$dates_option, $dates, '', false); + return; + } + + /* + * if the option does not contains key for this post_id, add it + * + */ + if (!isset($dates[$post_id])) { + $dates[$post_id] = array($acf_key => $acf_date); + update_option(self::$dates_option, $dates, false); + return; + } + + /* + * if the post_id dont contains this acf field yet just add it + * + */ + if (!isset($dates[$post_id][$acf_key])) { + $dates[$post_id][$acf_key] = $acf_date; + update_option(self::$dates_option, $dates, false); + return; + } + + /* + * if the acf_key already exists, + * only update option if it is different + * + */ + if ($dates[$post_id][$acf_key] !== $acf_date) { + $dates[$post_id][$acf_key] = $acf_date; + update_option(self::$dates_option, $dates, false); + return; + } + } + + /* + * if acf_date exists for this acf field, use it instead of the value + * return the date in acf format 'Ymd' or false if doesnt exists + * + */ + public static function get_acf_date($post_id, $acf_key) { + $dates = get_option(self::$dates_option); + + if ($dates === false) { + return false; + } + if (!isset($dates[$post_id])) { + return false; + } + if (!isset($dates[$post_id][$acf_key])) { + return false; + } + + /* + * returns the date, or false if is empty + * + */ + $date = $dates[$post_id][$acf_key]; + if (empty($date)) { + return false; + } + return $date; + } + + + + +} + + + + + +?> diff --git a/php/patches/calculations.php b/php/patches/calculations.php new file mode 100644 index 0000000..8b461e8 --- /dev/null +++ b/php/patches/calculations.php @@ -0,0 +1,27 @@ + diff --git a/php/patches/dates.php b/php/patches/dates.php new file mode 100644 index 0000000..ae2f663 --- /dev/null +++ b/php/patches/dates.php @@ -0,0 +1,184 @@ + the date is output in edit form - 7505 : ../../../../wordpress_docker/volumes/wp_volume/wp-content/plugins/divi-form-builder/includes/modules/FormField/FormField.php +* -> 845 : ../../../../wordpress_docker/volumes/wp_volume/wp-content/plugins/divi-form-builder/includes/DiviFormBuilder.php +* +*/ +//function process_form_acf_dates($form_id, $post_array) { +// $acf_field_start = 'acf_date_hidden_for_'; +// $acf_date_fields = \FBPATCH\array_has_keys_starting_with($acf_field_start, $post_array); +// if (empty($acf_date_fields)) { +// return; +// } +// +// foreach ($acf_date_fields as $field) { +// $acf_date = $post_array[$field]; +// $acf_field = substr($field, strlen($acf_field_start)); +// $new_date_text = $post_array['meta_input'][$acf_field]; +// $acf_key = $post_array['meta_input']['_'.$acf_field]; +// \FBPATCH\add_date_to_acf_object_class($acf_key, $acf_date); +// Fbpatch::update_acf_date($post_array['ID'], $acf_key, $acf_date); +// } +//} +//add_action('df_before_insert_post', __NAMESPACE__.'\process_form_acf_dates', 10, 2); +// +// +///* +//* to update a field object (so, globally) +//* using `acf_update_field` +//* -> 980 : ../../../../wordpress_docker/volumes/wp_volume/wp-content/plugins/advanced-custom-fields/includes/acf-field-functions.php +//* +//*/ +//function add_date_to_acf_object_class($acf_id) { +// $field = get_field_object($acf_id); +// $field_class = $field['wrapper']['class']; +// $field_classes = explode(' ', $field_class); +// $class = Fbpatch::ACF_DATE_CLASS; +// +// $field_classes = array_filter($field_classes, function($value) { +// $class = Fbpatch::ACF_DATE_CLASS; +// if (strpos($value, $class) !== 0) { +// return $value; +// } +// }); +// +// $field_classes[] = $class; +// $implode_class = implode(' ', $field_classes); +// $field['wrapper']['class'] = $implode_class; +// acf_update_field($field); +//} +// +// +// +// +// +///* +//* here i use it to check if there are hidden field for acf dates +//* +//*/ +//function array_has_keys_starting_with($needle, &$haystack) { +// $keys = array(); +// foreach ($haystack as $key => $value) { +// if (strpos($key, $needle) === 0) { +// $keys[] = $key; +// } +// } +// return $keys; +//} + + + + + +?> diff --git a/php/patches/hide_show.php b/php/patches/hide_show.php new file mode 100644 index 0000000..a4c9d5f --- /dev/null +++ b/php/patches/hide_show.php @@ -0,0 +1,125 @@ + diff --git a/php/patches/modals.php b/php/patches/modals.php new file mode 100644 index 0000000..f1dd077 --- /dev/null +++ b/php/patches/modals.php @@ -0,0 +1,25 @@ + diff --git a/php/patches/urls.php b/php/patches/urls.php new file mode 100644 index 0000000..32a0991 --- /dev/null +++ b/php/patches/urls.php @@ -0,0 +1,30 @@ +