diff --git a/README.md b/README.md
index 477e955c3..39018b1a3 100644
--- a/README.md
+++ b/README.md
@@ -15,9 +15,9 @@ It also highlights a DOM-based custom external editor triggered via hypergrid ev
* [Roadmap](#roadmap)
* [Contributing](#contributors)
-### Current Release (3.1.0 - 29 September 2018)
+### Current Release (3.2.0 - 17 November 2018)
-**Hypergrid 3.1 includes 3.0’s revised data model with some breaking changes.**
+> **CAUTION:** For those considering upgrading directly from v2, be advised Hypergrid v3 introduced a revised data model _with breaking changes._ The impact of these changes has been intentionally minimized and should not affect the vast majority of users. See the [v3.0.0 release notes](https://github.com/fin-hypergrid/core/releases/tag/v3.0.0) for more information.
_For a complete list of changes, see the [release notes](https://github.com/fin-hypergrid/core/releases)._
@@ -25,7 +25,7 @@ _For a complete list of changes, see the [release notes](https://github.com/fin-
#### npm module _(recommended)_
Published as a CommonJS module to npmjs.org.
-Specify a SEMVER of `"fin-hypergrid": "3.1.0"` (or `"^3.1.0"`) in your package.json file,
+Specify a SEMVER of `"fin-hypergrid": "3.2.0"` (or `"^3.2.0"`) in your package.json file,
issue the `npm install` command, and let your bundler (wepback,
Browserify) create a single file containing both Hypergrid and your application.
diff --git a/gulpfile.js b/gulpfile.js
index ae6bedf2b..86b9a5477 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -70,7 +70,7 @@ function clearBashScreen() {
function swallowImages() {
var config = {
src: {
- globs: [ 'images/*.png', 'images/*.gif','images/*.jpeg', 'images/*.jpg' ],
+ globs: 'images/*.{gif,png,jpg,jpeg,svg,ico}',
options: {}
},
transform: {
diff --git a/images/index.js b/images/index.js
index fc0b692d5..723fb52f4 100644
--- a/images/index.js
+++ b/images/index.js
@@ -12,6 +12,7 @@
'use strict';
var _ = require('object-iterators');
+var svgThemer = require('svg-themer');
var images = require('./images'); // this is the file generated by gulpfile.js (and ignored by git)
@@ -72,38 +73,148 @@ images['checkbox-on'] = images.checked;
images['checkbox-off'] = images.unchecked;
/**
- * @name add
* @method
- * @param {string} key
+ * @param {string} name
* @param {HTMLImageElement} img
+ * @param {boolean} [themeable] - If truthy, the image will be themed by {@link module:images.setTheme images.setTheme}, called by {@link Hypergrid.applyTheme}.
+ * If falsy, the image won't be themed until `images[name].themeable` is set to `true`.
+ * In any case the remaining parameters are processed.
+ * @param {function} [setSvgProps=svgThemer.setSvgProps] - Optional custom theming code for this image and the rules implied by `styles`. _If omitted, `styles` is promoted 2nd parameter position._
+ * @param {boolean|string[]} [styles] - Optional list style names with which to create CSS rules.
+ * * If falsy (or omitted), no rules are created.
+ * * Else if truthy but not an array, create a single rule:
+ * ```css
+ * `.hypergrid-background-image-name { background-image: url(...) }`
+ * where _name_ is the value of the `name` parameter.
+ * * Else if an array, create a CSS rule for each style named therein.
+ *
+ * For each rule thus created:
+ * * Inserted into `style#injected-stylesheet-grid`.
+ * * Selector is `.hypergrid-style-name` (where `style` is element value and `name` is image name).
+ * (If a rule with that selector already exists, it is replaced.)
+ * * Contains the named style with a value of `url(...)` where `...` is the image data.
+ * Possible styles must be one of those listed in {*link https://github.com/joneit/svg-themer/blob/master/README.md#cssimagepropertynames svgThemer.cssImagePropertyNames} (which you can extend if needed).
+ * * Will be automatically themed when the grid is themed (which is the whole point).
+ *
+ * @see {@link https://github.com/joneit/svg-themer}
+ * @memberOf module:images
+ */
+function add(name, img, themeable, setSvgProps, styles) {
+ if (/^data:image\/svg\+xml|\.svg/.test(img.src)) {
+ img.themeable = !!themeable;
+ if (typeof setSvgProps === 'object') {
+ styles = setSvgProps;
+ setSvgProps = undefined;
+ }
+ if (setSvgProps) {
+ img.setSvgProps = setSvgProps;
+ }
+ if (styles) {
+ img.themeableRules = createThemeableRules(name, img, setSvgProps, styles);
+ }
+ }
+ return (images[name] = img);
+}
+
+function createThemeableRules(key, img, setSvgProps, styles) {
+ // find or create stylesheet as needed
+ var styleEl = document.querySelector('style#injected-stylesheet-themeables');
+ if (!styleEl) {
+ styleEl = document.createElement('style');
+ styleEl.id = 'injected-stylesheet-themeables';
+ document.head.appendChild(styleEl);
+ }
+ var sheet = styleEl.sheet;
+
+ return (styles.length ? styles : ['background-image']).reduce(function(rules, styleName) {
+ var selectorText = '.hypergrid-' + styleName + '-' + key;
+
+ // find and delete existing rule, if any
+ var ruleIndex = Array.prototype.findIndex.call(sheet.cssRules, function(rule) {
+ return rule.selectorText === selectorText;
+ });
+ if (ruleIndex !== -1) {
+ sheet.deleteRule(ruleIndex);
+ }
+
+ // create and insert new rule consisting of selector + style "collection"
+ var ruleStyles = {};
+
+ // add image data style
+ ruleStyles[styleName] = 'url(' + img.src + ')';
+
+ // add dimensions if known
+ if (img.width) { ruleStyles.width = img.width + 'px'; }
+ if (img.height) { ruleStyles.height = img.height + 'px'; }
+
+ // combine the above styles into a semi-colon-separated "collection"
+ var styleCollection = Object.keys(ruleStyles).map(function(key) {
+ return key + ':' + ruleStyles[key];
+ }).join(';');
+
+ var ruleText = '{' + styleCollection + '}';
+ sheet.insertRule(selectorText + ruleText);
+
+ var themeableRule = {
+ rule: sheet.cssRules[0]
+ };
+ if (setSvgProps) {
+ themeableRule.setSvgProps = setSvgProps;
+ }
+ rules.push(themeableRule);
+ return rules;
+ }, []);
+}
+
+/**
+ * @param {object} theme
* @memberOf module:images
*/
-images.add = function(key, img) {
- return images[key] = img;
-};
+function setTheme(theme) {
+ Object.keys(images).forEach(function(name) {
+ var img = images[name];
+ if (img.themeable) {
+ svgThemer.setImgSvgProps.call(img, theme, img.setSvgProps);
+ }
+ if (img.themeableRules) {
+ img.themeableRules.forEach(function(themeable) {
+ var selectorText = themeable.rule.selectorText;
+ // extract style name using list of possible names
+ var regex = new RegExp('^\.hypergrid-(' + svgThemer.cssImagePropertyNames.join('|') + ')-.*$');
+ var styleName = selectorText.replace(regex, '$1');
+ svgThemer.setRuleSvgProps.call(themeable.rule, theme, img.setSvgProps, styleName);
+ });
+ }
+ });
+}
/**
* Convenience function.
- * @name checkbox
- * @method
* @param {boolean} state
* @returns {HTMLImageElement} {@link module:images.checked|checked} when `state` is truthy or {@link module:images.unchecked|unchecked} otherwise.
* @memberOf module:images
*/
-images.checkbox = function(state) {
+function checkbox(state) {
return images[state ? 'checked' : 'unchecked'];
-};
+}
/**
* Convenience function.
- * @name filter
- * @method
* @param {boolean} state
* @returns {HTMLImageElement} {@link module:images.filter-off|filter-off} when `state` is truthy or {@link module:images.filter-on|filter-on} otherwise.
* @memberOf module:images
*/
-images.filter = function(state) {
+function filter(state) {
return images[state ? 'filter-on' : 'filter-off'];
-};
+}
+
+// add methods as non-enumerable members so member images can be enumerated
+Object.defineProperties(images, {
+ add: { value: add },
+ setTheme: { value: setTheme },
+ checkbox: { value: checkbox },
+ filter: { value: filter }
+});
+
module.exports = images;
diff --git a/package.json b/package.json
index 0d8f059cf..574219574 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "fin-hypergrid",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "Canvas-based high-performance grid",
"main": "src/Hypergrid",
"repository": {
@@ -32,6 +32,7 @@
"overrider": "^0",
"rectangular": "1.0.1",
"sparse-boolean-array": "1.0.1",
+ "svg-themer": "^1.1.2",
"synonomous": "^2.1.2"
},
"devDependencies": {
@@ -41,7 +42,7 @@
"gulp-eslint": "^4.0.2",
"gulp-footer": "^1.1.1",
"gulp-header": "^1.8.2",
- "gulp-imagine-64": "^1.0.1",
+ "gulp-imagine-64": "^2.0.1",
"gulp-load-plugins": "^1.1.0",
"gulp-mocha": "^6.0.0",
"run-sequence": "^1.1.4"
diff --git a/src/Base.js b/src/Base.js
index 40fb2c335..7bb5eecf3 100644
--- a/src/Base.js
+++ b/src/Base.js
@@ -19,14 +19,16 @@ Object.defineProperty(Base.prototype, 'version', {
value: require('../package.json').version
});
-Base.prototype.atLeastVersion = function(neededVersion) {
- var neededParts = neededVersion.split('.'),
- thisParts = this.version.split('.'),
- delta;
- neededParts.find(function(neededPart, i) {
- return (delta = neededPart - thisParts[i]);
- });
- return delta >= 0;
+Base.prototype.versionAtLeast = function(neededVersion) {
+ var neededParts = neededVersion.split('.').map(Number),
+ delta = this.version.split('.').map(function(part, i) { return Number(part) - neededParts[i]; });
+ return (
+ delta[0] > 0 ||
+ delta[0] === 0 && (
+ delta[1] > 0 ||
+ delta[1] === 0 && delta[2] >= 0
+ )
+ );
};
Base.prototype.deprecated = require('./lib/deprecated');
diff --git a/src/Hypergrid/events.js b/src/Hypergrid/events.js
index 0f0f8db20..1128c27fb 100644
--- a/src/Hypergrid/events.js
+++ b/src/Hypergrid/events.js
@@ -458,6 +458,23 @@ exports.mixin = {
writable: true
}
);
+
+ // add some interesting mouse offsets
+ var drilldown;
+ if ((drilldown = primitiveEvent.primitiveEvent && primitiveEvent.primitiveEvent.detail)) {
+ decoratedEvent.gridPoint = drilldown.mouse;
+ if ((drilldown = drilldown.primitiveEvent)) {
+ decoratedEvent.clientPoint = {
+ x: drilldown.clientX,
+ y: drilldown.clientY
+ };
+ decoratedEvent.pagePoint = {
+ x: drilldown.clientX + window.scrollX,
+ y: drilldown.clientY + window.scrollY
+ };
+ }
+ }
+
cb.call(grid, decoratedEvent);
}
}
@@ -576,7 +593,7 @@ exports.mixin = {
});
this.addInternalEventListener('fin-canvas-context-menu', function(e) {
- handleMouseEvent(e, function(mouseEvent){
+ handleMouseEvent(e, function(mouseEvent) {
grid.delegateContextMenu(mouseEvent);
grid.fireSyntheticContextMenuEvent(mouseEvent);
});
diff --git a/src/Hypergrid/themes.js b/src/Hypergrid/themes.js
index d66a9617c..d5f819737 100644
--- a/src/Hypergrid/themes.js
+++ b/src/Hypergrid/themes.js
@@ -11,6 +11,7 @@ var _ = require('object-iterators'); // fyi: installs the Array.prototype.find p
var defaults = require('../defaults');
var dynamicPropertyDescriptors = require('../lib/dynamicProperties');
var HypergridError = require('../lib/error');
+var images = require('../../images');
var styles = [
'BackgroundColor',
@@ -73,15 +74,9 @@ stylers.reduce(function(theme, styler) {
var registry = Object.create(null, {
default: { value: defaultTheme }
});
-var pseudopropAdvice = {
- showRowNumbers: 'rowHeaderCheckboxes and rowHeaderNumbers',
- lineColor: 'gridLinesHColor and gridLinesVColor',
- lineWidth: 'gridLinesHWidth and gridLinesVWidth',
- gridBorder: 'gridBorderLeft, gridBorderRight, gridBorderTop, and gridBorderBottom'
-};
function applyTheme(theme) {
- var themeLayer, grids, props;
+ var themeLayer, grids, props, themeObject;
if (theme && typeof theme === 'object' && !Object.getOwnPropertyNames(theme).length) {
theme = null;
@@ -117,45 +112,50 @@ function applyTheme(theme) {
theme = theme || 'default';
}
- if (typeof theme === 'string') {
- if (!registry[theme]) {
- throw new HypergridError('Unknown theme "' + theme + '"');
- }
- theme = registry[theme];
+ if (typeof theme === 'object') {
+ themeObject = theme;
+ } else if (!registry[theme]) {
+ throw new HypergridError('Unknown theme "' + theme + '"');
+ } else {
+ themeObject = registry[theme];
}
- if (theme) {
+ if (themeObject) {
// When no theme name, set it to explicit `undefined` (to mask defaults.themeName).
- if (!theme.themeName) {
- theme.themeName = undefined;
+ if (!themeObject.themeName) {
+ themeObject.themeName = undefined;
}
- Object.keys(theme).forEach(function(key) {
+ Object.keys(themeObject).forEach(function(key) {
if (key in dynamicPropertyDescriptors) {
if (key in dynamicCosmetics) {
grids.forEach(function(grid) {
- grid.properties[key] = theme[key];
+ grid.properties[key] = themeObject[key];
});
} else {
// Dynamic properties are defined on properties layer; defining these
// r-values on the theme layer is ineffective so let's not allow it.
- var message = pseudopropAdvice[key];
- message = message
- ? 'Ignoring unexpected pseudo-prop ' + key + ' in theme object. Use actual props ' + message + ' instead.'
- : 'Ignoring invalid property ' + key + ' in theme object.';
- console.warn(message);
- delete theme[key];
+ switch (key) {
+ case 'lineColor':
+ themeObject.gridLinesHColor = themeObject.gridLinesVColor = themeObject[key];
+ break;
+ default:
+ console.warn('Ignoring unexpected dynamic property ' + key + ' from theme object.');
+ }
+ // delete themeObject[key];
}
}
});
// No .assign() because themeName is read-only in defaults layer
- Object.defineProperties(themeLayer, Object.getOwnPropertyDescriptors(theme));
+ Object.defineProperties(themeLayer, Object.getOwnPropertyDescriptors(themeObject));
}
grids.forEach(function(grid) {
grid.repaint();
});
+
+ return themeObject;
}
@@ -185,8 +185,8 @@ var mixin = {
* @this {Hypergrid}
* @param {object|string} [theme] - One of:
* * **string:** A registered theme name.
- * * **object:** A unregistered (anonymous) theme object. Empty object removes grid theme, exposing global theme.
- * * _falsy value:_ Also removes grid theme.
+ * * **object:** An anonymous (unregistered) theme object. Empty object removes grid theme, exposing global theme.
+ * * _falsy value:_ Also removes grid theme (like empty object).
* @param {string|undefined} [theme.themeName=undefined]
* @memberOf Hypergrid#
*/
@@ -213,7 +213,7 @@ var mixin = {
};
Object.defineProperty(mixin, 'theme', {
enumerable: true,
- set: mixin.applyTheme,
+ set: applyTheme,
get: mixin.getTheme
});
@@ -234,7 +234,8 @@ var sharedMixin = {
* ```javascript
* var myTheme = require('fin-hypergrid-themes').buildTheme();
* ```
- * If omitted, the theme named in the first parameter is unregistered.
+ * If omitted, unregister the theme named in the first parameter.
+ *
* Grid instances that have previously applied the named theme are unaffected by this action (whether re-registering or unregistering).
* @memberOf Hypergrid.
*/
@@ -284,7 +285,6 @@ var sharedMixin = {
* @summary Apply global theme.
* @desc Apply props from the given theme object to the global theme object,
* the `defaults` layer at the bottom of the properties hierarchy.
- * @this {Hypergrid.}
* @param {object|string} [theme=registry.default] - One of:
* * **string:** A registered theme name.
* * **object:** A theme object. Empty object removes global them, restoring defaults.
@@ -292,11 +292,14 @@ var sharedMixin = {
* @param {string|undefined} [theme.themeName=undefined]
* @memberOf Hypergrid.
*/
- applyTheme: applyTheme
+ applyTheme: function(theme) {
+ var themeObject = applyTheme.call(this, theme);
+ images.setTheme(themeObject);
+ }
};
Object.defineProperty(sharedMixin, 'theme', { // global theme setter/getter
enumerable: true,
- set: applyTheme,
+ set: sharedMixin.applyTheme,
get: function() { return defaults; } // the defaults layer *is* the global theme layer
});
diff --git a/src/cellRenderers/SimpleCell.js b/src/cellRenderers/SimpleCell.js
index 51d37d446..fe01b0d9d 100644
--- a/src/cellRenderers/SimpleCell.js
+++ b/src/cellRenderers/SimpleCell.js
@@ -141,7 +141,7 @@ var SimpleCell = CellRenderer.extend('SimpleCell', {
// 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);
+ 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);
@@ -152,7 +152,7 @@ var SimpleCell = CellRenderer.extend('SimpleCell', {
if (leftIcon) {
// Draw left icon
iyoffset = Math.round((height - leftIcon.height) / 2);
- gc.drawImage(leftIcon, x + iconPadding, y + iyoffset);
+ 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);
}
@@ -171,7 +171,7 @@ var SimpleCell = CellRenderer.extend('SimpleCell', {
// Draw right icon
iyoffset = Math.round((height - rightIcon.height) / 2);
- gc.drawImage(rightIcon, rightX, y + iyoffset);
+ 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);
}
@@ -190,6 +190,16 @@ var SimpleCell = CellRenderer.extend('SimpleCell', {
}
});
+/* [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