Blog

Code Snippet: Oxygen Stylesheets außerhalb des Builders bearbeiten

Version: 1.0.9 (31.05.2022)

Einführung

Oxygen ist ein mächtiges Plugin für WordPress, welches das WordPress Theme deaktiviert, die Erstellung eigener Templates mit einem visuellen Builder unterstützt, und damit ein ganz individuelles Design für die Website ermöglicht.

Innerhalb des Oxygen Builders kann man eigenes CSS hinterlegen, um spezielle Layouts und Formatierungen zu realisieren. Dies ist sehr praktisch und komfortabel.

Da jedoch das Öffnen des Builders je nach Plattform und Browser einige Zeit in Anspruch nehmen kann, ist schon alleine die Prüfung von CSS Code ein mitunter langwieriger Vorgang.

Lösung

Ich habe ein Code Snippet entwickelt, das die Bearbeitung der Oxygen Stylesheets auch außerhalb des Builders erlaubt. Dies ermöglicht einen sehr schnellen Zugriff auf das benutzerdefinierte CSS, ohne zuvor den Oxygen Builder starten zu müssen.

Im WordPress Admin Menü im Abschnitt Oxygen findet sich ein neuer Eintrag "Edit Stylesheets".

Zur Beachtung:
Mein Code Snippet verhindert das Speichern eines Stylesheets, wenn in einem anderen Browser Fenster (auch durch einen anderen Benutzer) ein Oxygen Builder geöffnet ist. Gleichzeitige Änderungen würden sich gegenseitig überschreiben.
Zu diesem Zweck prüft mein Code Snippet im Abstand weniger Sekunden, ob ein Oxygen Builder in einem anderen Tab oder von einem anderen Benutzer geöffnet wurde. In diesem Fall erfolgt eine Warnung und eine Speicherung wird verhindert.
Mein Code Snippet überprüft gleichzeitig, ob der Stand des gerade anzeigten Stylesheets noch dem Stand in der Datenbank entspricht. Sollten Abweichungen bestehen, weil beispielsweise in einem anderen Browser Fenster oder durch einen anderen Benutzer Änderungen vorgenommen wurden, wird ein erneutes Laden des Stylesheets empfohlen.

Download

Das Code Snippet steht hier zum Download zur Verfügung:

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

Zur Beachtung: Der Titel des Code Snippets hat sich geändert von früher "Oxygen: Edit Oxygen Stylesheets Outside Builder" zu jetzt "MA Oxygen Edit Stylesheets".

Zur Installation und Nutzung dieser JSON Datei wird das Plugin Code Snippets oder Advanced Scripts benötigt.
Dort kann diese JSON Datei mit der Funktion "Import" hochgeladen und anschließend aktiviert werden.

Falls noch mein vorheriges Snippet "Spalte 'Application' in Oxygen Template Liste" installiert ist, muss dieses bitte gelöscht werden! 

Alternativ: Am Ende dieser Seite kann der vollständige Source Code des Snippets eingesehen und kopiert werden.

Im Change Log sind neue Funktionalitäten und Fehlerbehebungen dokumentiert.

Spenden

Es macht mir viel Freude, Code Snippets zu entwickeln und damit Anforderungen zu lösen. Die Snippets stelle ich kostenfrei zur Verfügung.

Wenn Du möchtest, kannst Du meine vielen Stunden Arbeit mit einer kleinen Kaffee-Spende über PayPal honorieren.

  Bei Klick auf den Button wird eine Verbindung zu PayPal aufgebaut.

Spenden werden selbstverständlich ordnungsgemäß durch mich versteuert.

Disclaimer

Das Code Snippet habe ich nach bestem Wissen und Gewissen entwickelt und getestet unter:
- PHP 7.4, WordPress 6.0, Oxygen 3.9
- PHP 8.0, WordPress 6.0, Oxygen 4.0
Ich stelle das Code Snippet zur freien Verwendung zur Verfügung.
Eine Garantie für die Funktionalität in allen denkbaren WordPress Umgebungen kann ich nicht geben. 
Download und Nutzung dieses Code Snippets erfolgen auf eigene Gefahr und Verantwortung.

Change Log

Siehe 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