/** * Widget Builder Visual UI * Chapter 1: Panel shell, tab switching, Monaco initialization */ (function ($) { 'use strict'; /* ------------------------------------------------------------------ * Global references * ----------------------------------------------------------------*/ var editors = {}; // Monaco editor instances { html, css, js } var monacoReady = false; /* ------------------------------------------------------------------ * Tab switching — Center panel (Content / Style / Advanced) * ----------------------------------------------------------------*/ function initCenterTabs() { $('.wpr-wb-center-tab').on('click', function () { var tab = $(this).data('tab'); // Tabs $('.wpr-wb-center-tab').removeClass('active'); $(this).addClass('active'); // Panels $('.wpr-wb-center-panel').removeClass('active'); $('.wpr-wb-center-panel[data-tab="' + tab + '"]').addClass('active'); }); } /* ------------------------------------------------------------------ * Tab switching — Right panel (HTML / CSS / JS / Includes) * ----------------------------------------------------------------*/ function initCodeTabs() { $('.wpr-wb-code-tab').on('click', function () { var editor = $(this).data('editor'); // Tabs $('.wpr-wb-code-tab').removeClass('active'); $(this).addClass('active'); // Panels $('.wpr-wb-code-panel').removeClass('active'); $('.wpr-wb-code-panel[data-editor="' + editor + '"]').addClass('active'); // Re-layout the Monaco editor so it fills the container if (editors[editor]) { editors[editor].layout(); } }); // Template Tags Info popup $('#wpr-wb-template-info-btn').on('click', function () { $('#wpr-wb-template-info-popup').toggleClass('open'); }); $('.wpr-wb-template-info-close').on('click', function () { $('#wpr-wb-template-info-popup').removeClass('open'); }); $(document).on('click', function (e) { var $popup = $('#wpr-wb-template-info-popup'); if ($popup.hasClass('open') && !$(e.target).closest('#wpr-wb-template-info-popup, #wpr-wb-template-info-btn').length) { $popup.removeClass('open'); } }); } /* ------------------------------------------------------------------ * Footer buttons — Settings / Preview / Save * ----------------------------------------------------------------*/ function initPanelCollapse() { var $panel = $('.wpr-wb-panel-left'); var $btn = $('#wpr-wb-collapse-left'); // Restore saved state if (localStorage.getItem('wpr_wb_panel_collapsed') === '1') { $panel.addClass('collapsed'); $btn.find('i').removeClass('eicon-chevron-left').addClass('eicon-chevron-right'); } $btn.on('click', function () { $panel.toggleClass('collapsed'); $(this).find('i').toggleClass('eicon-chevron-left eicon-chevron-right'); localStorage.setItem('wpr_wb_panel_collapsed', $panel.hasClass('collapsed') ? '1' : '0'); }); } function initFooterButtons() { // SETTINGS button — just marks active state $('#wpr-wb-btn-settings').on('click', function () { $('.wpr-wb-footer-btn').not('.wpr-wb-footer-btn-save').removeClass('active'); $(this).addClass('active'); }); // PREVIEW button — open Elementor preview in new tab $('#wpr-wb-btn-preview').on('click', function () { $('.wpr-wb-footer-btn').not('.wpr-wb-footer-btn-save').removeClass('active'); $(this).addClass('active'); var url = wprWidgetBuilder.previewUrl; if (url) { window.open(url, '_blank'); } else { showToast('Save the widget first to preview.', 'error'); } }); // SAVE button $('#wpr-wb-btn-save, #wpr-wb-collapsed-save').on('click', function () { saveWidget(); }); } /* ------------------------------------------------------------------ * Monaco Editor — load via AMD & initialise * ----------------------------------------------------------------*/ function initMonaco() { var basePath = 'https://cdn.jsdelivr.net/npm/[email protected]/min'; // Inject the AMD loader if not present if (typeof window.require === 'undefined' || typeof window.require.config === 'undefined') { var script = document.createElement('script'); script.src = basePath + '/vs/loader.js'; script.onload = function () { configureAndCreateEditors(basePath); }; document.head.appendChild(script); } else { configureAndCreateEditors(basePath); } } function configureAndCreateEditors(basePath) { window.require.config({ paths: { vs: basePath + '/vs' } }); window.require(['vs/editor/editor.main'], function () { monacoReady = true; // Use decorations to highlight {{...}} template tags in white function applyTemplateTagDecorations(editor) { var model = editor.getModel(); if (!model) return; var decorations = []; var text = model.getValue(); var regex = /\{\{[^}]*\}\}/g; var match; while ((match = regex.exec(text)) !== null) { var startPos = model.getPositionAt(match.index); var endPos = model.getPositionAt(match.index + match[0].length); decorations.push({ range: new monaco.Range(startPos.lineNumber, startPos.column, endPos.lineNumber, endPos.column), options: { inlineClassName: 'wpr-template-tag' } }); } editor._templateTagDecorations = editor.deltaDecorations( editor._templateTagDecorations || [], decorations ); } editors.html = monaco.editor.create(document.getElementById('wpr-wb-editor-html'), { value: '', language: 'html', theme: 'vs-dark', lineNumbers: 'on', minimap: { enabled: false }, wordWrap: 'on', fontSize: 14, automaticLayout: true, scrollBeyondLastLine: false, tabSize: 4, }); editors.css = monaco.editor.create(document.getElementById('wpr-wb-editor-css'), { value: '', language: 'css', theme: 'vs-dark', lineNumbers: 'on', minimap: { enabled: false }, wordWrap: 'on', fontSize: 14, automaticLayout: true, scrollBeyondLastLine: false, tabSize: 4, }); editors.js = monaco.editor.create(document.getElementById('wpr-wb-editor-js'), { value: '', language: 'javascript', theme: 'vs-dark', lineNumbers: 'on', minimap: { enabled: false }, wordWrap: 'on', fontSize: 14, automaticLayout: true, scrollBeyondLastLine: false, tabSize: 4, }); // Apply template tag decorations to all editors ['html', 'css', 'js'].forEach(function (key) { if (editors[key]) { applyTemplateTagDecorations(editors[key]); editors[key].onDidChangeModelContent(function () { applyTemplateTagDecorations(editors[key]); }); } }); // Load saved data into editors loadWidget(); }); } /* ------------------------------------------------------------------ * Toast notification * ----------------------------------------------------------------*/ function showToast(message, type) { var $toast = $('#wpr-wb-toast'); $toast.text(message) .removeClass('success error visible') .addClass(type || '') .addClass('visible'); clearTimeout($toast.data('timer')); $toast.data('timer', setTimeout(function () { $toast.removeClass('visible'); }, 3000)); } /* ------------------------------------------------------------------ * Save Widget via REST API * ----------------------------------------------------------------*/ function saveWidget() { if (!wprWidgetBuilder.postId) { // Create new widget first createWidget(); return; } var data = collectData(); $('#wpr-wb-btn-save').prop('disabled', true).text('SAVING...'); $.ajax({ url: wprWidgetBuilder.apiBase + 'save/' + wprWidgetBuilder.postId, method: 'POST', contentType: 'application/json', headers: { 'X-WP-Nonce': wprWidgetBuilder.nonce }, data: JSON.stringify(data), success: function (res) { showToast('Widget saved successfully!', 'success'); $('#wpr-wb-btn-save').prop('disabled', false).text('SAVE'); }, error: function (xhr) { var msg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Save failed.'; showToast(msg, 'error'); $('#wpr-wb-btn-save').prop('disabled', false).text('SAVE'); } }); } /* ------------------------------------------------------------------ * Create new widget (when postId is 0) * ----------------------------------------------------------------*/ function createWidget() { var data = collectData(); $('#wpr-wb-btn-save').prop('disabled', true).text('SAVING...'); $.ajax({ url: wprWidgetBuilder.apiBase + 'save', method: 'POST', contentType: 'application/json', headers: { 'X-WP-Nonce': wprWidgetBuilder.nonce }, data: JSON.stringify(data), success: function (res) { if (res.post_id) { wprWidgetBuilder.postId = res.post_id; // Update URL without reload var newUrl = window.location.pathname.replace('post-new.php', 'post.php') + '?post=' + res.post_id + '&action=edit'; window.history.replaceState(null, '', newUrl); } showToast('Widget created successfully!', 'success'); $('#wpr-wb-btn-save').prop('disabled', false).text('SAVE'); }, error: function (xhr) { var msg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Create failed.'; showToast(msg, 'error'); $('#wpr-wb-btn-save').prop('disabled', false).text('SAVE'); } }); } /* ------------------------------------------------------------------ * Collect all data from UI * ----------------------------------------------------------------*/ var htmlReferenceComment = [ '<!-- ═══════════════════════════════════════════════════════════', 'HOW TO USE THE WIDGET BUILDER', '═══════════════════════════════════════════════════════════', '', '1. Add controls in the left panel (Text, Color, Media, etc.)', '2. Write your HTML here using template tags to display', ' the control values: {{key}}', '3. Style your widget in the CSS tab', '4. Add interactivity in the JS tab (optional)', '5. Save and use your widget in Elementor!', '', '───────────────────────────────────────────────────────', 'Template tags:', '───────────────────────────────────────────────────────', ' {{key}} Text, Number, Textarea, Select,', ' Choose, Color, Switcher, Hidden,', ' Font, Date/Time, URL', ' {{key.url}} Media — image/file URL', ' {{key.size}} Slider — numeric value', ' {{icon(key)}} Icons — renders icon SVG/font', ' {{key}} Code — shows code as text', '', '───────────────────────────────────────────────────────', 'Conditionals (show/hide HTML):', '───────────────────────────────────────────────────────', ' {{#if key}} … {{/if}}', ' {{#if key == value}} … {{else}} … {{/if}}', ' {{#if key != value}} … {{/if}}', '', '───────────────────────────────────────────────────────', 'Quick example:', '───────────────────────────────────────────────────────', ' <h2>{{title_1}}</h2>', ' <img src="{{media_1.url}}">', ' {{#if show_badge}}', ' <span class="badge">New</span>', ' {{/if}}', '', 'Notes:', ' • Switcher returns "yes" (ON) or "" (OFF)', ' • Group controls (Typography, Background, Border)', ' only use CSS Styles — no template tags needed', ' • Click the ⓘ button above for full reference', '', '═══════════════════════════════════════════════════════════ -->', ].join('\n'); var htmlReferenceMarker = '<!-- ═══════════════════════════════════════════════════════'; function getHtmlWithReference(markup) { if (!markup) markup = ''; if (markup.indexOf(htmlReferenceMarker) !== -1) return markup; return markup + '\n\n\n\n\n' + htmlReferenceComment; } function stripHtmlReference(markup) { if (!markup) return ''; var idx = markup.indexOf(htmlReferenceMarker); if (idx === -1) return markup; return markup.substring(0, idx).replace(/\s+$/, ''); } function collectData() { return { title: $('#wpr-wb-title').val() || 'New Widget', icon: $('#wpr-wb-icon').val() || 'eicon-cog', category: $('#wpr-wb-category').val() || 'wpr-widgets', tabs: { content: collectTabSections('content'), style: collectTabSections('style'), advanced: collectTabSections('advanced') }, markup: editors.html ? stripHtmlReference(editors.html.getValue()) : '', css: editors.css ? editors.css.getValue() : '', js: editors.js ? editors.js.getValue() : '', css_includes: $('#wpr-wb-css-includes').val() ? $('#wpr-wb-css-includes').val().split('\n').filter(Boolean) : [], js_includes: $('#wpr-wb-js-includes').val() ? $('#wpr-wb-js-includes').val().split('\n').filter(Boolean) : [], }; } /* ------------------------------------------------------------------ * Load Widget data via REST API * ----------------------------------------------------------------*/ function loadWidget() { if (!wprWidgetBuilder.postId) { // New widget — show reference comment if (editors.html) { editors.html.setValue(getHtmlWithReference('')); } return; } $('#wpr-wb-app').addClass('loading'); $.ajax({ url: wprWidgetBuilder.apiBase + 'load/' + wprWidgetBuilder.postId, method: 'GET', headers: { 'X-WP-Nonce': wprWidgetBuilder.nonce }, success: function (res) { populateUI(res.data || res); $('#wpr-wb-app').removeClass('loading'); }, error: function () { showToast('Failed to load widget data.', 'error'); $('#wpr-wb-app').removeClass('loading'); } }); } /* ------------------------------------------------------------------ * Populate UI from loaded data * ----------------------------------------------------------------*/ function populateUI(data) { if (!data) return; // Left panel if (data.title) { $('#wpr-wb-title').val(data.title); $('#wpr-wb-header-title').text(data.title); } if (data.icon) { $('#wpr-wb-icon').val(data.icon); $('#wpr-wb-icon-preview-i').attr('class', data.icon); } // Category: API stores as categories array, UI uses single value var cat = data.category || (data.categories && data.categories[0]) || ''; if (cat) { $('#wpr-wb-category').val(cat); } // Center panel — sections if (data.tabs) { populateSections('content', data.tabs.content); populateSections('style', data.tabs.style); populateSections('advanced', data.tabs.advanced); } // Right panel — Monaco editors if (editors.html) { editors.html.setValue(getHtmlWithReference(data.markup || '')); } if (editors.css && data.css) { editors.css.setValue(data.css); } if (editors.js && data.js) { editors.js.setValue(data.js); } // Includes if (data.css_includes && Array.isArray(data.css_includes)) { $('#wpr-wb-css-includes').val(data.css_includes.join('\n')); } if (data.js_includes && Array.isArray(data.js_includes)) { $('#wpr-wb-js-includes').val(data.js_includes.join('\n')); } } /* ------------------------------------------------------------------ * Update header title in real time * ----------------------------------------------------------------*/ function initTitleSync() { $('#wpr-wb-title').on('input', function () { var val = $(this).val() || 'New Widget'; $('#wpr-wb-header-title').text(val); }); } /* ------------------------------------------------------------------ * Keyboard shortcuts * ----------------------------------------------------------------*/ function initKeyboardShortcuts() { $(document).on('keydown', function (e) { // Ctrl+S / Cmd+S — Save if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveWidget(); } }); } /* ------------------------------------------------------------------ * Unsaved changes warning * ----------------------------------------------------------------*/ function initBeforeUnload() { var dirty = false; // Mark dirty on any input change $('#wpr-wb-app').on('input change', 'input, select, textarea', function () { dirty = true; }); // Mark dirty when Monaco content changes var checkMonacoDirty = setInterval(function () { if (monacoReady) { clearInterval(checkMonacoDirty); ['html', 'css', 'js'].forEach(function (key) { if (editors[key]) { editors[key].onDidChangeModelContent(function () { dirty = true; }); } }); } }, 500); // Clear dirty after save $(document).ajaxSuccess(function (e, xhr, settings) { if (settings.url && settings.url.indexOf('widget-builder/save') !== -1) { dirty = false; } }); window.addEventListener('beforeunload', function (e) { if (dirty) { e.preventDefault(); e.returnValue = ''; } }); } /* ------------------------------------------------------------------ * Sections system — add / rename / collapse / delete / reorder * ----------------------------------------------------------------*/ var sectionCounter = 0; // unique id counter function initSections() { // Add Section buttons $('.wpr-wb-add-section-btn').on('click', function () { var tab = $(this).data('tab'); addSection(tab); }); // Delegate: toggle collapse (with animation) $('.wpr-wb-center-body').on('click', '.wpr-wb-section-toggle', function (e) { e.stopPropagation(); var $item = $(this).closest('.wpr-wb-section-item'); var $body = $item.find('.wpr-wb-section-body'); if ($item.hasClass('collapsed')) { $item.removeClass('collapsed'); $body.hide().slideDown(150); } else { $body.slideUp(150, function () { $item.addClass('collapsed'); }); } }); // Delegate: delete section $('.wpr-wb-center-body').on('click', '.wpr-wb-section-delete', function (e) { e.stopPropagation(); var $section = $(this).closest('.wpr-wb-section-item'); $section.slideUp(200, function () { $section.remove(); }); }); // Delegate: toggle section settings panel $('.wpr-wb-center-body').on('click', '.wpr-wb-section-settings-btn', function (e) { e.stopPropagation(); var $settings = $(this).closest('.wpr-wb-section-item').find('.wpr-wb-section-settings'); $settings.slideToggle(150); }); // Delegate: sync section key when edited in settings $('.wpr-wb-center-body').on('input', '.wpr-wb-section-key-input', function () { var $section = $(this).closest('.wpr-wb-section-item'); $section.attr('data-section-key', $(this).val()); $section.data('section-key', $(this).val()); }); // Init sortable on each sections list $('.wpr-wb-sections-list').sortable({ handle: '.wpr-wb-section-drag', placeholder: 'wpr-wb-section-placeholder', tolerance: 'pointer', axis: 'y', cursor: 'grabbing', forcePlaceholderSize: true, }); } /** * Add a new section to a tab */ function addSection(tab, label, key, description) { sectionCounter++; var sKey = key || 'section_' + sectionCounter; var sLabel = label || 'Section ' + sectionCounter; var sDesc = description || ''; var html = '' + '<div class="wpr-wb-section-item" data-section-key="' + sKey + '">' + '<div class="wpr-wb-section-header">' + '<span class="wpr-wb-section-drag"><i class="eicon-handle"></i></span>' + '<input type="text" class="wpr-wb-section-title-input" value="' + escAttr(sLabel) + '" data-field="label">' + '<div class="wpr-wb-section-actions">' + '<button type="button" class="wpr-wb-section-settings-btn" title="Section Settings"><i class="eicon-cog"></i></button>' + '<button type="button" class="wpr-wb-section-toggle" title="Toggle"><i class="eicon-caret-down"></i></button>' + '<button type="button" class="wpr-wb-section-delete" title="Delete"><i class="eicon-trash-o"></i></button>' + '</div>' + '</div>' + '<div class="wpr-wb-section-settings" style="display:none;">' + '<div class="wpr-wb-control-field">' + '<label class="wpr-wb-control-field-label">Section Key</label>' + '<input type="text" class="wpr-wb-control-field-input wpr-wb-section-key-input" value="' + escAttr(sKey) + '" data-field="section_key">' + '</div>' + '<div class="wpr-wb-control-field">' + '<label class="wpr-wb-control-field-label">Description</label>' + '<input type="text" class="wpr-wb-control-field-input" value="' + escAttr(sDesc) + '" data-field="description" placeholder="Optional section description">' + '</div>' + '</div>' + '<div class="wpr-wb-section-body">' + '<div class="wpr-wb-section-controls"></div>' + '<button type="button" class="wpr-wb-add-control-btn"><i class="eicon-plus"></i> Add Control</button>' + '</div>' + '</div>'; var $list = $('.wpr-wb-sections-list[data-tab="' + tab + '"]'); var $section = $(html).hide(); $list.append($section); $section.slideDown(200); // Refresh sortable $list.sortable('refresh'); return $section; } /** * Collect all sections & controls from a tab into an array */ function collectTabSections(tab) { var sections = []; $('.wpr-wb-sections-list[data-tab="' + tab + '"] .wpr-wb-section-item').each(function () { var $s = $(this); var sectionData = { key: $s.find('.wpr-wb-section-key-input').val() || $s.data('section-key'), label: $s.find('.wpr-wb-section-title-input').val(), description: $s.find('[data-field="description"]').val() || '', controls: [] }; // Controls will be collected here in Chapter 4 $s.find('.wpr-wb-section-controls .wpr-wb-control-item').each(function () { var $c = $(this); sectionData.controls.push(collectControlData($c)); }); sections.push(sectionData); }); return sections; } /** * Populate sections from loaded data */ function populateSections(tab, sections) { if (!sections || !Array.isArray(sections)) return; for (var i = 0; i < sections.length; i++) { var s = sections[i]; sectionCounter++; var $section = addSection(tab, s.label, s.key, s.description); if (s.controls && Array.isArray(s.controls)) { for (var j = 0; j < s.controls.length; j++) { var $ctrl = addControl($section, s.controls[j]); $ctrl.addClass('collapsed'); // collapse when loading saved data } } } } /** * Simple HTML attribute escaping */ function escAttr(str) { return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); } /* ------------------------------------------------------------------ * Control Types Registry * ----------------------------------------------------------------*/ var controlTypes = { // Basic text: { label: 'Text', icon: 'eicon-t-letter-bold', group: 'Basic' }, number: { label: 'Number', icon: 'eicon-number-field', group: 'Basic' }, textarea: { label: 'Textarea', icon: 'eicon-text-area', group: 'Basic' }, wysiwyg: { label: 'WYSIWYG', icon: 'eicon-text', group: 'Basic' }, code: { label: 'Code', icon: 'eicon-code', group: 'Basic' }, // Choice select: { label: 'Select', icon: 'eicon-select', group: 'Choice' }, select2: { label: 'Select2', icon: 'eicon-select', group: 'Choice' }, switcher: { label: 'Switcher', icon: 'eicon-toggle', group: 'Choice' }, choose: { label: 'Choose', icon: 'eicon-text-align-left', group: 'Choice' }, // Media media: { label: 'Media', icon: 'eicon-image', group: 'Media' }, icons: { label: 'Icons', icon: 'eicon-star', group: 'Media' }, // Design color: { label: 'Color', icon: 'eicon-paint-brush', group: 'Design' }, slider: { label: 'Slider', icon: 'eicon-h-align-stretch', group: 'Design' }, dimensions: { label: 'Dimensions', icon: 'eicon-frame-expand', group: 'Design' }, url: { label: 'URL', icon: 'eicon-url', group: 'Basic' }, // Group Controls typography: { label: 'Typography', icon: 'eicon-typography', group: 'Group Controls' }, background: { label: 'Background', icon: 'eicon-background', group: 'Group Controls' }, border: { label: 'Border', icon: 'eicon-square', group: 'Group Controls' }, box_shadow: { label: 'Box Shadow', icon: 'eicon-lightbox', group: 'Group Controls' }, text_shadow: { label: 'Text Shadow', icon: 'eicon-font', group: 'Group Controls' }, // Other font: { label: 'Font', icon: 'eicon-font', group: 'Other' }, date_time: { label: 'Date/Time', icon: 'eicon-date', group: 'Other' }, hidden: { label: 'Hidden', icon: 'eicon-eye-slash', group: 'Other' }, }; /** Group controls that need selector instead of options/default */ var groupControlTypes = ['typography','background','border','box_shadow','text_shadow']; /** Controls that use options (key:value pairs) */ var optionControlTypes = ['select','select2','choose']; var controlCounter = 0; /* ------------------------------------------------------------------ * Control Type Picker Modal * ----------------------------------------------------------------*/ var pendingSection = null; // which section the control will be added to function initControlPicker() { // Build the type grid once buildControlTypeGrid(); // Close modal $('#wpr-wb-control-modal-close').on('click', closeControlModal); $('#wpr-wb-control-modal').on('click', function (e) { if (e.target === this) closeControlModal(); }); $(document).on('keydown', function (e) { if (e.key === 'Escape' && $('#wpr-wb-control-modal').hasClass('open')) { closeControlModal(); } }); // Search $('#wpr-wb-control-search').on('input', function () { filterControlTypes($(this).val().toLowerCase()); }); // Select a control type $('#wpr-wb-control-type-grid').on('click', '.wpr-wb-ct-item', function () { var type = $(this).data('type'); if (pendingSection) { addControl(pendingSection, { type: type }); pendingSection = null; } closeControlModal(); }); } function openControlModal($section) { pendingSection = $section; $('#wpr-wb-control-modal').addClass('open'); $('#wpr-wb-control-search').val('').focus(); filterControlTypes(''); } function closeControlModal() { $('#wpr-wb-control-modal').removeClass('open'); pendingSection = null; } function buildControlTypeGrid() { var groups = {}; for (var type in controlTypes) { var ct = controlTypes[type]; if (!groups[ct.group]) groups[ct.group] = []; groups[ct.group].push({ type: type, label: ct.label, icon: ct.icon }); } var html = ''; var groupOrder = ['Basic', 'Choice', 'Media', 'Design', 'Group', 'Other']; for (var g = 0; g < groupOrder.length; g++) { var gName = groupOrder[g]; if (!groups[gName]) continue; html += '<div class="wpr-wb-ct-group" data-group="' + gName + '">'; html += '<div class="wpr-wb-ct-group-title">' + gName + '</div>'; html += '<div class="wpr-wb-ct-group-items">'; for (var i = 0; i < groups[gName].length; i++) { var item = groups[gName][i]; html += '<div class="wpr-wb-ct-item" data-type="' + item.type + '" data-label="' + item.label.toLowerCase() + '">'; html += '<div class="wpr-wb-ct-item-icon"><i class="' + item.icon + '"></i></div>'; html += '<span class="wpr-wb-ct-item-name">' + item.label + '</span>'; html += '</div>'; } html += '</div></div>'; } $('#wpr-wb-control-type-grid').html(html); } function filterControlTypes(query) { if (!query) { $('#wpr-wb-control-type-grid .wpr-wb-ct-item').show(); $('#wpr-wb-control-type-grid .wpr-wb-ct-group').show(); return; } $('#wpr-wb-control-type-grid .wpr-wb-ct-item').each(function () { var match = $(this).data('label').indexOf(query) !== -1 || $(this).data('type').indexOf(query) !== -1; $(this).toggle(match); }); // Hide empty groups $('#wpr-wb-control-type-grid .wpr-wb-ct-group').each(function () { var visible = $(this).find('.wpr-wb-ct-item:visible').length > 0; $(this).toggle(visible); }); } /* ------------------------------------------------------------------ * Controls — add / delete / duplicate / collapse / reorder * ----------------------------------------------------------------*/ function addControl($section, conf) { controlCounter++; conf = conf || {}; var type = conf.type || 'text'; var ct = controlTypes[type] || controlTypes.text; var key = conf.key || type + '_' + controlCounter; var label = conf.label || ct.label; var isGroup = groupControlTypes.indexOf(type) !== -1; var html = '' + '<div class="wpr-wb-control-item" data-type="' + type + '">' + '<div class="wpr-wb-control-header">' + '<span class="wpr-wb-control-drag"><i class="eicon-handle"></i></span>' + '<span class="wpr-wb-control-type-icon"><i class="' + ct.icon + '"></i></span>' + '<div class="wpr-wb-control-info">' + '<span class="wpr-wb-control-label-text">' + escAttr(label) + '</span>' + '<span class="wpr-wb-control-key-text">' + escAttr(key) + '</span>' + '</div>' + '<div class="wpr-wb-control-actions">' + '<button type="button" class="wpr-wb-control-duplicate" title="Duplicate"><i class="eicon-clone"></i></button>' + '<button type="button" class="wpr-wb-control-remove" title="Delete"><i class="eicon-trash-o"></i></button>' + '<button type="button" class="wpr-wb-control-toggle-btn" title="Toggle"><i class="eicon-caret-down"></i></button>' + '</div>' + '</div>' + '<div class="wpr-wb-control-body">' + buildControlFields(type, key, label, conf, isGroup) + '</div>' + '</div>'; var $controls = $section.find('.wpr-wb-section-controls'); var $control = $(html); // Collapse all existing controls in this section $controls.find('.wpr-wb-control-item').addClass('collapsed'); // Store initial data $control.data('control', $.extend({ type: type, key: key, label: label }, conf)); $controls.append($control); initControlSortable($controls); // Sync default select with premade options for choice controls if (optionControlTypes.indexOf(type) !== -1) { syncDefaultSelect($control.find('.wpr-wb-control-body')); } return $control; } /** * Build a single option row for the options repeater */ function buildOptionRow(key, title, icon) { var hasIcon = (icon !== null && icon !== undefined); var h = '<div class="wpr-wb-option-row">'; h += '<span class="wpr-wb-option-drag"><i class="eicon-handle"></i></span>'; h += '<input type="text" class="wpr-wb-option-key" value="' + escAttr(key || '') + '" placeholder="key">'; h += '<input type="text" class="wpr-wb-option-title" value="' + escAttr(title || '') + '" placeholder="label">'; if (hasIcon) { h += '<button type="button" class="wpr-wb-option-icon-btn" title="Choose Icon">'; h += '<i class="' + (icon ? escAttr(icon) : 'eicon-star') + '"></i>'; h += '</button>'; h += '<input type="hidden" class="wpr-wb-option-icon" value="' + escAttr(icon || '') + '">'; } h += '<button type="button" class="wpr-wb-option-remove" title="Remove"><i class="eicon-close"></i></button>'; h += '</div>'; return h; } /* ------------------------------------------------------------------ * CSS Selector Repeater Helpers * ----------------------------------------------------------------*/ var selectorPlaceholders = { color: 'color: {{VALUE}}', slider: 'width: {{SIZE}}', dimensions: 'padding: {{TOP}} {{RIGHT}} {{BOTTOM}} {{LEFT}}', choose: 'text-align: {{VALUE}}', select: 'display: {{VALUE}}', number: 'opacity: {{VALUE}}', font: 'font-family: {{VALUE}}', }; function buildSelectorRow(selector, declaration, controlType, overridePlaceholder) { var placeholder = overridePlaceholder || selectorPlaceholders[controlType] || 'color: {{VALUE}}'; var h = '<div class="wpr-wb-selector-row">'; h += '<div class="wpr-wb-selector-row-header">'; h += '<span class="wpr-wb-selector-row-drag"><i class="eicon-handle"></i></span>'; h += '<button type="button" class="wpr-wb-selector-row-remove"><i class="eicon-close"></i></button>'; h += '</div>'; h += '<input type="text" class="wpr-wb-selector-row-sel" value="' + escAttr(selector || '') + '" placeholder=".my-element">'; h += '<input type="text" class="wpr-wb-selector-row-decl" value="' + escAttr(declaration || '') + '" placeholder="' + placeholder + '">'; h += '</div>'; return h; } /** * Sync the default value <select> with options from the repeater. */ function syncDefaultSelect($body) { var $select = $body.find('.wpr-wb-default-select'); if (!$select.length) return; var curVal = $select.val(); var html = '<option value="">None</option>'; $body.find('.wpr-wb-options-repeater .wpr-wb-option-row').each(function () { var k = $(this).find('.wpr-wb-option-key').val() || ''; var l = $(this).find('.wpr-wb-option-title').val() || k; if (k) { var sel = (k === curVal) ? ' selected' : ''; html += '<option value="' + escAttr(k) + '"' + sel + '>' + escAttr(l) + '</option>'; } }); $select.html(html); } /** * Build the configuration fields HTML for a control based on its type */ function buildControlFields(type, key, label, conf, isGroup) { conf = conf || {}; var h = ''; // Label field (all controls) h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Label</label>'; h += '<input type="text" class="wpr-wb-control-field-input" data-field="label" value="' + escAttr(label) + '">'; h += '</div>'; // Key field (all controls) h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Key</label>'; h += '<div class="wpr-wb-key-input-wrap">'; h += '<input type="text" class="wpr-wb-control-field-input" data-field="key" value="' + escAttr(key) + '">'; h += '<button type="button" class="wpr-wb-key-copy-btn" title="Copy key"><i class="eicon-copy"></i></button>'; h += '</div>'; var hintTag = (type === 'icons') ? '{{icon(' + escAttr(key) + ')}}' : (type === 'media') ? '{{' + escAttr(key) + '.url}}' : '{{' + escAttr(key) + '}}'; h += '<span class="wpr-wb-control-field-hint">Copy to use in HTML: ' + hintTag + '</span>'; if (type === 'switcher') { h += '<span class="wpr-wb-control-field-hint">Conditional: {{#if ' + escAttr(key) + '}}...{{else}}...{{/if}}</span>'; } h += '</div>'; // Hidden control: only key + default value if (type === 'hidden') { h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Default Value</label>'; h += '<input type="text" class="wpr-wb-control-field-input" data-field="default" value="' + escAttr(conf.default || '') + '">'; h += '<span class="wpr-wb-control-field-hint">This value is stored but not visible to the user in Elementor</span>'; h += '</div>'; return h; } if (isGroup) { // Selector for group controls h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">CSS Selector</label>'; h += '<input type="text" class="wpr-wb-control-field-input" data-field="selector" value="' + escAttr(conf.selector || '') + '" placeholder=".my-element">'; h += '<span class="wpr-wb-control-field-hint">Applied to: {{WRAPPER}} .selector</span>'; h += '</div>'; } else { // Default value (non-group, skip icons) if (type !== 'icons') { h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Default Value</label>'; if (type === 'switcher') { // On/Off select for switcher var swVal = conf.default || ''; h += '<select class="wpr-wb-control-field-input" data-field="default">'; h += '<option value=""' + (swVal !== 'yes' ? ' selected' : '') + '>Off</option>'; h += '<option value="yes"' + (swVal === 'yes' ? ' selected' : '') + '>On</option>'; h += '</select>'; } else if (optionControlTypes.indexOf(type) !== -1) { // Select dropdown for choice controls — populated dynamically from options repeater h += '<select class="wpr-wb-control-field-input wpr-wb-default-select" data-field="default">'; h += '<option value="">None</option>'; if (conf.options && typeof conf.options === 'object') { for (var dk in conf.options) { var dlabel = (typeof conf.options[dk] === 'object') ? (conf.options[dk].title || dk) : (conf.options[dk] || dk); var dsel = (dk === (conf.default || '')) ? ' selected' : ''; h += '<option value="' + escAttr(dk) + '"' + dsel + '>' + escAttr(dlabel) + '</option>'; } } h += '</select>'; } else { h += '<input type="text" class="wpr-wb-control-field-input" data-field="default" value="' + escAttr(conf.default || '') + '">'; } h += '</div>'; // Code language if (type === 'code') { var codeLangs = { html: 'HTML', css: 'CSS', sass: 'SASS', scss: 'SCSS', javascript: 'JavaScript', json: 'JSON', less: 'LESS', markdown: 'Markdown', php: 'PHP', python: 'Python', mysql: 'MySQL', sql: 'SQL', svg: 'SVG', text: 'Text', twig: 'Twig', typescript: 'TypeScript' }; var curLang = conf.language || 'html'; h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Language</label>'; h += '<select class="wpr-wb-control-field-input" data-field="language">'; for (var lk in codeLangs) { var sel = (lk === curLang) ? ' selected' : ''; h += '<option value="' + lk + '"' + sel + '>' + codeLangs[lk] + '</option>'; } h += '</select>'; h += '</div>'; } // Slider/Number min/max/step if (type === 'slider' || type === 'number') { var sliderMin = (conf.slider_min !== undefined && conf.slider_min !== '') ? conf.slider_min : '0'; var sliderMax = (conf.slider_max !== undefined && conf.slider_max !== '') ? conf.slider_max : '1000'; var sliderStep = (conf.slider_step !== undefined && conf.slider_step !== '') ? conf.slider_step : '1'; h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Range</label>'; h += '<div class="wpr-wb-slider-range-row">'; h += '<div class="wpr-wb-slider-range-input"><span>Min</span><input type="number" step="any" class="wpr-wb-control-field-input" data-field="slider_min" value="' + escAttr(sliderMin) + '"></div>'; h += '<div class="wpr-wb-slider-range-input"><span>Max</span><input type="number" step="any" class="wpr-wb-control-field-input" data-field="slider_max" value="' + escAttr(sliderMax) + '"></div>'; h += '<div class="wpr-wb-slider-range-input"><span>Step</span><input type="number" step="any" class="wpr-wb-control-field-input" data-field="slider_step" value="' + escAttr(sliderStep) + '"></div>'; h += '</div>'; h += '</div>'; // No units toggle (slider only) if (type === 'slider') { var noUnits = conf.no_units ? 'yes' : ''; h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Disable Unit Selector</label>'; h += '<select class="wpr-wb-control-field-input" data-field="no_units">'; h += '<option value=""' + (noUnits !== 'yes' ? ' selected' : '') + '>No</option>'; h += '<option value="yes"' + (noUnits === 'yes' ? ' selected' : '') + '>Yes</option>'; h += '</select>'; h += '<span class="wpr-wb-control-field-hint">Hides px/em/% selector in Elementor — use your own unit in CSS Styles</span>'; h += '</div>'; } } h += '<hr class="wpr-wb-control-separator">'; } // Allowed Dimensions (dimensions only) if (type === 'dimensions') { var dimMode = conf.allowed_dimensions || 'all'; h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Allowed Dimensions</label>'; h += '<select class="wpr-wb-control-field-input" data-field="allowed_dimensions">'; h += '<option value="all"' + (dimMode === 'all' ? ' selected' : '') + '>All Fields</option>'; h += '<option value="vertical"' + (dimMode === 'vertical' ? ' selected' : '') + '>Top & Bottom</option>'; h += '<option value="horizontal"' + (dimMode === 'horizontal' ? ' selected' : '') + '>Left & Right</option>'; h += '</select>'; h += '<span class="wpr-wb-control-field-hint">Which dimension fields to show in Elementor</span>'; h += '</div>'; } // Options repeater for select/select2/choose if (optionControlTypes.indexOf(type) !== -1) { var isChoose = (type === 'choose'); var hasExisting = conf.options && typeof conf.options === 'object' && Object.keys(conf.options).length > 0; h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Options</label>'; h += '<div class="wpr-wb-options-repeater" data-type="' + type + '">'; if (hasExisting) { // Render existing options for (var ok in conf.options) { var optVal = conf.options[ok]; var optTitle = ''; var optIcon = ''; if (isChoose && typeof optVal === 'object') { optTitle = optVal.title || ''; optIcon = optVal.icon || ''; } else { optTitle = (typeof optVal === 'string') ? optVal : ''; } h += buildOptionRow(ok, optTitle, isChoose ? optIcon : null); } } else { // Premade default options for new controls if (isChoose) { h += buildOptionRow('value-1', 'Option 1', 'eicon-h-align-left'); h += buildOptionRow('value-2', 'Option 2', 'eicon-h-align-center'); h += buildOptionRow('value-3', 'Option 3', 'eicon-h-align-right'); } else { h += buildOptionRow('value-1', 'Option 1', null); h += buildOptionRow('value-2', 'Option 2', null); h += buildOptionRow('value-3', 'Option 3', null); } } h += '</div>'; // end repeater h += '<button type="button" class="wpr-wb-option-add-btn"><i class="eicon-plus"></i> Add Option</button>'; var optHint = (type === 'choose') ? 'Add options with key, label, and icon' : 'Add key-value pairs for dropdown options'; h += '<span class="wpr-wb-control-field-hint">' + optHint + '</span>'; h += '</div>'; h += '<hr class="wpr-wb-control-separator">'; } // CSS Styles (only for style-related controls) var selectorTypes = ['color', 'slider', 'dimensions', 'choose', 'select', 'number', 'font']; if (selectorTypes.indexOf(type) !== -1) { // Determine placeholder based on control settings var sliderNoUnits = (type === 'slider' && conf.no_units); var dimMode = (type === 'dimensions') ? (conf.allowed_dimensions || 'all') : ''; var curSelectorPlaceholder; if (sliderNoUnits) { curSelectorPlaceholder = 'width: {{SIZE}}px'; } else if (dimMode === 'vertical') { curSelectorPlaceholder = 'padding-top: {{TOP}}'; } else if (dimMode === 'horizontal') { curSelectorPlaceholder = 'padding-left: {{LEFT}}'; } else { curSelectorPlaceholder = selectorPlaceholders[type] || 'color: {{VALUE}}'; } if (type === 'font') { // Font: single selector input, font-family: {{VALUE}} applied automatically var fontSel = ''; if (conf.selectors) { var fontKeys = Object.keys(conf.selectors); if (fontKeys.length) fontSel = fontKeys[0]; } h += '<div class="wpr-wb-control-field wpr-wb-font-selector-field">'; h += '<label class="wpr-wb-control-field-label">Selector</label>'; h += '<input type="text" class="wpr-wb-control-field-input" data-field="font_selector" value="' + escAttr(fontSel) + '" placeholder=".my-element">'; h += '<span class="wpr-wb-control-field-hint">font-family: {{VALUE}} is applied automatically</span>'; h += '</div>'; } else { h += '<div class="wpr-wb-control-field wpr-wb-selectors-field" data-control-type="' + type + '">'; h += '<label class="wpr-wb-control-field-label">CSS Styles</label>'; h += '<div class="wpr-wb-selector-repeater">'; if (conf.selectors) { for (var sk in conf.selectors) { // Split merged declarations back into separate rows var fullDecl = conf.selectors[sk]; var parts = fullDecl.split(';').map(function (p) { return p.trim(); }).filter(Boolean); if (parts.length > 1) { for (var pi = 0; pi < parts.length; pi++) { h += buildSelectorRow(sk, parts[pi], type, curSelectorPlaceholder); } } else { h += buildSelectorRow(sk, fullDecl.replace(/;?\s*$/, ''), type, curSelectorPlaceholder); } } } h += '</div>'; h += '<button type="button" class="wpr-wb-selector-add-btn"><i class="eicon-plus"></i> Add Selector</button>'; h += '<span class="wpr-wb-control-field-hint">e.g. ' + escAttr(curSelectorPlaceholder) + '</span>'; h += '</div>'; } h += '<hr class="wpr-wb-control-separator">'; } } // Condition (all controls) var condKey = (conf.condition && conf.condition.key) ? conf.condition.key : ''; var condVal = (conf.condition && conf.condition.value) ? conf.condition.value : ''; h += '<div class="wpr-wb-control-field wpr-wb-condition-field">'; h += '<label class="wpr-wb-control-field-label">Condition</label>'; h += '<div class="wpr-wb-condition-row">'; h += '<input type="text" class="wpr-wb-control-field-input" data-field="condition_key" value="' + escAttr(condKey) + '" placeholder="control_key" style="width:48%">'; h += '<span class="wpr-wb-condition-eq">=</span>'; h += '<input type="text" class="wpr-wb-control-field-input" data-field="condition_value" value="' + escAttr(condVal) + '" placeholder="value" style="width:48%">'; h += '</div>'; h += '<span class="wpr-wb-control-field-hint">Show this control only when another control equals a value</span>'; h += '</div>'; // Separator (all controls) h += '<div class="wpr-wb-control-field">'; h += '<label class="wpr-wb-control-field-label">Separator</label>'; h += '<select class="wpr-wb-control-field-input" data-field="separator">'; h += '<option value=""' + (!conf.separator ? ' selected' : '') + '>None</option>'; h += '<option value="before"' + (conf.separator === 'before' ? ' selected' : '') + '>Before</option>'; h += '<option value="after"' + (conf.separator === 'after' ? ' selected' : '') + '>After</option>'; h += '</select>'; h += '</div>'; return h; } /** * Collect data from a single control element */ function collectControlData($control) { var data = { type: $control.data('type'), key: $control.find('[data-field="key"]').val(), label: $control.find('[data-field="label"]').val(), }; var isGroup = groupControlTypes.indexOf(data.type) !== -1; if (isGroup) { data.selector = $control.find('[data-field="selector"]').val() || ''; } else { data.default = $control.find('[data-field="default"]').val() || ''; // Collect options from repeater var $repeater = $control.find('.wpr-wb-options-repeater'); if ($repeater.length) { var opts = {}; var isChooseType = ($repeater.data('type') === 'choose'); $repeater.find('.wpr-wb-option-row').each(function () { var optKey = $(this).find('.wpr-wb-option-key').val().trim(); var optTitle = $(this).find('.wpr-wb-option-title').val().trim(); if (!optKey) return; if (isChooseType) { var optIcon = $(this).find('.wpr-wb-option-icon').val() || ''; opts[optKey] = { title: optTitle, icon: optIcon }; } else { opts[optKey] = optTitle; } }); if (Object.keys(opts).length > 0) { data.options = opts; } } // Font: single selector input if (data.type === 'font') { var fontSel = $control.find('[data-field="font_selector"]').val(); if (fontSel && fontSel.trim()) { data.selectors = {}; data.selectors[fontSel.trim()] = 'font-family: {{VALUE}};'; } } else { // Parse selectors from repeater rows var $selectorsField = $control.find('.wpr-wb-selectors-field'); if ($selectorsField.length) { var sels = {}; $selectorsField.find('.wpr-wb-selector-row').each(function () { var sel = $(this).find('.wpr-wb-selector-row-sel').val().trim(); var decl = $(this).find('.wpr-wb-selector-row-decl').val().trim(); if (sel && decl) { // Ensure declaration ends with semicolon for merging var declClean = decl.replace(/;?\s*$/, ''); if (sels[sel]) { sels[sel] = sels[sel].replace(/;?\s*$/, '') + '; ' + declClean + ';'; } else { sels[sel] = declClean + ';'; } } }); if (Object.keys(sels).length > 0) { data.selectors = sels; } } } } // Condition var condKey = $control.find('[data-field="condition_key"]').val(); var condVal = $control.find('[data-field="condition_value"]').val(); if (condKey) { data.condition = { key: condKey, value: condVal || '' }; } // Code language if (data.type === 'code') { var lang = $control.find('[data-field="language"]').val(); if (lang) data.language = lang; } // Slider min/max if (data.type === 'slider' || data.type === 'number') { var sMin = $control.find('[data-field="slider_min"]').val(); var sMax = $control.find('[data-field="slider_max"]').val(); var sStep = $control.find('[data-field="slider_step"]').val(); if (sMin !== '' && sMin !== undefined) data.slider_min = sMin; if (sMax !== '' && sMax !== undefined) data.slider_max = sMax; if (sStep !== '' && sStep !== undefined) data.slider_step = sStep; if (data.type === 'slider') { var noUnits = $control.find('[data-field="no_units"]').val(); if (noUnits === 'yes') data.no_units = true; } } // Allowed dimensions if (data.type === 'dimensions') { var dimMode = $control.find('[data-field="allowed_dimensions"]').val(); if (dimMode && dimMode !== 'all') { data.allowed_dimensions = dimMode; } } var sep = $control.find('[data-field="separator"]').val(); if (sep) data.separator = sep; return data; } /** * Parse options textarea value into object */ function parseOptions(str, type) { var opts = {}; var lines = str.trim().split('\n'); for (var i = 0; i < lines.length; i++) { var parts = lines[i].split('|'); if (parts.length < 2) continue; var key = parts[0].trim(); if (type === 'choose' && parts.length >= 3) { opts[key] = { title: parts[1].trim(), icon: parts[2].trim() }; } else { opts[key] = parts[1].trim(); } } return opts; } /** * Parse selectors textarea value into object */ function parseSelectors(str) { var sels = {}; var lines = str.trim().split('\n'); for (var i = 0; i < lines.length; i++) { var parts = lines[i].split('||'); if (parts.length < 2) continue; sels[parts[0].trim()] = parts[1].trim(); } return sels; } /** * Init sortable on a controls list */ function initControlSortable($controls) { if ($controls.data('ui-sortable')) { $controls.sortable('refresh'); return; } $controls.sortable({ handle: '.wpr-wb-control-drag', placeholder: 'wpr-wb-control-placeholder', connectWith: '.wpr-wb-section-controls', tolerance: 'pointer', cursor: 'grabbing', forcePlaceholderSize: true, }); } function initControlEvents() { var $body = $('.wpr-wb-center-body'); // Open control picker (overrides the placeholder from Chapter 3) $body.off('click', '.wpr-wb-add-control-btn'); $body.on('click', '.wpr-wb-add-control-btn', function () { var $section = $(this).closest('.wpr-wb-section-item'); openControlModal($section); }); // Toggle collapse — clicking header or toggle button $body.on('click', '.wpr-wb-control-header', function (e) { // Don't toggle if clicking drag handle, duplicate, or delete buttons if ($(e.target).closest('.wpr-wb-control-drag, .wpr-wb-control-duplicate, .wpr-wb-control-remove').length) { return; } e.stopPropagation(); var $item = $(this).closest('.wpr-wb-control-item'); var isCollapsed = $item.hasClass('collapsed'); // Collapse all other controls in the same section $item.closest('.wpr-wb-section-controls').find('.wpr-wb-control-item').not($item).addClass('collapsed'); $item.toggleClass('collapsed'); // Init sortable on option repeaters and selector repeaters when expanding if (!$item.hasClass('collapsed')) { $item.find('.wpr-wb-options-repeater').each(function () { if (!$(this).data('ui-sortable')) { $(this).sortable({ handle: '.wpr-wb-option-drag', placeholder: 'wpr-wb-option-placeholder', tolerance: 'pointer', axis: 'y', cursor: 'grabbing', }); } }); $item.find('.wpr-wb-selector-repeater').each(function () { if (!$(this).data('ui-sortable')) { $(this).sortable({ handle: '.wpr-wb-selector-row-drag', placeholder: 'wpr-wb-selector-row-placeholder', tolerance: 'pointer', axis: 'y', cursor: 'grabbing', }); } }); } }); // Delete $body.on('click', '.wpr-wb-control-remove', function (e) { e.stopPropagation(); var $ctrl = $(this).closest('.wpr-wb-control-item'); $ctrl.slideUp(150, function () { $ctrl.remove(); }); }); // Duplicate $body.on('click', '.wpr-wb-control-duplicate', function (e) { e.stopPropagation(); var $ctrl = $(this).closest('.wpr-wb-control-item'); var $section = $ctrl.closest('.wpr-wb-section-item'); var data = collectControlData($ctrl); controlCounter++; data.key = data.type + '_' + controlCounter; addControl($section, data); }); // Sync label display when label field changes $body.on('input', '.wpr-wb-control-body [data-field="label"]', function () { var $item = $(this).closest('.wpr-wb-control-item'); $item.find('.wpr-wb-control-label-text').text($(this).val()); }); // Sync key display when key field changes $body.on('input', '.wpr-wb-control-body [data-field="key"]', function () { var $item = $(this).closest('.wpr-wb-control-item'); $item.find('.wpr-wb-control-key-text').text($(this).val()); // Update template hint var ctrlType = $item.data('type'); var k = $(this).val(); var hint = (ctrlType === 'icons') ? '{{icon(' + k + ')}}' : (ctrlType === 'media') ? '{{' + k + '.url}}' : '{{' + k + '}}'; $item.find('.wpr-wb-control-field-hint').first().text('Copy to use in HTML: ' + hint); }); // Copy key as template tag $body.on('click', '.wpr-wb-key-copy-btn', function (e) { e.stopPropagation(); e.preventDefault(); var $item = $(this).closest('.wpr-wb-control-item'); var $input = $(this).closest('.wpr-wb-key-input-wrap').find('[data-field="key"]'); var keyVal = $input.val(); var ctrlType = $item.data('type'); if (ctrlType === 'icons') { var tag = '{{icon(' + keyVal + ')}}'; } else if (ctrlType === 'media') { var tag = '{{' + keyVal + '.url}}'; } else { var tag = '{{' + keyVal + '}}'; } // Use temporary textarea for reliable copy var $temp = $('<textarea>'); $temp.val(tag).css({ position: 'fixed', left: '-9999px' }).appendTo('body'); $temp[0].select(); document.execCommand('copy'); $temp.remove(); showToast('Copied ' + tag, 'success'); }); // Options repeater: Add option $body.on('click', '.wpr-wb-option-add-btn', function () { var $repeater = $(this).siblings('.wpr-wb-options-repeater'); var isChoose = ($repeater.data('type') === 'choose'); var newRow = buildOptionRow('', '', isChoose ? '' : null); $repeater.append(newRow); // Init sortable on the repeater if not already if (!$repeater.data('ui-sortable')) { $repeater.sortable({ handle: '.wpr-wb-option-drag', placeholder: 'wpr-wb-option-placeholder', tolerance: 'pointer', axis: 'y', cursor: 'grabbing', }); } else { $repeater.sortable('refresh'); } syncDefaultSelect($(this).closest('.wpr-wb-control-body')); }); // Options repeater: Remove option $body.on('click', '.wpr-wb-option-remove', function () { var $control = $(this).closest('.wpr-wb-control-body'); $(this).closest('.wpr-wb-option-row').remove(); syncDefaultSelect($control); }); // Options repeater: Sync on key/label change $body.on('input', '.wpr-wb-option-key, .wpr-wb-option-title', function () { syncDefaultSelect($(this).closest('.wpr-wb-control-body')); }); // Dimensions: update CSS Styles placeholders/hint when "Allowed Dimensions" changes $body.on('change', '[data-field="allowed_dimensions"]', function () { var $ctrlBody = $(this).closest('.wpr-wb-control-body'); var mode = $(this).val(); var placeholder, hint; if (mode === 'vertical') { placeholder = 'padding-top: {{TOP}}'; hint = 'e.g. padding-top: {{TOP}}, padding-bottom: {{BOTTOM}}'; } else if (mode === 'horizontal') { placeholder = 'padding-left: {{LEFT}}'; hint = 'e.g. padding-left: {{LEFT}}, padding-right: {{RIGHT}}'; } else { placeholder = 'padding: {{TOP}} {{RIGHT}} {{BOTTOM}} {{LEFT}}'; hint = 'e.g. padding: {{TOP}} {{RIGHT}} {{BOTTOM}} {{LEFT}}'; } $ctrlBody.find('.wpr-wb-selector-row-decl').attr('placeholder', placeholder); $ctrlBody.find('.wpr-wb-selectors-field > .wpr-wb-control-field-hint').text(hint); }); // Slider: update CSS Styles placeholders/hint when "Disable Unit Selector" changes $body.on('change', '[data-field="no_units"]', function () { var $ctrlBody = $(this).closest('.wpr-wb-control-body'); var noUnits = $(this).val() === 'yes'; var placeholder = noUnits ? 'width: {{SIZE}}px' : 'width: {{SIZE}}'; var hint = noUnits ? 'e.g. width: {{SIZE}}px' : 'e.g. width: {{SIZE}}'; $ctrlBody.find('.wpr-wb-selector-row-decl').attr('placeholder', placeholder); $ctrlBody.find('.wpr-wb-selectors-field > .wpr-wb-control-field-hint').text(hint); }); // Options repeater: Icon picker for choose options $body.on('click', '.wpr-wb-option-icon-btn', function () { var $btn = $(this); openOptionIconPicker($btn); }); // CSS Selector repeater: Add row $body.on('click', '.wpr-wb-selector-add-btn', function () { var $repeater = $(this).siblings('.wpr-wb-selector-repeater'); var controlType = $(this).closest('.wpr-wb-selectors-field').data('control-type') || 'color'; var $ctrlBody = $(this).closest('.wpr-wb-control-body'); var override = null; if (controlType === 'slider' && $ctrlBody.find('[data-field="no_units"]').val() === 'yes') { override = 'width: {{SIZE}}px'; } else if (controlType === 'dimensions') { var dm = $ctrlBody.find('[data-field="allowed_dimensions"]').val() || 'all'; if (dm === 'vertical') override = 'padding-top: {{TOP}}'; else if (dm === 'horizontal') override = 'padding-left: {{LEFT}}'; } $repeater.append(buildSelectorRow('', '', controlType, override)); if (!$repeater.data('ui-sortable')) { $repeater.sortable({ handle: '.wpr-wb-selector-row-drag', placeholder: 'wpr-wb-selector-row-placeholder', tolerance: 'pointer', axis: 'y', cursor: 'grabbing', }); } else { $repeater.sortable('refresh'); } }); // CSS Selector repeater: Remove row $body.on('click', '.wpr-wb-selector-row-remove', function () { $(this).closest('.wpr-wb-selector-row').remove(); }); } /* ------------------------------------------------------------------ * Option Icon Picker (reuses icon list from main icon picker) * ----------------------------------------------------------------*/ var pendingOptionIconBtn = null; function openOptionIconPicker($btn) { pendingOptionIconBtn = $btn; var $modal = $('#wpr-wb-icon-modal'); $modal.addClass('open').attr('data-mode', 'option-icon'); $('#wpr-wb-icon-search').val('').focus(); if (allIcons.length === 0) { loadIconList(); } else { renderIcons(allIcons); } } /* ------------------------------------------------------------------ * Icon Picker * ----------------------------------------------------------------*/ var allIcons = []; // cached list of eicon-* class names function initIconPicker() { // Open modal $('#wpr-wb-change-icon').on('click', function () { openIconModal(); }); // Close modal $('#wpr-wb-icon-modal-close').on('click', closeIconModal); $('#wpr-wb-icon-modal').on('click', function (e) { if (e.target === this) closeIconModal(); }); $(document).on('keydown', function (e) { if (e.key === 'Escape' && $('#wpr-wb-icon-modal').hasClass('open')) { closeIconModal(); } }); // Search $('#wpr-wb-icon-search').on('input', function () { var query = $(this).val().toLowerCase(); filterIcons(query); }); // Select icon (delegated) $('#wpr-wb-icon-grid').on('click', '.wpr-wb-icon-grid-item', function () { var iconClass = $(this).data('icon'); var mode = $('#wpr-wb-icon-modal').attr('data-mode'); if (mode === 'option-icon' && pendingOptionIconBtn) { // Update the option row icon pendingOptionIconBtn.find('i').attr('class', iconClass); pendingOptionIconBtn.siblings('.wpr-wb-option-icon').val(iconClass); pendingOptionIconBtn = null; } else { // Widget icon picker $('#wpr-wb-icon').val(iconClass); $('#wpr-wb-icon-preview-i').attr('class', iconClass); } $('.wpr-wb-icon-grid-item').removeClass('selected'); $(this).addClass('selected'); closeIconModal(); }); } function openIconModal() { var $modal = $('#wpr-wb-icon-modal'); $modal.addClass('open').attr('data-mode', 'widget-icon'); $('#wpr-wb-icon-search').val('').focus(); if (allIcons.length === 0) { loadIconList(); } else { renderIcons(allIcons); } } function closeIconModal() { $('#wpr-wb-icon-modal').removeClass('open').removeAttr('data-mode'); pendingOptionIconBtn = null; } function loadIconList() { var $grid = $('#wpr-wb-icon-grid'); $grid.html('<div class="wpr-wb-icon-grid-empty">Loading icons...</div>'); $.getJSON(wprWidgetBuilder.eiconsUrl, function (data) { // Filter out advanced/pro icons that don't render in free version var excludePrefixes = [ 'eicon-ehp-', 'eicon-e-', 'eicon-atomic', 'eicon-library-', 'eicon-kit-', 'eicon-upgrade', 'eicon-notification', 'eicon-light-mode', 'eicon-dark-mode', 'eicon-off-canvas', 'eicon-speakerphone', 'eicon-div-block', 'eicon-flexbox', 'eicon-taxonomy-filter', 'eicon-tab-content', 'eicon-tab-menu', 'eicon-elementor-circle', 'eicon-elementor', 'eicon-editor-underline', 'eicon-contact', 'eicon-layout', 'eicon-components', 'eicon-accessibility', 'eicon-lock-outline', 'eicon-advanced' ]; allIcons = Object.keys(data).map(function (key) { return 'eicon-' + key; }).filter(function (icon) { for (var i = 0; i < excludePrefixes.length; i++) { if (icon === excludePrefixes[i] || icon.indexOf(excludePrefixes[i]) === 0) { return false; } } return true; }); renderIcons(allIcons); }).fail(function () { // Fallback: use a small built-in list of common icons allIcons = [ 'eicon-cog','eicon-code','eicon-text','eicon-image','eicon-video-camera', 'eicon-button','eicon-heading','eicon-divider','eicon-spacer','eicon-icon-box', 'eicon-tabs','eicon-accordion','eicon-toggle','eicon-star','eicon-counter', 'eicon-slider-push','eicon-carousel','eicon-gallery-grid','eicon-posts-grid', 'eicon-form-horizontal','eicon-table','eicon-countdown','eicon-price-table', 'eicon-social-icons','eicon-google-maps','eicon-menu-bar','eicon-search', 'eicon-person','eicon-mail','eicon-cart','eicon-heart','eicon-play', 'eicon-share','eicon-download-button','eicon-alert','eicon-info-circle' ]; renderIcons(allIcons); }); } function renderIcons(icons) { var $grid = $('#wpr-wb-icon-grid'); var currentIcon = $('#wpr-wb-icon').val(); var html = ''; if (icons.length === 0) { html = '<div class="wpr-wb-icon-grid-empty">No icons found.</div>'; } else { for (var i = 0; i < icons.length; i++) { var sel = icons[i] === currentIcon ? ' selected' : ''; html += '<div class="wpr-wb-icon-grid-item' + sel + '" data-icon="' + icons[i] + '" title="' + icons[i] + '">'; html += '<i class="' + icons[i] + '"></i>'; html += '</div>'; } } $grid.html(html); } function filterIcons(query) { if (!query) { renderIcons(allIcons); return; } var filtered = allIcons.filter(function (icon) { return icon.indexOf(query) !== -1; }); renderIcons(filtered); } /* ------------------------------------------------------------------ * Initialise everything on DOM ready * ----------------------------------------------------------------*/ $(function () { initPanelCollapse(); initCenterTabs(); initCodeTabs(); initFooterButtons(); initTitleSync(); initSections(); initControlPicker(); initControlEvents(); initIconPicker(); initKeyboardShortcuts(); initBeforeUnload(); initMonaco(); }); })(jQuery);