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:
@brand-primary: #428bca;
Not colors:
@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
@font-size-base: 14px;
@font-size-large: ceil(@font-size-base * 1.25);
A reference to another variable. It could be a color:
@link-color: @brand-primary;
A color function:
@gray: lighten(#000, 33.5%);
@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.
@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.
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...
if(value.subtr(0,1)==="#") {
...into...
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:
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) === "@") {
nameValue = line.split(":");
name = nameValue[0].trim();
value = nameValue[1].trim();
value = value.split(";")[0];
if (storedViewData.hasOwnProperty(name)) {
value = storedViewData[name];
}
if (isColor(value)) {
if (value.length === 4) {
value += value.substr(1, 3);
}
viewModel.categories[category].variables.push(name);
viewModel.variables[name] = {
type: ko.observable("color"),
value: ko.observable(value)
}
continue;
}
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
;
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:
<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.
var viewModel = window.viewModel = {
categories: {},
variables: {},
gotoReference: function (data) {
alert(data.value());
}
},
And then bind it to the link.
<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...
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!
var viewModel = {
categories: {},
variables: {},
gotoReference: function (data) {
var $dialog,
variableModel = viewModel.variables[data.value()]
;
variableModel.gotoReference = viewModel.gotoReference;
$dialog = bootbox.dialog({
title: data.value(),
message: $("#" + variableModel.type).html(),
buttons: {
ok: {
label: "Ok"
}
}
});
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
.
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.
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:
- Create a function called
parseLine
and send in a single line. The name
variable will then be in that closure. - 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.
(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:
<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.