fin-hypergrid
Version:
Canvas-based high-performance grid
429 lines (366 loc) • 15.8 kB
JavaScript
;
var CellRenderer = require('./CellRenderer');
var Rectangle = require('rectangular').Rectangle;
var images = require('../../images');
var WHITESPACE = /\s\s+/g;
/**
* @constructor
* @summary The default cell renderer for a vanilla cell.
* @desc Great care has been taken in crafting this function as it needs to perform extremely fast.
*
* Use `gc.cache` instead which we have implemented to cache the graphics context properties. Reads on the graphics context (`gc`) properties are expensive but not quite as expensive as writes. On read of a `gc.cache` prop, the actual `gc` prop is read into the cache once and from then on only the cache is referenced for that property. On write, the actual prop is only written to when the new value differs from the cached value.
*
* Clipping bounds are not set here as this is also an expensive operation. Instead, we employ a number of strategies to truncate overflowing text and content.
*
* @extends CellRenderer
*/
var SimpleCell = CellRenderer.extend('SimpleCell', {
paint: function(gc, config) {
var val = config.value,
bounds = config.bounds,
x = bounds.x,
y = bounds.y,
width = bounds.width,
height = bounds.height,
iconPadding = config.iconPadding,
partialRender = config.prefillColor === undefined, // signifies abort before rendering if same
snapshot = config.snapshot,
same = snapshot && partialRender,
valWidth = 0,
textColor, textFont,
ixoffset, iyoffset,
leftIcon, rightIcon, centerIcon,
leftPadding, rightPadding,
hover, hoverColor, selectColor, foundationColor, inheritsBackgroundColor,
c, colors;
// setting gc properties are expensive, let's not do it needlessly
if (val && val.constructor === Array) {
leftIcon = val[0];
rightIcon = val[2];
val = config.exec(val[1]);
if (val && val.naturalWidth !== undefined) { // must be an image (much faster than instanceof HTMLImageElement)
centerIcon = val;
val = null;
}
} else {
leftIcon = images[config.leftIcon];
centerIcon = images[config.centerIcon];
rightIcon = images[config.rightIcon];
}
// Note: vf == 0 is fastest equivalent of vf === 0 || vf === false which excludes NaN, null, undefined
var renderValue = val || config.renderFalsy && val == 0; // eslint-disable-line eqeqeq
if (renderValue) {
val = config.formatValue(val, config);
textFont = config.isSelected ? config.foregroundSelectionFont : config.font;
textColor = gc.cache.strokeStyle = config.isSelected
? config.foregroundSelectionColor
: config.color;
} else {
val = '';
}
same = same &&
val === snapshot.value &&
textFont === snapshot.textFont &&
textColor === snapshot.textColor;
// fill background only if our bgColor is populated or we are a selected cell
colors = [];
c = 0;
if (config.isCellHovered && config.hoverCellHighlight.enabled) {
hoverColor = config.hoverCellHighlight.backgroundColor;
} else if (config.isRowHovered && (hover = config.hoverRowHighlight).enabled) {
hoverColor = config.isDataColumn || !hover.header || hover.header.backgroundColor === undefined ? hover.backgroundColor : hover.header.backgroundColor;
} else if (config.isColumnHovered && (hover = config.hoverColumnHighlight).enabled) {
hoverColor = config.isDataRow || !hover.header || hover.header.backgroundColor === undefined ? hover.backgroundColor : hover.header.backgroundColor;
}
if (gc.alpha(hoverColor) < 1) {
if (config.isSelected) {
selectColor = config.backgroundSelectionColor;
}
if (gc.alpha(selectColor) < 1) {
inheritsBackgroundColor = (config.backgroundColor === config.prefillColor);
if (!inheritsBackgroundColor) {
foundationColor = true;
colors.push(config.backgroundColor);
same = same && foundationColor === snapshot.foundationColor &&
config.backgroundColor === snapshot.colors[c++];
}
}
if (selectColor !== undefined) {
colors.push(selectColor);
same = same && selectColor === snapshot.colors[c++];
}
}
if (hoverColor !== undefined) {
colors.push(hoverColor);
same = same && hoverColor === snapshot.colors[c++];
}
// todo check if icons have changed
if (same && c === snapshot.colors.length) {
return;
}
// return a snapshot to save in cellEvent for future comparisons by partial renderer
config.snapshot = {
value: val,
textColor: textColor,
textFont: textFont,
foundationColor: foundationColor,
colors: colors
};
layerColors(gc, colors, x, y, width, height, foundationColor);
// Measure left and right icons, needed for rendering and for return value (min width)
leftPadding = leftIcon ? iconPadding + leftIcon.width + iconPadding : config.cellPadding;
rightPadding = rightIcon ? iconPadding + rightIcon.width + iconPadding : config.cellPadding;
if (renderValue) {
// draw text
gc.cache.fillStyle = textColor;
gc.cache.font = textFont;
valWidth = config.isHeaderRow && config.headerTextWrapping
? renderMultiLineText(gc, config, val, leftPadding, rightPadding)
: renderSingleLineText(gc, config, val, leftPadding, rightPadding);
}
if (centerIcon) {
// Measure & draw center icon
iyoffset = Math.round((height - centerIcon.height) / 2);
ixoffset = width - Math.round((width - centerIcon.width) / 2) - centerIcon.width;
gc.drawImage(centerIcon, x + ixoffset, y + iyoffset, centerIcon.width, centerIcon.height); // see [SIZE NOTE]!
valWidth = iconPadding + centerIcon.width + iconPadding;
if (config.hotIcon === 'center') {
config.clickRect = new Rectangle(ixoffset, iyoffset, centerIcon.width, centerIcon.height);
}
}
if (leftIcon) {
// Draw left icon
iyoffset = Math.round((height - leftIcon.height) / 2);
gc.drawImage(leftIcon, x + iconPadding, y + iyoffset, leftIcon.width, leftIcon.height); // see [SIZE NOTE]!
if (config.hotIcon === 'left') {
config.clickRect = new Rectangle(iconPadding, iyoffset, leftIcon.width, leftIcon.height);
}
}
if (rightIcon) {
// Repaint background before painting right icon, because text may have flowed under where it will be.
// This is a work-around to clipping which is too expensive to perform here.
ixoffset = width - (rightIcon.width + iconPadding);
var rightX = x + ixoffset;
if (inheritsBackgroundColor) {
foundationColor = true;
colors.unshift(config.backgroundColor);
}
layerColors(gc, colors, rightX, y, rightPadding, height, foundationColor);
// Draw right icon
iyoffset = Math.round((height - rightIcon.height) / 2);
gc.drawImage(rightIcon, rightX, y + iyoffset, rightIcon.width, rightIcon.height); // see [SIZE NOTE]!
if (config.hotIcon === 'right') {
config.clickRect = new Rectangle(ixoffset, iyoffset, rightIcon.width, rightIcon.height);
}
}
if (config.cellBorderThickness) {
gc.beginPath();
gc.rect(x, y, width, height);
gc.cache.lineWidth = config.cellBorderThickness;
gc.cache.strokeStyle = config.cellBorderStyle;
gc.stroke();
gc.closePath();
}
config.minWidth = leftPadding + valWidth + rightPadding;
}
});
/* [SIZE NOTE] (11/1/2018): Always call `drawImage` with explicit width and height overload.
* Possible browser bug: Although 3rd and 4th parameters to `drawImage` are optional,
* when image data derived from SVG source, some browsers (e.g., Chrome 70) implementation
* of `drawImage` only respects _implicit_ `width` x `height` specified in the root <svg>
* element `width` & `height` attributes. Otherwise, image is copied into canvas using its
* `naturalWidth` x `naturalHeight`. That is, _explict_ settings of `width` & `height`
* (i.e, via property assignment, calling setAttribute, or in `new Image` call) have no
* effect on `drawImage` in the case of SVGs on these browsers.
*/
/**
* @summary Renders single line text.
* @param {CanvasRenderingContext2D} gc
* @param {object} config
* @param {Rectangle} config.bounds - The clipping rect of the cell to be rendered.
* @param {*} val - The text to render in the cell.
* @memberOf SimpleCell.prototype
*/
function renderMultiLineText(gc, config, val, leftPadding, rightPadding) {
var x = config.bounds.x,
y = config.bounds.y,
width = config.bounds.width,
height = config.bounds.height,
cleanVal = (val + '').trim().replace(WHITESPACE, ' '), // trim and squeeze whitespace
lines = findLines(gc, config, cleanVal.split(' '), width);
if (lines.length === 1) {
return renderSingleLineText(gc, config, cleanVal, leftPadding, rightPadding);
}
var halignOffset = leftPadding,
valignOffset = config.voffset,
halign = config.halign,
textHeight = gc.getTextHeight(config.font).height;
switch (halign) {
case 'right':
halignOffset = width - rightPadding;
break;
case 'center':
halignOffset = width / 2;
break;
}
var hMin = 0, vMin = Math.ceil(textHeight / 2);
valignOffset += Math.ceil((height - (lines.length - 1) * textHeight) / 2);
halignOffset = Math.max(hMin, halignOffset);
valignOffset = Math.max(vMin, valignOffset);
gc.cache.save(); // define a clipping region for cell
gc.beginPath();
gc.rect(x, y, width, height);
gc.clip();
gc.cache.textAlign = halign;
gc.cache.textBaseline = 'middle';
for (var i = 0; i < lines.length; i++) {
gc.simpleText(lines[i], x + halignOffset, y + valignOffset + (i * textHeight));
}
gc.cache.restore(); // discard clipping region
return width;
}
/**
* @summary Renders single line text.
* @param {CanvasRenderingContext2D} gc
* @param {object} config
* @param {Rectangle} config.bounds - The clipping rect of the cell to be rendered.
* @param {*} val - The text to render in the cell.
* @memberOf SimpleCell.prototype
*/
function renderSingleLineText(gc, config, val, leftPadding, rightPadding) {
var x = config.bounds.x,
y = config.bounds.y,
width = config.bounds.width,
halignOffset = leftPadding,
halign = config.halign,
minWidth,
metrics;
if (config.columnAutosizing) {
metrics = gc.getTextWidthTruncated(val, width - leftPadding, config.truncateTextWithEllipsis);
minWidth = metrics.width;
val = metrics.string || val;
switch (halign) {
case 'right':
halignOffset = width - rightPadding - metrics.width;
break;
case 'center':
halignOffset = (width - metrics.width) / 2;
break;
}
} else {
metrics = gc.getTextWidthTruncated(val, width - leftPadding, config.truncateTextWithEllipsis, true);
minWidth = 0;
if (metrics.string !== undefined) {
val = metrics.string;
} else {
switch (halign) {
case 'right':
halignOffset = width - rightPadding - metrics.width;
break;
case 'center':
halignOffset = (width - metrics.width) / 2;
break;
}
}
}
if (val !== null) {
x += Math.max(leftPadding, halignOffset);
y += Math.floor(config.bounds.height / 2);
if (config.isUserDataArea) {
if (config.link) {
if (config.isCellHovered || !config.linkOnHover) {
if (config.linkColor) {
gc.cache.strokeStyle = config.linkColor;
}
gc.beginPath();
underline(config, gc, val, x, y, 1);
gc.stroke();
gc.closePath();
}
if (config.linkColor && (config.isCellHovered || !config.linkColorOnHover)) {
gc.cache.fillStyle = config.linkColor;
}
}
if (config.strikeThrough === true) {
gc.beginPath();
strikeThrough(config, gc, val, x, y, 1);
gc.stroke();
gc.closePath();
}
}
gc.cache.textAlign = 'left';
gc.cache.textBaseline = 'middle';
gc.simpleText(val, x, y);
}
return minWidth;
}
function findLines(gc, config, words, width) {
if (words.length === 1) {
return words;
}
// starting with just the first word...
var stillFits, line = [words.shift()];
while (
// so lone as line still fits within current column...
(stillFits = gc.getTextWidth(line.join(' ')) < width)
// ...AND there are more words available...
&& words.length
) {
// ...add another word to end of line and retest
line.push(words.shift());
}
if (
!stillFits // if line is now too long...
&& line.length > 1 // ...AND is multiple words...
) {
words.unshift(line.pop()); // ...back off by (i.e., remove) one word
}
line = [line.join(' ')];
if (words.length) { // if there's anything left...
line = line.concat(findLines(gc, config, words, width)); // ...break it up as well
}
return line;
}
function strikeThrough(config, gc, text, x, y, thickness) {
var textWidth = gc.getTextWidth(text);
switch (gc.cache.textAlign) {
case 'center':
x -= textWidth / 2;
break;
case 'right':
x -= textWidth;
break;
}
y = Math.round(y) + 0.5;
gc.cache.lineWidth = thickness;
gc.moveTo(x - 1, y);
gc.lineTo(x + textWidth + 1, y);
}
function underline(config, gc, text, x, y, thickness) {
var textHeight = gc.getTextHeight(config.font).height,
textWidth = gc.getTextWidth(text);
switch (gc.cache.textAlign) {
case 'center':
x -= textWidth / 2;
break;
case 'right':
x -= textWidth;
break;
}
y = Math.ceil(y) + Math.round(textHeight / 2) - 0.5;
//gc.beginPath();
gc.cache.lineWidth = thickness;
gc.moveTo(x, y);
gc.lineTo(x + textWidth, y);
}
function layerColors(gc, colors, x, y, width, height, foundationColor) {
for (var i = 0; i < colors.length; i++) {
if (foundationColor && !i) {
gc.clearFill(x, y, width, height, colors[i]);
} else {
gc.cache.fillStyle = colors[i];
gc.fillRect(x, y, width, height);
}
}
}
module.exports = SimpleCell;