LESS > CSS/PHP

Eliminating legacy code and modernizing large CSS codebases.

It’s the small things.

At SmugMug our technology stack consists mainly of PHP for our backend, and YUI and React in our front-end code. Over the years we’ve seen technologies come and go, and some of that is still visible in the codebase. For example, there are small areas of the code that have some dependency on YUI 2, while others are written using modern React code bundled using ES6 syntax. When dealing with larger, older codebases like ours, you accumulate certain amounts of technical debt. This is not unique to us but simply the nature of the beast.

When technical debt like this is isolated, it can be reduced, refactored, and eventually replaced or removed. If they’re not isolated, this technical debt starts to become tricky to deal with.

Until recently we had one bit of technical debt that was the complete opposite of isolated: it was everywhere! Our CSS.

There’s something terribly wrong in that code. What are those PHP tags doing there?!

Customization and SmugMug.

Before we dig in, here’s a bit of background as to why we had CSS with PHP tags. SmugMug allows our customers to personalize their sites to their hearts’ content. We provide them with prebuilt interactive Content Blocks to showcase their beautiful photos for others to see and purchase. They also have the ability to choose their themes, which include fonts, color schemes, opacities, and more.

Themes have 5 main properties: primary color, accent color, text color, header font and body font. Since the majority of the PHP usage is around Colors, let’s focus on those.

Theme Editor Window
Our Theme Editor showing default theme settings.

Theme colors are used to generate a color palette of 32 colors for each color type. This spectrum of colors lets us to work with all kinds of color combinations, allowing us to create customized pages and interactive Content Blocks that match the selected theme.

color2bpalette2bflipped

As developers-with guidance from our designers-we select one of the 32 colors to use in our CSS for the different parts of our site, like our Lightbox, Galleries, and Content Blocks. The algorithmic nature of the color palette guarantees that, regardless of the selected theme, our customers’ sites will look their best. This level of customization has been a key feature of SmugMug since early 2004.

screenshot2b-2bblog2bpost

Screenshot of old cobranding examples or interfaces (from archive.org).

How it’s done.

In order to generate CSS code with dynamic color values you need some sort of variable mechanism to substitute values at run time. So naturally, this being a PHP codebase, we added PHP to our CSS! This allowed our CSS to have variables with values that could change on the fly.

Why didn’t we use SCSS, SASS, LESS, or any of the countless other tools available to developers?

Simple. We didn’t have a choice! Back in 2004, when the first signs of this feature were appearing, the set of CSS tools was much more limited. In fact, none of the preprocessors in use today existed. SASS didn’t arrive until two years later. As you can imagine, having PHP in our CSS has caused more than a couple of headaches in the past, sometimes holding us back.

For example, in order to enable CSS minification, we had a workaround where we commented out PHP tags with multiline comments, minified the CSS and undid the comment process to bring PHP tags back. The fact that this worked is half a miracle. This was not a sustainable way forward.

We decided to investigate the tools available now that would be up to the task, but first we needed to understand how our themes were defined and built.

Finding a solution

Of the two most popular tools (SASS and LESS) that could serve our needs, we decided to go with LESS. Both tools could work equally well, but LESS had one minor advantage during the experimentation phase that sealed it as our choice: the inline import.

For those unfamiliar with LESS, the inline import directive allows you to instruct the compiler to treat a certain file as valid CSS regardless of its contents. The contents of this file are placed verbatim into the resulting final CSS produced by the LESS compiler.

This import directive would allow us to do something like

While this in-between state is less than ideal, it would allow us to transition the codebase in small chunks. Whereas modules that are converted to LESS can no longer contain PHP. As people worked on older CSS, we could encourage developers to move the code over to LESS.

With this tool at hand, a proof of concept was built and presented at our quarterly hackathon. This inline import directive allowed us to skip most of the hard tedious work and focus on the initial challenge: themes and theme colors.

PHP + LESS to the rescue.

For any non-theme CSS code, we can use the standard LESS compiler as part of our regular build process. To help manage some repeated code like media breakpoints and some internal skin code, we created a global variables.less. This file is imported by all bundles and provides a universal library of useful variables and mixins. For those that did depend on themes, we engineered a solution for compiling LESS code within PHP. A LESS-PHP bridge.

LESS-PHP bridge

Leveraging a PHP library capable of compiling LESS code, we created a mechanism for generating color variables on the fly. Using a simple template LESS file, we can build a dynamic set of variables for each color and its unique value.

This file can then be appended to the end of any theme-able bundles. Due to CSS/LESS precedence rules, the last declaration of a value takes precedence allowing a global override of any variables; in this case, skin-color values.

With the bridge complete and the ability to inline import CSS files using PHP without causing issues, we had a way forward. At the end of the hackathon, we were able to serve some pages with a mix of LESS and CSS/PHP.

Taking the plunge.

The demo of a working proof of concept sparked a bit of conversation among the engineering team, and a deeper look was required. Upon closer inspection, one of our engineering leads noticed a pattern in all our PHP in use. This pattern allowed us to easily script a full transition to LESS without the intermediate inline step. In four days we’d developed a quick Python script that could convert about 90% of all our usage of PHP in CSS. The other 10% was either dead legacy code that could be purged (some IE6 workarounds) or easy enough to change by hand.

In the end it took about six days of scripting, manual fixes and developer validation to get all our CSS/PHP in LESS files. Not only did the scripts eliminate a large amount of tedious work, it also made it possible to eliminate a large set of possible errors. By leveraging automation we could guarantee that if the transition script worked correctly in one case, it would always work the same way elsewhere. All these benefits, with nothing more than a set of simple regular expressions.

Still, in an abundance of caution, we decided to do a full regression test of the site and functionality related to themes. This process revealed some smaller issues that were missed by the manual diff inspections, but overall no major issues surfaced during this test. For a change consisting of over 25k lines of CSS, this was a nice surprise.

Lessons learned.

For the longest time this had been a part of the code that engineers thought would not be worth the huge effort needed to refactor and update. But in a short amount of time it went from being a small annoyance to a junior engineer to a successful refactoring effort that improved and modernized our codebase. While there were plenty of bumps in the road, the amount of effort required to do this transition went from months to days, all thanks to some clever thinking and scripting. Just goes to show large projects can sometimes be simplified with the tools we use every day.

Looking to the future.

We’re very happy with LESS and the benefits it has brought us, and we’re looking to take advantage of its features in the future, like plug-ins for browser pre-fixing. There’s also room for performance improvements in our themes to provide faster live previews.

Did any of that post get you thinking about a better or different way we could have done this? If any of it sounded interesting to you, please drop us a line at jobs@smugmug.com. We’re always looking for talented engineers interested in solving problems like these.

Using HTML5’s Fullscreen API for Fun and Profit

For the past few weeks I’ve been working on a new super magical awesome feature that involves using the new HTML5 Fullscreen API. As with most brand spankin’ new web APIs, its support and implementation varies per browser. I think it’s worth the effort considering how freaking awesome it is to do fullscreen web apps.

The Basics

OK, let’s get started with the basics of how this new API works. Via the JavaScript function requestFullscreen you tell the browser you want a specific HTML DOM element to fill the entire screen with no browser chrome displayed.

var myNode = document.querySelector("#myFullscreenNode");
myNode.requestFullscreen();

This is not the normal fullscreen mode that many browsers have where the browser’s viewport is simply stretched to the edges of the edges of the screen and the browser chrome is hidden. As far as I know that type of fullscreen mode is not standardized and is not accessible via JavaScript.

Currently Firefox, Safari and Chrome support the fullscreen API. But of course each implements it slightly differently, which is exactly why I’m writing this article and you’re reading it.

Getting started

According to the W3C specification, the first thing you should do is determine if the browser supports the fullscreen API and is currently in a state where it’s safe to go fullscreen. This is achieved via the `fullscreenEnabled` property on the document object. If the property exists and is true this means you can request the browser’s fullscreen mode. (Note the terminology: request. There’s no guarantee it will always work so don’t expect it to.)

You want to use this flag (if available) because a browser may support the fullscreen API but be in a state where it can’t go fullscreen (still loading content, a browser preference pane may be focused, etc).

To determine if fullscreen mode is available, check the .fullscreenEnabled property on the document object like this:

if(document.fullscreenenabled) {
	var myNode = document.querySelector("#myFullscreenNode");
	myNode.requestFullscreen();
} else {
	dont();
}

Currently only Firefox has this property on the document object as ‘mozFullScreenEnabled’ (note the capitalization), so it’s not worth relying on unless you really want to adhere to a draft spec.

The easier way to check if a browser supports the fullscreen API is to create a test HTML Node object and check if it has the requestFullscreen function on it:

var testNode = document.createElement('div');

if(testNode.requestFullscreen) {
	var myNode = document.querySelector("#myFullscreenNode");
	myNode.requestFullscreen();
} else {
	//Fail
}

The above snippet is the spec format, for use in Firefox/Chrome use .mozRequestFullScreen and .webkitRequestFullScreen (again note capitalization!).

Are we there yet?

Let’s assume we have a browser that supports fullscreen mode. We can just call requestFullscreen() on the DOM Node we specify and we’re golden, right? Wrong! Just because we call the function doesn’t mean we’re guaranteed to go fullscreen. The user could press the Escape key during the transition to fullscreen or something could occur in the browser itself where it needs to abort. This is where listening for the events ‘fullscreenchange’ and ‘fullscreenerror’ is helpful (both are available prefixed in Firefox and Chrome, fullscreenerror is not available in Safari).

These events are fired on the document object, not on the node that was requested to go fullscreen. Adding to our code snippet above we get this:

var testNode = document.createElement('div');

if(testNode.requestFullscreen) {
	document.onfullscreenchange = function(event) {
		//Fullscreen mode has changed
	}

	document.onfullscreenerror = function(event) {
		//Error!
	}

	var myNode = document.querySelector("#myFullscreenNode");
	myNode.requestFullscreen();
}

Again, the above code is per the spec, for Firefox and Chrome/Safari use ‘onmozfullscreenchange’ and ‘onwebkitfullscreenchange’, respectively.

Given there’s a fullscreen change event object you’d assume that it will tell you which mode the browser is currently in, right? Wrong! You can’t tell which mode the browser is in from the event fired. Luckily there is a document property to determine which mode the browser is in. (Are we having fun yet?!)

To determine which mode the browser is in check the ‘fullscreenElement’ property of the document object. If this property is not null the browser is in fullscreen mode (and the value is the DOM node that is fullscreen). Firefox, Chrome and Safari all support this property (namespaced).

if(document.fullscreenElement) {
    //Yay, we're fullscreen!
}

Checking for errors

Even if we’ve checked the ‘fullscreenEnabled’ and ‘fullscreenElement’ properties betore we request fullscreenmode, it’s still possible that the browser will deny our request. When this happens the browser will fire a ‘fullscreenerror’ event on the document object.

This can happen if there’s a user preference, security risk or platform limitation regarding fullscreen mode. Fullscreen mode is also only triggerable via user input (click, key press, etc), so if it is requested outside of those events the fullscreenerror event will be fired.

var testNode = document.createElement('div');
if(testNode.requestFullscreen) {
	document.onfullscreenerror = function(event) {
		//Error!
	}
}

Firefox and Chrome support the onfullscreenerror events (prefixed), Safari does not.

All together now

Combining all our code examples above we get the following:

if(document.fullscreenEnabled) {
	document.onfullscreenchange = function(event) {
		if(document.fullscreenElement) {
			//We are fullscreen! Rejoice!
		} else {
			//We're not fullscreen 😦
		}
	}

	document.onfullscreenerror = function(event) {
		//Something went wrong...
	}

	var fullscreenNode = document.querySelector("#myFullscreenNode");
	fullscreenNode.requestFullscreen();
}

Unfortunately none of this code will work in any current browsers. A lot of conditional logic is needed to determine which set of fullscreen APIs are available (Firefox, Chrome and Safari all differ).

Fortunately for you, I’ve wrapped it all up into a convenience function that will return the correct set of events and fullscreen function for the browser or false if the current browser does not have fullscreen support:

function FullScreenSupport() {
    var TEST_NODE = document.createElement('div');
        REQUEST_FULLSCREEN_FUNCS = {
            'requestFullscreen': {'change':'onfullscreenchange',
                                  'request':'requestFullscreen',
                                  'error':'onfullscreenerror',
                                  'enabled':'fullscreenEnabled',
                                  'cancel': 'exitFullscreen',
                                  'fullScreenElement':'fullscreenElement'
            },
            'mozRequestFullScreen':{'change':'onmozfullscreenchange',
                                    'request':'mozRequestFullScreen',
                                    'error':'onmozfullscreenerror',
                                    'cancel': 'mozCancelFullScreen',
                                    'enabled':'mozFullScreenEnabled',
                                    'fullScreenElement':'mozFullScreenElement'
            },
            'webkitRequestFullScreen':{'change': 'onwebkitfullscreenchange',
                                       'request': 'webkitRequestFullScreen',
                                       'cancel': 'webkitCancelFullScreen',
                                       'error': 'onwebkitfullscreenerror',
                                       'fullScreenElement': 'webkitCurrentFullScreenElement'
            }
        },

        fullscreen = false;

        for(var prop in REQUEST_FULLSCREEN_FUNCS) {
            if(REQUEST_FULLSCREEN_FUNCS.hasOwnProperty(prop)) {
                if(prop in TEST_NODE) {
                    fullscreen = REQUEST_FULLSCREEN_FUNCS[prop];
                    //Still need to verify all properties are there as
                    //Chrome and Safari have different versions of Webkit
                    for(var item in fullscreen) {
                        if(!(fullscreen[item] in document) &&
                            !(fullscreen[item] in TEST_NODE)) {
                            delete fullscreen[item];
                        }
                    }
                }
            }

            if(fullscreen) {
                break;
            }
        }

        return fullscreen;
}

It ain’t pretty, but it does work. The function will return false if the browser doesn’t support fullscreen mode. Also note that just because a browser supports full screen mode doesn’t mean every function and property related to full screen mode is available, make sure it’s in the object FullScreenSupport returns.

Styling it all

Thought you were done? 🙂

Along with the long list of JavaScript functions for fullscreen mode, there’s a little bit of CSS styling that is applied to the element that is shown fullscreen. According to the spec, an element that is fullscreen gets this CSS:

  position:fixed;
  top:0; right:0; bottom:0; left:0;
  margin:0;
  box-sizing:border-box;
  width:100%;
  height:100%;
  object-fit:contain;

Firefox applies this by default along with background-color: black, Safari and Chrome do not apply the width and height properties. To make Chrome and Safari match the spec and Firefox’s behavior, you can use the :fullscreen pseudo class to apply these styles:

#myFullscreenNode:-webkit-full-screen { //webkit prefix
  width:100%;
  height:100%;
  background-color: black;
}

Combine that with the FullScreenSupport function and you’ll have a relatively easy to use fullscreen API in three browsers! Also, if you happen to know anyone on the IE team please let them know they should implement it!

References