Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion css/cloudinary.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/cloudinary.js

Large diffs are not rendered by default.

267 changes: 18 additions & 249 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion php/templates/settings-header.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<div class="wrap">
<h1><?php echo esc_html( $this->ui['page_title'] ); ?></h1>
<?php if ( ! empty( $current_tab ) ) : ?>
<form method="post" action="options.php" novalidate="novalidate" class="render-trigger" data-event="tabs.init">
<form method="post" action="options.php" novalidate="novalidate" class="render-trigger cld-settings-form" data-event="tabs.init" data-cld-settings-form="true">
<input type="hidden" name="tab" value="<?php echo esc_attr( $current_tab ); ?>"/>
<?php settings_fields( $setting_slug ); ?>
<?php endif; ?>
4 changes: 4 additions & 0 deletions php/ui/component/class-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ protected function form( $struct ) {
'method' => 'post',
'action' => 'options.php',
'novalidate' => 'novalidate',
'data-cld-settings-form' => 'true',
);
$struct['attributes'] = wp_parse_args( $struct['attributes'], $form_atts );
$classes = isset( $struct['attributes']['class'] ) ? (array) $struct['attributes']['class'] : array();
$classes[] = 'cld-settings-form';
$struct['attributes']['class'] = array_unique( $classes );

// Don't run action if page has tabs, since the page actions will be different for each tab.
$struct['children'] = $this->page_actions();
Expand Down
5 changes: 5 additions & 0 deletions src/css/components/_settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@
margin: 0;
}

.cld-settings-submit-dirty {
outline: 2px solid #dba617 !important;
outline-offset: 2px;
}

.cloudinary-collapsible {
width: 95%;
padding: 10px;
Expand Down
239 changes: 239 additions & 0 deletions src/js/components/settings-page.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,241 @@
( function () {
const __ =
window.wp && window.wp.i18n && window.wp.i18n.__
? window.wp.i18n.__
: ( text ) => text;
const UNSAVED_CHANGES_MESSAGE = __(
'You have unsaved changes. If you leave this page, your changes will be lost.',
'cloudinary'
);

const isTrackableField = function ( field ) {
if ( ! field || ! field.name || field.disabled ) {
return false;
}

if (
'_wp_http_referer' === field.name ||
'_wpnonce' === field.name ||
'option_page' === field.name ||
'action' === field.name ||
'tab' === field.name
) {
return false;
}

return ! [ 'button', 'submit', 'reset', 'image', 'file' ].includes(
field.type
);
};

const serializeFormState = function ( form ) {
const state = {};
const fields = Array.from(
form.querySelectorAll( 'input, select, textarea' )
).filter( isTrackableField );

fields.forEach( ( field ) => {
const key = field.name;
let value = field.value;

if ( 'checkbox' === field.type ) {
value = field.checked ? field.value : '__unchecked__';
} else if ( 'radio' === field.type && ! field.checked ) {
value = '__unchecked__';
}

if ( ! Object.prototype.hasOwnProperty.call( state, key ) ) {
state[ key ] = [];
}

state[ key ].push( String( value ) );
} );

Object.keys( state ).forEach( ( key ) => {
state[ key ] = state[ key ].sort();
} );

return JSON.stringify( state );
};

const getSaveButtons = function ( form ) {
// Only select the actual settings save buttons by their name attribute.
// Deliberately excludes other button-primary buttons (e.g. Disconnect)
// that share the same form but are not save actions.
const named = Array.from(
form.querySelectorAll( 'button[name="cld_submission"]' )
);
if ( named.length ) {
return named;
}
// Fallback for options.php–style forms that use a standard submit input.
const legacy = form.querySelector( '#submit, input[type="submit"]' );
return legacy ? [ legacy ] : [];
};

const getButtonLabel = function ( button ) {
return 'input' === button.tagName.toLowerCase()
? button.value
: button.textContent.trim();
};

const setButtonLabel = function ( button, label ) {
if ( 'input' === button.tagName.toLowerCase() ) {
button.value = label;
} else {
button.textContent = label;
}
};

const createDirtyTracker = function ( form ) {
const saveButtons = getSaveButtons( form );
const originalLabels = new Map(
saveButtons.map( ( btn ) => [ btn, getButtonLabel( btn ) ] )
);
const tracker = {
form,
saveButtons,
initialState: serializeFormState( form ),
isDirty: false,
allowNavigation: false,
};

const syncButtonState = function () {
tracker.saveButtons.forEach( function ( btn ) {
if ( tracker.isDirty ) {
btn.classList.add( 'cld-settings-submit-dirty' );
setButtonLabel( btn, originalLabels.get( btn ) + ' *' );
} else {
btn.classList.remove( 'cld-settings-submit-dirty' );
setButtonLabel( btn, originalLabels.get( btn ) );
}
} );
};

const updateDirtyState = function () {
if ( tracker.allowNavigation ) {
return;
}

tracker.isDirty =
serializeFormState( tracker.form ) !== tracker.initialState;
tracker.form.classList.toggle(
'cld-settings-form-has-unsaved',
tracker.isDirty
);
syncButtonState();
};

tracker.form.addEventListener( 'input', function ( event ) {
if ( ! isTrackableField( event.target ) ) {
return;
}

updateDirtyState();
} );
tracker.form.addEventListener( 'change', function ( event ) {
if ( ! isTrackableField( event.target ) ) {
return;
}

updateDirtyState();
} );
tracker.form.addEventListener( 'submit', function () {
tracker.allowNavigation = true;
tracker.isDirty = false;
tracker.form.classList.remove( 'cld-settings-form-has-unsaved' );
tracker.saveButtons.forEach( function ( btn ) {
btn.classList.remove( 'cld-settings-submit-dirty' );
setButtonLabel( btn, originalLabels.get( btn ) );
} );
} );

return tracker;
};

const initUnsavedChangesGuards = function () {
const forms = Array.from(
document.querySelectorAll(
'form[data-cld-settings-form="true"], #cloudinary-settings-page form.render-trigger, #cloudinary-settings-page form[action*="options.php"], #cloudinary-settings-page form'
)
)
.filter(
( form, index, list ) =>
list.findIndex( ( candidate ) => candidate === form ) ===
index
)
.filter( ( form ) =>
Boolean(
form.querySelector(
'input[name="cloudinary-active-slug"], input[name="option_page"]'
)
)
);
if ( ! forms.length ) {
return;
}

const trackers = forms.map( createDirtyTracker );
const hasUnsavedChanges = function () {
return trackers.some(
( tracker ) => tracker.isDirty && ! tracker.allowNavigation
);
};
const unlockNavigation = function () {
trackers.forEach( ( tracker ) => {
tracker.allowNavigation = true;
} );
};

window.addEventListener( 'beforeunload', function ( event ) {
if ( ! hasUnsavedChanges() ) {
return;
}

event.preventDefault();
event.returnValue = UNSAVED_CHANGES_MESSAGE;
} );

document.addEventListener( 'click', function ( event ) {
if ( ! hasUnsavedChanges() ) {
return;
}

const link = event.target.closest( 'a[href]' );
if ( ! link || ! link.href ) {
return;
}

if (
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}

const href = link.getAttribute( 'href' );
if (
'_blank' === link.target ||
link.hasAttribute( 'download' ) ||
! href ||
href.startsWith( '#' ) ||
href.startsWith( 'javascript:' )
) {
return;
}

if ( ! window.confirm( UNSAVED_CHANGES_MESSAGE ) ) {
event.preventDefault();
event.stopPropagation();
return;
}

unlockNavigation();
} );
};

// Disable the "off" dropdown option for Autoplay if
// the player isn't set to Cloudinary or if Show Controls if unchecked.
const disableAutoplayOff = function () {
Expand Down Expand Up @@ -154,5 +391,7 @@
event = trigger.data( 'event' );
trigger.trigger( event, this );
} );

window.setTimeout( initUnsavedChangesGuards, 0 );
} );
} )( window, jQuery );
2 changes: 1 addition & 1 deletion ui-definitions/components/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
$connected = $cloudinary->settings->get_param( 'connected' );
$active_slug = $admin->get_param( 'active_slug' );
?>
<form method="post" novalidate="novalidate">
<form method="post" novalidate="novalidate" class="cld-settings-form" data-cld-settings-form="true">
<?php $admin->render_notices(); ?>
<div class="cld-ui-wrap cld-row">
<?php wp_nonce_field( 'cloudinary-settings', '_cld_nonce' ); ?>
Expand Down