Snapshot: MLS sync fixes, image refresh, plugin/theme updates

MLS plugin fixes from this session:
- Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls,
  causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL
  with ST_PointFromText so the spatial column is populated atomically.
- Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by
  a property-level GET_LOCK so concurrent fetches share one API refresh.
- Normalize WP_Error to null in mls_get_property_image() so callers can rely on the
  documented string|null contract.
- Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the
  homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm)
  actually filters correctly.
- Incremental sync now looks back 10 minutes past the latest modification timestamp
  as a safety margin against missed records.
- Smart sync exits silently (info-level, not warning) when a full sync is in progress.

Operational:
- New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync).
- New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/
  (/usr/local/bin/mls-image-cache-cap).
- Logrotate config for wp-content/debug.log (2-day retention, daily rotation,
  delaycompress).

Repo policy:
- CLAUDE.md updated with explicit "commit everything except build artifacts" policy.
- .gitignore: untrack runtime image caches and debug.log rotations.

Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-29 15:32:23 +00:00
parent 57b752f54e
commit b6df4dbb92
5385 changed files with 838580 additions and 2416 deletions
@@ -0,0 +1,170 @@
/* global wpforms_ai_chat_element */
/**
* @param wpforms_ai_chat_element.ajaxurl
* @param wpforms_ai_chat_element.errors.network
* @param wpforms_ai_chat_element.errors.default
*/
/**
* The WPForms AI API wrapper.
*
* @since 1.9.1
*
* @return {Function} The app cloning function.
*/
export default function() { // eslint-disable-line no-unused-vars, max-lines-per-function
/**
* Public functions and properties.
*
* @since 1.9.1
*
* @type {Object}
*/
const app = {
/**
* AI chat mode.
*
* @since 1.9.1
*
* @type {string}
*/
mode: '',
/**
* AI AJAX actions.
*
* @since 1.9.1
*
* @type {Object}
*/
actions: {
rate: 'wpforms_rate_ai_response',
choices: 'wpforms_get_ai_choices',
forms: 'wpforms_get_ai_form',
},
/**
* AJAX request.
*
* @param {Object} data Data to send.
*
* @return {Promise} The fetch result data promise.
*/
// eslint-disable-next-line complexity
async ajax( data ) {
if ( ! data.nonce ) {
data.nonce = wpforms_ai_chat_element.nonce;
}
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams( data ).toString(),
};
const response = await fetch( wpforms_ai_chat_element.ajaxurl, options )
.catch( ( error ) => {
if ( error.message === 'Failed to fetch' ) {
throw new Error( wpforms_ai_chat_element.errors.network );
} else {
throw new Error( error.message );
}
} );
if ( ! response.ok ) {
throw new Error( wpforms_ai_chat_element.errors.network );
}
const result = await response.json();
if ( ! result.success || result.data?.error ) {
throw new Error(
result.data?.error ?? wpforms_ai_chat_element.errors.default,
{
cause: result.data?.code ?? 400,
} );
}
return result.data;
},
/**
* Prompt.
*
* @param {string} prompt The question to ask.
* @param {string} sessionId Session ID.
*
* @return {Promise} The response data in promise.
*/
async prompt( prompt, sessionId ) {
const data = {
action: app.actions[ this.mode ] ?? app.actions.choices,
prompt,
};
if ( sessionId ) {
data.session_id = sessionId; // eslint-disable-line camelcase
}
return app.ajax( data );
},
/**
* Rate.
*
* @param {boolean} helpful Whether the response was helpful or not.
* @param {string} responseId Response ID.
*
* @return {Promise} The response data in promise.
*/
async rate( helpful, responseId ) {
const data = {
action: app.actions.rate,
helpful,
response_id: responseId, // eslint-disable-line camelcase
};
return app.ajax( data );
},
setUp() {
app.actions = {
...app.actions,
...wpforms_ai_chat_element.actions,
};
return this;
},
/**
* Set the AI chat mode.
*
* @since 1.9.1
*
* @param {string} mode The mode to set.
*
* @return {Object} The app object.
*/
setMode( mode ) {
this.mode = mode;
return this;
},
};
/**
* Return a clone of an app object.
*
* @since 1.9.1
*
* @param {string} mode The AI prompt mode.
*
* @return {Object} Cloned app object.
*/
return function( mode ) {
const obj = { ...app };
return obj.setUp().setMode( mode );
};
}
@@ -0,0 +1 @@
export default function(){let t={mode:"",actions:{rate:"wpforms_rate_ai_response",choices:"wpforms_get_ai_choices",forms:"wpforms_get_ai_form"},async ajax(e){e.nonce||(e.nonce=wpforms_ai_chat_element.nonce);e={method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(e).toString()},e=await fetch(wpforms_ai_chat_element.ajaxurl,e).catch(e=>{throw"Failed to fetch"===e.message?new Error(wpforms_ai_chat_element.errors.network):new Error(e.message)});if(!e.ok)throw new Error(wpforms_ai_chat_element.errors.network);e=await e.json();if(!e.success||e.data?.error)throw new Error(e.data?.error??wpforms_ai_chat_element.errors.default,{cause:e.data?.code??400});return e.data},async prompt(e,r){e={action:t.actions[this.mode]??t.actions.choices,prompt:e};return r&&(e.session_id=r),t.ajax(e)},async rate(e,r){e={action:t.actions.rate,helpful:e,response_id:r};return t.ajax(e)},setUp(){return t.actions={...t.actions,...wpforms_ai_chat_element.actions},this},setMode(e){return this.mode=e,this}};return function(e){return{...t}.setUp().setMode(e)}}
@@ -0,0 +1,319 @@
/* global WPFormsAIChatHTMLElement, WPFormsBuilder, wpf, wpforms_builder */
/**
* The WPForms AI chat element.
*
* Choices helpers module.
*
* @since 1.9.1
*
* @param {WPFormsAIChatHTMLElement} chat The chat element.
*
* @return {Object} The choices' helpers object.
*/
export default function( chat ) { // eslint-disable-line max-lines-per-function
/**
* The `choices` mode helpers object.
*
* @since 1.9.1
*/
return {
/**
* Get the `choices` answer based on AI response data.
*
* @since 1.9.1
*
* @param {Object} response The response data.
*
* @return {string} Answer HTML markup.
*/
getAnswer( response ) {
if ( response.choices?.length < 1 ) {
return '';
}
const li = [];
for ( const i in response.choices ) {
li.push( `
<li class="wpforms-ai-chat-choices-item">
${ chat.htmlSpecialChars( response.choices[ i ] ) }
</li>
` );
}
let answerHtml = `
<h4>${ chat.htmlSpecialChars( response.heading ?? '' ) }</h4>
<ol>
${ li.join( '' ) }
</ol>
`;
// Add footer to the first answer only.
if ( ! chat.sessionId ) {
answerHtml += `<span>${ chat.modeStrings.footer }</span>`;
}
return answerHtml;
},
/**
* Get the answer pre-buttons HTML markup.
*
* @since 1.9.1
*
* @return {string} The answer pre-buttons HTML markup.
*/
getAnswerButtonsPre() {
return `
<button type="button" class="wpforms-ai-chat-choices-insert wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${ chat.modeStrings.insert }</span>
</button>
`;
},
/**
* Get the warning message HTML markup.
*
* @since 1.9.1
*
* @return {string} The warning message HTML markup.
*/
getWarningMessage() {
// Trigger event before warning message insert.
chat.triggerEvent( 'wpformsAIModalBeforeWarningMessageInsert', { fieldId: chat.fieldId } );
return `<div class="wpforms-ai-chat-divider"></div>
<div class="wpforms-chat-item-notice">
<div class="wpforms-chat-item-notice-content">
<span>${ chat.modeStrings.warning }</span>
</div>
</div>`;
},
/**
* If the field has default choices, the welcome screen is active.
*
* @since 1.9.1
*
* @return {boolean} True if the field has default choices, false otherwise.
*/
isWelcomeScreen() {
const items = document.getElementById( `wpforms-field-option-row-${ chat.fieldId }-choices` )
.querySelectorAll( 'li input.label' );
if ( items.length === 1 && ! items[ 0 ].value.trim() ) {
return true;
}
if ( items.length > 3 ) {
return false;
}
const defaults = Object.values( chat.modeStrings.defaults );
for ( let i = 0; i < items.length; i++ ) {
if ( ! defaults.includes( items[ i ].value ) ) {
return false;
}
}
return true;
},
/**
* Add the `choices` answer.
*
* @since 1.9.1
*
* @param {HTMLElement} element The answer element.
*/
addedAnswer( element ) {
const button = element.querySelector( '.wpforms-ai-chat-choices-insert' );
// Listen to the button click event.
button?.addEventListener( 'click', this.insertButtonClick.bind( this ) );
},
/**
* Sanitize response.
*
* @since 1.9.2
*
* @param {Object} response The response data to sanitize.
*
* @return {Object} The sanitized response.
*/
sanitizeResponse( response ) {
if ( ! Array.isArray( response?.choices ) ) {
return response;
}
let choices = response.choices;
// Sanitize choices.
choices = choices.map( ( choice ) => {
return wpf.sanitizeHTML( choice, wpforms_builder.allowed_label_html_tags );
} );
// Remove empty choices.
response.choices = choices.filter( ( choice ) => {
return choice.trim() !== '';
} );
return response;
},
/**
* Check if the response has a prohibited code.
*
* @since 1.9.2
*
* @param {Object} response The response data.
* @param {Array} sanitizedResponse The sanitized response data.
*
* @return {boolean} Whether the answer has a prohibited code.
*/
hasProhibitedCode( response, sanitizedResponse ) {
// If the number of choices has changed after sanitization, it means that the answer contains prohibited code.
return sanitizedResponse?.choices?.length !== response?.choices?.length;
},
/**
* Click on the Use Choices button.
*
* @since 1.9.1
*
* @param {Event} e The event object.
*/
insertButtonClick( e ) {
const button = e.target;
const answer = button.closest( '.wpforms-chat-item.wpforms-chat-item-choices' );
const responseId = answer?.getAttribute( 'data-response-id' );
const choicesList = answer?.querySelector( 'ol' );
const items = choicesList.querySelectorAll( '.wpforms-ai-chat-choices-item' );
const choiceItems = [];
// Get choices data.
for ( const i in items ) {
if ( ! items.hasOwnProperty( i ) || ! items[ i ].textContent ) {
continue;
}
choiceItems.push( items[ i ].textContent.trim() );
}
// Rate the response.
chat.wpformsAiApi.rate( true, responseId );
// Replace field choices.
this.replaceChoices( choiceItems );
// Toggle to the field.
jQuery( `#wpforms-field-${ chat.fieldId }` ).click().promise().done( function() {
jQuery( `#wpforms-field-option-basic-${ chat.fieldId } a.wpforms-field-option-group-toggle` ).click();
} );
},
/**
* Replace field choices.
*
* @since 1.9.1
*
* @param {Array} choices Choices array.
*/
replaceChoices( choices ) {
const choicesOptionRow = document.getElementById( `wpforms-field-option-row-${ chat.fieldId }-choices` );
const choicesList = choicesOptionRow.querySelector( 'ul.choices-list' );
const choiceRow = choicesList.querySelector( 'li:first-child' ).cloneNode( true );
choiceRow.innerHTML = choiceRow.innerHTML.replace( /\[choices\]\[\d+\]/g, `[choices][{{key}}]` );
// Clear existing choices.
choicesList.innerHTML = '';
// Add new choices.
for ( const i in choices ) {
const key = ( Number( i ) + 1 ).toString();
const choice = choices[ i ];
// Clone choice item element.
let li = choiceRow.cloneNode( true );
// Get updated single choice item.
li = this.getUpdatedSingleChoiceItem( li, key, choice );
// Add new choice item.
choicesList.appendChild( li );
}
// Update data-next-id attribute for choices list.
choicesList.setAttribute( 'data-next-id', choices.length + 1 );
// Update field preview.
const fieldOptions = document.getElementById( `wpforms-field-option-${ chat.fieldId }` );
const fieldType = fieldOptions.querySelector( 'input.wpforms-field-option-hidden-type' )?.value;
WPFormsBuilder.fieldChoiceUpdate( fieldType, chat.fieldId, choices.length );
WPFormsBuilder.triggerBuilderEvent( 'wpformsFieldChoiceAdd' );
// Trigger event after choices insert.
chat.triggerEvent( 'wpformsAIModalAfterChoicesInsert', { fieldId: chat.fieldId } );
},
/**
* Get updated single choice item.
*
* @since 1.9.1
*
* @param {HTMLElement} li Choice item element.
* @param {string} key Choice key.
* @param {string} choice Choice value.
*
* @return {HTMLElement} The updated choice item.
*/
getUpdatedSingleChoiceItem( li, key, choice ) {
li.setAttribute( 'data-key', key.toString() );
// Update choice item inputs name attributes.
li.innerHTML = li.innerHTML.replaceAll( '{{key}}', key );
// Sanitize choice before set.
choice = wpf.sanitizeHTML( choice );
const inputDefault = li.querySelector( 'input.default' );
inputDefault.removeAttribute( 'checked' );
// Set label
const inputLabel = li.querySelector( 'input.label' );
inputLabel.value = choice;
inputLabel.setAttribute( 'value', choice );
// Set value.
const inputValue = li.querySelector( 'input.value' );
inputValue.value = choice;
inputValue.setAttribute( 'value', choice );
// Reset image upload.
const imageUpload = li.querySelector( '.wpforms-image-upload' );
const inputImage = imageUpload.querySelector( 'input.source' );
inputImage.value = '';
inputImage.setAttribute( 'value', '' );
imageUpload.querySelector( '.preview' ).innerHTML = '';
imageUpload.querySelector( '.wpforms-image-upload-add' ).style.display = 'block';
// Reset icon choice.
const iconSelect = li.querySelector( '.wpforms-icon-select' );
iconSelect.querySelector( '.ic-fa-preview' ).setAttribute( 'class', 'ic-fa-preview ic-fa-regular ic-fa-face-smile' );
iconSelect.querySelector( 'input.source-icon' ).value = 'face-smile';
iconSelect.querySelector( 'input.source-icon-style' ).value = 'regular';
return li;
},
};
}
@@ -0,0 +1,19 @@
export default function(c){return{getAnswer(e){if(e.choices?.length<1)return"";var t,r=[];for(t in e.choices)r.push(`
<li class="wpforms-ai-chat-choices-item">
${c.htmlSpecialChars(e.choices[t])}
</li>
`);let i=`
<h4>${c.htmlSpecialChars(e.heading??"")}</h4>
<ol>
${r.join("")}
</ol>
`;return c.sessionId||(i+=`<span>${c.modeStrings.footer}</span>`),i},getAnswerButtonsPre(){return`
<button type="button" class="wpforms-ai-chat-choices-insert wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${c.modeStrings.insert}</span>
</button>
`},getWarningMessage(){return c.triggerEvent("wpformsAIModalBeforeWarningMessageInsert",{fieldId:c.fieldId}),`<div class="wpforms-ai-chat-divider"></div>
<div class="wpforms-chat-item-notice">
<div class="wpforms-chat-item-notice-content">
<span>${c.modeStrings.warning}</span>
</div>
</div>`},isWelcomeScreen(){var t=document.getElementById(`wpforms-field-option-row-${c.fieldId}-choices`).querySelectorAll("li input.label");if(1!==t.length||t[0].value.trim()){if(3<t.length)return!1;var r=Object.values(c.modeStrings.defaults);for(let e=0;e<t.length;e++)if(!r.includes(t[e].value))return!1}return!0},addedAnswer(e){e.querySelector(".wpforms-ai-chat-choices-insert")?.addEventListener("click",this.insertButtonClick.bind(this))},sanitizeResponse(t){if(Array.isArray(t?.choices)){let e=t.choices;e=e.map(e=>wpf.sanitizeHTML(e,wpforms_builder.allowed_label_html_tags)),t.choices=e.filter(e=>""!==e.trim())}return t},hasProhibitedCode(e,t){return t?.choices?.length!==e?.choices?.length},insertButtonClick(e){var t,e=e.target.closest(".wpforms-chat-item.wpforms-chat-item-choices"),r=e?.getAttribute("data-response-id"),i=(e?.querySelector("ol")).querySelectorAll(".wpforms-ai-chat-choices-item"),o=[];for(t in i)i.hasOwnProperty(t)&&i[t].textContent&&o.push(i[t].textContent.trim());c.wpformsAiApi.rate(!0,r),this.replaceChoices(o),jQuery("#wpforms-field-"+c.fieldId).click().promise().done(function(){jQuery(`#wpforms-field-option-basic-${c.fieldId} a.wpforms-field-option-group-toggle`).click()})},replaceChoices(e){var t,r=document.getElementById(`wpforms-field-option-row-${c.fieldId}-choices`).querySelector("ul.choices-list"),i=r.querySelector("li:first-child").cloneNode(!0);for(t in i.innerHTML=i.innerHTML.replace(/\[choices\]\[\d+\]/g,"[choices][{{key}}]"),r.innerHTML="",e){var o=(Number(t)+1).toString(),l=e[t],s=i.cloneNode(!0),s=this.getUpdatedSingleChoiceItem(s,o,l);r.appendChild(s)}r.setAttribute("data-next-id",e.length+1);var n=document.getElementById("wpforms-field-option-"+c.fieldId).querySelector("input.wpforms-field-option-hidden-type")?.value;WPFormsBuilder.fieldChoiceUpdate(n,c.fieldId,e.length),WPFormsBuilder.triggerBuilderEvent("wpformsFieldChoiceAdd"),c.triggerEvent("wpformsAIModalAfterChoicesInsert",{fieldId:c.fieldId})},getUpdatedSingleChoiceItem(e,t,r){e.setAttribute("data-key",t.toString()),e.innerHTML=e.innerHTML.replaceAll("{{key}}",t),r=wpf.sanitizeHTML(r);e.querySelector("input.default").removeAttribute("checked");t=e.querySelector("input.label"),t.value=r,t.setAttribute("value",r),t=e.querySelector("input.value"),t.value=r,t.setAttribute("value",r),t=e.querySelector(".wpforms-image-upload"),r=t.querySelector("input.source"),r.value="",r.setAttribute("value",""),t.querySelector(".preview").innerHTML="",t.querySelector(".wpforms-image-upload-add").style.display="block",r=e.querySelector(".wpforms-icon-select");return r.querySelector(".ic-fa-preview").setAttribute("class","ic-fa-preview ic-fa-regular ic-fa-face-smile"),r.querySelector("input.source-icon").value="face-smile",r.querySelector("input.source-icon-style").value="regular",e}}}
@@ -0,0 +1,163 @@
/* global WPFormsAIChatHTMLElement, WPFormsAIFormGenerator, wpf, wpforms_builder */
/**
* @param chat.modeStrings.footerFirst
* @param chat.modeStrings.inactiveAnswerTitle
* @param chat.preventResizeInput
* @param response.form_title
* @param wpforms_builder.allowed_label_html_tags
*/
/**
* The WPForms AI chat element.
*
* Forms mode helpers module.
*
* @since 1.9.2
*
* @param {WPFormsAIChatHTMLElement} chat The chat element.
*
* @return {Object} Forms helpers object.
*/
export default function( chat ) { // eslint-disable-line no-unused-vars, max-lines-per-function
/**
* The default `forms` mode helpers object.
*
* @since 1.9.2
*/
const forms = {
/**
* Init `forms` mode.
*
* @since 1.9.2
*/
init() {
// Set the initial form generator state.
if ( chat.sessionId ) {
WPFormsAIFormGenerator.state.chatStart = true;
// Remove the selected state from the current template card.
WPFormsAIFormGenerator.main.el.$templateCard
.next( '.selected' ).removeClass( 'selected' );
}
},
/**
* Reset the message input field.
*
* @since 1.9.2
*/
resetInput() {
chat.resizeInput();
},
/**
* Get the answer based on AI response data.
*
* @since 1.9.2
*
* @param {Object} response The AI response data.
*
* @return {string} HTML markup.
*/
getAnswer( response ) {
if ( ! response ) {
return '';
}
const rnd = Math.floor( Math.random() * chat.modeStrings.footer.length );
const footer = chat.modeStrings.footer[ rnd ];
const answer = response.explanation || ( response.form_title ?? '' );
return `
<h4>${ answer }</h4>
<span>${ footer }</span>
`;
},
/**
* Get the answer pre-buttons HTML markup.
*
* @since 1.9.2
*
* @return {string} The answer pre-buttons HTML markup.
*/
getAnswerButtonsPre() {
return `
<button type="button" class="wpforms-ai-chat-use-form wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${ chat.modeStrings.useForm }</span>
</button>
`;
},
/**
* The answer was added.
*
* @since 1.9.2
*
* @param {HTMLElement} element The answer element.
*/
addedAnswer( element ) { // eslint-disable-line no-unused-vars
forms.updateInactiveAnswers();
},
/**
* Set active answer.
*
* @since 1.9.2
*
* @param {HTMLElement} element The answer element.
*/
setActiveAnswer( element ) {
forms.updateInactiveAnswers();
element.querySelector( '.wpforms-chat-item-content' ).setAttribute( 'title', '' );
},
/**
* Update inactive answers.
*
* @since 1.9.2
*/
updateInactiveAnswers() {
chat.messageList.querySelectorAll( '.wpforms-chat-item-answer:not(.active) .wpforms-chat-item-content' )
.forEach( ( el ) => {
// Set title attribute for inactive answers.
el.setAttribute( 'title', chat.modeStrings.inactiveAnswerTitle );
} );
},
/**
* Determine whether the Welcome Screen should be displayed.
*
* @since 1.9.2
*
* @return {boolean} Display the Welcome Screen or not.
*/
isWelcomeScreen() {
return true;
},
/**
* Sanitize response.
*
* @since 1.9.2
*
* @param {Object} response The response data to sanitize.
*
* @return {Object} The sanitized response.
*/
sanitizeResponse( response ) {
if ( ! response.explanation ) {
return response;
}
// Sanitize explanation string.
response.explanation = wpf.sanitizeHTML( response.explanation, wpforms_builder.allowed_label_html_tags );
return response;
},
};
return forms;
}
@@ -0,0 +1,8 @@
export default function(r){let t={init(){r.sessionId&&(WPFormsAIFormGenerator.state.chatStart=!0,WPFormsAIFormGenerator.main.el.$templateCard.next(".selected").removeClass("selected"))},resetInput(){r.resizeInput()},getAnswer(e){var t;return e?(t=Math.floor(Math.random()*r.modeStrings.footer.length),t=r.modeStrings.footer[t],`
<h4>${e.explanation||(e.form_title??"")}</h4>
<span>${t}</span>
`):""},getAnswerButtonsPre(){return`
<button type="button" class="wpforms-ai-chat-use-form wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${r.modeStrings.useForm}</span>
</button>
`},addedAnswer(e){t.updateInactiveAnswers()},setActiveAnswer(e){t.updateInactiveAnswers(),e.querySelector(".wpforms-chat-item-content").setAttribute("title","")},updateInactiveAnswers(){r.messageList.querySelectorAll(".wpforms-chat-item-answer:not(.active) .wpforms-chat-item-content").forEach(e=>{e.setAttribute("title",r.modeStrings.inactiveAnswerTitle)})},isWelcomeScreen(){return!0},sanitizeResponse(e){return e.explanation&&(e.explanation=wpf.sanitizeHTML(e.explanation,wpforms_builder.allowed_label_html_tags)),e}};return t}
@@ -0,0 +1,67 @@
/* global WPFormsAIChatHTMLElement */
/**
* The WPForms AI chat element.
*
* Choices helpers module.
*
* @since 1.9.1
*
* @param {WPFormsAIChatHTMLElement} chat The chat element.
*
* @return {Function} The app cloning function.
*/
export default function( chat ) { // eslint-disable-line no-unused-vars
/**
* The default `text` mode helpers object.
*
* @since 1.9.1
*/
return {
/**
* Get the `text` answer based on AI response data.
*
* @since 1.9.1
*
* @param {Object} response The AI response data.
*
* @return {string} HTML markup.
*/
getAnswer( response ) {
return `
<h4>${ response?.heading ?? '' }</h4>
<p>${ response?.text ?? '' }</p>
<span>${ response?.footer ?? '' }</span>
`;
},
/**
* Get the answer pre-buttons HTML markup.
*
* @since 1.9.1
*
* @return {string} The answer pre-buttons HTML markup.
*/
getAnswerButtonsPre() {
return '';
},
/**
* Added answer callback.
*
* @since 1.9.1
*/
addedAnswer() {},
/**
* Determine whether the Welcome Screen should be displayed.
*
* @since 1.9.2
*
* @return {boolean} Display the Welcome Screen or not.
*/
isWelcomeScreen() {
return true;
},
};
}
@@ -0,0 +1,5 @@
export default function(e){return{getAnswer(e){return`
<h4>${e?.heading??""}</h4>
<p>${e?.text??""}</p>
<span>${e?.footer??""}</span>
`},getAnswerButtonsPre(){return""},addedAnswer(){},isWelcomeScreen(){return!0}}}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,245 @@
/* global wpforms_ai_chat_element */
/**
* @param wpforms_ai_chat_element.pinChat
* @param wpforms_ai_chat_element.unpinChat
* @param wpforms_ai_chat_element.close
*/
/**
* Dock JS module.
*
* @since 1.9.5
*
* @param {jQuery} $ jQuery object.
*
* @return {Object} Dock object.
*/
// eslint-disable-next-line no-unused-vars
const wpFormsAIDock = ( function( $ ) {
/**
* Pin modal.
*
* @since 1.9.5
*
* @param {jQuery} $modal Modal element.
*/
function pinModal( $modal ) {
localStorage.setItem( 'wpforms-ai-chat-prefers-pinned', '1' );
$modal.find( '.wpforms-ai-modal-pin' ).attr( 'title', wpforms_ai_chat_element.unpinChat );
const $toolbar = $( '#wpforms-builder-form .wpforms-toolbar' );
// Get the distance from the top of the screen to the bottom border of the toolbar.
const toolbarHeight = $toolbar.offset().top + $toolbar.outerHeight();
$modal.addClass( 'pinned' );
if ( $( '#wpadminbar' ).is( ':visible' ) ) {
$modal.addClass( 'with-wpadminbar' );
}
$modal.insertAfter( $toolbar ).promise().done( function() {
$modal.css( {
top: toolbarHeight,
} );
} );
}
/**
* Unpin modal.
*
* @since 1.9.5
*
* @param {jQuery} $modal Modal element.
*/
function unPinModal( $modal ) {
localStorage.setItem( 'wpforms-ai-chat-prefers-pinned', '0' );
$modal.find( '.wpforms-ai-modal-pin' ).attr( 'title', wpforms_ai_chat_element.pinChat );
$modal.removeClass( 'pinned' );
$modal.removeClass( 'with-wpadminbar' );
$modal.appendTo( $( 'body' ) ).promise().done( function() {
$modal.css( {
top: 0,
} );
} );
$modal.find( '.wpforms-ai-modal-top-bar' ).removeClass( 'scrolled' );
}
/**
* Handle click on the pin button.
*
* @since 1.9.5
*/
function onPinIconClick() {
if ( $( this ).hasClass( 'not-allowed' ) ) {
return;
}
const $modal = $( this ).closest( '.jconfirm.jconfirm-wpforms-ai-modal' );
if ( $modal.hasClass( 'pinned' ) ) {
unPinModal( $modal );
} else {
pinModal( $modal );
}
// Re-apply this action also for other modals but hide them.
const $otherModals = $( '.jconfirm.jconfirm-wpforms-ai-modal' ).not( $modal );
$otherModals.each( function() {
const $otherModal = $( this );
if ( $otherModal.hasClass( 'pinned' ) ) {
unPinModal( $otherModal );
} else {
pinModal( $otherModal );
}
$otherModal.hide();
} );
}
/**
* Handle click on the panel toggle button.
*
* Hide pinned chats if this is not the fields panel.
*
* @since 1.9.5
*
* @param {Object} clicked Clicked a button object.
*/
function onPanelToggleClick( clicked ) {
const $this = $( clicked.target ),
dataPanel = $this.closest( 'button' ).data( 'panel' );
if ( dataPanel === 'fields' ) {
return;
}
$( '.jconfirm.jconfirm-wpforms-ai-modal.pinned' ).each( function() {
$( this ).hide();
} );
}
/**
* Bind events.
*
* @since 1.9.5
*/
function bindEvents() {
const AIModalPin = $( '.wpforms-ai-modal-pin' );
$( document )
.off( 'click', '.wpforms-ai-modal-pin' )
.on( 'click', '.wpforms-ai-modal-pin', onPinIconClick )
// Hide pin icon for the time of response generation.
.on( 'wpformsAIChatBeforeSendMessage', () => AIModalPin.addClass( 'not-allowed' ) )
.on( 'wpformsAIChatBeforeError wpformsAIChatAfterTypeText', () => AIModalPin.removeClass( 'not-allowed' ) );
$( '#wpforms-panels-toggle button' )
.off( 'click', onPanelToggleClick )
.on( 'click', onPanelToggleClick );
}
/**
* Get the modal element.
*
* @since 1.9.5
*
* @param {number} fieldId Field ID.
*
* @return {jQuery} Modal element.
*/
function getModal( fieldId ) {
const $chatElement = $( 'wpforms-ai-chat[field-id="' + fieldId + '"]' );
return $chatElement.closest( '.jconfirm.jconfirm-wpforms-ai-modal' ).last();
}
/**
* Prepare dock by adding pin button.
*
* @since 1.9.5
*
* @param {number} fieldId Field ID.
*/
function prepareDock( fieldId ) {
const $jconfirmAIModal = getModal( fieldId ),
dockAlreadyPrepared = $jconfirmAIModal.find( '.wpforms-ai-modal-pin' ).length;
if ( dockAlreadyPrepared ) {
return;
}
const $closeIcon = $jconfirmAIModal.find( '.jconfirm-closeIcon' );
// Append new bar after close icon and move close icon inside this bar.
$closeIcon.after(
`<div class="wpforms-ai-modal-top-bar">
<div class="wpforms-ai-modal-pin" title="${ wpforms_ai_chat_element.pinChat }"></div>
</div>`
).promise().done( function() {
const $topBar = $jconfirmAIModal.find( '.wpforms-ai-modal-top-bar' );
$closeIcon.appendTo( $topBar );
} );
$closeIcon.attr( 'title', wpforms_ai_chat_element.close );
const $topBar = $jconfirmAIModal.find( '.wpforms-ai-modal-top-bar' ),
$messageList = $jconfirmAIModal.find( '.wpforms-ai-chat-message-list' );
$messageList.off( 'scroll' );
$messageList.on( 'scroll', function() {
if ( $messageList.scrollTop() > 0 ) {
$topBar.addClass( 'scrolled' );
return;
}
$topBar.removeClass( 'scrolled' );
} );
$jconfirmAIModal.on( 'remove', function() {
$messageList.off( 'scroll' );
} );
}
/**
* Maybe pin modal on open.
*
* @since 1.9.5
*
* @param {string} fieldId Field ID.
*/
function onOpen( fieldId ) {
const savedState = localStorage.getItem( 'wpforms-ai-chat-prefers-pinned' ) || '0';
if ( savedState === '0' ) {
return;
}
pinModal( getModal( fieldId ) );
}
/**
* Initialize the dock.
*
* @since 1.9.5
*
* @param {number} fieldId Field ID.
*/
function init( fieldId ) {
prepareDock( fieldId );
bindEvents();
onOpen( fieldId );
}
return { init };
}( jQuery ) );
@@ -0,0 +1,3 @@
let wpFormsAIDock=(e=>{function s(o){localStorage.setItem("wpforms-ai-chat-prefers-pinned","1"),o.find(".wpforms-ai-modal-pin").attr("title",wpforms_ai_chat_element.unpinChat);var a=e("#wpforms-builder-form .wpforms-toolbar");let i=a.offset().top+a.outerHeight();o.addClass("pinned"),e("#wpadminbar").is(":visible")&&o.addClass("with-wpadminbar"),o.insertAfter(a).promise().done(function(){o.css({top:i})})}function a(o){localStorage.setItem("wpforms-ai-chat-prefers-pinned","0"),o.find(".wpforms-ai-modal-pin").attr("title",wpforms_ai_chat_element.pinChat),o.removeClass("pinned"),o.removeClass("with-wpadminbar"),o.appendTo(e("body")).promise().done(function(){o.css({top:0})}),o.find(".wpforms-ai-modal-top-bar").removeClass("scrolled")}function n(){var o;e(this).hasClass("not-allowed")||(((o=e(this).closest(".jconfirm.jconfirm-wpforms-ai-modal")).hasClass("pinned")?a:s)(o),e(".jconfirm.jconfirm-wpforms-ai-modal").not(o).each(function(){var o=e(this);(o.hasClass("pinned")?a:s)(o),o.hide()}))}function t(o){"fields"!==e(o.target).closest("button").data("panel")&&e(".jconfirm.jconfirm-wpforms-ai-modal.pinned").each(function(){e(this).hide()})}function r(o){return e('wpforms-ai-chat[field-id="'+o+'"]').closest(".jconfirm.jconfirm-wpforms-ai-modal").last()}return{init:function(a){{var i=a;let e=r(i),o=e.find(".wpforms-ai-modal-pin").length;if(!o){let a=e.find(".jconfirm-closeIcon"),o=(a.after(`<div class="wpforms-ai-modal-top-bar">
<div class="wpforms-ai-modal-pin" title="${wpforms_ai_chat_element.pinChat}"></div>
</div>`).promise().done(function(){var o=e.find(".wpforms-ai-modal-top-bar");a.appendTo(o)}),a.attr("title",wpforms_ai_chat_element.close),e.find(".wpforms-ai-modal-top-bar")),i=e.find(".wpforms-ai-chat-message-list");i.off("scroll"),i.on("scroll",function(){0<i.scrollTop()?o.addClass("scrolled"):o.removeClass("scrolled")}),e.on("remove",function(){i.off("scroll")})}}{let o=e(".wpforms-ai-modal-pin");e(document).off("click",".wpforms-ai-modal-pin").on("click",".wpforms-ai-modal-pin",n).on("wpformsAIChatBeforeSendMessage",()=>o.addClass("not-allowed")).on("wpformsAIChatBeforeError wpformsAIChatAfterTypeText",()=>o.removeClass("not-allowed")),e("#wpforms-panels-toggle button").off("click",t).on("click",t)}i=a,"0"!==(localStorage.getItem("wpforms-ai-chat-prefers-pinned")||"0")&&s(r(i))}}})(jQuery);
@@ -0,0 +1,307 @@
/* global wpforms_ai_chat_element, wpFormsAIDock */
// noinspection ES6ConvertVarToLetConst
/**
* AI modal.
*
* @since 1.9.1
*/
// eslint-disable-next-line no-var
var WPFormsAIModal = window.WPFormsAIModal || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.9.1
*
* @type {Object}
*/
const app = {
/**
* Default modal options.
*
* @since 1.9.1
*/
defaultOptions: {
title: false,
content: '',
type: 'ai',
smoothContent: true,
bgOpacity: 1,
boxWidth: 650,
contentMaxHeight: 600,
closeIcon: true,
buttons: false,
},
/**
* Start the engine.
*
* @since 1.9.1
*/
init() {
$( app.ready );
},
/**
* Initialized once the DOM is fully loaded.
*
* @since 1.9.1
*/
ready() {
app.extendJqueryConfirm();
app.bindChoicesActions();
},
/**
* Process various events for choices modal.
*
* @since 1.9.1
*/
bindChoicesActions() {
$( document )
.on( 'click', '.wpforms-ai-choices-button', app.initChoicesModal )
.on( 'wpformsAIChatBeforeRefreshConfirm', app.beforeChoicesRefreshConfirm )
.on( 'wpformsAIModalBeforeWarningMessageInsert', app.refreshModalHeight )
.on( 'wpformsAIChatAfterRefresh', app.refreshModalHeight )
.on( 'wpformsAIChatCancelRefresh', app.cancelChoicesRefresh )
.on( 'wpformsAIChatBeforeSendMessage', function( e ) {
app.resizeModalHeight( e.detail.fieldId );
} )
.on( 'wpformsAIChatAfterAddAnswer', function( e ) {
app.resizeModalHeight( e.detail.fieldId );
} )
.on( 'wpformsAIModalAfterChoicesInsert', function( e ) {
app.hideChoicesModal( e.detail.fieldId );
} );
$( window ).on( 'resize', function() {
$( '.jconfirm-wpforms-ai-modal wpforms-ai-chat' ).each( function() {
app.resizeModalHeight( $( this ).attr( 'field-id' ) );
} );
} );
},
/**
* Init modal window.
*
* @since 1.9.1
*
* @param {Object} args Modal window arguments.
*/
initModal( args ) {
// Open the modal window.
$.confirm( { ...app.defaultOptions, ...args } );
},
/**
* Init choices modal window.
*
* @since 1.9.1
*/
initChoicesModal() {
const $button = $( this );
if ( $button.hasClass( 'wpforms-prevent-default' ) ) {
$button.trigger( 'blur' );
return;
}
const fieldId = $button.data( 'field-id' ),
$modal = $( `.jconfirm-wpforms-ai-modal-choices-${ fieldId }` );
// Close any other modals.
$( `.jconfirm-wpforms-ai-modal:not(.jconfirm-wpforms-ai-modal-choices-${ fieldId })` )
.addClass( 'wpforms-hidden' )
.fadeOut();
if ( $modal.length ) {
$modal.removeClass( 'wpforms-hidden' ).fadeIn();
return;
}
const args = {},
hideChoices = function() {
app.hideChoicesModal( fieldId );
return false;
};
args.content = `<wpforms-ai-chat mode="choices" field-id="${ fieldId }" />`;
args.theme = `wpforms-ai-modal, wpforms-ai-purple, wpforms-ai-modal-choices-${ fieldId }`;
args.backgroundDismiss = hideChoices;
args.backgroundDismissAnimation = '';
args.contentMaxHeight = Math.min( app.defaultOptions.contentMaxHeight, app.getMaxModalHeight() );
args.onOpen = function() {
// Unbind the click event from the close icon and use our own instead.
this.$closeIcon.off( 'click' );
this.$closeIcon.on( 'click', hideChoices );
};
args.onOpenBefore = function() {
wpFormsAIDock.init( fieldId );
};
app.initModal( args );
},
/**
* Hide the choices modal window.
*
* @since 1.9.1
*
* @param {string} fieldId Choice field ID.
*/
hideChoicesModal( fieldId ) {
$( `.jconfirm-wpforms-ai-modal-choices-${ fieldId }` ).addClass( 'wpforms-hidden' ).fadeOut();
},
/**
* Show the choices modal window.
*
* @since 1.9.1
*
* @param {string} fieldId Choice field ID.
*/
showChoicesModal( fieldId ) {
$( `.jconfirm-wpforms-ai-modal-choices-${ fieldId }` ).removeClass( 'wpforms-hidden' ).fadeIn();
},
/**
* Resize choices modal window height.
*
* @since 1.9.4
*
* @param {string} fieldId Field ID.
*/
resizeModalHeight( fieldId ) {
const modalHeight = app.getMaxModalHeight();
const $modal = $( '.jconfirm-wpforms-ai-modal' ).filter( function() {
// find class starts with jconfirm-wpforms-ai-modal- and ends with -{fieldId}.
return $( this ).attr( 'class' ).match( new RegExp( 'jconfirm-wpforms-ai-modal-.*-' + fieldId, 'i' ) );
} );
$modal.find( '.jconfirm-content-pane' )
.css( {
height: modalHeight,
'max-height': modalHeight,
} );
},
/**
* Before choices refresh confirm is displayed.
*
* @since 1.9.1
*
* @param {Event} e Event object.
*/
beforeChoicesRefreshConfirm( e ) {
const fieldId = e.detail?.fieldId || 0;
app.hideChoicesModal( fieldId );
},
/**
* Cancel choices' refresh.
*
* @since 1.9.1
*
* @param {Event} e Event object.
*/
cancelChoicesRefresh( e ) {
const fieldId = e.detail?.fieldId || 0;
app.showChoicesModal( fieldId );
},
/**
* Refresh the main modal window height.
*
* @since 1.9.1
*
* @param {Event} e Event object.
*/
refreshModalHeight( e ) {
const fieldId = e.detail?.fieldId || 0;
const maxHeight = Math.min( app.getMaxModalHeight(), app.defaultOptions.contentMaxHeight );
app.showChoicesModal( fieldId );
// Reset choices modal window height.
$( `.jconfirm-wpforms-ai-modal-choices-${ fieldId } .jconfirm-content-pane` )
.css( {
height: maxHeight,
'max-height': maxHeight,
} );
},
/**
* Get the max modal height.
*
* @since 1.9.1
*
* @return {number} The max modal height.
*/
getMaxModalHeight() {
// 80% of the window height, but not more than 800 px.
return Math.min( $( window ).height() * 0.8, 800 );
},
/**
* Extend jquery-confirm plugin with support of max-height for the content area.
*
* @since 1.9.1
*/
extendJqueryConfirm() {
// Extend a method of global instance.
window.Jconfirm.prototype._updateContentMaxHeight = function() {
this.$contentPane.css( {
'max-height': this.contentMaxHeight + 'px',
} );
};
},
/**
* Confirm a modal window.
*
* This is a wrapper for the `jquery.confirm` plugin.
*
* @since 1.9.1
*
* @param {Object} args Modal window arguments.
*/
confirmModal( args ) {
const options = {
title: false,
content: '',
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_ai_chat_element.btnYes,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
if ( typeof args.onConfirm === 'function' ) {
args.onConfirm();
}
},
},
cancel: {
text: wpforms_ai_chat_element.btnCancel,
action() {
if ( typeof args.onCancel === 'function' ) {
args.onCancel();
}
},
},
},
};
$.confirm( { ...options, ...args } );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsAIModal.init();
@@ -0,0 +1 @@
var WPFormsAIModal=window.WPFormsAIModal||((e,o,t)=>{let n={defaultOptions:{title:!1,content:"",type:"ai",smoothContent:!0,bgOpacity:1,boxWidth:650,contentMaxHeight:600,closeIcon:!0,buttons:!1},init(){t(n.ready)},ready(){n.extendJqueryConfirm(),n.bindChoicesActions()},bindChoicesActions(){t(e).on("click",".wpforms-ai-choices-button",n.initChoicesModal).on("wpformsAIChatBeforeRefreshConfirm",n.beforeChoicesRefreshConfirm).on("wpformsAIModalBeforeWarningMessageInsert",n.refreshModalHeight).on("wpformsAIChatAfterRefresh",n.refreshModalHeight).on("wpformsAIChatCancelRefresh",n.cancelChoicesRefresh).on("wpformsAIChatBeforeSendMessage",function(e){n.resizeModalHeight(e.detail.fieldId)}).on("wpformsAIChatAfterAddAnswer",function(e){n.resizeModalHeight(e.detail.fieldId)}).on("wpformsAIModalAfterChoicesInsert",function(e){n.hideChoicesModal(e.detail.fieldId)}),t(o).on("resize",function(){t(".jconfirm-wpforms-ai-modal wpforms-ai-chat").each(function(){n.resizeModalHeight(t(this).attr("field-id"))})})},initModal(e){t.confirm({...n.defaultOptions,...e})},initChoicesModal(){var o=t(this);if(o.hasClass("wpforms-prevent-default"))o.trigger("blur");else{let i=o.data("field-id"),e=t(".jconfirm-wpforms-ai-modal-choices-"+i);if(t(`.jconfirm-wpforms-ai-modal:not(.jconfirm-wpforms-ai-modal-choices-${i})`).addClass("wpforms-hidden").fadeOut(),e.length)e.removeClass("wpforms-hidden").fadeIn();else{let e={},o=function(){return n.hideChoicesModal(i),!1};e.content=`<wpforms-ai-chat mode="choices" field-id="${i}" />`,e.theme="wpforms-ai-modal, wpforms-ai-purple, wpforms-ai-modal-choices-"+i,e.backgroundDismiss=o,e.backgroundDismissAnimation="",e.contentMaxHeight=Math.min(n.defaultOptions.contentMaxHeight,n.getMaxModalHeight()),e.onOpen=function(){this.$closeIcon.off("click"),this.$closeIcon.on("click",o)},e.onOpenBefore=function(){wpFormsAIDock.init(i)},n.initModal(e)}}},hideChoicesModal(e){t(".jconfirm-wpforms-ai-modal-choices-"+e).addClass("wpforms-hidden").fadeOut()},showChoicesModal(e){t(".jconfirm-wpforms-ai-modal-choices-"+e).removeClass("wpforms-hidden").fadeIn()},resizeModalHeight(e){var o=n.getMaxModalHeight();t(".jconfirm-wpforms-ai-modal").filter(function(){return t(this).attr("class").match(new RegExp("jconfirm-wpforms-ai-modal-.*-"+e,"i"))}).find(".jconfirm-content-pane").css({height:o,"max-height":o})},beforeChoicesRefreshConfirm(e){e=e.detail?.fieldId||0;n.hideChoicesModal(e)},cancelChoicesRefresh(e){e=e.detail?.fieldId||0;n.showChoicesModal(e)},refreshModalHeight(e){var e=e.detail?.fieldId||0,o=Math.min(n.getMaxModalHeight(),n.defaultOptions.contentMaxHeight);n.showChoicesModal(e),t(`.jconfirm-wpforms-ai-modal-choices-${e} .jconfirm-content-pane`).css({height:o,"max-height":o})},getMaxModalHeight(){return Math.min(.8*t(o).height(),800)},extendJqueryConfirm(){o.Jconfirm.prototype._updateContentMaxHeight=function(){this.$contentPane.css({"max-height":this.contentMaxHeight+"px"})}},confirmModal(e){var o={title:!1,content:"",icon:"fa fa-exclamation-circle",type:"orange",buttons:{confirm:{text:wpforms_ai_chat_element.btnYes,btnClass:"btn-confirm",keys:["enter"],action(){"function"==typeof e.onConfirm&&e.onConfirm()}},cancel:{text:wpforms_ai_chat_element.btnCancel,action(){"function"==typeof e.onCancel&&e.onCancel()}}}};t.confirm({...o,...e})}};return n})(document,window,jQuery);WPFormsAIModal.init();
@@ -0,0 +1,223 @@
/* global wpforms_ai_form_generator, wpf, wpforms_ai_chat_element, WPFormsBuilder */
/**
* @param strings.dismissed.installAddons
* @param strings.isLicenseActive
* @param strings.modules.main
* @param strings.templateCard.buttonTextInit
* @param strings.templateCard.imageSrc
* @param strings.liteConnectAllowed
* @param strings.liteConnectEnabled
* @param strings.liteConnectNotAllowed
* @param window.WPFormsAIFormGenerator
* @param wpforms_builder.is_ai_disabled
*/
// noinspection ES6ConvertVarToLetConst
/**
* WPForms AI Form Generator.
*
* @since 1.9.2
*/
// eslint-disable-next-line no-var
var WPFormsAIFormGenerator = window.WPFormsAIFormGenerator || ( function( document, window, $ ) {
/**
* Localized strings.
*
* @since 1.9.2
*
* @type {Object}
*/
const strings = wpforms_ai_form_generator;
/**
* Public functions and properties.
*
* @since 1.9.2
*
* @type {Object}
*/
const app = {
/**
* State data holder.
*
* @since 1.9.2
*
* @type {Object}
*/
state: {},
/**
* Main module.
*
* @since 1.9.2
*
* @type {Object}
*/
main: null,
/**
* The form preview module.
*
* @since 1.9.2
*
* @type {Object}
*/
preview: null,
/**
* Start the engine.
*
* @since 1.9.2
*/
init() {
if ( window.wpforms_builder?.is_ai_disabled || app.isLoaded ) {
return;
}
app.updateLocationUrl();
app.events();
app.isLoaded = true;
},
/**
* Events.
*
* @since 1.9.2
*/
events() {
$( document )
.on( 'wpformsSetupPanelBeforeInitTemplatesList', app.addTemplateCard );
$( '#wpforms-builder' )
.on( 'wpformsBuilderReady', app.maybeSaveForm )
.on( 'wpformsBuilderPanelLoaded', app.panelLoaded );
},
/**
* Panel loaded event.
*
* @since 1.9.2
*
* @param {Object} e Event object.
* @param {string} panel Panel name.
*/
panelLoaded( e, panel ) {
if ( panel !== 'setup' ) {
return;
}
// Load generator modules and run the main module.
Promise.all( [
import( strings.modules.main ),
import( strings.modules.preview ),
import( strings.modules.modals ),
] )
.then( ( [ moduleMain, modulePreview, moduleModals ] ) => {
app.main = moduleMain.default( app, $ );
app.preview = modulePreview.default( app, $ );
app.modals = moduleModals.default( app, $ );
// Run the main module.
app.main.init();
} );
},
/**
* Add the generator template card to the list.
*
* At this point, before the list is rendered, we can add our card.
* The card will be added to the top of the list.
* Event handlers will be attached later by the main module.
*
* @since 1.9.2
*/
addTemplateCard() {
if ( $( '#wpforms-template-generate' ).length ) {
return;
}
$( '#wpforms-setup-templates-list .list' ).prepend( app.renderTemplateCard() );
wpf.initTooltips( $( '#wpforms-template-generate .wpforms-template-buttons' ) );
},
/**
* Render the template card HTML.
*
* @since 1.9.2
*
* @return {string} The card markup.
*/
renderTemplateCard() { // eslint-disable-line complexity
const cardClass = window.wpforms_builder?.template_slug === 'generate' ? 'selected' : '';
let buttonAttr = '';
let buttonClass = ! Object.keys( strings.addonsData ).length || strings.dismissed.installAddons
? 'wpforms-template-generate'
: 'wpforms-template-generate-install-addons';
// In Lite, we should disable the button in the case Lite Connect is not allowed.
if ( ! strings.isPro && ! strings.liteConnectAllowed ) {
buttonClass += ' wpforms-inactive wpforms-help-tooltip wpforms-prevent-default';
buttonAttr = `data-tooltip-position="top" title="${ strings.templateCard.liteConnectNotAllowed }"`;
}
// In Lite, we should show the modal to enable Lite Connect if it is allowed.
if ( ! strings.isPro && ! strings.liteConnectEnabled && strings.liteConnectAllowed ) {
buttonClass += ' enable-lite-connect-modal wpforms-prevent-default';
}
return `
<div class="wpforms-template ${ cardClass }" id="wpforms-template-generate">
<div class="wpforms-template-thumbnail">
<div class="wpforms-template-thumbnail-placeholder">
<img src="${ strings.templateCard.imageSrc }" alt="${ strings.templateCard.name }" loading="lazy">
</div>
</div>
<div class="wpforms-template-name-wrap">
<h3 class="wpforms-template-name categories has-access favorite slug subcategories fields" data-categories="all,new" data-subcategories="" data-fields="" data-has-access="1" data-favorite="" data-slug="generate">
${ strings.templateCard.name }
</h3>
<span class="wpforms-badge wpforms-badge-sm wpforms-badge-inline wpforms-badge-purple wpforms-badge-rounded">${ strings.templateCard.new }</span>
</div>
<p class="wpforms-template-desc">
${ strings.templateCard.desc }
</p>
<div class="wpforms-template-buttons">
<a href="#" class="${ buttonClass } wpforms-btn wpforms-btn-md wpforms-btn-purple-dark" ${ buttonAttr }>
${ strings.templateCard.buttonTextInit }
</a>
</div>
</div>
`;
},
/**
* Save the form when the generated form opened.
*
* @since 1.9.2
*/
maybeSaveForm() {
// Only in case the generated form was used, we have a chat session in the localized vars.
if ( wpforms_ai_chat_element.forms?.chatHtml && ! wpf.getQueryString( 'newform' ) ) {
WPFormsBuilder.formSave( false );
}
},
/**
* Remove the session from URL.
*
* @since 1.9.2
*/
updateLocationUrl() {
history.replaceState( {}, null, wpf.updateQueryString( 'session', null ) );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsAIFormGenerator.init();
@@ -0,0 +1,23 @@
var WPFormsAIFormGenerator=window.WPFormsAIFormGenerator||((e,r,s)=>{let l=wpforms_ai_form_generator,o={state:{},main:null,preview:null,init(){r.wpforms_builder?.is_ai_disabled||o.isLoaded||(o.updateLocationUrl(),o.events(),o.isLoaded=!0)},events(){s(e).on("wpformsSetupPanelBeforeInitTemplatesList",o.addTemplateCard),s("#wpforms-builder").on("wpformsBuilderReady",o.maybeSaveForm).on("wpformsBuilderPanelLoaded",o.panelLoaded)},panelLoaded(e,t){"setup"===t&&Promise.all([import(l.modules.main),import(l.modules.preview),import(l.modules.modals)]).then(([e,t,a])=>{o.main=e.default(o,s),o.preview=t.default(o,s),o.modals=a.default(o,s),o.main.init()})},addTemplateCard(){s("#wpforms-template-generate").length||(s("#wpforms-setup-templates-list .list").prepend(o.renderTemplateCard()),wpf.initTooltips(s("#wpforms-template-generate .wpforms-template-buttons")))},renderTemplateCard(){var e="generate"===r.wpforms_builder?.template_slug?"selected":"";let t="",a=!Object.keys(l.addonsData).length||l.dismissed.installAddons?"wpforms-template-generate":"wpforms-template-generate-install-addons";return l.isPro||l.liteConnectAllowed||(a+=" wpforms-inactive wpforms-help-tooltip wpforms-prevent-default",t=`data-tooltip-position="top" title="${l.templateCard.liteConnectNotAllowed}"`),l.isPro||l.liteConnectEnabled||!l.liteConnectAllowed||(a+=" enable-lite-connect-modal wpforms-prevent-default"),`
<div class="wpforms-template ${e}" id="wpforms-template-generate">
<div class="wpforms-template-thumbnail">
<div class="wpforms-template-thumbnail-placeholder">
<img src="${l.templateCard.imageSrc}" alt="${l.templateCard.name}" loading="lazy">
</div>
</div>
<div class="wpforms-template-name-wrap">
<h3 class="wpforms-template-name categories has-access favorite slug subcategories fields" data-categories="all,new" data-subcategories="" data-fields="" data-has-access="1" data-favorite="" data-slug="generate">
${l.templateCard.name}
</h3>
<span class="wpforms-badge wpforms-badge-sm wpforms-badge-inline wpforms-badge-purple wpforms-badge-rounded">${l.templateCard.new}</span>
</div>
<p class="wpforms-template-desc">
${l.templateCard.desc}
</p>
<div class="wpforms-template-buttons">
<a href="#" class="${a} wpforms-btn wpforms-btn-md wpforms-btn-purple-dark" ${t}>
${l.templateCard.buttonTextInit}
</a>
</div>
</div>
`},maybeSaveForm(){wpforms_ai_chat_element.forms?.chatHtml&&!wpf.getQueryString("newform")&&WPFormsBuilder.formSave(!1)},updateLocationUrl(){history.replaceState({},null,wpf.updateQueryString("session",null))}};return o})(document,window,jQuery);WPFormsAIFormGenerator.init();
@@ -0,0 +1,563 @@
/* global wpf, wpforms_ai_form_generator, wpforms_ai_chat_element, WPFormsBuilder, wpforms_builder, WPFormsChallenge */
/**
* @param strings.panel.backToTemplates
* @param strings.panel.emptyStateDesc
* @param strings.panel.emptyStateTitle
* @param strings.templateCard.buttonTextContinue
* @param wpforms_ai_chat_element.forms.responseHistory
* @param wpforms_builder.template_slug
*/
/**
* The WPForms AI form generator app.
*
* Main module.
*
* @since 1.9.2
*
* @param {Object} generator The AI form generator.
* @param {Object} $ jQuery function.
*
* @return {Object} The main module object.
*/
export default function( generator, $ ) { // eslint-disable-line max-lines-per-function
/**
* Localized strings.
*
* @since 1.9.2
*
* @type {Object}
*/
const strings = wpforms_ai_form_generator;
/**
* The main module object.
*
* @since 1.9.2
*/
const main = {
/**
* DOM elements.
*
* @since 1.9.2
*/
el: {},
/**
* Init generator.
*
* @since 1.9.2
*/
init() {
main.initState();
main.initElementsCache();
main.initStateProxy();
// Magic, we just need to set the state property to `true` to add the panel to the DOM.
generator.state.panelAdd = true;
generator.preview.init();
generator.modals.init();
main.events();
},
/**
* Init generator state.
*
* @since 1.9.2
*/
initState() {
generator.state = {
formId: $( '#wpforms-builder-form' ).data( 'id' ),
panelAdd: false,
panelOpen: false,
chatStart: false,
aiResponse: null,
};
},
/**
* Events.
*
* @since 1.9.2
*/
events() {
// Setup panel events.
main.el.$setupPanel
.on( 'click', '.wpforms-template-generate', main.event.clickGenerateFormBtn )
.on( 'click', '.wpforms-template-generate-install-addons', generator.modals.openAddonsModal );
// Generator panel events.
main.el.$generatorPanel
.on( 'click', '.wpforms-btn-back-to-templates', main.event.clickBackToTemplatesBtn )
.on( 'click', '.wpforms-ai-chat-reload-link', main.event.reloadPage )
.on( 'click', '.wpforms-ai-chat-use-form', main.event.useForm );
// The Form Builder events
main.el.$builder
.on( 'wpformsPanelSwitch', main.event.panelSwitch );
// AI chat events.
main.el.$doc
.on( 'wpformsBuilderReady', main.maybeOpenPanel )
.on( 'wpformsAIChatBeforeAddAnswer', main.event.chatBeforeAddAnswer )
.on( 'wpformsAIChatAddedAnswer', main.event.chatAddedAnswer )
.on( 'wpformsAIChatAfterRefresh', main.event.chatAfterRefresh )
.on( 'wpformsAIChatSetActiveAnswer', main.event.chatSetActiveAnswer );
},
/**
* Init elements cache.
*
* @since 1.9.2
*/
initElementsCache() {
// Cache DOM elements.
main.el.$doc = $( document );
main.el.$builder = $( '#wpforms-builder' );
main.el.$builderToolbar = $( '#wpforms-builder .wpforms-toolbar' );
main.el.$templatesList = $( '#wpforms-setup-templates-list .list' ); // The templates list container.
main.el.$templateCard = $( '#wpforms-template-generate' ); // The generator template card.
main.el.$generatorPanel = $( '#wpforms-panel-ai-form' ); // The generator panel.
main.el.$setupPanel = $( '#wpforms-panel-setup' ); // The Setup panel.
main.el.$panelsContainer = $( '.wpforms-panels' ); // All panels container.
main.el.$allPanels = $( '.wpforms-panel' ); // All panels.
main.el.$chat = main.el.$generatorPanel.find( 'wpforms-ai-chat .wpforms-ai-chat' ); // The chat container.
},
/**
* Init state proxy.
*
* @since 1.9.2
*/
initStateProxy() {
generator.state = new Proxy( generator.state, {
set( state, key, value ) {
// Set the state property.
state[ key ] = value;
if ( typeof main.setStateHandler[ key ] !== 'function' ) {
return true;
}
// Run the set state property handler.
main.setStateHandler[ key ]( value );
// Debug log.
wpf.debug( 'Form Generator state changed:', key, '=', value );
return true;
},
} );
},
/**
* Event handlers
*
* @since 1.9.2
*/
event: {
/**
* Click on the `Generate Form` button.
*
* @since 1.9.2
*
* @param {Object} e Event object.
*/
clickGenerateFormBtn( e ) {
e.preventDefault();
if ( $( this ).hasClass( 'wpforms-prevent-default' ) ) {
return;
}
// Open the Form Generator panel.
generator.state.panelOpen = true;
},
/**
* Click on the `Back to Templates` button.
*
* @since 1.9.2
*/
clickBackToTemplatesBtn() {
// Close the Form Generator panel.
generator.state.panelOpen = false;
},
/**
* Before adding the answer to the chat.
*
* @since 1.9.2
*
* @param {Object} e Event object.
*/
chatBeforeAddAnswer( e ) {
// Store the AI response data in state.
generator.state.aiResponse = e.originalEvent.detail?.response;
generator.state.aiResponseHistory = generator.state.aiResponseHistory || {};
generator.state.aiResponseHistory[ generator.state.aiResponse?.responseId ] = generator.state.aiResponse;
},
/**
* The answer added to the chat.
*
* @since 1.9.2
*
* @param {Object} e Event object.
*/
chatAddedAnswer( e ) {
const chat = e.originalEvent.detail?.chat || {};
// Set chatStart state.
if ( chat?.sessionId && ! generator.state.chatStart ) {
generator.state.chatStart = true;
}
},
/**
* Refresh the chat triggered.
*
* @since 1.9.2
*/
chatAfterRefresh() {
generator.preview.clear();
},
/**
* Set active answer. Switch form preview to the active answer.
*
* @since 1.9.2
*
* @param {Object} e Event object.
*/
chatSetActiveAnswer( e ) {
generator.state.aiResponse = generator.state.aiResponseHistory[ e.originalEvent.detail?.responseId ];
},
/**
* Click on the "use this form" button.
*
* @since 1.9.2
*
* @param {Object} e Event object.
*/
useForm( e ) {
e?.preventDefault();
const $button = $( this );
const formId = generator.state.formId;
if ( ! formId || wpforms_builder.template_slug === 'generate' ) {
main.useFormAjax( $button );
} else {
generator.modals.openExistingFormModal( $button );
}
},
/**
* Click on the "reload" link.
*
* @since 1.9.2
*
* @param {Object} e Event object.
*/
reloadPage( e ) {
e?.preventDefault();
window.location = window.location + '&ai-form';
},
/**
* Switch the Form Builder panel.
*
* @since 1.9.2
*/
panelSwitch() {
generator.state.panelOpen = false;
},
},
/**
* Set state property handlers.
*
* Each handler runs when the appropriate state property was set.
* For example, when `panelAdd` state property was set, the `setStateHandler.panelAdd()` handler will run.
*
* @since 1.9.2
*/
setStateHandler: {
/**
* `panelAdd` state handler.
*
* When the value is `true`, the panel will be added to the DOM, otherwise removed.
*
* @since 1.9.2
*
* @param {boolean} value The state value.
*/
panelAdd( value ) {
// Remove the panel from DOM.
if ( ! value ) {
main.el.$generatorPanel?.remove();
return;
}
// The panel already added, no need to add again.
if ( main.el.$generatorPanel?.length ) {
return;
}
// Add panel to DOM.
main.el.$panelsContainer.append( main.render.generatorPanel() );
// Cache elements.
main.el.$generatorPanel = $( '#wpforms-panel-ai-form' );
main.el.$chat = main.el.$generatorPanel.find( 'wpforms-ai-chat .wpforms-ai-chat' );
},
/**
* Panel open state handler.
*
* @since 1.9.2
*
* @param {boolean} value The state value.
*/
panelOpen( value ) {
main.el.$generatorPanel.toggleClass( 'active', value );
main.el.$templateCard.addClass( 'selected' );
main.setToolbarState( value );
// Freeze/unfreeze the Challenge.
window.WPFormsChallenge?.core.freezeChallenge( value, strings.misc.frozenChallengeTooltip );
$( 'body' ).toggleClass( 'wpforms-ai-form-generator-active', value );
if (
generator.state.aiResponseHistory ||
! wpforms_ai_chat_element.forms.responseHistory
) {
return;
}
// Update the response history if it exists.
generator.state.aiResponseHistory = wpforms_ai_chat_element.forms.responseHistory;
const $activeResponse = main.el.$chat.find( '.wpforms-chat-item-answer.active' );
const activeResponseId = $activeResponse.data( 'response-id' );
generator.state.aiResponse = generator.state.aiResponseHistory[ activeResponseId ];
// Scroll to the active response.
$activeResponse[ 0 ].scrollIntoView( { behavior: 'smooth', block: 'end' } );
},
/**
* Chat start state handler.
*
* @since 1.9.2
*
* @param {boolean} value The state value.
*/
chatStart( value ) {
if ( ! value ) {
return;
}
// Update the generator template card button text.
main.el.$templateCard
.addClass( 'selected' )
.find( '.wpforms-template-generate' )
.text( strings.templateCard.buttonTextContinue );
},
/**
* AI response state handler.
*
* @since 1.9.2
*
* @param {Object} response The response data.
*/
aiResponse( response ) {
if ( ! response ) {
return;
}
// Update the preview.
generator.preview.update();
},
/**
* Is the form preview update in progress.
*
* @since 1.9.2
*
* @param {boolean} value Flag value.
*/
isPreviewUpdate( value ) {
main.el.$chat.toggleClass( 'wpforms-ai-chat-inactive', value );
},
},
/**
* HTML renderers.
*
* @since 1.9.2
*/
render: {
/**
* Render generator panel HTML.
*
* @since 1.9.2
*
* @return {string} The panel markup.
*/
generatorPanel() {
return `
<div class="wpforms-panel wpforms-panel-fields" id="wpforms-panel-ai-form">
<div class="wpforms-panel-sidebar-content">
<div class="wpforms-panel-sidebar">
<div class="wpforms-panel-sidebar-header">
<button type="button" class="wpforms-btn-back-to-templates" aria-label="${ strings.panel.backToTemplates }">
${ strings.panel.backToTemplates }
</button>
</div>
<wpforms-ai-chat mode="forms" class="wpforms-ai-chat-blue"/>
</div>
<div class="wpforms-panel-content-wrap">
<div class="wpforms-panel-content">
<div class="wpforms-panel-empty-state">
<h4>${ strings.panel.emptyStateTitle }</h4>
<p>${ strings.panel.emptyStateDesc }</p>
</div>
</div>
</div>
</div>
</div>
`;
},
},
/**
* Maybe open the form generator panel.
*
* @since 1.9.2
*/
maybeOpenPanel() {
// Open the panel only if the `ai-form` query string parameter exists.
if ( ! window.location.search.includes( '&ai-form' ) ) {
return;
}
// Remove the query string parameter from the URL.
history.replaceState( {}, null, wpf.updateQueryString( 'ai-form', null ) );
// Open the LiteConnect modal if it is not enabled.
const $buttonLiteConnect = $( '.wpforms-template-generate.enable-lite-connect-modal' );
if ( $buttonLiteConnect.length ) {
setTimeout(
function() {
$buttonLiteConnect.trigger( 'click' );
},
0
);
return;
}
// Open the panel if all addons are installed OR the modal is dismissed.
if ( ! Object.keys( strings.addonsData ).length || strings.dismissed.installAddons ) {
generator.state.panelOpen = true;
return;
}
// Open the addons install modal.
generator.modals.openAddonsModal( null );
},
/**
* The "Use this form" ajax call.
*
* @since 1.9.2
*
* @param {jQuery} $button Button element.
*/
useFormAjax( $button ) {
const sessionId = $button.closest( '.wpforms-ai-chat' ).data( 'session-id' );
const responseId = $button.closest( '.wpforms-chat-item' ).data( 'response-id' );
WPFormsBuilder.showLoadingOverlay();
// Rate the response.
main.getChatElement()?.wpformsAiApi.rate( true, responseId );
// Do not display the alert about unsaved changes.
WPFormsBuilder.setCloseConfirmation( false );
const data = {
action: 'wpforms_use_ai_form',
nonce: strings.nonce,
formId: generator.state.formId,
formData: generator.state.aiResponseHistory[ responseId ],
sessionId,
chatHtml: $button.closest( 'wpforms-ai-chat' ).html(),
responseHistory: generator.state.aiResponseHistory,
};
generator.preview.closeTooltips();
$.post( strings.ajaxUrl, data )
.done( function( res ) {
if ( ! res.success ) {
wpf.debug( 'Form Generator AJAX error:', res.data.error ?? res.data );
return;
}
const newForm = ! data.formId ? '&newform=1' : '';
if ( ! window.WPFormsChallenge ) {
window.location.assign( res.data.redirect + newForm );
return;
}
// When the Challenge is active, we need to resume it and continue the steps.
WPFormsChallenge.core.resumeChallengeAndExec( {}, () => {
WPFormsChallenge.core.stepCompleted( 2 )
.done( () => {
window.location.assign( res.data.redirect + newForm );
} );
} );
} )
.fail( function( xhr ) {
wpf.debug( 'Form Generator AJAX error:', xhr.responseText ?? xhr.statusText );
} );
},
/**
* Set the Builder's toolbar state.
*
* @since 1.9.2
*
* @param {boolean} isEmpty The toolbar is empty.
*/
setToolbarState( isEmpty ) {
main.el.$builderToolbar.toggleClass( 'empty', isEmpty );
main.el.$builderToolbar.find( '.js-wpforms-help span' ).toggleClass( 'screen-reader-text', ! isEmpty );
},
/**
* Get the AI chat element.
*
* @since 1.9.2
*
* @return {HTMLElement} The chat element.
*/
getChatElement() {
return main.el.$chat.parent()[ 0 ];
},
};
return main;
}
@@ -0,0 +1,22 @@
export default function(o,n){let r=wpforms_ai_form_generator,l={el:{},init(){l.initState(),l.initElementsCache(),l.initStateProxy(),o.state.panelAdd=!0,o.preview.init(),o.modals.init(),l.events()},initState(){o.state={formId:n("#wpforms-builder-form").data("id"),panelAdd:!1,panelOpen:!1,chatStart:!1,aiResponse:null}},events(){l.el.$setupPanel.on("click",".wpforms-template-generate",l.event.clickGenerateFormBtn).on("click",".wpforms-template-generate-install-addons",o.modals.openAddonsModal),l.el.$generatorPanel.on("click",".wpforms-btn-back-to-templates",l.event.clickBackToTemplatesBtn).on("click",".wpforms-ai-chat-reload-link",l.event.reloadPage).on("click",".wpforms-ai-chat-use-form",l.event.useForm),l.el.$builder.on("wpformsPanelSwitch",l.event.panelSwitch),l.el.$doc.on("wpformsBuilderReady",l.maybeOpenPanel).on("wpformsAIChatBeforeAddAnswer",l.event.chatBeforeAddAnswer).on("wpformsAIChatAddedAnswer",l.event.chatAddedAnswer).on("wpformsAIChatAfterRefresh",l.event.chatAfterRefresh).on("wpformsAIChatSetActiveAnswer",l.event.chatSetActiveAnswer)},initElementsCache(){l.el.$doc=n(document),l.el.$builder=n("#wpforms-builder"),l.el.$builderToolbar=n("#wpforms-builder .wpforms-toolbar"),l.el.$templatesList=n("#wpforms-setup-templates-list .list"),l.el.$templateCard=n("#wpforms-template-generate"),l.el.$generatorPanel=n("#wpforms-panel-ai-form"),l.el.$setupPanel=n("#wpforms-panel-setup"),l.el.$panelsContainer=n(".wpforms-panels"),l.el.$allPanels=n(".wpforms-panel"),l.el.$chat=l.el.$generatorPanel.find("wpforms-ai-chat .wpforms-ai-chat")},initStateProxy(){o.state=new Proxy(o.state,{set(e,t,a){return e[t]=a,"function"==typeof l.setStateHandler[t]&&(l.setStateHandler[t](a),wpf.debug("Form Generator state changed:",t,"=",a)),!0}})},event:{clickGenerateFormBtn(e){e.preventDefault(),n(this).hasClass("wpforms-prevent-default")||(o.state.panelOpen=!0)},clickBackToTemplatesBtn(){o.state.panelOpen=!1},chatBeforeAddAnswer(e){o.state.aiResponse=e.originalEvent.detail?.response,o.state.aiResponseHistory=o.state.aiResponseHistory||{},o.state.aiResponseHistory[o.state.aiResponse?.responseId]=o.state.aiResponse},chatAddedAnswer(e){(e.originalEvent.detail?.chat||{})?.sessionId&&!o.state.chatStart&&(o.state.chatStart=!0)},chatAfterRefresh(){o.preview.clear()},chatSetActiveAnswer(e){o.state.aiResponse=o.state.aiResponseHistory[e.originalEvent.detail?.responseId]},useForm(e){e?.preventDefault();e=n(this);o.state.formId&&"generate"!==wpforms_builder.template_slug?o.modals.openExistingFormModal(e):l.useFormAjax(e)},reloadPage(e){e?.preventDefault(),window.location=window.location+"&ai-form"},panelSwitch(){o.state.panelOpen=!1}},setStateHandler:{panelAdd(e){e?l.el.$generatorPanel?.length||(l.el.$panelsContainer.append(l.render.generatorPanel()),l.el.$generatorPanel=n("#wpforms-panel-ai-form"),l.el.$chat=l.el.$generatorPanel.find("wpforms-ai-chat .wpforms-ai-chat")):l.el.$generatorPanel?.remove()},panelOpen(e){var t;l.el.$generatorPanel.toggleClass("active",e),l.el.$templateCard.addClass("selected"),l.setToolbarState(e),window.WPFormsChallenge?.core.freezeChallenge(e,r.misc.frozenChallengeTooltip),n("body").toggleClass("wpforms-ai-form-generator-active",e),!o.state.aiResponseHistory&&wpforms_ai_chat_element.forms.responseHistory&&(o.state.aiResponseHistory=wpforms_ai_chat_element.forms.responseHistory,t=(e=l.el.$chat.find(".wpforms-chat-item-answer.active")).data("response-id"),o.state.aiResponse=o.state.aiResponseHistory[t],e[0].scrollIntoView({behavior:"smooth",block:"end"}))},chatStart(e){e&&l.el.$templateCard.addClass("selected").find(".wpforms-template-generate").text(r.templateCard.buttonTextContinue)},aiResponse(e){e&&o.preview.update()},isPreviewUpdate(e){l.el.$chat.toggleClass("wpforms-ai-chat-inactive",e)}},render:{generatorPanel(){return`
<div class="wpforms-panel wpforms-panel-fields" id="wpforms-panel-ai-form">
<div class="wpforms-panel-sidebar-content">
<div class="wpforms-panel-sidebar">
<div class="wpforms-panel-sidebar-header">
<button type="button" class="wpforms-btn-back-to-templates" aria-label="${r.panel.backToTemplates}">
${r.panel.backToTemplates}
</button>
</div>
<wpforms-ai-chat mode="forms" class="wpforms-ai-chat-blue"/>
</div>
<div class="wpforms-panel-content-wrap">
<div class="wpforms-panel-content">
<div class="wpforms-panel-empty-state">
<h4>${r.panel.emptyStateTitle}</h4>
<p>${r.panel.emptyStateDesc}</p>
</div>
</div>
</div>
</div>
</div>
`}},maybeOpenPanel(){if(window.location.search.includes("&ai-form")){history.replaceState({},null,wpf.updateQueryString("ai-form",null));let e=n(".wpforms-template-generate.enable-lite-connect-modal");e.length?setTimeout(function(){e.trigger("click")},0):!Object.keys(r.addonsData).length||r.dismissed.installAddons?o.state.panelOpen=!0:o.modals.openAddonsModal(null)}},useFormAjax(e){var t=e.closest(".wpforms-ai-chat").data("session-id"),a=e.closest(".wpforms-chat-item").data("response-id");WPFormsBuilder.showLoadingOverlay(),l.getChatElement()?.wpformsAiApi.rate(!0,a),WPFormsBuilder.setCloseConfirmation(!1);let s={action:"wpforms_use_ai_form",nonce:r.nonce,formId:o.state.formId,formData:o.state.aiResponseHistory[a],sessionId:t,chatHtml:e.closest("wpforms-ai-chat").html(),responseHistory:o.state.aiResponseHistory};o.preview.closeTooltips(),n.post(r.ajaxUrl,s).done(function(t){if(t.success){let e=s.formId?"":"&newform=1";window.WPFormsChallenge?WPFormsChallenge.core.resumeChallengeAndExec({},()=>{WPFormsChallenge.core.stepCompleted(2).done(()=>{window.location.assign(t.data.redirect+e)})}):window.location.assign(t.data.redirect+e)}else wpf.debug("Form Generator AJAX error:",t.data.error??t.data)}).fail(function(e){wpf.debug("Form Generator AJAX error:",e.responseText??e.statusText)})},setToolbarState(e){l.el.$builderToolbar.toggleClass("empty",e),l.el.$builderToolbar.find(".js-wpforms-help span").toggleClass("screen-reader-text",!e)},getChatElement(){return l.el.$chat.parent()[0]}};return l}
@@ -0,0 +1,389 @@
/* global wpforms_ai_form_generator, wpf, WPFormsBuilder, wpforms_builder */
/**
* @param strings.addonsAction
* @param strings.addonsData
* @param strings.addons.installTitle
* @param strings.addons.installContent
* @param strings.addons.activateContent
* @param strings.addons.installButton
* @param strings.addons.installConfirmButton
* @param strings.addons.activateConfirmButton
* @param strings.addons.cancelButton
* @param strings.addons.dontShow
* @param strings.addons.dismissErrorTitle
* @param strings.addons.dismissError
* @param strings.addons.addonsInstalledTitle
* @param strings.addons.addonsActivatedTitle
* @param strings.addons.addonsInstalledContent
* @param strings.addons.okay
* @param strings.addons.addonsInstallErrorTitle
* @param strings.addons.addonsActivateErrorTitle
* @param strings.addons.addonsInstallError
* @param strings.addons.addonsInstallErrorNetwork
* @param strings.adminNonce
* @param strings.misc.warningExistingForm
* @param this.$$confirm
* @param this.$$cancel
*/
/**
* The WPForms AI form generator app.
*
* Modal windows' module.
*
* @since 1.9.2
*
* @param {Object} generator The AI form generator.
* @param {Object} $ jQuery function.
*
* @return {Object} The preview module object.
*/
export default function( generator, $ ) { // eslint-disable-line max-lines-per-function
/**
* Localized strings.
*
* @since 1.9.2
*
* @type {Object}
*/
const strings = wpforms_ai_form_generator;
/**
* The preview module object.
*
* @since 1.9.2
*/
const modals = {
/**
* DOM elements.
*
* @since 1.9.2
*/
el: {},
/**
* AJAX error debug string.
*
* @since 1.9.2
*/
ajaxError: 'Form Generator AJAX error:',
/**
* Init generator.
*
* @since 1.9.2
*/
init() {
modals.el.$doc = $( document );
modals.el.$templateCard = $( '#wpforms-template-generate' );
modals.events();
},
/**
* Register events.
*/
events() {
modals.el.$doc.on( 'change', '.wpforms-ai-forms-install-addons-modal-dismiss', modals.dismissAddonsModal );
},
/**
* Open the addons modal.
*
* @since 1.9.2
*
* @param {Object} e Event object.
*/
openAddonsModal( e ) { // eslint-disable-line max-lines-per-function
e?.preventDefault();
const spinner = '<i class="wpforms-loading-spinner wpforms-loading-white wpforms-loading-inline"></i>';
const isInstall = strings.addonsAction === 'install';
const content = isInstall ? strings.addons.installContent : strings.addons.activateContent;
const options = {
title: strings.addons.installTitle,
content,
type: 'purple',
icon: 'fa fa-info-circle',
buttons: {
confirm: {
text: isInstall ? strings.addons.installConfirmButton : strings.addons.activateConfirmButton,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
const label = isInstall ? strings.addons.installing : strings.addons.activating;
this.$$confirm.prop( 'disabled', true ).html( spinner + label );
this.$$cancel.prop( 'disabled', true );
modals.installAddonsAjax( this );
return false;
},
},
cancel: {
text: strings.addons.cancelButton,
keys: [ 'esc' ],
btnClass: 'btn-cancel',
action() {
modals.updateGenerateFormButton( false );
// Open the Form Generator panel.
setTimeout( () => {
generator.state.panelOpen = true;
}, 250 );
},
},
},
onOpenBefore() {
// Add the checkbox to the modal.
const dontShowAgain = `
<label class="jconfirm-checkbox">
<input type="checkbox" class="jconfirm-checkbox-input wpforms-ai-forms-install-addons-modal-dismiss">
${ strings.addons.dontShow }
</label>
`;
this.$body
.addClass( 'wpforms-ai-forms-install-addons-modal' )
.find( '.jconfirm-buttons' )
.after( dontShowAgain );
},
};
$.confirm( options );
},
/**
* Install required addons AJAX.
*
* @since 1.9.2
*
* @param {Object} previousModal Previous modal instance.
*/
installAddonsAjax( previousModal ) { // eslint-disable-line max-lines-per-function
let chain = null;
let errorDisplayed = false;
const postDone = function( res ) {
if ( ! res.success ) {
wpf.debug( modals.ajaxError, res.data.error ?? res.data );
}
if ( ! res.success && ! errorDisplayed ) {
errorDisplayed = true;
modals.openErrorModal( {
title: strings.addonsAction === 'install' ? strings.addons.addonsInstallErrorTitle : strings.addons.addonsActivateErrorTitle,
content: strings.addons.addonsInstallError,
} );
}
};
const postFail = function( xhr ) {
if ( errorDisplayed ) {
return;
}
const error = xhr.responseText || strings.addons.addonsInstallErrorNetwork;
let content = strings.addons.addonsInstallError;
content += error && error !== 'error' ? '<br>' + error : '';
wpf.debug( modals.ajaxError, content );
modals.openErrorModal( {
title: strings.addonsAction === 'install' ? strings.addons.addonsInstallErrorTitle : strings.addons.addonsActivateErrorTitle,
content,
} );
errorDisplayed = true;
};
// Do not display the alert about unsaved changes.
WPFormsBuilder.setCloseConfirmation( false );
// Loop through all addons and make a chained AJAX calls.
for ( const slug in strings.addonsData ) {
const url = strings.addonsData[ slug ]?.url;
const data = {
action: url ? 'wpforms_install_addon' : 'wpforms_activate_addon',
nonce : strings.adminNonce,
plugin: url ? url : strings.addonsData[ slug ]?.path,
type : 'addon',
};
if ( chain === null ) {
chain = $.post( strings.ajaxUrl, data, postDone );
} else {
chain = chain.then( () => {
return $.post( strings.ajaxUrl, data, postDone );
} );
}
chain.fail( postFail );
}
// Open the Addons Installed modal after the last AJAX call.
chain
.then( () => {
if ( ! errorDisplayed ) {
modals.openAddonsInstalledModal();
}
} )
.always( () => {
previousModal.close();
modals.updateGenerateFormButton( false );
} );
},
/**
* Dismiss or de-dismiss element.
*
* @since 1.9.2
*/
dismissAddonsModal() {
const $checkbox = $( this );
const isChecked = $checkbox.prop( 'checked' );
const data = {
action: 'wpforms_dismiss_ai_form',
nonce: strings.nonce,
element: 'install-addons-modal',
dismiss: isChecked,
};
modals.updateGenerateFormButton( ! isChecked );
$.post( strings.ajaxUrl, data )
.done( function( res ) {
if ( res.success ) {
return;
}
modals.openErrorModal( {
title: strings.addons.dismissErrorTitle,
content: strings.addons.dismissError,
} );
wpf.debug( modals.ajaxError, res.data.error ?? res.data );
} )
.fail( function( xhr ) {
modals.openErrorModal( {
title: strings.addons.dismissErrorTitle,
content: strings.addons.dismissError + '<br>' + strings.addons.addonsInstallErrorNetwork,
} );
wpf.debug( modals.ajaxError, xhr.responseText ?? xhr.statusText );
} );
},
/**
* Update the Generate Form button to enable/disable install addons modal window.
*
* @since 1.9.2
*
* @param {boolean} shouldInstallAddons Should open install addons modal.
*/
updateGenerateFormButton( shouldInstallAddons ) {
if ( shouldInstallAddons ) {
$( '.wpforms-template-generate' )
.removeClass( 'wpforms-template-generate' )
.addClass( 'wpforms-template-generate-install-addons' );
} else {
$( '.wpforms-template-generate-install-addons' )
.removeClass( 'wpforms-template-generate-install-addons' )
.addClass( 'wpforms-template-generate' );
}
},
/**
* Open the Addons Installed modal.
*
* @since 1.9.2
*/
openAddonsInstalledModal() {
const options = {
title: strings.addonsAction === 'install' ? strings.addons.addonsInstalledTitle : strings.addons.addonsActivatedTitle,
content: strings.addons.addonsInstalledContent,
icon: 'fa fa-check-circle',
type: 'green',
buttons: {
confirm: {
text: strings.addons.okay,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
WPFormsBuilder.showLoadingOverlay();
window.location = window.location + '&ai-form';
},
},
},
onOpenBefore() {
this.$body
.addClass( 'wpforms-ai-forms-addons-installed-modal' );
},
};
$.confirm( options );
},
/**
* Warning for the existing form.
*
* @since 1.9.2
*
* @param {jQuery} $button The "Use This Form" button.
*/
openExistingFormModal( $button ) {
$.confirm( {
title: wpforms_builder.heads_up,
content: strings.misc.warningExistingForm,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
generator.main.useFormAjax( $button );
},
},
cancel: {
text: wpforms_builder.cancel,
},
},
} );
},
/**
* Open the error modal.
*
* @since 1.9.2
*
* @param {Object} args Arguments.
*/
openErrorModal( args ) {
const options = {
title: args.title ?? false,
content: args.content ?? false,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: strings.addons.okay,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
};
$.confirm( options );
},
};
return modals;
}
@@ -0,0 +1,6 @@
export default function(a,r){let l=wpforms_ai_form_generator,i={el:{},ajaxError:"Form Generator AJAX error:",init(){i.el.$doc=r(document),i.el.$templateCard=r("#wpforms-template-generate"),i.events()},events(){i.el.$doc.on("change",".wpforms-ai-forms-install-addons-modal-dismiss",i.dismissAddonsModal)},openAddonsModal(n){n?.preventDefault();let o="install"===l.addonsAction;n=o?l.addons.installContent:l.addons.activateContent,n={title:l.addons.installTitle,content:n,type:"purple",icon:"fa fa-info-circle",buttons:{confirm:{text:o?l.addons.installConfirmButton:l.addons.activateConfirmButton,btnClass:"btn-confirm",keys:["enter"],action(){var n=o?l.addons.installing:l.addons.activating;return this.$$confirm.prop("disabled",!0).html('<i class="wpforms-loading-spinner wpforms-loading-white wpforms-loading-inline"></i>'+n),this.$$cancel.prop("disabled",!0),i.installAddonsAjax(this),!1}},cancel:{text:l.addons.cancelButton,keys:["esc"],btnClass:"btn-cancel",action(){i.updateGenerateFormButton(!1),setTimeout(()=>{a.state.panelOpen=!0},250)}}},onOpenBefore(){var n=`
<label class="jconfirm-checkbox">
<input type="checkbox" class="jconfirm-checkbox-input wpforms-ai-forms-install-addons-modal-dismiss">
${l.addons.dontShow}
</label>
`;this.$body.addClass("wpforms-ai-forms-install-addons-modal").find(".jconfirm-buttons").after(n)}};r.confirm(n)},installAddonsAjax(n){function o(n){n.success||wpf.debug(i.ajaxError,n.data.error??n.data),n.success||t||(t=!0,i.openErrorModal({title:"install"===l.addonsAction?l.addons.addonsInstallErrorTitle:l.addons.addonsActivateErrorTitle,content:l.addons.addonsInstallError}))}let a=null,t=!1;function e(n){var o;t||(n=n.responseText||l.addons.addonsInstallErrorNetwork,o=l.addons.addonsInstallError,o+=n&&"error"!==n?"<br>"+n:"",wpf.debug(i.ajaxError,o),i.openErrorModal({title:"install"===l.addonsAction?l.addons.addonsInstallErrorTitle:l.addons.addonsActivateErrorTitle,content:o}),t=!0)}for(var s in WPFormsBuilder.setCloseConfirmation(!1),l.addonsData){var d=l.addonsData[s]?.url;let n={action:d?"wpforms_install_addon":"wpforms_activate_addon",nonce:l.adminNonce,plugin:d||l.addonsData[s]?.path,type:"addon"};(a=null===a?r.post(l.ajaxUrl,n,o):a.then(()=>r.post(l.ajaxUrl,n,o))).fail(e)}a.then(()=>{t||i.openAddonsInstalledModal()}).always(()=>{n.close(),i.updateGenerateFormButton(!1)})},dismissAddonsModal(){var n=r(this).prop("checked"),o={action:"wpforms_dismiss_ai_form",nonce:l.nonce,element:"install-addons-modal",dismiss:n};i.updateGenerateFormButton(!n),r.post(l.ajaxUrl,o).done(function(n){n.success||(i.openErrorModal({title:l.addons.dismissErrorTitle,content:l.addons.dismissError}),wpf.debug(i.ajaxError,n.data.error??n.data))}).fail(function(n){i.openErrorModal({title:l.addons.dismissErrorTitle,content:l.addons.dismissError+"<br>"+l.addons.addonsInstallErrorNetwork}),wpf.debug(i.ajaxError,n.responseText??n.statusText)})},updateGenerateFormButton(n){n?r(".wpforms-template-generate").removeClass("wpforms-template-generate").addClass("wpforms-template-generate-install-addons"):r(".wpforms-template-generate-install-addons").removeClass("wpforms-template-generate-install-addons").addClass("wpforms-template-generate")},openAddonsInstalledModal(){var n={title:"install"===l.addonsAction?l.addons.addonsInstalledTitle:l.addons.addonsActivatedTitle,content:l.addons.addonsInstalledContent,icon:"fa fa-check-circle",type:"green",buttons:{confirm:{text:l.addons.okay,btnClass:"btn-confirm",keys:["enter"],action(){WPFormsBuilder.showLoadingOverlay(),window.location=window.location+"&ai-form"}}},onOpenBefore(){this.$body.addClass("wpforms-ai-forms-addons-installed-modal")}};r.confirm(n)},openExistingFormModal(n){r.confirm({title:wpforms_builder.heads_up,content:l.misc.warningExistingForm,icon:"fa fa-exclamation-circle",type:"orange",buttons:{confirm:{text:wpforms_builder.ok,btnClass:"btn-confirm",keys:["enter"],action(){a.main.useFormAjax(n)}},cancel:{text:wpforms_builder.cancel}}})},openErrorModal(n){n={title:n.title??!1,content:n.content??!1,icon:"fa fa-exclamation-circle",type:"red",buttons:{confirm:{text:l.addons.okay,btnClass:"btn-confirm",keys:["enter"]}}};r.confirm(n)}};return i}
@@ -0,0 +1,410 @@
/* global wpforms_ai_form_generator, wpf, wpforms_addons */
/**
* @param strings.dismissed.previewNotice
* @param strings.licenseType
* @param strings.previewNotice.btnUpgrade
* @param strings.previewNotice.msgUpgrade
* @param wpforms_ai_form_generator.addonFields
*/
/**
* The WPForms AI form generator app.
*
* Form preview module.
*
* @since 1.9.2
*
* @param {Object} generator The AI form generator.
* @param {Object} $ jQuery function.
*
* @return {Object} The preview module object.
*/
export default function( generator, $ ) { // eslint-disable-line max-lines-per-function
/**
* Localized strings.
*
* @since 1.9.2
*
* @type {Object}
*/
const strings = wpforms_ai_form_generator;
/**
* The preview module object.
*
* @since 1.9.2
*/
const preview = {
/**
* DOM elements.
*
* @since 1.9.2
*/
el: {},
/**
* Mouse coordinates.
*
* @since 1.9.2
*/
mouse: {},
/**
* Init module.
*
* @since 1.9.2
*/
init() {
preview.el.$contentWrap = generator.main.el.$generatorPanel.find( '.wpforms-panel-content-wrap' );
preview.el.$content = preview.el.$contentWrap.find( '.wpforms-panel-content' );
preview.el.$emptyState = preview.el.$content.find( '.wpforms-panel-empty-state' );
preview.events();
},
/**
* Preview events.
*
* @since 1.9.2
*/
events() {
// Track mouse coordinates.
$( document ).on( 'mousemove', ( e ) => {
preview.mouse.x = e.pageX;
preview.mouse.y = e.pageY;
} );
preview.el.$contentWrap.on( 'scroll', preview.closeTooltips );
},
/**
* Update the preview according to the response stored in the generator state.
*
* @since 1.9.2
*/
update() { // eslint-disable-line complexity
/**
* @param response.fieldsOrder.length
* @param response.settings.submit_text
*/
const response = generator.state.aiResponse;
if ( ! response || ! response.fields ) {
return;
}
// Set the preview update flag.
generator.state.isPreviewUpdate = true;
// Reset preview fields. Here we will store the field ids that where added to the preview.
generator.state.previewFields = [];
// Remove existing fields and hide empty state.
preview.clear( false );
// Display the form header.
preview.displayHeader( response );
for ( const key in response.fieldsOrder ) {
const fieldId = response.fieldsOrder[ key ];
preview.field( response.fields[ fieldId ], key );
}
// Add submit button.
if ( response.fieldsOrder?.length ) {
preview.displaySubmit( response.settings?.submit_text || strings.panel.submitButton );
return;
}
// Show the empty state if there are no fields.
preview.el.$emptyState.removeClass( 'wpforms-hidden-strict' );
generator.state.isPreviewUpdate = false;
},
/**
* A single field preview.
*
* @since 1.9.2
*
* @param {Object} fieldSettings Field settings.
* @param {number} key Field key.
*/
async field( fieldSettings, key ) {
// Add a field placeholder to the preview.
const html = `
<div id="wpforms-generator-field-${ fieldSettings.id ?? '' }" class="wpforms-ai-form-generator-preview-field">
<div class="placeholder"></div>
<div class="wpforms-field wpforms-field-${ fieldSettings.type ?? '' }"></div>
</div>
`;
preview.el.$content.append( html );
const data = {
action: 'wpforms_get_ai_form_field_preview',
nonce: strings.nonce,
field: fieldSettings,
};
// Delay the AJAX request to simulate one-by-one field loading.
await preview.delay( 300 * key );
// Field preview AJAX request.
$.post( strings.ajaxUrl, data )
.done( function( res ) {
if ( ! res.success ) {
wpf.debug( 'Form Generator AJAX error:', res.data.error ?? res.data );
return;
}
preview.displayField( res.data ?? '', fieldSettings );
} )
.fail( function( xhr ) {
wpf.debug( 'Form Generator AJAX error:', xhr.responseText ?? xhr.statusText );
} );
},
/**
* Display the field in his placeholder.
*
* @since 1.9.2
*
* @param {string} fieldHtml Field HTML.
* @param {Object} fieldSettings Field settings.
*/
displayField( fieldHtml, fieldSettings ) {
if ( ! fieldSettings.id && fieldSettings.id !== 0 ) {
return;
}
const $fieldBlock = preview.el.$content.find( '#wpforms-generator-field-' + fieldSettings.id );
const $field = $fieldBlock.find( '.wpforms-field' );
const $placeholder = $fieldBlock.find( '.placeholder' );
$placeholder
.addClass( 'fade-out' );
$field
.html( fieldHtml ?? '' )
.addClass( 'fade-in' )
.toggleClass( 'wpforms-hidden', ! fieldHtml ) // Hide preview if the field is empty.
.toggleClass( 'required', fieldSettings.required === '1' ) // Display the required field mark (asterisk) on the field label.
.toggleClass( 'label_empty', ! fieldSettings.label ); // The field with an empty label.
preview.initTooltip( $field );
preview.initPageBreak( $field, fieldSettings );
generator.state.previewFields.push( fieldSettings.id );
// Detect whether all the fields are loaded.
if ( generator.state.previewFields.length !== Object.keys( generator.state.aiResponse?.fields ).length ) {
return;
}
generator.state.isPreviewUpdate = false;
},
/**
* Get addons used in AI response.
*
* @since 1.9.4
*
* @return {string} Addons used in the response.
*/
getAddonsUsedInResponse() { // eslint-disable-line complexity
const response = generator.state.aiResponse;
if ( ! response || ! response.fields ) {
return '';
}
const addons = [];
for ( const key in response.fields ) {
const addon = wpforms_ai_form_generator.addonFields[ response.fields[ key ].type ];
if ( ! addon ) {
continue;
}
const addonName = wpforms_addons[ 'wpforms-' + addon ]?.title.replace( strings.addons.addon, '' ).trim();
if ( ! addonName || addons.includes( addonName ) ) {
continue;
}
addons.push( addonName );
}
if ( ! addons.length ) {
return '';
}
let lastAddon = addons.pop();
lastAddon += ' ' + strings.addons.addon;
return addons.length ? addons.join( ', ' ) + ', ' + strings.addons.and + ' ' + lastAddon : lastAddon;
},
/**
* Init the page breaks.
*
* @since 1.9.2
*
* @param {jQuery} $field Field jQuery object.
* @param {Object} fieldSettings Field settings.
*/
initPageBreak( $field, fieldSettings ) {
if ( fieldSettings.type === 'pagebreak' && ! [ 'top', 'bottom' ].includes( fieldSettings.position ) ) {
$field.addClass( 'wpforms-pagebreak-normal' );
}
},
/**
* Init the preview tooltip.
*
* @since 1.9.2
*
* @param {jQuery} $field Field jQuery object.
*/
initTooltip( $field ) {
const width = 260;
const args = {
content: strings.panel.tooltipTitle + '<br>' + strings.panel.tooltipText,
trigger: 'manual',
interactive: true,
animationDuration: 100,
delay: 0,
side: [ 'top' ],
contentAsHTML: true,
functionPosition: ( instance, helper, position ) => {
// Set the tooltip position based on the mouse coordinates.
position.coord.top = preview.mouse.y - 57;
position.coord.left = preview.mouse.x - ( width / 2 );
return position;
},
};
// Initialize.
$field.tooltipster( args );
preview.toggleTooltipOnClick( $field );
},
/**
* Toggle the preview tooltip on click.
*
* @since 1.9.2
*
* @param {jQuery} $field Field jQuery object.
*/
toggleTooltipOnClick( $field ) {
$field.on( 'click', () => {
// Close opened tooltips on other fields.
preview.closeTooltips();
const status = $field.tooltipster( 'status' );
$field.tooltipster( status.state === 'closed' ? 'open' : 'close' );
if ( status.state !== 'closed' ) {
return;
}
const instance = $field.tooltipster( 'instance' );
// Adjust tooltip styling.
instance._$tooltip.css( {
height: 'auto',
} );
instance._$tooltip.find( '.tooltipster-arrow' ).css( {
left: '50%',
} );
// Close the tooltip after 5 seconds.
setTimeout( function() {
preview.closeTooltips();
}, 5000 );
} );
},
/**
* Close tooltips.
*
* @since 1.9.2
*/
closeTooltips() {
preview.el.$content.find( '.wpforms-field' ).each( function() {
const $this = $( this );
if ( $this.hasClass( 'tooltipstered' ) && $this.parent().length ) {
$this.tooltipster( 'close' );
}
} );
},
/**
* Display the form header.
*
* @since 1.9.4
*
* @param {Object} response Button text.
*/
displayHeader( response ) {
const title = `<h2 class="wpforms-ai-form-generator-preview-title">${ response.form_title ?? '' }</h2>`;
// Add form title.
preview.el.$content.prepend( title );
},
/**
* Display the `submit` button.
*
* @since 1.9.2
*
* @param {string} label Button text.
*/
displaySubmit( label ) {
preview.el.$content
.append( `<button type="button" value="${ label }" class="wpforms-ai-form-generator-preview-submit">${ label }</button>` );
},
/**
* Clear the preview content.
*
* @since 1.9.2
*
* @param {boolean} isEmptyState Whether to show the empty state or not.
*/
clear( isEmptyState = true ) {
preview.el.$content.find( '.wpforms-ai-form-generator-preview-field' ).remove();
preview.el.$content.find( '.wpforms-ai-form-generator-preview-placeholder' ).remove();
preview.el.$content.find( '.wpforms-ai-form-generator-preview-title' ).remove();
preview.el.$content.find( '.wpforms-ai-form-generator-preview-addons-notice' ).remove();
preview.el.$content.find( '.wpforms-ai-form-generator-preview-submit' ).remove();
preview.el.$emptyState.toggleClass( 'wpforms-hidden-strict', ! isEmptyState );
},
/**
* Delay promise.
*
* @since 1.9.2
*
* @param {number} time Time in milliseconds.
*
* @return {Promise} Promise.
*/
delay( time ) {
return new Promise( ( res ) => {
setTimeout( res, time );
} );
},
};
return preview;
}
@@ -0,0 +1,6 @@
export default function(s,i){let n=wpforms_ai_form_generator,r={el:{},mouse:{},init(){r.el.$contentWrap=s.main.el.$generatorPanel.find(".wpforms-panel-content-wrap"),r.el.$content=r.el.$contentWrap.find(".wpforms-panel-content"),r.el.$emptyState=r.el.$content.find(".wpforms-panel-empty-state"),r.events()},events(){i(document).on("mousemove",e=>{r.mouse.x=e.pageX,r.mouse.y=e.pageY}),r.el.$contentWrap.on("scroll",r.closeTooltips)},update(){var e=s.state.aiResponse;if(e&&e.fields){for(var t in s.state.isPreviewUpdate=!0,s.state.previewFields=[],r.clear(!1),r.displayHeader(e),e.fieldsOrder){var o=e.fieldsOrder[t];r.field(e.fields[o],t)}e.fieldsOrder?.length?r.displaySubmit(e.settings?.submit_text||n.panel.submitButton):(r.el.$emptyState.removeClass("wpforms-hidden-strict"),s.state.isPreviewUpdate=!1)}},async field(t,e){var o=`
<div id="wpforms-generator-field-${t.id??""}" class="wpforms-ai-form-generator-preview-field">
<div class="placeholder"></div>
<div class="wpforms-field wpforms-field-${t.type??""}"></div>
</div>
`,o=(r.el.$content.append(o),{action:"wpforms_get_ai_form_field_preview",nonce:n.nonce,field:t});await r.delay(300*e),i.post(n.ajaxUrl,o).done(function(e){e.success?r.displayField(e.data??"",t):wpf.debug("Form Generator AJAX error:",e.data.error??e.data)}).fail(function(e){wpf.debug("Form Generator AJAX error:",e.responseText??e.statusText)})},displayField(e,t){var o,i;!t.id&&0!==t.id||(i=(o=r.el.$content.find("#wpforms-generator-field-"+t.id)).find(".wpforms-field"),o.find(".placeholder").addClass("fade-out"),i.html(e??"").addClass("fade-in").toggleClass("wpforms-hidden",!e).toggleClass("required","1"===t.required).toggleClass("label_empty",!t.label),r.initTooltip(i),r.initPageBreak(i,t),s.state.previewFields.push(t.id),s.state.previewFields.length!==Object.keys(s.state.aiResponse?.fields).length)||(s.state.isPreviewUpdate=!1)},getAddonsUsedInResponse(){var e=s.state.aiResponse;if(!e||!e.fields)return"";var t,o,i=[];for(t in e.fields){var r=wpforms_ai_form_generator.addonFields[e.fields[t].type];r&&(r=wpforms_addons["wpforms-"+r]?.title.replace(n.addons.addon,"").trim())&&!i.includes(r)&&i.push(r)}return i.length?(o=i.pop(),o+=" "+n.addons.addon,i.length?i.join(", ")+", "+n.addons.and+" "+o:o):""},initPageBreak(e,t){"pagebreak"!==t.type||["top","bottom"].includes(t.position)||e.addClass("wpforms-pagebreak-normal")},initTooltip(e){var t={content:n.panel.tooltipTitle+"<br>"+n.panel.tooltipText,trigger:"manual",interactive:!0,animationDuration:100,delay:0,side:["top"],contentAsHTML:!0,functionPosition:(e,t,o)=>(o.coord.top=r.mouse.y-57,o.coord.left=r.mouse.x-130,o)};e.tooltipster(t),r.toggleTooltipOnClick(e)},toggleTooltipOnClick(t){t.on("click",()=>{r.closeTooltips();var e=t.tooltipster("status");t.tooltipster("closed"===e.state?"open":"close"),"closed"===e.state&&((e=t.tooltipster("instance"))._$tooltip.css({height:"auto"}),e._$tooltip.find(".tooltipster-arrow").css({left:"50%"}),setTimeout(function(){r.closeTooltips()},5e3))})},closeTooltips(){r.el.$content.find(".wpforms-field").each(function(){var e=i(this);e.hasClass("tooltipstered")&&e.parent().length&&e.tooltipster("close")})},displayHeader(e){e=`<h2 class="wpforms-ai-form-generator-preview-title">${e.form_title??""}</h2>`;r.el.$content.prepend(e)},displaySubmit(e){r.el.$content.append(`<button type="button" value="${e}" class="wpforms-ai-form-generator-preview-submit">${e}</button>`)},clear(e=!0){r.el.$content.find(".wpforms-ai-form-generator-preview-field").remove(),r.el.$content.find(".wpforms-ai-form-generator-preview-placeholder").remove(),r.el.$content.find(".wpforms-ai-form-generator-preview-title").remove(),r.el.$content.find(".wpforms-ai-form-generator-preview-addons-notice").remove(),r.el.$content.find(".wpforms-ai-form-generator-preview-submit").remove(),r.el.$emptyState.toggleClass("wpforms-hidden-strict",!e)},delay(t){return new Promise(e=>{setTimeout(e,t)})}};return r}
@@ -0,0 +1,249 @@
/* global wpf, WPFormsBuilder, WPFormsConstantContactV3AuthVars */
/**
* @param window.wpforms_admin
* @param window.wpforms_builder
* @param WPFormsConstantContactV3AuthVars.auth_url
*/
/**
* WPForms Constant Contact V3 Popup.
*
* @since 1.9.3
*/
const WPFormsConstantContactV3Auth = window.WPFormsConstantContactV3Auth || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.9.3
*
* @type {Object}
*/
const app = {
/**
* Is the authorization window opened?
*
* @since 1.9.3
*/
isOpened : false,
/**
* URL to listen for messages from the window.
*
* @since 1.9.3
*/
listenURL: '',
/**
* Start the engine.
*
* @since 1.9.3
*/
init: () => {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.9.3
*/
ready: () => {
const redirectUri = new URL( WPFormsConstantContactV3AuthVars.auth_url ).searchParams.get( 'redirect_uri' );
app.listenURL = new URL( redirectUri ).origin;
$( document )
.on( 'click', '.wpforms-constant-contact-v3-auth, .wpforms-builder-constant-contact-v3-provider-sign-up', app.showWindow )
.on( 'click', '#wpforms-settings-constant-contact-v3-migration-prompt-link', app.promptMigration );
},
/**
* Show a window.
*
* @since 1.9.3
*
* @param {Event} e Click event.
*/
showWindow: ( e ) => {
e.preventDefault();
if ( app.isOpened ) {
return;
}
const authUrl = WPFormsConstantContactV3AuthVars.auth_url,
width = 500,
height = 600,
left = ( screen.width / 2 ) - ( width / 2 ),
top = ( screen.height / 2 ) - ( height / 2 ),
loginHintEmail = $( '.wpforms-constant-contact-v3-auth' ).data( 'login-hint' ),
url = new URL( authUrl );
if ( loginHintEmail ) {
url.searchParams.set( 'login_hint', loginHintEmail );
}
const newWindow = window.open(
url.toString(),
'authPopup',
'width=' + width + ', height=' + height + ', top=' + top + ', left=' + left
);
window.addEventListener( 'message', app.listenResponse );
const checkWindowClosed = setInterval( () => {
if ( newWindow.closed ) {
clearInterval( checkWindowClosed );
app.isOpened = false;
}
}, 1000 );
app.isOpened = true;
},
/**
* Listen for response.
*
* @since 1.9.3
*
* @param {Event} event Message event.
*/
listenResponse: ( event ) => {
if ( event.origin !== app.listenURL ) {
return;
}
if ( ! event.data ) {
app.errorModal( WPFormsConstantContactV3AuthVars.strings.error );
return;
}
app.saveAccount( event.data );
},
/**
* Save account.
*
* @since 1.9.3
*
* @param {string} code Authorization code.
*/
saveAccount: ( code ) => {
const modal = app.waitModal();
$.post(
WPFormsConstantContactV3AuthVars.ajax_url,
{
action: 'wpforms_constant_contact_popup_auth',
data: JSON.stringify( { code } ),
nonce: WPFormsConstantContactV3AuthVars.nonce,
}
)
.done( ( response ) => {
if ( ! response.success ) {
modal.close();
const errorMessage =
'<p>' + WPFormsConstantContactV3AuthVars.strings.error + '</p><p><strong>' + wpf.sanitizeHTML( response.data ) + '</strong></p>';
app.errorModal( errorMessage );
return;
}
if ( typeof WPFormsBuilder === 'undefined' ) {
modal.close();
window.location.href = WPFormsConstantContactV3AuthVars.page_url;
return;
}
WPFormsBuilder.formSave( false ).done( () => {
WPFormsBuilder.setCloseConfirmation( false );
WPFormsBuilder.showLoadingOverlay();
location.reload();
} );
} );
},
/**
* Show a waiting modal.
*
* @since 1.9.3
*
* @return {Object} Modal object.
*/
waitModal: () => {
return $.alert( {
title: '',
content: WPFormsConstantContactV3AuthVars.strings.wait,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: false,
} );
},
/**
* Show an error modal.
*
* @since 1.9.3
*
* @param {string} content Alert text.
*
* @return {Object} Modal object.
*/
errorModal: ( content ) => {
const strings = window?.wpforms_builder || window?.wpforms_admin;
return $.alert( {
title: strings.uh_oh,
content,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
cancel: {
text: strings.cancel,
action: () => {
app.isOpened = false;
},
},
},
} );
},
/**
* Prompt and start migration from v2 to v3 in the notice.
*
* @since 1.9.3
*
* @param {Object} e Event object.
*/
promptMigration( e ) {
e.preventDefault();
const modal = app.waitModal();
$.post( {
url: WPFormsConstantContactV3AuthVars.ajax_url,
data: {
action: 'wpforms_constant_contact_migration_prompt',
nonce: WPFormsConstantContactV3AuthVars.nonce,
},
success: () => {
modal.close();
window.location.href = WPFormsConstantContactV3AuthVars.page_url;
},
error: () => {
modal.close();
app.errorModal( WPFormsConstantContactV3AuthVars.strings.error );
},
} );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsConstantContactV3Auth.init();
@@ -0,0 +1 @@
let WPFormsConstantContactV3Auth=window.WPFormsConstantContactV3Auth||((o,s,i)=>{let c={isOpened:!1,listenURL:"",init:()=>{i(c.ready)},ready:()=>{var t=new URL(WPFormsConstantContactV3AuthVars.auth_url).searchParams.get("redirect_uri");c.listenURL=new URL(t).origin,i(o).on("click",".wpforms-constant-contact-v3-auth, .wpforms-builder-constant-contact-v3-provider-sign-up",c.showWindow).on("click","#wpforms-settings-constant-contact-v3-migration-prompt-link",c.promptMigration)},showWindow:n=>{if(n.preventDefault(),!c.isOpened){var n=WPFormsConstantContactV3AuthVars.auth_url,a=screen.width/2-250,r=screen.height/2-300,e=i(".wpforms-constant-contact-v3-auth").data("login-hint"),n=new URL(n);e&&n.searchParams.set("login_hint",e);let t=s.open(n.toString(),"authPopup","width=500, height=600, top="+r+", left="+a),o=(s.addEventListener("message",c.listenResponse),setInterval(()=>{t.closed&&(clearInterval(o),c.isOpened=!1)},1e3));c.isOpened=!0}},listenResponse:t=>{t.origin===c.listenURL&&(t.data?c.saveAccount(t.data):c.errorModal(WPFormsConstantContactV3AuthVars.strings.error))},saveAccount:t=>{let o=c.waitModal();i.post(WPFormsConstantContactV3AuthVars.ajax_url,{action:"wpforms_constant_contact_popup_auth",data:JSON.stringify({code:t}),nonce:WPFormsConstantContactV3AuthVars.nonce}).done(t=>{t.success?"undefined"==typeof WPFormsBuilder?(o.close(),s.location.href=WPFormsConstantContactV3AuthVars.page_url):WPFormsBuilder.formSave(!1).done(()=>{WPFormsBuilder.setCloseConfirmation(!1),WPFormsBuilder.showLoadingOverlay(),location.reload()}):(o.close(),t="<p>"+WPFormsConstantContactV3AuthVars.strings.error+"</p><p><strong>"+wpf.sanitizeHTML(t.data)+"</strong></p>",c.errorModal(t))})},waitModal:()=>i.alert({title:"",content:WPFormsConstantContactV3AuthVars.strings.wait,icon:"fa fa-info-circle",type:"blue",buttons:!1}),errorModal:t=>{var o=s?.wpforms_builder||s?.wpforms_admin;return i.alert({title:o.uh_oh,content:t,icon:"fa fa-exclamation-circle",type:"red",buttons:{cancel:{text:o.cancel,action:()=>{c.isOpened=!1}}}})},promptMigration(t){t.preventDefault();let o=c.waitModal();i.post({url:WPFormsConstantContactV3AuthVars.ajax_url,data:{action:"wpforms_constant_contact_migration_prompt",nonce:WPFormsConstantContactV3AuthVars.nonce},success:()=>{o.close(),s.location.href=WPFormsConstantContactV3AuthVars.page_url},error:()=>{o.close(),c.errorModal(WPFormsConstantContactV3AuthVars.strings.error)}})}};return c})(document,window,jQuery);WPFormsConstantContactV3Auth.init();
@@ -0,0 +1,603 @@
/* global WPForms, wpf */
/**
* WPForms Providers Builder ConstantContactV3 module.
*
* @since 1.9.3
*/
WPForms.Admin.Builder.Providers.ConstantContactV3 = WPForms.Admin.Builder.Providers.ConstantContactV3 || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.9.3
*
* @type {Object}
*/
const app = {
/**
* CSS selectors.
*
* @since 1.9.3
*
* @type {Object}
*/
selectors: {
accountField: '.js-wpforms-builder-constant-contact-v3-provider-connection-account',
actionData: '.wpforms-builder-constant-contact-v3-provider-actions-data',
actionField: '.js-wpforms-builder-constant-contact-v3-provider-connection-action',
connection: '.wpforms-panel-content-section-constant-contact-v3 .wpforms-builder-provider-connection',
},
/**
* jQuery elements.
*
* @since 1.9.3
*
* @type {Object}
*/
$elements: {
$connections: $( '.wpforms-panel-content-section-constant-contact-v3 .wpforms-builder-provider-connections' ),
$holder: $( '#wpforms-panel-providers' ),
$panel: $( '#constant-contact-v3-provider' ),
},
/**
* Current provider slug.
*
* @since 1.9.3
*
* @type {string}
*/
provider: 'constant-contact-v3',
/**
* This is a shortcut to the WPForms.Admin.Builder.Providers object,
* that handles the parent all-providers functionality.
*
* @since 1.9.3
*
* @type {Object}
*/
Providers: {},
/**
* This is a shortcut to the WPForms.Admin.Builder.Templates object,
* that handles all the template management.
*
* @since 1.9.3
*
* @type {Object}
*/
Templates: {},
/**
* This is a shortcut to the WPForms.Admin.Builder.Providers.cache object,
* that handles all the cache management.
*
* @since 1.9.3
*
* @type {Object}
*/
Cache: {},
/**
* This is a flag for ready state.
*
* @since 1.9.3
*
* @type {boolean}
*/
isReady: false,
/**
* Start the engine.
*
* Run initialization on the providers panel only.
*
* @since 1.9.3
*/
init() {
// We are requesting/loading a Providers panel.
if ( wpf.getQueryString( 'view' ) === 'providers' ) {
app.$elements.$holder.on( 'WPForms.Admin.Builder.Providers.ready', app.ready );
}
// We have switched to a Providers panel.
$( document ).on( 'wpformsPanelSwitched', function( event, panel ) {
if ( panel === 'providers' ) {
app.ready();
}
} );
},
/**
* Initialized once the DOM and Providers are fully loaded.
*
* @since 1.9.3
*/
ready() {
if ( app.isReady ) {
return;
}
app.Providers = WPForms.Admin.Builder.Providers;
app.Templates = WPForms.Admin.Builder.Templates;
app.Cache = app.Providers.cache;
// Register custom Underscore.js templates.
app.Templates.add( [
'wpforms-constant-contact-v3-builder-content-connection',
'wpforms-constant-contact-v3-builder-content-connection-error',
'wpforms-constant-contact-v3-builder-content-connection-select-field',
'wpforms-constant-contact-v3-builder-content-connection-conditionals',
] );
// Events registration.
app.bindUIActions();
app.bindTriggers();
app.processInitial();
// Save a flag for ready state.
app.isReady = true;
},
/**
* Process various events as a response to UI interactions.
*
* @since 1.9.3
*/
bindUIActions() {
app.$elements.$panel
.on( 'connectionCreate', app.connection.create )
.on( 'connectionDelete', app.connection.delete )
.on( 'change', app.selectors.accountField, app.ui.accountField.change )
.on( 'change', app.selectors.actionField, app.ui.actionField.change );
},
/**
* Fire certain events on certain actions, specific for related connections.
* These are not directly caused by user manipulations.
*
* @since 1.9.3
*/
bindTriggers() {
app.$elements.$connections.on( 'connectionsDataLoaded', function( event, data ) {
if ( _.isEmpty( data.connections ) ) {
return;
}
for ( const connectionId in data.connections ) {
app.connection.generate( {
connection: data.connections[ connectionId ],
conditional: data.conditionals[ connectionId ],
} );
}
} );
app.$elements.$connections.on( 'connectionGenerated', function( event, data ) {
const $connection = app.connection.getById( data.connection.id );
if ( _.has( data.connection, 'isNew' ) && data.connection.isNew ) {
// Run replacing temporary connection ID if it's a new connection.
app.connection.replaceIds( data.connection.id, $connection );
return;
}
$( app.selectors.actionField, $connection ).trigger( 'change' );
} );
},
/**
* Compile template with data if any and display them on a page.
*
* @since 1.9.3
*/
processInitial() {
app.connection.dataLoad();
},
/**
* Connection property.
*
* @since 1.9.3
*/
connection: {
/**
* Sometimes we might need to a get a connection DOM element by its ID.
*
* @since 1.9.3
*
* @param {string} connectionId Connection ID to search for a DOM element by.
*
* @return {jQuery} jQuery object for connection.
*/
getById( connectionId ) {
return app.$elements.$connections.find( '.wpforms-builder-provider-connection[data-connection_id="' + connectionId + '"]' );
},
/**
* Sometimes in DOM we might have placeholders or temporary connection IDs.
* We need to replace them with actual values.
*
* @since 1.9.3
*
* @param {string} connectionId New connection ID to replace to.
* @param {Object} $connection jQuery DOM connection element.
*/
replaceIds( connectionId, $connection ) {
// Replace old temporary %connection_id% from PHP code with the new one.
$connection.find( 'input, select, label' ).each( function() {
const $this = $( this );
if ( $this.attr( 'name' ) ) {
$this.attr( 'name', $this.attr( 'name' ).replace( /%connection_id%/gi, connectionId ) );
}
if ( $this.attr( 'id' ) ) {
$this.attr( 'id', $this.attr( 'id' ).replace( /%connection_id%/gi, connectionId ) );
}
if ( $this.attr( 'for' ) ) {
$this.attr( 'for', $this.attr( 'for' ).replace( /%connection_id%/gi, connectionId ) );
}
if ( $this.attr( 'data-name' ) ) {
$this.attr( 'data-name', $this.attr( 'data-name' ).replace( /%connection_id%/gi, connectionId ) );
}
} );
},
/**
* Create a connection using the user entered name.
*
* @since 1.9.3
*
* @param {Object} event Event object.
* @param {string} name Connection name.
*/
create( event, name ) {
const connectionId = new Date().getTime().toString( 16 ),
connection = {
id: connectionId,
name,
isNew: true,
};
app.Cache.addTo( app.provider, 'connections', connectionId, connection );
app.connection.generate( {
connection,
} );
},
/**
* Connection is deleted - delete a cache as well.
*
* @since 1.9.3
*
* @param {Object} event Event object.
* @param {Object} $connection jQuery DOM element for a connection.
*/
delete( event, $connection ) {
const $holder = app.Providers.getProviderHolder( app.provider );
if ( ! $connection.closest( $holder ).length ) {
return;
}
const connectionId = $connection.data( 'connection_id' );
if ( _.isString( connectionId ) ) {
app.Cache.deleteFrom( app.provider, 'connections', connectionId );
}
},
/**
* Get the template and data for a connection and process it.
*
* @since 1.9.3
*
* @param {Object} data Connection data.
*
* @return {void}
*/
generate( data ) {
const accounts = app.Cache.get( app.provider, 'accounts' );
if ( _.isEmpty( accounts ) || ! app.account.isAccountExists( data.connection.account_id, accounts ) ) {
return;
}
const actions = app.Cache.get( app.provider, 'actions' ),
lists = app.Cache.get( app.provider, 'lists' );
return app.connection.renderConnections( accounts, lists, actions, data );
},
/**
* Render connections.
*
* @since 1.9.3
*
* @param {Object} accounts List of accounts.
* @param {Object} lists List of lists.
* @param {Object} actions List of actions.
* @param {Object} data Connection data.
*/
renderConnections( accounts, lists, actions, data ) {
if ( ! app.account.isAccountExists( data.connection.account_id, accounts ) ) {
return;
}
const tmplConnection = app.Templates.get( 'wpforms-' + app.provider + '-builder-content-connection' ),
tmplConditional = app.Templates.get( 'wpforms-constant-contact-v3-builder-content-connection-conditionals' ),
conditional = _.has( data.connection, 'isNew' ) && data.connection.isNew ? tmplConditional() : data.conditional;
app.$elements.$connections.prepend(
tmplConnection( {
accounts,
lists,
actions,
connection: data.connection,
conditional,
provider: app.provider,
} )
);
app.$elements.$connections.trigger( 'connectionGenerated', [ data ] );
},
/**
* Fire AJAX-request to retrieve the list of all saved connections.
*
* @since 1.9.3
*/
dataLoad() {
app
.Providers.ajax
.request( app.provider, {
data: {
task: 'connections_get',
},
} )
.done( function( response ) {
if (
! response.success ||
! _.has( response.data, 'connections' )
) {
return;
}
[
'accounts',
'actions',
'actions_fields',
'conditionals',
'connections',
'custom_fields',
'lists',
].forEach( ( dataType ) => {
app.Cache.set( app.provider, dataType, jQuery.extend( {}, response.data[ dataType ] ) );
} );
app.$elements.$connections.trigger( 'connectionsDataLoaded', [ response.data ] );
} );
},
},
/**
* Account property.
*
* @since 1.9.3
*/
account: {
/**
* Check if a provided account is listed inside an account list.
*
* @since 1.9.3
*
* @param {string} accountId Connection account ID to check.
* @param {Object} accounts Array of objects, usually received from API.
*
* @return {boolean} True if an account exists.
*/
isAccountExists( accountId, accounts ) {
if ( _.isEmpty( accounts ) ) {
return false;
}
// New connections that have not been saved don't have the account ID yet.
if ( _.isEmpty( accountId ) ) {
return true;
}
return _.has( accounts, accountId );
},
},
/**
* All methods that modify the UI of a page.
*
* @since 1.9.3
*/
ui: {
/**
* Account field methods.
*
* @since 1.9.3
*/
accountField: {
/**
* Callback-function on change event.
*
* @since 1.9.3
*/
change() {
const $this = $( this ),
$connection = $this.closest( app.selectors.connection ),
$actionName = $( app.selectors.actionField, $connection );
$actionName.prop( 'selectedIndex', 0 ).trigger( 'change' );
// If an account is empty.
if ( _.isEmpty( $this.val() ) ) {
$actionName.prop( 'disabled', true );
$( app.selectors.actionData, $connection ).html( '' );
return;
}
$actionName.prop( 'disabled', false );
$this.removeClass( 'wpforms-error' );
},
},
/**
* Action methods.
*
* @since 1.9.3
*/
actionField: {
/**
* Callback-function on change event.
*
* @since 1.9.3
*/
change() {
const $this = $( this ),
$connection = $this.closest( app.selectors.connection ),
$account = $( app.selectors.accountField, $connection ),
$action = $( app.selectors.actionField, $connection );
app.ui.actionField.render( {
action: 'action',
target: $this,
/* eslint-disable camelcase */
account_id: $account.val(),
action_name: $action.val(),
connection_id: $connection.data( 'connection_id' ),
/* eslint-enable camelcase */
} );
$this.removeClass( 'wpforms-error' );
},
/**
* Render HTML.
*
* @since 1.9.3
*
* @param {Object} args Arguments.
*/
render( args ) {
const fields = app.tmpl.renderActionFields( args ),
$connection = app.connection.getById( args.connection_id ),
$connectionData = $( app.selectors.actionData, $connection );
$connectionData.html( fields );
app.$elements.$holder.trigger( 'connectionRendered', [ app.provider, args.connection_id ] );
},
/**
* Get a list of constant-contact lists.
*
* @since 1.9.3
*
* @param {string} accountId Account ID.
*
* @return {Array} List of constant-contact lists.
*/
getList( accountId ) {
const listsCache = app.Cache.get( app.provider, 'lists' );
return ! _.isEmpty( listsCache ) && ! _.isEmpty( listsCache[ accountId ] ) ? listsCache[ accountId ] : [];
},
},
},
/**
* All methods for JavaScript templates.
*
* @since 1.9.3
*/
tmpl: {
/**
* Compile and retrieve an HTML for common elements.
*
* @since 1.9.3
* @deprecated 1.9.5
*
* @return {string} Compiled HTML.
*/
commonsHTML() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPForms.Admin.Builder.Providers.ConstantContactV3.tmpl.commonsHTML()" has been deprecated!' );
const tmplError = app.Templates.get( 'wpforms-' + app.provider + '-builder-content-connection-error' );
return tmplError();
},
/**
* Compile and retrieve an HTML for "Custom Fields Table".
*
* @since 1.9.3
*
* @param {Object} args Arguments
*
* @return {string} Compiled HTML.
*/
renderActionFields( args ) {
const fields = wpf.getFields(),
actionsFields = app.Cache.get( app.provider, 'actions_fields' ),
customFields = app.Cache.get( app.provider, 'custom_fields' ),
connection = app.Cache.getById( app.provider, 'connections', args.connection_id );
let fieldHTML = '';
$.each( actionsFields[ args.target.val() ], function( key, field ) {
if ( key === 'custom_fields' ) {
const tmplFields = app.Templates.get( 'wpforms-providers-builder-content-connection-fields' );
fieldHTML += tmplFields( {
connection,
fields,
provider: {
slug: app.provider,
fields: customFields[ args.account_id ],
},
isSupportSubfields: true,
} );
return;
}
const options = key === 'list' ? app.ui.actionField.getList( args.account_id ) : Object.values( fields );
const templateName = 'wpforms-' + app.provider + '-builder-content-connection-' + field.type + '-field';
const tmplField = app.Templates.get( templateName );
fieldHTML += tmplField( {
connection,
name: key,
field,
provider: {
slug: app.provider,
fields: actionsFields[ args.target.val() ],
},
options,
} );
} );
return fieldHTML;
},
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPForms.Admin.Builder.Providers.ConstantContactV3.init();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
/* global wpforms_divi_builder, WPFormsRepeaterField, ETBuilderBackendDynamic */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
/**
* WPFormsSelector component.
*
* @since 1.6.3
*/
class WPFormsSelector extends Component {
/**
* Module slug.
*
* @since 1.6.3
*
* @type {string}
*/
static slug = 'wpforms_selector';
/**
* Constructor.
*
* @since 1.6.3
*
* @param {string} props List of properties.
*/
constructor( props ) {
super( props );
this.state = {
error: null,
isLoading: true,
form: null,
};
}
/**
* Set types for properties.
*
* @since 1.6.3
*
* @returns {object} Properties type.
*/
static get propTypes() {
return {
form_id: PropTypes.number, // eslint-disable-line camelcase
show_title: PropTypes.string, // eslint-disable-line camelcase
show_desc: PropTypes.string, // eslint-disable-line camelcase
};
}
/**
* Check if form settings was updated.
*
* @since 1.6.3
*
* @param {object} prevProps List of previous properties.
*/
componentDidUpdate( prevProps ) {
if ( prevProps.form_id !== this.props.form_id || prevProps.show_title !== this.props.show_title || prevProps.show_desc !== this.props.show_desc ) {
this.componentDidMount();
}
}
/**
* Ajax request for form HTML.
*
* @since 1.6.3
*/
componentDidMount() {
const formData = new FormData();
formData.append( 'nonce', wpforms_divi_builder.nonce );
formData.append( 'action', 'wpforms_divi_preview' );
formData.append( 'form_id', this.props.form_id );
formData.append( 'show_title', this.props.show_title );
formData.append( 'show_desc', this.props.show_desc );
fetch(
wpforms_divi_builder.ajax_url,
{
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache',
},
body: new URLSearchParams( formData ),
},
)
.then( ( res ) => res.json() )
.then(
( result ) => {
this.setState( {
isLoading: false,
form: result.data,
} );
},
( error ) => {
this.setState( {
isLoading: false,
error,
} );
},
);
}
/**
* Render module view.
*
* @since 1.6.3
*
* @returns {JSX.Element} View for module.
*/
render() {
const { error, isLoaded, form } = this.state,
wrapperClasses = isLoaded ? 'wpforms-divi-form-preview loading' : 'wpforms-divi-form-preview';
if ( typeof this.props.form_id === 'undefined' || this.props.form_id === '' ) {
return (
<div className="wpforms-divi-empty-block">
<img src={ wpforms_divi_builder.block_empty_url } alt="" />
{ <p dangerouslySetInnerHTML={ { __html: wpforms_divi_builder.block_empty_text } } /> }
<button type="button" onClick={
() => {
window.open( wpforms_divi_builder.get_started_url, '_blank' );
}
}
>
{ wpforms_divi_builder.get_started_text }
</button>
<p className="wpforms-admin-no-forms-footer">
{ wpforms_divi_builder.help_text }&nbsp;
<a href={ wpforms_divi_builder.guide_url } onClick={
() => {
window.open( wpforms_divi_builder.guide_url, '_blank' );
}
}
>
{ wpforms_divi_builder.guide_text }.
</a>
</p>
</div>
);
}
if ( error || ! form ) {
return (
<div className="wpforms-divi-form-placeholder">
<img src={ wpforms_divi_builder.placeholder } alt="" />
</div>
);
}
return (
<div className={ wrapperClasses }>
{ <div dangerouslySetInnerHTML={ { __html: form } } /> }
</div>
);
}
}
jQuery( window )
// Register custom modules.
.on( 'et_builder_api_ready', ( event, API ) => {
API.registerModules( [ WPFormsSelector ] );
} )
// Re-initialize WPForms frontend.
.on( 'wpformsDiviModuleDisplay', () => {
window.wpforms.init();
} );
jQuery( document )
.on( 'wpformsReady', function() {
const $ = jQuery;
// Make all the modern dropdowns disabled.
$( '.choicesjs-select' ).each( function() {
const $instance = $( this ).data( 'choicesjs' );
if ( $instance && typeof $instance.disable === 'function' ) {
$instance.disable();
}
} );
// Init Repeater fields.
if ( 'undefined' !== typeof WPFormsRepeaterField ) {
WPFormsRepeaterField.ready();
}
} );
@@ -0,0 +1,70 @@
/* global $e, elementor */
// noinspection ES6ConvertVarToLetConst
/**
* WPForms script for editor context.
*
* @since 1.9.6
*/
var WPFormsElementorEditorContext = window.WPFormsElementorEditorContext || ( function( document, window, $ ) { // eslint-disable-line no-var
// noinspection JSUnusedGlobalSymbols
/**
* Public functions and properties.
*
* @since 1.9.6
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.6
*/
init() {
app.events();
},
/**
* Register JS events.
*
* @since 1.9.6
*/
events() {
$( window ).on( 'elementor/init', function() {
// To add action on save event, we should use hookUI.After.
$e.hooks.registerUIAfter( new class extends $e.modules.hookUI.After {
// noinspection JSUnusedGlobalSymbols
getCommand() {
return 'document/save/save';
}
// noinspection JSUnusedGlobalSymbols
getId() {
return 'wpforms-elementor-editor-context-after-save';
}
// noinspection JSUnusedGlobalSymbols
getConditions() {
return true;
}
apply() {
// Save custom themes in a preview window.
const previewWindow = elementor.$preview[ 0 ]?.contentWindow;
if ( previewWindow ) {
previewWindow.WPFormsElementorThemes.saveCustomThemes();
}
}
} );
} );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementorEditorContext.init();
@@ -0,0 +1 @@
var WPFormsElementorEditorContext=window.WPFormsElementorEditorContext||((e,t)=>{let o={init(){o.events()},events(){t(e).on("elementor/init",function(){$e.hooks.registerUIAfter(new class extends $e.modules.hookUI.After{getCommand(){return"document/save/save"}getId(){return"wpforms-elementor-editor-context-after-save"}getConditions(){return!0}apply(){var e=elementor.$preview[0]?.contentWindow;e&&e.WPFormsElementorThemes.saveCustomThemes()}})})}};return o})((document,window),jQuery);WPFormsElementorEditorContext.init();
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,623 @@
/* global wpformsElementorVars, elementor, elementorFrontend */
'use strict';
/**
* WPForms integration with Elementor in the editor.
*
* @since 1.6.0
* @since 1.6.2 Moved frontend integration to `wpforms-elementor-frontend.js`
*/
var WPFormsElementor = window.WPFormsElementor || ( function( document, window, $ ) {
/**
* Runtime variables.
*
* @since 1.6.2
*
* @type {object}
*/
var vars = {};
/**
* Public functions and properties.
*
* @since 1.6.0
*
* @type {object}
*/
var app = {
/**
* Start the engine.
*
* @since 1.6.0
*/
init: function() {
app.events();
},
/**
* Register JS events.
*
* @since 1.6.0
*/
events: function() {
// Widget events.
$( window ).on( 'elementor/frontend/init', function( event, id, instance ) {
// Widget buttons click.
elementor.channels.editor.on( 'elementorWPFormsAddFormBtnClick', app.addFormBtnClick );
// Widget frontend events.
elementorFrontend.hooks.addAction( 'frontend/element_ready/wpforms.default', app.widgetPreviewEvents );
// Initialize widget controls.
elementor.hooks.addAction( 'panel/open_editor/widget/wpforms', app.widgetPanelOpen );
// Initialize choiceJS.
elementorFrontend.hooks.addAction( 'frontend/element_ready/wpforms.default', app.loadChoicesJS );
} );
},
/**
* Init Modern style Dropdown fields (<select>) with choiceJS.
*
* @since 1.9.0
*
* @param {Object} $scope Elementor scope object.
*/
loadChoicesJS( $scope ) {
// Loads if function exists.
if ( typeof parent.Choices !== 'function' ) {
return;
}
const $elements = $scope.find( '.wpforms-field .choicesjs-select' );
const config = window.wpforms_choicesjs_config || {};
// Initialize ChoicesJS.
$elements.each( function( index, el ) {
if ( ! ( el instanceof parent.HTMLSelectElement ) ) {
return;
}
const $el = $( el );
if ( $el.data( 'choicesjs' ) ) {
return;
}
const $field = $el.closest( '.wpforms-field' );
config.callbackOnInit = function() {
const self = this,
$element = $( self.passedElement.element ),
$input = $( self.input.element ),
sizeClass = $element.data( 'size-class' );
// Add CSS-class for size.
if ( sizeClass ) {
$( self.containerOuter.element ).addClass( sizeClass );
}
/**
* If a multiple select has selected choices - hide a placeholder text.
* In case if select is empty - we return placeholder text.
*/
if ( $element.prop( 'multiple' ) ) {
// On init event.
$input.data( 'placeholder', $input.attr( 'placeholder' ) );
if ( self.getValue( true ).length ) {
$input.hide();
}
}
this.disable();
$field.find( '.is-disabled' ).removeClass( 'is-disabled' );
};
$el.data( 'choicesjs', new parent.Choices( el, config ) );
} );
},
/**
* Widget events.
*
* @since 1.6.2
*
* @param {jQuery} $scope The current element wrapped with jQuery.
*/
widgetPreviewEvents: function( $scope ) {
$scope
.on( 'click', '.wpforms-btn', app.addFormBtnClick )
.on( 'click', '.wpforms-admin-no-forms-container a', app.clickLinkInPreview )
.on( 'change', '.wpforms-elementor-form-selector select', app.selectFormInPreview )
.on( 'click mousedown focus keydown submit', '.wpforms-container *', app.disableEvents )
.on( 'click', '.wpforms-comprehensive-link', app.openComprehensiveLink );
app.updateSameForms( $scope );
},
/**
* Update all the same forms on the preview.
*
* @since 1.6.3
*
* @param {jQuery} $scope The current element wrapped with jQuery.
*/
updateSameForms: function( $scope ) {
var elementId = $scope.data( 'id' ),
$formContainer = $scope.find( '.wpforms-container' ),
formContainerHtml = $formContainer.html(),
formContainerId = $formContainer.attr( 'id' );
$scope
.closest( '.elementor-editor-active' )
.find( '.elementor-widget-wpforms:not(.elementor-element-' + elementId + ')' )
.each( function() {
var $anotherFormContainer = $( this ).find( '.wpforms-container' );
if ( $anotherFormContainer.attr( 'id' ) === formContainerId ) {
$anotherFormContainer.html( formContainerHtml );
}
} );
},
/**
* Initialize widget controls when widget is activated.
*
* @since 1.6.2
*
* @param {object} panel Panel object.
* @param {object} model Model object.
*/
widgetPanelOpen: function( panel, model ) {
vars.widgetId = model.attributes.id;
vars.formId = model.attributes.settings.attributes.form_id;
app.widgetPanelInit( panel );
app.widgetPanelObserver.init( panel );
},
/**
* Initialize widget controls when widget is activated.
*
* @since 1.6.2
*
* @param {object} panel Panel object.
*/
widgetPanelInit: function( panel ) {
var $formSelectControl = panel.$el.find( '.elementor-control.elementor-control-form_id' ),
$formSelect = $formSelectControl.find( 'select' ),
$addFormNoticeControl = panel.$el.find( '.elementor-control.elementor-control-add_form_notice' ),
$testFormNoticeControl = panel.$el.find( '.elementor-control.elementor-control-test_form_notice' );
// Update form select options if it is available after adding the form.
if ( vars.formSelectOptions ) {
$formSelect.html( vars.formSelectOptions );
}
// Update form select value.
if ( vars.formId && vars.formId !== '' ) {
$formSelect.val( vars.formId );
}
// Hide not needed controls.
if ( $formSelect.find( 'option' ).length > 0 ) {
$addFormNoticeControl.hide();
} else {
$formSelectControl.hide();
$testFormNoticeControl.hide();
}
// Show needed controls.
if ( parseInt( $formSelect.val(), 10 ) > 0 ) {
$testFormNoticeControl.show();
}
// Select form.
panel.$el.find( '.elementor-control.elementor-control-form_id' ).on( 'change', 'select', function() {
// Update `vars.formId` to be able to restore selected value after options update.
vars.formId = $( this ).val();
} );
// Click on the `Edit the selected form` link.
panel.$el.find( '.elementor-control.elementor-control-edit_form' ).on( 'click', 'a', app.editFormLinkClick );
},
/**
* The observer needed to re-init controls when the widget panel section and tabs switches.
*
* @since 1.6.3
*
* @member {object}
*/
widgetPanelObserver: {
/**
* Initialize observer.
*
* @since 1.6.3
*
* @param {object} panel Panel object.
*/
init: function( panel ) {
// Skip if observer for current widget already initialized.
if ( vars.observerWidgetId === vars.widgetId ) {
return;
}
// Disconnect previous widget observer.
if ( typeof vars.observer !== 'undefined' && typeof vars.observer.disconnect === 'function' ) {
vars.observer.disconnect();
}
var obs = {
targetNode : panel.$el.find( '#elementor-panel-content-wrapper' )[0],
config : {
childList: true,
subtree: true,
attributes: true,
},
};
app.widgetPanelObserver.panel = panel;
obs.observer = new MutationObserver( app.widgetPanelObserver.callback );
obs.observer.observe( obs.targetNode, obs.config );
vars.observerWidgetId = vars.widgetId;
vars.observer = obs.observer;
},
/**
* Observer callback.
*
* @since 1.6.3
*
* @param {Array} mutationsList Mutation list.
*/
callback: function( mutationsList ) {
var mutation,
quit = false;
for ( var i in mutationsList ) {
mutation = mutationsList[ i ];
if ( mutation.type === 'childList' && mutation.addedNodes.length > 0 ) {
quit = app.widgetPanelObserver.callbackMutationChildList( mutation );
}
if ( mutation.type === 'attributes' ) {
quit = app.widgetPanelObserver.callbackMutationAttributes( mutation );
}
if ( quit ) {
return;
}
}
},
/**
* Process 'childList' mutation.
*
* @since 1.6.3
*
* @param {MutationRecord} mutation Mutation record.
*
* @returns {boolean} True if detect needed node.
*/
callbackMutationChildList: function( mutation ) {
var addedNodes = mutation.addedNodes || [],
node;
for ( var n in addedNodes ) {
node = addedNodes[ n ];
if ( node && node.classList && node.classList.contains( 'elementor-control-section_form' ) ) {
app.widgetPanelInit( app.widgetPanelObserver.panel );
return true;
}
}
return false;
},
/**
* Process 'attributes' mutation.
*
* @since 1.6.3
*
* @param {MutationRecord} mutation Mutation record.
*
* @returns {boolean} True if detect needed target.
*/
callbackMutationAttributes: function( mutation ) {
if (
mutation.target &&
mutation.target.classList &&
mutation.target.classList.contains( 'elementor-tab-control-content' )
) {
app.widgetPanelInit( app.widgetPanelObserver.panel );
return true;
}
return false;
},
},
/**
* Edit selected form button click event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
editFormLinkClick: function( event ) {
app.findFormSelector( event );
app.openBuilderPopup( vars.$select.val() );
},
/**
* Add a new form button click event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
addFormBtnClick: function( event ) {
app.findFormSelector( event );
app.openBuilderPopup( 0 );
},
/**
* Find and store the form selector control wrapped in jQuery object.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
findFormSelector: function( event ) {
let view = elementor.getPanelView().getCurrentPageView();
// We need to be sure that we are on the widget Content section.
if ( view.activeSection && view.activeSection !== 'section_form' ) {
$( view.ui.tabs[0] ).trigger( 'click' );
}
vars.$select = event && event.$el ?
event.$el.closest( '#elementor-controls' ).find( 'select[data-setting="form_id"]' ) :
window.parent.jQuery( '#elementor-controls select[data-setting="form_id"]' );
},
/**
* Preview: Form selector event handler.
*
* @since 1.6.2
*/
selectFormInPreview: function() {
vars.formId = $( this ).val();
app.findFormSelector();
// To be sure, that both form selector selects are in sync.
app.refreshFormsList( null, vars.formId );
},
/**
* Preview: Click on the link event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
*/
clickLinkInPreview: function( event ) {
if ( event.target && event.target.href ) {
window.open( event.target.href, '_blank', 'noopener,noreferrer' );
}
},
/**
* Disable events.
*
* @since 1.6.2
*
* @param {object} event Event object.
*
* @returns {boolean} Always false.
*/
disableEvents: function( event ) {
event.preventDefault();
event.stopImmediatePropagation();
return false;
},
/**
* Open the compreshenvie guide link,
* as elementor disables all links in the preview.
*
* @since 1.8.3
*
* @param {object} event Event object.
*/
openComprehensiveLink: function( event ) {
const url = $( this ).attr( 'href' );
// Open the url in a new tab with JS bc elementor doesn't allow links in the preview.
window.open( url, '_blank' ).focus();
},
/**
* Open builder popup.
*
* @since 1.6.2
*
* @param {number} formId Form id. 0 for create new form.
*/
openBuilderPopup: function( formId ) {
formId = parseInt( formId || '0', 10 );
if ( ! vars.$popup ) {
// We need to add popup markup to the editor top document.
var $elementor = window.parent.jQuery( '#elementor-editor-wrapper' ),
popupTpl = wp.template( 'wpforms-builder-elementor-popup' );
$elementor.after( popupTpl() );
vars.$popup = $elementor.siblings( '#wpforms-builder-elementor-popup' );
}
var url = formId > 0 ? wpformsElementorVars.edit_form_url + formId : wpformsElementorVars.add_form_url,
$iframe = vars.$popup.find( 'iframe' );
app.builderCloseButtonEvent();
$iframe.attr( 'src', url );
vars.$popup.fadeIn();
},
/**
* Close button (inside the form builder) click event.
*
* @since 1.6.2
*/
builderCloseButtonEvent: function() {
vars.$popup
.off( 'wpformsBuilderInPopupClose' )
.on( 'wpformsBuilderInPopupClose', function( e, action, formId ) {
if ( action !== 'saved' || ! formId ) {
return;
}
app.refreshFormsList( null, formId );
} );
},
/**
* Refresh forms list event handler.
*
* @since 1.6.2
*
* @param {object} event Event object.
* @param {number} setFormId Set selected form to.
*/
refreshFormsList: function( event, setFormId ) {
if ( event ) {
event.preventDefault();
}
app.findFormSelector();
var data = {
action: 'wpforms_admin_get_form_selector_options',
nonce : wpformsElementorVars.nonce,
};
vars.$select.prop( 'disabled', true );
$.post( wpformsElementorVars.ajax_url, data )
.done( function( response ) {
if ( ! response.success ) {
app.debug( response );
return;
}
vars.formSelectOptions = response.data;
vars.$select.html( response.data );
if ( setFormId ) {
vars.formId = setFormId;
}
if ( vars.formId && vars.formId !== '' ) {
vars.$select.val( vars.formId ).trigger( 'change' );
}
} )
.fail( function( xhr, textStatus ) {
app.debug( {
xhr: xhr,
textStatus: textStatus,
} );
} )
.always( function() {
if ( ! vars.$select || vars.$select.length < 1 ) {
return;
}
vars.$select.prop( 'disabled', false );
var $formSelectOptions = vars.$select.find( 'option' ),
$formSelectControl = vars.$select.closest( '.elementor-control' );
if ( $formSelectOptions.length > 0 ) {
$formSelectControl.show();
$formSelectControl.siblings( '.elementor-control-add_form_notice' ).hide();
}
if ( parseInt( vars.$select.val(), 10 ) > 0 ) {
$formSelectControl.siblings( '.elementor-control-test_form_notice' ).show();
}
} );
},
/**
* Debug output helper.
*
* @since 1.6.2
*
* @param {mixed} msg Debug message.
*/
debug: function( msg ) {
if ( app.isDebug() ) {
console.log( 'WPForms Debug:', msg );
}
},
/**
* Is debug mode.
*
* @since 1.6.2
*
* @returns {boolean} True if the debug enabled.
*/
isDebug: function() {
return ( ( window.top.location.hash && '#wpformsdebug' === window.top.location.hash ) || wpformsElementorVars.debug );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementor.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,154 @@
/* global wpforms, wpformsElementorVars, wpformsModernFileUpload, wpformsRecaptchaLoad, grecaptcha, WPFormsRepeaterField, WPFormsStripePaymentElement */
/**
* WPForms integration with Elementor on the frontend.
*
* @since 1.6.2 Moved from `wpforms-elementor.js`
*/
var WPFormsElementorFrontend = window.WPFormsElementorFrontend || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.6.2
*
* @type {Object}
*/
var app = {
/**
* Flag to force load ChoicesJS.
*
* @since 1.9.0
*
* @type {boolean}
*/
forceLoadChoices: false,
/**
* Flag to force set Stripe.
*
* @since 1.9.3
*
* @type {boolean}
*/
forceSetStripe: false,
/**
* Start the engine.
*
* @since 1.6.2
*/
init() {
app.events();
},
/**
* Register JS events.
*
* @since 1.6.2
*/
events() {
window.addEventListener( 'elementor/popup/show', function( event ) {
const $modal = $( '#elementor-popup-modal-' + event.detail.id ),
$form = $modal.find( '.wpforms-form' );
if ( ! $form.length ) {
return;
}
app.forceSetStripe = true;
app.initFields( $form );
} );
// Add Elementor popup support for text limit.
window.addEventListener( 'elementor/popup/show', function() {
window.WPFormsTextLimit?.initHint( '.elementor-popup-modal' );
} );
// Force load ChoicesJS for elementor popup.
$( document ).on( 'elementor/popup/show', () => {
app.forceLoadChoices = true;
wpforms.loadChoicesJS();
} );
$( document ).on( 'wpformsBeforeLoadElementChoices', ( event, el ) => {
// Do not initialize on elementor popup.
if ( ! app.isFormInElementorPopup( el ) || app.forceLoadChoices ) {
return;
}
event.preventDefault();
} );
$( document ).on( 'wpformsBeforeStripePaymentElementSetup', ( event, el ) => {
// Do not initialize on elementor popup.
if ( ! app.isFormInElementorPopup( el ) || app.forceSetStripe ) {
return;
}
event.preventDefault();
} );
},
/**
* Check if the form is in Elementor popup.
*
* @since 1.9.3
*
* @param {Object} form Form element.
*
* @return {boolean} True if the form is in Elementor popup, false otherwise.
*/
isFormInElementorPopup( form ) {
return $( form ).parents( 'div[data-elementor-type="popup"]' ).length;
},
/**
* Init all things for WPForms.
*
* @since 1.6.2
*
* @param {Object} $form jQuery selector.
*/
initFields( $form ) { // eslint-disable-line complexity
// Init WPForms things.
wpforms.ready();
// Init `Modern File Upload` field.
if ( 'undefined' !== typeof wpformsModernFileUpload ) {
wpformsModernFileUpload.init();
}
// Init CAPTCHA.
if ( 'undefined' !== typeof wpformsRecaptchaLoad ) {
if ( 'recaptcha' === wpformsElementorVars.captcha_provider && 'v3' === wpformsElementorVars.recaptcha_type ) {
if ( 'undefined' !== typeof grecaptcha ) {
grecaptcha.ready( wpformsRecaptchaLoad );
}
} else {
wpformsRecaptchaLoad();
}
}
// Init Repeater fields.
if ( 'undefined' !== typeof WPFormsRepeaterField ) {
WPFormsRepeaterField.ready();
}
// Init Stripe payment.
if ( 'undefined' !== typeof WPFormsStripePaymentElement ) {
WPFormsStripePaymentElement.setupStripeForm( $form );
}
// Register a custom event.
$( document ).trigger( 'wpforms_elementor_form_fields_initialized', [ $form ] );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementorFrontend.init();
@@ -0,0 +1 @@
var WPFormsElementorFrontend=window.WPFormsElementorFrontend||((o,e,r)=>{var t={forceLoadChoices:!1,forceSetStripe:!1,init(){t.events()},events(){e.addEventListener("elementor/popup/show",function(e){e=r("#elementor-popup-modal-"+e.detail.id).find(".wpforms-form");e.length&&(t.forceSetStripe=!0,t.initFields(e))}),e.addEventListener("elementor/popup/show",function(){e.WPFormsTextLimit?.initHint(".elementor-popup-modal")}),r(o).on("elementor/popup/show",()=>{t.forceLoadChoices=!0,wpforms.loadChoicesJS()}),r(o).on("wpformsBeforeLoadElementChoices",(e,o)=>{t.isFormInElementorPopup(o)&&!t.forceLoadChoices&&e.preventDefault()}),r(o).on("wpformsBeforeStripePaymentElementSetup",(e,o)=>{t.isFormInElementorPopup(o)&&!t.forceSetStripe&&e.preventDefault()})},isFormInElementorPopup(e){return r(e).parents('div[data-elementor-type="popup"]').length},initFields(e){wpforms.ready(),"undefined"!=typeof wpformsModernFileUpload&&wpformsModernFileUpload.init(),"undefined"!=typeof wpformsRecaptchaLoad&&("recaptcha"===wpformsElementorVars.captcha_provider&&"v3"===wpformsElementorVars.recaptcha_type?"undefined"!=typeof grecaptcha&&grecaptcha.ready(wpformsRecaptchaLoad):wpformsRecaptchaLoad()),"undefined"!=typeof WPFormsRepeaterField&&WPFormsRepeaterField.ready(),"undefined"!=typeof WPFormsStripePaymentElement&&WPFormsStripePaymentElement.setupStripeForm(e),r(o).trigger("wpforms_elementor_form_fields_initialized",[e])}};return t})(document,window,jQuery);WPFormsElementorFrontend.init();
@@ -0,0 +1,965 @@
/* global elementor, elementorCommon, wpformsElementorVars, WPFormsElementorModern */
// noinspection TypeScriptUMDGlobal
/**
* @param wpformsElementorVars.route_namespace
* @param strings.form_themes
* @param strings.theme_name
* @param strings.theme_delete
* @param strings.theme_delete_title
* @param strings.theme_delete_confirm
* @param strings.theme_delete_cant_undone
* @param strings.theme_delete_yes
* @param strings.theme_copy
* @param strings.theme_custom
* @param strings.theme_noname
* @param strings.themes_error
* @param strings.button_background
* @param strings.button_text
* @param strings.field_label
* @param strings.field_sublabel
* @param strings.field_border
*/
// noinspection ES6ConvertVarToLetConst
/**
* WPForms integration with Elementor (modern widget).
*
* @since 1.9.6
*/
var WPFormsElementorThemes = window.WPFormsElementorThemes || ( function( document, window, $ ) { // eslint-disable-line no-var
/**
* Localized data aliases.
*
* @since 1.9.6
*/
const { isAdmin, isPro, isLicenseActive, strings, route_namespace: routeNamespace } = wpformsElementorVars;
/**
* Runtime state.
*
* @since 1.9.6
*
* @type {Object}
*/
const state = {};
/**
* Themes data.
*
* @since 1.9.6
*
* @type {Object}
*/
const themesData = {
wpforms: null,
custom: null,
};
/**
* Enabled themes.
*
* @since 1.9.6
*
* @type {Object}
*/
let enabledThemes = null;
/**
* Elements holder.
*
* @since 1.9.6
*
* @type {Object}
*/
const el = {};
// noinspection JSUnusedGlobalSymbols
/**
* Public functions and properties.
*
* @since 1.9.6
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.6
*/
init() {
el.$window = $( window );
app.fetchThemesData();
app.events();
},
/**
* Register JS events.
*
* @since 1.9.6
*/
events() {
// noinspection JSUnusedLocalSymbols
$( window )
.on( 'elementor/frontend/init', function() {
elementor.channels.editor.on( 'section:activated', app.themesControlSetup );
} );
},
/**
* Get all themes data.
*
* @since 1.9.6
*
* @return {Object} Themes data.
*/
getAllThemes() {
return { ...( themesData.custom || {} ), ...( themesData.wpforms || {} ) };
},
/**
* On section change event handler.
*
* @since 1.9.6
*
* @param {string} sectionName The current section name.
* @param {Object} editor Editor instance.
*/
themesControlSetup( sectionName, editor ) {
if ( sectionName !== 'themes' || editor.model.attributes.widgetType !== 'wpforms' ) {
return;
}
const $panelContent = editor.$childViewContainer[ 0 ];
const $themesControl = $( $panelContent ).find( '.wpforms-elementor-themes-control' );
// Scrollbar fix for Mac.
if ( app.isMac() ) {
$themesControl.addClass( 'wpforms-is-mac' );
}
app.updateThemesList( editor, $themesControl );
},
/**
* Update themes list.
*
* @since 1.9.6
* @param {Object} editor Editor instance.
* @param {Object} $themesControl Themes control object.
*/
updateThemesList( editor, $themesControl ) {
const selectedTheme = editor.model.attributes.settings.attributes.wpformsTheme ?? 'default';
// Get all themes.
const html = app.getThemesListMarkup( selectedTheme );
$themesControl.html( html );
app.addThemesEvents( $themesControl, editor );
},
/**
* On settings change event handler.
*
* @since 1.9.6
*
* @param {Object} $themesControl Themes control element.
* @param {Object} editor Editor instance.
*/
addThemesEvents( $themesControl, editor ) {
const debouncedMaybeCreate = _.debounce( ( settings ) => {
app.maybeCreateCustomTheme( settings );
}, 300 );
const settingsModel = editor.model.get( 'settings' );
if ( settingsModel.attributes.isMigrated !== 'true' ) {
app.maybeMigrateToCustomTheme( settingsModel, $themesControl, editor );
}
settingsModel.on( 'change', ( one ) => {
debouncedMaybeCreate( one.attributes );
app.maybeUpdateCustomTheme( one );
} );
const $radioButtons = $themesControl.find( '[role="radio"]' );
// Add event listeners to the radio buttons.
$radioButtons.off( 'click' ).on( 'click', function() {
$radioButtons.removeClass( 'is-active' );
$( this ).addClass( 'is-active' );
const selectedValue = $( this ).val();
app.selectTheme( selectedValue );
} );
// Add event listeners to the theme delete button.
elementor.channels.editor
.off( 'WPFormsDeleteThemeButtonClick' )
.on( 'WPFormsDeleteThemeButtonClick', () => {
app.deleteThemeModal( editor.model.attributes.settings.attributes, editor );
} );
// Listen for the theme name change.
editor.model.get( 'settings' )
.off( 'change:customThemeName' )
.on( 'change:customThemeName', function( model ) {
const newName = model.get( 'customThemeName' );
app.changeThemeName( newName, model );
app.updateThemesList( editor, $themesControl );
} );
},
/**
* Maybe migrate to the custom theme.
*
* @since 1.9.6
*
* @param {Object} settingsModel Settings model.
* @param {Object} $themesControl Themes Control object.
* @param {Object} editor Editor object.
*/
maybeMigrateToCustomTheme( settingsModel, $themesControl, editor ) {
const previousSettings = settingsModel._previousAttributes;
const atts = settingsModel.attributes;
if ( 'copyPasteJsonValue' in previousSettings && ! previousSettings.wpformsTheme && ! atts.isCustomTheme ) {
const currentStyles = app.getCurrentStyleAttributes( settingsModel.attributes );
app.createCustomTheme( settingsModel.attributes, currentStyles, true );
app.updateThemesList( editor, $themesControl );
}
settingsModel.setExternalChange( {
isMigrated: 'true',
} );
},
/**
* Maybe update the custom theme settings.
*
* @since 1.9.6
*
* @param {Object} model Settings model.
*/
maybeUpdateCustomTheme( model ) {
const atts = model.attributes;
const isCustomTheme = atts.isCustomTheme === 'true';
if ( ! isCustomTheme ) {
return;
}
const changedAtts = model.changed;
const allowedKeys = WPFormsElementorModern.getStyleAttributesKeys();
// Update only allowed attributes.
for ( const element in changedAtts ) {
if ( ! allowedKeys.includes( element ) ) {
continue;
}
const attrValue = WPFormsElementorModern.prepareComplexAttrValues( changedAtts[ element ], element );
app.updateCustomThemeAttribute( element, attrValue, atts );
}
},
/**
* Get the Themes control markup.
*
* @since 1.9.6
*
* @param {string} selectedTheme Selected theme slug.
*
* @return {string} Themes items HTML.
*/
// eslint-disable-next-line complexity
getThemesListMarkup( selectedTheme ) {
if ( ! themesData.wpforms ) {
app.fetchThemesData();
// Return markup with an error message if themes are not available.
return `<div class="wpforms-no-themes">${ strings.themes_error }</div>`;
}
const allThemes = app.getAllThemes();
if ( ! allThemes ) {
return '';
}
const themes = Object.keys( allThemes );
let theme, firstThemeSlug;
let html = '';
let itemsHtml = '';
if ( ! app.isWPFormsTheme( selectedTheme ) ) {
firstThemeSlug = selectedTheme;
itemsHtml += app.getThemesItemMarkup( app.getTheme( firstThemeSlug ), firstThemeSlug, firstThemeSlug );
}
for ( const key in themes ) {
const slug = themes[ key ];
// Skip the first theme.
if ( firstThemeSlug && firstThemeSlug === slug ) {
continue;
}
// Ensure that all the theme settings are present.
theme = { ...allThemes.default, ...( allThemes[ slug ] || {} ) };
theme.settings = { ...allThemes.default.settings, ...( theme.settings || {} ) };
itemsHtml += app.getThemesItemMarkup( theme, slug, selectedTheme );
}
html = `<div role="radiogroup" class="wpforms-elementor-themes-radio-group">
${ itemsHtml }
</div>`;
return html;
},
/**
* Get the Themes list item markup.
*
* @since 1.9.6
*
* @param {Object} theme Theme properties.
* @param {string} slug Theme slug.
* @param {string} selectedTheme Selected theme slug.
*
* @return {string} Themes items HTML.
*/
getThemesItemMarkup( theme, slug, selectedTheme ) {
if ( ! theme ) {
return '';
}
const title = theme.name?.length > 0 ? theme.name : strings.theme_noname;
let radioClasses = 'wpforms-elementor-themes-radio ';
const buttonClass = slug === selectedTheme ? 'is-active' : '';
radioClasses += app.isDisabledTheme( slug ) ? 'wpforms-elementor-themes-radio-disabled' : ' wpforms-elementor-themes-radio-enabled';
return `<button type="button" class="${ buttonClass }" value="${ slug }" role="radio">
<div class="wpforms-elementor-themes-radio ${ radioClasses }">
<div class="wpforms-elementor-themes-radio-title">${ title }</div>
</div>
<div class="wpforms-elementor-themes-indicators">
<span class="component-color-indicator" title="${ strings.button_background }" style="background: ${ theme.settings.buttonBackgroundColor };" data-index="0"></span>
<span class="component-color-indicator" title="${ strings.button_text }" style="background: ${ theme.settings.buttonTextColor }" data-index="1"></span>
<span class="component-color-indicator" title="${ strings.field_label }" style="background: ${ theme.settings.labelColor };" data-index="2"></span>
<span class="component-color-indicator" title="${ strings.field_sublabel } " style="background: ${ theme.settings.labelSublabelColor };" data-index="3"></span>
<span class="component-color-indicator" title="${ strings.field_border }" style="background: ${ theme.settings.fieldBorderColor };" data-index="4"></span>
</div>
</button>`;
},
/**
* Get theme data.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
*
* @return {Object|null} Theme settings.
*/
getTheme( slug ) {
return app.getAllThemes()[ slug ] || null;
},
/**
* Get enabled themes data.
*
* @since 1.9.6
*
* @return {Object} Themes data.
*/
getEnabledThemes() {
if ( enabledThemes ) {
return enabledThemes;
}
const allThemes = app.getAllThemes();
if ( isPro && isLicenseActive ) {
return allThemes;
}
enabledThemes = Object.keys( allThemes ).reduce( ( acc, key ) => {
if ( allThemes[ key ].settings?.fieldSize && ! allThemes[ key ].disabled ) {
acc[ key ] = allThemes[ key ];
}
return acc;
}, {} );
return enabledThemes;
},
/**
* Update enabled themes.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
* @param {Object} theme Theme settings.
*/
updateEnabledThemes( slug, theme ) {
if ( ! enabledThemes ) {
return;
}
enabledThemes = {
...enabledThemes,
[ slug ]: theme,
};
},
/**
* Whether the theme is disabled.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
*
* @return {boolean} True if the theme is disabled.
*/
isDisabledTheme( slug ) {
return ! app.getEnabledThemes()?.[ slug ];
},
/**
* Whether the theme is one of the WPForms themes.
*
* @since 1.9.6
*
* @param {string} slug Theme slug.
*
* @return {boolean} True if the theme is one of the WPForms themes.
*/
isWPFormsTheme( slug ) {
return Boolean( themesData.wpforms[ slug ]?.settings );
},
/**
* Fetch themes data from API.
*
* @since 1.9.6
*/
fetchThemesData() {
// If a fetch is already in progress, exit the function.
if ( state.isFetchingThemes || themesData.wpforms ) {
return;
}
// Set the flag to true indicating a fetch is in progress.
state.isFetchingThemes = true;
try {
// Fetch themes data.
wp.apiFetch( {
path: routeNamespace + 'elementor/themes/',
method: 'GET',
cache: 'no-cache',
} )
.then( ( response ) => {
themesData.wpforms = response.wpforms || {};
themesData.custom = response.custom || {};
} )
.catch( ( error ) => {
// eslint-disable-next-line no-console
console.error( error?.message );
} )
.finally( () => {
state.isFetchingThemes = false;
} );
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( error );
}
},
/**
* Save the custom themes.
*
* @since 1.9.6
*/
saveCustomThemes() {
if ( ! isAdmin ) {
return;
}
// Custom themes do not exist.
if ( state.isSavingThemes || ! themesData.custom ) {
return;
}
// Set the flag to true indicating a saving is in progress.
state.isSavingThemes = true;
try {
// Save themes.
wp.apiFetch( {
path: routeNamespace + 'elementor/themes/custom/',
method: 'POST',
data: { customThemes: themesData.custom },
} )
.then( ( response ) => {
if ( ! response?.result ) {
// eslint-disable-next-line no-console
console.log( response?.error );
}
} )
.catch( ( error ) => {
// eslint-disable-next-line no-console
console.error( error?.message );
} )
.finally( () => {
state.isSavingThemes = false;
} );
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( error );
}
},
/**
* Get the current style attributes state.
*
* @since 1.9.6
*
* @param {Object} atts Widget attributes.
*
* @return {Object} Whether the custom theme is created.
*/
getCurrentStyleAttributes( atts ) {
const defaultAttributes = Object.keys( themesData.wpforms.default?.settings );
const currentStyleAttributes = {};
for ( const key in defaultAttributes ) {
const attr = defaultAttributes[ key ];
currentStyleAttributes[ attr ] = WPFormsElementorModern.prepareComplexAttrValues( atts[ attr ], defaultAttributes[ key ] ) ?? '';
}
return currentStyleAttributes;
},
/**
* Maybe create a custom theme.
*
* @since 1.9.6
*
* @param {Object} atts Widget attributes.
*
* @return {boolean} Whether the custom theme is created.
*/
// eslint-disable-next-line complexity
maybeCreateCustomTheme( atts ) {
const currentStyles = app.getCurrentStyleAttributes( atts );
const isWPFormsTheme = !! themesData.wpforms[ atts.wpformsTheme ];
const isCustomTheme = !! themesData.custom[ atts.wpformsTheme ];
// It is one of the default themes without any changes.
if (
isWPFormsTheme &&
JSON.stringify( themesData.wpforms[ atts.wpformsTheme ]?.settings ) === JSON.stringify( currentStyles )
) {
return false;
}
// It is a modified default theme OR unknown custom theme.
if ( isWPFormsTheme || ! isCustomTheme ) {
app.createCustomTheme( atts, currentStyles );
}
return true;
},
/**
* Create a custom theme.
*
* @since 1.9.6
*
* @param {Object} atts Widget properties.
* @param {Object} currentStyles Current style settings.
* @param {boolean} migrateToCustomTheme Whether it is necessary to migrate to custom theme.
*
* @return {boolean} Whether the custom theme is created.
*/
createCustomTheme( atts, currentStyles = null, migrateToCustomTheme = false ) { // eslint-disable-line complexity
let counter = 0;
let themeSlug = atts.wpformsTheme;
const baseTheme = app.getTheme( atts.wpformsTheme ) || themesData.wpforms.default;
let themeName = baseTheme.name;
themesData.custom = themesData.custom || {};
if ( migrateToCustomTheme ) {
themeSlug = 'custom';
themeName = strings.theme_custom;
}
// Determine the theme slug and the number of copies.
do {
counter++;
themeSlug = themeSlug + '-copy-' + counter;
} while ( themesData.custom[ themeSlug ] && counter < 10000 );
const copyStr = counter < 2 ? strings.theme_copy : strings.theme_copy + ' ' + counter;
themeName += ' (' + copyStr + ')';
// The first migrated Custom Theme should be without a ` (Copy)` suffix.
themeName = migrateToCustomTheme && counter < 2 ? strings.theme_custom : themeName;
// Add the new custom theme.
themesData.custom[ themeSlug ] = {
name: themeName,
settings: currentStyles || app.getCurrentStyleAttributes( atts ),
};
app.updateEnabledThemes( themeSlug, themesData.custom[ themeSlug ] );
const widget = elementor.getPanelView().getCurrentPageView().getOption( 'editedElementView' );
const settingsModel = widget.model.get( 'settings' );
settingsModel.setExternalChange( {
wpformsTheme: themeSlug,
isCustomTheme: 'true',
customThemeName: themeName,
} );
return true;
},
/**
* Maybe create a custom theme by given attributes.
*
* @since 1.9.6
*
* @param {Object} attributes Widget attributes.
*
* @return {string} New theme's slug.
*/
maybeCreateCustomThemeFromAttributes( attributes ) { // eslint-disable-line complexity
const newThemeSlug = attributes.theme;
/**
* @type {Object|null}
* @property {Object} settings Theme settings.
*/
const existingTheme = app.getTheme( attributes.theme );
const keys = Object.keys( attributes );
let isExistingTheme = Boolean( existingTheme?.settings );
// Check if the theme already exists and has the same settings.
if ( isExistingTheme ) {
for ( const i in keys ) {
const key = keys[ i ];
if ( ! existingTheme.settings[ key ] || existingTheme.settings[ key ] !== attributes[ key ] ) {
isExistingTheme = false;
break;
}
}
}
// The theme exists and has the same settings.
if ( isExistingTheme ) {
return newThemeSlug;
}
// The theme doesn't exist.
// Normalize the attributes to the default theme settings.
const defaultAttributes = Object.keys( themesData.wpforms.default.settings );
const newSettings = {};
for ( const i in defaultAttributes ) {
const attr = defaultAttributes[ i ];
newSettings[ attr ] = attributes[ attr ] ?? '';
}
// Create a new custom theme.
themesData.custom[ newThemeSlug ] = {
name: attributes.themeName ?? strings.theme_custom,
settings: newSettings,
};
app.updateEnabledThemes( newThemeSlug, themesData.custom[ newThemeSlug ] );
return newThemeSlug;
},
/**
* Update custom theme.
*
* @since 1.9.6
*
* @param {string} attribute Attribute name.
* @param {string} value New attribute value.
* @param {Object} atts Widget properties.
*/
updateCustomThemeAttribute( attribute, value, atts ) { // eslint-disable-line complexity
const themeSlug = atts.wpformsTheme;
// Skip if it is one of the WPForms themes OR the attribute is not in the theme settings.
if (
themesData.wpforms[ themeSlug ] ||
(
attribute !== 'themeName' &&
! themesData.wpforms.default.settings[ attribute ]
)
) {
return;
}
// Skip if the custom theme doesn't exist in some rare cases.
if ( ! themesData.custom[ themeSlug ] ) {
return;
}
// Update the theme data.
if ( attribute === 'themeName' ) {
themesData.custom[ themeSlug ].name = value;
} else {
themesData.custom[ themeSlug ].settings = themesData.custom[ themeSlug ].settings || themesData.wpforms.default.settings;
themesData.custom[ themeSlug ].settings[ attribute ] = value;
}
},
/**
* Set the widget theme.
*
* @since 1.9.6
*
* @param {string} themeSlug The theme slug.
*
* @return {boolean} True on success.
*/
setWidgetTheme( themeSlug ) { // eslint-disable-line complexity
if ( app.maybeDisplayUpgradeModal( themeSlug ) ) {
return false;
}
const theme = app.getTheme( themeSlug );
if ( ! theme?.settings ) {
return false;
}
const attributes = Object.keys( theme.settings );
const widget = elementor.getPanelView().getCurrentPageView().getOption( 'editedElementView' );
const settingsModel = widget.model.get( 'settings' );
const isCustomTheme = !! themesData.custom[ themeSlug ];
// Set the theme attribute.
settingsModel.setExternalChange( {
wpformsTheme: themeSlug,
isCustomTheme: isCustomTheme ? 'true' : '',
customThemeName: isCustomTheme ? themesData.custom[ themeSlug ].name : '',
} );
// Clean up the attributes.
const cleanSettings = {};
for ( const key in attributes ) {
const attr = attributes[ key ];
const value = theme.settings[ attr ];
cleanSettings[ attr ] = typeof value === 'string'
? value.replace( /px$/, '' )
: value;
}
// Update the theme settings.
app.updateStylesAtts( cleanSettings, settingsModel );
// Activate the Publish button.
const $pageView = elementor.getPanelView().getCurrentPageView().$el;
$pageView.find( '.elementor-control-isCustomTheme input' ).trigger( 'input' );
return true;
},
/**
* Update styles atts.
*
* @since 1.9.6
*
* @param {Object} themeSettings Theme settings.
* @param {Object} settingsModel Settings model.
*/
// eslint-disable-next-line complexity
updateStylesAtts( themeSettings, settingsModel ) {
const allowedKeys = WPFormsElementorModern.getStyleAttributesKeys();
const validSettings = {};
for ( const key in themeSettings ) {
if ( ! allowedKeys.includes( key ) ) {
continue;
}
let value = themeSettings[ key ];
if ( key === 'backgroundUrl' && typeof value === 'string' ) {
const match = value.match( /^url\(\s*['"]?(.*?)['"]?\s*\)$/i );
if ( match && match[ 1 ] ) {
value = { id: '', url: match[ 1 ] };
} else {
value = '';
}
}
validSettings[ key ] = value;
}
// Update the widget settings.
if ( Object.keys( validSettings ).length ) {
settingsModel.setExternalChange( validSettings );
}
},
/**
* Maybe display upgrades modal in Lite.
*
* @since 1.9.6
*
* @param {string} themeSlug The theme slug.
*
* @return {boolean} True if modal was displayed.
*/
maybeDisplayUpgradeModal( themeSlug ) {
if ( ! app.isDisabledTheme( themeSlug ) ) {
return false;
}
if ( ! isPro ) {
WPFormsElementorModern.showProModal( 'themes', strings.form_themes );
return true;
}
if ( ! isLicenseActive ) {
WPFormsElementorModern.showLicenseModal( strings.form_themes );
return true;
}
return false;
},
/**
* Select widget theme event handler.
*
* @since 1.9.6
*
* @param {string} value New attribute value.
*/
selectTheme( value ) {
if ( ! app.setWidgetTheme( value ) ) {
return;
}
app.onSelectThemeWithBG( value );
},
/**
* Change theme name event handler.
*
* @since 1.9.6
*
* @param {string} value New attribute value.
* @param {Object} model Model object.
*/
changeThemeName( value, model ) {
app.updateCustomThemeAttribute( 'themeName', value, model.attributes );
},
/**
* Open the theme delete confirmation window.
*
* @since 1.9.6
*
* @param {Object} atts Widget properties.
* @param {Object} editor Editor object.
*/
deleteThemeModal( atts, editor ) {
const themeName = app.getTheme( atts.wpformsTheme )?.name;
const confirm = strings.theme_delete_confirm.replace( '%1$s', `<b>${ themeName }</b>` );
const content = `<p class="wpforms-theme-delete-text">${ confirm } ${ strings.theme_delete_cant_undone }</p>`;
const $panelContent = editor.$childViewContainer[ 0 ];
const $themesControl = $( $panelContent ).find( '.wpforms-elementor-themes-control' );
const dialog = elementorCommon.dialogsManager.createWidget( 'confirm', {
message: content,
headerMessage: strings.theme_delete_title,
onConfirm: () => {
// Remove theme from the theme storage.
delete themesData.custom[ atts.wpformsTheme ];
app.selectTheme( 'default' );
app.updateThemesList( editor, $themesControl );
},
} );
dialog.show();
},
/**
* Open stock photos install modal on the select theme.
*
* @since 1.9.6
*
* @param {string} themeSlug The theme slug.
*/
onSelectThemeWithBG( themeSlug ) {
if ( WPFormsElementorModern.stockPhotos.isPicturesAvailable() ) {
return;
}
// Check only WPForms themes.
if ( ! app.isWPFormsTheme( themeSlug ) ) {
return;
}
/**
* @type {Object|null}
* @property {Object|null} settings Settings.
*/
const theme = app.getTheme( themeSlug );
const bgUrl = theme.settings?.backgroundUrl;
if ( bgUrl?.length && bgUrl !== 'url()' ) {
WPFormsElementorModern.stockPhotos.installModal( 'themes' );
}
},
/**
* Determine if the user is on a Mac.
*
* @return {boolean} True if the user is on a Mac.
*/
isMac() {
return navigator.userAgent.includes( 'Macintosh' );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsElementorThemes.init();
@@ -0,0 +1,15 @@
var WPFormsElementorThemes=window.WPFormsElementorThemes||((e,m)=>{let{isAdmin:t,isPro:r,isLicenseActive:o,strings:u,route_namespace:s}=wpformsElementorVars,n={},h={wpforms:null,custom:null},a=null,i={},c={init(){i.$window=m(e),c.fetchThemesData(),c.events()},events(){m(e).on("elementor/frontend/init",function(){elementor.channels.editor.on("section:activated",c.themesControlSetup)})},getAllThemes(){return{...h.custom||{},...h.wpforms||{}}},themesControlSetup(e,t){"themes"===e&&"wpforms"===t.model.attributes.widgetType&&(e=t.$childViewContainer[0],e=m(e).find(".wpforms-elementor-themes-control"),c.isMac()&&e.addClass("wpforms-is-mac"),c.updateThemesList(t,e))},updateThemesList(e,t){var s=e.model.attributes.settings.attributes.wpformsTheme??"default",s=c.getThemesListMarkup(s);t.html(s),c.addThemesEvents(t,e)},addThemesEvents(s,r){let t=_.debounce(e=>{c.maybeCreateCustomTheme(e)},300);var e=r.model.get("settings");"true"!==e.attributes.isMigrated&&c.maybeMigrateToCustomTheme(e,s,r),e.on("change",e=>{t(e.attributes),c.maybeUpdateCustomTheme(e)});let o=s.find('[role="radio"]');o.off("click").on("click",function(){o.removeClass("is-active"),m(this).addClass("is-active");var e=m(this).val();c.selectTheme(e)}),elementor.channels.editor.off("WPFormsDeleteThemeButtonClick").on("WPFormsDeleteThemeButtonClick",()=>{c.deleteThemeModal(r.model.attributes.settings.attributes,r)}),r.model.get("settings").off("change:customThemeName").on("change:customThemeName",function(e){var t=e.get("customThemeName");c.changeThemeName(t,e),c.updateThemesList(r,s)})},maybeMigrateToCustomTheme(e,t,s){var r=e._previousAttributes,o=e.attributes;"copyPasteJsonValue"in r&&!r.wpformsTheme&&!o.isCustomTheme&&(r=c.getCurrentStyleAttributes(e.attributes),c.createCustomTheme(e.attributes,r,!0),c.updateThemesList(s,t)),e.setExternalChange({isMigrated:"true"})},maybeUpdateCustomTheme(e){var t=e.attributes;if("true"===t.isCustomTheme){var s,r,o=e.changed,m=WPFormsElementorModern.getStyleAttributesKeys();for(s in o)m.includes(s)&&(r=WPFormsElementorModern.prepareComplexAttrValues(o[s],s),c.updateCustomThemeAttribute(s,r,t))}},getThemesListMarkup(e){if(!h.wpforms)return c.fetchThemesData(),`<div class="wpforms-no-themes">${u.themes_error}</div>`;var t=c.getAllThemes();if(!t)return"";var s=Object.keys(t);let r,o;let m="";for(var n in c.isWPFormsTheme(e)||(o=e,m+=c.getThemesItemMarkup(c.getTheme(o),o,o)),s){n=s[n];o&&o===n||((r={...t.default,...t[n]||{}}).settings={...t.default.settings,...r.settings||{}},m+=c.getThemesItemMarkup(r,n,e))}return`<div role="radiogroup" class="wpforms-elementor-themes-radio-group">
${m}
</div>`},getThemesItemMarkup(e,t,s){var r,o;return e?(r=0<e.name?.length?e.name:u.theme_noname,o="wpforms-elementor-themes-radio ",`<button type="button" class="${t===s?"is-active":""}" value="${t}" role="radio">
<div class="wpforms-elementor-themes-radio ${o+=c.isDisabledTheme(t)?"wpforms-elementor-themes-radio-disabled":" wpforms-elementor-themes-radio-enabled"}">
<div class="wpforms-elementor-themes-radio-title">${r}</div>
</div>
<div class="wpforms-elementor-themes-indicators">
<span class="component-color-indicator" title="${u.button_background}" style="background: ${e.settings.buttonBackgroundColor};" data-index="0"></span>
<span class="component-color-indicator" title="${u.button_text}" style="background: ${e.settings.buttonTextColor}" data-index="1"></span>
<span class="component-color-indicator" title="${u.field_label}" style="background: ${e.settings.labelColor};" data-index="2"></span>
<span class="component-color-indicator" title="${u.field_sublabel} " style="background: ${e.settings.labelSublabelColor};" data-index="3"></span>
<span class="component-color-indicator" title="${u.field_border}" style="background: ${e.settings.fieldBorderColor};" data-index="4"></span>
</div>
</button>`):""},getTheme(e){return c.getAllThemes()[e]||null},getEnabledThemes(){if(!a){let s=c.getAllThemes();if(r&&o)return s;a=Object.keys(s).reduce((e,t)=>(s[t].settings?.fieldSize&&!s[t].disabled&&(e[t]=s[t]),e),{})}return a},updateEnabledThemes(e,t){a=a&&{...a,[e]:t}},isDisabledTheme(e){return!c.getEnabledThemes()?.[e]},isWPFormsTheme(e){return Boolean(h.wpforms[e]?.settings)},fetchThemesData(){if(!n.isFetchingThemes&&!h.wpforms){n.isFetchingThemes=!0;try{wp.apiFetch({path:s+"elementor/themes/",method:"GET",cache:"no-cache"}).then(e=>{h.wpforms=e.wpforms||{},h.custom=e.custom||{}}).catch(e=>{console.error(e?.message)}).finally(()=>{n.isFetchingThemes=!1})}catch(e){console.error(e)}}},saveCustomThemes(){if(t&&!n.isSavingThemes&&h.custom){n.isSavingThemes=!0;try{wp.apiFetch({path:s+"elementor/themes/custom/",method:"POST",data:{customThemes:h.custom}}).then(e=>{e?.result||console.log(e?.error)}).catch(e=>{console.error(e?.message)}).finally(()=>{n.isSavingThemes=!1})}catch(e){console.error(e)}}},getCurrentStyleAttributes(e){var t,s=Object.keys(h.wpforms.default?.settings),r={};for(t in s){var o=s[t];r[o]=WPFormsElementorModern.prepareComplexAttrValues(e[o],s[t])??""}return r},maybeCreateCustomTheme(e){var t=c.getCurrentStyleAttributes(e),s=!!h.wpforms[e.wpformsTheme],r=!!h.custom[e.wpformsTheme];return!(s&&JSON.stringify(h.wpforms[e.wpformsTheme]?.settings)===JSON.stringify(t)||(!s&&r||c.createCustomTheme(e,t),0))},createCustomTheme(e,t=null,s=!1){let r=0,o=e.wpformsTheme;let m=(c.getTheme(e.wpformsTheme)||h.wpforms.default).name;for(h.custom=h.custom||{},s&&(o="custom",m=u.theme_custom);r++,o=o+"-copy-"+r,h.custom[o]&&r<1e4;);var n=r<2?u.theme_copy:u.theme_copy+" "+r;return m+=" ("+n+")",m=s&&r<2?u.theme_custom:m,h.custom[o]={name:m,settings:t||c.getCurrentStyleAttributes(e)},c.updateEnabledThemes(o,h.custom[o]),elementor.getPanelView().getCurrentPageView().getOption("editedElementView").model.get("settings").setExternalChange({wpformsTheme:o,isCustomTheme:"true",customThemeName:m}),!0},maybeCreateCustomThemeFromAttributes(e){var t=e.theme,s=c.getTheme(e.theme),r=Object.keys(e);let o=Boolean(s?.settings);if(o)for(var m in r){m=r[m];if(!s.settings[m]||s.settings[m]!==e[m]){o=!1;break}}if(!o){var n,a=Object.keys(h.wpforms.default.settings),i={};for(n in a){var l=a[n];i[l]=e[l]??""}h.custom[t]={name:e.themeName??u.theme_custom,settings:i},c.updateEnabledThemes(t,h.custom[t])}return t},updateCustomThemeAttribute(e,t,s){s=s.wpformsTheme;h.wpforms[s]||"themeName"!==e&&!h.wpforms.default.settings[e]||h.custom[s]&&("themeName"===e?h.custom[s].name=t:(h.custom[s].settings=h.custom[s].settings||h.wpforms.default.settings,h.custom[s].settings[e]=t))},setWidgetTheme(e){if(c.maybeDisplayUpgradeModal(e))return!1;var t=c.getTheme(e);if(!t?.settings)return!1;var s,r=Object.keys(t.settings),o=elementor.getPanelView().getCurrentPageView().getOption("editedElementView").model.get("settings"),m=!!h.custom[e],n=(o.setExternalChange({wpformsTheme:e,isCustomTheme:m?"true":"",customThemeName:m?h.custom[e].name:""}),{});for(s in r){var a=r[s],i=t.settings[a];n[a]="string"==typeof i?i.replace(/px$/,""):i}return c.updateStylesAtts(n,o),elementor.getPanelView().getCurrentPageView().$el.find(".elementor-control-isCustomTheme input").trigger("input"),!0},updateStylesAtts(t,e){var s,r,o=WPFormsElementorModern.getStyleAttributesKeys(),m={};for(s in t)if(o.includes(s)){let e=t[s];"backgroundUrl"===s&&"string"==typeof e&&(r=e.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/i),e=r&&r[1]?{id:"",url:r[1]}:""),m[s]=e}Object.keys(m).length&&e.setExternalChange(m)},maybeDisplayUpgradeModal(e){return!(!c.isDisabledTheme(e)||(r?o||(WPFormsElementorModern.showLicenseModal(u.form_themes),0):(WPFormsElementorModern.showProModal("themes",u.form_themes),0)))},selectTheme(e){c.setWidgetTheme(e)&&c.onSelectThemeWithBG(e)},changeThemeName(e,t){c.updateCustomThemeAttribute("themeName",e,t.attributes)},deleteThemeModal(e,t){var s=c.getTheme(e.wpformsTheme)?.name,s=`<p class="wpforms-theme-delete-text">${u.theme_delete_confirm.replace("%1$s",`<b>${s}</b>`)} ${u.theme_delete_cant_undone}</p>`,r=t.$childViewContainer[0];let o=m(r).find(".wpforms-elementor-themes-control");elementorCommon.dialogsManager.createWidget("confirm",{message:s,headerMessage:u.theme_delete_title,onConfirm:()=>{delete h.custom[e.wpformsTheme],c.selectTheme("default"),c.updateThemesList(t,o)}}).show()},onSelectThemeWithBG(e){WPFormsElementorModern.stockPhotos.isPicturesAvailable()||c.isWPFormsTheme(e)&&(e=c.getTheme(e).settings?.backgroundUrl)?.length&&"url()"!==e&&WPFormsElementorModern.stockPhotos.installModal("themes")},isMac(){return navigator.userAgent.includes("Macintosh")}};return c})((document,window),jQuery);WPFormsElementorThemes.init();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,385 @@
/* global wpforms_gutenberg_form_selector, JSX */
/* jshint es3: false, esversion: 6 */
/**
* @param strings.update_wp_notice_head
* @param strings.update_wp_notice_text
* @param strings.update_wp_notice_link
* @param strings.wpforms_empty_help
* @param strings.wpforms_empty_info
*/
const { serverSideRender: ServerSideRender = wp.components.ServerSideRender } = wp;
const { createElement, Fragment } = wp.element;
const { registerBlockType } = wp.blocks;
const { InspectorControls } = wp.blockEditor || wp.editor;
const { SelectControl, ToggleControl, PanelBody, Placeholder } = wp.components;
const { __ } = wp.i18n;
const wpformsIcon = createElement( 'svg', { width: 20, height: 20, viewBox: '0 0 612 612', className: 'dashicon' },
createElement( 'path', {
fill: 'currentColor',
d: 'M544,0H68C30.445,0,0,30.445,0,68v476c0,37.556,30.445,68,68,68h476c37.556,0,68-30.444,68-68V68 C612,30.445,581.556,0,544,0z M464.44,68L387.6,120.02L323.34,68H464.44z M288.66,68l-64.26,52.02L147.56,68H288.66z M544,544H68 V68h22.1l136,92.14l79.9-64.6l79.56,64.6l136-92.14H544V544z M114.24,263.16h95.88v-48.28h-95.88V263.16z M114.24,360.4h95.88 v-48.62h-95.88V360.4z M242.76,360.4h255v-48.62h-255V360.4L242.76,360.4z M242.76,263.16h255v-48.28h-255V263.16L242.76,263.16z M368.22,457.3h129.54V408H368.22V457.3z',
} )
);
/**
* Popup container.
*
* @since 1.8.3
*
* @type {Object}
*/
let $popup = {};
/**
* Close button (inside the form builder) click event.
*
* @since 1.8.3
*
* @param {string} clientID Block Client ID.
*/
const builderCloseButtonEvent = function( clientID ) {
$popup
.off( 'wpformsBuilderInPopupClose' )
.on( 'wpformsBuilderInPopupClose', function( e, action, formId, formTitle ) {
if ( action !== 'saved' || ! formId ) {
return;
}
// Insert a new block when a new form is created from the popup to update the form list and attributes.
const newBlock = wp.blocks.createBlock( 'wpforms/form-selector', {
formId: formId.toString(), // Expects string value, make sure we insert string.
} );
// eslint-disable-next-line camelcase
wpforms_gutenberg_form_selector.forms = [ { ID: formId, post_title: formTitle } ];
// Insert a new block.
wp.data.dispatch( 'core/block-editor' ).removeBlock( clientID );
wp.data.dispatch( 'core/block-editor' ).insertBlocks( newBlock );
} );
};
/**
* Init Modern style Dropdown fields (<select>) with choiceJS.
*
* @since 1.9.0
*
* @param {Object} e Block Details.
*/
const loadChoiceJS = function( e ) {
if ( typeof window.Choices !== 'function' ) {
return;
}
const $form = jQuery( e.detail.block.querySelector( `#wpforms-${ e.detail.formId }` ) );
const config = window.wpforms_choicesjs_config || {};
$form.find( '.choicesjs-select' ).each( function( index, element ) {
if ( ! ( element instanceof HTMLSelectElement ) ) {
return;
}
const $el = jQuery( element );
if ( $el.data( 'choicesjs' ) ) {
return;
}
const $field = $el.closest( '.wpforms-field' );
config.callbackOnInit = function() {
const self = this,
$element = jQuery( self.passedElement.element ),
$input = jQuery( self.input.element ),
sizeClass = $element.data( 'size-class' );
// Add CSS-class for size.
if ( sizeClass ) {
jQuery( self.containerOuter.element ).addClass( sizeClass );
}
/**
* If a multiple select has selected choices - hide a placeholder text.
* In case if select is empty - we return placeholder text.
*/
if ( $element.prop( 'multiple' ) ) {
// On init event.
$input.data( 'placeholder', $input.attr( 'placeholder' ) );
if ( self.getValue( true ).length ) {
$input.removeAttr( 'placeholder' );
}
}
this.disable();
$field.find( '.is-disabled' ).removeClass( 'is-disabled' );
};
$el.data( 'choicesjs', new window.Choices( element, config ) );
// Placeholder fix on iframes.
if ( $el.val() ) {
$el.parent().find( '.choices__input' ).attr( 'style', 'display: none !important' );
}
} );
};
// on document ready
jQuery( function() {
jQuery( window ).on( 'wpformsFormSelectorFormLoaded', loadChoiceJS );
} );
/**
* Open builder popup.
*
* @since 1.6.2
*
* @param {string} clientID Block Client ID.
*/
const openBuilderPopup = function( clientID ) {
if ( jQuery.isEmptyObject( $popup ) ) {
const tmpl = jQuery( '#wpforms-gutenberg-popup' );
const parent = jQuery( '#wpwrap' );
parent.after( tmpl );
$popup = parent.siblings( '#wpforms-gutenberg-popup' );
}
const url = wpforms_gutenberg_form_selector.get_started_url,
$iframe = $popup.find( 'iframe' );
builderCloseButtonEvent( clientID );
$iframe.attr( 'src', url );
$popup.fadeIn();
};
const hasForms = function() {
return wpforms_gutenberg_form_selector.forms.length > 0;
};
registerBlockType( 'wpforms/form-selector', {
title: wpforms_gutenberg_form_selector.strings.title,
description: wpforms_gutenberg_form_selector.strings.description,
icon: wpformsIcon,
keywords: wpforms_gutenberg_form_selector.strings.form_keywords,
category: 'widgets',
attributes: {
formId: {
type: 'string',
},
displayTitle: {
type: 'boolean',
},
displayDesc: {
type: 'boolean',
},
preview: {
type: 'boolean',
},
},
example: {
attributes: {
preview: true,
},
},
supports: {
customClassName: hasForms(),
},
edit( props ) { // eslint-disable-line max-lines-per-function
const { attributes: { formId = '', displayTitle = false, displayDesc = false, preview = false }, setAttributes } = props;
const formOptions = wpforms_gutenberg_form_selector.forms.map( ( value ) => (
{ value: value.ID, label: value.post_title }
) );
const strings = wpforms_gutenberg_form_selector.strings;
let jsx;
formOptions.unshift( { value: '', label: wpforms_gutenberg_form_selector.strings.form_select } );
function selectForm( value ) { // eslint-disable-line jsdoc/require-jsdoc
setAttributes( { formId: value } );
}
function toggleDisplayTitle( value ) { // eslint-disable-line jsdoc/require-jsdoc
setAttributes( { displayTitle: value } );
}
function toggleDisplayDesc( value ) { // eslint-disable-line jsdoc/require-jsdoc
setAttributes( { displayDesc: value } );
}
/**
* Get block empty JSX code.
*
* @since 1.8.3
*
* @param {Object} blockProps Block properties.
*
* @return {JSX.Element} Block empty JSX code.
*/
function getEmptyFormsPreview( blockProps ) {
const clientId = blockProps.clientId;
return (
<Fragment
key="wpforms-gutenberg-form-selector-fragment-block-empty">
<div className="wpforms-no-form-preview">
<img src={ wpforms_gutenberg_form_selector.block_empty_url } alt="" />
<p dangerouslySetInnerHTML={ { __html: strings.wpforms_empty_info } }></p>
<button type="button" className="get-started-button components-button is-button is-primary"
onClick={
() => {
openBuilderPopup( clientId );
}
}
>
{ __( 'Get Started', 'wpforms-lite' ) }
</button>
<p className="empty-desc" dangerouslySetInnerHTML={ { __html: strings.wpforms_empty_help } }></p>
{ /* Template for popup with builder iframe */ }
<div id="wpforms-gutenberg-popup" className="wpforms-builder-popup">
<iframe src="about:blank" width="100%" height="100%" id="wpforms-builder-iframe" title="wpforms-gutenberg-popup"></iframe>
</div>
</div>
</Fragment>
);
}
/**
* Print empty forms notice.
*
* @since 1.8.3
*
* @param {string} clientId Block client ID.
*
* @return {JSX.Element} Field styles JSX code.
*/
function printEmptyFormsNotice( clientId ) {
return (
<InspectorControls key="wpforms-gutenberg-form-selector-inspector-main-settings">
<PanelBody className="wpforms-gutenberg-panel" title={ strings.form_settings }>
<p className="wpforms-gutenberg-panel-notice wpforms-warning wpforms-empty-form-notice" style={ { display: 'block' } }>
<strong>{ __( 'You havent created a form, yet!', 'wpforms-lite' ) }</strong>
{ __( 'What are you waiting for?', 'wpforms-lite' ) }
</p>
<button type="button" className="get-started-button components-button is-button is-secondary"
onClick={
() => {
openBuilderPopup( clientId );
}
}
>
{ __( 'Get Started', 'wpforms-lite' ) }
</button>
</PanelBody>
</InspectorControls>
);
}
/**
* Get styling panels preview.
*
* @since 1.8.8
*
* @return {JSX.Element} JSX code.
*/
function getStylingPanelsPreview() {
return (
<Fragment>
<PanelBody className="wpforms-gutenberg-panel disabled_panel" title={ strings.themes }>
<div className="wpforms-panel-preview wpforms-panel-preview-themes"></div>
</PanelBody>
<PanelBody className="wpforms-gutenberg-panel disabled_panel" title={ strings.field_styles }>
<div className="wpforms-panel-preview wpforms-panel-preview-field"></div>
</PanelBody>
<PanelBody className="wpforms-gutenberg-panel disabled_panel" title={ strings.label_styles }>
<div className="wpforms-panel-preview wpforms-panel-preview-label"></div>
</PanelBody>
<PanelBody className="wpforms-gutenberg-panel disabled_panel" title={ strings.button_styles }>
<div className="wpforms-panel-preview wpforms-panel-preview-button"></div>
</PanelBody>
<PanelBody className="wpforms-gutenberg-panel disabled_panel" title={ strings.container_styles }>
<div className="wpforms-panel-preview wpforms-panel-preview-container"></div>
</PanelBody>
<PanelBody className="wpforms-gutenberg-panel disabled_panel" title={ strings.background_styles }>
<div className="wpforms-panel-preview wpforms-panel-preview-background"></div>
</PanelBody>
</Fragment>
);
}
if ( ! hasForms() ) {
jsx = [ printEmptyFormsNotice( props.clientId ) ];
jsx.push( getEmptyFormsPreview( props ) );
return jsx;
}
jsx = [
<InspectorControls key="wpforms-gutenberg-form-selector-inspector-controls">
<PanelBody title={ wpforms_gutenberg_form_selector.strings.form_settings }>
<SelectControl
label={ wpforms_gutenberg_form_selector.strings.form_selected }
value={ formId }
options={ formOptions }
onChange={ selectForm }
/>
<ToggleControl
label={ wpforms_gutenberg_form_selector.strings.show_title }
checked={ displayTitle }
onChange={ toggleDisplayTitle }
/>
<ToggleControl
label={ wpforms_gutenberg_form_selector.strings.show_description }
checked={ displayDesc }
onChange={ toggleDisplayDesc }
/>
<p className="wpforms-gutenberg-panel-notice wpforms-warning">
<strong>{ strings.update_wp_notice_head }</strong>
{ strings.update_wp_notice_text } <a href={ strings.update_wp_notice_link } rel="noreferrer" target="_blank">{ strings.learn_more }</a>
</p>
</PanelBody>
{ getStylingPanelsPreview() }
</InspectorControls>,
];
if ( formId ) {
jsx.push(
<ServerSideRender
key="wpforms-gutenberg-form-selector-server-side-renderer"
block="wpforms/form-selector"
attributes={ props.attributes }
/>
);
} else if ( preview ) {
jsx.push(
<Fragment
key="wpforms-gutenberg-form-selector-fragment-block-preview">
<img src={ wpforms_gutenberg_form_selector.block_preview_url } style={ { width: '100%' } } alt="" />
</Fragment>
);
} else {
jsx.push(
<Placeholder
key="wpforms-gutenberg-form-selector-wrap"
className="wpforms-gutenberg-form-selector-wrap">
<img src={ wpforms_gutenberg_form_selector.logo_url } alt="" />
<SelectControl
key="wpforms-gutenberg-form-selector-select-control"
value={ formId }
options={ formOptions }
onChange={ selectForm }
/>
</Placeholder>
);
}
return jsx;
},
save() {
return null;
},
} );
@@ -0,0 +1,164 @@
/* global wpforms_gutenberg_form_selector */
/* jshint es3: false, esversion: 6 */
/**
* @param strings.custom_css
* @param strings.custom_css_notice
* @param strings.copy_paste_settings
* @param strings.copy_paste_notice
*/
/**
* Gutenberg editor block.
*
* Advanced Settings module.
*
* @since 1.8.8
*/
export default ( function( $ ) {
/**
* WP core components.
*
* @since 1.8.8
*/
const { addFilter } = wp.hooks;
const { createHigherOrderComponent } = wp.compose;
const { Fragment } = wp.element;
const { InspectorAdvancedControls } = wp.blockEditor || wp.editor;
const { TextareaControl } = wp.components;
/**
* Localized data aliases.
*
* @since 1.8.8
*/
const { strings } = wpforms_gutenberg_form_selector;
/**
* Public functions and properties.
*
* @since 1.8.8
*
* @type {Object}
*/
const app = {
/**
* Initialize module.
*
* @since 1.8.8
*
* @param {Object} commonModule Common module.
*/
init( commonModule ) {
app.common = commonModule;
app.hooks();
app.events();
},
/**
* Hooks.
*
* @since 1.8.8
*/
hooks() {
addFilter(
'editor.BlockEdit',
'editorskit/custom-advanced-control',
app.withAdvancedControls
);
},
/**
* Events.
*
* @since 1.8.8
*/
events() {
$( document )
.on( 'focus click', 'textarea', app.copyPasteFocus );
},
/**
* Copy / Paste Style Settings textarea focus event.
*
* @since 1.8.8
*/
copyPasteFocus() {
const $input = $( this );
if ( $input.siblings( 'label' ).text() === strings.copy_paste_settings ) {
// Select all text, so it's easier to copy and paste value.
$input.select();
}
},
/**
* Get fields.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
*
* @return {Object} Inspector advanced controls JSX code.
*/
getFields( props ) {
// Proceed only for WPForms block and when form ID is set.
if ( props?.name !== 'wpforms/form-selector' || ! props?.attributes?.formId ) {
return null;
}
// Common event handlers.
const handlers = app.common.getSettingsFieldsHandlers( props );
return (
<InspectorAdvancedControls>
<div className={ app.common.getPanelClass( props ) + ' advanced' }>
<TextareaControl
className="wpforms-gutenberg-form-selector-custom-css"
label={ strings.custom_css }
rows="5"
spellCheck="false"
value={ props.attributes.customCss }
onChange={ ( value ) => handlers.attrChange( 'customCss', value ) }
/>
<div className="wpforms-gutenberg-form-selector-legend" dangerouslySetInnerHTML={ { __html: strings.custom_css_notice } }></div>
<TextareaControl
className="wpforms-gutenberg-form-selector-copy-paste-settings"
label={ strings.copy_paste_settings }
rows="4"
spellCheck="false"
value={ props.attributes.copyPasteJsonValue }
onChange={ ( value ) => handlers.pasteSettings( value ) }
/>
<div className="wpforms-gutenberg-form-selector-legend" dangerouslySetInnerHTML={ { __html: strings.copy_paste_notice } }></div>
</div>
</InspectorAdvancedControls>
);
},
/**
* Add controls on Advanced Settings Panel.
*
* @param {Function} BlockEdit Block edit component.
*
* @return {Function} BlockEdit Modified block edit component.
*/
withAdvancedControls: createHigherOrderComponent(
( BlockEdit ) => {
return ( props ) => {
return (
<Fragment>
<BlockEdit { ...props } />
{ app.getFields( props ) }
</Fragment>
);
};
},
'withAdvancedControls'
),
};
// Provide access to public functions/properties.
return app;
}( jQuery ) );
@@ -0,0 +1,66 @@
/* global wpforms_gutenberg_form_selector */
/* jshint es3: false, esversion: 6 */
import PropTypes from 'prop-types';
/**
* @param strings.remove_image
*/
/**
* React component for the background preview.
*
* @since 1.8.8
*
* @param {Object} props Component props.
* @param {Object} props.attributes Block attributes.
* @param {Function} props.onRemoveBackground Function to remove the background.
* @param {Function} props.onPreviewClicked Function to handle the preview click.
*
* @return {Object} React component.
*/
const BackgroundPreview = ( { attributes, onRemoveBackground, onPreviewClicked } ) => {
const { Button } = wp.components;
const { strings } = wpforms_gutenberg_form_selector;
return (
<div className="wpforms-gutenberg-form-selector-background-preview">
<style>
{ `
.wpforms-gutenberg-form-selector-background-preview-image {
--wpforms-background-url: ${ attributes.backgroundUrl };
}
` }
</style>
<input
className="wpforms-gutenberg-form-selector-background-preview-image"
onClick={ onPreviewClicked }
tabIndex={ 0 }
type="button"
onKeyDown={
( event ) => {
if ( event.key === 'Enter' || event.key === ' ' ) {
onPreviewClicked();
}
}
}
>
</input>
<Button
isSecondary
className="is-destructive"
onClick={ onRemoveBackground }
>
{ strings.remove_image }
</Button>
</div>
);
};
BackgroundPreview.propTypes = {
attributes: PropTypes.object.isRequired,
onRemoveBackground: PropTypes.func.isRequired,
onPreviewClicked: PropTypes.func.isRequired,
};
export default BackgroundPreview;
@@ -0,0 +1,607 @@
/* global wpforms_gutenberg_form_selector */
/* jshint es3: false, esversion: 6 */
import BackgroundPreview from './background-preview.js';
/**
* @param strings.background_styles
* @param strings.bottom_center
* @param strings.bottom_left
* @param strings.bottom_right
* @param strings.center_center
* @param strings.center_left
* @param strings.center_right
* @param strings.choose_image
* @param strings.image_url
* @param strings.media_library
* @param strings.no_repeat
* @param strings.repeat_x
* @param strings.repeat_y
* @param strings.select_background_image
* @param strings.select_image
* @param strings.stock_photo
* @param strings.tile
* @param strings.top_center
* @param strings.top_left
* @param strings.top_right
*/
/**
* Gutenberg editor block.
*
* Background styles panel module.
*
* @since 1.8.8
*/
export default ( function() {
/**
* WP core components.
*
* @since 1.8.8
*/
const { PanelColorSettings } = wp.blockEditor || wp.editor;
const { SelectControl, PanelBody, Flex, FlexBlock, __experimentalUnitControl, TextControl, Button } = wp.components;
/**
* Localized data aliases.
*
* @since 1.8.8
*/
const { strings, defaults } = wpforms_gutenberg_form_selector;
/**
* Public functions and properties.
*
* @since 1.8.8
*
* @type {Object}
*/
const app = {
/**
* Get block attributes.
*
* @since 1.8.8
*
* @return {Object} Block attributes.
*/
getBlockAttributes() {
return {
backgroundImage: {
type: 'string',
default: defaults.backgroundImage,
},
backgroundPosition: {
type: 'string',
default: defaults.backgroundPosition,
},
backgroundRepeat: {
type: 'string',
default: defaults.backgroundRepeat,
},
backgroundSizeMode: {
type: 'string',
default: defaults.backgroundSizeMode,
},
backgroundSize: {
type: 'string',
default: defaults.backgroundSize,
},
backgroundWidth: {
type: 'string',
default: defaults.backgroundWidth,
},
backgroundHeight: {
type: 'string',
default: defaults.backgroundHeight,
},
backgroundColor: {
type: 'string',
default: defaults.backgroundColor,
},
backgroundUrl: {
type: 'string',
default: defaults.backgroundUrl,
},
};
},
/**
* Get Background Styles panel JSX code.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block handlers.
* @param {Object} formSelectorCommon Block properties.
* @param {Object} stockPhotos Stock Photos module.
* @param {Object} uiState UI state.
*
* @return {Object} Field styles JSX code.
*/
getBackgroundStyles( props, handlers, formSelectorCommon, stockPhotos, uiState ) { // eslint-disable-line max-lines-per-function, complexity
const isNotDisabled = uiState.isNotDisabled;
const isProEnabled = uiState.isProEnabled;
const showBackgroundPreview = uiState.showBackgroundPreview;
const setShowBackgroundPreview = uiState.setShowBackgroundPreview;
const lastBgImage = uiState.lastBgImage;
const setLastBgImage = uiState.setLastBgImage;
const tabIndex = isNotDisabled ? 0 : -1;
const cssClass = formSelectorCommon.getPanelClass( props ) + ( isNotDisabled ? '' : ' wpforms-gutenberg-panel-disabled' );
return (
<PanelBody className={ cssClass } title={ strings.background_styles }>
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
className="wpforms-gutenberg-form-selector-panel-body"
onClick={ ( event ) => {
if ( isNotDisabled ) {
return;
}
event.stopPropagation();
if ( ! isProEnabled ) {
return formSelectorCommon.education.showProModal( 'background', strings.background_styles );
}
formSelectorCommon.education.showLicenseModal( 'background', strings.background_styles, 'background-styles' );
} }
onKeyDown={ ( event ) => {
if ( isNotDisabled ) {
return;
}
event.stopPropagation();
if ( ! isProEnabled ) {
return formSelectorCommon.education.showProModal( 'background', strings.background_styles );
}
formSelectorCommon.education.showLicenseModal( 'background', strings.background_styles, 'background-styles' );
} }
>
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<SelectControl
label={ strings.image }
tabIndex={ tabIndex }
value={ props.attributes.backgroundImage }
options={ [
{ label: strings.none, value: 'none' },
{ label: strings.media_library, value: 'library' },
{ label: strings.stock_photo, value: 'stock' },
] }
onChange={ ( value ) => app.setContainerBackgroundImageWrapper( props, handlers, value, lastBgImage, setLastBgImage ) }
/>
</FlexBlock>
<FlexBlock>
{ ( props.attributes.backgroundImage !== 'none' || ! isNotDisabled ) && (
<SelectControl
label={ strings.position }
value={ props.attributes.backgroundPosition }
tabIndex={ tabIndex }
options={ [
{ label: strings.top_left, value: 'top left' },
{ label: strings.top_center, value: 'top center' },
{ label: strings.top_right, value: 'top right' },
{ label: strings.center_left, value: 'center left' },
{ label: strings.center_center, value: 'center center' },
{ label: strings.center_right, value: 'center right' },
{ label: strings.bottom_left, value: 'bottom left' },
{ label: strings.bottom_center, value: 'bottom center' },
{ label: strings.bottom_right, value: 'bottom right' },
] }
disabled={ ( props.attributes.backgroundImage === 'none' && isNotDisabled ) }
onChange={ ( value ) => handlers.styleAttrChange( 'backgroundPosition', value ) }
/>
) }
</FlexBlock>
</Flex>
{ ( props.attributes.backgroundImage !== 'none' || ! isNotDisabled ) && (
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<SelectControl
label={ strings.repeat }
tabIndex={ tabIndex }
value={ props.attributes.backgroundRepeat }
options={ [
{ label: strings.no_repeat, value: 'no-repeat' },
{ label: strings.tile, value: 'repeat' },
{ label: strings.repeat_x, value: 'repeat-x' },
{ label: strings.repeat_y, value: 'repeat-y' },
] }
disabled={ ( props.attributes.backgroundImage === 'none' && isNotDisabled ) }
onChange={ ( value ) => handlers.styleAttrChange( 'backgroundRepeat', value ) }
/>
</FlexBlock>
<FlexBlock>
<SelectControl
label={ strings.size }
tabIndex={ tabIndex }
value={ props.attributes.backgroundSizeMode }
options={ [
{ label: strings.dimensions, value: 'dimensions' },
{ label: strings.cover, value: 'cover' },
] }
disabled={ ( props.attributes.backgroundImage === 'none' && isNotDisabled ) }
onChange={ ( value ) => app.handleSizeFromDimensions( props, handlers, value ) }
/>
</FlexBlock>
</Flex>
) }
{ ( ( props.attributes.backgroundSizeMode === 'dimensions' && props.attributes.backgroundImage !== 'none' ) || ! isNotDisabled ) && (
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<__experimentalUnitControl
label={ strings.width }
tabIndex={ tabIndex }
value={ props.attributes.backgroundWidth }
isUnitSelectTabbable={ isNotDisabled }
onChange={ ( value ) => app.handleSizeFromWidth( props, handlers, value ) }
/>
</FlexBlock>
<FlexBlock>
<__experimentalUnitControl
label={ strings.height }
tabIndex={ tabIndex }
value={ props.attributes.backgroundHeight }
isUnitSelectTabbable={ isNotDisabled }
onChange={ ( value ) => app.handleSizeFromHeight( props, handlers, value ) }
/>
</FlexBlock>
</Flex>
) }
{ ( ! showBackgroundPreview || props.attributes.backgroundUrl === 'url()' ) && (
( props.attributes.backgroundImage === 'library' && (
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<Button
isSecondary
tabIndex={ tabIndex }
className={ 'wpforms-gutenberg-form-selector-media-library-button' }
onClick={ app.openMediaLibrary.bind( null, props, handlers, setShowBackgroundPreview ) }
>
{ strings.choose_image }
</Button>
</FlexBlock>
</Flex>
) ) || ( props.attributes.backgroundImage === 'stock' && (
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<Button
isSecondary
tabIndex={ tabIndex }
className={ 'wpforms-gutenberg-form-selector-media-library-button' }
onClick={ stockPhotos?.openModal.bind( null, props, handlers, 'bg-styles', setShowBackgroundPreview ) }
>
{ strings.choose_image }
</Button>
</FlexBlock>
</Flex>
) )
) }
{ ( ( showBackgroundPreview && props.attributes.backgroundImage !== 'none' ) || props.attributes.backgroundUrl !== 'url()' ) && (
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<div>
<BackgroundPreview
attributes={ props.attributes }
onRemoveBackground={
() => {
app.onRemoveBackground( setShowBackgroundPreview, handlers, setLastBgImage );
}
}
onPreviewClicked={ () => {
if ( props.attributes.backgroundImage === 'library' ) {
return app.openMediaLibrary( props, handlers, setShowBackgroundPreview );
}
return stockPhotos?.openModal( props, handlers, 'bg-styles', setShowBackgroundPreview );
} }
/>
</div>
<TextControl
label={ strings.image_url }
tabIndex={ tabIndex }
value={ props.attributes.backgroundImage !== 'none' && props.attributes.backgroundUrl }
className={ 'wpforms-gutenberg-form-selector-image-url' }
onChange={ ( value ) => handlers.styleAttrChange( 'backgroundUrl', value ) }
onLoad={ ( value ) => props.attributes.backgroundImage !== 'none' && handlers.styleAttrChange( 'backgroundUrl', value ) }
/>
</FlexBlock>
</Flex>
) }
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<div className="wpforms-gutenberg-form-selector-control-label">{ strings.colors }</div>
<PanelColorSettings
__experimentalIsRenderedInSidebar
enableAlpha
showTitle={ false }
tabIndex={ tabIndex }
className="wpforms-gutenberg-form-selector-color-panel"
colorSettings={ [
{
value: props.attributes.backgroundColor,
onChange: ( value ) => {
if ( ! isNotDisabled ) {
return;
}
handlers.styleAttrChange( 'backgroundColor', value );
},
label: strings.background,
},
] }
/>
</FlexBlock>
</Flex>
</div>
</PanelBody>
);
},
/**
* Open media library modal and handle image selection.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block handlers.
* @param {Function} setShowBackgroundPreview Set show background preview.
*/
openMediaLibrary( props, handlers, setShowBackgroundPreview ) {
const frame = wp.media( {
title: strings.select_background_image,
multiple: false,
library: {
type: 'image',
},
button: {
text: strings.select_image,
},
} );
frame.on( 'select', () => {
const attachment = frame.state().get( 'selection' ).first().toJSON();
const setAttr = {};
const attribute = 'backgroundUrl';
if ( attachment.url ) {
const value = `url(${ attachment.url })`;
setAttr[ attribute ] = value;
props.setAttributes( setAttr );
handlers.styleAttrChange( 'backgroundUrl', value );
setShowBackgroundPreview( true );
}
} );
frame.open();
},
/**
* Set container background image.
*
* @since 1.8.8
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*
* @return {boolean} True if the value was set, false otherwise.
*/
setContainerBackgroundImage( container, value ) {
if ( value === 'none' ) {
container.style.setProperty( `--wpforms-background-url`, 'url()' );
}
return true;
},
/**
* Set container background image.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block event handlers.
* @param {string} value Value.
* @param {string} lastBgImage Last background image.
* @param {Function} setLastBgImage Set last background image.
*/
setContainerBackgroundImageWrapper( props, handlers, value, lastBgImage, setLastBgImage ) {
if ( value === 'none' ) {
setLastBgImage( props.attributes.backgroundUrl );
props.attributes.backgroundUrl = 'url()';
handlers.styleAttrChange( 'backgroundUrl', 'url()' );
} else if ( lastBgImage ) {
props.attributes.backgroundUrl = lastBgImage;
handlers.styleAttrChange( 'backgroundUrl', lastBgImage );
}
handlers.styleAttrChange( 'backgroundImage', value );
},
/**
* Set container background position.
*
* @since 1.8.8
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*
* @return {boolean} True if the value was set, false otherwise.
*/
setContainerBackgroundPosition( container, value ) {
container.style.setProperty( `--wpforms-background-position`, value );
return true;
},
/**
* Set container background repeat.
*
* @since 1.8.8
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*
* @return {boolean} True if the value was set, false otherwise.
*/
setContainerBackgroundRepeat( container, value ) {
container.style.setProperty( `--wpforms-background-repeat`, value );
return true;
},
/**
* Handle real size from dimensions.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block handlers.
* @param {string} value Value.
*/
handleSizeFromDimensions( props, handlers, value ) {
if ( value === 'cover' ) {
props.attributes.backgroundSize = 'cover';
handlers.styleAttrChange( 'backgroundWidth', props.attributes.backgroundWidth );
handlers.styleAttrChange( 'backgroundHeight', props.attributes.backgroundHeight );
handlers.styleAttrChange( 'backgroundSizeMode', 'cover' );
handlers.styleAttrChange( 'backgroundSize', 'cover' );
} else {
props.attributes.backgroundSize = 'dimensions';
handlers.styleAttrChange( 'backgroundSizeMode', 'dimensions' );
handlers.styleAttrChange( 'backgroundSize', props.attributes.backgroundWidth + ' ' + props.attributes.backgroundHeight );
}
},
/**
* Handle real size from width.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block handlers.
* @param {string} value Value.
*/
handleSizeFromWidth( props, handlers, value ) {
props.attributes.backgroundSize = value + ' ' + props.attributes.backgroundHeight;
props.attributes.backgroundWidth = value;
handlers.styleAttrChange( 'backgroundSize', value + ' ' + props.attributes.backgroundHeight );
handlers.styleAttrChange( 'backgroundWidth', value );
},
/**
* Handle real size from height.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block handlers.
* @param {string} value Value.
*/
handleSizeFromHeight( props, handlers, value ) {
props.attributes.backgroundSize = props.attributes.backgroundWidth + ' ' + value;
props.attributes.backgroundHeight = value;
handlers.styleAttrChange( 'backgroundSize', props.attributes.backgroundWidth + ' ' + value );
handlers.styleAttrChange( 'backgroundHeight', value );
},
/**
* Set container background width.
*
* @since 1.8.8
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*
* @return {boolean} True if the value was set, false otherwise.
*/
setContainerBackgroundWidth( container, value ) {
container.style.setProperty( `--wpforms-background-width`, value );
return true;
},
/**
* Set container background height.
*
* @since 1.8.8
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*
* @return {boolean} True if the value was set, false otherwise.
*/
setContainerBackgroundHeight( container, value ) {
container.style.setProperty( `--wpforms-background-height`, value );
return true;
},
/**
* Set container background url.
*
* @since 1.8.8
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*
* @return {boolean} True if the value was set, false otherwise.
*/
setBackgroundUrl( container, value ) {
container.style.setProperty( `--wpforms-background-url`, value );
return true;
},
/**
* Set container background color.
*
* @since 1.8.8
*
* @param {HTMLElement} container Container element.
* @param {string} value Value.
*
* @return {boolean} True if the value was set, false otherwise.
*/
setBackgroundColor( container, value ) {
container.style.setProperty( `--wpforms-background-color`, value );
return true;
},
_showBackgroundPreview( props ) {
return props.attributes.backgroundImage !== 'none' &&
props.attributes.backgroundUrl &&
props.attributes.backgroundUrl !== 'url()';
},
/**
* Remove background image.
*
* @since 1.8.8
*
* @param {Function} setShowBackgroundPreview Set show background preview.
* @param {Object} handlers Block handlers.
* @param {Function} setLastBgImage Set last background image.
*/
onRemoveBackground( setShowBackgroundPreview, handlers, setLastBgImage ) {
setShowBackgroundPreview( false );
handlers.styleAttrChange( 'backgroundUrl', 'url()' );
setLastBgImage( '' );
},
};
return app;
}() );
@@ -0,0 +1,181 @@
/* global wpforms_gutenberg_form_selector */
/* jshint es3: false, esversion: 6 */
/**
* @param strings.border_radius
* @param strings.border_size
* @param strings.button_color_notice
* @param strings.button_styles
* @param strings.dashed
* @param strings.solid
*/
/**
* Gutenberg editor block.
*
* Button styles panel module.
*
* @since 1.8.8
*/
export default ( ( function() {
/**
* WP core components.
*
* @since 1.8.8
*/
const { PanelColorSettings } = wp.blockEditor || wp.editor;
const { SelectControl, PanelBody, Flex, FlexBlock, __experimentalUnitControl } = wp.components;
/**
* Localized data aliases.
*
* @since 1.8.8
*/
const { strings, defaults } = wpforms_gutenberg_form_selector;
// noinspection UnnecessaryLocalVariableJS
/**
* Public functions and properties.
*
* @since 1.8.8
*
* @type {Object}
*/
const app = {
/**
* Get block attributes.
*
* @since 1.8.8
*
* @return {Object} Block attributes.
*/
getBlockAttributes() {
return {
buttonSize: {
type: 'string',
default: defaults.buttonSize,
},
buttonBorderStyle: {
type: 'string',
default: defaults.buttonBorderStyle,
},
buttonBorderSize: {
type: 'string',
default: defaults.buttonBorderSize,
},
buttonBorderRadius: {
type: 'string',
default: defaults.buttonBorderRadius,
},
buttonBackgroundColor: {
type: 'string',
default: defaults.buttonBackgroundColor,
},
buttonTextColor: {
type: 'string',
default: defaults.buttonTextColor,
},
buttonBorderColor: {
type: 'string',
default: defaults.buttonBorderColor,
},
};
},
/**
* Get Button styles JSX code.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block event handlers.
* @param {Object} sizeOptions Size selector options.
* @param {Object} formSelectorCommon Form selector common object.
*
* @return {Object} Button styles JSX code.
*/
getButtonStyles( props, handlers, sizeOptions, formSelectorCommon ) { // eslint-disable-line max-lines-per-function
return (
<PanelBody className={ formSelectorCommon.getPanelClass( props ) } title={ strings.button_styles }>
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<SelectControl
label={ strings.size }
value={ props.attributes.buttonSize }
options={ sizeOptions }
onChange={ ( value ) => handlers.styleAttrChange( 'buttonSize', value ) }
/>
</FlexBlock>
<FlexBlock>
<SelectControl
label={ strings.border }
value={ props.attributes.buttonBorderStyle }
options={
[
{ label: strings.none, value: 'none' },
{ label: strings.solid, value: 'solid' },
{ label: strings.dashed, value: 'dashed' },
{ label: strings.dotted, value: 'dotted' },
]
}
onChange={ ( value ) => handlers.styleAttrChange( 'buttonBorderStyle', value ) }
/>
</FlexBlock>
</Flex>
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<__experimentalUnitControl
label={ strings.border_size }
value={ props.attributes.buttonBorderStyle === 'none' ? '' : props.attributes.buttonBorderSize }
min={ 0 }
disabled={ props.attributes.buttonBorderStyle === 'none' }
onChange={ ( value ) => handlers.styleAttrChange( 'buttonBorderSize', value ) }
isUnitSelectTabbable
/>
</FlexBlock>
<FlexBlock>
<__experimentalUnitControl
onChange={ ( value ) => handlers.styleAttrChange( 'buttonBorderRadius', value ) }
label={ strings.border_radius }
min={ 0 }
isUnitSelectTabbable
value={ props.attributes.buttonBorderRadius } />
</FlexBlock>
</Flex>
<div className="wpforms-gutenberg-form-selector-color-picker">
<div className="wpforms-gutenberg-form-selector-control-label">{ strings.colors }</div>
<PanelColorSettings
__experimentalIsRenderedInSidebar
enableAlpha
showTitle={ false }
className={ formSelectorCommon.getColorPanelClass( props.attributes.buttonBorderStyle ) }
colorSettings={ [
{
value: props.attributes.buttonBackgroundColor,
onChange: ( value ) => handlers.styleAttrChange( 'buttonBackgroundColor', value ),
label: strings.background,
},
{
value: props.attributes.buttonBorderColor,
onChange: ( value ) => handlers.styleAttrChange( 'buttonBorderColor', value ),
label: strings.border,
},
{
value: props.attributes.buttonTextColor,
onChange: ( value ) => handlers.styleAttrChange( 'buttonTextColor', value ),
label: strings.text,
},
] } />
<div className="wpforms-gutenberg-form-selector-legend wpforms-button-color-notice">
{ strings.button_color_notice }
</div>
</div>
</PanelBody>
);
},
};
return app;
} )() );
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,255 @@
/* global wpforms_gutenberg_form_selector */
/* jshint es3: false, esversion: 6 */
/**
* @param strings.border_color
* @param strings.border_style
* @param strings.border_width
* @param strings.container_styles
* @param strings.shadow_size
*/
/**
* Gutenberg editor block.
*
* Container styles panel module.
*
* @since 1.8.8
*/
export default ( ( $ ) => {
/**
* WP core components.
*
* @since 1.8.8
*/
const { PanelColorSettings } = wp.blockEditor || wp.editor;
const { SelectControl, PanelBody, Flex, FlexBlock, __experimentalUnitControl } = wp.components;
/**
* Localized data aliases.
*
* @since 1.8.8
*/
const { strings, defaults } = wpforms_gutenberg_form_selector;
/**
* Public functions and properties.
*
* @since 1.8.8
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.8.8
*/
init() {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.8.8
*/
ready() {
app.events();
},
/**
* Events.
*
* @since 1.8.8
*/
events() {
},
/**
* Get block attributes.
*
* @since 1.8.8
*
* @return {Object} Block attributes.
*/
getBlockAttributes() {
return {
containerPadding: {
type: 'string',
default: defaults.containerPadding,
},
containerBorderStyle: {
type: 'string',
default: defaults.containerBorderStyle,
},
containerBorderWidth: {
type: 'string',
default: defaults.containerBorderWidth,
},
containerBorderColor: {
type: 'string',
default: defaults.containerBorderColor,
},
containerBorderRadius: {
type: 'string',
default: defaults.containerBorderRadius,
},
containerShadowSize: {
type: 'string',
default: defaults.containerShadowSize,
},
};
},
/**
* Get Container Styles panel JSX code.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block handlers.
* @param {Object} formSelectorCommon Common form selector functions.
*
* @param {Object} uiState UI state.
*
* @return {Object} Field styles JSX code.
*/
getContainerStyles( props, handlers, formSelectorCommon, uiState ) { // eslint-disable-line max-lines-per-function, complexity
let cssClass = formSelectorCommon.getPanelClass( props );
const isNotDisabled = uiState.isNotDisabled;
const isProEnabled = uiState.isProEnabled;
if ( ! isNotDisabled ) {
cssClass += ' wpforms-gutenberg-panel-disabled';
}
return (
<PanelBody className={ cssClass } title={ strings.container_styles }>
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
className="wpforms-gutenberg-form-selector-panel-body"
onClick={ ( event ) => {
if ( isNotDisabled ) {
return;
}
event.stopPropagation();
if ( ! isProEnabled ) {
return formSelectorCommon.education.showProModal( 'container', strings.container_styles );
}
formSelectorCommon.education.showLicenseModal( 'container', strings.container_styles, 'container-styles' );
} }
onKeyDown={ ( event ) => {
if ( isNotDisabled ) {
return;
}
event.stopPropagation();
if ( ! isProEnabled ) {
return formSelectorCommon.education.showProModal( 'container', strings.container_styles );
}
formSelectorCommon.education.showLicenseModal( 'container', strings.container_styles, 'container-styles' );
} }
>
<Flex gap={ 4 } align="flex-start" className="wpforms-gutenberg-form-selector-flex" justify="space-between">
<FlexBlock>
<__experimentalUnitControl
label={ strings.padding }
tabIndex={ isNotDisabled ? 0 : -1 }
value={ props.attributes.containerPadding }
min={ 0 }
isUnitSelectTabbable={ isNotDisabled }
onChange={ ( value ) => handlers.styleAttrChange( 'containerPadding', value ) }
/>
</FlexBlock>
<FlexBlock>
<SelectControl
label={ strings.border_style }
tabIndex={ isNotDisabled ? 0 : -1 }
value={ props.attributes.containerBorderStyle }
options={ [
{ label: strings.none, value: 'none' },
{ label: strings.solid, value: 'solid' },
{ label: strings.dotted, value: 'dotted' },
{ label: strings.dashed, value: 'dashed' },
{ label: strings.double, value: 'double' },
] }
onChange={ ( value ) => handlers.styleAttrChange( 'containerBorderStyle', value ) }
/>
</FlexBlock>
</Flex>
<Flex gap={ 4 } align="flex-start" className="wpforms-gutenberg-form-selector-flex" justify="space-between">
<FlexBlock>
<__experimentalUnitControl
label={ strings.border_width }
tabIndex={ isNotDisabled ? 0 : -1 }
value={ props.attributes.containerBorderStyle === 'none' ? '' : props.attributes.containerBorderWidth }
min={ 0 }
disabled={ props.attributes.containerBorderStyle === 'none' }
isUnitSelectTabbable={ isNotDisabled }
onChange={ ( value ) => handlers.styleAttrChange( 'containerBorderWidth', value ) }
/>
</FlexBlock>
<FlexBlock>
<__experimentalUnitControl
label={ strings.border_radius }
tabIndex={ isNotDisabled ? 0 : -1 }
value={ props.attributes.containerBorderRadius }
min={ 0 }
isUnitSelectTabbable={ isNotDisabled }
onChange={ ( value ) => handlers.styleAttrChange( 'containerBorderRadius', value ) }
/>
</FlexBlock>
</Flex>
<Flex gap={ 4 } align="flex-start" className="wpforms-gutenberg-form-selector-flex" justify="space-between">
<FlexBlock>
<SelectControl
label={ strings.shadow_size }
tabIndex={ isNotDisabled ? 0 : -1 }
value={ props.attributes.containerShadowSize }
options={ [
{ label: strings.none, value: 'none' },
{ label: strings.small, value: 'small' },
{ label: strings.medium, value: 'medium' },
{ label: strings.large, value: 'large' },
] }
onChange={ ( value ) => handlers.styleAttrChange( 'containerShadowSize', value ) }
/>
</FlexBlock>
</Flex>
<Flex gap={ 4 } align="flex-start" className="wpforms-gutenberg-form-selector-flex" justify="space-between">
<FlexBlock>
<div className="wpforms-gutenberg-form-selector-control-label">{ strings.colors }</div>
<PanelColorSettings
__experimentalIsRenderedInSidebar
enableAlpha
showTitle={ false }
tabIndex={ isNotDisabled ? 0 : -1 }
className={ props.attributes.containerBorderStyle === 'none' ? 'wpforms-gutenberg-form-selector-color-panel wpforms-gutenberg-form-selector-color-panel-disabled' : 'wpforms-gutenberg-form-selector-color-panel' }
colorSettings={ [
{
value: props.attributes.containerBorderColor,
onChange: ( value ) => {
if ( ! isNotDisabled ) {
return;
}
handlers.styleAttrChange( 'containerBorderColor', value );
},
label: strings.border_color,
},
] }
/>
</FlexBlock>
</Flex>
</div>
</PanelBody>
);
},
};
return app;
} )( jQuery );
@@ -0,0 +1,76 @@
/* global wpforms_education, WPFormsEducation */
/**
* WPForms Education Modal module.
*
* @since 1.8.8
*/
export default ( ( $ ) => {
/**
* Public functions and properties.
*
* @since 1.8.8
*
* @type {Object}
*/
const app = {
/**
* Open educational popup for users with no Pro license.
*
* @since 1.8.8
*
* @param {string} panel Panel slug.
* @param {string} feature Feature name.
*/
showProModal( panel, feature ) {
const type = 'pro';
const message = wpforms_education.upgrade[ type ].message_plural.replace( /%name%/g, feature );
const utmContent = {
container: 'Upgrade to Pro - Container Styles',
background: 'Upgrade to Pro - Background Styles',
themes: 'Upgrade to Pro - Themes',
};
$.alert( {
backgroundDismiss: true,
title: feature + ' ' + wpforms_education.upgrade[ type ].title_plural,
icon: 'fa fa-lock',
content: message,
boxWidth: '550px',
theme: 'modern,wpforms-education',
closeIcon: true,
onOpenBefore: function() { // eslint-disable-line object-shorthand
this.$btnc.after( '<div class="discount-note">' + wpforms_education.upgrade_bonus + '</div>' );
this.$btnc.after( wpforms_education.upgrade[ type ].doc.replace( /%25name%25/g, 'AP - ' + feature ) );
this.$body.find( '.jconfirm-content' ).addClass( 'lite-upgrade' );
},
buttons: {
confirm: {
text: wpforms_education.upgrade[ type ].button,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action: () => {
window.open( WPFormsEducation.core.getUpgradeURL( utmContent[ panel ], type ), '_blank' );
WPFormsEducation.core.upgradeModalThankYou( type );
},
},
},
} );
},
/**
* Open license modal.
*
* @since 1.8.8
*
* @param {string} feature Feature name.
* @param {string} fieldName Field name.
* @param {string} utmContent UTM content.
*/
showLicenseModal( feature, fieldName, utmContent ) {
WPFormsEducation.proCore.licenseModal( feature, fieldName, utmContent );
},
};
return app;
} )( jQuery );
@@ -0,0 +1,189 @@
/* global wpforms_gutenberg_form_selector */
/* jshint es3: false, esversion: 6 */
/**
* @param strings.field_styles
* @param strings.lead_forms_panel_notice_head
* @param strings.lead_forms_panel_notice_text
* @param strings.learn_more
* @param strings.use_modern_notice_head
* @param strings.use_modern_notice_link
* @param strings.use_modern_notice_text
*/
/**
* Gutenberg editor block.
*
* Field styles panel module.
*
* @since 1.8.8
*/
export default ( ( function() {
/**
* WP core components.
*
* @since 1.8.8
*/
const { PanelColorSettings } = wp.blockEditor || wp.editor;
const { SelectControl, PanelBody, Flex, FlexBlock, __experimentalUnitControl } = wp.components;
/**
* Localized data aliases.
*
* @since 1.8.8
*/
const { strings, defaults } = wpforms_gutenberg_form_selector;
// noinspection UnnecessaryLocalVariableJS
/**
* Public functions and properties.
*
* @since 1.8.8
*
* @type {Object}
*/
const app = {
/**
* Get block attributes.
*
* @since 1.8.8
*
* @return {Object} Block attributes.
*/
getBlockAttributes() {
return {
fieldSize: {
type: 'string',
default: defaults.fieldSize,
},
fieldBorderStyle: {
type: 'string',
default: defaults.fieldBorderStyle,
},
fieldBorderSize: {
type: 'string',
default: defaults.fieldBorderSize,
},
fieldBorderRadius: {
type: 'string',
default: defaults.fieldBorderRadius,
},
fieldBackgroundColor: {
type: 'string',
default: defaults.fieldBackgroundColor,
},
fieldBorderColor: {
type: 'string',
default: defaults.fieldBorderColor,
},
fieldTextColor: {
type: 'string',
default: defaults.fieldTextColor,
},
fieldMenuColor: {
type: 'string',
default: defaults.fieldMenuColor,
},
};
},
/**
* Get Field styles JSX code.
*
* @since 1.8.8
*
* @param {Object} props Block properties.
* @param {Object} handlers Block event handlers.
* @param {Object} sizeOptions Size selector options.
* @param {Object} formSelectorCommon Form selector common object.
*
* @return {Object} Field styles JSX code.
*/
getFieldStyles( props, handlers, sizeOptions, formSelectorCommon ) { // eslint-disable-line max-lines-per-function
return (
<PanelBody className={ formSelectorCommon.getPanelClass( props ) } title={ strings.field_styles }>
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<SelectControl
label={ strings.size }
value={ props.attributes.fieldSize }
options={ sizeOptions }
onChange={ ( value ) => handlers.styleAttrChange( 'fieldSize', value ) }
/>
</FlexBlock>
<FlexBlock>
<SelectControl
label={ strings.border }
value={ props.attributes.fieldBorderStyle }
options={
[
{ label: strings.none, value: 'none' },
{ label: strings.solid, value: 'solid' },
{ label: strings.dashed, value: 'dashed' },
{ label: strings.dotted, value: 'dotted' },
]
}
onChange={ ( value ) => handlers.styleAttrChange( 'fieldBorderStyle', value ) }
/>
</FlexBlock>
</Flex>
<Flex gap={ 4 } align="flex-start" className={ 'wpforms-gutenberg-form-selector-flex' } justify="space-between">
<FlexBlock>
<__experimentalUnitControl
label={ strings.border_size }
value={ props.attributes.fieldBorderStyle === 'none' ? '' : props.attributes.fieldBorderSize }
min={ 0 }
disabled={ props.attributes.fieldBorderStyle === 'none' }
onChange={ ( value ) => handlers.styleAttrChange( 'fieldBorderSize', value ) }
isUnitSelectTabbable
/>
</FlexBlock>
<FlexBlock>
<__experimentalUnitControl
label={ strings.border_radius }
value={ props.attributes.fieldBorderRadius }
min={ 0 }
isUnitSelectTabbable
onChange={ ( value ) => handlers.styleAttrChange( 'fieldBorderRadius', value ) }
/>
</FlexBlock>
</Flex>
<div className="wpforms-gutenberg-form-selector-color-picker">
<div className="wpforms-gutenberg-form-selector-control-label">{ strings.colors }</div>
<PanelColorSettings
__experimentalIsRenderedInSidebar
enableAlpha
showTitle={ false }
className={ formSelectorCommon.getColorPanelClass( props.attributes.fieldBorderStyle ) }
colorSettings={ [
{
value: props.attributes.fieldBackgroundColor,
onChange: ( value ) => handlers.styleAttrChange( 'fieldBackgroundColor', value ),
label: strings.background,
},
{
value: props.attributes.fieldBorderColor,
onChange: ( value ) => handlers.styleAttrChange( 'fieldBorderColor', value ),
label: strings.border,
},
{
value: props.attributes.fieldTextColor,
onChange: ( value ) => handlers.styleAttrChange( 'fieldTextColor', value ),
label: strings.text,
},
{
value: props.attributes.fieldMenuColor,
onChange: ( value ) => handlers.styleAttrChange( 'fieldMenuColor', value ),
label: strings.menu,
},
] }
/>
</div>
</PanelBody>
);
},
};
return app;
} )() );
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,416 @@
/* global wpforms_builder, WPFormsBuilderPaymentsUtils */
/**
* WPForms Square builder function.
*
* @since 1.9.5
*/
const WPFormsBuilderSquare = window.WPFormsBuilderSquare || ( function( document, window, $ ) {
/**
* Elements holder.
*
* @since 1.9.5
*
* @type {Object}
*/
const el = {};
/**
* Public functions and properties.
*
* @since 1.9.5
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.5
*/
init() {
$( app.ready );
},
/**
* Initialized once the DOM and Providers are fully loaded.
*
* @since 1.9.5
*/
ready() {
// Cache DOM elements.
el.$singlePaymentControl = $( '#wpforms-panel-field-square-enable_one_time' );
el.$recurringPaymentControl = $( '#wpforms-panel-field-square-enable_recurring' );
el.$panelContent = $( '#wpforms-panel-content-section-payment-square' );
el.$AJAXSubmitOption = $( '#wpforms-panel-field-settings-ajax_submit' );
el.$cardButton = $( '#wpforms-add-fields-square' );
el.$alert = $( '#wpforms-square-credit-card-alert' );
el.$feeNotice = $( '.wpforms-square-notice-info' );
app.bindUIActions();
app.bindPlanUIActions();
if ( ! wpforms_builder.square_is_pro ) {
const baseSelector = '.wpforms-panel-content-section-square',
toggleInput = `${ baseSelector } .wpforms-panel-content-section-payment-toggle input`,
planNameInput = `${ baseSelector } .wpforms-panel-content-section-payment-plan-name input`;
$( toggleInput ).each( WPFormsBuilderPaymentsUtils.toggleContent );
$( planNameInput ).each( WPFormsBuilderPaymentsUtils.checkPlanName );
$( '#wpforms-panel-payments' )
.on( 'click', toggleInput, WPFormsBuilderPaymentsUtils.toggleContent )
.on( 'click', `${ baseSelector } .wpforms-panel-content-section-payment-plan-head-buttons-toggle`, WPFormsBuilderPaymentsUtils.togglePlan )
.on( 'click', `${ baseSelector } .wpforms-panel-content-section-payment-plan-head-buttons-delete`, WPFormsBuilderPaymentsUtils.deletePlan )
.on( 'input', planNameInput, WPFormsBuilderPaymentsUtils.renamePlan )
.on( 'focusout', planNameInput, WPFormsBuilderPaymentsUtils.checkPlanName );
}
},
/**
* Process various events.
*
* @since 1.9.5
*/
bindUIActions() {
$( document ).on( 'wpformsSaved', app.ajaxRequiredCheck )
.on( 'wpformsSaved', app.paymentsEnabledCheck )
.on( 'wpformsSaved', app.requiredFieldsCheck )
.on( 'wpformsFieldAdd', app.fieldAdded )
.on( 'wpformsFieldDelete', app.fieldDeleted )
.on( 'wpformsPaymentsPlanCreated', app.toggleMultiplePlansWarning )
.on( 'wpformsPaymentsPlanCreated', app.bindPlanUIActions )
.on( 'wpformsPaymentsPlanDeleted', app.toggleMultiplePlansWarning );
el.$cardButton.on( 'click', app.connectionCheck );
},
/**
* Bind plan UI actions.
*
* @since 1.9.5
*/
bindPlanUIActions() {
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan-body .wpforms-panel-field-select select' ).on( 'change', app.resetRequiredPlanFieldError );
},
/**
* Notify user if AJAX submission is not required.
*
* @since 1.9.5
*/
ajaxRequiredCheck() {
if ( ! $( '#wpforms-panel-fields .wpforms-field.wpforms-field-square' ).length ) {
return;
}
if ( app.isAJAXSubmitEnabled() ) {
return;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.square_ajax_required,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Notify user if Square Payments are not enabled.
*
* @since 1.9.5
*/
paymentsEnabledCheck() {
if ( ! $( '#wpforms-panel-fields .wpforms-field.wpforms-field-square' ).length ) {
return;
}
if ( app.isPaymentsEnabled() ) {
return;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.square_payments_enabled_required,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* On form save notify users about required fields.
*
* @since 1.9.5
*/
requiredFieldsCheck() {
if ( ! el.$recurringPaymentControl.is( ':checked' ) || el.$panelContent.hasClass( 'wpforms-hidden' ) ) {
return;
}
let showAlert = false;
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan' ).each( function() {
const $plan = $( this ),
planId = $plan.data( 'plan-id' ),
$emailField = $( `#wpforms-panel-field-square-recurring-${ planId }-customer_email` ),
$nameField = $( `#wpforms-panel-field-square-recurring-${ planId }-customer_name` );
if (
! $emailField.val()
) {
$emailField.addClass( 'wpforms-required-field-error' );
showAlert = true;
}
if (
! $nameField.val()
) {
$nameField.addClass( 'wpforms-required-field-error' );
showAlert = true;
}
} );
if ( ! showAlert ) {
return;
}
let alertMessage = wpforms_builder.square_recurring_payments_fields_required;
if ( ! $( '.wpforms-panel-content-section-square' ).is( ':visible' ) ) {
alertMessage += ' ' + wpforms_builder.square_recurring_payments_fields_settings;
}
$.alert( {
title: wpforms_builder.square_recurring_payments_fields_heading,
content: alertMessage,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
onOpen() {
$( '.wpforms-square-settings-redirect' ).on( 'click', app.settingsRedirect );
},
} );
},
/**
* Redirect to the settings tab.
*
* @since 1.9.5
*/
settingsRedirect() {
// Open the Square settings tab.
$( '.wpforms-panel-payments-button' ).trigger( 'click' );
$( '.wpforms-panel-sidebar-section-square' ).trigger( 'click' );
// Scroll to the Stripe settings.
window.location.href = window.location.pathname + window.location.search + '#wpforms-panel-field-square-enable_recurring-wrap';
// Close the alert.
$( this ).closest( '.jconfirm-box' ).find( '.btn-confirm' ).trigger( 'click' );
},
/**
* Maybe reset required recurring field error class.
*
* @since 1.9.5
*/
resetRequiredPlanFieldError() {
const $nameAttr = $( this ).attr( 'name' );
if ( ! $nameAttr.includes( 'customer_email' ) && ! $nameAttr.includes( 'customer_name' ) ) {
return;
}
$( this ).toggleClass( 'wpforms-required-field-error', ! $( this ).val() );
},
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Notify user if Square connection are missing.
*
* @since 1.9.5
*
* @return {boolean} False if button clicks should be prevented.
*/
connectionCheck() {
if ( $( this ).hasClass( 'wpforms-add-fields-button-disabled' ) ) {
return false;
}
if ( ! $( this ).hasClass( 'square-connection-required' ) ) {
return true;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.square_connection_required,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* We have to do several actions when the "Square" field is added.
*
* @since 1.9.5
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
fieldAdded( e, id, type ) {
if ( type === 'square' ) {
app.cardButtonToggle( true );
app.settingsToggle( true );
app.paymentsEnabledCheck();
el.$feeNotice.toggleClass( 'wpforms-hidden' );
}
},
/**
* We have to do several actions for UI when the "Square" credit card field is deleted.
*
* @since 1.9.5
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
fieldDeleted( e, id, type ) {
if ( type === 'square' ) {
app.cardButtonToggle( false );
app.settingsToggle( false );
app.disablePayments();
app.disableNotifications();
el.$feeNotice.toggleClass( 'wpforms-hidden' );
}
},
/**
* Toggles visibility of multiple plans warning.
*
* @since 1.9.5
*/
toggleMultiplePlansWarning() {
el.$panelContent.find( '.wpforms-square-multiple-plans-warning' ).toggleClass( 'wpforms-hidden', el.$panelContent.find( '.wpforms-panel-content-section-payment-plan' ).length === 1 );
},
/**
* Enable or disable the "Square" field in the fields list.
*
* @since 1.9.5
*
* @param {boolean} isDisabled If true then a card button will be disabled.
*/
cardButtonToggle( isDisabled ) {
el.$cardButton
.prop( 'disabled', isDisabled )
.toggleClass( 'wpforms-add-fields-button-disabled', isDisabled );
},
/**
* Toggle visibility of the Square payment settings.
*
* If the "Square" field has been added then reveal the settings,
* otherwise hide them.
*
* @since 1.9.5
*
* @param {boolean} display Show or hide settings.
*/
settingsToggle( display ) {
if ( ! el.$alert.length ) {
return;
}
el.$alert.toggleClass( 'wpforms-hidden', display );
$( '#wpforms-panel-content-section-payment-square' ).toggleClass( 'wpforms-hidden', ! display );
// Uncheck the Payments > Square > Enable Square Payments setting.
if ( ! display ) {
el.$singlePaymentControl.prop( 'checked', false ).trigger( 'change' );
el.$recurringPaymentControl.prop( 'checked', false ).trigger( 'change' );
}
},
/**
* Make sure that "One-Time Payments" and "Recurring Payments" toggles are turned off.
*
* @since 1.9.5
*/
disablePayments() {
const toggleInput = $( '#wpforms-panel-field-square-enable_one_time, #wpforms-panel-field-square-enable_recurring' );
toggleInput.prop( 'checked', false ).trigger( 'change' ).each( WPFormsBuilderPaymentsUtils.toggleContent );
},
/**
* Disable notifications.
*
* @since 1.9.5
*/
disableNotifications() {
const $notificationWrap = $( '.wpforms-panel-content-section-notifications [id*="-square-wrap"]' );
$notificationWrap.find( 'input[id*="-square"]' ).prop( 'checked', false );
$notificationWrap.addClass( 'wpforms-hidden' );
},
/**
* Determine whether payments are enabled in the Payments > Square panel.
*
* @since 1.9.5
*
* @return {boolean} Payments are enabled.
*/
isPaymentsEnabled() {
return el.$singlePaymentControl.is( ':checked' ) || el.$recurringPaymentControl.is( ':checked' );
},
/**
* Determine whether AJAX form submission is enabled in the Settings > General.
*
* @since 1.9.5
*
* @return {boolean} AJAX form submission is enabled.
*/
isAJAXSubmitEnabled() {
return el.$AJAXSubmitOption.is( ':checked' );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsBuilderSquare.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,375 @@
/* global wpforms_admin, WPFormsAdmin, wpf */
/**
* WPForms Square settings function.
*
* @since 1.9.5
*/
const WPFormsSettingsSquare = window.WPFormsSettingsSquare || ( function( document, window, $ ) {
/**
* Elements.
*
* @since 1.9.5
*
* @type {Object}
*/
const $el = {
sandboxModeCheckbox: $( '#wpforms-setting-square-sandbox-mode' ),
sandboxConnectionStatusBlock: $( '#wpforms-setting-row-square-connection-status-sandbox' ),
productionConnectionStatusBlock: $( '#wpforms-setting-row-square-connection-status-production' ),
sandboxLocationBlock: $( '#wpforms-setting-row-square-location-id-sandbox' ),
sandboxLocationStatusBlock: $( '#wpforms-setting-row-square-location-status-sandbox' ),
productionLocationBlock: $( '#wpforms-setting-row-square-location-id-production' ),
productionLocationStatusBlock: $( '#wpforms-setting-row-square-location-status-production' ),
refreshBtn: $( '.wpforms-square-refresh-btn' ),
copyButton: $( '#wpforms-setting-row-square-webhooks-endpoint-set .wpforms-copy-to-clipboard' ),
webhooksEnableCheckbox: $( '#wpforms-setting-square-webhooks-enabled' ),
webhookEndpointUrl: $( 'input#wpforms-square-webhook-endpoint-url' ),
webhookMethod: $( 'input[name="square-webhooks-communication"]' ),
webhookCommunicationStatusNotice: $( '#wpforms-setting-row-square-webhooks-communication-status' ),
webhookConnectBtn: $( '#wpforms-setting-square-webhooks-connect' ),
webhookConnectRow: $( '#wpforms-setting-row-square-webhooks-connect' ),
webhookConnectStatusRow: $( '#wpforms-setting-row-square-webhooks-connect-status-production, #wpforms-setting-row-square-webhooks-connect-status-sandbox' ),
};
/**
* Public functions and properties.
*
* @since 1.9.5
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.9.5
*/
init() {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.9.5
*/
ready() {
app.events();
},
/**
* Register JS events.
*
* @since 1.9.5
*/
events() {
$el.sandboxModeCheckbox.on( 'change', app.credentialsFieldsDisplay );
$el.refreshBtn.on( 'click', app.refreshTokensCallback );
$el.webhooksEnableCheckbox.on( 'change', app.webhooksEnableCallback );
$el.webhookConnectBtn.on( 'click', app.modals.displayWebhookConfigPopup );
$el.webhookMethod.on( 'change', app.updateWebhookEndpointUrl );
$el.copyButton.on( 'click', function( e ) {
wpf.copyValueToClipboard( e, $( this ), $el.webhookEndpointUrl );
} );
},
/**
* Update the endpoint URL.
*
* @since 1.9.5
*/
updateWebhookEndpointUrl() {
const checked = $el.webhookMethod.filter( ':checked' ).val(),
newUrl = wpforms_admin.square.webhook_urls[ checked ];
$el.webhookEndpointUrl.val( newUrl );
$el.webhookCommunicationStatusNotice.removeClass( 'wpforms-hide' );
},
/**
* Enable webhooks.
*
* @since 1.9.5
*/
webhooksEnableCallback() {
$el.webhookConnectRow.toggleClass( 'wpforms-hide', ! $( this ).is( ':checked' ) );
$el.webhookConnectStatusRow.toggleClass( 'wpforms-hide', ! $( this ).is( ':checked' ) );
},
/**
* Create a webhook.
*
* @since 1.9.5
*
* @param {string} token Personal access token.
*
* @return {Promise} Promise an object.
*/
createWebhook( token ) {
return new Promise( ( resolve, reject ) => {
$.ajax( {
url: wpforms_admin.ajax_url,
type: 'post',
dataType: 'json',
data: {
action: 'wpforms_square_create_webhook',
nonce: wpforms_admin.nonce,
token,
},
success( response ) {
if ( response.success ) {
resolve( response );
return;
}
reject( response );
},
error() {
reject( { success: false, message: 'An error occurred.' } );
},
} );
} );
},
/**
* Refresh tokens.
*
* @since 1.9.5
*/
refreshTokensCallback() {
const $btn = $( this );
const buttonWidth = $btn.outerWidth();
const buttonLabel = $btn.text();
const settings = {
url: wpforms_admin.ajax_url,
type: 'post',
dataType: 'json',
data: {
action: 'wpforms_square_refresh_connection',
nonce: wpforms_admin.nonce,
mode: $btn.data( 'mode' ),
},
beforeSend() {
$btn.css( 'width', buttonWidth ).html( WPFormsAdmin.settings.iconSpinner ).prop( 'disabled', true );
},
};
let errorMessage = wpforms_admin.square.refresh_error;
// Perform an Ajax request.
$.ajax( settings )
.done( function( response ) {
if ( response.success ) {
$btn
.css( 'pointerEvents', 'none' )
.removeClass( 'wpforms-btn-light-grey' )
.addClass( 'wpforms-btn-grey' )
.html( 'Refreshed!' );
$btn.closest( 'form' ).css( 'cursor', 'wait' );
window.location = $btn.data( 'url' );
return;
}
if (
Object.prototype.hasOwnProperty.call( response, 'data' ) &&
response.data !== ''
) {
errorMessage = response.data;
}
$btn
.css( 'width', 'auto' )
.html( buttonLabel )
.prop( 'disabled', false );
app.modals.refreshTokensError( errorMessage );
} )
.fail( function() {
$btn
.css( 'width', 'auto' )
.html( buttonLabel )
.prop( 'disabled', false );
app.modals.refreshTokensError( errorMessage );
} );
},
/**
* Conditionally show Square mode switch warning.
*
* @since 1.9.5
*/
credentialsFieldsDisplay() {
const sandboxModeEnabled = $el.sandboxModeCheckbox.is( ':checked' );
if ( sandboxModeEnabled ) {
$el.sandboxConnectionStatusBlock.show();
$el.sandboxLocationBlock.show();
$el.sandboxLocationStatusBlock.show();
$el.productionConnectionStatusBlock.hide();
$el.productionLocationBlock.hide();
$el.productionLocationStatusBlock.hide();
} else {
$el.sandboxConnectionStatusBlock.hide();
$el.sandboxLocationBlock.hide();
$el.sandboxLocationStatusBlock.hide();
$el.productionConnectionStatusBlock.show();
$el.productionLocationBlock.show();
$el.productionLocationStatusBlock.show();
}
if ( sandboxModeEnabled && $el.sandboxConnectionStatusBlock.find( '.wpforms-square-connected' ).length ) {
return;
}
if ( ! sandboxModeEnabled && $el.productionConnectionStatusBlock.find( '.wpforms-square-connected' ).length ) {
return;
}
app.modals.modeChangedWarning();
},
/**
* Modals.
*
* @since 1.9.5
*/
modals: {
/**
* Show the warning modal when Square mode is changed.
*
* @since 1.9.5
*/
modeChangedWarning() {
$.alert( {
title: wpforms_admin.heads_up,
content: wpforms_admin.square.mode_update,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_admin.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Refresh tokens error handling.
*
* @since 1.9.5
*
* @param {string} error Error message.
*/
refreshTokensError( error ) {
$.alert( {
title: false,
content: error,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_admin.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Show popup with the ability to register a new webhook route or retrieve existing one.
*
* @since 1.9.5
*/
// eslint-disable-next-line max-lines-per-function
displayWebhookConfigPopup() {
$.confirm( {
title: wpforms_admin.square.webhook_create_title,
content: wpforms_admin.square.webhook_create_description +
'<input type="text" id="wpforms-square-personal-access-token" placeholder="' + wpforms_admin.square.webhook_token_placeholder + '" value="">' +
'<p class="wpforms-square-webhooks-connect-error error" style="display:none;">' + wpforms_admin.square.token_is_required + '</p>',
icon: 'fa fa-info-circle',
type: 'blue',
buttons: {
confirm: {
text: wpforms_admin.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action() {
const modal = this;
const tokenField = modal.$content.find( '#wpforms-square-personal-access-token' );
const errorMsg = modal.$content.find( '.error' );
const token = tokenField.val().trim();
const title = modal.$title;
// Disable the button to prevent multiple clicks.
$el.webhookConnectBtn.addClass( 'inactive' );
// Reset error message before validation
errorMsg.hide().text( '' );
if ( token === '' ) {
errorMsg.text( wpforms_admin.square.token_is_required ).show();
return false; // Prevent modal from closing.
}
// Show loading indicator.
modal.buttons.confirm.setText( wpforms_admin.loading );
modal.buttons.confirm.disable();
// Call API.
app.createWebhook( token )
.then( ( response ) => {
modal.setContent( '<p>' + response.data.message + '</p>' );
// Hide OK button and rename Cancel to Close.
modal.buttons.confirm.hide();
title.text( '' ).hide();
modal.buttons.cancel.setText( wpforms_admin.close );
// Ensure user can manually close the modal.
modal.buttons.cancel.action = function() {
window.location.reload();
};
} )
.catch( ( responseError ) => {
errorMsg.text( responseError.data.message ).show();
// Re-enable confirm button for retrying.
modal.buttons.confirm.setText( wpforms_admin.ok );
modal.buttons.confirm.enable();
} );
return false; // Prevent modal from closing immediately.
},
},
cancel: {
text: wpforms_admin.cancel,
action() {
// Re-enable the button.
$el.webhookConnectBtn.removeClass( 'inactive' );
this.close();
},
},
},
} );
},
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsSettingsSquare.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,807 @@
/* global Square, wpforms, wpforms_settings, wpforms_square, WPForms, WPFormsUtils */
/**
* WPForms Square function.
*
* @since 1.9.5
*/
const WPFormsSquare = window.WPFormsSquare || ( function( document, window, $ ) {
/**
* Holder for original form submit handler.
*
* @since 1.9.5
*
* @type {Function}
*/
let originalSubmitHandler;
/**
* Credit card data.
*
* @since 1.9.5
*
* @type {Object}
*/
const cardData = {
cardNumber: {
empty: true,
valid: false,
},
expirationDate: {
empty: true,
valid: false,
},
cvv: {
empty: true,
valid: false,
},
postalCode: {
empty: true,
valid: false,
},
};
/**
* Public functions and properties.
*
* @since 1.9.5
*
* @type {Object}
*/
const app = {
/**
* Square payments object.
*
* @since 1.9.5
*
* @type {Object}
*/
payments: null,
/**
* Number of page locked to switch.
*
* @since 1.9.5
*
* @type {number}
*/
lockedPageToSwitch: 0,
/**
* Start the engine.
*
* @since 1.9.5
*/
init() {
app.payments = app.getPaymentsInstance();
// Bail if a Square payments object isn't initialized.
if ( app.payments === null ) {
return;
}
$( document )
.on( 'wpformsReady', app.setupForms )
.on( 'wpformsBeforePageChange', app.pageChange )
.on( 'wpformsPageChange', app.afterPageChange )
.on( 'wpformsProcessConditionalsField', app.conditionalLogicHandler );
},
/**
* Setup and configure Square forms.
*
* @since 1.9.5
*/
setupForms() {
if ( typeof $.fn.validate === 'undefined' ) {
return;
}
$( '.wpforms-square form' )
.filter( ( _, form ) => typeof $( form ).data( 'formid' ) === 'number' ) // filter out forms which are locked (formid changed to 'locked-...').
.each( app.updateSubmitHandler );
},
/**
* Update submitHandler for the forms containing Square.
*
* @since 1.9.5
*/
async updateSubmitHandler() {
const $form = $( this );
const validator = $form.data( 'validator' );
if ( ! validator || $form.hasClass( 'wpforms-square-initialization' ) || $form.hasClass( 'wpforms-square-initialized' ) ) {
return;
}
// if the form is inside the "raw" elementor popup, we should not initialize the Square and wait for the popup to be opened.
if ( $form.closest( '.elementor-location-popup' ).length && ! $form.closest( '.elementor-popup-modal' ).length ) {
return;
}
$form.addClass( 'wpforms-square-initialization' );
// Store the original submitHandler.
originalSubmitHandler = validator.settings.submitHandler;
// Replace the default submit handler.
validator.settings.submitHandler = app.submitHandler;
// Get a new Card object.
await app.getCardInstance( $form );
},
/**
* Trigger resize event if Square field has been conditionally unhidden.
*
* Allows Square Card field to resize itself (fixes the issue with doubled field height on some screen resolutions).
*
* @since 1.9.5
*
* @param {Event} e Event.
* @param {number} formID Form ID.
* @param {number} fieldID Field ID.
* @param {boolean} pass Pass condition logic.
* @param {string} action Action name.
*/
conditionalLogicHandler( e, formID, fieldID, pass, action ) {
if ( ! app.isVisibleField( pass, action ) ) {
return;
}
const el = document.getElementById( 'wpforms-' + formID + '-field_' + fieldID );
if ( ! el || ! el.classList.contains( 'wpforms-field-square-cardnumber' ) ) {
return;
}
window.dispatchEvent( new Event( 'resize' ) );
},
/**
* Determine if the field is visible after being triggered by Conditional Logic.
*
* @since 1.9.5
*
* @param {boolean} pass Pass condition logic.
* @param {string} action Action name.
*
* @return {boolean} The field is visible.
*/
isVisibleField( pass, action ) {
return ( action === 'show' && pass ) || ( action === 'hide' && ! pass );
},
/**
* Update submitHandler for forms containing Square.
*
* @since 1.9.5
*
* @param {Object} form JS form element.
*/
submitHandler( form ) {
const $form = $( form );
const validator = $form.data( 'validator' );
const validForm = validator.form();
const card = $form.find( '.wpforms-square-credit-card-hidden-input' ).data( 'square-card' );
if ( ! validForm || typeof card === 'undefined' || ! app.isProcessedCard( $form ) ) {
originalSubmitHandler( $form );
return;
}
app.tokenize( $form, card );
},
/**
* Tokenize a card payment.
*
* @param {jQuery} $form Form element.
* @param {Object} card Square Card object.
*/
async tokenize( $form, card ) {
app.disableSubmitBtn( $form );
const sourceId = await app.getSourceId( $form, card );
if ( sourceId === null ) {
app.enableSubmitBtn( $form );
return;
}
app.submitForm( $form );
},
/**
* Initialize a Square payments object and retrieve it.
*
* @since 1.9.5
*
* @return {Object|null} Square payments object or null.
*/
getPaymentsInstance() {
if ( ! window.Square ) {
app.displaySdkError( $( '.wpforms-square form' ), wpforms_square.i18n.missing_sdk_script );
return null;
}
try {
return Square.payments( wpforms_square.client_id, wpforms_square.location_id );
} catch ( e ) {
const message = ( typeof e === 'object' && Object.prototype.hasOwnProperty.call( e, 'message' ) ) ? e.message : wpforms_square.i18n.missing_creds;
app.displaySdkError( $( '.wpforms-square form' ), message );
return null;
}
},
/**
* Try to retrieve a Square Card object.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {Object|null} Square Card object or null.
*/
async getCardInstance( $form ) {
// Applying the modern styles to the card config if needed.
// eslint-disable-next-line prefer-const
let cardConfig = {};
cardConfig.style = wpforms_square.card_config.style ? wpforms_square.card_config.style : app.getModernMarkupCardStyles( $form );
try {
const card = await app.payments.card( cardConfig );
// Attach the Card form to the page.
await card.attach( $form.find( '.wpforms-field-square-cardnumber' ).get( 0 ) );
const eventsList = [ 'focusClassAdded', 'focusClassRemoved' ];
const eventsLength = eventsList.length;
let counter = 0;
// Bind the Card events.
for ( ; counter < eventsLength; counter++ ) {
card.addEventListener( eventsList[ counter ], function( e ) {
// Card field is filled.
cardData[ e.detail.field ].empty = e.detail.currentState.isEmpty;
cardData[ e.detail.field ].valid = e.detail.currentState.isCompletelyValid;
if ( cardData[ e.detail.field ].valid ) {
app.removeFieldError( $form );
}
} );
}
$form.find( '.wpforms-square-credit-card-hidden-input' ).data( 'square-card', card );
$form.removeClass( 'wpforms-square-initialization' );
$form.addClass( 'wpforms-square-initialized' );
return card;
} catch ( e ) {
app.displaySdkError( $form, wpforms_square.i18n.card_init_error );
$form.removeClass( 'wpforms-square-initialization' );
console.log( 'Error:', e ); // eslint-disable-line no-console
console.log( 'Config', cardConfig ); // eslint-disable-line no-console
return null;
}
},
/**
* Retrieve a source ID (card nonce).
*
* @param {jQuery} $form Form element.
* @param {Object} card Square Card object.
*
* @return {string|null} The source ID or null.
*/
async getSourceId( $form, card ) {
try {
const response = await card.tokenize( app.getChargeVerifyBuyerDetails( $form ) );
$form.find( '.wpforms-square-payment-source-id' ).remove();
if ( response.status !== 'OK' || ! response.token ) {
app.displayFormError( app.getCreditCardInput( $form ), app.getResponseError( response ) );
return null;
}
$form.append( '<input type="hidden" name="wpforms[square][source_id]" class="wpforms-square-payment-source-id" value="' + app.escapeTextString( response.token ) + '">' );
return response.token;
} catch ( e ) {
app.displayFormError( app.getCreditCardInput( $form ), wpforms_square.i18n.token_process_fail );
}
return null;
},
/**
* Retrieve a response error message.
*
* @param {Object} response The response received from a tokenization call.
*
* @return {string} The response error message.
*/
getResponseError( response ) {
const hasErrors = response.errors && Array.isArray( response.errors ) && response.errors.length;
return hasErrors ? response.errors[ 0 ].message : wpforms_square.i18n.token_status_error + ' ' + response.status;
},
/**
* Retrieve details about the buyer for a charge.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {Object} Buyer details.
*/
getChargeVerifyBuyerDetails( $form ) {
return {
amount: app.getTotalInMinorUnits( wpforms.amountTotalCalc( $form ) ),
billingContact: app.getBillingContactDetails( $form ),
currencyCode: wpforms_settings.currency_code,
intent: 'CHARGE',
customerInitiated: true,
sellerKeyedIn: false,
};
},
/**
* Retrieve the total amount in minor units.
*
* @since 1.9.5
*
* @param {number} total Total amount.
*
* @return {string} Total amount in minor units.
*/
getTotalInMinorUnits( total ) {
return parseInt( wpforms.numberFormat( total, wpforms_settings.currency_decimal, '', '' ), 10 ).toString();
},
/**
* Retrieve billing contact details.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {Object} Billing contact details.
*/
getBillingContactDetails( $form ) { // eslint-disable-line complexity
// Get the form ID and billing mapping for this form, if available.
const formId = $form.data( 'formid' );
const mapping = ( wpforms_square.billing_details && wpforms_square.billing_details[ formId ] ) || {};
const result = {};
// Use mapped selectors if provided.
const $emailField = mapping.buyer_email ? $( `.wpforms-field-email[data-field-id="${ mapping.buyer_email }"]` ) : '';
const $nameField = mapping.billing_name ? $( `.wpforms-field-name[data-field-id="${ mapping.billing_name }"]` ) : '';
const $addressField = mapping.billing_address ? $( `.wpforms-field-address[data-field-id="${ mapping.billing_address }"]` ) : '';
if ( $emailField.length ) {
const emailValue = $emailField.find( 'input' ).first().val(); // Use the first input field knowing there could be confirmation email input as well.
if ( emailValue && emailValue.trim() !== '' ) {
result.email = emailValue;
}
}
if ( $nameField.length ) {
jQuery.extend( result, app.getBillingNameDetails( $nameField ) );
}
if ( $addressField.length ) {
jQuery.extend( result, app.getBillingAddressDetails( $addressField ) );
}
return result;
},
/**
* Retrieve billing name details.
*
* @since 1.9.5
*
* @param {jQuery} $field Field element.
*
* @return {Object} Billing name details.
*/
getBillingNameDetails( $field ) { // eslint-disable-line complexity
const result = {};
let givenName = '';
let familyName = '';
// Try to find separate first and last name fields.
const $firstNameField = $field.find( '.wpforms-field-name-first' );
const $lastNameField = $field.find( '.wpforms-field-name-last' );
if ( $firstNameField.length && $lastNameField.length ) {
// Use separate fields if both are present.
givenName = $firstNameField.val() || '';
familyName = $lastNameField.val() || '';
if ( givenName && givenName.trim() !== '' ) {
result.givenName = givenName;
}
if ( familyName && familyName.trim() !== '' ) {
result.familyName = familyName;
}
return result;
}
// Otherwise, fall back to a single name input field.
const $nameField = $field.find( 'input' );
if ( ! $nameField.length ) {
return result;
}
const fullName = $nameField.val().trim();
if ( ! fullName.length ) {
return result;
}
// Split full name by space; the first part is givenName,
// the rest (if any) are combined as familyName.
const nameParts = fullName.split( ' ' );
givenName = nameParts.shift() || '';
familyName = nameParts.join( ' ' ) || '';
if ( givenName && givenName.trim() !== '' ) {
result.givenName = givenName;
}
if ( familyName && familyName.trim() !== '' ) {
result.familyName = familyName;
}
return result;
},
/**
* Retrieve billing address details.
*
* @since 1.9.5
*
* @param {jQuery} $addressField Field element.
*
* @return {Object} Billing address details.
*/
getBillingAddressDetails( $addressField ) { // eslint-disable-line complexity
const result = {};
// For address fields, use the closest wrapper.
const $addressWrapper = $addressField.closest( '.wpforms-field' );
// Retrieve address components, defaulting to empty strings if not found.
const addressLine1 = $addressWrapper.find( '.wpforms-field-address-address1' ).val() || '';
const addressLine2 = $addressWrapper.find( '.wpforms-field-address-address2' ).val() || '';
const city = $addressWrapper.find( '.wpforms-field-address-city' ).val() || '';
const state = $addressWrapper.find( '.wpforms-field-address-state' ).val() || '';
const country = $addressWrapper.find( '.wpforms-field-address-country' ).val() || 'US';
const addressLines = [ addressLine1, addressLine2 ].filter( ( line ) => line && line.trim() !== '' );
if ( addressLines.length ) {
result.addressLines = addressLines;
}
if ( city && city.trim() !== '' ) {
result.city = city;
}
if ( state && state.trim() !== '' ) {
result.state = state;
}
if ( country && country.trim() !== '' ) {
result.countryCode = country;
}
return result;
},
/**
* Retrieve a jQuery selector for Credit Card hidden input.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {jQuery} Credit Card hidden input.
*/
getCreditCardInput( $form ) {
return $form.find( '.wpforms-square-credit-card-hidden-input' );
},
/**
* Submit the form using the original submitHandler.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*/
submitForm( $form ) {
const validator = $form.data( 'validator' );
if ( validator ) {
originalSubmitHandler( $form );
}
},
/**
* Determine if a credit card should be processed.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*
* @return {boolean} True if a credit card should be processed.
*/
isProcessedCard( $form ) {
const $squareDiv = $form.find( '.wpforms-field-square-cardnumber' );
const condHidden = $squareDiv.closest( '.wpforms-field-square' ).hasClass( 'wpforms-conditional-hide' );
const ccRequired = !! $squareDiv.data( 'required' );
if ( condHidden ) {
return false;
}
return ccRequired || app.isCardDataNotEmpty();
},
/**
* Determine if card data not empty.
*
* @since 1.9.5
*
* @return {boolean} True if at least one credit card sub-field is filled.
*/
isCardDataNotEmpty() {
return ! cardData.cardNumber.empty || ! cardData.expirationDate.empty || ! cardData.cvv.empty || ! cardData.postalCode.empty;
},
/**
* Determine if card data is completely valid.
*
* @since 1.9.5
*
* @return {boolean} True if at least one credit card sub-field is filled.
*/
isCardDataValid() {
return cardData.cardNumber.valid && cardData.expirationDate.valid && cardData.cvv.valid && cardData.postalCode.valid;
},
/**
* Display a SDK error.
*
* @param {jQuery} $form Form element.
* @param {string} message Error messages.
*
* @since 1.9.5
*/
displaySdkError( $form, message ) {
$form
.find( '.wpforms-square-credit-card-hidden-input' )
.closest( '.wpforms-field-square-number' )
.append( $( '<label></label>', {
text: message,
class: 'wpforms-error',
} ) );
},
/**
* Remove field error.
*
* @param {jQuery} $form Form element.
*
* @since 1.9.7
*/
removeFieldError( $form ) {
$form.find( '.wpforms-field-square-number .wpforms-error' ).remove();
},
/**
* Display a field error using jQuery Validate library.
*
* @param {jQuery} $field Form element.
* @param {string} message Error messages.
*
* @since 1.9.5
*/
displayFormError( $field, message ) {
const fieldName = $field.attr( 'name' );
const $form = $field.closest( 'form' );
const error = {};
error[ fieldName ] = message;
wpforms.displayFormAjaxFieldErrors( $form, error );
wpforms.scrollToError( $field );
},
/**
* Disable submit button for the form.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*/
disableSubmitBtn( $form ) {
$form.find( '.wpforms-submit' ).prop( 'disabled', true );
},
/**
* Enable submit button for the form.
*
* @since 1.9.5
*
* @param {jQuery} $form Form element.
*/
enableSubmitBtn( $form ) {
$form.find( '.wpforms-submit' ).prop( 'disabled', false );
},
/**
* Replaces &, <, >, ", `, and ' with their escaped counterparts.
*
* @since 1.9.5
*
* @param {string} string String to escape.
*
* @return {string} Escaped string.
*/
escapeTextString( string ) {
return $( '<span></span>' ).text( string ).html();
},
/**
* Callback for a page changing.
*
* @since 1.9.5
*
* @param {Event} event Event.
* @param {number} currentPage Current page.
* @param {jQuery} $form Current form.
* @param {string} action The navigation action.
*/
pageChange( event, currentPage, $form, action ) { // eslint-disable-line complexity
const $squareDiv = $form.find( '.wpforms-field-square-cardnumber' );
// Stop navigation through page break pages.
if (
! $squareDiv.is( ':visible' ) ||
( ! $squareDiv.data( 'required' ) && ! app.isCardDataNotEmpty() ) ||
( app.lockedPageToSwitch && app.lockedPageToSwitch !== currentPage ) ||
action === 'prev'
) {
return;
}
if ( app.isCardDataValid() ) {
app.removeFieldError( $form );
return;
}
app.lockedPageToSwitch = currentPage;
app.displayFormError( app.getCreditCardInput( $form ), wpforms_square.i18n.empty_details );
event.preventDefault();
},
/**
* Callback for a after page changing.
*
* @since 1.9.5
*/
afterPageChange() {
window.dispatchEvent( new Event( 'resize' ) );
},
/**
* Get CSS property value.
* In case of exception return empty string.
*
* @since 1.9.5
*
* @param {jQuery} $element Element.
* @param {string} property Property.
*
* @return {string} Property value.
*/
getCssPropertyValue( $element, property ) {
try {
return $element.css( property );
} catch ( e ) {
return '';
}
},
/**
* Determine whether modern style are needed.
*
* Force run on the classic markup if it is conversational or lead form.
*
* @since 1.9.5
*
* @return {boolean} True if the form needs styles.
*/
needsStyles() {
// Styles are not needed if the classic markup is used
// and it's neither conversational nor lead form.
if (
( ! window.WPForms || ! WPForms.FrontendModern ) &&
! $( '#wpforms-conversational-form-page' ).length &&
! $( '.wpforms-lead-forms-container' ).length
) {
return false;
}
return true;
},
/**
* Get modern card styles.
*
* @since 1.9.5
*
* @param {jQuery} $form Current form.
*
* @return {Object} Card styles object.
*/
getModernMarkupCardStyles( $form ) {
if ( ! app.needsStyles() ) {
return {};
}
const $hiddenInput = app.getCreditCardInput( $form ),
hiddenInputColor = app.getCssPropertyValue( $hiddenInput, 'color' ),
inputStyle = {
fontSize: app.getCssPropertyValue( $hiddenInput, 'font-size' ),
colorText: hiddenInputColor,
colorTextPlaceholder: hiddenInputColor,
};
// Check if WPFormsUtils.cssColorsUtils object is available.
if ( WPFormsUtils.hasOwnProperty( 'cssColorsUtils' ) &&
typeof WPFormsUtils.cssColorsUtils.getColorWithOpacity === 'function' ) {
inputStyle.colorText = WPFormsUtils.cssColorsUtils.getColorWithOpacity( hiddenInputColor );
inputStyle.colorTextPlaceholder = WPFormsUtils.cssColorsUtils.getColorWithOpacity( hiddenInputColor, '0.5' );
}
return {
input: {
color: inputStyle.colorText,
fontSize: inputStyle.fontSize,
},
'input::placeholder': {
color: inputStyle.colorTextPlaceholder,
},
'input.is-error': {
color: inputStyle.colorText,
},
};
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsSquare.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,669 @@
/* global wpforms_builder, wpforms_builder_stripe, WPFormsBuilderPaymentsUtils */
// noinspection ES6ConvertVarToLetConst
/**
* Stripe builder function.
*
* @since 1.8.4
*/
// eslint-disable-next-line no-var
var WPFormsStripeModernBuilder = window.WPFormsStripeModernBuilder || ( function( document, window, $ ) {
/**
* Elements holder.
*
* @since 1.8.4
*
* @type {Object}
*/
let el = {};
/**
* Public functions and properties.
*
* @since 1.8.4
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.8.4
*/
init() {
$( app.ready );
},
/**
* Initialized once the DOM is fully loaded.
*
* @since 1.8.4
*/
ready() {
app.customMetadataActions();
if ( app.isLegacySettings() ) {
return;
}
// Cache DOM elements.
el = {
$alert: $( '#wpforms-stripe-credit-card-alert' ),
$panelContent: $( '#wpforms-panel-content-section-payment-stripe' ),
$feeNotice: $( '.wpforms-stripe-notice-info' ),
};
app.bindUIActions();
app.bindPlanUIActions();
if ( ! wpforms_builder_stripe.is_pro ) {
const baseSelector = '.wpforms-panel-content-section-stripe',
toggleInput = `${ baseSelector } .wpforms-panel-content-section-payment-toggle input`,
planNameInput = `${ baseSelector } .wpforms-panel-content-section-payment-plan-name input`;
$( toggleInput ).each( WPFormsBuilderPaymentsUtils.toggleContent );
$( planNameInput ).each( WPFormsBuilderPaymentsUtils.checkPlanName );
$( '#wpforms-panel-payments' )
.on( 'click', toggleInput, WPFormsBuilderPaymentsUtils.toggleContent )
.on( 'click', `${ baseSelector } .wpforms-panel-content-section-payment-plan-head-buttons-toggle`, WPFormsBuilderPaymentsUtils.togglePlan )
.on( 'click', `${ baseSelector } .wpforms-panel-content-section-payment-plan-head-buttons-delete`, WPFormsBuilderPaymentsUtils.deletePlan )
.on( 'input', planNameInput, WPFormsBuilderPaymentsUtils.renamePlan )
.on( 'focusout', planNameInput, WPFormsBuilderPaymentsUtils.checkPlanName );
}
},
/**
* Process custom metadata actions.
*
* @since 1.9.6
*/
customMetadataActions() {
$( '#wpforms-panel-payments' )
.on( 'focusout', '.wpforms-panel-field-stripe-custom-metadata-meta-key', function() {
// Remove invalid characters for meta key.
$( this ).val( $( this ).val().replace( /[^\p{L}\p{N}_-]/gu, '' ) );
} )
// Add metadata row.
.on( 'click', '.wpforms-panel-content-section-stripe-custom-metadata-add', function( e ) {
e.preventDefault();
const $table = $( this ).parents( '.wpforms-panel-content-section-stripe-custom-metadata-table' ),
$lastRow = $table.find( 'tr' ).last(),
$clone = $lastRow.clone( true ),
lastID = $lastRow.data( 'key' ),
nextID = lastID + 1;
// Update row key ID.
$clone.attr( 'data-key', nextID );
// Increment the counter.
$clone.html( $clone.html().replaceAll( '[' + lastID + ']', '[' + nextID + ']' ).replaceAll( '-' + lastID + '-', '-' + nextID + '-' ) );
// Clear values.
$clone.find( 'select, input' ).val( '' );
// Re-enable "delete" button.
$clone.find( '.wpforms-panel-content-section-stripe-custom-metadata-delete' ).removeClass( 'hidden' );
// Remove error classes.
$clone.find( '.wpforms-required-field-error' ).removeClass( 'wpforms-required-field-error' );
// Put it back to the table.
$table.find( 'tbody' ).append( $clone.get( 0 ) );
} )
// Delete metadata row.
.on( 'click', '.wpforms-panel-content-section-stripe-custom-metadata-delete', function( e ) {
e.preventDefault();
$( this ).parents( '.wpforms-panel-content-section-stripe-custom-metadata-table tr' ).remove();
} );
},
/**
* Process various events as a response to UI interactions.
*
* @since 1.8.4
*/
bindUIActions() {
const $builder = $( '#wpforms-builder' );
$builder.on( 'wpformsFieldDelete', app.disableNotifications )
.on( 'wpformsSaved', app.requiredFieldsCheck )
.on( 'wpformsFieldAdd', app.fieldAdded )
.on( 'wpformsFieldDelete', app.fieldDeleted )
.on( 'wpformsPaymentsPlanCreated', app.toggleMultiplePlansWarning )
.on( 'wpformsPaymentsPlanCreated', app.bindPlanUIActions )
.on( 'wpformsPaymentsPlanDeleted', app.toggleMultiplePlansWarning );
el.$panelContent
.find( '.wpforms-panel-content-section-payment-one-time' )
.on( 'change', '.wpforms-panel-field-stripe-custom-metadata-meta-key', app.resetCustomMetaKeyErrorClass );
},
/**
* Bind plan UI actions.
*
* @since 1.9.5
*/
bindPlanUIActions() {
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan-body .wpforms-panel-field-select select[name*="email"]' ).on( 'change', app.resetEmailAlertErrorClass );
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan-body .wpforms-panel-field-stripe-custom-metadata-meta-key' ).on( 'change', app.resetCustomMetaKeyErrorClass );
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan-period select' ).on( 'change', app.resetCyclesValues );
},
/**
* On form save notify users about required fields.
*
* @since 1.8.4
*/
requiredFieldsCheck() {
const validationState = app.validateFields();
if ( ! validationState?.invalid ) {
return;
}
app.openAlert(
app.getAlertMessage( validationState.type ),
() => validationState.invalid.$element.focus()
);
},
/**
* Validates form fields based on their visibility and specific conditions.
*
* @since 1.9.8.3
*
* @return {Object|boolean} Returns an object representing the validation state or `true` if the panel content is hidden.
*/
validateFields() {
if ( el.$panelContent.hasClass( 'wpforms-hidden' ) ) {
return true;
}
// We want to determine which fields are not filled to display an appropriate error message.
// Also, it contains the type of the payment: one-time or recurring.
const validationState = {};
if ( $( '#wpforms-panel-field-stripe-enable_one_time' ).is( ':checked' ) ) {
app.validateOneTimeFields( validationState );
}
if ( $( '#wpforms-panel-field-stripe-enable_recurring' ).is( ':checked' ) ) {
app.validateRecurringFields( validationState );
}
return validationState;
},
/**
* Validates the one-time payment fields within the form's panel content.
*
* @since 1.9.8.3
*
* @param {Object} validationState The current validation state object.
*/
validateOneTimeFields( validationState ) {
const $oneTimeScope = el.$panelContent.find( '.wpforms-panel-content-section-payment-one-time' );
validationState.type = 'one-time';
app.validateCustomMetaTable( $oneTimeScope, validationState );
},
/**
* Validates recurring fields within the payment plan section of the panel content.
*
* @since 1.9.8.3
*
* @param {Object} validationState An object representing the current state of validation.
*/
validateRecurringFields( validationState ) {
validationState.type = validationState.type ? 'both' : 'recurring';
el.$panelContent.find( '.wpforms-panel-content-section-payment-plan' ).each( function() {
const $plan = $( this );
app.validateEmailField( $plan, validationState );
app.validateCustomMetaTable( $plan, validationState );
} );
},
/**
* Validates the email field for a given plan and updates the validation state if the field is empty.
*
* @since 1.9.8.3
*
* @param {Object} $plan jQuery object representing the plan element.
* @param {Object} validationState Object representing the validation state that will be updated.
*/
validateEmailField( $plan, validationState ) {
const planId = $plan.data( 'plan-id' );
const $emailField = $( `#wpforms-panel-field-stripe-recurring-${ planId }-email` );
if ( $emailField.val() ) {
return;
}
$emailField.addClass( 'wpforms-required-field-error' );
validationState.invalid = validationState.invalid ?? {};
validationState.invalid.email = true;
if ( ! validationState.invalid.$element ) {
validationState.invalid.$element = $emailField;
}
},
/**
* Validates the custom metadata table rows within the specified scope.
*
* @since 1.9.8.3
*
* @param {Object} $scope The jQuery object representing the current scope containing the custom metadata table.
* @param {Object} validationState The object used to store the validation state for the custom metadata rows.
*
* @return {boolean|void} Returns `true` if no rows are present for validation, otherwise no return value.
*/
validateCustomMetaTable( $scope, validationState ) {
const $customMetaRows = $scope.find( '.wpforms-panel-content-section-stripe-custom-metadata-table tr[data-key]' );
if ( ! $customMetaRows.length ) {
return true;
}
$customMetaRows.each( function() {
const $row = $( this );
if ( app.isValidCustomMetaRow( $row ) ) {
return;
}
validationState.invalid = validationState.invalid ?? {};
validationState.invalid.customMeta = true;
if ( ! validationState.invalid.$element ) {
validationState.invalid.$element = $row.find( '.wpforms-panel-field-stripe-custom-metadata-meta-key' );
}
} );
},
/**
* Validates if a custom metadata row in the form is properly filled.
*
* @since 1.9.8.3
*
* @param {Object} $row jQuery object representing the custom metadata row to validate.
*
* @return {boolean} True if the custom metadata row is valid, otherwise false.
*/
isValidCustomMetaRow( $row ) {
const $metaKey = $row.find( '.wpforms-panel-field-stripe-custom-metadata-meta-key' );
const isValid = (
! $row.find( '.wpforms-panel-field-stripe-custom-metadata-object-type' ).val() ||
! $row.find( '.wpforms-panel-field-stripe-custom-metadata-meta-value' ).val() ||
$metaKey.val()
);
$metaKey.toggleClass( 'wpforms-required-field-error', ! isValid );
return isValid;
},
/**
* Maybe reset required email field error class.
*
* @since 1.9.5
*/
resetEmailAlertErrorClass() {
$( this ).toggleClass( 'wpforms-required-field-error', ! $( this ).val() );
},
/**
* Resets the error class for a custom meta key row.
*
* @since 1.9.8.3
*/
resetCustomMetaKeyErrorClass() {
const $row = $( this ).closest( 'tr[data-key]' );
app.isValidCustomMetaRow( $row );
},
/**
* Show alert for required recurring email field.
*
* @since 1.8.4
* @deprecated 1.9.8.3
*/
recurringEmailAlert() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsStripeModernBuilder.recurringEmailAlert()" has been deprecated, please use the "WPFormsStripeModernBuilder.openAlert()" function instead!' );
app.openAlert( wpforms_builder.stripe_recurring_email );
},
/**
* Opens an alert dialog with a customized message and optional callback action.
*
* @since 1.9.8.3
*
* @param {string} alertMessage The message to be displayed in the alert dialog.
* @param {Function} onClose Optional callback function to execute when the alert is closed.
*/
openAlert( alertMessage, onClose = () => {} ) {
$.alert( {
title: wpforms_builder.stripe_recurring_heading,
content: alertMessage,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
action: onClose,
},
},
onOpen() {
$( '.wpforms-stripe-settings-redirect' ).on( 'click', app.settingsRedirect );
},
} );
},
/**
* Returns an alert message based on the specified payment type and Stripe visibility settings.
*
* @since 1.9.8.3
*
* @param {string} paymentType The payment type (e.g., 'one-time', 'recurring', or 'both') used to determine the alert message.
*
* @return {string} The appropriate alert message for the specified payment type.
*/
getAlertMessage( paymentType ) {
if ( ! $( '.wpforms-panel-content-section-stripe' ).is( ':visible' ) ) {
return wpforms_builder.stripe_recurring_settings;
}
const typesMap = {
'one-time': 'stripe_required_one_time_fields',
recurring: 'stripe_required_recurring_fields',
both: 'stripe_required_both_fields',
};
return wpforms_builder[ typesMap[ paymentType ] ?? typesMap.recurring ];
},
/**
* Redirect to the settings tab.
*
* @since 1.9.5
*/
settingsRedirect() {
// Open the Stripe settings tab.
$( '.wpforms-panel-payments-button' ).trigger( 'click' );
$( '.wpforms-panel-sidebar-section-stripe' ).trigger( 'click' );
// Scroll to the Stripe settings.
window.location.href = window.location.pathname + window.location.search + '#wpforms-panel-field-stripe-enable_recurring-wrap';
// Close the alert.
$( this ).closest( '.jconfirm-box' ).find( '.btn-confirm' ).trigger( 'click' );
},
/**
* Disable notifications.
*
* @since 1.8.4
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
disableNotifications( e, id, type ) {
if ( ! app.isStripeField( type ) ) {
return;
}
if ( app.hasStripeCreditCardFieldInBuilder() ) {
return;
}
const $notificationWrap = $( '.wpforms-panel-content-section-notifications [id*="-stripe-wrap"]' );
$notificationWrap.find( 'input[id*="-stripe"]' ).prop( 'checked', false );
$notificationWrap.addClass( 'wpforms-hidden' );
},
/**
* Determine is legacy settings is loaded.
*
* @since 1.8.4
*
* @return {boolean} True is legacy settings loaded.
*/
isLegacySettings() {
return $( '#wpforms-panel-field-stripe-enable' ).length;
},
/**
* We have to do several actions when the "Stripe" field is added.
*
* @since 1.8.4
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
fieldAdded( e, id, type ) {
if ( ! app.isStripeField( type ) ) {
return;
}
if ( ! app.hasStripeCreditCardFieldInBuilder() ) {
return;
}
app.settingsToggle( true );
el.$feeNotice.toggleClass( 'wpforms-hidden' );
},
/**
* We have to do several actions when the "Stripe" field is deleted.
*
* @since 1.8.4
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
fieldDeleted( e, id, type ) {
if ( ! app.isStripeField( type ) ) {
return;
}
if ( app.hasStripeCreditCardFieldInBuilder() ) {
return;
}
app.settingsToggle( false );
app.disablePayments();
el.$feeNotice.toggleClass( 'wpforms-hidden' );
},
/**
* Determine if a field type is Stripe credit card.
*
* @since 1.8.4
*
* @param {string} type Field type.
*
* @return {boolean} True if Stripe field.
*/
isStripeField( type ) {
return type === wpforms_builder_stripe.field_slug;
},
/**
* Checks if the Stripe Credit Card field is in the form builder.
*
* @since 1.9.5
*
* @return {boolean} True if the Stripe Credit Card field is in the builder.
*/
hasStripeCreditCardFieldInBuilder() {
return $( `.wpforms-field.wpforms-field-${ wpforms_builder_stripe.field_slug }` ).length > 0;
},
/**
* Toggles visibility of multiple plans warning.
*
* @since 1.8.4
*/
toggleMultiplePlansWarning() {
el.$panelContent.find( '.wpforms-stripe-multiple-plans-warning' ).toggleClass( 'wpforms-hidden', el.$panelContent.find( '.wpforms-panel-content-section-payment-plan' ).length === 1 );
},
/**
* Toggles visibility of the Stripe addon settings.
*
* @since 1.8.4
*
* @param {boolean} display Show or hide settings.
*/
settingsToggle( display ) {
if (
! el.$alert.length &&
! el.$panelContent.length
) {
return;
}
el.$alert.toggleClass( 'wpforms-hidden', display );
el.$panelContent.toggleClass( 'wpforms-hidden', ! display );
},
/**
* Toggle payments content.
*
* @since 1.8.4
*/
toggleContent() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsStripeModernBuilder.toggleContent()" has been deprecated, please use the new "WPFormsPaymentsUtils.toggleContent()" function instead!' );
WPFormsBuilderPaymentsUtils.toggleContent();
},
/**
* Toggle a plan content.
*
* @since 1.8.4
*/
togglePlan() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsStripeModernBuilder.togglePlan()" has been deprecated, please use the new "WPFormsPaymentsUtils.togglePlan()" function instead!' );
WPFormsBuilderPaymentsUtils.togglePlan();
},
/**
* Delete a plan.
*
* @since 1.8.4
*/
deletePlan() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsStripeModernBuilder.checkPlanName()" has been deprecated, please use the new "WPFormsPaymentsUtils.deletePlan()" function instead!' );
WPFormsBuilderPaymentsUtils.deletePlan();
},
/**
* Check a plan name on empty value.
*
* @since 1.8.4
*/
checkPlanName() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsStripeModernBuilder.checkPlanName()" has been deprecated, please use the new "WPFormsPaymentsUtils.checkPlanName()" function instead!' );
WPFormsBuilderPaymentsUtils.checkPlanName();
},
/**
* Rename a plan.
*
* @since 1.8.4
*/
renamePlan() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsStripeModernBuilder.renamePlan()" has been deprecated, please use the new "WPFormsPaymentsUtils.renamePlan()" function instead!' );
WPFormsBuilderPaymentsUtils.renamePlan();
},
/**
* Make sure that "One-Time Payments" and "Recurring Payments" toggles are turned off.
*
* @since 1.9.5
*/
disablePayments() {
const toggleInput = $( '#wpforms-panel-field-stripe-enable_one_time, #wpforms-panel-field-stripe-enable_recurring' );
toggleInput.prop( 'checked', false ).trigger( 'change' ).each( WPFormsBuilderPaymentsUtils.toggleContent );
},
/**
* Reset cycles dropdown values based on the period selection.
*
* @since 1.9.8
*/
resetCyclesValues() {
const $el = $( this ),
$select = $el.closest( '.wpforms-panel-content-section-payment-plan-body' ).find( '.wpforms-panel-content-section-payment-plan-cycles select' ),
selectedValue = $select.val(); // Save current selected value
let max;
// Determine the maximum number of cycles based on the selected period.
// It has intentionally not been passed from PHP to avoid unnecessary complexity or unexpected behavior.
switch ( $el.val() ) {
case 'yearly':
max = 20;
break;
case 'semiyearly':
max = 40;
break;
case 'quarterly':
max = 80;
break;
default:
max = wpforms_builder_stripe.cycles_max;
}
const options = [
$( '<option>', { value: 'unlimited', text: wpforms_builder_stripe.i18n.cycles_default } ),
];
for ( let i = 1; i <= max; i++ ) {
options.push( $( '<option>', { value: i, text: i } ) );
}
// Populate select and re-apply selected value if still valid
$select.empty().append( options ).val( selectedValue );
// If selected value is no longer valid, default to 'unlimited'
if ( $select.val() !== selectedValue ) {
$select.val( 'unlimited' );
}
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsStripeModernBuilder.init();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,222 @@
/* global wpforms_builder, wpforms_builder_stripe */
/**
* WPForms Stripe Card Field function.
*
* @since 1.8.2
*/
'use strict';
var WPFormsStripeCardField = window.WPFormsStripeCardField || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.8.2
*
* @type {object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.8.2
*/
init: function() {
app.bindUIActions();
},
/**
* Process various events as a response to UI interactions.
*
* @since 1.8.2
*/
bindUIActions: function() {
$( document ).on( 'wpformsSaved', app.ajaxRequiredCheck );
$( document ).on( 'wpformsSaved', app.paymentsEnabledCheck );
$( document ).on( 'click', '#wpforms-add-fields-' + wpforms_builder_stripe.field_slug, app.stripeKeysCheck );
$( document ).on( 'change', '.wpforms-field-option-stripe-credit-card .wpforms-field-option-row-sublabel_position select', app.sublabelPositionChange );
$( document ).on( 'change', '.wpforms-field-option-stripe-credit-card .wpforms-field-option-row-link_email select', app.linkEmailChange );
$( document ).on( 'wpformsFieldAdd', app.disableAddCardButton );
$( document ).on( 'wpformsFieldDelete', app.enableAddCardButton );
$( document ).on( 'wpformsFieldDelete', app.maybeResetLinkEmailField );
},
/**
* On form save notify users if AJAX submission is required.
*
* @since 1.8.2
*/
ajaxRequiredCheck: function() {
if ( ! $( '.wpforms-field.wpforms-field-' + wpforms_builder_stripe.field_slug ).length ||
$( '#wpforms-panel-field-settings-ajax_submit' ).is( ':checked' ) ) {
return;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.stripe_ajax_required,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* On form save notify users if Stripe payments are not enabled.
*
* @since 1.8.2
*/
paymentsEnabledCheck: function() {
if ( ! $( `.wpforms-field.wpforms-field-${ wpforms_builder_stripe.field_slug }:visible` ).length ||
$( '#wpforms-panel-field-stripe-enable' ).is( ':checked' ) ||
$( '#wpforms-panel-field-stripe-enable_one_time' ).is( ':checked' ) ||
$( '#wpforms-panel-field-stripe-enable_recurring' ).is( ':checked' )
) {
return;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.payments_enabled_required,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* On adding Stripe Credit Card field notify users if Stripe keys are missing.
*
* @since 1.8.2
*/
stripeKeysCheck: function() {
if ( ! $( this ).hasClass( 'stripe-keys-required' ) ) {
return;
}
$.alert( {
title: wpforms_builder.heads_up,
content: wpforms_builder.stripe_keys_required,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Disable "Add Card" button in the fields list.
*
* @since 1.8.2
*
* @param {object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
disableAddCardButton: function( e, id, type ) {
if ( wpforms_builder_stripe.field_slug === type ) {
$( '#wpforms-add-fields-' + wpforms_builder_stripe.field_slug )
.prop( 'disabled', true );
app.paymentsEnabledCheck();
}
},
/**
* Enable "Add Card" button in the fields list.
*
* @since 1.8.2
*
* @param {object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
enableAddCardButton: function( e, id, type ) {
if ( wpforms_builder_stripe.field_slug === type ) {
$( '#wpforms-add-fields-' + wpforms_builder_stripe.field_slug )
.prop( 'disabled', false );
}
},
/**
* Switch sublabels preview mode.
*
* @since 1.8.2
*/
sublabelPositionChange: function() {
const fieldId = $( this ).parent().data( 'field-id' ),
$fieldPreview = $( `#wpforms-field-${fieldId}` ).find( '.wpforms-stripe-payment-element' );
$fieldPreview.toggleClass( 'above' );
$fieldPreview.toggleClass( 'floating' );
$fieldPreview.find( 'select' ).val( $fieldPreview.hasClass( 'above' ) ? 'empty' : 'country' );
},
/**
* Switch Link Email Field mapping.
*
* @since 1.8.2
*/
linkEmailChange: function() {
const fieldId = $( this ).parent().data( 'field-id' );
$( `#wpforms-field-${fieldId}` ).find( '.wpforms-stripe-link-email' ).toggleClass( 'wpforms-hidden', $( this ).val() !== '' );
},
/**
* Maybe reset link email field if mapped email was removed.
*
* @since 1.8.2
*
* @param {object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
maybeResetLinkEmailField: function( e, id, type ) {
if ( type !== 'email' ) {
return;
}
$( '.wpforms-field-option-stripe-credit-card .wpforms-field-option-row-link_email select' ).trigger( 'change' );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsStripeCardField.init();
@@ -0,0 +1 @@
var WPFormsStripeCardField=window.WPFormsStripeCardField||((e,s)=>{let t={init:function(){t.bindUIActions()},bindUIActions:function(){s(e).on("wpformsSaved",t.ajaxRequiredCheck),s(e).on("wpformsSaved",t.paymentsEnabledCheck),s(e).on("click","#wpforms-add-fields-"+wpforms_builder_stripe.field_slug,t.stripeKeysCheck),s(e).on("change",".wpforms-field-option-stripe-credit-card .wpforms-field-option-row-sublabel_position select",t.sublabelPositionChange),s(e).on("change",".wpforms-field-option-stripe-credit-card .wpforms-field-option-row-link_email select",t.linkEmailChange),s(e).on("wpformsFieldAdd",t.disableAddCardButton),s(e).on("wpformsFieldDelete",t.enableAddCardButton),s(e).on("wpformsFieldDelete",t.maybeResetLinkEmailField)},ajaxRequiredCheck:function(){s(".wpforms-field.wpforms-field-"+wpforms_builder_stripe.field_slug).length&&!s("#wpforms-panel-field-settings-ajax_submit").is(":checked")&&s.alert({title:wpforms_builder.heads_up,content:wpforms_builder.stripe_ajax_required,icon:"fa fa-exclamation-circle",type:"orange",buttons:{confirm:{text:wpforms_builder.ok,btnClass:"btn-confirm",keys:["enter"]}}})},paymentsEnabledCheck:function(){!s(`.wpforms-field.wpforms-field-${wpforms_builder_stripe.field_slug}:visible`).length||s("#wpforms-panel-field-stripe-enable").is(":checked")||s("#wpforms-panel-field-stripe-enable_one_time").is(":checked")||s("#wpforms-panel-field-stripe-enable_recurring").is(":checked")||s.alert({title:wpforms_builder.heads_up,content:wpforms_builder.payments_enabled_required,icon:"fa fa-exclamation-circle",type:"red",buttons:{confirm:{text:wpforms_builder.ok,btnClass:"btn-confirm",keys:["enter"]}}})},stripeKeysCheck:function(){s(this).hasClass("stripe-keys-required")&&s.alert({title:wpforms_builder.heads_up,content:wpforms_builder.stripe_keys_required,icon:"fa fa-exclamation-circle",type:"orange",buttons:{confirm:{text:wpforms_builder.ok,btnClass:"btn-confirm",keys:["enter"]}}})},disableAddCardButton:function(e,i,r){wpforms_builder_stripe.field_slug===r&&(s("#wpforms-add-fields-"+wpforms_builder_stripe.field_slug).prop("disabled",!0),t.paymentsEnabledCheck())},enableAddCardButton:function(e,i,r){wpforms_builder_stripe.field_slug===r&&s("#wpforms-add-fields-"+wpforms_builder_stripe.field_slug).prop("disabled",!1)},sublabelPositionChange:function(){var e=s(this).parent().data("field-id"),e=s("#wpforms-field-"+e).find(".wpforms-stripe-payment-element");e.toggleClass("above"),e.toggleClass("floating"),e.find("select").val(e.hasClass("above")?"empty":"country")},linkEmailChange:function(){var e=s(this).parent().data("field-id");s("#wpforms-field-"+e).find(".wpforms-stripe-link-email").toggleClass("wpforms-hidden",""!==s(this).val())},maybeResetLinkEmailField:function(e,i,r){"email"===r&&s(".wpforms-field-option-stripe-credit-card .wpforms-field-option-row-link_email select").trigger("change")}};return t})(document,(window,jQuery));WPFormsStripeCardField.init();
@@ -0,0 +1,242 @@
/* global wpforms_builder, wpforms_builder_stripe */
// noinspection ES6ConvertVarToLetConst
/**
* Stripe builder function.
*
* @since 1.8.2
*/
// eslint-disable-next-line no-var
var WPFormsStripe = window.WPFormsStripe || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.8.2
*
* @type {Object}
*/
const app = {
/**
* Start the engine.
*
* @since 1.8.2
*/
init() {
$( app.ready );
},
/**
* Initialized once the DOM is fully loaded.
*
* @since 1.8.2
*/
ready() {
if ( ! app.isLegacySettings() ) {
return;
}
app.settingsDisplay();
app.settingsConditions();
app.bindUIActions();
},
/**
* Process various events as a response to UI interactions.
*
* @since 1.8.2
*/
bindUIActions() {
$( document )
.on( 'wpformsFieldDelete', app.disableNotifications )
.on( 'wpformsSaved', app.requiredFieldsCheck )
.on( 'wpformsFieldUpdate', app.settingsDisplay )
.on( 'wpformsFieldUpdate', app.settingsConditions );
$( '#wpforms-panel-field-stripe-recurring-email' ).on( 'change', app.resetEmailAlertErrorClass );
},
/**
* Toggles visibility of the Stripe settings.
*
* If a credit card field has been added, then reveal the settings.
* Otherwise, hide them.
*
* @since 1.8.2
*/
settingsDisplay() {
const $alert = $( '#wpforms-stripe-credit-card-alert' );
const $content = $( '#stripe-provider' );
// Check if any Credit Card fields were added to the form.
const ccFieldsAdded = wpforms_builder_stripe.field_slugs.filter( function( fieldSlug ) {
const $el = $( '.wpforms-field-option-' + fieldSlug );
return $el.length ? $el : null;
} );
if ( ccFieldsAdded.length ) {
$alert.hide();
$content.find( '#wpforms-stripe-new-interface-alert, .wpforms-stripe-notice-info, .wpforms-panel-field, .wpforms-conditional-block-panel, h2' ).show();
} else {
$alert.show();
$content.find( '#wpforms-stripe-new-interface-alert, .wpforms-stripe-notice-info, .wpforms-panel-field, .wpforms-conditional-block-panel, h2' ).hide();
$content.find( '#wpforms-panel-field-stripe-enable' ).prop( 'checked', false ).trigger( 'change' );
}
},
/**
* Toggles the visibility of the related settings.
*
* @since 1.8.2
*/
settingsConditions() {
$( '#wpforms-panel-field-stripe-enable' ).conditions( {
conditions: {
element: '#wpforms-panel-field-stripe-enable',
type: 'checked',
operator: 'is',
},
actions: {
if: {
element: '.wpforms-panel-content-section-stripe-body',
action: 'show',
},
else: {
element: '.wpforms-panel-content-section-stripe-body',
action: 'hide',
},
},
effect: 'appear',
} );
$( '#wpforms-panel-field-stripe-recurring-enable' ).conditions( {
conditions: {
element: '#wpforms-panel-field-stripe-recurring-enable',
type: 'checked',
operator: 'is',
},
actions: {
if: {
element: '#wpforms-panel-field-stripe-recurring-period-wrap,#wpforms-panel-field-stripe-recurring-conditional_logic-wrap,#wpforms-conditional-groups-payments-stripe-recurring,#wpforms-panel-field-stripe-recurring-email-wrap,#wpforms-panel-field-stripe-recurring-name-wrap',
action: 'show',
},
else: {
element: '#wpforms-panel-field-stripe-recurring-period-wrap,#wpforms-panel-field-stripe-recurring-conditional_logic-wrap,#wpforms-conditional-groups-payments-stripe-recurring,#wpforms-panel-field-stripe-recurring-email-wrap,#wpforms-panel-field-stripe-recurring-name-wrap',
action: 'hide',
},
},
effect: 'appear',
} );
},
/**
* On form save notify users about required fields.
*
* @since 1.8.2
*/
requiredFieldsCheck() {
if (
! $( '#wpforms-panel-field-stripe-enable' ).is( ':checked' ) ||
! $( '#wpforms-panel-field-stripe-recurring-enable' ).is( ':checked' )
) {
return;
}
const $emailField = $( '#wpforms-panel-field-stripe-recurring-email' );
if ( $emailField.val() ) {
return;
}
$emailField.addClass( 'wpforms-required-field-error' );
let alertMessage = wpforms_builder.stripe_recurring_email;
if ( ! $( '.wpforms-panel-content-section-stripe' ).is( ':visible' ) ) {
alertMessage += ' ' + wpforms_builder.stripe_recurring_settings;
}
$.alert( {
title: wpforms_builder.stripe_recurring_heading,
content: alertMessage,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
confirm: {
text: wpforms_builder.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
onOpen() {
$( '.wpforms-stripe-settings-redirect' ).on( 'click', app.settingsRedirect );
},
} );
},
/**
* Redirect to the settings tab.
*
* @since 1.9.5
*/
settingsRedirect() {
// Open the Stripe settings tab.
$( '.wpforms-panel-payments-button' ).trigger( 'click' );
$( '.wpforms-panel-sidebar-section-stripe' ).trigger( 'click' );
// Scroll to the Stripe settings.
window.location.href = window.location.pathname + window.location.search + '#wpforms-panel-field-stripe-enable_recurring-wrap';
// Close the alert.
$( this ).closest( '.jconfirm-box' ).find( '.btn-confirm' ).trigger( 'click' );
},
/**
* Maybe reset required email field error class.
*
* @since 1.9.5
*/
resetEmailAlertErrorClass() {
$( this ).toggleClass( 'wpforms-required-field-error', ! $( this ).val() );
},
/**
* Disable notifications.
*
* @since 1.8.2
*
* @param {Object} e Event object.
* @param {number} id Field ID.
* @param {string} type Field type.
*/
disableNotifications( e, id, type ) {
if ( ! wpforms_builder_stripe.field_slugs.includes( type ) ) {
return;
}
const $notificationWrap = $( '.wpforms-panel-content-section-notifications [id*="-stripe-wrap"]' );
$notificationWrap.find( 'input[id*="-stripe"]' ).prop( 'checked', false );
$notificationWrap.addClass( 'wpforms-hidden' );
},
/**
* Determine is legacy settings is loaded.
*
* @since 1.8.4
*
* @return {boolean} True is legacy settings loaded.
*/
isLegacySettings() {
return $( '#wpforms-panel-field-stripe-enable' ).length;
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsStripe.init();
@@ -0,0 +1 @@
var WPFormsStripe=window.WPFormsStripe||((e,i,n)=>{let r={init(){n(r.ready)},ready(){r.isLegacySettings()&&(r.settingsDisplay(),r.settingsConditions(),r.bindUIActions())},bindUIActions(){n(e).on("wpformsFieldDelete",r.disableNotifications).on("wpformsSaved",r.requiredFieldsCheck).on("wpformsFieldUpdate",r.settingsDisplay).on("wpformsFieldUpdate",r.settingsConditions),n("#wpforms-panel-field-stripe-recurring-email").on("change",r.resetEmailAlertErrorClass)},settingsDisplay(){var e=n("#wpforms-stripe-credit-card-alert"),i=n("#stripe-provider");wpforms_builder_stripe.field_slugs.filter(function(e){e=n(".wpforms-field-option-"+e);return e.length?e:null}).length?(e.hide(),i.find("#wpforms-stripe-new-interface-alert, .wpforms-stripe-notice-info, .wpforms-panel-field, .wpforms-conditional-block-panel, h2").show()):(e.show(),i.find("#wpforms-stripe-new-interface-alert, .wpforms-stripe-notice-info, .wpforms-panel-field, .wpforms-conditional-block-panel, h2").hide(),i.find("#wpforms-panel-field-stripe-enable").prop("checked",!1).trigger("change"))},settingsConditions(){n("#wpforms-panel-field-stripe-enable").conditions({conditions:{element:"#wpforms-panel-field-stripe-enable",type:"checked",operator:"is"},actions:{if:{element:".wpforms-panel-content-section-stripe-body",action:"show"},else:{element:".wpforms-panel-content-section-stripe-body",action:"hide"}},effect:"appear"}),n("#wpforms-panel-field-stripe-recurring-enable").conditions({conditions:{element:"#wpforms-panel-field-stripe-recurring-enable",type:"checked",operator:"is"},actions:{if:{element:"#wpforms-panel-field-stripe-recurring-period-wrap,#wpforms-panel-field-stripe-recurring-conditional_logic-wrap,#wpforms-conditional-groups-payments-stripe-recurring,#wpforms-panel-field-stripe-recurring-email-wrap,#wpforms-panel-field-stripe-recurring-name-wrap",action:"show"},else:{element:"#wpforms-panel-field-stripe-recurring-period-wrap,#wpforms-panel-field-stripe-recurring-conditional_logic-wrap,#wpforms-conditional-groups-payments-stripe-recurring,#wpforms-panel-field-stripe-recurring-email-wrap,#wpforms-panel-field-stripe-recurring-name-wrap",action:"hide"}},effect:"appear"})},requiredFieldsCheck(){if(n("#wpforms-panel-field-stripe-enable").is(":checked")&&n("#wpforms-panel-field-stripe-recurring-enable").is(":checked")){var i=n("#wpforms-panel-field-stripe-recurring-email");if(!i.val()){i.addClass("wpforms-required-field-error");let e=wpforms_builder.stripe_recurring_email;n(".wpforms-panel-content-section-stripe").is(":visible")||(e+=" "+wpforms_builder.stripe_recurring_settings),n.alert({title:wpforms_builder.stripe_recurring_heading,content:e,icon:"fa fa-exclamation-circle",type:"red",buttons:{confirm:{text:wpforms_builder.ok,btnClass:"btn-confirm",keys:["enter"]}},onOpen(){n(".wpforms-stripe-settings-redirect").on("click",r.settingsRedirect)}})}}},settingsRedirect(){n(".wpforms-panel-payments-button").trigger("click"),n(".wpforms-panel-sidebar-section-stripe").trigger("click"),i.location.href=i.location.pathname+i.location.search+"#wpforms-panel-field-stripe-enable_recurring-wrap",n(this).closest(".jconfirm-box").find(".btn-confirm").trigger("click")},resetEmailAlertErrorClass(){n(this).toggleClass("wpforms-required-field-error",!n(this).val())},disableNotifications(e,i,r){wpforms_builder_stripe.field_slugs.includes(r)&&((r=n('.wpforms-panel-content-section-notifications [id*="-stripe-wrap"]')).find('input[id*="-stripe"]').prop("checked",!1),r.addClass("wpforms-hidden"))},isLegacySettings(){return n("#wpforms-panel-field-stripe-enable").length}};return r})(document,window,jQuery);WPFormsStripe.init();
@@ -0,0 +1,158 @@
/* global wpforms_admin_settings_stripe, wpforms_admin, wpf */
/**
* Stripe integration settings script.
*
* @since 1.8.2
*/
const WPFormsSettingsStripe = window.WPFormsSettingsStripe || ( function( document, window, $ ) {
/**
* Elements holder.
*
* @since 1.8.2
*
* @type {Object}
*/
const el = {};
/**
* Runtime variables.
*
* @since 1.8.2
*
* @type {Object}
*/
const vars = {
alertTitle: wpforms_admin.heads_up,
alertContent: wpforms_admin_settings_stripe.mode_update,
ok: wpforms_admin.ok,
hideClassName: 'wpforms-hide',
};
/**
* Public functions and properties.
*
* @since 1.8.2
*/
const app = {
/**
* Start the engine.
*
* @since 1.8.2
*/
init() {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.8.2
*/
ready() {
app.setup();
app.bindEvents();
},
/**
* Setup. Prepare some variables.
*
* @since 1.8.2
*/
setup() {
// Cache DOM elements.
el.$wrapper = $( '.wpforms-admin-content-payments' );
el.$liveConnectionBlock = $( '.wpforms-stripe-connection-status-live' );
el.$testConnectionBlock = $( '.wpforms-stripe-connection-status-test' );
el.$testModeCheckbox = $( '#wpforms-setting-stripe-test-mode' );
el.copyButton = $( '#wpforms-setting-row-stripe-webhooks-endpoint-set .wpforms-copy-to-clipboard' );
el.webhookEndpointUrl = $( 'input#wpforms-stripe-webhook-endpoint-url' );
el.webhookMethod = $( 'input[name="stripe-webhooks-communication"]' );
},
/**
* Bind events.
*
* @since 1.8.2
*/
bindEvents() {
el.$wrapper
.on( 'change', '#wpforms-setting-stripe-test-mode', app.triggerModeSwitchAlert );
el.copyButton
.on( 'click', function( e ) {
wpf.copyValueToClipboard( e, $( this ), el.webhookEndpointUrl );
} );
el.webhookMethod
.on( 'change', app.onMethodChange );
},
/**
* Conditionally show Stripe mode switch warning.
*
* @since 1.8.2
*/
triggerModeSwitchAlert() {
if ( el.$testModeCheckbox.is( ':checked' ) ) {
el.$liveConnectionBlock.addClass( vars.hideClassName );
el.$testConnectionBlock.removeClass( vars.hideClassName );
} else {
el.$testConnectionBlock.addClass( vars.hideClassName );
el.$liveConnectionBlock.removeClass( vars.hideClassName );
}
if ( $( '#wpforms-setting-row-stripe-connection-status .wpforms-connected' ).is( ':visible' ) ) {
return;
}
$.alert( {
title: vars.alertTitle,
content: vars.alertContent,
icon: 'fa fa-exclamation-circle',
type: 'orange',
buttons: {
confirm: {
text: vars.ok,
btnClass: 'btn-confirm',
keys: [ 'enter' ],
},
},
} );
},
/**
* Copy webhooks endpoint URL to clipboard.
*
* @since 1.8.4
*
* @deprecated 1.9.5 Changed to the wpf.copyWebhooksEndpoint().
*
* @param {Object} event Event object.
*/
copyWebhooksEndpoint( event ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsSettingsStripe.copyWebhooksEndpoint()" has been deprecated! Use wpf.copyWebhooksEndpoint() instead.' );
wpf.copyValueToClipboard( event, $( this ), el.webhookEndpointUrl );
},
/**
* Update the endpoint URL.
*
* @since 1.8.4
*/
onMethodChange() {
const checked = el.webhookMethod.filter( ':checked' ).val(),
newUrl = wpforms_admin_settings_stripe.webhook_urls[ checked ];
el.webhookEndpointUrl.val( newUrl );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsSettingsStripe.init();
@@ -0,0 +1 @@
let WPFormsSettingsStripe=window.WPFormsSettingsStripe||(o=>{let t={},e={alertTitle:wpforms_admin.heads_up,alertContent:wpforms_admin_settings_stripe.mode_update,ok:wpforms_admin.ok,hideClassName:"wpforms-hide"},n={init(){o(n.ready)},ready(){n.setup(),n.bindEvents()},setup(){t.$wrapper=o(".wpforms-admin-content-payments"),t.$liveConnectionBlock=o(".wpforms-stripe-connection-status-live"),t.$testConnectionBlock=o(".wpforms-stripe-connection-status-test"),t.$testModeCheckbox=o("#wpforms-setting-stripe-test-mode"),t.copyButton=o("#wpforms-setting-row-stripe-webhooks-endpoint-set .wpforms-copy-to-clipboard"),t.webhookEndpointUrl=o("input#wpforms-stripe-webhook-endpoint-url"),t.webhookMethod=o('input[name="stripe-webhooks-communication"]')},bindEvents(){t.$wrapper.on("change","#wpforms-setting-stripe-test-mode",n.triggerModeSwitchAlert),t.copyButton.on("click",function(e){wpf.copyValueToClipboard(e,o(this),t.webhookEndpointUrl)}),t.webhookMethod.on("change",n.onMethodChange)},triggerModeSwitchAlert(){(t.$testModeCheckbox.is(":checked")?(t.$liveConnectionBlock.addClass(e.hideClassName),t.$testConnectionBlock):(t.$testConnectionBlock.addClass(e.hideClassName),t.$liveConnectionBlock)).removeClass(e.hideClassName),o("#wpforms-setting-row-stripe-connection-status .wpforms-connected").is(":visible")||o.alert({title:e.alertTitle,content:e.alertContent,icon:"fa fa-exclamation-circle",type:"orange",buttons:{confirm:{text:e.ok,btnClass:"btn-confirm",keys:["enter"]}}})},copyWebhooksEndpoint(e){console.warn('WARNING! Function "WPFormsSettingsStripe.copyWebhooksEndpoint()" has been deprecated! Use wpf.copyWebhooksEndpoint() instead.'),wpf.copyValueToClipboard(e,o(this),t.webhookEndpointUrl)},onMethodChange(){var e=t.webhookMethod.filter(":checked").val(),e=wpforms_admin_settings_stripe.webhook_urls[e];t.webhookEndpointUrl.val(e)}};return n})((document,window,jQuery));WPFormsSettingsStripe.init();
@@ -0,0 +1,472 @@
/* global Stripe, wpforms, wpforms_settings, wpforms_stripe, WPForms */
'use strict';
/**
* WPForms Stripe Elements function.
*
* @since 1.8.2
*/
var WPFormsStripeElements = window.WPFormsStripeElements || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.8.2
*
* @type {object}
*/
const app = {
stripe: null,
/**
* Number of page locked to switch.
*
* @since 1.8.2
*
* @type {int}
*/
lockedPageToSwitch: 0,
/**
* Start the engine.
*
* @since 1.8.2
*/
init: function() {
app.stripe = Stripe( // eslint-disable-line new-cap
wpforms_stripe.publishable_key,
{ 'locale': wpforms_stripe.data.element_locale }
);
$( document ).on( 'wpformsReady', function() {
$( '.wpforms-stripe form' )
.filter( ( _, form ) => typeof $( form ).data( 'formid' ) === 'number' ) // filter out forms which are locked (formid changed to 'locked-...').
.each( app.setupStripeForm );
} );
$( document ).on( 'wpformsBeforePageChange', app.pageChange );
},
/**
* Setup and configure a Stripe form.
*
* @since 1.8.2
*/
setupStripeForm: function() {
let $form = $( this );
app.updateFormSubmitHandler( $form );
$form.on( 'wpformsAjaxSubmitActionRequired', app.handleCardActionCallback );
app.updateCardElementStylesModern( $form );
},
/**
* Setup, mount and configure Stripe Card Element.
*
* @since 1.8.2
*
* @param {jQuery} $form Form element.
* @param {object} formValidator jQuery Validator object.
*
* @returns {card|void} Stripe Card element.
*/
setupCardElement: function( $form, formValidator ) {
const $hiddenInput = $form.find( '.wpforms-stripe-credit-card-hidden-input' );
if ( ! $hiddenInput || $hiddenInput.length === 0 ) {
return;
}
let cardElement = $hiddenInput.data( 'stripe-element' );
if ( cardElement ) {
return cardElement;
}
let style = wpforms_stripe.data.element_style;
if ( $.isEmptyObject( style ) ) {
style = app.getElementStyleDefault( $hiddenInput );
}
let cardSettings = {
classes : wpforms_stripe.data.element_classes,
hidePostalCode: true,
style : style,
};
cardElement = app.stripe.elements().create( 'card', cardSettings );
cardElement.mount( $form.find( '.wpforms-field-stripe-credit-card-cardnumber' ).get( 0 ) );
cardElement.on( 'change', function( e ) {
if ( ! e.error ) {
formValidator.hideThese( formValidator.errorsFor( $hiddenInput.get( 0 ) ) );
return;
}
let message = e.error.message;
if ( 'incomplete_number' === e.error.code || 'invalid_number' === e.error.code ) {
message = wpforms_settings.val_creditcard;
}
app.displayStripeError( $form, message );
} );
cardElement.on( 'focus', function() {
$( document ).trigger( 'wpformsStripePaymentElementFocus', [ $form ] );
} );
$hiddenInput.data( 'stripe-element', cardElement );
return cardElement;
},
/**
* Get default styles for card settings.
*
* @since 1.8.2
*
* @param {jQuery} $hiddenInput Input element.
*
* @returns {object|void} Base styles.
*/
getElementStyleDefault: function( $hiddenInput ) {
if ( ! $hiddenInput || $hiddenInput.length === 0 ) {
return;
}
const textColor = $hiddenInput.css( 'color' );
const fontSize = $hiddenInput.css( 'font-size' );
const style = {
base: {
fontSize,
color: textColor,
'::placeholder': {
color: textColor,
fontSize,
},
},
invalid: {
color: textColor,
},
};
let fontFamily = $hiddenInput.css( 'font-family' );
const regExp = /[“”<>!@$%^&*=~`|{}[\]]/;
if ( regExp.test( fontFamily ) || fontFamily.indexOf( 'MS Shell Dlg' ) !== -1 ) {
fontFamily = $( 'p' ).css( 'font-family' );
}
if ( ! regExp.test( fontFamily ) ) {
style.base.fontFamily = fontFamily;
style.base[ '::placeholder' ].fontFamily = fontFamily;
}
return style;
},
/**
* Update submitHandler for the forms containing Stripe.
*
* @since 1.8.2
*
* @param {jQuery} $form Form element.
*/
updateFormSubmitHandler: function( $form ) {
let formValidator = $form.validate(),
formSubmitHandler = formValidator.settings.submitHandler,
cardElement = app.setupCardElement( $form, formValidator ),
$stripeDiv = $form.find( '.wpforms-field-stripe-credit-card-cardnumber' );
// Replace the default submit handler.
formValidator.settings.submitHandler = function() {
let valid = $form.validate().form(),
ccEmpty = $stripeDiv.hasClass( wpforms_stripe.data.element_classes.empty ),
ccRequired = $stripeDiv.data( 'required' ),
condHidden = $stripeDiv.closest( '.wpforms-field-stripe-credit-card' ).hasClass( 'wpforms-conditional-hide' ),
processCard = false;
if ( ! condHidden ) {
processCard = ccRequired || ( ! ccEmpty && ! ccRequired );
}
if ( valid && processCard ) {
$form.find( '.wpforms-submit' ).prop( 'disabled', true );
app.createPaymentMethod( $form, cardElement, ccRequired, formSubmitHandler );
} else if ( valid ) {
// Form is valid, however no credit card to process.
$form.find( '.wpforms-submit' ).prop( 'disabled', false );
return formSubmitHandler( $form );
} else {
$form.find( '.wpforms-submit' ).prop( 'disabled', false );
$form.validate().cancelSubmit = true;
}
};
},
/**
* Create a PaymentMethod out of card details provided.
*
* @since 1.8.2
*
* @param {jQuery} $form Form element.
* @param {card} cardElement Stripe Card element.
* @param {boolean} ccRequired Card field is required.
* @param {Function} formSubmitHandler jQuery Validation SubmitHandler function.
*/
createPaymentMethod: function( $form, cardElement, ccRequired, formSubmitHandler ) {
app.stripe.createPaymentMethod( 'card', cardElement, {
billing_details: {
name: $form.find( '.wpforms-field-stripe-credit-card-cardname' ).val(),
},
} ).then( function( result ) {
if ( result.error && ccRequired ) {
$form.find( '.wpforms-submit' ).prop( 'disabled', false );
app.displayStripeError( $form, result.error.message );
$form.validate().cancelSubmit = true;
return;
}
if ( ! result.error ) {
$form.find( '.wpforms-stripe-payment-method-id' ).remove();
if ( result.paymentMethod ) {
$form.append( '<input type="hidden" class="wpforms-stripe-payment-method-id" name="wpforms[payment_method_id]" value="' + result.paymentMethod.id + '">' );
}
}
formSubmitHandler( $form );
} );
},
/**
* Handle 'action_required' server response.
*
* @param {object} e Event object.
* @param {object} json Data returned form a server.
*
* @since 1.8.2
*/
handleCardActionCallback: function( e, json ) {
const $form = $( this );
if ( json.success && json.data.action_required ) {
app.stripe.handleCardPayment(
json.data.payment_intent_client_secret,
{
// eslint-disable-next-line camelcase
payment_method: json.data.payment_method_id,
}
).then( function( result ) {
app.handleCardPaymentCallback( $form, result );
} );
}
},
/**
* Callback for Stripe 'handleCardPayment' method.
*
* @param {jQuery} $form Form element.
* @param {Object} result Data returned by 'handleCardPayment'.
*
* @since 1.8.2
*/
handleCardPaymentCallback( $form, result ) {
if ( result.error ) {
wpforms.restoreSubmitButton( $form, $form.closest( '.wpforms-container' ) );
$form.find( '.wpforms-field-stripe-credit-card-cardnumber' ).addClass( wpforms_stripe.data.element_classes.invalid );
app.displayStripeError( $form, result.error.message );
} else if ( result.paymentIntent && 'succeeded' === result.paymentIntent.status ) {
$form.find( '.wpforms-stripe-payment-method-id' ).remove();
$form.find( '.wpforms-stripe-payment-intent-id' ).remove();
$form.append( '<input type="hidden" class="wpforms-stripe-payment-intent-id" name="wpforms[payment_intent_id]" value="' + result.paymentIntent.id + '">' );
wpforms.formSubmitAjax( $form );
} else {
wpforms.restoreSubmitButton( $form, $form.closest( '.wpforms-container' ) );
}
},
/**
* Display a field error using jQuery Validate library.
*
* @param {jQuery} $form Form element.
* @param {object} message Error message.
*
* @since 1.8.2
*/
displayStripeError: function( $form, message ) {
const fieldName = $form.find( '.wpforms-stripe-credit-card-hidden-input' ).attr( 'name' ),
$stripeDiv = $form.find( '.wpforms-field-stripe-credit-card-cardnumber' );
let errors = {};
errors[fieldName] = message;
wpforms.displayFormAjaxFieldErrors( $form, errors );
// Switch page for the multipage form.
if ( ! $stripeDiv.is( ':visible' ) && $form.find( '.wpforms-page-indicator-steps' ).length > 0 ) {
// Empty $json object needed to change the page to the first one.
wpforms.setCurrentPage( $form, {} );
}
wpforms.scrollToError( $stripeDiv );
},
/**
* Unblock the AJAX form.
*
* @since 1.8.2
* @deprecated 1.9.5
*
* @param {jQuery} $form Form element.
*/
formAjaxUnblock( $form ) {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPFormsStripeElements.formAjaxUnblock()" has been deprecated, please use the new "wpforms.restoreSubmitButton()" function instead!' );
wpforms.restoreSubmitButton( $form, $form.closest( '.wpforms-container' ) );
},
/**
* Callback for a page changing.
*
* @since 1.8.2
*
* @param {Event} event Event.
* @param {int} currentPage Current page.
* @param {jQuery} $form Current form.
* @param {string} action The navigation action.
*/
pageChange: function( event, currentPage, $form, action ) {
const $stripeDiv = $form.find( '.wpforms-field-stripe-credit-card-cardnumber' ),
ccComplete = $stripeDiv.hasClass( wpforms_stripe.data.element_classes.complete ),
ccEmpty = $stripeDiv.hasClass( wpforms_stripe.data.element_classes.empty ),
ccInvalid = $stripeDiv.hasClass( wpforms_stripe.data.element_classes.invalid );
// Stop navigation through page break pages.
if (
! $stripeDiv.is( ':visible' ) ||
( ! $stripeDiv.data( 'required' ) && ccEmpty ) ||
( app.lockedPageToSwitch && app.lockedPageToSwitch !== currentPage ) ||
action === 'prev'
) {
return;
}
if ( ccComplete ) {
$stripeDiv.find( '.wpforms-error' ).remove();
return;
}
app.lockedPageToSwitch = currentPage;
event.preventDefault();
if ( ccInvalid ) {
return;
}
app.displayStripeError( $form, wpforms_stripe.i18n.empty_details );
},
/**
* Get CSS property value.
* In case of exception return empty string.
*
* @since 1.8.6
*
* @param {jQuery} $element Element.
* @param {string} property Property.
*
* @return {string} Property value.
*/
getCssPropertyValue( $element, property ) {
try {
return $element.css( property );
} catch ( e ) {
return '';
}
},
/**
* Update Card Element styles in Modern Markup mode.
*
* @since 1.8.2
*
* @param {jQuery} $form Form object.
*/
updateCardElementStylesModern( $form ) {
// Should work only in Modern Markup mode.
if ( ! window.WPForms || ! WPForms.FrontendModern || ! $.isEmptyObject( wpforms_stripe.data.element_style ) ) {
return;
}
if ( ! $form || $form.length === 0 ) {
return;
}
$form.find( '.wpforms-stripe-credit-card-hidden-input' ).each( function() {
const $hiddenInput = $( this );
const cardElement = $hiddenInput.data( 'stripe-element' );
const inputStyle = {
fontSize: app.getCssPropertyValue( $hiddenInput, 'font-size' ),
colorText: app.getCssPropertyValue( $hiddenInput, 'color' ),
};
if ( ! cardElement ) {
return;
}
const styles = {
base: {
color: inputStyle.colorText,
fontSize: inputStyle.fontSize,
'::placeholder': {
color: WPForms.FrontendModern.getColorWithOpacity( inputStyle.colorText, '0.5' ),
fontSize: inputStyle.fontSize,
},
},
invalid: {
color: inputStyle.colorText,
},
};
cardElement.update( { style: styles } );
} );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsStripeElements.init();
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,74 @@
/* global wpforms_woocommerce_notifications */
/**
* WooCommerce Notifications integration script.
*
* @since 1.8.9
*/
const WPFormsWoocommerceNotifications = window.WPFormsWoocommerceNotifications || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.8.9
*/
const app = {
/**
* Start the engine.
*
* @since 1.8.9
*/
init() {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.8.9
*/
ready() {
app.events();
},
/**
* Events.
*
* @since 1.8.9
*/
events() {
$( '#wpforms-woocommerce-close' ).on( 'click', app.dismiss );
},
/**
* Hide notification.
*
* @since 1.8.9
*/
dismiss() {
const $btn = $( this );
const $notification = $btn.closest( '.wpforms-woocommerce-notification' );
$notification.remove();
const data = {
action: 'wpforms_woocommerce_dismiss',
nonce: wpforms_woocommerce_notifications.nonce,
};
$.post( wpforms_woocommerce_notifications.ajax_url, data, function( res ) {
if ( ! res.success ) {
// eslint-disable-next-line no-console
console.log( res );
}
} ).fail( function( xhr ) {
// eslint-disable-next-line no-console
console.log( xhr.responseText );
} );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsWoocommerceNotifications.init();
@@ -0,0 +1 @@
let WPFormsWoocommerceNotifications=window.WPFormsWoocommerceNotifications||(e=>{let o={init(){e(o.ready)},ready(){o.events()},events(){e("#wpforms-woocommerce-close").on("click",o.dismiss)},dismiss(){e(this).closest(".wpforms-woocommerce-notification").remove();var o={action:"wpforms_woocommerce_dismiss",nonce:wpforms_woocommerce_notifications.nonce};e.post(wpforms_woocommerce_notifications.ajax_url,o,function(o){o.success||console.log(o)}).fail(function(o){console.log(o.responseText)})}};return o})((document,window,jQuery));WPFormsWoocommerceNotifications.init();
@@ -0,0 +1,165 @@
/* global wpformsWpcodeVars, List, wpforms_admin */
/**
* @param wpformsWpcodeVars.installing_text
*/
// noinspection ES6ConvertVarToLetConst
/**
* WPCode integration script.
*
* @since 1.8.5
*/
// eslint-disable-next-line no-var
var WPFormsWPCode = window.WPFormsWPCode || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.8.5
*/
const app = {
/**
* Blue spinner HTML.
*
* @since 1.8.5
*
* @type {Object}
*/
spinnerBlue: '<i class="wpforms-loading-spinner wpforms-loading-blue wpforms-loading-inline"></i>',
/**
* White spinner HTML.
*
* @since 1.8.5
*
* @type {Object}
*/
spinnerWhite: '<i class="wpforms-loading-spinner wpforms-loading-white wpforms-loading-inline"></i>',
/**
* List.js object.
*
* @since 1.8.5
*
* @type {Object}
*/
snippetSearch: null,
/**
* Start the engine.
*
* @since 1.8.5
*/
init() {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.8.5
*/
ready() {
app.snippetSearch = new List(
'wpforms-wpcode-snippets-list',
{
valueNames: [ 'wpforms-wpcode-snippet-title' ],
}
);
app.events();
},
/**
* Events.
*
* @since 1.8.5
*/
events() {
$( '.wpforms-wpcode-snippet-button' ).on( 'click', app.installSnippet );
$( '.wpforms-wpcode-popup-button' ).on( 'click', app.installPlugin );
$( '#wpforms-wpcode-snippet-search' ).on( 'keyup search', function() {
app.searchSnippet( this );
} );
},
/**
* Install snippet.
*
* @since 1.8.5
*/
installSnippet() {
const $button = $( this );
if ( $button.data( 'action' ) === 'edit' ) {
return;
}
const originalWidth = $button.width();
const $badge = $button.prev( '.wpforms-wpcode-snippet-badge' );
$badge.addClass( 'wpforms-wpcode-installing-in-progress' ).text( wpformsWpcodeVars.installing_text );
$button.width( originalWidth ).html( app.spinnerBlue );
},
/**
* Search snippet.
*
* @param {Object} searchField The search field html element.
* @since 1.8.5
*/
searchSnippet( searchField ) {
const searchTerm = $( searchField ).val();
const searchResults = app.snippetSearch.search( searchTerm );
const $noResultsMessage = $( '#wpforms-wpcode-no-results' );
if ( searchResults.length === 0 ) {
$noResultsMessage.show();
} else {
$noResultsMessage.hide();
}
},
/**
* Install or activate WPCode plugin by button click.
*
* @since 1.8.5
*/
installPlugin() {
const $btn = $( this );
if ( $btn.hasClass( 'disabled' ) ) {
return;
}
const action = $btn.attr( 'data-action' ),
plugin = $btn.attr( 'data-plugin' ),
// eslint-disable-next-line camelcase
args = JSON.stringify( { overwrite_package: true } ),
ajaxAction = action === 'activate' ? 'wpforms_activate_addon' : 'wpforms_install_addon';
// Fix original button width, add spinner and disable it.
$btn.width( $btn.width() ).html( app.spinnerWhite ).addClass( 'disabled' );
const data = {
action: ajaxAction,
nonce: wpforms_admin.nonce,
plugin,
args,
type: 'plugin',
};
$.post( wpforms_admin.ajax_url, data )
.done( function() {
location.reload();
} );
},
};
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsWPCode.init();
@@ -0,0 +1 @@
var WPFormsWPCode=window.WPFormsWPCode||(t=>{let p={spinnerBlue:'<i class="wpforms-loading-spinner wpforms-loading-blue wpforms-loading-inline"></i>',spinnerWhite:'<i class="wpforms-loading-spinner wpforms-loading-white wpforms-loading-inline"></i>',snippetSearch:null,init(){t(p.ready)},ready(){p.snippetSearch=new List("wpforms-wpcode-snippets-list",{valueNames:["wpforms-wpcode-snippet-title"]}),p.events()},events(){t(".wpforms-wpcode-snippet-button").on("click",p.installSnippet),t(".wpforms-wpcode-popup-button").on("click",p.installPlugin),t("#wpforms-wpcode-snippet-search").on("keyup search",function(){p.searchSnippet(this)})},installSnippet(){var i,n=t(this);"edit"!==n.data("action")&&(i=n.width(),n.prev(".wpforms-wpcode-snippet-badge").addClass("wpforms-wpcode-installing-in-progress").text(wpformsWpcodeVars.installing_text),n.width(i).html(p.spinnerBlue))},searchSnippet(i){var i=t(i).val(),i=p.snippetSearch.search(i),n=t("#wpforms-wpcode-no-results");0===i.length?n.show():n.hide()},installPlugin(){var i,n,e,s=t(this);s.hasClass("disabled")||(e=s.attr("data-action"),i=s.attr("data-plugin"),n=JSON.stringify({overwrite_package:!0}),e="activate"===e?"wpforms_activate_addon":"wpforms_install_addon",s.width(s.width()).html(p.spinnerWhite).addClass("disabled"),s={action:e,nonce:wpforms_admin.nonce,plugin:i,args:n,type:"plugin"},t.post(wpforms_admin.ajax_url,s).done(function(){location.reload()}))}};return p})((document,window,jQuery));WPFormsWPCode.init();