Plugin Directory

Changeset 3789028


Ignore:
Timestamp:
11/07/2026 06:07:59 AM (less than one hour ago)
Author:
muzaffarpakhtoon
Message:

1.2.8 - Adds Number (- / +) field, outlined form style, button icon image upload, and keeps Submission Storage settings when saving other settings. Dropdown and stepper fixes.

Location:
submitrelay
Files:
9 added
5 edited

Legend:

Unmodified
Added
Removed
  • submitrelay/trunk/assets/admin.js

    r3694551 r3789028  
    1 /* SubmitRelay admin asset handle. Dynamic admin behavior is attached with wp_add_inline_script(). */
     1/* SubmitRelay admin asset handle. Other admin behavior is attached with wp_add_inline_script(). */
     2(function () {
     3    'use strict';
     4
     5    document.addEventListener('DOMContentLoaded', function () {
     6        var input = document.getElementById('submitrelay-button-icon-image');
     7        var chooseBtn = document.getElementById('submitrelay-icon-choose');
     8        var resetBtn = document.getElementById('submitrelay-icon-reset');
     9        var preview = document.getElementById('submitrelay-icon-preview');
     10        var frame = null;
     11
     12        if (!input || !chooseBtn || !resetBtn || !preview) {
     13            return;
     14        }
     15
     16        function refresh() {
     17            var url = input.value.trim();
     18            if (url) {
     19                preview.setAttribute('src', url);
     20                preview.hidden = false;
     21            } else {
     22                preview.removeAttribute('src');
     23                preview.hidden = true;
     24            }
     25        }
     26
     27        chooseBtn.addEventListener('click', function (event) {
     28            event.preventDefault();
     29            if (!window.wp || !window.wp.media) {
     30                return;
     31            }
     32            if (!frame) {
     33                frame = window.wp.media({
     34                    title: chooseBtn.getAttribute('data-title') || '',
     35                    button: { text: chooseBtn.getAttribute('data-button') || '' },
     36                    library: { type: 'image' },
     37                    multiple: false
     38                });
     39                frame.on('select', function () {
     40                    var attachment = frame.state().get('selection').first().toJSON();
     41                    if (attachment && attachment.url) {
     42                        input.value = attachment.url;
     43                        refresh();
     44                    }
     45                });
     46            }
     47            frame.open();
     48        });
     49
     50        resetBtn.addEventListener('click', function (event) {
     51            event.preventDefault();
     52            input.value = '';
     53            refresh();
     54        });
     55
     56        input.addEventListener('input', refresh);
     57        refresh();
     58    });
     59}());
  • submitrelay/trunk/assets/public.js

    r3694551 r3789028  
    1 /* SubmitRelay public asset handle. Dynamic public behavior is attached with wp_add_inline_script(). */
     1/* SubmitRelay public assets. Form submission behavior is attached with wp_add_inline_script(). */
     2(function () {
     3    'use strict';
     4
     5    // Load once, even if the script is printed or combined more than once.
     6    if (window.submitrelayStepperLoaded) { return; }
     7    window.submitrelayStepperLoaded = true;
     8
     9    // Number (- / +) stepper fields used in the popup and the shortcode form.
     10    // The - and + buttons are only shown after this script has run (the
     11    // "submitrelay-stepper-ready" class), so they are never visible but dead.
     12    function clamp(input) {
     13        var min = parseInt(input.getAttribute('min'), 10);
     14        var max = parseInt(input.getAttribute('max'), 10);
     15        var value = parseInt(input.value, 10);
     16        if (isNaN(min)) { min = 0; }
     17        if (isNaN(max)) { max = 999; }
     18        if (isNaN(value)) { value = min; }
     19        value = Math.max(min, Math.min(max, value));
     20        input.value = String(value);
     21        return { value: value, min: min, max: max };
     22    }
     23
     24    function syncStepper(wrap) {
     25        if (!wrap) { return; }
     26        var input = wrap.querySelector('.submitrelay-step-input');
     27        var minus = wrap.querySelector('.submitrelay-step-minus');
     28        var plus = wrap.querySelector('.submitrelay-step-plus');
     29        if (!input) { return; }
     30        var state = clamp(input);
     31        if (minus) { minus.disabled = state.value <= state.min; }
     32        if (plus) { plus.disabled = state.value >= state.max; }
     33        wrap.classList.add('submitrelay-stepper-ready');
     34    }
     35
     36    function syncAll(onlyNew) {
     37        var selector = onlyNew ? '.submitrelay-stepper:not(.submitrelay-stepper-ready)' : '.submitrelay-stepper';
     38        var steppers = document.querySelectorAll(selector);
     39        for (var i = 0; i < steppers.length; i++) { syncStepper(steppers[i]); }
     40    }
     41
     42    // Capture phase, so themes or page builders that stop click events cannot block the buttons.
     43    document.addEventListener('click', function (event) {
     44        var target = event.target;
     45        if (target && target.nodeType === 3) { target = target.parentNode; }
     46        var button = (target && target.closest) ? target.closest('.submitrelay-step-btn') : null;
     47        if (!button) { return; }
     48        var wrap = button.closest('.submitrelay-stepper');
     49        var input = wrap ? wrap.querySelector('.submitrelay-step-input') : null;
     50        if (!input) { return; }
     51        event.preventDefault();
     52        var state = clamp(input);
     53        var delta = button.classList.contains('submitrelay-step-plus') ? 1 : -1;
     54        input.value = String(Math.max(state.min, Math.min(state.max, state.value + delta)));
     55        syncStepper(wrap);
     56    }, true);
     57
     58    document.addEventListener('change', function (event) {
     59        var input = event.target;
     60        if (input && input.classList && input.classList.contains('submitrelay-step-input')) {
     61            syncStepper(input.closest('.submitrelay-stepper'));
     62        }
     63    });
     64
     65    // After a form reset the field returns to its starting value.
     66    document.addEventListener('reset', function () {
     67        window.setTimeout(function () { syncAll(false); }, 0);
     68    });
     69
     70    if (document.readyState === 'loading') {
     71        document.addEventListener('DOMContentLoaded', function () { syncAll(false); });
     72    } else {
     73        syncAll(false);
     74    }
     75
     76    // Forms added to the page later (page builders, AJAX popups).
     77    if (window.MutationObserver) {
     78        var scheduled = false;
     79        new MutationObserver(function () {
     80            if (scheduled) { return; }
     81            scheduled = true;
     82            (window.requestAnimationFrame || window.setTimeout)(function () {
     83                scheduled = false;
     84                syncAll(true);
     85            });
     86        }).observe(document.documentElement, { childList: true, subtree: true });
     87    }
     88}());
  • submitrelay/trunk/readme.txt

    r3781257 r3789028  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.2.7
     7Stable tag: 1.2.8
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    193193== Changelog ==
    194194
     195= 1.2.8 =
     196* Fix: the number (- / +) buttons are shown only once their script has loaded, and scripts are cache-busted, so the buttons are never visible but inactive on sites with caching or optimization plugins.
     197* Fix: saving the main settings no longer resets the Submission Storage choices (for example IP Address switching back off).
     198* New: "Number (- / +)" field type for the form builder, for things like Adults and Child. Set a minimum, maximum and starting value. Works in the popup and the shortcode form.
     199* Change: form fields in both the popup and the shortcode form now use an outlined style, with the label sitting on the field border. A "Field Label Background" colour setting matches the label to your page background.
     200* New: optional Button Icon Image for the floating button. Choose an image from the Media Library, or click "Use Default Icon" to return to the text/emoji icon. The default icon is unchanged.
     201
    195202= 1.2.7 =
    196203* Fix: dropdown text no longer gets cut off in the shortcode form on themes that set a fixed select height.
    … …  
    334341== Upgrade Notice ==
    335342
     343= 1.2.8 =
     344Form fields now use an outlined style (label on the field border), and the floating button icon can be an uploaded image. Forms will look different after updating.
     345
    336346= 1.2.7 =
    337347Fixes dropdown text being cut off in the shortcode form on some themes.
  • submitrelay/trunk/submitrelay.php

    r3781257 r3789028  
    33 * Plugin Name:    SubmitRelay – Form & Chat
    44 * Description:    WordPress contact form with WhatsApp chat and pre-filled messages, email notifications, submission storage, popup and shortcode forms.
    5  * Version:        1.2.7
     5 * Version:        1.2.8
    66 * Author:         Muzaffer Ahmad Pakhtoon
    77 * Author URI:     https://wp.apharwat.net/
    … …  
    3232     * database migration.
    3333     */
    34     private const PLUGIN_VERSION = '1.2.7';
     34    private const PLUGIN_VERSION = '1.2.8';
    3535    private const DB_VERSION = '1.2.0';
    3636
    … …  
    337337            'scroll_percent'        => 0,
    338338            'popup_position'        => 'center',
     339            'button_icon_image'     => '',
     340            'outlined_label_bg'     => '#ffffff',
    339341            'schedule_enabled'      => 0,
    340342            'schedule_start'        => '',
    … …  
    423425            'field_text_color' => $defaults['field_text_color'],
    424426            'placeholder_text_color' => $defaults['placeholder_text_color'],
     427            'outlined_label_bg' => $defaults['outlined_label_bg'],
    425428        ];
    426429        foreach ($color_defaults as $key => $fallback) {
    … …  
    438441        $settings['button_position'] = in_array($settings['button_position'], $this->button_positions, true) ? $settings['button_position'] : $defaults['button_position'];
    439442        $settings['popup_position'] = in_array($settings['popup_position'], $this->popup_positions, true) ? $settings['popup_position'] : $defaults['popup_position'];
     443        $settings['button_icon_image'] = esc_url_raw((string) ($settings['button_icon_image'] ?? ''));
    440444        $settings['mobile_button_position'] = in_array($settings['mobile_button_position'], ['left', 'center', 'right'], true) ? $settings['mobile_button_position'] : $defaults['mobile_button_position'];
    441445        $settings['mobile_button_style'] = in_array($settings['mobile_button_style'], ['icon_text', 'icon'], true) ? $settings['mobile_button_style'] : $defaults['mobile_button_style'];
    … …  
    458462        $settings['form_fields'] = $this->sanitize_form_fields($settings['form_fields']);
    459463        return $settings;
     464    }
     465
     466    /**
     467     * Script version used for cache busting: plugin version plus the file's
     468     * modification time, so caches and CDNs never serve a stale script.
     469     */
     470    private function asset_version($relative_path) {
     471        $file  = plugin_dir_path(__FILE__) . $relative_path;
     472        $mtime = file_exists($file) ? filemtime($file) : false;
     473        return $mtime ? self::PLUGIN_VERSION . '.' . $mtime : self::PLUGIN_VERSION;
    460474    }
    461475
    … …  
    572586        }
    573587        wp_register_style('submitrelay-public-css', false, [], self::PLUGIN_VERSION);
    574         wp_register_script('submitrelay-public-js', plugin_dir_url(__FILE__) . 'assets/public.js', [], self::PLUGIN_VERSION, true);
     588        wp_register_script('submitrelay-public-js', plugin_dir_url(__FILE__) . 'assets/public.js', [], $this->asset_version('assets/public.js'), true);
    575589    }
    576590
    … …  
    582596        if ($hook === 'toplevel_page_submitrelay') {
    583597            wp_enqueue_editor();
     598            wp_enqueue_media();
    584599        }
    585600
    … …  
    587602        wp_enqueue_style('submitrelay-admin-css');
    588603
    589         wp_enqueue_script('submitrelay-admin-js', plugin_dir_url(__FILE__) . 'assets/admin.js', [], self::PLUGIN_VERSION, true);
     604        wp_enqueue_script('submitrelay-admin-js', plugin_dir_url(__FILE__) . 'assets/admin.js', [], $this->asset_version('assets/admin.js'), true);
    590605        wp_localize_script('submitrelay-admin-js', 'SubmitRelayAdmin', [
    591606            'settingsKey' => $this->settings_key,
    … …  
    599614                'select' => __('Select / Dropdown', 'submitrelay'),
    600615                'radio' => __('Radio', 'submitrelay'),
     616                'number' => __('Number (- / +)', 'submitrelay'),
     617                'numberMin' => __('Minimum', 'submitrelay'),
     618                'numberMax' => __('Maximum', 'submitrelay'),
     619                'numberStart' => __('Starting value', 'submitrelay'),
    601620                'newField' => __('New Field', 'submitrelay'),
    602621                'required' => __('Required', 'submitrelay'),
    … …  
    612631        ]);
    613632
     633        wp_add_inline_style('submitrelay-admin-css', '.submitrelay-icon-preview { width: 32px; height: 32px; object-fit: contain; vertical-align: middle; margin-left: 8px; padding: 4px; border-radius: 6px; background: #2563eb; } .submitrelay-icon-preview[hidden] { display: none; } .submitrelay-number-options { display: none; } .submitrelay-number-options label { margin-right: 14px; }');
    614634        wp_add_inline_style('submitrelay-admin-css', '
    615635            .submitrelay-tab-content {
    … …  
    936956        }
    937957        $base = wp_parse_args(is_array($existing) ? $existing : [], $defaults);
     958        // Keep the effective storage choices (including the legacy-install behaviour)
     959        // whenever any other settings page is saved.
     960        $current_settings = $this->settings();
     961        $base['submission_storage'] = $current_settings['submission_storage'];
    938962        foreach (['custom_css', 'button_border_radius', 'button_padding', 'popup_width', 'popup_height', 'popup_border_radius', 'close_size', 'close_position', 'close_bg_color', 'close_text_color'] as $obsolete_key) {
    939963            unset($base[$obsolete_key]);
    … …  
    961985        }
    962986
     987        if (!$storage_only) {
     988            $output['submission_storage'] = $base['submission_storage'];
     989        }
    963990        if ($storage_only) {
    964991            $storage_input = is_array($input['submission_storage']) ? $input['submission_storage'] : [];
    … …  
    10391066        $output['bottom_text'] = sanitize_textarea_field($input['bottom_text'] ?? $base['bottom_text']);
    10401067
    1041         $color_fields = ['button_color', 'button_text_color', 'popup_bg_color', 'overlay_color', 'field_text_color', 'placeholder_text_color'];
     1068        $color_fields = ['button_color', 'button_text_color', 'popup_bg_color', 'overlay_color', 'field_text_color', 'placeholder_text_color', 'outlined_label_bg'];
    10421069        foreach ($color_fields as $color) {
    10431070            $output[$color] = sanitize_hex_color($input[$color] ?? $base[$color]);
    … …  
    10591086        $output['animation_type'] = in_array($input['animation_type'] ?? '', $this->animation_types, true) ? $input['animation_type'] : $base['animation_type'];
    10601087        $output['popup_position'] = in_array($input['popup_position'] ?? '', $this->popup_positions, true) ? $input['popup_position'] : $base['popup_position'];
     1088        $output['button_icon_image'] = esc_url_raw((string) ($input['button_icon_image'] ?? ($base['button_icon_image'] ?? '')));
    10611089        $output['device_visibility'] = in_array($input['device_visibility'] ?? '', $this->device_visibility_opts, true) ? $input['device_visibility'] : $base['device_visibility'];
    10621090        $output['visibility_mode'] = in_array($input['visibility_mode'] ?? '', $this->visibility_modes, true) ? $input['visibility_mode'] : $base['visibility_mode'];
    … …  
    16541682            <?php $this->render_checkbox('enabled', __('Enable Floating Button', 'submitrelay')); ?>
    16551683            <?php $this->render_text_field('button_text', __('Button Text', 'submitrelay')); ?>
    1656             <?php $this->render_text_field('button_icon', __('Button Icon', 'submitrelay')); ?>
     1684            <?php
     1685            $submitrelay_icon_settings = $this->settings();
     1686            $submitrelay_icon_image    = (string) ($submitrelay_icon_settings['button_icon_image'] ?? '');
     1687            ?>
     1688            <tr>
     1689                <th scope="row"><label for="submitrelay-button-icon"><?php esc_html_e('Button Icon', 'submitrelay'); ?></label></th>
     1690                <td>
     1691                    <input type="text" class="regular-text" id="submitrelay-button-icon" name="<?php echo esc_attr($this->settings_key); ?>[button_icon]" value="<?php echo esc_attr($submitrelay_icon_settings['button_icon'] ?? ''); ?>" maxlength="255">
     1692                    <input type="hidden" id="submitrelay-button-icon-image" name="<?php echo esc_attr($this->settings_key); ?>[button_icon_image]" value="<?php echo esc_url($submitrelay_icon_image); ?>">
     1693                    <button type="button" class="button" id="submitrelay-icon-choose" data-title="<?php esc_attr_e('Choose Button Icon', 'submitrelay'); ?>" data-button="<?php esc_attr_e('Use this image', 'submitrelay'); ?>"><?php esc_html_e('Upload Image', 'submitrelay'); ?></button>
     1694                    <button type="button" class="button" id="submitrelay-icon-reset"><?php esc_html_e('Use Default Icon', 'submitrelay'); ?></button>
     1695                    <img id="submitrelay-icon-preview" class="submitrelay-icon-preview" alt=""<?php if ($submitrelay_icon_image !== '') : ?> src="<?php echo esc_url($submitrelay_icon_image); ?>"<?php else : ?> hidden<?php endif; ?>>
     1696                    <p class="description"><?php esc_html_e('Type text or an emoji, or upload an image to use instead. Click "Use Default Icon" to remove the image and show the text icon again.', 'submitrelay'); ?></p>
     1697                </td>
     1698            </tr>
    16571699            <?php $this->render_color('button_color', __('Floating Button Color', 'submitrelay')); ?>
    16581700            <?php $this->render_color('popup_bg_color', __('Background Color', 'submitrelay')); ?>
    … …  
    17061748            if (!is_array($field)) continue;
    17071749            $raw_type = $field['type'] ?? 'text';
    1708             $type = in_array($raw_type, ['text','email','phone','date','time','textarea','select','radio'], true) ? $raw_type : 'text';
     1750            $type = in_array($raw_type, ['text','email','phone','date','time','textarea','select','radio','number'], true) ? $raw_type : 'text';
    17091751            $label = sanitize_text_field($field['label'] ?? '');
    17101752            $id = sanitize_key($field['id'] ?? '');
    … …  
    17331775                }
    17341776            }
     1777            $number = [];
     1778            if ($type === 'number') {
     1779                $num_min   = (isset($field['min']) && is_numeric($field['min'])) ? (int) $field['min'] : 0;
     1780                $num_max   = (isset($field['max']) && is_numeric($field['max'])) ? (int) $field['max'] : 10;
     1781                $num_min   = max(0, min(999, $num_min));
     1782                $num_max   = max($num_min, min(999, $num_max));
     1783                $num_start = (isset($field['start']) && is_numeric($field['start'])) ? (int) $field['start'] : $num_min;
     1784                $num_start = max($num_min, min($num_max, $num_start));
     1785                $number    = ['min' => $num_min, 'max' => $num_max, 'start' => $num_start];
     1786            }
    17351787            // Backward compatibility: older installations used one `enabled`
    17361788            // flag for both forms. If the new per-form flags are absent, map
    … …  
    17511803                'width'=>$width,
    17521804                'options'=>$options,
    1753             ];
     1805            ] + $number;
    17541806            $seen[$id]=true;
    17551807        }
    … …  
    17741826                    <span class="submitrelay-drag" title="<?php esc_attr_e('Drag to reorder','submitrelay'); ?>">☰</span>
    17751827                    <select name="<?php echo esc_attr($this->settings_key); ?>[form_fields][<?php echo (int)$i; ?>][type]" class="submitrelay-field-type">
    1776                         <?php foreach (['text'=>__('Text','submitrelay'),'email'=>__('Email','submitrelay'),'phone'=>__('Phone','submitrelay'),'date'=>__('Date','submitrelay'),'time'=>__('Time','submitrelay'),'textarea'=>__('Textarea','submitrelay'),'select'=>__('Select / Dropdown','submitrelay'),'radio'=>__('Radio','submitrelay')] as $type=>$label): ?><option value="<?php echo esc_attr($type); ?>" <?php selected($field['type'],$type); ?>><?php echo esc_html($label); ?></option><?php endforeach; ?>
     1828                        <?php foreach (['text'=>__('Text','submitrelay'),'email'=>__('Email','submitrelay'),'phone'=>__('Phone','submitrelay'),'date'=>__('Date','submitrelay'),'time'=>__('Time','submitrelay'),'textarea'=>__('Textarea','submitrelay'),'select'=>__('Select / Dropdown','submitrelay'),'radio'=>__('Radio','submitrelay'),'number'=>__('Number (- / +)','submitrelay')] as $type=>$label): ?><option value="<?php echo esc_attr($type); ?>" <?php selected($field['type'],$type); ?>><?php echo esc_html($label); ?></option><?php endforeach; ?>
    17771829                    </select>
    17781830                    <input type="text" name="<?php echo esc_attr($this->settings_key); ?>[form_fields][<?php echo (int)$i; ?>][label]" value="<?php echo esc_attr($field['label']); ?>" aria-label="<?php esc_attr_e('Label','submitrelay'); ?>">
    … …  
    17991851                    </select>
    18001852                    <button type="button" class="button-link-delete submitrelay-remove-field" title="<?php echo esc_attr__('Remove field','submitrelay'); ?>" aria-label="<?php echo esc_attr__('Remove field','submitrelay'); ?>" <?php echo $is_core?'disabled':''; ?>><span class="dashicons dashicons-trash" aria-hidden="true"></span></button>
     1853                    <div class="submitrelay-number-options" style="grid-column:2 / -1; <?php echo ($field['type'] === 'number') ? 'display:block;' : ''; ?>">
     1854                        <label><?php esc_html_e('Minimum','submitrelay'); ?> <input type="number" class="small-text" min="0" max="999" name="<?php echo esc_attr($this->settings_key); ?>[form_fields][<?php echo (int)$i; ?>][min]" value="<?php echo esc_attr((string) ($field['min'] ?? 0)); ?>"></label>
     1855                        <label><?php esc_html_e('Maximum','submitrelay'); ?> <input type="number" class="small-text" min="0" max="999" name="<?php echo esc_attr($this->settings_key); ?>[form_fields][<?php echo (int)$i; ?>][max]" value="<?php echo esc_attr((string) ($field['max'] ?? 10)); ?>"></label>
     1856                        <label><?php esc_html_e('Starting value','submitrelay'); ?> <input type="number" class="small-text" min="0" max="999" name="<?php echo esc_attr($this->settings_key); ?>[form_fields][<?php echo (int)$i; ?>][start]" value="<?php echo esc_attr((string) ($field['start'] ?? 0)); ?>"></label>
     1857                    </div>
    18011858                    <div class="submitrelay-custom-options" style="grid-column:2 / -1; <?php echo in_array($field['type'], ['select','radio'], true) ? 'display:block;' : ''; ?>">
    18021859                        <label><?php esc_html_e('Options (one per line)','submitrelay'); ?><br><textarea rows="3" name="<?php echo esc_attr($this->settings_key); ?>[form_fields][<?php echo (int)$i; ?>][options_text]"><?php echo esc_textarea(implode("\n", $field['options'] ?? [])); ?></textarea></label>
    … …  
    18161873            function uniqueId(row,base){ const used=new Set([...rows.querySelectorAll('.submitrelay-field-row')].filter(r=>r!==row).map(r=>r.querySelector('input[name$="[id]"]')?.value.trim()).filter(Boolean)); let id=base||'field', n=2; while(used.has(id)){ id=base+'_'+n++; } return id; }
    18171874            function syncGeneratedId(row){ const label=row.querySelector('input[name$="[label]"]'); const idInput=row.querySelector('input[name$="[id]"]'); if(!label||!idInput||idInput.readOnly)return; const current=idInput.value.trim(); if(!current || row.dataset.autoId==='1'){ const id=uniqueId(row,slugifyLabel(label.value)); idInput.value=id; row.dataset.generatedId=id; row.dataset.autoId='1'; } }
    1818             function toggleOptions(row){ const type=row.querySelector('.submitrelay-field-type'); const box=row.querySelector('.submitrelay-custom-options'); if(type&&box) box.style.display=(type.value==='select'||type.value==='radio')?'block':'none'; }
     1875            function toggleOptions(row){ const type=row.querySelector('.submitrelay-field-type'); const box=row.querySelector('.submitrelay-custom-options'); const numBox=row.querySelector('.submitrelay-number-options'); if(type&&box) box.style.display=(type.value==='select'||type.value==='radio')?'block':'none'; if(type&&numBox) numBox.style.display=(type.value==='number')?'block':'none'; }
    18191876            function clearDropIndicators(){ rows.querySelectorAll('.submitrelay-field-row').forEach(r=>r.classList.remove('drop-before','drop-after','drop-side')); }
    18201877            function setWidth(row,value){ const select=row?.querySelector('select[name$="[width]"]'); if(select){ select.value=value; } }
    … …  
    18481905                reindex();
    18491906            });            const i18n = (window.SubmitRelayAdmin && window.SubmitRelayAdmin.i18n) || {};
    1850             add?.addEventListener('click',()=>{const i=rows.children.length; const wrap=document.createElement('div'); wrap.className='submitrelay-field-row'; wrap.draggable=true; wrap.dataset.autoId='1'; const base=window.SubmitRelayAdmin.settingsKey+'[form_fields]['+i+']'; const initialId=uniqueId(wrap,slugifyLabel(i18n.newField)); wrap.dataset.generatedId=initialId; wrap.innerHTML='<span class="submitrelay-drag">☰</span><select name="'+base+'[type]" class="submitrelay-field-type"><option value="text">'+i18n.text+'</option><option value="email">'+i18n.email+'</option><option value="phone">'+i18n.phone+'</option><option value="date">'+i18n.date+'</option><option value="time">'+i18n.time+'</option><option value="textarea">'+i18n.textarea+'</option><option value="select">'+i18n.select+'</option><option value="radio">'+i18n.radio+'</option></select><input type="text" name="'+base+'[label]" value="'+i18n.newField+'"><input type="text" name="'+base+'[id]" value="'+initialId+'"><input type="text" name="'+base+'[placeholder]" value=""><label class="submitrelay-switch" title="'+i18n.required+'"><input type="hidden" name="'+base+'[required]" value="0"><input type="checkbox" name="'+base+'[required]" value="1" aria-label="'+i18n.required+'"><span class="submitrelay-switch-track" aria-hidden="true"></span></label><label class="submitrelay-switch" title="'+i18n.popup+'"><input type="hidden" name="'+base+'[popup]" value="0"><input type="checkbox" name="'+base+'[popup]" value="1" checked aria-label="'+i18n.popup+'"><span class="submitrelay-switch-track" aria-hidden="true"></span></label><label class="submitrelay-switch" title="'+i18n.form+'"><input type="hidden" name="'+base+'[form]" value="0"><input type="checkbox" name="'+base+'[form]" value="1" checked aria-label="'+i18n.form+'"><span class="submitrelay-switch-track" aria-hidden="true"></span></label><select name="'+base+'[width]" aria-label="'+i18n.width+'"><option value="full">'+i18n.fullWidth+'</option><option value="half">'+i18n.halfWidth+'</option></select><button type="button" class="button-link-delete submitrelay-remove-field" title="'+i18n.removeField+'" aria-label="'+i18n.removeField+'"><span class="dashicons dashicons-trash" aria-hidden="true"></span></button><div class="submitrelay-custom-options" style="grid-column:2 / -1"><label>'+i18n.options+'<br><textarea rows="3" name="'+base+'[options_text]"></textarea></label></div>'; rows.appendChild(wrap); bind(wrap); reindex(); });
     1907            add?.addEventListener('click',()=>{const i=rows.children.length; const wrap=document.createElement('div'); wrap.className='submitrelay-field-row'; wrap.draggable=true; wrap.dataset.autoId='1'; const base=window.SubmitRelayAdmin.settingsKey+'[form_fields]['+i+']'; const initialId=uniqueId(wrap,slugifyLabel(i18n.newField)); wrap.dataset.generatedId=initialId; wrap.innerHTML='<span class="submitrelay-drag">☰</span><select name="'+base+'[type]" class="submitrelay-field-type"><option value="text">'+i18n.text+'</option><option value="email">'+i18n.email+'</option><option value="phone">'+i18n.phone+'</option><option value="date">'+i18n.date+'</option><option value="time">'+i18n.time+'</option><option value="textarea">'+i18n.textarea+'</option><option value="select">'+i18n.select+'</option><option value="radio">'+i18n.radio+'</option><option value="number">'+i18n.number+'</option></select><input type="text" name="'+base+'[label]" value="'+i18n.newField+'"><input type="text" name="'+base+'[id]" value="'+initialId+'"><input type="text" name="'+base+'[placeholder]" value=""><label class="submitrelay-switch" title="'+i18n.required+'"><input type="hidden" name="'+base+'[required]" value="0"><input type="checkbox" name="'+base+'[required]" value="1" aria-label="'+i18n.required+'"><span class="submitrelay-switch-track" aria-hidden="true"></span></label><label class="submitrelay-switch" title="'+i18n.popup+'"><input type="hidden" name="'+base+'[popup]" value="0"><input type="checkbox" name="'+base+'[popup]" value="1" checked aria-label="'+i18n.popup+'"><span class="submitrelay-switch-track" aria-hidden="true"></span></label><label class="submitrelay-switch" title="'+i18n.form+'"><input type="hidden" name="'+base+'[form]" value="0"><input type="checkbox" name="'+base+'[form]" value="1" checked aria-label="'+i18n.form+'"><span class="submitrelay-switch-track" aria-hidden="true"></span></label><select name="'+base+'[width]" aria-label="'+i18n.width+'"><option value="full">'+i18n.fullWidth+'</option><option value="half">'+i18n.halfWidth+'</option></select><button type="button" class="button-link-delete submitrelay-remove-field" title="'+i18n.removeField+'" aria-label="'+i18n.removeField+'"><span class="dashicons dashicons-trash" aria-hidden="true"></span></button><div class="submitrelay-custom-options" style="grid-column:2 / -1"><label>'+i18n.options+'<br><textarea rows="3" name="'+base+'[options_text]"></textarea></label></div><div class="submitrelay-number-options" style="grid-column:2 / -1"><label>'+i18n.numberMin+' <input type="number" class="small-text" min="0" max="999" name="'+base+'[min]" value="0"></label> <label>'+i18n.numberMax+' <input type="number" class="small-text" min="0" max="999" name="'+base+'[max]" value="10"></label> <label>'+i18n.numberStart+' <input type="number" class="small-text" min="0" max="999" name="'+base+'[start]" value="0"></label></div>'; rows.appendChild(wrap); bind(wrap); reindex(); });
    18511908        })();
    18521909
    … …  
    18681925            <div style="margin-top:18px;">
    18691926                <table class="form-table" role="presentation">
     1927                    <?php $this->render_color('outlined_label_bg', __('Field Label Background (shortcode form)', 'submitrelay')); ?>
    18701928                    <?php $this->render_color('field_text_color', __('Field Text Color', 'submitrelay')); ?>
    18711929                    <?php $this->render_color('placeholder_text_color', __('Placeholder Text Color', 'submitrelay')); ?>
    … …  
    20092067            <?php $this->render_select('popup_position', __('Popup Position', 'submitrelay'), $this->get_position_option_labels('popup')); ?>
    20102068            <?php $this->render_text_field('auto_open_delay', __('Auto-open Delay (sec)', 'submitrelay'), 'number', 'small-text'); ?>
    2011         </table>
    2012         <?php
    2013     }
    2014 
    2015     private function render_overlay_tab() {
    2016         $settings = $this->settings();
    2017         ?>
    2018         <table class="form-table">
    2019             <?php $this->render_color('overlay_color', __('Overlay Color', 'submitrelay')); ?>
    2020             <tr>
    2021                 <th scope="row"><?php esc_html_e('Overlay Opacity', 'submitrelay'); ?></th>
    2022                 <td>
    2023                     <div class="submitrelay-opacity-wrapper">
    2024                         <input type="range" name="<?php echo esc_attr($this->settings_key); ?>[overlay_opacity]" min="0" max="1" step="0.05" value="<?php echo esc_attr($settings['overlay_opacity']); ?>">
    2025                         <span id="opacityValue"><?php echo esc_html($settings['overlay_opacity']); ?></span>
    2026                     </div>
    2027                 </td>
    2028             </tr>
    2029             <?php $this->render_select('animation_type', __('Animation Type', 'submitrelay'), $this->get_position_option_labels('animation')); ?>
    20302069        </table>
    20312070        <?php
    … …  
    24002439                foreach (($field['options'] ?? []) as $option) $html .= '<option value="' . esc_attr($option) . '">' . esc_html($option) . '</option>';
    24012440                $html .= '</select>';
     2441            } elseif ($type === 'number') {
     2442                $num_min   = isset($field['min']) ? (int) $field['min'] : 0;
     2443                $num_max   = isset($field['max']) ? (int) $field['max'] : 10;
     2444                $num_start = isset($field['start']) ? (int) $field['start'] : $num_min;
     2445                $html .= '<div class="submitrelay-stepper">';
     2446                $html .= '<button type="button" class="submitrelay-step-btn submitrelay-step-minus" aria-label="' . esc_attr__('Decrease', 'submitrelay') . '">&minus;</button>';
     2447                $html .= '<input type="number" id="' . esc_attr($input_id) . '" name="' . esc_attr($name) . '" value="' . esc_attr((string) $num_start) . '" min="' . esc_attr((string) $num_min) . '" max="' . esc_attr((string) $num_max) . '" step="1" inputmode="numeric" class="submitrelay-step-input">';
     2448                $html .= '<button type="button" class="submitrelay-step-btn submitrelay-step-plus" aria-label="' . esc_attr__('Increase', 'submitrelay') . '">+</button>';
     2449                $html .= '</div>';
    24022450            } elseif ($type === 'radio') {
    24032451                $radio_legend_id = $input_id . '-legend';
    … …  
    25242572        echo $compact_css; ?>
    25252573        <?php
    2526         return ob_get_clean();
     2574        $popup_css = ob_get_clean();
     2575        if (!empty($settings['button_icon_image'])) {
     2576            $popup_css .= '.fsp-btn-icon-img { display: block; width: 22px; height: 22px; object-fit: contain; }';
     2577        }
     2578        $popup_css .= $this->get_outlined_field_css('.fsp-form-wrap', $settings['popup_bg_color'] ?? '#ffffff');
     2579        return $popup_css;
     2580    }
     2581
     2582    /**
     2583     * Built-in "outlined labels" field style. This is a fixed preset (not custom
     2584     * CSS input); the only configurable value is a validated hex colour.
     2585     */
     2586    private function get_outlined_field_css($scope, $label_bg) {
     2587        $s = $scope . '.submitrelay-outlined';
     2588        $bg = sanitize_hex_color($label_bg);
     2589        if (!$bg) {
     2590            $bg = '#ffffff';
     2591        }
     2592        $css  = $s . ' .submitrelay-field { position: relative; margin: 10px 0 12px !important; }';
     2593        $css .= $s . ' .submitrelay-field > .submitrelay-field-label { position: absolute !important; top: 0 !important; left: 14px !important; width: auto !important; height: auto !important; max-width: calc(100% - 28px) !important; margin: 0 !important; padding: 0 6px !important; transform: translateY(-50%); clip: auto !important; overflow: hidden !important; text-overflow: ellipsis; white-space: nowrap !important; border: 0 !important; background: ' . $bg . ' !important; font-size: 12px !important; font-weight: 500 !important; line-height: 1.2 !important; letter-spacing: 0.04em; text-transform: uppercase; z-index: 2; pointer-events: none; }';
     2594        $css .= $s . ' input, ' . $s . ' textarea, ' . $s . ' select { background-color: transparent !important; border: 1px solid #9ca3af !important; }';
     2595        $css .= $s . ' input::placeholder, ' . $s . ' textarea::placeholder { color: transparent !important; opacity: 0; }';
     2596        $css .= $s . ' input:focus, ' . $s . ' textarea:focus, ' . $s . ' select:focus { background-color: transparent !important; border-color: #2563eb !important; box-shadow: 0 0 0 1px #2563eb !important; outline: none !important; }';
     2597        $css .= $s . ' .submitrelay-stepper { display: flex; align-items: center; justify-content: space-between; gap: 8px; box-sizing: border-box; width: 100%; margin: 0; padding: 0 10px; border: 1px solid #9ca3af; border-radius: 12px; }';
     2598        $css .= $s . ' .submitrelay-stepper:focus-within { border-color: #2563eb; box-shadow: 0 0 0 1px #2563eb; }';
     2599        $css .= $s . ' .submitrelay-stepper .submitrelay-step-input { flex: 1 1 auto; min-width: 0; width: 100% !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 11px 0 !important; border: 0 !important; box-shadow: none !important; background: transparent !important; text-align: center; }';
     2600        $css .= $s . ' .submitrelay-stepper-ready .submitrelay-step-input { -moz-appearance: textfield; appearance: textfield; }';
     2601        $css .= $s . ' .submitrelay-stepper-ready .submitrelay-step-input::-webkit-outer-spin-button, ' . $s . ' .submitrelay-stepper-ready .submitrelay-step-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }';
     2602        $css .= $s . ' .submitrelay-stepper:not(.submitrelay-stepper-ready) .submitrelay-step-btn { display: none !important; }';
     2603        $css .= $s . ' .submitrelay-stepper .submitrelay-step-btn { flex: 0 0 auto !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; position: relative !important; width: 24px !important; min-width: 24px !important; height: 24px !important; min-height: 24px !important; margin: 0 !important; padding: 0 !important; border: 1.5px solid #1f2937 !important; border-radius: 50% !important; background: transparent !important; color: #1f2937 !important; box-shadow: none !important; filter: none !important; transform: none !important; font-size: 0 !important; line-height: 0 !important; overflow: hidden; cursor: pointer; }';
     2604        $css .= $s . ' .submitrelay-stepper .submitrelay-step-btn::before { content: ""; position: absolute; top: 50%; left: 50%; width: 10px; height: 1.5px; background: #1f2937; transform: translate(-50%, -50%); }';
     2605        $css .= $s . ' .submitrelay-stepper .submitrelay-step-plus::after { content: ""; position: absolute; top: 50%; left: 50%; width: 1.5px; height: 10px; background: #1f2937; transform: translate(-50%, -50%); }';
     2606        $css .= $s . ' .submitrelay-stepper .submitrelay-step-btn:hover:not(:disabled) { background: rgba(31, 41, 55, 0.08) !important; filter: none !important; transform: none !important; }';
     2607        $css .= $s . ' .submitrelay-stepper .submitrelay-step-btn:disabled { opacity: 0.35 !important; cursor: not-allowed; }';
     2608        $css .= $s . ' input[type="radio"], ' . $s . ' input[type="checkbox"] { border: revert !important; box-shadow: none !important; }';
     2609        return $css;
    25272610    }
    25282611
    … …  
    25462629                'id' => true, 'class' => true, 'style' => true, 'aria-hidden' => true,
    25472630            ],
     2631            'img' => [
     2632                'class' => true, 'src' => true, 'alt' => true, 'width' => true, 'height' => true,
     2633            ],
    25482634            'fieldset' => [
    25492635                'id' => true, 'class' => true, 'aria-labelledby' => true,
    … …  
    25602646                'class' => true, 'placeholder' => true, 'autocomplete' => true,
    25612647                'required' => true, 'checked' => true, 'disabled' => true,
    2562                 'aria-label' => true, 'aria-required' => true, 'aria-hidden' => true, 'inputmode' => true, 'title' => true, 'data-*' => true, 'maxlength' => true, 'pattern' => true,
     2648                'aria-label' => true, 'aria-required' => true, 'aria-hidden' => true, 'inputmode' => true, 'title' => true, 'data-*' => true, 'maxlength' => true, 'pattern' => true, 'min' => true, 'max' => true, 'step' => true,
    25632649            ],
    25642650            'textarea' => [
    … …  
    25852671    private function render_popup_html($settings) {
    25862672        $button_html = '';
    2587         if (!empty($settings['button_icon'])) $button_html .= '<span class="fsp-btn-icon">' . wp_kses_post($settings['button_icon']) . '</span> ';
     2673        if (!empty($settings['button_icon_image'])) {
     2674            $button_html .= '<span class="fsp-btn-icon"><img class="fsp-btn-icon-img" src="' . esc_url($settings['button_icon_image']) . '" alt="" width="22" height="22"></span> ';
     2675        } elseif (!empty($settings['button_icon'])) {
     2676            $button_html .= '<span class="fsp-btn-icon">' . wp_kses_post($settings['button_icon']) . '</span> ';
     2677        }
    25882678        $button_html .= '<span class="fsp-btn-text">' . esc_html($settings['button_text']) . '</span>';
    25892679
    … …  
    25942684        ?>
    25952685        <button id="fsp-float-btn"><?php echo wp_kses_post($button_html); ?></button>
     2686
    25962687        <div id="fsp-overlay"></div>
    25972688        <div id="fsp-popup" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e('Contact form', 'submitrelay'); ?>">
    … …  
    26032694                <div class="fsp-description"><?php echo nl2br(esc_html($settings['popup_description'])); ?></div>
    26042695            <?php endif; ?>
    2605             <div class="fsp-form-wrap"><?php echo wp_kses($contact_form, $this->get_popup_allowed_html()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Form markup is generated internally with escaped attributes and a WordPress-generated nonce. ?></div>
     2696            <div class="fsp-form-wrap submitrelay-outlined"><?php echo wp_kses($contact_form, $this->get_popup_allowed_html()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Form markup is generated internally with escaped attributes and a WordPress-generated nonce. ?></div>
    26062697            <?php if (!empty($settings['show_bottom_text']) && !empty($settings['bottom_text'])): ?>
    26072698                <div class="fsp-bottom-text"><?php echo wp_kses_post($settings['bottom_text']); ?></div>
    … …  
    31543245                wp_send_json_error(['message' => sprintf(__('Please choose a valid option for %s.', 'submitrelay'), sanitize_text_field($field['label'] ?? $id))]);
    31553246            }
     3247            if (($field['type'] ?? '') === 'number') {
     3248                $num_min = isset($field['min']) ? (int) $field['min'] : 0;
     3249                $num_max = isset($field['max']) ? (int) $field['max'] : 10;
     3250                if ($value !== '' && (!preg_match('/^\d{1,3}$/', $value) || (int) $value < $num_min || (int) $value > $num_max)) {
     3251                    /* translators: %s is the form field label. */
     3252                    wp_send_json_error(['message' => sprintf(__('Please enter a valid number for %s.', 'submitrelay'), sanitize_text_field($field['label'] ?? $id))]);
     3253                }
     3254            }
    31563255            if (($field['type'] ?? '') === 'phone' && $value !== '' && !$this->is_valid_phone_number($value)) {
    31573256                /* translators: %s is the form field label. */
    … …  
    33353434        $form_fields = $this->render_form_fields($settings, 'shortcode');
    33363435
    3337         $form_html = '<div class="submitrelay-shortcode-wrapper" style="display:block;width:100%;max-width:100%;min-width:0;margin:0 auto;box-sizing:border-box;">
     3436        $form_html = '<div class="submitrelay-shortcode-wrapper submitrelay-outlined" style="display:block;width:100%;max-width:100%;min-width:0;margin:0 auto;box-sizing:border-box;">
    33383437            <form method="post" class="submitrelay-shortcode-form" style="display:block;width:100%;max-width:100%;min-width:0;margin:0;box-sizing:border-box;" data-wa-enabled="' . ($whatsapp_enabled ? '1' : '0') . '" data-wa-number="' . esc_attr($wa_number) . '" data-wa-template="' . esc_attr($wa_template) . '" data-wa-delay="' . $wa_delay . '" data-wa-target="' . esc_attr($settings['whatsapp_redirect_target'] ?? 'new_tab') . '" data-success-msg="' . $success_msg . '" data-error-msg="' . $error_msg . '" data-site-title="' . $site_title_js . '" data-site-url="' . $site_url_js . '">
    33393438                ' . $form_fields . '
    … …  
    35943693        }());
    35953694        ';
     3695        $inline_css .= $this->get_outlined_field_css('.submitrelay-shortcode-wrapper', $settings['outlined_label_bg'] ?? '#ffffff');
    35963696        wp_enqueue_style('submitrelay-public-css');
    35973697        wp_enqueue_script('submitrelay-public-js');
Note: See TracChangeset for help on using the changeset viewer.