From e1582b10ccedcf25a2d55aa9fa61502b3acbf7ef Mon Sep 17 00:00:00 2001 From: Silvio Montanari Date: Sun, 7 Jul 2013 16:59:44 +1000 Subject: [PATCH] using less for mifos custom styles. using require css and less plugins to dynamically load styles --- index.html | 4 - js/loader.js | 42 +- js/mifosXComponents.js | 41 +- js/mifosXStyles.js | 16 + lib/require-css/.gitignore | 1 + lib/require-css/css-builder.js | 251 ++ lib/require-css/css.js | 435 +++ lib/require-css/normalize.js | 138 + lib/require-less/.gitignore | 1 + lib/require-less/less-builder.js | 42 + lib/require-less/less.js | 46 + lib/require-less/lessc-server.js | 96 + lib/require-less/lessc.js | 4441 ++++++++++++++++++++++++++++++ stylesheets/mifosX.css | 24 - stylesheets/mifosX.less | 26 + test/functional/scenarioTest.js | 2 +- 16 files changed, 5541 insertions(+), 65 deletions(-) create mode 100644 js/mifosXStyles.js create mode 100644 lib/require-css/.gitignore create mode 100644 lib/require-css/css-builder.js create mode 100644 lib/require-css/css.js create mode 100644 lib/require-css/normalize.js create mode 100644 lib/require-less/.gitignore create mode 100644 lib/require-less/less-builder.js create mode 100644 lib/require-less/less.js create mode 100644 lib/require-less/lessc-server.js create mode 100644 lib/require-less/lessc.js delete mode 100644 stylesheets/mifosX.css create mode 100644 stylesheets/mifosX.less diff --git a/index.html b/index.html index de0f0135..1ff43c24 100644 --- a/index.html +++ b/index.html @@ -4,10 +4,6 @@ MifosX prototype - - - -
diff --git a/js/loader.js b/js/loader.js index 7dd70026..4ac99abe 100644 --- a/js/loader.js +++ b/js/loader.js @@ -6,35 +6,37 @@ 'angular-mocks': '../lib/angular/angular-mocks', 'underscore': '../lib/underscore/underscore', 'webstorage': '../lib/angular-webstorage', - 'test': '../test/functional' + 'require-css': '../lib/require-css', + 'require-less': '../lib/require-less', + 'styles': '../stylesheets', + 'test': '../test/functional', }, shim: { - 'angular': { - exports: 'angular' - }, - 'angular-resource': { - deps: ['angular'] - }, - 'webstorage': { - deps: ['angular'] - }, + 'angular': { exports: 'angular' }, + 'angular-resource': { deps: ['angular'] }, + 'angular-mocks': { deps: ['angular'] }, + 'webstorage': { deps: ['angular'] }, 'mifosX': { deps: ['angular', 'angular-resource', 'webstorage'], exports: 'mifosX' } }, + packages: [ + { + name: 'css', + location: '../lib/require-css', + main: 'css' + }, + { + name: 'less', + location: '../lib/require-less', + main: 'less' + } + ] }); - require(['mifosXComponents', 'mifosX', 'underscore'], function(components) { - var dependencies = _.reduce(_.keys(components), function(list, group) { - return list.concat(_.map(components[group], function(name) { return group + "/" + name; })); - }, [ - 'test/testInitializer', - 'routes', - 'webstorage-configuration' - ]); - - require(dependencies, function(testMode) { + require(['mifosXComponents', 'mifosXStyles'], function() { + require(['test/testInitializer'], function(testMode) { if (!testMode) { angular.bootstrap(document, ["MifosX_Application"]); } diff --git a/js/mifosXComponents.js b/js/mifosXComponents.js index 0edf3d58..8d7fb280 100644 --- a/js/mifosXComponents.js +++ b/js/mifosXComponents.js @@ -1,17 +1,26 @@ -define({ - models: [ - 'User', - 'roleMap' - ], - controllers: [ - 'MainController', - 'LoginFormController' - ], - services: [ - 'ResourceFactoryProvider', - 'HttpServiceProvider', - 'AuthenticationService', - 'SessionManager' - ], - directives: [] +define(['underscore', 'mifosX'], function() { + var components = { + models: [ + 'User', + 'roleMap' + ], + controllers: [ + 'MainController', + 'LoginFormController' + ], + services: [ + 'ResourceFactoryProvider', + 'HttpServiceProvider', + 'AuthenticationService', + 'SessionManager' + ], + directives: [] + }; + + require(_.reduce(_.keys(components), function(list, group) { + return list.concat(_.map(components[group], function(name) { return group + "/" + name; })); + }, [ + 'routes', + 'webstorage-configuration' + ])); }); diff --git a/js/mifosXStyles.js b/js/mifosXStyles.js new file mode 100644 index 00000000..1e5a570b --- /dev/null +++ b/js/mifosXStyles.js @@ -0,0 +1,16 @@ +define(['underscore'], function() { + var styles = { + css: [ + 'skeleton/base', + 'skeleton/skeleton', + 'skeleton/layout' + ], + less: [ + 'mifosX' + ] + }; + + require(_.reduce(_.keys(styles), function(list, pluginName) { + return list.concat(_.map(styles[pluginName], function(stylename) { return pluginName + "!styles/" + stylename; })); + }, [])); +}); diff --git a/lib/require-css/.gitignore b/lib/require-css/.gitignore new file mode 100644 index 00000000..777c0b92 --- /dev/null +++ b/lib/require-css/.gitignore @@ -0,0 +1 @@ +example/www-built diff --git a/lib/require-css/css-builder.js b/lib/require-css/css-builder.js new file mode 100644 index 00000000..093d5b8d --- /dev/null +++ b/lib/require-css/css-builder.js @@ -0,0 +1,251 @@ +define(['require', './normalize'], function(req, normalize) { + var nodePrint = function() {}; + if (requirejs.tools) + requirejs.tools.useLib(function(req) { + req(['node/print'], function(_nodePrint) { + nodePrint = _nodePrint; + }, function(){}); + }); + + var cssAPI = {}; + + function compress(css) { + if (typeof process !== "undefined" && process.versions && !!process.versions.node && require.nodeRequire) { + try { + var csso = require.nodeRequire('csso'); + var csslen = css.length; + css = csso.justDoIt(css); + nodePrint('Compressed CSS output to ' + Math.round(css.length / csslen * 100) + '%.'); + return css; + } + catch(e) { + nodePrint('Compression module not installed. Use "npm install csso -g" to enable.'); + return css; + } + } + nodePrint('Compression not supported outside of nodejs environments.'); + return css; + } + + //load file code - stolen from text plugin + function loadFile(path) { + if (typeof process !== "undefined" && process.versions && !!process.versions.node && require.nodeRequire) { + var fs = require.nodeRequire('fs'); + var file = fs.readFileSync(path, 'utf8'); + if (file.indexOf('\uFEFF') === 0) + return file.substring(1); + return file; + } + else { + var file = new java.io.File(path), + lineSeparator = java.lang.System.getProperty("line.separator"), + input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), 'utf-8')), + stringBuffer, line; + try { + stringBuffer = new java.lang.StringBuffer(); + line = input.readLine(); + if (line && line.length() && line.charAt(0) === 0xfeff) + line = line.substring(1); + stringBuffer.append(line); + while ((line = input.readLine()) !== null) { + stringBuffer.append(lineSeparator).append(line); + } + return String(stringBuffer.toString()); + } + finally { + input.close(); + } + } + } + + + function saveFile(path, data) { + if (typeof process !== "undefined" && process.versions && !!process.versions.node && require.nodeRequire) { + var fs = require.nodeRequire('fs'); + fs.writeFileSync(path, data, 'utf8'); + } + else { + var content = new java.lang.String(data); + var output = new java.io.BufferedWriter(new java.io.OutputStreamWriter(new java.io.FileOutputStream(path), 'utf-8')); + + try { + output.write(content, 0, content.length()); + output.flush(); + } + finally { + output.close(); + } + } + } + + //when adding to the link buffer, paths are normalised to the baseUrl + //when removing from the link buffer, paths are normalised to the output file path + function escape(content) { + return content.replace(/(["'\\])/g, '\\$1') + .replace(/[\f]/g, "\\f") + .replace(/[\b]/g, "\\b") + .replace(/[\n]/g, "\\n") + .replace(/[\t]/g, "\\t") + .replace(/[\r]/g, "\\r"); + } + + // NB add @media query support for media imports + var importRegEx = /@import\s*(url)?\s*(('([^']*)'|"([^"]*)")|\(('([^']*)'|"([^"]*)"|([^\)]*))\))\s*;?/g; + + var loadCSSFile = function(fileUrl) { + var css = loadFile(fileUrl); + + // normalize the css (except import statements) + css = normalize(css, fileUrl, baseUrl, cssBase); + + // detect all import statements in the css and normalize + var importUrls = []; + var importIndex = []; + var importLength = []; + var match; + while (match = importRegEx.exec(css)) { + var importUrl = match[4] || match[5] || match[7] || match[8] || match[9]; + + // normalize import url + if (importUrl.substr(importUrl.length - 5, 5) != '.less' && importUrl.substr(importUrl.length - 4, 4) != '.css') + importUrl += '.css'; + + // contains a protocol + if (importUrl.match(/:\/\//)) + continue; + + // relative to css base + if (importUrl.substr(0, 1) == '/' && cssBase) + importUrl = cssBase + importUrl; + else + importUrl = baseUrl + importUrl; + + importUrls.push(importUrl); + importIndex.push(importRegEx.lastIndex - match[0].length); + importLength.push(match[0].length); + } + + // load the import stylesheets and substitute into the css + for (var i = 0; i < importUrls.length; i++) + (function(i) { + var importCSS = loadCSSFile(importUrls[i]); + css = css.substr(0, importIndex[i]) + importCSS + css.substr(importIndex[i] + importLength[i]); + var lenDiff = importCSS.length - importLength[i]; + for (var j = i + 1; j < importUrls.length; j++) + importIndex[j] += lenDiff; + })(i); + + return css; + } + + + var baseUrl; + var cssBase; + var curModule; + cssAPI.load = function(name, req, load, config, parse) { + if (!baseUrl) + baseUrl = config.baseUrl; + + if (!cssBase) + cssBase = config.cssBase; + + if (config.modules) { + //run through the module list - the first one without a layer set is the current layer we are in + //allows to track the current layer number for layer-specific config + for (var i = 0; i < config.modules.length; i++) + if (config.modules[i].layer === undefined) { + curModule = i; + break; + } + } + + //store config + cssAPI.config = cssAPI.config || config; + + name += !parse ? '.css' : '.less'; + + var fileUrl = req.toUrl(name); + + //external URLS don't get added (just like JS requires) + if (fileUrl.substr(0, 7) == 'http://' || fileUrl.substr(0, 8) == 'https://') + return; + + //add to the buffer + _cssBuffer[name] = loadCSSFile(fileUrl); + + // parse if necessary + if (parse) + _cssBuffer[name] = parse(_cssBuffer[name]); + + load(); + } + + cssAPI.normalize = function(name, normalize) { + if (name.substr(name.length - 4, 4) == '.css') + name = name.substr(0, name.length - 4); + return normalize(name); + } + + //list of cssIds included in this layer + var _layerBuffer = []; + var _cssBuffer = []; + cssAPI.write = function(pluginName, moduleName, write, parse) { + //external URLS don't get added (just like JS requires) + if (moduleName.substr(0, 7) == 'http://' || moduleName.substr(0, 8) == 'https://') + return; + + var resourceName = moduleName + (!parse ? '.css' : '.less'); + _layerBuffer.push(_cssBuffer[resourceName]); + + var separateCSS = false; + if (cssAPI.config.separateCSS) + separateCSS = true; + if (typeof curModule == 'number' && cssAPI.config.modules[curModule].separateCSS !== undefined) + separateCSS = cssAPI.config.modules[curModule].separateCSS; + if (separateCSS) + write.asModule(pluginName + '!' + moduleName, 'define(function(){})'); + else + write("requirejs.s.contexts._.nextTick = function(f){f()}; require(['css'], function(css) { css.addBuffer('" + resourceName + "'); }); requirejs.s.contexts._.nextTick = requirejs.nextTick;"); + } + + cssAPI.onLayerEnd = function(write, data, parser) { + firstWrite = true; + //separateCSS parameter set either globally or as a layer setting + var separateCSS = false; + if (cssAPI.config.separateCSS) + separateCSS = true; + if (typeof curModule == 'number' && cssAPI.config.modules[curModule].separateCSS !== undefined) + separateCSS = cssAPI.config.modules[curModule].separateCSS; + curModule = null; + + //calculate layer css + var css = _layerBuffer.join(''); + + if (separateCSS) { + nodePrint('Writing CSS! file: ' + data.name + '\n'); + + //calculate the css output path for this layer + var path = this.config.appDir ? this.config.baseUrl + data.name + '.css' : cssAPI.config.out.replace(/\.js$/, '.css'); + + //renormalize the css to the output path + var output = compress(normalize(css, baseUrl, path)); + + saveFile(path, output); + } + else { + if (css == '') + return; + //write the injection and layer index into the layer + //prepare the css + css = escape(compress(css)); + + //the code below overrides async require functionality to ensure instant buffer injection + write("requirejs.s.contexts._.nextTick = function(f){f()}; require(['css'], function(css) { css.setBuffer('" + css + (parser ? "', true" : "'") + "); }); requirejs.s.contexts._.nextTick = requirejs.nextTick; "); + } + + //clear layer buffer for next layer + _layerBuffer = []; + } + + return cssAPI; +}); diff --git a/lib/require-css/css.js b/lib/require-css/css.js new file mode 100644 index 00000000..40337c06 --- /dev/null +++ b/lib/require-css/css.js @@ -0,0 +1,435 @@ +/* + * Require-CSS RequireJS css! loader plugin + * Guy Bedford 2013 + * MIT + */ + +/* + * + * Usage: + * require(['css!./mycssFile']); + * + * NB leave out the '.css' extension. + * + * - Fully supports cross origin CSS loading + * - Works with builds + * + * Tested and working in (up to latest versions as of March 2013): + * Android + * iOS 6 + * IE 6 - 10 + * Chome 3 - 26 + * Firefox 3.5 - 19 + * Opera 10 - 12 + * + * browserling.com used for virtual testing environment + * + * Credit to B Cavalier & J Hann for the elegant IE 6 - 9 hack. + * + * Sources that helped along the way: + * - https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent + * - http://www.phpied.com/when-is-a-stylesheet-really-loaded/ + * - https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js + * + */ + +define(['./normalize'], function(normalize) { + function indexOf(a, e) { for (var i=0, l=a.length; i < l; i++) if (a[i] === e) return i; return -1 } + + if (typeof window == 'undefined') + return { load: function(n, r, load){ load() } }; + + // set to true to enable test prompts for device testing + var testing = false; + + var head = document.getElementsByTagName('head')[0]; + + var engine = window.navigator.userAgent.match(/Trident\/([^ ;]*)|AppleWebKit\/([^ ;]*)|Opera\/([^ ;]*)|rv\:([^ ;]*)(.*?)Gecko\/([^ ;]*)|MSIE\s([^ ;]*)/); + var hackLinks = false; + + if (!engine) {} + else if (engine[1] || engine[7]) { + hackLinks = parseInt(engine[1]) < 6 || parseInt(engine[7]) <= 9; + engine = 'trident'; + } + else if (engine[2]) { + // unfortunately style querying still doesnt work with onload callback in webkit + hackLinks = true; + engine = 'webkit'; + } + else if (engine[3]) { + // engine = 'opera'; + } + else if (engine[4]) { + hackLinks = parseInt(engine[4]) < 18; + engine = 'gecko'; + } + else if (testing) + alert('Engine detection failed'); + + //main api object + var cssAPI = {}; + + var absUrlRegEx = /^\/|([^\:\/]*:)/; + + cssAPI.pluginBuilder = './css-builder'; + + // used by layer builds to register their css buffers + + // the current layer buffer items (from addBuffer) + var curBuffer = []; + + // the callbacks for buffer loads + var onBufferLoad = {}; + + // the full list of resources in the buffer + var bufferResources = []; + + cssAPI.addBuffer = function(resourceId) { + // just in case layer scripts are included twice, also check + // against the previous buffers + if (indexOf(curBuffer, resourceId) != -1) + return; + if (indexOf(bufferResources, resourceId) != -1) + return; + curBuffer.push(resourceId); + bufferResources.push(resourceId); + } + cssAPI.setBuffer = function(css, isLess) { + var pathname = window.location.pathname.split('/'); + pathname.pop(); + pathname = pathname.join('/') + '/'; + + var baseParts = require.toUrl('base_url').split('/'); + baseParts.pop(); + var baseUrl = baseParts.join('/') + '/'; + baseUrl = normalize.convertURIBase(baseUrl, pathname, '/'); + if (!baseUrl.match(absUrlRegEx)) + baseUrl = '/' + baseUrl; + if (baseUrl.substr(baseUrl.length - 1, 1) != '/') + baseUrl = baseUrl + '/'; + + cssAPI.inject(normalize(css, baseUrl, pathname)); + + // set up attach callback if registered + // clear the current buffer for the next layer + // (just the less or css part as we have two buffers in one effectively) + for (var i = 0; i < curBuffer.length; i++) { + // find the resources in the less or css buffer dependening which one this is + if ((isLess && curBuffer[i].substr(curBuffer[i].length - 5, 5) == '.less') || + (!isLess && curBuffer[i].substr(curBuffer[i].length - 4, 4) == '.css')) { + (function(resourceId) { + // mark that the onBufferLoad is about to be called (set to true if not already a callback function) + onBufferLoad[resourceId] = onBufferLoad[resourceId] || true; + + // set a short timeout (as injection isn't instant in Chrome), then call the load + setTimeout(function() { + if (typeof onBufferLoad[resourceId] == 'function') + onBufferLoad[resourceId](); + // remove from onBufferLoad to indicate loaded + delete onBufferLoad[resourceId]; + }, 7); + })(curBuffer[i]); + + // remove the current resource from the buffer + curBuffer.splice(i--, 1); + } + } + } + cssAPI.attachBuffer = function(resourceId, load) { + // attach can happen during buffer collecting, or between injection and callback + // we assume it is not possible to attach multiple callbacks + // requirejs plugin load function ensures this by queueing duplicate calls + + // check if the resourceId is in the current buffer + for (var i = 0; i < curBuffer.length; i++) + if (curBuffer[i] == resourceId) { + onBufferLoad[resourceId] = load; + return true; + } + + // check if the resourceId is waiting for injection callback + // (onBufferLoad === true is a shortcut indicator for this) + if (onBufferLoad[resourceId] === true) { + onBufferLoad[resourceId] = load; + return true; + } + + // if it's in the full buffer list and not either of the above, its loaded already + if (indexOf(bufferResources, resourceId) != -1) { + load(); + return true; + } + } + + var webkitLoadCheck = function(link, callback) { + setTimeout(function() { + for (var i = 0; i < document.styleSheets.length; i++) { + var sheet = document.styleSheets[i]; + if (sheet.href == link.href) + return callback(); + } + webkitLoadCheck(link, callback); + }, 10); + } + + var mozillaLoadCheck = function(style, callback) { + setTimeout(function() { + try { + style.sheet.cssRules; + return callback(); + } catch (e){} + mozillaLoadCheck(style, callback); + }, 10); + } + + // ie link detection, as adapted from https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js + if (engine == 'trident' && hackLinks) { + var ieStyles = [], + ieQueue = [], + ieStyleCnt = 0; + var ieLoad = function(url, callback) { + var style; + ieQueue.push({ + url: url, + cb: callback + }); + style = ieStyles.shift(); + if (!style && ieStyleCnt++ < 12) { + style = document.createElement('style'); + head.appendChild(style); + } + ieLoadNextImport(style); + } + var ieLoadNextImport = function(style) { + var curImport = ieQueue.shift(); + if (!curImport) { + style.onload = noop; + ieStyles.push(style); + return; + } + style.onload = function() { + curImport.cb(curImport.ss); + ieLoadNextImport(style); + }; + var curSheet = style.styleSheet; + curImport.ss = curSheet.imports[curSheet.addImport(curImport.url)]; + } + } + + // uses the load method + var createLink = function(url) { + var link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; + link.href = url; + return link; + } + + var noop = function(){} + + cssAPI.linkLoad = function(url, callback) { + var timeout = setTimeout(function() { + if (testing) alert('timeout'); + callback(); + }, waitSeconds * 1000 - 100); + var _callback = function() { + clearTimeout(timeout); + if (link) + link.onload = noop; + // for style querying, a short delay still seems necessary + setTimeout(callback, 7); + } + if (!hackLinks) { + var link = createLink(url); + link.onload = _callback; + head.appendChild(link); + } + // hacks + else { + if (engine == 'webkit') { + var link = createLink(url); + webkitLoadCheck(link, _callback); + head.appendChild(link); + } + else if (engine == 'gecko') { + var style = document.createElement('style'); + style.textContent = '@import "' + url + '"'; + mozillaLoadCheck(style, _callback); + head.appendChild(style); + } + else if (engine == 'trident') + ieLoad(url, _callback); + } + } + + /* injection api */ + var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0']; + var fileCache = {}; + var get = function(url, callback, errback) { + if (fileCache[url]) { + callback(fileCache[url]); + return; + } + + var xhr, i, progId; + if (typeof XMLHttpRequest !== 'undefined') + xhr = new XMLHttpRequest(); + else if (typeof ActiveXObject !== 'undefined') + for (i = 0; i < 3; i += 1) { + progId = progIds[i]; + try { + xhr = new ActiveXObject(progId); + } + catch (e) {} + + if (xhr) { + progIds = [progId]; // so faster next time + break; + } + } + + xhr.open('GET', url, requirejs.inlineRequire ? false : true); + + xhr.onreadystatechange = function (evt) { + var status, err; + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + status = xhr.status; + if (status > 399 && status < 600) { + //An http 4xx or 5xx error. Signal an error. + err = new Error(url + ' HTTP status: ' + status); + err.xhr = xhr; + errback(err); + } + else { + fileCache[url] = xhr.responseText; + callback(xhr.responseText); + } + } + }; + + xhr.send(null); + } + //uses the