Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

BootBrander a bootstrap .less Generator UI (Part 3 / UI Refinement)

0.00/5 (No votes)
2 Mar 2015CPOL7 min read 13.9K   62  
Creates a MVC site with user inputs to change the bootstrap variables and generate a custom branded bootstrap.css

Introduction

This is already part 3 in this series. If you didn't read Part 1 and Part 2, you should do that first. 

We left our bootstrap color picker UI in a working state in the previous article. There were some things missing though. Not all the variables that represented a color were being exposed to the user.

This because when parsing the variables.less, we concluded that every variable with a value starting with a "#" would be a color. Which is correct, but that does not mean it's all the colors.

If you want, come along for the ride. The download of the second article is a good starting point.

Article Index

Other Variable Types

If we poke around in the variables.less, we can find a lot more types of variables. Some do not represent colors. Some are references to other variables and some contain color functions. Let's look at a few examples.

Just a color:

CSS
@brand-primary:         #428bca;

Not colors:

CSS
@font-family-sans-serif:  "Helvetica Neue", Helvetica, Arial, sans-serif;
@font-size-base:          14px;
@font-size-large:         ceil(@font-size-base * 1.25); // ~18px

A reference to another variable. It could be a color:

CSS
@link-color:            @brand-primary;

A color function:

CSS
@gray:                  lighten(#000, 33.5%); // #555
@link-hover-color:      darken(@link-color, 15%);

The last one is clearly the worst. It's both a function and a reference.

Let's ignore how to actually deal with the color function for now. But we need to create a function that can tell us if something would end up being a color.

If a variable is a reference to another variable, we'll have to walk up the tree to the end point. The original value that is a color. It could potentially be some steps away.

CSS
@top-level-color: #ff0000;
@second-color:    @top-level-color;
@third-color:     @second-color;

It could be that when we find the top level variable, it isn't a color at all. Then we should discard it.

The isColor Function

Let's create a function that will tell us if a variable is a color. To resolve our reference variables, this will be a recursing function. We are in luck, because we know that if we find a reference, the target variable should already be in our model. That's because the less compiler will not allow a reference to a variable that has not yet been declared.

JavaScript
/**
 *  Finds out if a value is color
 *  @function isColor
 *  @returns {boolean}
 */
function isColor(value) {

    if(value.substr(0,1)==="#") {
        return true;
    }
    else {
        if(value.substr(0,1)==="@") {
            if(viewModel.variables.hasOwnProperty(value)) {
                console.log(viewModel.variables[value]);
                return isColor(viewModel.variables[value].value());
            }
        }
    }
    return false;
}

So what this does is; First see if the actual value starts with a "#" in which case we're done. Otherwise, we'll need to see if the value is a reference. If so, we'll look it up in our variables object to see if it had been added to our viewModel previously and if that was a color.

Change the Parser

In the parser, we should change the line that goes... 

JavaScript
if(value.subtr(0,1)==="#") {

...into...

JavaScript
if(isColor(value) {

But this is not the only thing we'll do. For this example, we will stick to just the colors. But maybe we will at some point allow the user to change all the variables. So we'll also add everything that isn't a color to the viewModel

This won't hurt anything. And we can then easily add functionality later on. So in our parser loop, we will first state continue at the end of the isColor if and add the other variables below that with a type hidden.

The entire parseVariablesLess function should look something like this:

JavaScript
/**
 * Parses the variables.less file
 * @function parseVariablesLess
 */
function parseVariablesLess(responseText) {
    var lines = responseText.split('\n'),
        line,
        i,
        nameValue,
        name,
        value,
        category
    ;

    for (i = 0, j = lines.length; i < j; i++) {

        line = lines[i];

        if (line.substr(0, 4) === "//==") {
            category = line.substr(5, line.length).trim();

            viewModel.categories[category] = {
                variables: ko.observableArray()
            };
            continue;
        }

        if (line.substr(0, 1) === "@") {
            //this is a variable
            nameValue = line.split(":");
            name = nameValue[0].trim();
            value = nameValue[1].trim();
            //This line has change to! In the previous version we just replaced the ;
            //but there could be comments behind them
            value = value.split(";")[0];

            if (storedViewData.hasOwnProperty(name)) {
                value = storedViewData[name];
            }

            if (isColor(value)) {
                //this is color

                if (value.length === 4) {
                    value += value.substr(1, 3);
                }

                //add the name to the categories
                viewModel.categories[category].variables.push(name);

                //add the variable to the variables object
                viewModel.variables[name] = {
                    type: ko.observable("color"),
                    value: ko.observable(value)
                }
                continue;
            }

            //add the variable to the variables object
            //storing them for later potential use
            viewModel.variables[name] = {
                type: ko.observable("hidden"),
                value: ko.observable(value)
            }
        }
    }
}

Dealing with the Reference Colors

If we now start the interface, we will see: 

If the user clicks on the color wheel of @component-active-bg and changes that, it will break the link with the original color @brand-primary. Even though I feel that this should be possible, I would also like to have a way to 'jump' to the other color.

To do this, we need to introduce a new variable type "color-reference" and also create a new knockout template. A dailog and a function within our viewModel.

We'll start with detecting this new type. Let's change the if around isColor;

HTML
 if (isColor(value)) {
    //this is color

    if (value.length === 4) {
        value += value.substr(1, 3);
    }

    //add the name to the categories
    viewModel.categories[category].variables.push(name);

    if (value.substr(0, 1) === "@") {
        //add the variable to the variables object
        viewModel.variables[name] = {
            type: ko.observable("color-reference"),
            value: ko.observable(value)
        }
    }
    else {
        //add the variable to the variables object
        viewModel.variables[name] = {
            type: ko.observable("color"),
            value: ko.observable(value)
        }
    }

    continue;
}

Testing

Running the site, we see that not all the categories are showing up. And there is an error in the console. This is coming from knockout; Message: Cannot find template with ID color-reference

We'll need to provide it with a template to start. Just start by copying to color one and add some changes:

HTML
<script type="text/html" id="color-reference">
    <div class="row">
        <div class="col-xs-9" style="padding-right: 0;">
            <a class="form-control" data-bind="text: value" ></a>
        </div>
        <div class="col-xs-3" style="padding-left: 0;">
            <input type="color" 
            class="form-control" data-bind="value: value" />
        </div>
    </div>
</script>

I changed the input box into a link. If you'd hover the mouse over it. You will see that it gets decorated with underlining. If the user clicks this link, I'd like to show a dialog with the UI for the color this color is an inheritence of.

But first, we'll need a function to bind to the link. We will add this to the viewModel so that we can bind it with knockout.

JavaScript
var viewModel = window.viewModel = {
     categories: {},
     variables: {},
     gotoReference: function (data) {
         alert(data.value());
     }
 },

And then bind it to the link.

HTML
<a class="form-control" data-bind="text: value, 
click: $root.gotoReference"></a>

What does that actually say? It says go to the root of the viewModel and find the function gotoReference and bind it to the click eventhandler.

Testing

If we run this and find for instance the @link-color in scaffolding and click on the form control, it should popup an alert that says "@brand-primary".

Create the Reference Dialog

If the user clicks such a link, I want to start a dialog with the color template in it. If the reference color itself is a reference to yet another color, it should start another screen.

To create the dialogs I'm going for bootbox. You can find this through the NuGet package manager. After installing, I have the tendency to just add it to the bootstrap bundle in BundleConfig.cs.

Running...

JavaScript
bootbox.alert("Hello world!");

...from the console should give you a nice dialog.

To fill the dialog, we will use our previously made knockout templates. Then, we will create a new binding to the dialog. Let's just dive in!

JavaScript
var viewModel = {
    categories: {},
    variables: {},
    gotoReference: function (data) {
        var $dialog,
            //This will select the parent variable
            variableModel = viewModel.variables[data.value()]
        ;

        //Create a reference to the main viewModel
        variableModel.gotoReference = viewModel.gotoReference;

         $dialog = bootbox.dialog({
            title: data.value(),
            //By sending in the type, this would create a new reference modal if needed
            message: $("#" + variableModel.type).html(),
            buttons: {
                ok: {
                    label: "Ok"
                }
            }
        });

        //Apply binding with our variableModel, but just for the dialog!
        ko.applyBindings(variableModel, $dialog.get(0));

    }
},

Testing

  • Start the site
  • Find the Dropdowns section
  • Click on the value of @dropdown-link-active-bg
    • A dailog should open with then name @component-active-bg and the value @brand-primary
  • Click on the value @brand-primary
    • A dialog should open with the name @brand-primary and it should be an actual color
  • Change the color and close the dialogs
    • The change should be reflected through the site.

Fix the Color Spinners

As you might have seen, the color spinners of these reference variables are just black. We need to do a couple of things. First, we need to be able to find the real value of a parent color. We will do this through a function getColor.

JavaScript
/**
 *  Finds out if a value is color
 *  @function getColor
 *  @param {string} name The name of this variable
 *  @returns {string} The color
 */
function getColor(name) {
    var value = viewModel.variables[name].value();
    if (value.substr(0, 1) === "@") {
        return getColor(value);
    }
    else {
        return value;
    }
}

Next to this, we will need a ko.computed variable that returns this color and sets the value for the spinner. But first, we'll need to fix something. I will show the code we'll end up with around our color-reference observable.

JavaScript
viewModel.variables[name] = {
    type: ko.observable("color-reference"),
    value: ko.observable(value)
};

viewModel.variables[name].colorSpinnerValue=ko.computed(function () {
    return getColor(viewModel.variables[name].value());
});

We need to set the colorSpinnerValue after the actual creation of the variable itself. Because otherwise, it doesn't  exist yet when we try to get to it. But that's not the problem. This code will change all the reference fields in the last color there is. Initially, it will work. But we are doing this in a loop. So if the computed function ever gets recalled, the variable "name" doesn't hold the same value any more.

We can fix in 2 ways:

  1. Create a function called parseLine and send in a single line. The name variable will then be in that closure.
  2. Create a closure inside the loop. (This will flag jshint/jslint to complain about it.)

Even though it's not the right way, I'm choosing the second for brevity. 

JavaScript
(function (name) {

    viewModel.variables[name] = {
        type: ko.observable("color-reference"),
        value: ko.observable(value)
    };

    viewModel.variables[name].colorSpinnerValue = ko.computed(function () {
        return getColor(viewModel.variables[name].value());
    });

})(name);

Now, we still need to change the knockout binding:

HTML
<div class="col-xs-3" style="padding-left: 0;">
    <input type="color" class="form-control"
    data-bind="value: colorSpinnerValue" />
</div>

Reperforming the previous test should now reflect the changes through all the color wheels.

Concluding

I think it would need more functionality. The user should be able to 'break' a link with a referenced variable. You can already achieve this by chancing the type() of a variable, knockout will immediately flip the normal color dialog in place.

We still need to deal with darken and lighten functions.

I also feel the categories with no option in them should be suppressed. But I will leave this up to you.

The next step to me would be to give the user the ability to download the .css file reflecting his choices.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)