Blog

Code Snippet: Oxygen Rename Global Colors

(2020)

Contents

Introduction

Oxygen is a powerful plugin for WordPress that deactivates the WordPress theme, supports the creation of own templates with a visual builder and thus enables a fully individual design for your website.

Within the Oxygen Builder you can define global colors with color value and name, which are then available in all color selection fields of the Builder. For better organization, you can also create your own color folders ("Color Sets") to group the defined colors thematically, for example.

Unfortunately, you cannot change the name of a color set or color in Oxygen.

Solution

I have developed a code snippet that allows renaming of color sets and colors.

In the WordPress admin menu in the Oxygen section, there is a new entry "Global Colors" for this purpose.

For attention:
My code snippet prevents saving color definitions when an Oxygen Builder is open in another browser window (even by another user). Simultaneous changes would overwrite each other.
For this purpose my Code Snippet checks every few seconds whether an Oxygen Builder has been opened in another tab or by another user. In this case a warning is issued and saving is prevented.
At the same time my Code Snippet checks whether the displayed color definitions still correspond to the state in the database. If there are discrepancies, for example because changes were made in another browser window or by another user, reloading of the color definitions is recommended.

Warning!
If you use elements from Oxygen's Design Sets they add color sets, named like the design set (e.g. "atomic"), and add colors to that color set.
You better DON'T rename those color sets or colors. Otherwise, if you add another element from that design set, a new color set with the original design set name will be created and colors will be added.

Download

The code snippet is available for download here:

oxygen-rename-global-color-sets-and-colors.code-snippets.json
Version 1.0.2, 2021-03-21

For installation and use of the downloaded JSON file you will need the plugin Code Snippets or Advanced Scripts.
You can install the JSON file using the "Import" function of the plugin. 
Don't forget to activate the snippet after import.

Alternative: At the end of this page you can view and copy the complete source code of the snippet.

New functionalities and bug fixes are documented in the change log.

Disclaimer

I have developed and tested the code snippet to the best of my knowledge under WordPress 5.7 and Oxygen 3.7.1.
This snippet will NOT work with Oxygen Version older than 3.6.
I provide the code snippet for free use.
I cannot give any guarantee for the functionality because of the countless possible variations in WordPress environments.
Download and use of this code snippet is at your own risk and responsibility.

Donation

I enjoy developing code snippets and solving requirements with them. I provide the snippets free of charge.

If you like, you can honor my many hours of work with a small coffee donation via PayPal.

  When clicking the button, a connection to PayPal is established.

Your donation will of course be taxed properly by me.

Change Log

See Source Code.

Source Code

<?php 
/*
Plugin Name:  MA Oxygen Global Colors
Description:  Rename Oxygen Global Color Sets and Colors
Author:       <a href="https://www.altmann.de/">Matthias Altmann</a>
Version:      1.0.2
Plugin URI:   https://www.altmann.de/en/blog-en/code-snippet-oxygen-rename-global-colors/
Description:  en: https://www.altmann.de/en/blog-en/code-snippet-oxygen-rename-global-colors/
              de: https://www.altmann.de/blog/code-snippet-oxygen-globale-farben-umbenennen/
Copyright:    © 2020-2021, Matthias Altmann

Version History:
Date		Version		Description
---------------------------------------------------------------------------------------------------------------------
2020-12-28				Development start
2020-12-29 	1.0.0		Initial Release 
2021-01-09	1.0.1		New Feature: Show color values and allow copy to clipboard
2021-03-21	1.0.2		Bug fix: Check Oxygen plugin state before adding admin menu or accessing global colors
						(Thanks to Adrien Robert for reporting!)

*/

class MA_Oxygen_Global_Colors {

	const TITLE     	= 'MA Oxygen Global Colors';
	const SLUG	    	= 'ma_oxygen_global_colors';
	const VERSION   	= '1.0.2';
	
	// Configuration
	private static $debug				= false; // caution! may produce a lot of output
	private static $timing				= false; // caution! may produce a lot of output
	private static $status_interval 	= 5000; // interval (ms) to refresh  "builder open" and "colors changed" status
	
	// Internal data
	private static $oxygen_message 		= ''; // temp storage for succes/error message from oxygen

	//-------------------------------------------------------------------------------------------------------------------
	static function init() {
		add_action('admin_menu', array( __CLASS__, 'admin_menu' ), 20 );
		add_action('wp_ajax_'.self::SLUG.'_status', [__CLASS__, 'status' ]);
	}

	//-------------------------------------------------------------------------------------------------------------------
	static function admin_menu() {
		// check Oxygen plugin state and user access
		if (!function_exists('oxygen_vsb_current_user_can_full_access') || !oxygen_vsb_current_user_can_full_access()) {return;}

		// Add submenu page to the Oxygen menu.
		add_submenu_page(	'ct_dashboard_page', 						// parent slug of "Oxygen"
							_x('Global Colors','admin page title','ma_oxygen_global_colors'), 	// page title
							_x('Global Colors','admin menu title','ma_oxygen_global_colors'), 	// menu title
							'manage_options',							// capabilitiy
							self::SLUG.'_page',							// menu slug
							[__CLASS__,'global_colors']					// method
		);
		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() admin subpage added.',self::TITLE,__FUNCTION__));}
	}
	//-------------------------------------------------------------------------------------------------------------------
	// Ajax handler to check 1) Oxygen Builder active, 2) Colors changed in database
	static function status() {
		$st = microtime(true);
		// get parameters
		$colset_id 		= isset($_REQUEST['colset-id'])		? $_REQUEST['colset-id']	: null; if (isset($colset_id)) {$colset_id = intval($colset_id);}
		$colors_hash 	= isset($_REQUEST['colors-hash'])	? $_REQUEST['colors-hash']	: null;
		// prepare data object to be returned
		$retval = (object)[
			'oxygen_builder_active' => false,
			'oxygen_colors_changed'	=> false,
		];
		// check if Oxygen Builder is active
		$retval->oxygen_builder_active = get_transient('oxygen_post_edit_lock');
		// if we currently have a color selected...
		if (isset($colset_id) && $colors_hash) {
			// ... check if this color has changed
			$colors = get_option('oxygen_vsb_global_colors');
			$new_hash = sha1(serialize($colors));
			$retval->oxygen_colors_changed = ($colors_hash !== $new_hash);
		}

		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() retval: %s',self::TITLE,__FUNCTION__,print_r($retval,true)));}
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',self::TITLE,__FUNCTION__,$et-$st));}

		// return JSON encoded data object
		echo json_encode($retval);
		wp_die();
	}
	//-------------------------------------------------------------------------------------------------------------------
	static function global_colors() {
		$st = microtime(true);

		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() request: %s',self::TITLE,__FUNCTION__,print_r($_REQUEST,true)));}
		
		// check Oxygen plugin state and user access
		if (!function_exists('oxygen_vsb_current_user_can_full_access') || !oxygen_vsb_current_user_can_full_access()) {return;}

		// check if action = save 
		if (isset($_REQUEST['action']) && ($_REQUEST['action'] == 'save-colset') && isset($_REQUEST['colset-id']) && isset($_REQUEST['colset-name'])) {
			// save
			$colset_id = @$_REQUEST['colset-id'] ?? null; if (isset($colset_id)) {$colset_id = intval($colset_id);}
			if (!isset($_REQUEST['save-colset-nonce']) || !wp_verify_nonce($_REQUEST['save-colset-nonce'], 'save-colset-'.$colset_id)) {
				echo '<div class="notice notice-error"><p>Invalid request (Nonce error)</p></div>';
			} else {
				// read current colors
				$colors = get_option('oxygen_vsb_global_colors');
				// cange color set name
				$colset_name = trim(stripslashes(@$_REQUEST['colset-name']));
				foreach ($colors['sets'] as &$colset) {
					if ($colset['id'] == $colset_id) {
						$colset['name'] = $colset_name; 
						break;
					}
				}
				// change color names
				foreach ($colors['colors'] as &$color) {
					$color_name = @$_REQUEST['color-name-'.$color['id']];
					if (isset($color_name)) {
						$color_name = trim(stripslashes($color_name));
						$color['name'] = $color_name;
					}
				}
				update_option('oxygen_vsb_global_colors',$colors);
				?>
				<script>
					jQuery(document).ready(function() {
						jQuery('#notice-colors-saved').show();
						setTimeout(function(){jQuery('#notice-colors-saved').fadeOut('slow');},5000);
					});
				</script>
				<?php
			}
		}

		// colors, current color set
		$colset_id = @$_REQUEST['colset-id'] ?? null; if (isset($colset_id)) {$colset_id = intval($colset_id);}
		$colset_name = '';
		$colors = get_option('oxygen_vsb_global_colors');
		$colors_hash = sha1(serialize($colors));
		$colset_tree = '<ul id="colset-tree">';
		if ($colors) {
			// loop through sets
			foreach ($colors['sets'] as $colset) {
				$colset_active = false;
				if ($colset_id === $colset['id']) {
					$colset_name = $colset['name'];
					$colset_active = true;
				}
				$colset_tree .=	sprintf('<li %s><a onclick="ma_oxygen_global_colors_switch_set(%d)">%s</a></li>', $colset_active ? 'class="active"':'', $colset['id'], $colset['name']);
			}
		}
		$colset_tree .= '</ul>';
		?>
		<script>
		var $builder_open 	= false;	// builder open flag
		var $data_valid 	= true; 	// data valid flag
		var $has_changed 	= false; 	// gets true if colors have been edited
		var $colors 		= JSON.parse('<?php echo addslashes(json_encode($colors)); ?>'); // colors for duplicate check
		var $colset_id 		= <?php echo $colset_id ?? 'null'; ?>; // current color set id
		//-------------------------------------------------------------------------------------------------------------------
		function ma_oxygen_global_colors_change($elm) {
			var $elm_name = $elm.attr('name');
			var $elm_id = $elm_name == 'colset-name' 
						? $elm.closest('form').find('input[name="colset-id"]').val() 
						: $elm_name.match(/\-(\d+)$/)[1];
			var $elm_value = $elm.val().trim();
			var $elm_orgval = $elm.data('orgval');
			var $message = null;
			// check changed
			if ($elm_value != $elm_orgval) {$has_changed = true;}
			// check empty
			if ($elm_value.trim() == '') {$message = 'Name can not be empty.'}
			// check for duplicates
			var $is_duplicate = false
			if ($elm_name == 'colset-name') {
				// check for duplicate color set name
				for (var $set of $colors.sets) {
					if ($set.id == $elm_id) 		{continue;}	// skip current color set
					if ($set.name == $elm_value) 	{$is_duplicate = true; break;}
				}
			} else {
				// check for duplicate color name in set
				for (var $col of $colors.colors) {
					if ($col.id == $elm_id) 		{continue;} // skip current color
					if ($col.set != $colset_id) 	{continue;} // skip if not current set
					if ($col.name == $elm_value)	{$is_duplicate = true; break;}
				}
			}
			if ($is_duplicate) {$message = 'Duplicate name.';}
			$elm.siblings('.validity-message').remove();
			// show message
			if ($message) {
				$data_valid = false;
				$elm.after('<span class="validity-message"><br>'+$message+'</span>');
				jQuery('#but-colors-save').attr('disabled','disabled');
			} else {
				$data_valid = true;	
				if ($data_valid && !$builder_open) {jQuery('#but-colors-save').removeAttr('disabled');}
			}
		}
		//-------------------------------------------------------------------------------------------------------------------
		function ma_oxygen_global_colors_switch_set($id) {
			var $switch = true;
			if ($has_changed) {
				$switch = confirm('<?php _ex('Unsaved changes. Switch color set?','unsaved changes notice','ma_oxygen_global_colors')?>');
			}
			if ($switch) {
				document.location.href='?page=<?php echo self::SLUG.'_page'; ?>&colset-id='+$id;
			}
		}
		//-------------------------------------------------------------------------------------------------------------------
		function ma_oxygen_global_colors_status() {
			var $ajax_data = {
				'action': '<?php echo self::SLUG.'_status'; ?>', // name of the AJAX function
				'colset-id': '<?php echo $colset_id; ?>',
				'colors-hash': '<?php echo $colors_hash; ?>',
			};
			jQuery.ajax({
				url: ajaxurl, // this will point to admin-ajax.php
				type: 'POST',
				data: $ajax_data, 
				success: function ($response) {
					var $response = JSON.parse($response);
					//console.log('ma_oxygen_global_colors_status',$response);
					$notice = [];
					if ($response.oxygen_builder_active) {
						jQuery('#notice-builder-open').show();
						$builder_open = true;
						jQuery('#but-colors-save').attr('disabled','disabled');
					} else {
						jQuery('#notice-builder-open').hide();
						$builder_open = false;
						if ($data_valid && !$builder_open) {jQuery('#but-colors-save').removeAttr('disabled');}
					}
					if ($response.oxygen_colors_changed) {
						jQuery('#notice-colors-changed').show();
					} else {
						jQuery('#notice-colors-changed').hide();
					}
				}
			});
			setTimeout(ma_oxygen_global_colors_status,<?php echo self::$status_interval; ?>);
		}
		(function($){
			$(document).ready(function(){
				// assign copy function
				$('span.color-copy-source').each(function(){
					$(this).on('click',function(){
						var $temp = $('<input>');
		   				$('body').append($temp);
						var $color = $(this).text();
						console.log('color',$color);
						$temp.val($color).select();
						document.execCommand("copy");
						$temp.remove();
						// inform in tooltip
						$(this).closest('.color-value').find('.color-copy-tooltip').html('Copied '+$color);
					});
					$(this).on('mouseout',function(){
						// reset tooltip text
						$(this).closest('.color-value').find('.color-copy-tooltip').html('Copy to clipboard');
					});
				});
			});

		})(jQuery);
		</script>
		<style>
		h1 {margin-bottom:30px;}
		#notices {margin-top: 30px;}
		#edit-screen {display:flex;flex-direction:row;flex-wrap:wrap;}
		.edit-screen__control {margin-bottom:30px;}
		.edit-screen__content {flex-grow:1;}
		.edit-screen__content #code {display:none;}
		.edit-screen__title {font-size:18px;margin-bottom:10px;}
		#colset-tree {margin:0 20px 0 0;width:300px;border:1px solid darkgray;border-radius:3px;padding:5px;}
		#colset-tree li {margin:0 0margin-bottom:5px;}
		#colset-tree li a {display:inline-block;width:calc(100% - 10px);padding:5px;border-radius:3px;background-color:lightgray;color:black;cursor:pointer;}
		#colset-tree li.active a {background-color:lightblue;color:black;}
		#notice-designset {margin-bottom:10px;border:1px solid lightgray;border-left:4px solid red;padding:1px 12px;background-color:white;color:red;}
		#colset-editor {width:600px;border:1px solid darkgray;border-radius:3px;padding:5px;}
		#colset-editor .color-row {display:flex;flex-direction:row;align-items:center;margin-bottom:3px;}
		#colset-editor label {display:inline-block;}
		#colset-editor label.colset {width:60px;}
		#colset-editor label.color {width:80px;}
		#colset-editor input.colset {width:300px;}
		#colset-editor input.color {width:280px;}
		#colset-editor .color-sample {display:inline-block;width:1.5em;height:1.5em;margin:0 5px;border:1px solid darkgray;border-radius:100%;}
		#colset-editor .color-value {font-family:monospace;}
		#colset-editor .validity-message {position:absolute;line-height:0;top:14px;right:5px;color:red;background-color:rgba(255,255,255,.75);}
		.color-value  {position:relative;display:inline-block;cursor:grab;}
		.color-copy-tooltip {visibility:hidden;width:140px;background-color:#555;color:#fff;text-align:center;border-radius:6px;padding:5px;position:absolute;z-index:1;bottom:150%;left:50%;margin-left:-75px;opacity:0;transition:opacity 0.3s;}
		.color-copy-tooltip::after {content:'';position:absolute;top:100%;left:50%;margin-left:-5px;border-width:5px;border-style:solid;border-color:#555 transparent transparent transparent;}
		.color-value:hover .color-copy-tooltip {visibility:visible;opacity:1;}
		</style>
		<div class="wrap">
			<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
			<div id="notices">
				<div id="notice-builder-open" class="notice notice-error" style="display:<?php echo get_transient('oxygen_post_edit_lock')?'block':'none'?>;">
					<p><?php _ex('Oxygen Builder is open in another browser tab. Changes are not allowed.','builder open warning','ma_oxygen_global_colors'); ?></p>
				</div>
				<div id="notice-colors-changed" class="notice notice-warning" style="display:none">
					<p><?php _ex('Colors have been changed in database. Please <a href="" onclick="location.reload()">reload</a>!','colors changed warning','ma_oxygen_global_colors'); ?></p>
				</div>
				<div id="notice-colors-saved" class="notice notice-success is-dismissible" style="display:none;">
					<p><?php _ex('Colors saved successfully','colors saved notice','ma_oxygen_global_colors'); ?></p>
				</div>
			<div id="edit-screen">
				<div class="edit-screen__control">
					<div class="edit-screen__title"><?php _ex('Global Color Sets','color sets headline','ma_oxygen_global_colors'); ?>:</div>
					<?php echo $colset_tree; ?>
				</div>
				<?php if (isset($colset_id)) : ?>
					<div class="edit-screen__content">
						<div class="edit-screen__title"><?php _ex('Current Color Set','current set headline','ma_oxygen_global_colors'); ?>: <?php echo $colset_name; ?></div>
						<div id="colset-editor">
							<div id="notice-designset" style="display:none;">
								<p><?php _ex('This color set seems to belong to a Design Set.<br>Please be aware that changes to the color set or color names could lead to duplicate color sets or colors.','design set warning','ma_oxygen_global_colors'); ?></p>
							</div>
							<form method="post">
								<input type="hidden" name="action" value="save-colset"/>
								<input type="hidden" name="colset-id" value="<?php echo $colset_id ; ?>"/>
								<?php wp_nonce_field('save-colset-'.$colset_id,'save-colset-nonce'); ?>
								<div class="color-row">
									<label class="colset" for="colset-name"><?php _ex('Color Set','label for color set name','ma_oxygen_global_colors'); ?></label>
										<div style="position:relative">
											<input class="colset" name="colset-name" type="text" value="<?php echo htmlspecialchars($colset_name) ; ?>" data-orgval="<?php echo htmlspecialchars($colset_name) ; ?>" onkeyup="ma_oxygen_global_colors_change(jQuery(this));" />
										</div>
								</div>
								<?php
								$is_designset = false;
								foreach ($colors['colors'] as $color) {
									if ($color['set'] === $colset_id) {
										if (array_key_exists('sourceVal',$color)) {$is_designset = true;}
										?>
										<div class="color-row">
										<label class="color" for="color-name-<?php echo $color['id']; ?>">ID <?php echo $color['id']; ?></label>
											<div style="position:relative">
												<input class="color" name="color-name-<?php echo $color['id']; ?>" type="text" value="<?php echo htmlspecialchars($color['name']); ?>" data-orgval="<?php echo htmlspecialchars($color['name']) ; ?>" onkeyup="ma_oxygen_global_colors_change(jQuery(this));"/>
											</div>
											<div class="color-sample" style="background-color:<?php echo $color['value']; ?>" title="<?php echo htmlentities($color['value']); ?>"></div>
											<div class="color-value">
												<span class="color-copy-tooltip">Copy to clipboard</span>
												<span class="color-copy-source"><?php echo htmlentities($color['value']); ?></span>
											</div>
										</div>
										<?php
									}
								}
								if ($is_designset) { ?>
									<script>
									//jQuery('input[name="colset-name"]').attr('disabled','disabled');
									jQuery('#notice-designset').show();
									</script>
								<?php }
								?>
								<p><button id="but-colors-save" type="submit" class="button button-primary" disabled >Save</button></p>
							</form>
						</div>
					</div>
				<?php endif; ?>
			</div>
		</div>
		<script>ma_oxygen_global_colors_status();</script>
		<?php
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',self::TITLE,__FUNCTION__,$et-$st));}
	}
}
// only initialize if Oxygen plugin is initialized
if (defined('CT_VERSION')) {
	MA_Oxygen_Global_Colors::init();
}


magnifier