| 1 |
/** |
| 2 |
* WordPress dependencies |
| 3 |
*/ |
| 4 |
import { escapeHTML } from '@wordpress/escape-html'; |
| 5 |
import { __, sprintf } from '@wordpress/i18n'; |
| 6 |
|
| 7 |
/** |
| 8 |
* Internal dependencies |
| 9 |
*/ |
| 10 |
import { |
| 11 |
ContentHelperError, |
| 12 |
ContentHelperErrorCode, |
| 13 |
} from '../content-helper/common/content-helper-error'; |
| 14 |
import { CheckAuthProvider } from '../content-helper/common/providers/check-auth-provider'; |
| 15 |
|
| 16 |
document.addEventListener( 'DOMContentLoaded', (): void => { |
| 17 |
displayContentHelperSectionMessages(); |
| 18 |
addContentHelperTabEventHandlers(); |
| 19 |
setActiveTab(); |
| 20 |
window.addEventListener( 'hashchange', setActiveTab ); |
| 21 |
document.querySelector( '.media-single-image button.browse' )?.addEventListener( 'click', selectImage ); |
| 22 |
} ); |
| 23 |
|
| 24 |
function setActiveTab(): void { |
| 25 |
const activeTab = location.hash !== '' ? location.hash.substring( 1 ) : 'basic-section'; |
| 26 |
|
| 27 |
document.querySelectorAll( '.nav-tab' )?.forEach( ( t: Element ): void => { |
| 28 |
if ( t.classList.contains( activeTab + '-tab' ) ) { |
| 29 |
t.classList.add( 'nav-tab-active' ); |
| 30 |
} else { |
| 31 |
t.classList.remove( 'nav-tab-active' ); |
| 32 |
} |
| 33 |
} ); |
| 34 |
|
| 35 |
document.querySelectorAll( '.tab-content' )?.forEach( ( t: Element ): void => { |
| 36 |
if ( t.classList.contains( activeTab ) ) { |
| 37 |
t.setAttribute( 'style', 'display: initial' ); |
| 38 |
} else { |
| 39 |
t.setAttribute( 'style', 'display: none' ); |
| 40 |
} |
| 41 |
} ); |
| 42 |
|
| 43 |
const form = document.querySelector( 'form[name="parsely"]' ); |
| 44 |
if ( form ) { |
| 45 |
form.removeAttribute( 'hidden' ); |
| 46 |
form.setAttribute( 'action', `options.php#${ activeTab }` ); |
| 47 |
} |
| 48 |
} |
| 49 |
|
| 50 |
function selectImage( event: Event ) { |
| 51 |
const optionName = ( event.target as HTMLButtonElement ).dataset.option; |
| 52 |
|
| 53 |
const imageFrame = window.wp.media( { |
| 54 |
multiple: false, |
| 55 |
library: { |
| 56 |
type: 'image', |
| 57 |
}, |
| 58 |
} ); |
| 59 |
|
| 60 |
imageFrame.on( 'select', function() { |
| 61 |
const url = imageFrame.state().get( 'selection' ).first().toJSON().url; |
| 62 |
const inputSelector: string = '#media-single-image-' + optionName + ' input.file-path'; |
| 63 |
|
| 64 |
const inputElement: HTMLInputElement | null = document.querySelector( inputSelector ); |
| 65 |
if ( inputElement ) { |
| 66 |
inputElement.value = url; |
| 67 |
} |
| 68 |
} ); |
| 69 |
|
| 70 |
imageFrame.open(); |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Adds the necessary event handlers to the Content Helper tab. |
| 75 |
* |
| 76 |
* @since 3.16.0 |
| 77 |
*/ |
| 78 |
function addContentHelperTabEventHandlers(): void { |
| 79 |
// Selector for the checkbox that enables/disables all AI features. |
| 80 |
const aiFeaturesCheckbox = document.querySelector<HTMLInputElement>( |
| 81 |
'input#content_helper_ai_features_enabled' |
| 82 |
); |
| 83 |
|
| 84 |
// Selector for the checkboxes that enable/disable individual AI features. |
| 85 |
const featureCheckboxes = document.querySelectorAll<HTMLInputElement>( |
| 86 |
'input#content_helper_smart_linking_enabled, ' + |
| 87 |
'input#content_helper_title_suggestions_enabled, ' + |
| 88 |
'input#content_helper_excerpt_suggestions_enabled, ' + |
| 89 |
'input#content_helper_traffic_boost_enabled' |
| 90 |
); |
| 91 |
|
| 92 |
// Selector for all fieldsets in the Content Helper section. |
| 93 |
const fieldsets = document.querySelectorAll( |
| 94 |
'div.content-helper-section fieldset' |
| 95 |
); |
| 96 |
|
| 97 |
// Event handlers. |
| 98 |
enableAllFormFieldsOnSubmit(); |
| 99 |
updateAllFeatureSections(); // Must run on load to update the UI. |
| 100 |
|
| 101 |
aiFeaturesCheckbox?.addEventListener( 'change', (): void => { |
| 102 |
updateAllFeatureSections(); |
| 103 |
} ); |
| 104 |
|
| 105 |
featureCheckboxes.forEach( ( checkbox ): void => { |
| 106 |
checkbox.addEventListener( 'change', (): void => { |
| 107 |
updateFeatureSection( checkbox ); |
| 108 |
} ); |
| 109 |
} ); |
| 110 |
|
| 111 |
/** |
| 112 |
* Turns on/off all AI feature sections in the UI. |
| 113 |
* |
| 114 |
* @since 3.16.0 |
| 115 |
*/ |
| 116 |
function updateAllFeatureSections(): void { |
| 117 |
if ( ! aiFeaturesCheckbox ) { |
| 118 |
return; |
| 119 |
} |
| 120 |
|
| 121 |
if ( aiFeaturesCheckbox.checked ) { |
| 122 |
// Enable all applicable fieldsets. |
| 123 |
fieldsets.forEach( ( fieldset: Element ): void => { |
| 124 |
setDisabled( fieldset, false ); |
| 125 |
|
| 126 |
featureCheckboxes.forEach( ( checkbox ): void => { |
| 127 |
updateFeatureSection( checkbox ); |
| 128 |
} ); |
| 129 |
} ); |
| 130 |
} else { |
| 131 |
// Disable all fieldsets. |
| 132 |
fieldsets.forEach( ( fieldset: Element ): void => { |
| 133 |
if ( ! fieldset.querySelector( `#${ aiFeaturesCheckbox.id }` ) ) { |
| 134 |
setDisabled( fieldset ); |
| 135 |
} |
| 136 |
} ); |
| 137 |
|
| 138 |
// Disable "Enabled" labels. |
| 139 |
document.querySelectorAll( 'label.prevent-disable' ) |
| 140 |
.forEach( ( label: Element ): void => { |
| 141 |
setPreventDisable( label, false ); |
| 142 |
} ); |
| 143 |
} |
| 144 |
} |
| 145 |
|
| 146 |
/** |
| 147 |
* Turns on/off a specific feature section in the UI. |
| 148 |
* |
| 149 |
* @since 3.16.0 |
| 150 |
* |
| 151 |
* @param {HTMLInputElement} checkbox The checkbox controlling the feature. |
| 152 |
*/ |
| 153 |
function updateFeatureSection( checkbox: HTMLInputElement ): void { |
| 154 |
const userRolesFieldset = checkbox |
| 155 |
.closest( 'fieldset' )?.nextSibling?.nextSibling as HTMLElement; |
| 156 |
|
| 157 |
if ( checkbox.checked ) { |
| 158 |
setDisabled( [ checkbox, userRolesFieldset ], false ); |
| 159 |
} else { |
| 160 |
setDisabled( userRolesFieldset ); |
| 161 |
|
| 162 |
// Keep enabled styling on "Enabled" labels for visibility. |
| 163 |
const enabledLabel = checkbox.parentElement as HTMLLabelElement; |
| 164 |
setPreventDisable( enabledLabel ); |
| 165 |
} |
| 166 |
} |
| 167 |
|
| 168 |
/** |
| 169 |
* Allows or prevents an element from being shown as disabled. |
| 170 |
* |
| 171 |
* This is done by injecting a class into the element, which is used in CSS. |
| 172 |
* |
| 173 |
* @since 3.16.0 |
| 174 |
* |
| 175 |
* @param {Element} element The target element. |
| 176 |
* @param {boolean} status true to prevent disabled style, false to allow. |
| 177 |
*/ |
| 178 |
function setPreventDisable( element: Element, status: boolean = true ): void { |
| 179 |
if ( status ) { |
| 180 |
element.classList.add( 'prevent-disable' ); |
| 181 |
} else { |
| 182 |
element.classList.remove( 'prevent-disable' ); |
| 183 |
} |
| 184 |
} |
| 185 |
|
| 186 |
/** |
| 187 |
* Sets the disabled attribute on an element. |
| 188 |
* |
| 189 |
* @since 3.16.0 |
| 190 |
* |
| 191 |
* @param {Element | Element[]} element The target element. |
| 192 |
* @param {boolean} status true to disable, false to enable. |
| 193 |
*/ |
| 194 |
function setDisabled( element: Element | Element[], status: boolean = true ): void { |
| 195 |
if ( ! Array.isArray( element ) ) { |
| 196 |
element = [ element ]; |
| 197 |
} |
| 198 |
|
| 199 |
element.forEach( ( el: Element ): void => { |
| 200 |
if ( status ) { |
| 201 |
el.setAttribute( 'disabled', 'disabled' ); |
| 202 |
} else { |
| 203 |
el.removeAttribute( 'disabled' ); |
| 204 |
} |
| 205 |
} ); |
| 206 |
} |
| 207 |
|
| 208 |
/** |
| 209 |
* Allows the form to post its whole data. |
| 210 |
* |
| 211 |
* When elements are disabled, they are not included when submitting the |
| 212 |
* form, resulting in missing data. |
| 213 |
* |
| 214 |
* This workaround removes any disabled attribute so the form can submit its |
| 215 |
* whole data. It also prevents styling changes while the data is being |
| 216 |
* submitted, in order to avoid visual glitches. |
| 217 |
* |
| 218 |
* @since 3.16.0 |
| 219 |
*/ |
| 220 |
function enableAllFormFieldsOnSubmit(): void { |
| 221 |
document.querySelector( '.wp-admin form[name="parsely"]' ) |
| 222 |
?.addEventListener( 'submit', (): void => { |
| 223 |
const baseSelector = '.wp-admin .content-helper-section fieldset'; |
| 224 |
|
| 225 |
document.querySelectorAll( `${ baseSelector }[disabled]` ) |
| 226 |
.forEach( ( fieldset: Element ): void => { |
| 227 |
// Style the whole setting table row as disabled. |
| 228 |
fieldset.parentElement?.parentElement?.classList.add( |
| 229 |
'disabled-before-posting' |
| 230 |
); |
| 231 |
|
| 232 |
// Avoid disabled checkbox styling changes. |
| 233 |
fieldset.querySelectorAll( |
| 234 |
`${ baseSelector } label input[type="checkbox"]` |
| 235 |
).forEach( ( input: Element ): void => { |
| 236 |
input.classList.add( 'disabled' ); |
| 237 |
} ); |
| 238 |
|
| 239 |
fieldset.removeAttribute( 'disabled' ); |
| 240 |
} ); |
| 241 |
} ); |
| 242 |
} |
| 243 |
} |
| 244 |
|
| 245 |
/** |
| 246 |
* Displays any messages needed under the Content Helper section. |
| 247 |
* |
| 248 |
* @since 3.19.0 |
| 249 |
*/ |
| 250 |
async function displayContentHelperSectionMessages(): Promise<void> { |
| 251 |
let message = null; |
| 252 |
let authResponse = null; |
| 253 |
|
| 254 |
try { |
| 255 |
const [ apiAuth, trafficBoostAuth ] = await Promise.all( [ |
| 256 |
CheckAuthProvider.getInstance().getAuthorizationResponse( |
| 257 |
{ auth_scope: 'suggestions_api' } |
| 258 |
), |
| 259 |
CheckAuthProvider.getInstance().getAuthorizationResponse( |
| 260 |
{ auth_scope: 'traffic_boost' } |
| 261 |
), |
| 262 |
] ); |
| 263 |
|
| 264 |
authResponse = { |
| 265 |
api: apiAuth, |
| 266 |
traffic_boost: trafficBoostAuth, |
| 267 |
}; |
| 268 |
} catch ( err: unknown ) { |
| 269 |
console.error( err ); // eslint-disable-line no-console |
| 270 |
|
| 271 |
if ( err instanceof ContentHelperError ) { |
| 272 |
if ( ContentHelperErrorCode.PluginSettingsApiSecretNotSet === err.code ) { |
| 273 |
message = sprintf( '<p><strong>%s</strong></p>', escapeHTML( __( 'All Content Helper AI functionality is disabled because an API Secret has not been set.', 'wp-parsely' ) ) ); |
| 274 |
} |
| 275 |
} |
| 276 |
} finally { |
| 277 |
if ( authResponse ) { |
| 278 |
if ( 200 !== authResponse.api.code ) { |
| 279 |
const requestAccessLink = sprintf( '<a href="%1$s" target="_blank" rel="noopener">%2$s</a>', 'https://wpvip.com/content-helper/#content-helper-form', __( 'Request access here', 'wp-parsely' ) ); |
| 280 |
/* translators: %s: Link to request access to Content Helper AI functionality. */ |
| 281 |
const messageWithAccessLink = sprintf( escapeHTML( __( 'All Content Helper AI functionality is disabled for this website. %s.', 'wp-parsely' ) ), requestAccessLink ); |
| 282 |
message = sprintf( '<p><strong>%s</strong></p>', messageWithAccessLink ); |
| 283 |
} else if ( 200 === authResponse.api.code && 200 !== authResponse.traffic_boost.code ) { |
| 284 |
const contactSupportLink = sprintf( '<a href="%1$s">%2$s</a>', 'mailto:support@parsely.com', 'support@parsely.com' ); |
| 285 |
/* translators: %s: Link to request access to Content Helper AI functionality. */ |
| 286 |
const messageWithAccessLink = sprintf( escapeHTML( __( 'Engagement Boost functionality is disabled for this website. To enable it, contact %s.', 'wp-parsely' ) ), contactSupportLink ); |
| 287 |
message = sprintf( '<p><strong>%s</strong></p>', messageWithAccessLink ); |
| 288 |
} |
| 289 |
} |
| 290 |
|
| 291 |
if ( message ) { |
| 292 |
const div = document.createElement( 'div' ); |
| 293 |
div.className = 'content-helper-message notice notice-error'; |
| 294 |
div.innerHTML = message; |
| 295 |
const contentHelperSection = document.querySelector( '.content-helper-section' ); |
| 296 |
if ( contentHelperSection ) { |
| 297 |
contentHelperSection.insertBefore( div, contentHelperSection.firstChild ); |
| 298 |
} |
| 299 |
} |
| 300 |
} |
| 301 |
} |
| 302 |
|