Blog

Code Snippet: Edit Oxygen Stylesheets Outside Builder

Version: 1.0.9 (May 31, 2022)

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 your own CSS to implement special layouts and formatting. This is very handy and comfortable.

However, since loading the Builder can take some time depending on the platform and browser, even just checking CSS code can be a tedious process.

Solution

I developed a code snippet that allows editing the Oxygen stylesheets outside of the Builder. This provides very quick access to the custom CSS without having to launch the Oxygen Builder first.

In the WordPress admin menu in the Oxygen section you will find a new entry "Edit Stylesheets".

For attention:
My code snippet prevents saving a stylesheet when an Oxygen Builder is open in another browser window (also by another user). Simultaneous changes would overwrite each other.
For this purpose my Code Snippet checks in intervals of a 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 state of the currently displayed stylesheet still corresponds to the state in the database. If there are deviations, for example because changes were made in another browser window or by another user, a reload of the stylesheet is recommended.

Download

The code snippet is available for download here:

ma-oxygen-edit-stylesheets.code-snippets.json
Version 1.0.9, 2022-05-31

Please note: The title of the code snippet has changed from "Oxygen: Edit Oxygen Stylesheets Outside Builder" to "MA Oxygen Edit Stylesheets".

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.

If my previous snippet "Column 'Application' in Oxygen template list" is still installed, please delete it!

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.

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.

Disclaimer

I have developed and tested the code snippet to the best of my knowledge under
- PHP 7.4, WordPress 6.0, Oxygen 3.9
- PHP 8.0, WordPress 6.0, Oxygen 4.0
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.

Change Log

See source code.

Source Code

<?php
/*
Plugin Name:  MA Oxygen Edit Stylesheets
Description:  Direct access to Oxygen Stylesheet Editor
Author:       <a href="https://www.altmann.de/">Matthias Altmann</a>
Version:      1.0.9
Plugin URI:   https://www.altmann.de/en/blog-en/code-snippet-edit-oxygen-stylesheets-outside-builder/
Description:  en: https://www.altmann.de/en/blog-en/code-snippet-edit-oxygen-stylesheets-outside-builder/
              de: https://www.altmann.de/blog/code-snippet-oxygen-stylesheets-ausserhalb-des-builders-bearbeiten/
Copyright:    © 2020-2022, Matthias Altmann

Version History:
Date		Version		Description
---------------------------------------------------------------------------------------------------------------------
2022-05-31	1.0.9		Fix: 
						- Corrected folder/stylesheet hierarchy
						Changes:
						- Renamed snippet from "Oxygen: Edit Oxygen Stylesheets Outside Builder" to "MA Oxygen Edit Stylesheets" 
						- Removed configuration and all code for debugging.
						Tested: 
						- PHP 7.4, WordPress 6.0, Oxygen 3.9
						- PHP 8.0, WordPress 6.0, Oxygen 4.0
2022-02-11				Tested:
						- PHP 8.0, WordPress 5.9, Oxygen 4.0 beta 1
						Changes:
						- Added hint to select a stylesheet to edit
						- Added notice if no global stylesheets defined
2021-07-26	1.0.8		Bug fix: Restored compatibility with WP 5.8 (wp_add_inline_script() did not work anymore as expected)
						(Thanks to André Babiak for reporting!)
2021-04-13	1.0.7		Bug fix: Postponed Oxygen check to init action to also support plugin mode.
2021-03-21	1.0.6		Bug fix: Check Oxygen plugin state before adding admin menu or accessing global colors
						(Thanks to Adrien Robert for reporting!)
2021-02-20	1.0.5		Tweak: Full height editor (Safari, finally?), scrollable stylesheets list
2021-01-08	1.0.4		Tweak: Full height editor 
2021-01-07	1.0.3		Bug fix: Corrected changed flag logic 
2021-01-06	1.0.2		Bug fix: Correct handling of quotes that get backslashed during transfer
						Tweak: Implemented confirmation dialog for switching stylesheets when current one has been changed
2020-12-30	1.0.1		Bug fix: Fresh Oxygen site threw error since no stylesheets defined
2020-12-19	1.0.0		Initial Release 
2020-12-17				Development start
*/

class MA_Oxygen_Edit_Stylesheets {

	const TITLE     	= 'MA Oxygen Edit Stylesheets';
	const SLUG	    	= 'ma_oxygen_edit_stylesheets';
	const VERSION   	= '1.0.9';
	
	// Configuration
	private static $timing				= false; // caution! may produce a lot of output
	private static $status_interval 	= 5000; // interval (ms) to refresh  "builder open" and "stylesheet changed" status
	
	// Internal data
	private static $oxygen_universal_message = ''; // temp storage for succes/error message from oxygen

	//-------------------------------------------------------------------------------------------------------------------
	static function init() {
		$st = microtime(true);
		add_action('admin_menu', array( __CLASS__, 'admin_menu' ), 20 );
		add_action('wp_ajax_'.self::SLUG.'_status', [__CLASS__, 'status' ]);
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.', self::TITLE, __FUNCTION__, $et-$st));}
	}

	//-------------------------------------------------------------------------------------------------------------------
	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('Edit Stylesheets','admin page title','ma_oxygen_edit_stylesheets'), 	// page title
							_x('Edit Stylesheets','admin menu title','ma_oxygen_edit_stylesheets'), 	// menu title
							'manage_options',							// capabilitiy
							self::SLUG.'_page',							// menu slug
							[__CLASS__,'edit_stylesheets']				// method
		);
	}
	//-------------------------------------------------------------------------------------------------------------------
	// Ajax handler to check 1) Oxygen Builder active, 2) Stylesheets changed in database
	static function status() {
		$st = microtime(true);
		// get parameters
		$ssid = isset($_REQUEST['ssid']) ? $_REQUEST['ssid'] : null;
		$hash = isset($_REQUEST['hash']) ? $_REQUEST['hash'] : null;
		// prepare data object to be returned
		$retval = (object)[
			'ct_builder_active' 	=> false,
			'ct_stylesheet_changed'	=> false,
		];
		// check if Oxygen Builder is active
		$retval->ct_builder_active = get_transient('oxygen_post_edit_lock');
		// if we currently have a stylesheet selected...
		if ($ssid && $hash) {
			// ... check if this stylesheet has changed
			$stylesheets = get_option('ct_style_sheets');
			$new_hash = null;
			// loop through stylesheets to find current one
			foreach ($stylesheets as $stylesheet) {
				if ($stylesheet['id']==$ssid) {
					$sscode = base64_decode($stylesheet['css']);
					$new_hash = sha1($sscode);
					break;
				}
			}
			$retval->ct_stylesheet_changed = ($hash !== $new_hash);
		}

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

		// return JSON encoded data object
		echo json_encode($retval);
		wp_die();
	}
	//-------------------------------------------------------------------------------------------------------------------
	static function edit_stylesheets() {
		$st = microtime(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-css') && isset($_REQUEST['ssid']) && isset($_REQUEST['code'])) {
			// save
			$ssid = $_REQUEST['ssid'];
			$code = $_REQUEST['code'];
			if (!isset($_REQUEST['save-css-nonce']) || !wp_verify_nonce($_REQUEST['save-css-nonce'], 'save-css-'.$ssid)) {
				echo '<div class="notice notice-error"><p>Invalid request (Nonce error)</p></div>';
			} else {
				// read current stylesheets
				$stylesheets = get_option('ct_style_sheets');
				if (!is_array($stylesheets)) $stylesheets = [];
				foreach ($stylesheets as &$stylesheet) {
					if ($stylesheet['id']==$ssid) {
						// remove backslashes from quotes that might be added during transfer 
						$code = stripslashes($code);
						// override css code
						$stylesheet['css'] = base64_encode($code);
						update_option('ct_style_sheets',$stylesheets);
						// regenerate universal cache and show Oxygen's result message
						$result = oxygen_vsb_cache_universal_css();
						if ($result) 	{self::$oxygen_universal_message = __('Universal CSS cache generated successfully.','oxygen');}
						else 			{self::$oxygen_universal_message = __('Universal CSS cache not generated.','oxygen');}
						break;
					}
				}
				add_action('admin_notices',function(){
					echo '<div id="notice-saved" class="notice notice-success is-dismissible"><p>Saved successfully. Oxygen '.self::$oxygen_universal_message.'</p></div>'.
							'<script>setTimeout(function(){jQuery("#notice-saved").fadeOut("slow");},5000);</script>';
				});
			}
		}

		

		// stylesheet selection, current stylesheet code
		$ssid = @$_REQUEST['ssid'] ?? null;
		$ssdata = null;
		$sscode = '';
		$sshash = '';

		$stylesheets = get_option('ct_style_sheets');
		// @since 1.0.9
		if (!is_array($stylesheets)) $stylesheets = [];
		// build stylesheet hierarchy (may be out of order)
		$hierarchy = [
				'0' => ['id'=>0, 'parent'=>null, 'folder'=>1, 'name'=>'Uncategorized'],
		];
		// 1) find folders
		foreach ($stylesheets as $stylesheet) {
			if (array_key_exists('folder',$stylesheet)) {
				$hierarchy[$stylesheet['id']] = $stylesheet;
				continue;
			}
		}
		// 2) find stylesheets
		foreach ($stylesheets as $stylesheet) {
			if (array_key_exists('parent',$stylesheet)) {
				$parent = $stylesheet['parent'];
				if (array_key_exists($parent,$hierarchy)) {
					if (!array_key_exists('stylesheets',$hierarchy[$parent])) $hierarchy[$parent]['stylesheets'] = [];
				}
				$hierarchy[$parent]['stylesheets'][] = $stylesheet;
				continue;
			}
		}


		if (!count($hierarchy)) {
			// TODO: emit message?			
		}

		// list enabled folders and stylesheets
		$tree = '<div class="ss-list">';
		foreach (['Enabled','Disabled'] as $endis) {
			$tree .= sprintf('<h3>%s</h3><ul class="ss-tree">', $endis);
			foreach ($hierarchy as $folder) {

				if (($endis == 'Enabled') 	&& (array_key_exists('status',$folder) && $folder['status']==0)) continue;
				if (($endis == 'Disabled') 	&& (!array_key_exists('status',$folder) || $folder['status']!=0)) continue;
				
				$tree .= sprintf('<li><span>%s</span><ul>', $folder['name']);
				if (array_key_exists('stylesheets',$folder)) {
					foreach ($folder['stylesheets'] as $stylesheet) {
						$tree .= sprintf('<li %s><a onclick="ma_oxygen_edit_stylesheets_switch(%d)">%s</a></li>',$stylesheet['id']==$ssid?'class="active"':'', $stylesheet['id'], $stylesheet['name']);
						if ($stylesheet['id']==$ssid) {
							$ssdata = $stylesheet;
							$sscode = base64_decode($stylesheet['css']);
							$sshash = sha1($sscode);
						}
					}
				}
				$tree .= '</ul></li>';
			}
			$tree .= '</ul>';
		}
		$tree .= '</div>';
		?>
		<script>
		var $can_change = false; //
		var $has_changed = false; // gets true if css code has been edited
		function sscode_changed() {
			$has_changed = true;
			//if ($can_change) {jQuery('#but-css-save').removeAttr('disabled');}
		}
		function ma_oxygen_edit_stylesheets_switch($id) {
			if (!$has_changed || confirm('Stylesheet has been changed. Switch to another stylesheet anyways?')) {
				jQuery('.edit-screen__content').first().html('<div class="edit-screen__title">Loading...</div>');
				setTimeout(function(){document.location.href='?page=<?php echo self::SLUG.'_page'; ?>&ssid='+$id;}, 50);
			}
		}

		function ma_oxygen_edit_stylesheets_status() {
			var $ajax_data = {
				'action': '<?php echo self::SLUG.'_status'; ?>', // name of the AJAX function
				'ssid': '<?php echo $ssid; ?>',
				'hash': '<?php echo $sshash; ?>',
			};
			jQuery.ajax({
				url: ajaxurl, // this will point to admin-ajax.php
				type: 'POST',
				data: $ajax_data, 
				success: function ($response) {
					$response = JSON.parse($response);
					//console.log('ma_oxygen_edit_stylesheets_status',$response);
					$notice = [];
					if ($response.ct_builder_active) {
						jQuery('#notice-builder-open').show();
						$can_change = false;
						jQuery('#but-css-save').attr('disabled','disabled');
					} else {
						jQuery('#notice-builder-open').hide();
						$can_change = true;
						jQuery('#but-css-save').removeAttr('disabled');
					}
					if ($response.ct_stylesheet_changed) {
						jQuery('#notice-stylesheet-changed').show();
					} else {
						jQuery('#notice-stylesheet-changed').hide();
					}
				}
			});
			setTimeout(ma_oxygen_edit_stylesheets_status,<?php echo self::$status_interval; ?>);
		}
		</script>
		<style id="ma_edit_stylesheet">
		#wpbody-content * {box-sizing:border-box;}
		#wpbody-content {position:relative;height:calc(100vh - 32px - 40px);max-width:100%;padding-bottom:0;margin-right:20px;}
		#wpbody-content > .wrap {width:100%;height:100%;display:flex;flex-direction:column;margin:0;}
		#edit-screen {margin-top:2em;flex-grow:1;display:flex;flex-direction:row;max-height:100%;}
		@media (max-width:900px) {#edit-screen {display:flex;flex-direction:column;}}
		/* left column */
		.edit-screen__control {display:flex;flex-direction:column;}
		.ss-list {margin:0 20px 0 0;width:300px;flex-grow:1;overflow-y:auto;}
		.ss-tree {}
		.ss-tree li {margin:0 0margin-bottom:5px;}
		.ss-tree li span {display:block;padding:5px;font-weight:bold;background-color:darkgray;border-radius:3px;margin-bottom:5px;}
		.ss-tree ul {margin-left:20px;}
		.ss-tree ul li {}
		.ss-tree ul li a {display:inline-block;width:calc(100% - 10px);padding:5px;border-radius:3px;background-color:lightgray;color:black;cursor:pointer;}
		.ss-tree ul li.active a {background-color:lightblue;color:black;}
		/* right column */
		.edit-screen__content {flex-grow:1;display:flex;flex-direction:column;}
		.edit-screen__content #code {display:none;}
		.edit-screen__title {font-size:18px;margin-bottom:10px;}

		.ma_oxygen_edit_stylesheets_code {border: 1px solid lightgray;flex-grow:1;margin-bottom:20px;margin-right:20px;display:flex;flex-direction:column;}
		.ma_oxygen_edit_stylesheets_code form {flex-grow:1;display:flex;flex-direction:column;}
		.ma_oxygen_edit_stylesheets_code .CodeMirror {flex-grow:1;display:flex;flex-direction:column;}
		.ma_oxygen_edit_stylesheets_code .CodeMirror-scroll {flex-grow:1;}
		
		</style>
		<div class="wrap">
			<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
			<?php do_action( 'admin_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. CSS changes not allowed.','builder open warning','ma_oxygen_edit_stylesheets'); ?></p>
			</div>
			<div id="notice-stylesheet-changed" class="notice notice-warning" style="display:none">
				<p><?php _ex('This stylesheet has been changed in database. Please <a href="" onclick="location.reload()">reload</a>!','stylesheet changed warning','ma_oxygen_edit_stylesheets'); ?></p>
			</div>
			
			<div id="edit-screen">
				<div class="edit-screen__control">
					<div class="edit-screen__title">Stylesheets:</div>
					<?php if (!is_array($stylesheets) || !count($stylesheets)) : ?>
						<p>No global stylesheets defined in Oxygen.</p>
					<?php else :?>
						<?php echo $tree; ?>
					<?php endif; ?>
				</div>
				<div class="edit-screen__content" >
					<?php if (!$ssid) : ?>
						<?php if (is_array($stylesheets) && count($stylesheets)) : ?>
							<p style="margin-top:4em;text-align:center;">◀︎ Please select a stylesheet.</p>
						<?php else :?>
							<p>No stylesheets defined in Oxygen.</p>
						<?php endif; ?>
					<?php else : ?>
						<div class="edit-screen__title">Current Stylesheet: <?php echo (isset($ssdata['name']) ? $ssdata['name'] : ''); ?></div>
						<div class="ma_oxygen_edit_stylesheets_code">
							<form method="post">
								<input type="hidden" name="action" value="save-css"/>
								<input type="hidden" name="id" value="<?php echo $ssid ; ?>"/>
								<?php wp_nonce_field('save-css-'.$ssid,'save-css-nonce'); ?>
								<textarea id="code" name="code"><?php echo $sscode; ?></textarea>
								<p style="text-align:center;"><button id="but-css-save" type="submit" class="button button-primary" disabled >Save</button></p>
							</form>
						</div>
					<?php endif; ?>
				</div>
			</div>
		</div>
		<script>ma_oxygen_edit_stylesheets_status();</script>
		<?php $editor = wp_enqueue_code_editor(['type'=>'text/css']); ?>
		<script>
		jQuery( function($) {
			if ( $('#code').length ) {
				let editor = wp.codeEditor.initialize('code', <?php wp_json_encode( $editor ) ?>);
				editor.codemirror.on('change',function(codemirror) {sscode_changed();});
			}
		} );
		</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
add_action('init',function(){
	if (defined('CT_VERSION')) {
		MA_Oxygen_Edit_Stylesheets::init();
	}
});


magnifier