diff --git a/.gitignore b/.gitignore index b5b56cf..ac1e8f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules -public-min *.log .DS_Store diff --git a/app/main.js b/app/main.js new file mode 100644 index 0000000..54ceca7 --- /dev/null +++ b/app/main.js @@ -0,0 +1,49 @@ +var $ = require('jquery'); +var Arrow = require('./models/arrow'); +var ArrowConfigurationView = require('./views/arrow_configuration_view'); +var ArrowPreviewView = require('./views/arrow_preview_view'); +var ArrowCSSView = require('./views/arrow_css_view'); + +/** +@class App +@constructor +@description + Main application object. + Acts as view dispatcher +**/ +var App = function () { + this.init.apply(this, arguments); +}; + +App.prototype = { + + init: function () { + this.model = new Arrow(); + this._initViews(); + }, + + /** + @method _initViews + @description initializes views + @protected + **/ + _initViews: function () { + var model = this.model; + + this.views = [ + new ArrowConfigurationView({ model: model, container: $('.configuration') }), + new ArrowPreviewView({ model: model, container: $('').appendTo('body') }), + new ArrowCSSView({ model: model, container: $('.css_result') }), + ]; + }, + + render: function () { + $.each(this.views, function (i, view) { + view.render(); + }); + } + +}; + +new App().render(); + diff --git a/app/models/arrow.js b/app/models/arrow.js new file mode 100644 index 0000000..56aef31 --- /dev/null +++ b/app/models/arrow.js @@ -0,0 +1,291 @@ +var $ = require('jquery'); + +/** +@class Arrow +@constructor +**/ +var Arrow = function () { + this.init.apply(this, arguments); +}; + +Arrow.prototype = { + + init: function () { + // jquerify 'this' + this._$self = $(this); + + this._createAttrs(); + }, + + /** + @method invertedPosition + @description + returns the opposite of the position + so 'top' becomes 'bottom' and 'left' becomes 'right' + @returns {String} + **/ + invertedPosition: function () { + var pos = this.get('position'); + + if ( pos === 'top' ) return 'bottom'; + else if ( pos === 'bottom') return 'top'; + else if ( pos === 'left' ) return 'right'; + else if ( pos === 'right' ) return 'left'; + }, + + /** + @method hexToRGB + @description + returns an rgb color from an hex color + @returns {Array} + **/ + hexToRGB: function (h) { + if ( typeof h !== 'string' || !h.match(/^#([0-9A-F]{3}$)|([0-9A-F]{6}$)/i) ) return [0, 0, 0]; + else if ( h.match(/^(#[0-9a-f]{3})$/i) ) h = '#' + h[1] + h[1] + h[2] + h[2] + h[3] + h[3]; + var rgb = [], + i = 1; + + for(; i < 6; i+=2) { + rgb.push(parseInt(h.substring(i, i + 2), 16)); + } + return rgb; + }, + + /** + @method _baseCSS + @description generates the base css + @returns {String} css + @protected + **/ + _baseCSS: function () { + var pos = this.get('position'), + iPos = this.invertedPosition(), + color = this.get('color'), + borderWidth = this.get('borderWidth'), + borderColor = this.get('borderColor'), + hasBorder = borderWidth > 0, + css = '.arrow_box {\n'; + + color = this._sanitizeHexColors(color); + + css += '\tposition: relative;\n'; + css += '\tbackground: ' + color + ';\n'; + + if (hasBorder) { + borderColor = this._sanitizeHexColors(borderColor); + css += '\tborder: ' + borderWidth + 'px solid ' + borderColor + ';\n'; + } + + css += '}\n'; + css += '.arrow_box:after'; + + if (hasBorder) css += ', .arrow_box:before {\n'; + else css += ' {\n'; + + css += '\t' + iPos +': 100%;\n'; + + if (pos === 'top' || pos === 'bottom') { + css += '\tleft: 50%;\n'; + } + else { + css += '\ttop: 50%;\n'; + } + + css += '\tborder: solid transparent;\n'; + css += '\tcontent: "";\n'; + css += '\theight: 0;\n'; + css += '\twidth: 0;\n'; + css += '\tposition: absolute;\n'; + css += '\tpointer-events: none;\n'; + + if (hasBorder) css += '}\n'; + + return css; + }, + + /** + @method _arrowCSS + @description generates arrow css + @param {String} color the color of the arrow + @param {Integer} size the size of the arrow + @param {String} layer :after or :before (defaults to :after) + @returns {String} css + @protected + **/ + _arrowCSS: function (color, size, layer) { + color = this._sanitizeHexColors(color); + var pos = this.get('position'), + iPos = this.invertedPosition(), + rgbColor = this.hexToRGB(color), + borderWidth = this.get('borderWidth'), + css = ""; + + layer = layer || 'after'; + + if(borderWidth > 0) css += '.arrow_box:' + layer + ' {\n'; + + css += '\tborder-color: rgba(' + rgbColor.join(', ') + ', 0);\n'; + css += '\tborder-' + iPos + '-color: ' + color + ';\n'; + css += '\tborder-width: ' + size + 'px;\n'; + + if (pos === 'top' || pos === 'bottom') { + css += '\tmargin-left: -' + size + 'px;\n'; + } + else { + css += '\tmargin-top: -' + size + 'px;\n'; + } + + css += '}'; + + return css; + }, + + /** + @method _baseArrowCSS + @description generates the base arrow + @returns {String} css + @protected + **/ + _baseArrowCSS: function () { + return this._arrowCSS( + this.get('color'), + this.get('size'), + 'after' + ); + }, + + /** + @method _arrowBorderCSS + @description generates the border arrow + @returns {String} css + @protected + **/ + _arrowBorderCSS: function () { + var css = '', + borderWidth = this.get('borderWidth'); + + if (borderWidth > 0) { + css = this._arrowCSS( + this.get('borderColor'), + this.get('size') + Math.round(borderWidth * Math.sqrt(2)), // cos(PI/4) * 2 + 'before' + ); + } + + return css; + }, + + /** + @method toCSS + @description returns a CSS representation of the arrow + @returns {String} css + **/ + toCSS: function () { + + var css = [ + this._baseCSS(), + this._baseArrowCSS(), + this._arrowBorderCSS() + ]; + + return css.join(css[2] ? '\n':''); + }, + + /** + @method _createAttrs + @description creates attributes from the ATTR constant + @protected + **/ + _createAttrs: function () { + var ATTRS = Arrow.ATTRS, + attributes = {}; + + $.each(ATTRS, function (attr, value) { + attributes[attr] = value; + }); + + this._attributes = attributes; + }, + + /** + @method _sanitizeHexColors + @description prefix hexcolors with # if necessary + @returns {String} h + @protected + **/ + _sanitizeHexColors: function(h) { + return (h.charAt(0)==='#')?h:'#' + h; + }, + + /** + @method getAttrs + @description returns all the attributes + @returns {Object} all the model attributes + **/ + getAttrs: function () { + return this._attributes; + }, + + /** + @method get + @description returns the provided attribute + @param {String} attr the attribute to return + @returns {?} the attribute + **/ + get: function (attr) { + return this._attributes[attr]; + }, + + /** + @method set + @description updates the provided attribute + @param {String} attr the attribute to update + @param {?} val the value to update with + **/ + set: function (attr, val) { + if (!(attr in this._attributes)) return; + + this._attributes[attr] = val; + this.fire('change'); + }, + + /** + @method on + @description adds event listeners + @note uses jQuery custom events under the hood + @param {String} evType the event type + @param {Function} callback the event handler + @param {Object} context the 'this' for the callback + **/ + on: function (evType, callback, context) { + var $self = this._$self; + + $self.on( + evType, + $.proxy(callback, context || this) + ); + }, + + /** + @method fire + @description trigger event + @note uses jQuery custom events under the hood + @param {String} evType the event type + **/ + fire: function (evType) { + var $self = this._$self; + + $self.trigger(evType); + } + +}; + +Arrow.ATTRS = { + position: 'top', + size: 30, + color: '#88b7d5', + borderWidth: 4, + borderColor: '#c2e1f5' +}; + +module.exports = Arrow; diff --git a/app/views/arrow_configuration_view.js b/app/views/arrow_configuration_view.js new file mode 100644 index 0000000..93595c6 --- /dev/null +++ b/app/views/arrow_configuration_view.js @@ -0,0 +1,108 @@ +var $ = require('jquery'); + +/** +@class ArrowConfigurationView +@constructor +**/ +var ArrowConfigurationView = function () { + this.init.apply(this, arguments); +}; + +ArrowConfigurationView.prototype = { + + init: function (options) { + this.container = options.container; + this.model = options.model; + + this._attachEvents(); + }, + + /** + @method render + @chainable + **/ + render: function () { + this._setDefaults(); + return this; + }, + + /** + @method _setDetaults + @description update the view with the model defaults + **/ + _setDefaults: function () { + var container = this.container, + model = this.model; + + container.find('.position').val([ model.get('position') ]); + container.find('.size').val( model.get('size') ); + container.find('.base_color').val( model.get('color') ); + container.find('.border_width').val( model.get('borderWidth') ); + container.find('.border_color').val( model.get('borderColor') ); + }, + + /** + @method _attachEvents + @description attaches dom events + @protected + **/ + _attachEvents: function () { + var _updateModelProxy = this._updateModel.bind(this), + _updateInputProxy = this._updateInput.bind(this), + container = this.container, + selectors = [ { classname: '.position', keyboard_interactive: false }, + { classname: '.size', keyboard_interactive: true }, + { classname: '.base_color', keyboard_interactive: false }, + { classname: '.border_width', keyboard_interactive: true }, + { classname: '.border_color', keyboard_interactive: false } + ]; + + selectors.forEach(function (selector, i) { + container.delegate(selector.classname, 'change', _updateModelProxy); + if (selector.keyboard_interactive) { + container.delegate(selector.classname, 'keydown', _updateInputProxy); + } + }); + }, + + _updateModel: function (ev) { + var target = $(ev.currentTarget), + val = target.val(), + attr; + + + if (target.hasClass('border_width')) { + attr = 'borderWidth'; + } + else if (target.hasClass('border_color')) { + attr = 'borderColor'; + } + else if (target.hasClass('base_color')) { + attr = 'color'; + } + else { + attr = target.attr('class'); + } + + if (attr === 'borderWidth' || attr === 'size') val = parseInt(val, 10); + this.model.set(attr, val); + }, + + _updateInput: function (ev) { + if (ev.keyCode != 38 && ev.keyCode != 40) return; + + var target = $(ev.currentTarget), + val = parseInt(target.val()), + increment = ev.keyCode == 38 ? 1 : -1, + multiply = ev.shiftKey ? 10 : 1, + newVal = val + increment * multiply; + + if (newVal < 0) newVal = 0; + + target.val(newVal); + this._updateModel(ev); + } + +}; + +module.exports = ArrowConfigurationView; diff --git a/app/views/arrow_css_view.js b/app/views/arrow_css_view.js new file mode 100644 index 0000000..efe89ce --- /dev/null +++ b/app/views/arrow_css_view.js @@ -0,0 +1,44 @@ +/** +@class ArrowCSSView +@constructor +**/ +var ArrowCSSView = function () { + this.init.apply(this, arguments); +}; + +ArrowCSSView.prototype = { + + init: function (options) { + this.container = options.container; + this._codeNode = this.container.find('.code'); + this._copyNode = this.container.find('.copy_code'); + + this.model = options.model; + this.model.on('change', this._handleChange, this); + }, + + /** + @method _handleChange + @description handles changes to the model + @chainable + **/ + _handleChange: function () { + this.render(); + }, + + /** + @method render + @description renders the model's css + @chainable + **/ + render: function () { + var css = this.model.toCSS(); + + this._codeNode.text( css ); + + return this; + } + +}; + +module.exports = ArrowCSSView; diff --git a/app/views/arrow_preview_view.js b/app/views/arrow_preview_view.js new file mode 100644 index 0000000..61d7941 --- /dev/null +++ b/app/views/arrow_preview_view.js @@ -0,0 +1,39 @@ +/** +@class ArrowPreviewView +@constructor +**/ +var ArrowPreviewView = function () { + this.init.apply(this, arguments); +}; + +ArrowPreviewView.prototype = { + + init: function (options) { + this.container = options.container; + this.model = options.model; + + this.model.on('change', this._handleChange, this); + }, + + /** + @method _handleChange + @description handles changes to the model + @chainable + **/ + _handleChange: function () { + this.render(); + }, + + /** + @method render + @description renders the css to style the preview + @chainable + **/ + render: function () { + this.container.text( this.model.toCSS() ); + return this; + } + +}; + +module.exports = ArrowPreviewView; diff --git a/bin/server b/bin/server deleted file mode 100644 index f1ef1b5..0000000 --- a/bin/server +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env node - -var connect = require('connect'), - http = require('http'), - port = process.env.PORT || 3000, - static; - -if (process.argv.indexOf('--development') !== -1) { - console.log('CSS Arrow Please in development on http://localhost:' + port); - static = connect.static('public'); -} -else { - static = connect.static('public-min', { - maxAge: 365 * 24 * 60 * 60 * 1000 - }); -} - -http.createServer( connect().use( static ) ).listen( port ); diff --git a/package.json b/package.json index c458abe..0686635 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "cssarrowplease", - "version": "0.2.2-16", + "version": "0.2.2-19", "author": { "name": "Simon Højberg", "email": "r.hackr@gmail.com" }, "description": "Generate the CSS for a tooltip arrow", - "main": "", "repository": { "type": "git", "url": "git://github.com/hojberg/cssarrowplease.git" @@ -17,16 +16,22 @@ ], "homepage": "http://cssarrowplease.com/", "scripts": { - "start": "node ./bin/server" + "start": "node ./server.js", + "build": "./node_modules/browserify/bin/cmd.js -e app/main.js -o public/js/cssarrowplease.js", + "test": "mocha --require=test/test_helper.js" }, "dependencies": { - "connect": "~2.0.3" - }, - "devDependencies": { - "assetgraph-builder": ">=0.2.37" + "connect": "~2.0.3", + "browserify": "latest", + "jquery": "latest", + "jsdom": "latest", + "mocha": "latest", + "chai": "latest", + "sinon": "latest", + "sinon-chai": "latest" }, "engines": { - "node": ">=0.6" + "node": ">=8.1.4" }, "licenses": [ { @@ -41,4 +46,4 @@ "cssarrowplease.com", "www.cssarrowplease.com" ] -} \ No newline at end of file +} diff --git a/public/css/app.css b/public/css/app.css index ea7a844..8a96c1a 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -12,7 +12,7 @@ input[type='radio'] { border: 0; } /* =LAYOUT ====================================================== */ -#content { width: 800px; margin: auto; padding: 100px; padding-bottom: 60px; +#content { width: 800px; margin: auto; padding: 50px; padding-bottom: 60px; /* white radial gradient background */ background: -moz-radial-gradient(center, ellipse cover, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 70%); background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(255,255,255,0.5)), color-stop(70%,rgba(255,255,255,0))); @@ -23,21 +23,19 @@ input[type='radio'] { border: 0; } } #footer { padding-top: 10px; font-size: 13px; color: rgba(255, 255, 255, 0.7); text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); border-top: 1px solid #797f85; border-top-color: rgba(0, 0, 0, 0.15); box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.2); vertical-align: top; text-align: center; } #footer a { vertical-align: top; color: #fff; } +#footer span { vertical-align: top;} .clr:after { clear:both; content:"."; display: block; height:0; visibility: hidden; line-height:0; font-size:0; } .ir { border: 0; font: 0/0 a; text-shadow: none; color: transparent; background-color: transparent; } +.description { margin-bottom: 50px; font-size: 16px; text-align: center; } + .preview_and_configuration { float: left; width: 395px; } /* =MODULES ====================================================== */ /* preview */ -.arrow_box { padding: 40px; width: 280px; height: 100px; border-radius: 6px; - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - - -webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.3)); - filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.3)); -} +.arrow_box { padding: 40px; width: 280px; height: 100px; border-radius: 6px; } /* logo */ .logo { color: #ddf8c6; text-align: center; font-size: 54px; line-height: 54px; font-weight: bold; text-transform: uppercase; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); } @@ -59,8 +57,6 @@ input[type='radio'] { border: 0; } /* css_result */ .css_result { position: relative; float: right; width: 402px; } .css_result .code { white-space: pre; padding: 10px; display: block; width: 380px; font-size: 12px; font-family: 'Courier new'; font-weight: bold; background: #8c9196; background: rgba(0, 0, 0, 0.15); border-radius: 4px; color: #fff; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); border: 1px solid #696d72; border-color: rgba(0, 0, 0, 0.2); box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3), inset 0 1px 5px rgba(0, 0, 0, 0.1); } -.css_result .copy_code { position: absolute; bottom: 5px; right: 10px; width: 14px; height: 22px; background: url(../img/clippy.png) no-repeat 0 4px; } - /* fork_me */ .fork_me { position: absolute; top: 0; right: 0; display: block; width: 149px; height: 149px; background: url(../img/fork.png); } diff --git a/public/index.html b/public/index.html index 413f523..196123e 100644 --- a/public/index.html +++ b/public/index.html @@ -3,12 +3,16 @@