From d0b589c693b02deb6362917f6bb4983a71a292ea Mon Sep 17 00:00:00 2001 From: Cody Reichert Date: Sun, 4 Oct 2026 21:20:43 -0500 Subject: [PATCH 1/6] Organize and simplify the SimplyRETS admin settings UI --- src/assets/css/simply-rets-admin.css | 107 ++++++++++++- src/assets/js/simply-rets-admin.js | 218 ++++++++++++++++++--------- src/simply-rets-admin.php | 127 ++++++++-------- src/simply-rets-post-pages.php | 4 +- 4 files changed, 318 insertions(+), 138 deletions(-) diff --git a/src/assets/css/simply-rets-admin.css b/src/assets/css/simply-rets-admin.css index 3c6b4ba..b6147bd 100644 --- a/src/assets/css/simply-rets-admin.css +++ b/src/assets/css/simply-rets-admin.css @@ -16,10 +16,9 @@ .sr-admin-wrap .sr-admin-logo { display: inline-block; - max-height: 50px; - margin-bottom: 5px; - margin-right: 5px; vertical-align: bottom; + max-height: 42px; + margin: 0 12px 5px 0; } .sr-admin-wrap .sr-admin-title { @@ -29,6 +28,9 @@ line-height: 50px; margin-top: 0px; margin-bottom: 5px; + font-size: 26px; + font-weight: 600; + letter-spacing: -.5px; } .sr-admin-msg-btn { @@ -44,4 +46,101 @@ #admin-msg { display: inline-block; -} \ No newline at end of file +} +/* Settings styles stay inside the plugin admin page. */ +.sr-admin-wrap { + max-width: 1180px; + color: #25343c; + padding-bottom: 24px; +} +.sr-admin-wrap [hidden] { display: none !important; } +.sr-admin-wrap .sr-settings-header { margin: 26px 0 18px; } +.sr-admin-wrap .sr-settings-intro { margin: 6px 0 0; color: #596971; font-size: 14px; } +.sr-admin-wrap .sr-doc-links { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-bottom: 22px; + border-bottom: 1px solid #dce2e5; + margin-bottom: 22px; +} +.sr-admin-wrap .sr-doc-links p { margin: 0; color: #c2cccf; } +.sr-admin-wrap a { color: #096c65; } +.sr-admin-wrap .sr-doc-links a { margin-right: 6px; text-decoration: none; } +.sr-admin-wrap .sr-doc-links a:hover { text-decoration: underline; } +.sr-admin-wrap .sr-settings-tools { display: flex; flex-wrap: wrap; gap: 8px; } +.sr-admin-wrap .sr-settings-tools .button { margin: 0; } +.sr-admin-wrap .sr-settings-toolbar { margin: 0 0 24px; max-width: 620px; } +.sr-admin-wrap .sr-settings-toolbar > label { display: block; font-weight: 600; margin-bottom: 8px; } +.sr-admin-wrap .sr-settings-search-box { display: flex; align-items: center; gap: 8px; } +.sr-admin-wrap .sr-settings-search-box .dashicons { position: absolute; margin-left: 12px; color: #73838a; pointer-events: none; } +.sr-admin-wrap #sr-settings-search { width: 100%; min-height: 42px; padding-left: 40px; border-color: #b9c6cc; border-radius: 6px; } +.sr-admin-wrap .sr-clear-search { flex-shrink: 0; } +.sr-admin-wrap .sr-search-status { margin: 8px 0 0; color: #596971; } +.sr-admin-wrap .sr-search-status:empty { display: none; } +.sr-admin-wrap.sr-settings-enhanced .sr-settings-layout { display: grid; grid-template-columns: 230px minmax(0, 1fr); gap: 24px; align-items: start; } +.sr-admin-wrap .sr-settings-nav { position: sticky; top: 52px; } +.sr-admin-wrap .sr-nav-heading { padding-left: 14px; margin: 0 0 10px; font-size: 10px; font-weight: 700; letter-spacing: 1.5px; color: #728087; } +.sr-admin-wrap .sr-settings-nav a { display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin-bottom: 4px; border-radius: 6px; text-decoration: none; color: #43545d; border: 1px solid transparent; } +.sr-admin-wrap .sr-settings-nav a strong { display: block; font-size: 13px; font-weight: 600; } +.sr-admin-wrap .sr-settings-nav a small { display: block; font-size: 11px; margin-top: 3px; color: #647780; } +.sr-admin-wrap .sr-settings-nav a:hover { background: #e7edee; } +.sr-admin-wrap .sr-settings-nav a[aria-current] { background: #e1efec; color: #075c55; border-color: #bfd9d2; } +.sr-admin-wrap .sr-settings-nav a:last-child { border-top: 1px solid #dce2e5; border-radius: 0; margin-top: 16px; padding-top: 18px; } +.sr-admin-wrap .sr-settings-form { min-width: 0; } +.sr-admin-wrap .sr-settings-panel, .sr-admin-wrap .sr-settings-empty { background: #fff; border: 1px solid #dce2e5; border-radius: 8px; padding: 28px 30px; margin: 0 0 20px; box-shadow: 0 2px 4px rgba(24, 46, 54, .03); } +.sr-admin-wrap .sr-settings-panel > h2 { margin: 0 0 10px; font-size: 21px; line-height: 1.4; letter-spacing: -.3px; } +.sr-admin-wrap .sr-settings-panel h3 { font-size: 14px; margin: 26px 0 12px; padding-top: 22px; border-top: 1px solid #edf0f2; } +.sr-admin-wrap .sr-settings-panel > h2 + h3 { padding-top: 14px; margin-top: 14px; } +.sr-admin-wrap .sr-settings-panel p, .sr-admin-wrap .sr-settings-panel li { line-height: 1.7; } +.sr-admin-wrap .sr-settings-panel table { width: 100%; border-collapse: collapse; } +.sr-admin-wrap .sr-settings-panel td { padding: 10px 0; vertical-align: top; line-height: 1.65; } +.sr-admin-wrap .sr-settings-panel td + td { padding-left: 18px; width: 55%; } +.sr-admin-wrap .sr-settings-panel label { display: block; cursor: pointer; } +.sr-admin-wrap .sr-settings-panel input[type="checkbox"], .sr-admin-wrap .sr-settings-panel input[type="radio"] { margin-top: 2px; } +.sr-admin-wrap .sr-settings-panel input[type="text"], +.sr-admin-wrap .sr-settings-panel input[type="password"], +.sr-admin-wrap .sr-settings-panel input[type="email"], +.sr-admin-wrap .sr-settings-panel select, +.sr-admin-wrap .sr-settings-panel textarea { width: 100%; max-width: 540px; min-height: 38px; border-color: #b9c6cc; border-radius: 5px; } +.sr-admin-wrap .sr-settings-panel textarea { padding: 12px; font-family: monospace; } +.sr-admin-wrap .sr-settings-panel small { font-size: 12px; color: #64717b; } +.sr-admin-wrap .sr-demo-note { padding: 14px 16px; background: #f1f6f5; border-radius: 5px; color: #52666a; } +.sr-admin-wrap .sr-demo-note span { line-height: 1.8; } +.sr-admin-wrap .sr-demo-note i { display: block; font-style: normal; } +.sr-admin-wrap .sr-reveal-credentials { margin: 8px 0; } +.sr-admin-wrap .sr-settings-savebar { display: flex; justify-content: space-between; align-items: center; gap: 16px; position: sticky; bottom: 0; z-index: 5; padding: 16px 22px; border: 1px solid #dce2e5; border-radius: 8px; background: #fff; box-shadow: 0 -3px 16px rgba(24, 46, 54, .06); } +.sr-admin-wrap .sr-settings-savebar strong { display: block; font-size: 13px; } +.sr-admin-wrap .sr-settings-savebar span { display: block; font-size: 12px; margin-top: 3px; color: #647780; } +.sr-admin-wrap .sr-settings-savebar.sr-is-dirty { border-color: #9cc8ba; } +.sr-admin-wrap .sr-is-dirty .sr-save-status { color: #08665d; } +.sr-admin-wrap .sr-settings-savebar .button-primary { background: #08776e; border-color: #08776e; padding: 2px 20px; min-height: 38px; } +.sr-admin-wrap .sr-settings-savebar .button-primary:hover { background: #065e57; border-color: #065e57; } +.sr-admin-wrap .sr-search-match { background: #edf6f2; outline: 4px solid #edf6f2; } +@media (max-width: 1100px) { + .sr-admin-wrap.sr-settings-enhanced .sr-settings-layout { grid-template-columns: 200px minmax(0, 1fr); gap: 16px; } + .sr-admin-wrap .sr-settings-panel { padding: 22px; } +} +@media (max-width: 782px) { + .sr-admin-wrap .sr-admin-title { height: auto; line-height: 1.35; font-size: 23px; } + .sr-admin-wrap .sr-admin-logo { display: none; } + .sr-admin-wrap.sr-settings-enhanced .sr-settings-layout { display: block; } + .sr-admin-wrap .sr-settings-nav { position: static; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; margin-bottom: 20px; } + .sr-admin-wrap .sr-nav-heading { display: none; } + .sr-admin-wrap .sr-settings-nav a { padding: 10px; margin: 0; } + .sr-admin-wrap .sr-settings-nav a small { display: none; } + .sr-admin-wrap .sr-settings-nav a:last-child { margin: 0; border: 1px solid transparent; padding-top: 10px; border-radius: 6px; } + .sr-admin-wrap .sr-settings-panel { padding: 20px 16px; } + .sr-admin-wrap .sr-settings-panel td { display: block; width: auto; padding: 8px 0; } + .sr-admin-wrap .sr-settings-panel td + td { width: auto; padding-left: 0; } + .sr-admin-wrap .sr-settings-panel td > div[style] { max-width: none !important; } + .sr-admin-wrap .sr-settings-panel textarea { max-width: 100%; box-sizing: border-box; } + .sr-admin-wrap #sr_leadcapture_custom_form { margin-left: 0 !important; } + .sr-admin-wrap .sr-settings-savebar { padding: 12px; gap: 8px; border-radius: 6px; } + .sr-admin-wrap .sr-settings-savebar .button-primary { padding: 4px 12px; flex-shrink: 0; } + .sr-admin-wrap .sr-settings-search-box { flex-wrap: wrap; } +} + +.sr-admin-wrap .sr-settings-panel { scroll-margin-top: 54px; } diff --git a/src/assets/js/simply-rets-admin.js b/src/assets/js/simply-rets-admin.js index bf2d5a9..c3a0b07 100644 --- a/src/assets/js/simply-rets-admin.js +++ b/src/assets/js/simply-rets-admin.js @@ -7,82 +7,164 @@ */ -jQuery(function() { +// Listing-page editor filters use the existing labels and control IDs. +jQuery(function($) { + var filterInputs = { + 'Minimum Price': '#sr-min-price-span', + 'Maximum Price': '#sr-max-price-span', + 'Minimum Beds': '#sr-min-bed-span', + 'Maximum Beds': '#sr-max-bed-span', + 'Minimum Bathrooms': '#sr-min-bath-span', + 'Maximum Bathrooms': '#sr-max-bath-span', + 'Listing Agent': '#sr-listing-agent-span', + 'Listing Type': '#sr-listing-type-span', + 'Amount of listings': '#sr-limit-span' + }; - // create jquery objects for all of our different input boxes - var minPriceFilterInput = jQuery('#sr-min-price-span'); - var maxPriceFilterInput = jQuery('#sr-max-price-span'); - var minBedFilterInput = jQuery('#sr-min-bed-span'); - var maxBedFilterInput = jQuery('#sr-max-bed-span'); - var minBathFilterInput = jQuery('#sr-min-bath-span'); - var maxBathFilterInput = jQuery('#sr-max-bath-span'); - var agentFilterInput = jQuery('#sr-listing-agent-span'); - var listingTypeSelect = jQuery('#sr-listing-type-span'); - var limitInput = jQuery('#sr-limit-span'); + $('#sr-filter-select').on('change', function() { + var selector = filterInputs[$(this).val()]; + if (!selector) return; + $('.current-filters').append($(selector).show()); + $(this).find('option:selected').remove(); + }); + + $('.sr-remove-filter').on('click', function() { + $(this).parent('.sr-filter-input').hide(); + $(this).prev('input').val(''); + }); +}); - var filterArea = jQuery('.current-filters'); - var filterSelectBox = jQuery('#sr-filter-select'); - filterSelectBox.change(function() { +/* Enhance only the settings page. Every section remains available without JS, + * and hidden panels keep their controls in the single WordPress settings form. */ +jQuery(function($) { + var root = $('.sr-admin-wrap'); + var form = root.find('#sr-settings-form'); + if (!form.length) return; - var filterVal = filterSelectBox.val(); + var panels = form.find('[data-sr-panel]'); + var nav = root.find('.sr-settings-nav'); + var search = root.find('#sr-settings-search'); + var clear = root.find('.sr-clear-search'); + var empty = form.find('.sr-settings-empty'); + var searchStatus = root.find('.sr-search-status'); + var savebar = form.find('.sr-settings-savebar'); + var saveStatus = savebar.find('.sr-save-status'); + + // Keep custom-form visibility and submission behavior together with the + // other settings handlers, instead of an inline script in the PHP view. + form.find('#sr_enable_custom_lead_form_toggle').on('change', function() { + form.find('#sr_custom_lead_form_row').toggle(this.checked); + form.find('#sr_leadcapture_custom_form').prop('disabled', !this.checked); + }); + var active = 'account'; + var storageKey = 'simplyrets-settings-section'; + var submitting = false; + var firstInvalid = null; + var initialValues = form.serialize(); + var searchIndex = []; + + // Index help text and option names once, never stored field values. + function searchText(element) { + var text = element.clone(); + text.find('input, textarea, select, button').remove(); + var names = element.find('[name]').map(function() { return this.name; }).get().join(' '); + return (text.text() + ' ' + names).toLowerCase(); + } + panels.each(function() { + var panel = $(this); + var rows = panel.find('tr').map(function() { + var row = $(this); + return { row: row, text: searchText(row) }; + }).get(); + searchIndex.push({ panel: panel, text: searchText(panel), rows: rows }); + }); - // when a new filter is selected, show the input box and remove the - // option from the dropdown menu - switch(filterVal) { - case "Minimum Price": - filterArea.append(minPriceFilterInput); - minPriceFilterInput.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Maximum Price": - filterArea.append(maxPriceFilterInput); - maxPriceFilterInput.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Minimum Beds": - filterArea.append(minBedFilterInput); - minBedFilterInput.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Maximum Beds": - filterArea.append(maxBedFilterInput); - maxBedFilterInput.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Minimum Bathrooms": - filterArea.append(minBathFilterInput); - minBathFilterInput.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Maximum Bathrooms": - filterArea.append(maxBathFilterInput); - maxBathFilterInput.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Listing Agent": - filterArea.append(agentFilterInput); - agentFilterInput.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Listing Type": - filterArea.append(listingTypeSelect); - listingTypeSelect.show(); - jQuery(this).find("option:selected").remove(); - break; - case "Amount of listings": - filterArea.append(limitInput); - limitInput.show(); - jQuery(this).find("option:selected").remove(); - break; - default: - break; + function showSection(section, remember) { + if (section !== 'all' && !panels.filter(function() { return $(this).attr('data-sr-panel') === section; }).length) section = 'account'; + active = section; + search.val(''); + clear.prop('hidden', true); + panels.each(function() { this.hidden = section !== 'all' && $(this).attr('data-sr-panel') !== section; }); + empty.prop('hidden', true); + searchStatus.text(''); + nav.find('a').removeAttr('aria-current').filter(function() { return $(this).attr('data-sr-section') === section; }).attr('aria-current', 'true'); + form.find('.sr-search-match').removeClass('sr-search-match'); + if (remember) { + try { window.sessionStorage.setItem(storageKey, section); } catch (error) { /* Storage may be unavailable. */ } } + } + try { active = window.sessionStorage.getItem(storageKey) || active; } catch (error) { /* Use the account section. */ } + root.addClass('sr-settings-enhanced'); + root.find('.sr-settings-toolbar, .sr-settings-nav, .sr-settings-savebar-info, .sr-reveal-credentials').prop('hidden', false); + showSection(active, false); + + nav.on('click', 'a', function(event) { + event.preventDefault(); + showSection($(this).attr('data-sr-section'), true); + var visiblePanel = panels.filter(function() { return !this.hidden; }).first()[0]; + var position = visiblePanel.getBoundingClientRect(); + if (window.innerWidth <= 782 || position.top < 52 || position.top > window.innerHeight - 100) visiblePanel.scrollIntoView(); + }); + + search.on('input', function() { + var query = search.val().trim().toLowerCase(); + if (!query) { showSection(active, false); return; } + var matches = 0; + nav.find('a').removeAttr('aria-current'); + clear.prop('hidden', false); + form.find('.sr-search-match').removeClass('sr-search-match'); + $.each(searchIndex, function(index, entry) { + var match = entry.text.indexOf(query) !== -1; + entry.panel.prop('hidden', !match); + if (match) { + matches++; + $.each(entry.rows, function(index, entryRow) { + entryRow.row.toggleClass('sr-search-match', entryRow.text.indexOf(query) !== -1); + }); + } + }); + empty.prop('hidden', matches !== 0); + searchStatus.text(matches + (matches === 1 ? ' section matches' : ' sections match') + ' “' + search.val() + '”.'); + }); + clear.on('click', function() { showSection(active, false); search.trigger('focus'); }); + search.on('keydown', function(event) { if (event.key === 'Escape') { showSection(active, false); } }); + + root.find('.sr-reveal-credentials').on('click', function() { + var reveal = $(this).attr('aria-pressed') !== 'true'; + form.find('[name="sr_api_name"], [name="sr_api_key"]').attr('type', reveal ? 'text' : 'password'); + $(this).attr('aria-pressed', reveal ? 'true' : 'false').text(reveal ? 'Hide credentials' : 'Show credentials'); }); - jQuery('.sr-remove-filter').click(function() { - jQuery(this).parent('.sr-filter-input').hide(); - jQuery(this).prev('input').val(""); + // Give legacy text fields accessible names without replacing existing IDs. + form.find('input:not([type="hidden"]):not([type="submit"]), select, textarea').each(function() { + var field = $(this); + if (field.attr('aria-label') || field.closest('label').length) return; + var label = field.closest('tr').find('strong').first().text() || field.closest('tr').find('p').first().text(); + if (!label) label = field.closest('[data-sr-panel]').find('h3').first().text(); + if (this.name === 'sr_custom_no_results_message') label = 'No search results message'; + if (label) field.attr('aria-label', label.trim()); }); + form.on('input change', ':input', function() { + var dirty = form.serialize() !== initialValues; + saveStatus.text(dirty ? 'You have unsaved changes' : 'No unsaved changes'); + savebar.toggleClass('sr-is-dirty', dirty); + }); + // Browser validation must be able to focus an invalid field on a hidden panel. + form[0].addEventListener('invalid', function(event) { + if (firstInvalid) return; + firstInvalid = event.target; + var panel = $(event.target).closest('[data-sr-panel]'); + if (panel.length) showSection(panel.attr('data-sr-panel'), true); + window.setTimeout(function() { firstInvalid = null; }, 0); + }, true); + form.on('submit', function() { submitting = true; }); + $(window).on('beforeunload', function(event) { + if (!submitting && form.serialize() !== initialValues) { + event.preventDefault(); + event.originalEvent.returnValue = ''; + return ''; + } + }); }); diff --git a/src/simply-rets-admin.php b/src/simply-rets-admin.php index 8f8b517..4a3514e 100644 --- a/src/simply-rets-admin.php +++ b/src/simply-rets-admin.php @@ -162,9 +162,19 @@ public static function createDemoPage() { } public static function sr_admin_page() { - global $wpdb; $logo_path = plugin_dir_url(__FILE__) . 'assets/img/logo_button.png'; + $sections = array( + 'account' => array('Account', 'API credentials', 'admin-network'), + 'listings' => array('Listing pages', 'Contact forms & display', 'admin-home'), + 'maps' => array('Maps', 'Search layout & API key', 'location-alt'), + 'compliance' => array('MLS compliance', 'Attribution & IDX rules', 'shield'), + 'open-houses' => array('Open houses', 'Dates & timezone', 'calendar-alt'), + 'permalinks' => array('Permalinks', 'Listing URLs', 'admin-links'), + 'analytics' => array('Analytics', 'ListHub tracking', 'chart-bar'), + 'messages' => array('Custom messages', 'Disclaimers & empty results', 'editor-alignleft') + ); + // If meta data refresh action was posted, verify nonce and perform action $update_meta_nonce_field = 'sr_update_meta_data_nonce_field'; $update_meta_nonce_action = 'sr_update_meta_data_nonce_action'; @@ -183,8 +193,11 @@ public static function sr_admin_page() {

- +
+

SimplyRETS Admin Settings

+

Make your listings feel at home. Manage your connection, display preferences, and MLS requirements.

+
+ -
-
+
+ + -
+

Account Credentials

Enter your SimplyRETS API credentials in the fields below. @@ -229,7 +263,7 @@ public static function sr_admin_page() { API Key - + @@ -237,22 +271,21 @@ public static function sr_admin_page() { API Secret - + -

-
+ +

Note - to use the SimplyRETS demo data, you can use these API credentials: API Key: simplyrets API Secret: simplyrets -

- -
-
+

+
+

Single Listing Page Settings

Contact Form Lead Capture

@@ -270,7 +303,7 @@ public static function sr_admin_page() {
-

Send Lead Capture forms submissions to:

+

Send Lead Capture forms submissions to:

- -
-
+

Open house settings

Default timezone @@ -482,10 +513,8 @@ public static function sr_admin_page() { timestamps, select "None".

- -
-
+

Listing Compliance Settings

Show listing agent and office information

@@ -652,9 +681,7 @@ public static function sr_admin_page() {
- -
-