/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/* eslint-env browser */
/*global niagara */
define([ 'jquery',
'Promise',
'nmodule/js/rc/asyncUtils/asyncUtils' ], function (
$,
Promise,
asyncUtils) {
'use strict';
var doRequire = asyncUtils.doRequire,
replaceMap = {
'/': '-',
'\\': '-',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
" ": ' '
},
moduleOrdPrefix = /^(local:\|)?module:\/\//,
toReplaceCss = /[/\\<>' "]/g,
toReplaceImg = /[\\<>'"]/g,
moduleDir = /^\/*module\//,
toRemoveQueryParameters = /\?.*$/i,
fileExtension = /\.(png|gif|bmp|jpg|jpeg)$/i,
// icons/x16/blank.png encoded as base64
blank = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAAtJREFUCB1jIBEAAAAwAAEK50gUAAAAAElFTkSuQmCC',
defaultSpritesheet = 'css!nmodule/icons/sprite/sprite',
dummyElement = $('<div class="bajaux-icon-iconUtils" style="display: none;"/>')
.appendTo($('body')),
// uri -> metrics object: is the img at this uri accounted for in a spritesheet?
spriteCache = {},
// uri -> metrics object: for images known not to be in the spritesheet
imageCache = {},
requireCache = {},
cssClassCache = {};
const URL_REGEX = /^url\(['"](.*)['"]\)$/;
/**
* Utility functions for working with icons and associated HTML.
*
* @exports bajaux/icon/iconUtils
*/
const exports = {};
////////////////////////////////////////////////////////////////
// Support functions
////////////////////////////////////////////////////////////////
function safeReplace(str) { return replaceMap[str]; }
function toUri(treatAsOrd, str) {
var iconIsOrd = treatAsOrd || isOrd(str),
uri = String(str),
isModuleOrd = uri.match(moduleOrdPrefix);
if (isModuleOrd) {
return uri.replace(moduleOrdPrefix, '/module/');
} else {
return (iconIsOrd ? '/ord/' : '') + uri;
}
}
function isOrd(obj) {
return obj && typeof obj.getType === 'function' &&
String(obj.getType()) === 'baja:Ord';
}
function getThemeName() {
return typeof niagara !== 'undefined' &&
niagara.env &&
niagara.env.themeName;
}
/**
* Creates and preloads an Image with the given src.
*
* @inner
* @param {String} src
* @returns {Promise} promise to be resolved after the image finishes loading
*/
function preloadImage(src) {
// eslint-disable-next-line promise/avoid-new
return new Promise(function (resolve, reject) {
var img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.onabort = reject;
img.src = src;
});
}
/**
* Create the `span` html to hold an icon for the given URI.
*
* @inner
* @param {String} uri e.g. `/module/icons/x16/action.png`
* @param {String} imgSrc the src to set on the img in the span
* @param {boolean} asElement true if this should return an Element
* @returns {string|HTMLSpanElement}
*/
function toSpan(uri, imgSrc, asElement) {
const cssClass = exports.toCssClass(uri);
let span;
if (asElement) {
span = document.createElement('span');
span.classList.add(cssClass);
const img = document.createElement('img');
img.src = imgSrc.replace(toReplaceImg, safeReplace);
span.appendChild(img);
} else {
span = '<span class="' + cssClass + '"><img src="' +
imgSrc.replace(toReplaceImg, safeReplace) + '"></span>';
}
if (spriteCache[uri] === undefined) {
/* get it in the DOM so CSS rules apply */
var jq = $(span).appendTo(dummyElement);
if (jq.children('img').css('display') === 'none') {
const {
backgroundImage,
backgroundPositionX,
backgroundPositionY,
width,
height
} = window.getComputedStyle(jq[0], ':before');
spriteCache[uri] = {
uri: parseUriFromCss(backgroundImage),
x: -parseFloat(backgroundPositionX, 10),
y: -parseFloat(backgroundPositionY, 10),
width: parseFloat(width, 10),
height: parseFloat(height, 10)
};
} else {
spriteCache[uri] = false;
}
jq.detach();
}
return span;
}
function parseUriFromCss(css) {
const match = URL_REGEX.exec(css);
if (match) { return match[1]; }
return css;
}
/**
* @param {string} uri
* @returns {Promise.<object>}
*/
function toImageMetrics(uri) {
let metrics = imageCache[uri];
if (metrics) { return Promise.resolve(metrics); }
return preloadImage(uri)
.then((img) => {
metrics = { uri, x: 0, y: 0, width: img.width, height: img.height };
imageCache[uri] = metrics;
return metrics;
});
}
function cachedRequire(id) {
return requireCache[id] || (requireCache[id] = doRequire(id));
}
////////////////////////////////////////////////////////////////
// Exports
////////////////////////////////////////////////////////////////
/**
* Preload the default spritesheet as well as the theme spritesheet.
*
* @private
* @returns {Promise}
*/
exports.$preloadSpritesheets = function () {
var themeName = getThemeName();
return Promise.all([
cachedRequire(defaultSpritesheet),
themeName && cachedRequire('css!nmodule/theme' + themeName + '/sprite/sprite')
]);
};
/**
* Convert the URI to a usable CSS class, expected to be represented in the
* spritesheet CSS for that module.
*
* @param {String} uri
* @returns {String}
*/
exports.toCssClass = function (uri) {
let cssClass = cssClassCache[uri];
if (!cssClass) {
cssClass = cssClassCache[uri] = 'icon-' + toUri(false, uri)
.replace(moduleDir, '')
.replace(toRemoveQueryParameters, '')
.replace(fileExtension, '')
.replace(toReplaceCss, safeReplace);
}
return cssClass;
};
/**
* Given an icon value (string, `baja.Icon`, etc), convert it into a usable
* HTML snippet.
*
* The HTML will consist of a one or more `span` tags. Each `span` will have
* one `img` element. If the icon is accounted for in a spritesheet, the
* `img` tag will be hidden and the icon will be represented solely by the
* `span` using pure CSS. If the icon is not in a spritesheet, the `img`
* tag will be shown and have its `src` tag set to the raw icon image.
*
* @param {String|baja.Ord|Array.<String|baja.Ord>|baja.Simple} icon icon as
* ORD, URI, or array of the same; a baja.Icon; (as of 4.12) a `gx:Image`
* @param {boolean} [sync=false] set to true to wait for the image to finish
* loading (making it possible to query it for width/height) before the
* `toHtml` promise resolves
* @returns {Promise.<string>} promise to be resolved with a raw HTML string containing
* one or more `span` tags
*/
exports.toHtml = function (icon, sync) {
return toHtml(icon, sync, false)
.then((spans) => spans.join(''));
};
/**
* Just like `toHtml`, but resolves an array of raw Elements instead.
*
* @param {String|baja.Ord|Array.<String|baja.Ord>|baja.Simple} icon icon as
* ORD, URI, or array of the same; a baja.Icon; (as of 4.12) a `gx:Image`
* @param {boolean} [sync=false] set to true to wait for the image to finish
* loading (making it possible to query it for width/height) before the
* `toHtml` promise resolves
* @returns {Promise.<HTMLSpanElement[]>} to be resolved with an array of
* `span` elements
*/
exports.toElements = function (icon, sync) {
return toHtml(icon, sync, true);
};
function toHtml(icon, sync, asElements) {
return exports.$preloadSpritesheets()
.then(() => {
return Promise.all(exports.toUris(icon).map((uri) => {
const span = toSpan(uri, blank, asElements);
const isInSprite = spriteCache[uri];
if (!isInSprite) {
/* not accounted for in spritesheet - use raw icon */
if (sync) {
return preloadImage(uri)
.then(function () {
return toSpan(uri, uri, asElements);
});
}
return toSpan(uri, uri, asElements);
}
return span;
}));
});
}
/**
* Given an icon, calculate the metrics needed to paint the icon in a painting
* context such as a canvas.
*
* @param {String|baja.Ord|Array.<String|baja.Ord>|baja.Simple} icon icon as
* ORD, URI, or array of the same; a baja.Icon; (as of 4.12) a `gx:Image`
* @returns {Promise.<module:bajaux/icon/iconUtils~ImageMetrics>}
* @since Niagara 4.11
*/
exports.toImageMetrics = function (icon) {
return exports.$preloadSpritesheets()
.then(() => Promise.all(exports.toUris(icon).map((uri) => {
toSpan(uri, blank);
return spriteCache[uri] || toImageMetrics(uri);
})));
};
/**
* Convert a value to an array of image URIs.
*
* @param {String|baja.Ord|Array.<String|baja.Ord>|baja.Simple} icon a string
* or array of strings. Each string can be a URI directly, or a `module://`
* ORD. These will be converted to URIs to image files. If passing in
* arbitrary ORDs, it's recommended to relativizeToSession() first. Can also
* be a `baja.Icon` or (as of 4.12) a `gx:Image`
*
* @returns {Array.<String>} array of image URIs
* @throws {Error} if invalid input given
*/
exports.toUris = function (icon) {
var arr, iconIsOrd = isOrd(icon);
if (icon && typeof icon.getImageUris === 'function') {
return icon.getImageUris();
} else if (Array.isArray(icon)) {
arr = icon;
} else if (typeof icon === 'string' || iconIsOrd) {
arr = String(icon).split('\n');
} else {
throw new Error('string, array, Icon, or Ord required');
}
return arr.map(toUri.bind(null, iconIsOrd));
};
/**
* Metrics needed to paint an icon in a painting context such as a canvas.
*
* @typedef module:bajaux/icon/iconUtils~ImageMetrics
* @property {string} uri URI of the image to paint
* @property {number} x pixels from left edge of the image
* @property {number} y pixels from top edge of the image
* @property {number} width width in pixels
* @property {number} height height in pixels
*/
return exports;
});