文件操作 - admin.js
返回文件管理
返回主菜单
删除本文件
文件: /storage/v12552/stanleysteamer/public_html/wp-content/themes/bricks/assets/js/admin.js
编辑文件内容
/** * Classic editor (toggle editor tabs: Visual, Text, Bricks) * * @since 1.0 */ function bricksAdminClassicEditor() { var bricksEditor = document.getElementById('bricks-editor') var wpEditor = document.getElementById('postdivrich') if (!bricksEditor || !wpEditor) { return } // Create "Bricks" button & add to classic editor tabs (next to "Visual", and "Text") var bricksButton = document.createElement('button') bricksButton.type = 'button' bricksButton.id = 'switch-bricks' bricksButton.classList.add('wp-switch-editor', 'switch-bricks') bricksButton.innerText = window.bricksData.title var editorTabs = wpEditor.querySelector('.wp-editor-tabs') if (editorTabs) { editorTabs.appendChild(bricksButton) } // Add Bricks editor tab content to DOM bricksEditor.after(wpEditor) document.addEventListener('click', function (e) { // Bricks tab if (e.target.id === 'switch-bricks') { // Don't trigger WordPress button events e.preventDefault() e.stopPropagation() // Hide WordPress content visual and text editors wpEditor.style.display = 'none' bricksEditor.style.display = 'block' // Toggle editor mode input field value document.getElementById('bricks-editor-mode').value = 'bricks' } // WordPress tab (Visual, Text) else if (['content-html', 'content-tmce'].indexOf(e.target.id) !== -1) { wpEditor.style.display = 'block' bricksEditor.style.display = 'none' // Toggle editor mode input field value document.getElementById('bricks-editor-mode').value = 'wordpress' } }) // Automatically toggle Bricks button if the page is rendered with Bricks and has no post content (@since 1.12) if (window.bricksData.renderWithBricks) { bricksButton.click() } } /** * Admin import (Bricks settings, Bricks templates, etc.) * * @since 1.0 */ function bricksAdminImport() { var importForm = document.getElementById('bricks-admin-import-form') if (!importForm) { return } var addNewButton = document.querySelector('#wpbody-content .page-title-action') if (!addNewButton) { return } var templateTagsButton = document.getElementById('bricks-admin-template-tags') if (templateTagsButton) { addNewButton.after(templateTagsButton) } var templateBundlesButton = document.getElementById('bricks-admin-template-bundles') if (templateBundlesButton) { addNewButton.after(templateBundlesButton) } var importButton = document.getElementById('bricks-admin-import-action') if (importButton) { addNewButton.after(importButton) } var importFormContent = document.getElementById('bricks-admin-import-form-wrapper') addNewButton.after(importFormContent) var toggleTemplateImporter = document.querySelectorAll('.bricks-admin-import-toggle') toggleTemplateImporter.forEach(function (toggle) { toggle.addEventListener('click', function () { importFormContent.style.display = importFormContent.style.display === 'block' ? 'none' : 'block' }) }) var progressDiv = document.querySelector('#bricks-admin-import-form-wrapper .import-progress') importForm.addEventListener('submit', function (event) { event.preventDefault() // Adds action, nonce and referrer from form hidden fields (@since 1.5.4) var formData = new FormData(importForm) var files = document.getElementById('bricks_import_files').files for (var i = 0; i < files.length; i++) { var file = files[i] formData.append('files[' + i + ']', file) } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: formData, processData: false, contentType: false, beforeSend: () => { importForm.setAttribute('disabled', 'disabled') if (progressDiv) { progressDiv.classList.add('is-active') } }, success: function (res) { importForm.removeAttribute('disabled') if (progressDiv) { progressDiv.classList.remove('is-active') } location.reload() } }) }) } /** * Save & revalidate license key * * @since 1.0 * @since 2.1.3 Re-validate license button added */ function bricksAdminSaveLicenseKey() { var licenseKeyForm = document.getElementById('bricks-license-key-form') if (!licenseKeyForm) { return } var action = licenseKeyForm.action.value var nonce = licenseKeyForm.nonce.value var submitButton = licenseKeyForm.querySelector('input[type=submit]') var revalidateButton = document.getElementById('bricks-revalidate-license') var errorMessage = licenseKeyForm.querySelector('.error-message') var successMessage = licenseKeyForm.querySelector('.success-message') function resetLicenseMessages() { if (errorMessage) { errorMessage.innerHTML = '' errorMessage.classList.remove('is-warning') } if (successMessage) { successMessage.innerHTML = '' } } function showLicenseMessage(messageType, message) { resetLicenseMessages() if (!message) { return } if (messageType === 'success' && successMessage) { successMessage.innerHTML = message } else if (errorMessage) { errorMessage.innerHTML = message if (messageType === 'warning') { errorMessage.classList.add('is-warning') } } } licenseKeyForm.addEventListener('submit', function (e) { e.preventDefault() submitButton.disabled = true resetLicenseMessages() var licenseKey = licenseKeyForm.license_key.value jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: action, licenseKey: licenseKey, nonce: nonce }, success: function (response) { if (action === 'bricks_deactivate_license') { location.reload() } else if (action === 'bricks_activate_license') { if (response.success) { if (response.data.hasOwnProperty('message')) { showLicenseMessage( response.data.status === 'error_remote' ? 'warning' : 'success', response.data.message ) } setTimeout(() => { location.reload() }, 1000) } else { submitButton.disabled = false if (response.data.hasOwnProperty('message')) { showLicenseMessage('error', response.data.message) } } } } }) }) // Re-validate license button handler (@since 2.1.3) if (revalidateButton) { revalidateButton.addEventListener('click', function (e) { e.preventDefault() // Clear previous messages resetLicenseMessages() // Disable button during request revalidateButton.disabled = true var originalText = revalidateButton.value revalidateButton.value = window.bricksData?.i18n?.validating jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_revalidate_license', nonce: nonce }, success: function (response) { revalidateButton.disabled = false revalidateButton.value = originalText if (response.success) { if (response.data.hasOwnProperty('message')) { showLicenseMessage( response.data.status === 'error_remote' ? 'warning' : 'success', response.data.message ) } // Update status display if status is returned if (response.data.hasOwnProperty('status')) { var statusSpan = licenseKeyForm.querySelector('.status-wrapper .status') if (statusSpan) { statusSpan.className = 'status ' + response.data.status statusSpan.textContent = response.data.status.replace(/_/g, ' ') } } // Reload page after 1.5 seconds to show updated status setTimeout(() => { location.reload() }, 1500) } else { if (response.data.hasOwnProperty('message')) { showLicenseMessage('error', response.data.message) } } }, error: function (xhr, status, error) { revalidateButton.disabled = false revalidateButton.value = originalText showLicenseMessage('error', window.bricksData?.i18n?.licenseRevalidateError) } }) }) } } /** * Toggle license key (input type: plain text/password) * * @since 1.3.5 */ function bricksAdminToggleLicenseKey() { var toggleLicenseKeyIcon = document.getElementById('bricks-toggle-license-key') if (!toggleLicenseKeyIcon) { return } toggleLicenseKeyIcon.addEventListener('click', function (e) { e.preventDefault() if (e.target.classList.contains('dashicons-hidden')) { e.target.classList.remove('dashicons-hidden') e.target.classList.add('dashicons-visibility') e.target.previousElementSibling.type = 'text' } else { e.target.classList.remove('dashicons-visibility') e.target.classList.add('dashicons-hidden') e.target.previousElementSibling.type = 'password' } }) } function bricksAdminSettings() { var settingsForm = document.querySelector('#bricks-settings') if (!settingsForm) { return } // Toggle tabs var settingsTabs = document.querySelectorAll('#bricks-settings-tabs-wrapper a') var settingsFormTables = settingsForm.querySelectorAll('table') function showTab(tabId) { var tabTable = document.getElementById(tabId) for (var i = 0; i < settingsFormTables.length; i++) { var table = settingsFormTables[i] if (table.getAttribute('id') === tabId) { table.classList.add('active') } else { table.classList.remove('active') } } } // Switch tabs listener for (var i = 0; i < settingsTabs.length; i++) { settingsTabs[i].addEventListener('click', function (e) { e.preventDefault() var tabId = e.target.getAttribute('data-tab-id') if (!tabId) { return } location.hash = tabId window.scrollTo({ top: 0 }) for (var i = 0; i < settingsTabs.length; i++) { settingsTabs[i].classList.remove('nav-tab-active') } e.target.classList.add('nav-tab-active') showTab(tabId) }) } // Check URL for active tab on page load var activeTabId = location.hash.replace('#', '') if (activeTabId) { var activeTab = document.querySelector('[data-tab-id="' + activeTabId + '"]') if (activeTab) { activeTab.click() } } // Save/reset settings var submitWrapper = settingsForm.querySelector('.submit-wrapper') var spinner = settingsForm.querySelector('.spinner.saving') if (!settingsForm) { return } settingsForm.addEventListener('submit', function (e) { e.preventDefault() }) // Save settings var saveSettingsButton = settingsForm.querySelector('input[name="save"]') if (saveSettingsButton) { saveSettingsButton.addEventListener('click', function () { if (submitWrapper) { submitWrapper.remove() } if (spinner) { spinner.classList.add('is-active') } window.jQuery.ajax({ type: 'POST', url: window.bricksData.ajaxUrl, data: { action: 'bricks_save_settings', formData: window.jQuery(settingsForm).serialize(), nonce: window.bricksData.nonce }, success: function () { // Show save message let hash = window.location.hash window.location.href = window.location.search += `&bricks_notice=settings_saved${hash}` } }) }) } // Reset settings var resetSettingsButton = settingsForm.querySelector('input[name="reset"]') if (resetSettingsButton) { resetSettingsButton.addEventListener('click', function () { var confirmed = confirm(window.bricksData.i18n.confirmResetSettings) if (!confirmed) { return } if (submitWrapper) { submitWrapper.remove() } if (spinner) { spinner.classList.add('is-active') } window.jQuery.ajax({ type: 'POST', url: window.bricksData.ajaxUrl, data: { action: 'bricks_reset_settings', nonce: window.bricksData.nonce }, success: function () { // Show reset message window.location.href = window.location.search += '&bricks_notice=settings_resetted' } }) }) } // Enable/disable code execution var enableCodeExecutionCheckbox = settingsForm.querySelector('input[name="executeCodeEnabled"]') if (enableCodeExecutionCheckbox) { enableCodeExecutionCheckbox.addEventListener('click', function (e) { var executeCodeCapabilities = settingsForm.querySelectorAll( 'input[name^="executeCodeCapabilities"' ) executeCodeCapabilities.forEach(function (checkboxInput) { checkboxInput.disabled = !e.target.checked }) }) } } /** * Generate CSS files * * By first getting list of all CSS files that need to be generated. * Then generated them one-by-one via individual AJAX calls to avoid any server timeouts. */ function bricksAdminGenerateCssFiles() { button = document.querySelector('#bricks-css-loading-generate button') if (!button) { return } button.addEventListener('click', function (e) { e.preventDefault() button.setAttribute('disabled', 'disabled') button.classList.add('wait') var resultsEl = document.querySelector('#bricks-css-loading-generate .results') if (resultsEl) { resultsEl.classList.remove('hide') var results = resultsEl.querySelector('ul') var counter = resultsEl.querySelector('.count') var done = resultsEl.querySelector('.done') results.innerHTML = '' counter.innerHTML = 0 if (done) { done.remove() } var theEnd = resultsEl.querySelector('.end') if (theEnd) { theEnd.remove() } } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_get_css_files_list', nonce: bricksData.nonce }, success: function (res) { // Start generating CSS files (index = 0) bricksAdminGenerateCssFile(0, results, counter, res.data) } }) }) } /** * Code review actions * * @since 1.9.7 */ function bricksAdminCodeReview() { let viewMode = 'individual' // Show all code review items or review individual item const reviewCount = document.querySelector('.bricks-code-review-description') const showAllButton = document.querySelector('.bricks-code-review-action.show-all') const individualButton = document.querySelector('.bricks-code-review-action.individual') const codeReviewItems = document.querySelectorAll('.bricks-code-review-item') const prevButton = document.querySelector('.bricks-code-review-action.prev') const nextButton = document.querySelector('.bricks-code-review-action.next') const checkedButtons = document.querySelectorAll('.bricks-code-review-item-check') if (!showAllButton || !codeReviewItems || !individualButton) { return } const recalculateTotalReviewed = (count = 'up') => { let totalReviewed = document.querySelector('.bricks-code-review-total-reviewed') // let totalMarked = document.querySelectorAll('.bricks-code-review-item.item-marked').length let totalMarked = totalReviewed.innerText totalMarked = totalMarked ? parseInt(totalMarked) : 1 // Next button if (count === 'up') { totalMarked++ } // Prev button else { totalMarked-- } if (totalReviewed) { totalReviewed.innerText = totalMarked } } // Show all code review items showAllButton.addEventListener('click', function (e) { e.preventDefault() viewMode = 'all' // Hide review count reviewCount.classList.add('action-hide') // Hide itself showAllButton.classList.add('action-hide') // Show individual button individualButton.classList.remove('action-hide') // Hide previous & next buttons prevButton.classList.add('action-hide') nextButton.classList.add('action-hide') // Show all code review items codeReviewItems.forEach(function (item) { item.classList.remove('item-hide') }) }) // Show individual code review item individualButton.addEventListener('click', function (e) { e.preventDefault() viewMode = 'individual' // Show review count reviewCount.classList.remove('action-hide') // Hide itself individualButton.classList.add('action-hide') // Show show all button showAllButton.classList.remove('action-hide') // Show previous & next buttons prevButton.classList.remove('action-hide') nextButton.classList.remove('action-hide') // Hide all code review items codeReviewItems.forEach(function (item) { item.classList.add('item-hide') }) // Show the item-current let currentItem = document.querySelector('.bricks-code-review-item.item-current') if (currentItem) { currentItem.classList.remove('item-hide') } }) // Show previous code review item prevButton.addEventListener('click', function (e) { e.preventDefault() let currentItem = document.querySelector('.bricks-code-review-item.item-current') let previousItem = currentItem.previousElementSibling if (previousItem) { currentItem.classList.remove('item-current') currentItem.classList.add('item-hide') previousItem.classList.add('item-current') previousItem.classList.remove('item-hide') recalculateTotalReviewed('down') } }) // Show next code review item nextButton.addEventListener('click', function (e) { e.preventDefault() let currentItem = document.querySelector('.bricks-code-review-item.item-current') let nextItem = currentItem.nextElementSibling if (nextItem) { currentItem.classList.remove('item-current') currentItem.classList.add('item-hide') nextItem.classList.add('item-current') nextItem.classList.remove('item-hide') recalculateTotalReviewed('up') } }) // Mark the code review item as checked if (checkedButtons.length) { checkedButtons.forEach(function (button) { button.addEventListener('click', function (e) { e.preventDefault() let item = button.closest('.bricks-code-review-item') if (item) { item.classList.add('item-marked') recalculateTotalReviewed() // Go to next item if (viewMode === 'individual') { setTimeout(() => { nextButton.click() }, 400) } } }) }) } } /** * Regenerate Bricks CSS files for modified default breakpoint width * * @since 1.5.1 */ function bricksAdminBreakpointsRegenerateCssFiles() { let button = document.getElementById('breakpoints-regenerate-css-files') if (!button) { return } let checkIcon = button.querySelector('i') button.addEventListener('click', function (e) { e.preventDefault() jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_regenerate_bricks_css_files', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') checkIcon.classList.add('hide') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') checkIcon.classList.remove('hide') } }) }) } function bricksAdminGenerateCssFile(index, results, counter, data) { /** * Helper function to handle completion of current file (success or error) * Continues with next file or finalizes the process * * #86c68dyd5 @since 2.2 */ function handleCompletion() { if (index === data.length) { // Finished processing all entries var button = document.querySelector('#bricks-css-loading-generate button') button.removeAttribute('disabled') button.classList.remove('wait') var infoText = document.querySelector('#bricks-css-loading-generate .info') if (infoText) { infoText.remove() } if (results) { results.insertAdjacentHTML('beforebegin', '<div class="done">... THE END :)</div>') } } else { // Continue with next entry bricksAdminGenerateCssFile(index + 1, results, counter, data) } } return jQuery .ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_regenerate_css_file', data: data[index], index: index }, success: function (res) { var fileName = res.data.hasOwnProperty('file_name') ? res.data.file_name : false if (fileName) { var html = '' var count = counter ? parseInt(counter.innerText) : 0 if (Array.isArray(fileName)) { fileName.forEach(function (fileName) { html += '<li>' + fileName + '</li>' count++ }) } else { html += '<li>' + fileName + '</li>' count++ } if (!res.success) { html = html.replace('<li>', '<li class="error">') } if (results) { results.insertAdjacentHTML('afterbegin', html) } if (counter) { counter.innerText = count } } }, error: function (xhr, status, error) { // Handle AJAX error var fileName = data[index].hasOwnProperty('file_name') ? data[index].file_name : 'post-' + data[index] + '.min.css' var html = '<li class="error">' + fileName + ' generation failed.</li>' var count = counter ? parseInt(counter.innerText) : 0 count++ if (results) { results.insertAdjacentHTML('afterbegin', html) } if (counter) { counter.innerText = count } } }) .then(handleCompletion, handleCompletion) } /** * Run Converter * * @since 1.4: Convert 'bricks-element-' ID & class name prefix to 'brxe-' * @since 1.5: Convert elements to nestable elements */ function bricksAdminRunConverter() { var converterButtons = document.querySelectorAll('.bricks-run-converter') if (!converterButtons.length) { return } converterButtons.forEach(function (button) { button.addEventListener('click', function (e) { e.preventDefault() let data = { action: 'bricks_get_converter_items', nonce: bricksData.nonce, convert: [] } // Convert global elements to components (@since 2.0) if (button.id === 'bricks-run-converter-global-elements') { data.convert.push('globalElementsToComponents') } if (document.getElementById('convert_element_ids_classes').checked) { data.convert.push('elementClasses') } if (document.getElementById('convert_container').checked) { data.convert.push('container') } // @since 1.5.1 to add position: relative as needed if (document.getElementById('add_position_relative').checked) { data.convert.push('addPositionRelative') } // @since 1.6 to convert entry animation ('_animation') to interactions if (document.getElementById('entry_animation_to_interaction').checked) { data.convert.push('entryAnimationToInteraction') } if (!data.convert.length) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: (res) => { console.info('bricks_get_converter_items', res.data) // Start running converter (index = 0) let index = 0 let data = res.data.items let convert = res.data.convert bricksAdminConvert(index, data, convert) } }) }) }) } function bricksAdminConvert(index, data, convert) { return jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_run_converter', nonce: bricksData.nonce, data: data[index], convert: convert }, success: function (res) { var button = document.querySelector('.bricks-run-converter[disabled]') var resultsEl = button.parentNode.querySelector('.results') // Add results HTML (div.results > ul) if (!resultsEl) { resultsEl = document.createElement('div') resultsEl.classList.add('results') var resultsList = document.createElement('ul') resultsEl.appendChild(resultsList) button.parentNode.appendChild(resultsEl) } // Re-run converter: Clear results else if (resultsEl && index === 0) { resultsEl.querySelector('ul').innerHTML = '' } var label = res.data.hasOwnProperty('label') ? res.data.label : false // Add converted item as list item (<li>) if (label) { var resultItem = document.createElement('li') resultItem.innerHTML = label resultsEl.querySelector('ul').prepend(resultItem) } console.warn('run_converter', index, label, res.data) // Finished processing all entries if (index === data.length) { button.removeAttribute('disabled') button.classList.remove('wait') var resultItem = document.createElement('li') resultItem.classList.add('done') resultItem.innerText = '... THE END :)' resultsEl.querySelector('ul').prepend(resultItem) } // Continue with next entry else { bricksAdminConvert(index + 1, data, convert) } } }) } /** * Copy template shortcode to clipboard */ function bricksTemplateShortcodeCopyToClipboard() { var copyToClipboardElements = document.querySelectorAll('.bricks-copy-to-clipboard') if (!copyToClipboardElements) { return } copyToClipboardElements.forEach(function (element) { element.addEventListener('click', function (e) { if (navigator.clipboard) { if (!window.isSecureContext) { alert('Clipboard API rejected: Not in secure context (HTTPS)') return } // Return: Don't copy if already copied (prevents double-click issue) if (element.classList.contains('copied')) { e.preventDefault() return } var content = element.value var message = element.getAttribute('data-success') navigator.clipboard.writeText(content) element.value = message element.classList.add('copied') setTimeout(function () { element.value = content element.classList.remove('copied') }, 2000) } }) }) } /** * Dismiss HTTPS notice * * Timeout required to ensure the node is added to the DOM. * * @since 1.8.4 */ function bricksDismissHttpsNotice() { setTimeout(() => { let dismissButton = document.querySelector('.brxe-https-notice .notice-dismiss') if (dismissButton) { dismissButton.addEventListener('click', function () { jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_dismiss_https_notice', nonce: bricksData.nonce } }) }) } }, 400) } /** * Delete form submissions table * * @since 1.9.2 */ function bricksDropFormSubmissionsTable() { let button = document.getElementById('bricks-drop-form-db') if (!button) { return } button.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmDropFormSubmissionsTable) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_form_submissions_drop_table', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') alert(res.data.message) location.reload() } }) }) } /** * Reset form submissions entries * * @since 1.9.2 */ function bricksResetFormSubmissionsTable() { let button = document.getElementById('bricks-reset-form-db') if (!button) { return } button.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmResetFormSubmissionsTable) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_form_submissions_reset_table', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') alert(res.data.message) location.reload() } }) }) } /** * Delete all submissions of spefic form (ID) * * @since 1.9.2 */ function bricksDeleteFormSubmissionsByFormId() { // Return: Not on "Form submissions" page if (!document.body.classList.contains('bricks_page_bricks-form-submissions')) { return } let deleteButtons = document.querySelectorAll('.column-actions [data-form-id]') for (var i = 0; i < deleteButtons.length; i++) { let button = deleteButtons[i] button.addEventListener('click', function (e) { e.preventDefault() let formId = button.getAttribute('data-form-id') var confirmed = confirm( bricksData.i18n.confirmResetFormSubmissionsFormId.replace('[form_id]', `"${formId}"`) ) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_form_submissions_delete_form_id', nonce: bricksData.nonce, formId: formId }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') alert(res.data.message) location.reload() } }) }) } } /** * Dismiss Instagram access token admin notice * * Timeout required to ensure the node is added to the DOM. * * @since 1.9.1 */ function bricksDismissInstagramAccessTokenNotice() { setTimeout(() => { let dismissButton = document.querySelector('.brxe-instagram-token-notice .notice-dismiss') if (dismissButton) { dismissButton.addEventListener('click', function () { jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_dismiss_instagram_access_token_notice', nonce: bricksData.nonce } }) }) } }, 400) } /** * Remote templates URLs: Add button logic * * @since 1.9.4 */ function bricksRemoteTemplateUrls() { let addMoreButton = document.getElementById('add-remote-template-button') if (!addMoreButton) { return } addMoreButton.addEventListener('click', function (e) { e.preventDefault() // Get last remote template wrapper to clone it for new remote template let remoteTemplateWrappers = document.querySelectorAll('.remote-template-wrapper') let remoteTemplateWrapper = remoteTemplateWrappers[remoteTemplateWrappers.length - 1] if (!remoteTemplateWrapper) { return } let clone = remoteTemplateWrapper.cloneNode(true) let labels = clone.querySelectorAll('label') labels.forEach((label) => { // Replace 'remoteTemplates[index]' 'for' attribute with new index label.setAttribute( 'for', label.getAttribute('for').replace(/\[(\d+)\]/, function (match, index) { return '[' + (parseInt(index) + 1) + ']' }) ) }) let inputs = clone.querySelectorAll('input') inputs.forEach((input) => { // Clear URL input value input.value = '' // Replace 'remoteTemplates[index]' 'name' attribute with new index input.name = input.name.replace(/\[(\d+)\]/, function (match, index) { return '[' + (parseInt(index) + 1) + ']' }) // Replace 'remoteTemplates[index]' 'id' attribute with new index input.id = input.id.replace(/\[(\d+)\]/, function (match, index) { return '[' + (parseInt(index) + 1) + ']' }) }) remoteTemplateWrapper.after(clone) }) } /** * Delete my template screenshots * * @since 1.10 */ function bricksDeleteTemplateScreenshots() { let button = document.getElementById('delete-template-screenshots-button') if (!button) { return } button.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmDeleteTemplateScreenshots) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_delete_template_screenshots', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') alert(res.data.message) if (res.success) { location.reload() } } }) }) } /** * Reindex filters * * @since 1.9.6 */ function bricksReindexFilters() { let button = document.getElementById('bricks-reindex-filters') let progressText = document.querySelector('.indexer-progress') let indexButton = document.getElementById('bricks-run-index-job') if (!button || !progressText || !indexButton) { return } button.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmReindexFilters) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_reindex_query_filters', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') if (res.success) { progressText.innerText = res.data.message setTimeout(() => { // Trigger indexerObserver to start checking progress after a short delay indexButton.setAttribute('data-no-confirm', 'true') indexButton.click() }, 500) } else { console.error('bricks_reindex_query_filters:error', res.data) } } }) }) } /** * Run query filter index job * * @since 1.10 */ function bricksRunIndexJob() { let button = document.getElementById('bricks-run-index-job') let progressText = document.querySelector('.indexer-progress') let checkIcon = button ? button.querySelector('i') : false let removeJobsDiv = document.getElementById('bricks-remove-jobs-wrapper') let removeJobsButton = document.getElementById('bricks-remove-index-jobs') let queryFiltersTd = document.getElementById('bricks-query-filter-td') let halted = false // Flag to stop observer if (!button || !progressText || !checkIcon || !removeJobsButton || !removeJobsDiv) { return } // Check progress every 3 seconds: Trigger background indexer manually instead of waiting for WP Cron const IndexerObserver = () => { jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_run_index_job', nonce: bricksData.nonce }, success: function (res) { let progress = res?.data?.progress || false let pending = res?.data?.pending || false if (progressText && progress) { progressText.innerHTML = progress } if (pending == 0 || halted) { button.removeAttribute('disabled') button.classList.remove('wait') checkIcon.classList.remove('hide') if (halted && pending > 0) { removeJobsDiv.classList.remove('hide') } } else { // Wait for 3 seconds and check again setTimeout(() => { IndexerObserver() }, 3000) } }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') removeJobsDiv.classList.add('hide') } }) } let isRunning = button.classList.contains('wait') if (isRunning) { // Initial load page detected indexer is running, then start observer IndexerObserver() } // Continue index job button button.addEventListener('click', function (e) { e.preventDefault() // Check if no-confirm attribute is set (from reindex filters) let noConfirm = button.getAttribute('data-no-confirm') if (!noConfirm) { var confirmed = confirm(bricksData.i18n.confirmTriggerIndexJob) if (!confirmed) { return } } IndexerObserver() // Always reset no-confirm attribute button.removeAttribute('data-no-confirm') checkIcon.classList.add('hide') // Always hide remove jobs div removeJobsDiv.classList.add('hide') }) // Remove all index jobs button (only visible if indexer is not running) removeJobsButton.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmRemoveAllIndexJobs) if (!confirmed) { return } halted = true // Stop observer for indexer jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_remove_all_index_jobs', nonce: bricksData.nonce }, success: function (res) { if (res.success) { // Wait for 10 seconds and reload the page, give some time for the running indexer to stop setTimeout(() => { alert(res.data.message) location.reload() }, 10000) } }, beforeSend: () => { removeJobsButton.setAttribute('disabled', 'disabled') removeJobsButton.classList.add('wait') queryFiltersTd.classList.add('blocking') let infoDiv = document.createElement('div') infoDiv.classList.add('blocking-info-wrapper') infoDiv.innerHTML = `<p class="message info">${bricksData.i18n.removingIndexJobsInfo}</p>` queryFiltersTd.appendChild(infoDiv) } }) }) } /** * Fix filter element database * * @since 1.12.2 */ function bricksFixElementDB() { let button = document.getElementById('bricks-fix-filter-element-db') if (!button) { return } button.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmFixElementDB) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_fix_filter_element_db', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') alert(res.data.message) location.reload() } }) }) } /** * Regenerate code element & codeEditor signatures * * @since 1.9.7 */ function bricksRegenerateCodeSignatures() { let button = document.getElementById('bricks-regenerate-code-signatures') if (!button) { return } button.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmRegenerateCodeSignatures) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_regenerate_code_signatures', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') alert(res.data.message) if (res.success) { location.reload() } else { console.error('bricks_regenerate_code_element_signatures:error', res.data) } } }) }) } /** * Code review filter * * @since 1.9.7 */ function bricksAdminCodeReviewFilter() { let filterSelect = document.getElementById('code-review-filter') if (!filterSelect) { return } filterSelect.addEventListener('change', (e) => { let url = new URL(window.location.href) url.searchParams.set('code-review', e.target.value) window.location.href = url.toString() }) } /** * Global elements review * * @since 2.0 */ function bricksAdminGlobalElementsReview() { const convertButtons = document.querySelectorAll('.bricks-convert-global-elements') convertButtons.forEach((button) => { button.addEventListener('click', (e) => { e.preventDefault() let unlinkNestables = false let reviewItemNode = e.target.closest('.bricks-code-review-item') if (reviewItemNode && reviewItemNode.querySelector('.nestable')) { unlinkNestables = confirm(window.bricksData.i18n.globalElementsConvertConfirm) } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_convert_global_elements', nonce: bricksData.nonce, postId: button.getAttribute('data-post-id'), unlinkNestables: unlinkNestables }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: (res) => { button.removeAttribute('disabled') button.classList.remove('wait') if (res.data.message) { alert(res.data.message) } // Reload tab (easier than updating the DOM) location.reload() } }) }) }) } /** * Maintenance mode: Toggle visibility of render header/footer checkboxes * * @since 1.9.9 */ function bricksAdminMaintenanceTemplateListener() { let maintenanceTemplateSelect = document.getElementById('maintenance-template') let maintenanceTemplateSelectSection = document.getElementById('maintenance-template-section') let renderSection = document.getElementById('maintenance-render-section') if (!renderSection) { return } function toggleRenderOptions() { let selectedValue = maintenanceTemplateSelect.value if (selectedValue === '') { renderSection.style.display = 'none' maintenanceTemplateSelectSection.style.borderBottom = 'none' } else { renderSection.style.display = 'block' maintenanceTemplateSelectSection.style.borderBottom = '1px solid var(--admin-color-border)' } } // Initial check toggleRenderOptions() // Add event listener maintenanceTemplateSelect.addEventListener('change', toggleRenderOptions) } /** * Adds a 'scroll' class to thumbnail anchors if the image inside is taller than the anchor. * This function ensures that the scroll animation is only applied to images that are * visually taller than their container. * * NOTE: We didn't use @keyframes as we need a dynamic duration and to only scroll when the image is taller than the thumbnail. * * @since 1.10 */ function bricksTemplateThumbnailAddScrollAnimation() { const thumbnails = document.querySelectorAll('.template_thumbnail a') thumbnails.forEach((thumbnail) => { const img = thumbnail.querySelector('img') /** * Checks the computed height of the image and the thumbnail anchor. * Adds the 'scroll' class to the thumbnail anchor if the image height * is greater than the thumbnail height, and sets up the scroll event listeners. */ function checkImageHeight() { const imgHeight = img.getBoundingClientRect().height const thumbnailHeight = thumbnail.getBoundingClientRect().height if (imgHeight > thumbnailHeight) { const scrollAmount = thumbnailHeight - imgHeight const duration = calculateScrollDuration(scrollAmount) thumbnail.classList.add('scroll') thumbnail.addEventListener('mouseenter', () => { startScrollAnimation(img, 0, scrollAmount, duration) }) thumbnail.addEventListener('mouseleave', () => { const currentTop = parseFloat(img.style.top) || scrollAmount startScrollAnimation(img, currentTop, 0, duration) }) } } /** * Calculates the scroll duration based on the scroll amount. * * @param {number} scrollAmount - The amount to scroll. * @returns {number} - The calculated duration in milliseconds. */ function calculateScrollDuration(scrollAmount) { const baseDuration = 2000 // Base duration in milliseconds for a significant scroll const maxScrollAmount = 200 // Define a max scroll amount for reference return (Math.abs(scrollAmount) * baseDuration) / maxScrollAmount } /** * Animates the image's top property to create a smooth scrolling effect. * * @param {HTMLElement} img - The image element to scroll. * @param {number} startTop - The initial top position. * @param {number} endTop - The final top position. * @param {number} duration - The duration of the scroll animation in milliseconds. */ function startScrollAnimation(img, startTop, endTop, duration) { let animationFrame let start function scroll(timestamp) { if (!start) start = timestamp // Initialize start time const elapsed = timestamp - start // Calculate elapsed time const progress = Math.min(elapsed / duration, 1) // Calculate progress // Update the image's top position based on progress img.style.top = startTop + (endTop - startTop) * progress + 'px' // Continue the animation if it's not finished if (progress < 1) { animationFrame = requestAnimationFrame(scroll) } } cancelAnimationFrame(animationFrame) // Cancel any ongoing animation animationFrame = requestAnimationFrame(scroll) // Start a new animation } // Check the image height after it has loaded img.addEventListener('load', checkImageHeight) // For cached images that might load instantly, check the height immediately if (img.complete) { checkImageHeight() } }) } /** * Toggle visibility of WordPress auth URL redirect page dropdown * * @since 1.11 */ function bricksAdminAuthUrlBehaviorListener() { let behaviorSelect = document.getElementById('wp_auth_url_behavior') let redirectPageWrapper = document.getElementById('wp_auth_url_redirect_page_wrapper') if (!behaviorSelect || !redirectPageWrapper) { return } function toggleRedirectPageOption() { if (behaviorSelect.value === 'custom') { redirectPageWrapper.style.display = 'block' } else { redirectPageWrapper.style.display = 'none' } } // Initial check toggleRedirectPageOption() // Add event listener behaviorSelect.addEventListener('change', toggleRedirectPageOption) } /* WooCommerce settings * * @since 1.11 */ function bricksAdminWooSettings() { let ajaxErrorSelect = document.getElementById('woocommerceAjaxErrorAction') let ajaxErrorActionDiv = document.getElementById('wooAjaxErrorScrollToNotice') if (ajaxErrorSelect && ajaxErrorActionDiv) { // Hide/show the scroll to notice div based on the selected option ajaxErrorSelect.addEventListener('change', function (e) { if (e.target.value === 'notice') { ajaxErrorActionDiv.classList.remove('hide') } else { ajaxErrorActionDiv.classList.add('hide') } }) } } /** * Create a multiselect dropdown for any select element with AJAX support * * @since 2.0 * * @param {string} selectId - The ID of the select element to transform * @param {Object} options - Configuration options * @param {string} options.placeholder - Placeholder text when no items selected * @param {string} options.searchPlaceholder - Placeholder text for search input * @param {string} options.dataAttribute - Data attribute name for selected items * @param {boolean} options.ajaxSearch - Whether to use AJAX for searching * @param {Object} options.ajaxOptions - AJAX specific options * @param {string} options.ajaxOptions.action - AJAX action to call * @param {Object} options.ajaxOptions.params - Additional parameters to send with the AJAX request * @param {number} options.ajaxOptions.minSearchLength - Minimum search length to trigger AJAX search * @param {number} options.ajaxOptions.debounceTime - Debounce time in milliseconds * @returns {void} */ function bricksCreateMultiselect(selectId, options = {}) { const select = document.getElementById(selectId) const wrapper = select?.parentElement if (!select || !wrapper) { return } // Default options const defaults = { placeholder: window.bricksData?.i18n?.selectItems, searchPlaceholder: window.bricksData?.i18n?.searchItems, dataAttribute: 'data-item-id', ajaxSearch: false, ajaxOptions: { action: 'bricks_get_posts', params: {}, minSearchLength: 2, debounceTime: 300 } } // Merge defaults with provided options const config = { ...defaults, ...options } // Merge ajax options if (options.ajaxOptions) { config.ajaxOptions = { ...defaults.ajaxOptions, ...options.ajaxOptions } } // Create custom control wrapper const control = document.createElement('div') control.setAttribute('data-control', 'select') control.className = 'multiple bricks-multiselect' control.setAttribute('tabindex', '0') // Create input display area const input = document.createElement('div') input.className = 'input' // Create options wrapper const optionsWrapper = document.createElement('div') optionsWrapper.className = 'options-wrapper' // Create search input const searchWrapper = document.createElement('div') searchWrapper.className = 'searchable-wrapper' searchWrapper.innerHTML = ` <input class="searchable" type="text" spellcheck="false" placeholder="${config.searchPlaceholder}"> <span class="search-status"></span> ` // Create dropdown list const dropdown = document.createElement('ul') dropdown.className = 'dropdown' const appendDropdownLabel = (container, label) => { const span = document.createElement('span') span.textContent = label container.appendChild(span) } // Add options to dropdown (initial load) if (!config.ajaxSearch) { populateDropdownFromSelect() } else { // For AJAX search, only add the selected options initially populateDropdownFromSelectedOptions() // Add a message for initial state if (Array.from(select.selectedOptions).length === 0) { const initialMessage = document.createElement('li') initialMessage.className = 'message' appendDropdownLabel(initialMessage, window.bricksData?.i18n?.typeToSearch || '') dropdown.appendChild(initialMessage) } } // Build structure optionsWrapper.appendChild(searchWrapper) optionsWrapper.appendChild(dropdown) control.appendChild(input) control.appendChild(optionsWrapper) // Hide original select select.style.display = 'none' select.after(control) // Function to populate dropdown from select options function populateDropdownFromSelect() { dropdown.innerHTML = '' Array.from(select.options).forEach((option, index) => { const li = document.createElement('li') li.setAttribute('data-index', index) li.setAttribute('data-value', option.value) li.className = option.selected ? 'selected' : '' appendDropdownLabel(li, option.text) dropdown.appendChild(li) }) } // Function to populate dropdown from selected options only function populateDropdownFromSelectedOptions() { dropdown.innerHTML = '' Array.from(select.selectedOptions).forEach((option, index) => { const li = document.createElement('li') li.setAttribute('data-index', index) li.setAttribute('data-value', option.value) li.className = 'selected' appendDropdownLabel(li, option.text) dropdown.appendChild(li) }) } // Update selected items display const updateSelection = () => { input.innerHTML = '' input.className = 'input' const selected = Array.from(select.selectedOptions) if (selected.length) { input.classList.add('has-value') selected.forEach((option) => { const value = document.createElement('span') value.className = 'value' value.setAttribute(config.dataAttribute, option.value) value.appendChild(document.createTextNode(option.text)) const closeIcon = document.createElement('span') closeIcon.className = 'dashicons dashicons-no-alt' closeIcon.setAttribute('data-name', 'close-box') value.appendChild(closeIcon) input.appendChild(value) }) } else { const placeholder = document.createElement('span') placeholder.className = 'placeholder' placeholder.textContent = config.placeholder const arrowIcon = document.createElement('span') arrowIcon.className = 'dashicons dashicons-arrow-down' input.appendChild(placeholder) input.appendChild(arrowIcon) } } // Initial selection updateSelection() // Toggle dropdown control.addEventListener('click', (e) => { const isSearchInput = e.target.classList.contains('searchable') if (!isSearchInput) { control.classList.toggle('open') // Auto-focus search input when dropdown opens if (control.classList.contains('open')) { const searchInput = control.querySelector('.searchable') if (searchInput) { setTimeout(() => { searchInput.focus() }, 10) } } } }) // Handle option selection dropdown.addEventListener('click', (e) => { const li = e.target.closest('li') if (li && !li.classList.contains('message') && !li.classList.contains('loading')) { const value = li.getAttribute('data-value') const option = select.querySelector(`option[value="${value}"]`) if (option) { option.selected = !option.selected li.classList.toggle('selected') updateSelection() } } }) // Handle remove tag click input.addEventListener('click', (e) => { const closeBox = e.target.closest('[data-name="close-box"]') if (closeBox) { e.stopPropagation() const tag = closeBox.closest('.value') const itemId = tag.getAttribute(config.dataAttribute) const option = select.querySelector(`option[value="${itemId}"]`) if (option) { option.selected = false const li = dropdown.querySelector(`li[data-value="${itemId}"]`) if (li) { li.classList.remove('selected') } updateSelection() } } }) // Debounce function for search function debounce(func, wait) { let timeout return function (...args) { const context = this clearTimeout(timeout) timeout = setTimeout(() => func.apply(context, args), wait) } } // AJAX search function function performAjaxSearch(searchTerm) { const searchStatus = searchWrapper.querySelector('.search-status') searchStatus.textContent = window.bricksData?.i18n?.searching searchStatus.classList.add('active') // Show loading indicator in dropdown const loadingItem = document.createElement('li') loadingItem.className = 'loading' appendDropdownLabel(loadingItem, window.bricksData?.i18n?.searching || '') // Clear previous results but keep selected items const selectedItems = Array.from(dropdown.querySelectorAll('li.selected')) dropdown.innerHTML = '' selectedItems.forEach((item) => dropdown.appendChild(item)) dropdown.appendChild(loadingItem) // Prepare AJAX data const data = { action: config.ajaxOptions.action, search: searchTerm, ...config.ajaxOptions.params } // Add nonce if available if (window.bricksData?.nonce) { data.nonce = window.bricksData.nonce } // Perform AJAX request jQuery.ajax({ type: 'GET', url: window.bricksData?.ajaxUrl, data: data, success: function (response) { // Remove loading indicator const loadingItems = dropdown.querySelectorAll('li.loading') loadingItems.forEach((item) => item.remove()) // Update search status searchStatus.textContent = '' searchStatus.classList.remove('active') if (response.success && response.data) { // Get currently selected values const selectedValues = Array.from(select.selectedOptions).map((opt) => opt.value) // Add new options to select if they don't exist Object.entries(response.data).forEach(([id, title]) => { if (!select.querySelector(`option[value="${id}"]`)) { const newOption = document.createElement('option') newOption.value = id newOption.text = title newOption.selected = selectedValues.includes(id) select.appendChild(newOption) } }) // Clear dropdown except selected items const selectedItems = Array.from(dropdown.querySelectorAll('li.selected')) dropdown.innerHTML = '' selectedItems.forEach((item) => dropdown.appendChild(item)) // Add results to dropdown Object.entries(response.data).forEach(([id, title]) => { // Skip if already in dropdown (selected) if (dropdown.querySelector(`li[data-value="${id}"]`)) { return } const li = document.createElement('li') li.setAttribute('data-value', id) li.className = selectedValues.includes(id) ? 'selected' : '' appendDropdownLabel(li, title) dropdown.appendChild(li) }) // Show no results message if empty if (dropdown.children.length === 0) { const noResults = document.createElement('li') noResults.className = 'message' appendDropdownLabel(noResults, window.bricksData?.i18n?.noResults || '') dropdown.appendChild(noResults) } } else { // Show error message const errorItem = document.createElement('li') errorItem.className = 'message error' appendDropdownLabel(errorItem, window.bricksData?.i18n?.searchError || '') dropdown.appendChild(errorItem) } }, error: function () { // Remove loading indicator const loadingItems = dropdown.querySelectorAll('li.loading') loadingItems.forEach((item) => item.remove()) // Update search status searchStatus.textContent = '' searchStatus.classList.remove('active') // Show error message const errorItem = document.createElement('li') errorItem.className = 'message error' appendDropdownLabel(errorItem, window.bricksData?.i18n?.searchError || '') dropdown.appendChild(errorItem) } }) } // Debounced search function const debouncedSearch = debounce(function (searchTerm) { performAjaxSearch(searchTerm) }, config.ajaxOptions.debounceTime) // Handle search const searchInput = searchWrapper.querySelector('.searchable') searchInput.addEventListener('input', (e) => { const search = e.target.value.toLowerCase() if (config.ajaxSearch) { // Clear message when user starts typing const messageItems = dropdown.querySelectorAll('li.message') messageItems.forEach((item) => item.remove()) // If search is empty, show only selected items if (search === '') { populateDropdownFromSelectedOptions() // Add a message for empty search if (dropdown.children.length === 0) { const initialMessage = document.createElement('li') initialMessage.className = 'message' initialMessage.innerHTML = `<span>${window.bricksData?.i18n?.typeToSearch}</span>` dropdown.appendChild(initialMessage) } return } // Check if search meets minimum length requirement if (search.length >= config.ajaxOptions.minSearchLength) { debouncedSearch(search) } else if (search.length > 0) { // Show message about minimum length const minLengthMessage = document.createElement('li') minLengthMessage.className = 'message' minLengthMessage.innerHTML = `<span>${window.bricksData?.i18n?.minSearchLength}</span>` // Clear dropdown except selected items const selectedItems = Array.from(dropdown.querySelectorAll('li.selected')) dropdown.innerHTML = '' selectedItems.forEach((item) => dropdown.appendChild(item)) dropdown.appendChild(minLengthMessage) } } else { // Regular filtering for non-AJAX search Array.from(dropdown.children).forEach((li) => { const text = li.textContent.toLowerCase() li.style.display = text.includes(search) ? '' : 'none' }) } }) // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!control.contains(e.target)) { control.classList.remove('open') } }) } /** * Element manager: Bricks > Elements * * @since 2.0 */ function bricksElementManager() { let elementManagerForm = document.getElementById('bricks-element-manager') if (!elementManagerForm) { return } elementFilters = document.querySelectorAll('button[data-filter-by]') // STEP: Filter elements elementFilters.forEach((filter) => { filter.addEventListener('click', function (e) { e.preventDefault() let filterActive = e.target.classList.contains('active') let filterBy = filter.dataset.filterBy let iconNode = e.target.querySelector('i') // Toggle icon if (iconNode) { if (filterActive) { iconNode.classList.remove('dashicons-remove') iconNode.classList.add('dashicons-insert') } else { iconNode.classList.remove('dashicons-insert') iconNode.classList.add('dashicons-remove') } } // Toggle button if (filterActive) { filter.classList.remove('button-primary') filter.classList.add('button-scondary') } else { filter.classList.remove('button-secondary') filter.classList.add('button-primary') } // Apply/remove filter via #bricks-element-manager attributes if (filterActive) { if (filterBy === 'unused') { delete elementManagerForm.dataset.filterUnused } if (filterBy === 'native') { delete elementManagerForm.dataset.filterNative } if (filterBy === 'custom') { delete elementManagerForm.dataset.filterCustom } } else { if (filterBy === 'unused') { elementManagerForm.dataset.filterUnused = 'on' } if (filterBy === 'native') { elementManagerForm.dataset.filterNative = 'on' } if (filterBy === 'custom') { elementManagerForm.dataset.filterCustom = 'on' } } filter.classList.toggle('active') }) }) elementManagerForm.addEventListener('click', function (e) { // STEP: Update element status changes let elementStatus = e.target.dataset.status if (elementStatus) { // Add .sticky class to .submit-wrapper let submitWrapper = elementManagerForm.querySelector('.submit-wrapper') if (submitWrapper) { submitWrapper.classList.add('sticky') } // Remove 'current' class from buttons Array.from(e.target.parentNode.children).forEach((child) => { child.classList.remove('current') }) e.target.classList.add('current') let tableRow = e.target.closest('tr') if (tableRow) { tableRow.dataset.status = e.target.dataset.status } } // STEP: Update element permission changes let elementPermission = e.target.closest('.element-permission input') if (elementPermission) { // Toggle all other inputs in the same row if input value is 'all' if (elementPermission.value === 'all') { let row = elementPermission.closest('tr') let inputs = row.querySelectorAll('.element-permission input') inputs.forEach((input) => { if (input.value !== 'all') { input.checked = elementPermission.checked } }) } // Non-all input toggled else { // Uncheck 'all' input if any other input is unchecked if (!elementPermission.checked) { let row = elementPermission.closest('tr') let allInput = row.querySelector('.element-permission input[value="all"]') if (allInput) { allInput.checked = false } } // Check 'all' input if all other inputs are checked else { let row = elementPermission.closest('tr') let inputs = row.querySelectorAll('.element-permission input') let allChecked = true inputs.forEach((input) => { if (input.value !== 'all' && !input.checked) { allChecked = false } }) if (allChecked) { let allInput = row.querySelector('.element-permission input[value="all"]') if (allInput) { allInput.checked = true } } } } } }) // STEP: Submit element manager form elementManagerForm.addEventListener('submit', function (e) { e.preventDefault() let elements = {} // Get all element name & status from table rows let tableRows = elementManagerForm.querySelectorAll('tbody tr') tableRows.forEach((row) => { let elementName = row.dataset.name let elementStatus = row.dataset.status elements[elementName] = { status: elementStatus, permission: [] } // Get all checked input values inside .element-permission (NOTE: Not in use yet) let permissions = row.querySelectorAll('.element-permission input:checked') permissions.forEach((permission) => { elements[elementName].permission.push(permission.value) }) }) let resetElementManager = document.activeElement.getAttribute('name') === 'reset' if (resetElementManager) { let letsReset = confirm('Are you sure you want to reset the element manager?') if (!letsReset) { return } } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_save_element_manager', elements: elements, nonce: bricksData.nonce, reset: resetElementManager }, beforeSend: () => { elementManagerForm.classList.add('wait') }, success: function () { elementManagerForm.classList.remove('wait') if (resetElementManager) { alert('Element manager has been reset.') location.reload() } else { alert('Element manager has been saved.') // Remove .sticky class from .submit-wrapper let submitWrapper = elementManagerForm.querySelector('.submit-wrapper') if (submitWrapper) { submitWrapper.classList.remove('sticky') } } } }) }) } function bricksAdminElementManagerUsage() { // Only run on the element manager page if (!document.getElementById('bricks-element-manager')) { return } // Process elements in batches to avoid overwhelming the server const BATCH_SIZE = 25 let elementsToProcess = [] let processingElements = false // Get all elements that need their usage count fetched document.querySelectorAll('.element-usage').forEach((cell) => { const elementName = cell.dataset.elementName if (elementName) { elementsToProcess.push(elementName) } }) // Process elements in batches of 25 function processNextBatch() { if (processingElements || elementsToProcess.length === 0) { return } processingElements = true // Get the next batch of elements const batch = elementsToProcess.splice(0, BATCH_SIZE) getElementUsageCount(batch) // Process the next batch setTimeout(() => { processingElements = false processNextBatch() }, 200) } // Get the usage count for a specific element function getElementUsageCount(elementNames) { const formData = new FormData() formData.append('action', 'bricks_get_element_usage_count') formData.append('nonce', bricksData.nonce) formData.append('elementNames', elementNames) jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_get_element_usage_count', nonce: bricksData.nonce, elementNames: elementNames }, success: function (response) { const countByElementName = response.data?.results || {} elementNames.forEach((elementName) => { const cell = document.querySelector(`.element-usage[data-element-name="${elementName}"]`) if (cell) { cell.innerHTML = countByElementName[elementName]?.count || '-' // Element count as data-count attribute for unused elements filter cell .closest('tr') .setAttribute('data-count', countByElementName[elementName]?.count || 0) } }) } }) } // Start processing elements processNextBatch() } /** * Handle custom capabilities UI */ function bricksAdminCustomCapabilities() { const wrapper = document.querySelector('.bricks-custom-capabilities-wrapper') if (!wrapper) { return } // Initialize variables const addCapabilityButton = wrapper.querySelector('.new-capability') const capabilitiesList = wrapper.querySelector('.bricks-custom-capabilities-list') const customCapabilitiesInput = wrapper.querySelector('input[name="customCapabilities"]') let capabilities = [] // Parse capabilities from hidden input if (customCapabilitiesInput && customCapabilitiesInput.value) { try { capabilities = JSON.parse(customCapabilitiesInput.value) } catch (e) { console.error('Error parsing capabilities:', e) } } // Function to check if any access builder permissions are selected function hasAccessBuilderPermissions(modal) { const accessBuilderSection = modal.querySelector( '.permission-section[data-section="access_builder"]' ) if (!accessBuilderSection) return false const accessBuilderCheckboxes = accessBuilderSection.querySelectorAll( 'input[type="checkbox"][name="permissions[]"]' ) return Array.from(accessBuilderCheckboxes).some((checkbox) => checkbox.checked) } // Function to toggle all non-access-builder checkboxes function toggleNonAccessBuilderCheckboxes(modal, enable) { const allSections = modal.querySelectorAll('.permission-section') allSections.forEach((section) => { if (section.dataset.section === 'access_builder') return const checkboxes = section.querySelectorAll('input[type="checkbox"][name="permissions[]"]') const enableAllCheckbox = section.querySelector('.enable-all-checkbox') checkboxes.forEach((checkbox) => { checkbox.disabled = !enable if (!enable) { checkbox.checked = false } }) // Update the "Enable All" checkbox state after toggling if (enableAllCheckbox) { enableAllCheckbox.disabled = !enable enableAllCheckbox.checked = enable && Array.from(checkboxes).every((checkbox) => checkbox.checked) } }) } // Function to update the hidden input with current capabilities function updateCapabilitiesInput() { if (customCapabilitiesInput) { customCapabilitiesInput.value = JSON.stringify(capabilities) } } // Function to render capabilities in the list function renderCapabilities() { // Clear the list first capabilitiesList.innerHTML = '' // First render default capabilities if (window.bricksData.defaultCapabilities) { capabilitiesList.innerHTML += `<div class="sub">${window.bricksData.i18n.defaultCapabilities}</div>` Object.entries(window.bricksData.defaultCapabilities).forEach(([capId, capLabel]) => { const capabilityItem = document.createElement('div') capabilityItem.className = 'capability-item' capabilityItem.dataset.capability = capId capabilityItem.dataset.isDefault = 'true' capabilityItem.innerHTML = ` <div class="capability-header"> <div class="capability-name">${capLabel} <em>(${window.bricksData.i18n.default})</em></div> <div class="capability-actions"> <button type="button" class="button view-capability"> ${window.bricksData.i18n.view} </button> </div> </div> ` capabilitiesList.appendChild(capabilityItem) }) } if (capabilities.length) { let hr = document.createElement('hr') hr.className = 'capabilities-separator' capabilitiesList.appendChild(hr) capabilitiesList.innerHTML += `<div class="sub">${window.bricksData.i18n.customCapabilities}</div>` capabilitiesList.innerHTML += `<div class="description">${window.bricksData.i18n.customCapabilitiesDescription}</div>` } // Then render custom capabilities capabilities.forEach((capability) => { const capabilityItem = document.createElement('div') capabilityItem.className = 'capability-item' capabilityItem.dataset.capability = capability.id capabilityItem.innerHTML = ` <div class="capability-header"> <div class="capability-name">${capability.label}</div> <div class="capability-actions"> <button type="button" class="button edit-capability"> ${window.bricksData.i18n.edit} </button> <button type="button" class="button duplicate-capability"> ${window.bricksData.i18n.duplicate} </button> <button type="button" class="button delete-capability"> ${window.bricksData.i18n.delete} </button> </div> </div> ` capabilitiesList.appendChild(capabilityItem) }) } // Function to close the modal function closeModal() { const modal = document.querySelector('.bricks-capability-modal') const backdrop = document.querySelector('.bricks-capability-modal-backdrop') if (modal) modal.remove() if (backdrop) backdrop.remove() } // Function to save capability changes function saveCapabilityChanges(modal, capabilityId) { const nameInput = modal.querySelector('input[name="capability-name"]') const descriptionInput = modal.querySelector('textarea[name="capability-description"]') const checkboxes = modal.querySelectorAll('input[type="checkbox"][name="permissions[]"]') const nameError = modal.querySelector('.capability-name-error') // Validate capability name const label = nameInput.value.trim() const description = descriptionInput.value.trim() if (!label) { nameError.textContent = window.bricksData.i18n.capabilityNameRequired nameInput.classList.add('error') return false } // Check if name is a default capability if ( window.bricksData.defaultCapabilities && Object.keys(window.bricksData.defaultCapabilities).includes(capabilityId) && (!capabilityId || label !== capabilityId) ) { nameError.textContent = window.bricksData.i18n.capabilityNameReserved nameInput.classList.add('error') return false } // Check if name already exists const nameExists = capabilities.some( (cap) => cap.id === label && (!capabilityId || label !== capabilityId) ) if (nameExists) { nameError.textContent = window.bricksData.i18n.capabilityNameExists nameInput.classList.add('error') return false } // Get selected permissions const selectedPermissions = [] checkboxes.forEach((checkbox) => { if (checkbox.checked) { selectedPermissions.push(checkbox.value) } }) // Update or add capability if (capabilityId) { // Update existing capability const index = capabilities.findIndex((cap) => cap.id === capabilityId) if (index !== -1) { // Update builder access dropdowns if ID hasn't changed if (label !== capabilities[index].label) { // Update capability label in dropdowns const builderAccessSelects = document.querySelectorAll( 'select[name^="builderCapabilities"]' ) builderAccessSelects.forEach((select) => { const option = Array.from(select.options).find( (option) => option.value === capabilityId ) if (option) { option.textContent = label } }) } capabilities[index] = { id: capabilityId, label: label, description: description, permissions: selectedPermissions } } } else { // Generate new unique ID for new capability const newId = generateCapabilityId() // Add new capability capabilities.push({ id: newId, label: label, description: description, permissions: selectedPermissions }) // Add to builder access dropdowns updateBuilderAccessDropdowns({ id: newId, label: label }) } // Update hidden input updateCapabilitiesInput() // Render the updated capabilities list renderCapabilities() return true } // Function to show capability modal function showCapabilityModal(capabilityId, isViewOnly) { // Create modal HTML const modalHTML = renderModalTemplate(capabilityId, isViewOnly) document.body.insertAdjacentHTML('beforeend', modalHTML) // Get modal elements const modal = document.querySelector('.bricks-capability-modal') const closeButtons = modal.querySelectorAll('.close-modal') const nameInput = modal.querySelector('input[name="capability-name"]') const checkboxes = modal.querySelectorAll('input[type="checkbox"][name="permissions[]"]') const backdrop = document.querySelector('.bricks-capability-modal-backdrop') // Close modal on any close button click closeButtons.forEach((button) => { button.addEventListener('click', () => { if (!isViewOnly) { saveCapabilityChanges(modal, capabilityId) } closeModal() }) }) // Close on backdrop click if (backdrop) { backdrop.addEventListener('click', () => { if (!isViewOnly) { saveCapabilityChanges(modal, capabilityId) } closeModal() }) } // Close on ESC key document.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { if (!isViewOnly) { saveCapabilityChanges(modal, capabilityId) } closeModal() document.removeEventListener('keydown', escHandler) } }) // Disable inputs for default capabilities or view mode if (isViewOnly) { if (nameInput) nameInput.disabled = true checkboxes.forEach((checkbox) => { checkbox.disabled = true }) } // Initial check for access builder permissions if (!isViewOnly) { const hasAccess = hasAccessBuilderPermissions(modal) toggleNonAccessBuilderCheckboxes(modal, hasAccess) // Add event listeners to access builder checkboxes const accessBuilderSection = modal.querySelector( '.permission-section[data-section="access_builder"]' ) if (accessBuilderSection) { const accessBuilderCheckboxes = accessBuilderSection.querySelectorAll( 'input[type="checkbox"][name="permissions[]"]' ) accessBuilderCheckboxes.forEach((checkbox) => { checkbox.addEventListener('change', () => { const hasAccess = hasAccessBuilderPermissions(modal) toggleNonAccessBuilderCheckboxes(modal, hasAccess) saveCapabilityChanges(modal, capabilityId) }) }) } } // Set initial state of "Enable All" checkboxes if (!isViewOnly) { // For each section, check if all permissions are already checked modal.querySelectorAll('.permission-section').forEach((section) => { const sectionCheckboxes = section.querySelectorAll( 'input[type="checkbox"][name="permissions[]"]' ) const enableAllCheckbox = section.querySelector('.enable-all-checkbox') // Set the "Enable All" checkbox state based on whether all permissions are checked const allChecked = Array.from(sectionCheckboxes).every((checkbox) => checkbox.checked) enableAllCheckbox.checked = allChecked // Add event listener to "Enable All" checkbox enableAllCheckbox.addEventListener('change', function () { // Only allow enabling if this is the access_builder section or if access_builder permissions are selected if (section.dataset.section !== 'access_builder' && !hasAccessBuilderPermissions(modal)) { this.checked = false return } const isChecked = this.checked sectionCheckboxes.forEach((checkbox) => { if (!checkbox.disabled) { checkbox.checked = isChecked } }) // If this is the access_builder section, toggle other sections based on the checkbox state if (section.dataset.section === 'access_builder') { toggleNonAccessBuilderCheckboxes(modal, isChecked) } saveCapabilityChanges(modal, capabilityId) }) }) // Update "Enable All" checkbox when individual permissions change checkboxes.forEach((checkbox) => { checkbox.addEventListener('change', function () { const section = this.closest('.permission-section') const sectionCheckboxes = section.querySelectorAll( 'input[type="checkbox"][name="permissions[]"]' ) const enableAllCheckbox = section.querySelector('.enable-all-checkbox') // Update "Enable All" checkbox based on whether all permissions are checked const allChecked = Array.from(sectionCheckboxes).every( (checkbox) => checkbox.checked || checkbox.disabled ) enableAllCheckbox.checked = allChecked // If this is a checkbox in the access_builder section, check if we need to enable other sections if (section.dataset.section === 'access_builder') { const hasAccess = hasAccessBuilderPermissions(modal) toggleNonAccessBuilderCheckboxes(modal, hasAccess) } saveCapabilityChanges(modal, capabilityId) }) }) } // Handle name input validation and auto-save if (nameInput && !isViewOnly) { nameInput.addEventListener('input', function () { const nameError = modal.querySelector('.capability-name-error') nameInput.classList.remove('error') nameError.textContent = '' saveCapabilityChanges(modal, capabilityId) }) } } // Function to render modal template function renderModalTemplate(capabilityId, isViewOnly) { // Get existing capability data or use empty defaults let capability = { id: '', label: '', description: '', permissions: [] } if (capabilityId) { // Check if it's a default capability if ( window.bricksData.defaultCapabilityPermissions && window.bricksData.defaultCapabilityPermissions[capabilityId] ) { const defaultCapData = window.bricksData.defaultCapabilityPermissions[capabilityId] capability = { id: capabilityId, label: defaultCapData.label, permissions: defaultCapData.permissions || [] } } else { // Custom capability const existingCapability = capabilities.find((cap) => cap.id === capabilityId) if (existingCapability) { capability = existingCapability } } } const isDefaultCapability = window.bricksData.defaultCapabilityPermissions?.[capabilityId] return ` <div class="bricks-capability-modal"> <div class="bricks-capability-modal-header"> <h2>${ isViewOnly ? window.bricksData.i18n.view : capability.id ? window.bricksData.i18n.editCapability : window.bricksData.i18n.newCapability }</h2> <button type="button" class="close-modal"> <span class="dashicons dashicons-no-alt"></span> </button> </div> <div class="bricks-capability-modal-content"> <div class="capability-name-wrapper"> <label for="capability-name">${window.bricksData.i18n.capabilityName}</label> <span class="capability-id" title="${window.bricksData.i18n.capability}: ${ window.bricksData.i18n.capabilityKey }">${capability.id}</span> <input type="text" name="capability-name" id="capability-name" value="${capability.label}" ${ isViewOnly || isDefaultCapability ? 'disabled' : '' }> <div class="capability-name-error"></div> </div> ${ !isDefaultCapability ? ` <div class="capability-description-wrapper"> <label for="capability-description">${window.bricksData.i18n.description}</label> <textarea name="capability-description" id="capability-description" rows="3" ${ isViewOnly ? 'disabled' : '' }>${capability.description || ''}</textarea> </div> ` : '' } <div class="capability-permissions"> ${Object.entries(window.bricksData.builderAccessPermissions) .map( ([sectionKey, section]) => ` <div class="permission-section" data-section="${sectionKey}"> <div class="section-header"> <h3>${section.label}</h3> <label class="enable-all-wrapper"> <input type="checkbox" class="enable-all-checkbox" ${isViewOnly ? 'disabled' : ''}> <span>${window.bricksData.i18n.enableAll}</span> </label> <div class="description">${section.description || ''}</div> </div> <div class="permission-grid"> ${Object.entries(section.permissions) .map( ([permissionId, permissionLabel]) => ` <label class="permission-item"> <input type="checkbox" name="permissions[]" value="${permissionId}" ${ capability.permissions.includes(permissionId) ? 'checked' : '' } ${isViewOnly ? 'disabled' : ''}> <span>${permissionLabel}</span> </label> ` ) .join('')} </div> </div> ` ) .join('')} </div> </div> <div class="bricks-capability-modal-footer"> <div class="save-reminder">${window.bricksData.i18n.saveSettingsToApplyChanges}</div> <button type="button" class="button close-modal">${window.bricksData.i18n.close}</button> </div> </div> <div class="bricks-capability-modal-backdrop"></div> ` } // Function to update builder access dropdowns with new capability function updateBuilderAccessDropdowns(capability) { // Find all builder access dropdowns const builderAccessSelects = document.querySelectorAll('select[name^="builderCapabilities"]') // Skip if no dropdowns found if (!builderAccessSelects.length) { return } // Create new option element const newOption = document.createElement('option') newOption.value = capability.id newOption.textContent = capability.label // Add the new option to each dropdown (before the "Full access" option) builderAccessSelects.forEach((select) => { // Skip the administrator role dropdown (it's disabled) if (select.disabled) { return } // Remove existing option if updating const existingOption = select.querySelector(`option[value="${capability.id}"]`) if (existingOption) { existingOption.remove() } // Find the "Full access" option (it should be the last one) const fullAccessOption = Array.from(select.options).find( (option) => option.value === 'bricks_full_access' ) // If found, insert before it; otherwise append to the end if (fullAccessOption) { select.insertBefore(newOption.cloneNode(true), fullAccessOption) } else { select.appendChild(newOption.cloneNode(true)) } }) } // Function to generate a random 6-character string function generateRandomString(length = 6) { return Math.random() .toString(36) .substring(2, 2 + length) } // Function to generate a unique capability ID function generateCapabilityId() { return `bricks_builder_access_${generateRandomString()}` } // Add new capability button if (addCapabilityButton) { addCapabilityButton.addEventListener('click', function () { // Generate new capability with unique ID const newCapability = { id: generateCapabilityId(), label: window.bricksData.i18n.newCapability, permissions: [] } // Add to capabilities array capabilities.push(newCapability) // Update hidden input updateCapabilitiesInput() // Add to builder access dropdowns updateBuilderAccessDropdowns(newCapability) // Update the UI renderCapabilities() // Show edit modal for the new capability showCapabilityModal(newCapability.id) }) } // Function to update builder access dropdowns with capability function updateBuilderAccessDropdowns(capability) { // Find all builder access dropdowns const builderAccessSelects = document.querySelectorAll('select[name^="builderCapabilities"]') // Skip if no dropdowns found if (!builderAccessSelects.length) { return } // Create new option element const newOption = document.createElement('option') newOption.value = capability.id newOption.textContent = capability.label // Add the new option to each dropdown (before the "Full access" option) builderAccessSelects.forEach((select) => { // Skip the administrator role dropdown (it's disabled) if (select.disabled) { return } // Remove existing option if updating const existingOption = select.querySelector(`option[value="${capability.id}"]`) if (existingOption) { existingOption.remove() } // Find the "Full access" option (it should be the last one) const fullAccessOption = Array.from(select.options).find( (option) => option.value === 'bricks_full_access' ) // If found, insert before it; otherwise append to the end if (fullAccessOption) { select.insertBefore(newOption.cloneNode(true), fullAccessOption) } else { select.appendChild(newOption.cloneNode(true)) } }) } // Initial render of capabilities renderCapabilities() // Event delegation for capability items if (capabilitiesList) { capabilitiesList.addEventListener('click', function (e) { const capabilityItem = e.target.closest('.capability-item') if (!capabilityItem) { return } const capabilityId = capabilityItem.dataset.capability // View button (for default capabilities) if (e.target.classList.contains('view-capability')) { showCapabilityModal(capabilityId, true) } // Edit button (for custom capabilities) else if (e.target.classList.contains('edit-capability')) { showCapabilityModal(capabilityId) } // Duplicate button (for custom capabilities) else if ( e.target.classList.contains('duplicate-capability') || e.target.closest('.duplicate-capability') ) { // Find the capability to duplicate const capability = capabilities.find((cap) => cap.id === capabilityId) if (capability) { // Generate new unique ID for duplicated capability const newId = generateCapabilityId() // Create new capability with duplicated data const duplicatedCapability = { id: newId, label: `${capability.label} (${window.bricksData.i18n.duplicate})`, description: capability.description || '', permissions: [...capability.permissions] } // Add to capabilities array capabilities.push(duplicatedCapability) // Update hidden input updateCapabilitiesInput() // Add to builder access dropdowns updateBuilderAccessDropdowns(duplicatedCapability) // Update the UI renderCapabilities() } } // Delete button (for custom capabilities) else if ( e.target.classList.contains('delete-capability') || e.target.closest('.delete-capability') ) { const confirmed = confirm(window.bricksData.i18n.confirmDeleteCapability) if (confirmed) { // Remove from capabilities array capabilities = capabilities.filter((cap) => cap.id !== capabilityId) // Remove from builder access dropdowns const builderAccessSelects = document.querySelectorAll( 'select[name^="builderCapabilities"]' ) builderAccessSelects.forEach((select) => { const option = select.querySelector(`option[value="${capabilityId}"]`) if (option) { option.remove() } }) // Update hidden input updateCapabilitiesInput() // Update the UI renderCapabilities() } } }) } } /** * Template exclusion multiselect handler * * @since 1.12.2 */ function bricksAdminTemplateExclusion() { bricksCreateMultiselect('excludedTemplates', { placeholder: window.bricksData?.i18n?.selectTemplates, searchPlaceholder: window.bricksData?.i18n?.searchTemplates, dataAttribute: 'data-template-id' }) } /** * Maintenance mode excluded posts/pages multiselect handler * * @since 2.0 */ function bricksAdminMaintenanceExcludedPosts() { bricksCreateMultiselect('maintenanceExcludedPosts', { placeholder: window.bricksData?.i18n?.selectPosts, searchPlaceholder: window.bricksData?.i18n?.searchPosts, dataAttribute: 'data-post-id', ajaxSearch: true, ajaxOptions: { action: 'bricks_get_posts', params: { postType: 'any', postStatus: 'publish', excludePostTypes: ['bricks_template', 'attachment'] }, minSearchLength: 2, debounceTime: 300 } }) } /** * User activation * * @since 2.1 */ function bricksAdminUserActivation() { // STEP: Get needed elements const userActivationCheckbox = document.getElementById('userActivationEnabled') const activationElements = document.querySelectorAll('[data-on="userActivationEnabled"]') // STEP: Check if all required elements exist, otherwise return if (!userActivationCheckbox || !activationElements.length) { return } // General function to toggle visibility of elements const toggle = function (elements, toggle) { elements.forEach((element) => { if (toggle) { element.classList.remove('hide') } else { element.classList.add('hide') } }) } // STEP: Toggle visibility of activation elements toggle(activationElements, userActivationCheckbox.checked) // STEP: Add event listeners userActivationCheckbox.addEventListener('change', function (e) { toggle(activationElements, e.target.checked) }) } /** * Handle attribute & term image swatch uploads * * @since 2.0 */ function bricksAttributeImageSwatches() { // Handle image upload document.addEventListener('click', function (e) { if (!e.target.matches('.bricks_swatch_image_upload')) return e.preventDefault() const button = e.target const parent = button.parentNode // Handle both term fields and attribute settings fields const imageIdInput = parent.querySelector( 'input[name="swatch_image_value"], input[name="swatch_default_image"]' ) const imagePreview = parent.querySelector('.swatch-image-preview') let wp = window.wp // Create media frame if (!wp.media.frames.file_frame) { wp.media.frames.file_frame = wp.media({ multiple: false }) } // When image selected wp.media.frames.file_frame.off('select').on('select', function () { const attachment = wp.media.frames.file_frame.state().get('selection').first().toJSON() // Update hidden input with image ID imageIdInput.value = attachment.id // Update or create preview if (imagePreview) { imagePreview.src = attachment.url } else { const img = document.createElement('img') img.src = attachment.url img.className = 'swatch-image-preview' img.style.cssText = 'max-width: 150px; display: block; margin-bottom: 8px;' button.parentNode.insertBefore(img, button) } // Show remove button button.nextElementSibling.style.display = '' }) wp.media.frames.file_frame.open() }) // Handle image removal document.addEventListener('click', function (e) { if (!e.target.matches('.bricks_swatch_image_remove')) return e.preventDefault() const button = e.target const parent = button.parentNode // Handle both term fields and attribute settings fields const imageIdInput = parent.querySelector( 'input[name="swatch_image_value"], input[name="swatch_default_image"]' ) const imagePreview = parent.querySelector('.swatch-image-preview') // Clear input value imageIdInput.value = '' // Remove preview if (imagePreview) { imagePreview.remove() } // Hide remove button button.style.display = 'none' }) } /** * Handle color swatch removal * * @since 2.0 */ function bricksAttributeColorSwatches() { // Handle color swatch remove button document.addEventListener('click', function (e) { if (!e.target.matches('.bricks-remove-color')) { return } e.preventDefault() const inputId = e.target.dataset.input const input = document.getElementById(inputId) const wrapper = input.closest('.bricks-color-swatch-wrapper') // Replace color input with Select Color button wrapper.innerHTML = ` <div style="display: inline-block"> <button type="button" class="button show-color-picker" data-input-id="${inputId}"> ${window.bricksData.i18n.selectColor} </button> <input type="hidden" name="${input.name}" id="${inputId}" value="none"> </div> ` }) // Handle show color picker button document.addEventListener('click', function (e) { if (!e.target.matches('.show-color-picker')) { return } const inputId = e.target.dataset.inputId const wrapper = e.target.closest('div') const inputName = inputId === 'swatch_default_color' ? 'swatch_default_color' : 'swatch_color_value' // Replace button with color input and add wrapper with positioning context wrapper.outerHTML = ` <div class="bricks-color-input-wrapper" style="position: relative; display: inline-block;"> <input type="color" name="${inputName}" id="${inputId}" class="bricks-color-input"> <button type="button" class="button bricks-remove-color" data-input="${inputId}"> ${window.bricksData.i18n.remove} </button> </div> ` // After creating the color input, open the color picker at the click position const colorInput = document.getElementById(inputId) // If not using our custom picker, use the native one if (!openColorPicker(e, colorInput)) { // For non-Chrome browsers, just focus and click the input directly colorInput.focus() setTimeout(() => { colorInput.click() }, 10) } }) // Fix Chrome color picker positioning by using a custom approach document.addEventListener('click', function (e) { if (!e.target.matches('input[type="color"].bricks-color-input')) { return } // Use our custom color picker for Chrome openColorPicker(e, e.target) }) // Create and open a properly positioned color picker // NOTE: This is a workaround to fix the color picker positioning issue in Chrome which automatically opens the picker at the top left of the page const openColorPicker = (e, targetInput) => { // If we're in Chrome, use our custom color picker positioning if (navigator.userAgent.indexOf('Chrome') !== -1) { // Get the current color value const currentColor = targetInput.value || '#ffffff' // Create a custom positioned color input just for picking const tempInput = document.createElement('input') tempInput.type = 'color' tempInput.value = currentColor tempInput.style.position = 'absolute' tempInput.style.left = e.pageX - 5 + 'px' tempInput.style.top = e.pageY - 5 + 'px' tempInput.style.padding = '0' tempInput.style.margin = '0' tempInput.style.width = '1px' tempInput.style.height = '1px' tempInput.style.opacity = '0.01' tempInput.style.pointerEvents = 'none' tempInput.style.zIndex = '999999' // Listen for changes to our temporary input tempInput.addEventListener('input', function () { // Update the original input value targetInput.value = tempInput.value }) tempInput.addEventListener('change', function () { // Once done, update original input and remove temp targetInput.value = tempInput.value document.body.removeChild(tempInput) }) // Add to body, focus and click to open the picker document.body.appendChild(tempInput) tempInput.focus() tempInput.click() return true } return false } } /** * Handle attribute swatch type visibility * * @since 2.0 */ function bricksAttributeSwatchTypeVisibility() { const swatchType = document.getElementById('swatch_type') const fallbacks = document.querySelectorAll('.bricks-swatch-fallback') if (!swatchType) return function updateVisibility() { const type = swatchType.value // Show/hide type-specific fallback fallbacks.forEach((el) => { el.style.display = 'none' }) if (type) { const fallbackField = document.querySelector('.bricks-swatch-fallback-' + type) if (fallbackField) { fallbackField.style.display = '' } } } swatchType.addEventListener('change', updateVisibility) updateVisibility() } /** * Clean up orphaned elements across site * * @since 2.0 */ function bricksCleanupOrphanedElements() { let button = document.getElementById('cleanup-all-orphaned-elements') if (!button) { return } button.addEventListener('click', function (e) { e.preventDefault() var confirmed = confirm(bricksData.i18n.confirmCleanupOrphanedElements) if (!confirmed) { return } jQuery.ajax({ type: 'POST', url: bricksData.ajaxUrl, data: { action: 'bricks_cleanup_orphaned_elements', nonce: bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') if (res.success) { alert(res.data.message) // Refresh the results or hide them since they're cleaned up location.reload() } else { alert(res.data.message || bricksData.i18n.errorOccurred) } }, error: function () { button.removeAttribute('disabled') button.classList.remove('wait') alert(bricksData.i18n.errorOccurredCleaningUpOrphanedElements) } }) }) } /** * Scan for orphaned elements across site * * @since 2.0 */ function bricksScanOrphanedElements() { let button = document.getElementById('scan-orphaned-elements') let resultsContainer = document.getElementById('orphaned-elements-results') if (!button || !resultsContainer) { return } button.addEventListener('click', function (e) { e.preventDefault() jQuery.ajax({ type: 'POST', url: window.bricksData.ajaxUrl, data: { action: 'bricks_scan_orphaned_elements', nonce: window.bricksData.nonce }, beforeSend: () => { button.setAttribute('disabled', 'disabled') button.classList.add('wait') }, success: function (res) { button.removeAttribute('disabled') button.classList.remove('wait') if (res.success) { bricksDisplayOrphansScanResults(res.data, resultsContainer) } else { alert(res.data.message || window.bricksData.i18n.error) } }, error: function () { button.removeAttribute('disabled') button.classList.remove('wait') alert(window.bricksData.i18n.errorOccurredScanningOrphanedElements) } }) }) } /** * Display scan results for orphaned elements * * @since 2.0 */ function bricksDisplayOrphansScanResults(data, container) { let html = '' if (data.total_orphans === 0) { html = '<div class="separator"></div>' html += '<p class="message success"><strong>' + window.bricksData.i18n.noOrphanedElementsFound + '</strong></p>' } else { html = '<div class="separator"></div>' // Use sprintf-like replacement for the message with placeholders let errorMessage = window.bricksData.i18n.orphanedElementsFoundMessage .replace('%1$d', data.total_orphans) .replace('%2$d', data.total_posts) html += '<h3 class="hero">' + window.bricksData.i18n.results + ': ' + window.bricksData.i18n.orphanedElementsReview + '</h3>' html += '<p class="message error"><strong>' + errorMessage + '</strong></p>' html += '<div class="actions-wrapper" style="margin: 15px 0;">' html += '<button type="button" id="cleanup-all-orphaned-elements" class="ajax button button-primary" style="margin-right: 10px;">' html += '<span class="text">' + window.bricksData.i18n.cleanupAllOrphanedElements + '</span>' html += '<span class="spinner is-active"></span>' html += '<i class="dashicons dashicons-yes hide"></i>' html += '</button>' html += '</div>' html += '<div class="orphaned-posts-list">' html += '<ul>' // Build the list of posts with orphaned elements for (let postId in data.orphaned_by_post_id) { let postData = data.orphaned_by_post_id[postId] let permalink = postData.permalink let editUrl // Construct edit URL using permalink and builderParam (similar to getEditTemplateLink in PopupTemplates.vue) if (permalink.indexOf('?') === -1) { editUrl = `${permalink}?${window.bricksData.builderParam}=run` } else { editUrl = `${permalink}&${window.bricksData.builderParam}=run` } html += '<li>' html += '<a href="' + editUrl + '" target="_blank">' html += '<strong>' + postData.post_title + '</strong>' html += '</a>' html += ' - ' html += '<span style="color: #d63638;">' html += window.bricksData.i18n.orphanedElementsCountMessage.replace( '%d', postData.total_orphans ) html += '</span>' html += '</li>' } html += '</ul>' html += '</div>' } container.innerHTML = html container.style.display = 'block' // Initialize cleanup button if orphaned elements were found if (data.total_orphans > 0) { bricksCleanupOrphanedElements() } } document.addEventListener('DOMContentLoaded', function (e) { bricksAdminClassicEditor() bricksAdminImport() bricksAdminSaveLicenseKey() bricksAdminToggleLicenseKey() bricksAdminSettings() bricksAdminRunConverter() bricksAdminBreakpointsRegenerateCssFiles() bricksAdminGenerateCssFiles() bricksAdminCodeReview() bricksAdminCodeReviewFilter() bricksAdminCustomCapabilities() bricksAdminGlobalElementsReview() bricksTemplateShortcodeCopyToClipboard() bricksDismissHttpsNotice() bricksDismissInstagramAccessTokenNotice() bricksDropFormSubmissionsTable() bricksResetFormSubmissionsTable() bricksDeleteFormSubmissionsByFormId() bricksRemoteTemplateUrls() bricksDeleteTemplateScreenshots() bricksReindexFilters() bricksRunIndexJob() bricksFixElementDB() bricksRegenerateCodeSignatures() bricksCleanupOrphanedElements() bricksScanOrphanedElements() bricksTemplateThumbnailAddScrollAnimation() bricksAdminMaintenanceTemplateListener() bricksAdminAuthUrlBehaviorListener() bricksAdminWooSettings() bricksAdminTemplateExclusion() bricksAdminMaintenanceExcludedPosts() bricksAdminUserActivation() bricksAdminElementManagerUsage() bricksElementManager() bricksAttributeImageSwatches() bricksAttributeColorSwatches() bricksAttributeSwatchTypeVisibility() // Move table navigation top & bottom outside of table container to make table horizontal scrollable let tableContainer = document.querySelector('.wp-list-table-container') let tablenavTop = document.querySelector('.tablenav.top') let tablenavBottom = document.querySelector('.tablenav.bottom') if (tableContainer && tablenavTop) { // Insert tablenav top before table tableContainer.parentNode.insertBefore(tablenavTop, tableContainer) } if (tableContainer && tablenavBottom) { // Insert tablenav top before table tableContainer.parentNode.insertBefore(tablenavBottom, tableContainer.nextSibling) } // Set search_box placeholder let formSubmissionsForm = document.getElementById('bricks-form-submissions') let searchBox = formSubmissionsForm ? formSubmissionsForm.querySelector('.search-box input[type=search]') : false if (searchBox) { searchBox.placeholder = window.bricksData?.i18n.formSubmissionsSearchPlaceholder } })
修改文件时间
将文件时间修改为当前时间的前一年
删除文件