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}