-
Notifications
You must be signed in to change notification settings - Fork 144
Icons
Hypergrid's rendering engine calls functions known as cell renderers to do the actual rendering of each cell's content. Cell renderers render data by calling any combination of 2D canvas graphics context methods. They can therefore render image data into a cell by calling the 2D graphics context's drawImage()
.
The default cell renderer, the included SimpleCell
cell renderer, optionally renders icons to the left or right of the data; or in place of the data. The remainder of this document discusses SimpleCell
's icon rendering capabilities. If these capabilities do not suit your needs, you also have the additional option of writing your own cell renderer.
Images can be specified to SimpleCell
in one of two ways:
-
Programmatically with
Image
objects - Declaratively with registered image names
Normally, the data for a cell contains a single value to be formatted and displayed. This value is passed to the cell renderer as config.value
. If SimpleCell
is passed an array instead of a scalar value, it interprets it as an image triplet, a 3-element array as follows:
- One of:
-
Image
object — Renders the image into the cell, left-aligned - falsy value — to render nothing on the left side of the cell
-
- One of:
-
Image
object - Renders the image into the cell, center-aligned - A function returning a stringifiable value - Value is formatted per cell's localizer (
format
property) and rendered into the cell, aligned per cell'shalign
property - A stringifiable value - Value is formatted per cell's localizer (
format
property) and rendered into the cell, aligned per cell'shalign
property - Otherwise renders nothing into the center of the cell
-
- One of:
-
Image
object — Renders the image into the cell, right-aligned - falsy value — Renders nothing on the left side of the cell
-
There are three ways the cell value can become such an image triplet.
The triplet can come directly from the data source:
// access the value from the 4th data column, 8th row
grid.getValue(3, 7); // may return a scalar or a 3-element array as discussed above
Data is typically sent in JSON format so this approach would require that the received data be processed to insert the Image
objects.
The application can explicitly set the value to such a triplet:
// set a value + right-side icon of a specific cell in a specific column
var value = [null, 23, images.triangle];
grid.setValue(3, 7, value);
In the example above, the value 23
is displayed in the cell, along with a right-side icon (presumably a triangle).
Applications can override the data model's getCell
method. As described in the getCell wiki, the method is passed a config
object and a rendererName
string. The standard implementation simply returns a reference to the named cell renderer.
The override can include logic to replace the value based on the cell coordinates. For example, the following implementation of a getCell
override adds an age-dependent image to the right of the age value for each cell in the "age" column:
dataModel.getCell = function(config, rendererName) {
if (config.column.name === 'age') {
var icon = images['ageDecade' + Math.floor(config.value / 10)];
config.value = [null, config.value, icon];
}
return grid.cellRenderers.get(rendererName);
}
The reference to config.value
above assumes it is not already a triplet, a fair assumption to make when you are in control of the data.
Note that getCell
is called for every cell, not just data cells, which provides the opportunity to set images on metadata cells (e.g., headers).
For example, when the optional row checkboxes and/or numbers are turned on, Hypergrid automatically generates the row header column, an additional column displayed to the left of all the other columns. The cells in this column for each data row contain a checkbox image + a data row number. To make the checkbox follow the row number rather than the other way around:
grid.behavior.dataModel.getCell = function(config, rendererName) {
if (config.isHandleColumn && config.isDataRow) {
var icon = images[config.isRowSelected ? 'checked' : 'unchecked'];
config.value = [null, config.value, icon];
}
return grid.cellRenderers.get(rendererName);
}
(Normally, without the above getCell logic, the row header checkboxes use the declarative method discussed below. The above code will override that and use the programmatic method because the SimpleCell cell renderer gives preference to the programmatic method of determining icons over the declarative method.)
Images are specified declaratively as property values.
Hypergrid has a cascading structure of properties objects, beginning with the grid properties object; then specific columns extended from that object; and (optionally) specific cells extended from the column properties objects. Thus for example, if a column does not declare a specific property of its own, the value defaults to that of the grid property of the same name. (See the Properties wiki for more information.)
Specify an image in one or more of the following properties:
leftIcon
centerIcon
rightIcon
The values are registered image names. (See Image registry below.) For example, to set the right-side icon to an image registered as "triangle":
// get a reference to the column properties object
var columnProps = grid.behavior.getColumn(3).properties;
// declare an image for all cells in a specific column
columnProps.rightIcon = 'triangle';
// declare an image for a specific cell in a specific column
columnProps.setCellProperty(5, 'rightIcon', 'triangle');
// or:
grid.behavior.setCellProperty(3, 5, 'rightIcon', 'triangle');
Instead of declaring a primitive string value for a property, it is also possible to define a getter for the property. So the icon properties, which normally have registered image names as their values, can be redefined as getters that return such names.
The following example chooses the same age-related right icon image as the programmatic getCell
example above:
var iconDescriptor = {
enumerable: true,
get: function() {
return 'ageDecade' + Math.floor(this.value / 10);
}
};
Object.defineProperty(columnProps, 'rightIcon', iconDescriptor);
The execution context (the this
value) for property getters is the relevant properties object itself, which will include at render time additional properties added by the renderer, such as value
in the above.
Remember that the icon properties are strings containing registered image names — unlike the image triplet elements described above which are references to actual Image
objects.
Note that a property defined as a getter as above is read-only and cannot be set to an explicit value. This is generally appropriate for a calculated value. That said, if you do need to be able to subsequently reset such a property, first make sure it is "configurable" and then add the following "self-defeating" setter which redefines the property (both the getter and this setter itself) as a simple instance var:
iconDescriptor.configurable = true;
iconDescriptor.set = function(value) {
Object.defineProperty(this, 'rightIcon', {
configurable: true,
enumerable: true,
writable: true,
value: value
});
};
NOTE: It is assumed you will be calling the above setter on the object owning the property, i.e., the column properties object. (Specifically, do not invoke the setter through getCell
's config
parameter. The object owning the definition is in config
's prototype chain because config
is created from the column properties object on every cell render. The setter, when invoked through config
, creates a new definition which is "owned" by config
and will therefore only be effective for the current render. Even if you were to set it on the correct object in the prototype chain, this is still the wrong place to do this because it only needs to be done once; the action would be redundant on subsequent getCell
calls.)
The image registry is a simple hash of image names paired with Image
objects.
To register an image:
This approach gets the data directly from an image file loaded with the page response.
<body>
...
<img id="triangle-icon" style="display:none" src="my-triangle-icon.png">
...
</body>
// if using the fin-hypergrid.js build
var imageRegistry = fin.Hypergrid.require('fin-hypergrid/images');
// if using the fin-hypergrid npm module
var imageRegistry = require('fin-hypergrid/images');
The image registry is merely a JavaScript hash of Image
objects.
Make sure the image is loaded. (For example, by waiting for the image's or the window's load event.) Then register the image:
// register the image under the name "tricon"
imageRegistry.add('tricon', document.getElementById('triangle-icon'));
To avoid depending on external files, you can encode the binary image data in Base64 text format. There are many utilities available to help you do this, such as this web client one; and my gulp-imagine-64
npm gulp module.
Once you have the data, to register the image under the name "tricon" for example:
var img = new Image();
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAECAYAAABcDxXOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjExR/NCNwAAABpJREFUGFdjgIL/eDAKIKgABggqgAE0BQwMAPTlD/Fpi0JfAAAAAElFTkSuQmCC';
imageRegistry.add('tricon', img);
Images may derive from SVG data, which has the advantage that it is fully themeable. See the Themeable Images wiki for details.
The registry comes pre-loaded with a few icon images (using the raw image data method described above). You can see these in the registry hash:
console.log(Object.keys(imageRegistry));
For example, the checkbox found in the row number column uses two of these images, one for when the checkbox is empty (unchecked
); and one for when it is checked (checked
).
If you wish to replace any of these images with your own, simply override them with a different Image
object or reset their src
property. Suppose for example that you had replacement images for checked
and unchecked
:
imageRegistry.checked = myCheckedImage;
imageRegistry.unchecked = myUncheckedImage;
(See Image Registry above for how to get images into variables.)
The row handle column has its own properties object. This object has a predefined getter for leftIcon
as described above in Calculated property values. This getter chooses between the 'checked'
vs. 'unchecked'
registered image names based on whether or not the row in question is currently unselected vs. selected, respectively.
If you prefer to put the checkboxes on the right, you could "move" the getter to the rightIcon
property:
var rowHeaderProps = grid.behavior.columns[-1].properties.rowHeader;
var iconDescriptor = Object.getOwnPropertyDescriptor(rowHeaderProps, 'leftIcon');
delete rowHeaderProps.leftIcon;
Object.defineProperty(rowHeaderProps, 'rightIcon', iconDescriptor);
Although you could similarly move it to the centerIcon
property, the cell value (the row number) would override it so it would be pointless.
One way to eliminate the row handle column's checkboxes entirely might be to delete this getter and replace it with an unregistered image name:
rowHeaderProps.leftIcon = '';
Another way would be to delete the images from the registry (although that would render them unusable thereafter):
delete imageRegistry.checked;
delete imageRegistry.unchecked;
Update: While the above examples will work, be aware that (as of v2.1.0) the boolean rowHeaderCheckboxes
property can be used to hide just the checkboxes from the row headers; and the boolean rowHeaderNumbers
property can be used to hide just the row numbers. The boolean property showRowNumbers
is now a setter that sets both of the first two properties; and a getter that returns truthy if either is truthy.
It is up to the cell renderer programmer to make sure the image is rendered neatly within the cell. The easiest way to do this is to not worry about it all, which works fine so long as you keep the images small (which is why we're referring to them as "icons"). In particular, keep the height ≤ the row height.
As a general rule, you want to avoid defining a clipping region in your renderer because of the cost: Clipping every cell will significantly slow down grid rendering. That said, you could clip just those cells with images in danger of overflowing the cell boundaries. But of course you don't really want your image to be clipped anyway, right? So that is probably the wrong approach. On the other hand, having the image overflow the cell boundaries is even worse. If the resulting speed is still adequate for your needs, go for it. But the best advice is, again, to keep the images small and you shouldn't have any issues.