diff --git a/.gitignore b/.gitignore index 6740b78..ac1e8f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules -public-min *.log -.DS_Store \ No newline at end of file +.DS_Store diff --git a/README.md b/README.md index 5c1b24d..5595316 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,9 @@ Generate the CSS for a tooltip arrow. Check it out at http://cssarrowplease.com +## Contributing +You can simply open the public/index.html file in your browser +or start the development node app (not required): `node bin/server --development` + ## License CSSArrowPlease is Copyright © 2012 Simon Højberg. CSSArrowPlease is free software, and may be redistributed under the terms specified in the LICENSE file. 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 2c1d805..0000000 --- a/bin/server +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env node - -var connect = require('connect'), - http = require('http'); - -var app = connect().use(connect.static('public-min')); - -http.createServer(app).listen(process.env.PORT || 3000); diff --git a/package.json b/package.json index 8ed7a85..0686635 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "cssarrowplease", - "version": "0.2.2-3", + "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": "latest" + "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/clippy.swf b/public/clippy.swf new file mode 100755 index 0000000..e46886c Binary files /dev/null and b/public/clippy.swf differ diff --git a/public/css/app.css b/public/css/app.css index 522a871..8a96c1a 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -2,16 +2,17 @@ ====================================================== */ html, body { background: url(../img/noisebg.png); } body { font-size: 18px; font-family: 'Terminal Dosis', sans-serif; padding: 0; margin: 0; color: #fff; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); } -a { color: rgba(255, 255, 255, 0.7); } +a { color: #fff; color: rgba(255, 255, 255, 0.7); } ul, ol, form { margin: 0; padding: 0; } ul, ol { list-style-type: none; } h1 { margin: 0; padding: 0; } -h2 { margin: 0; margin-bottom: 10px; padding: 0; font-weight: normal; font-size: 30px; color: rgba(0, 0, 0, 0.4); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); } +h2 { margin: 0; margin-bottom: 10px; padding: 0; font-weight: normal; font-size: 30px; color: #626569; color: rgba(0, 0, 0, 0.4); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); } input { font-size: 14px; border: 1px solid #777; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(255, 255, 255, 0.3); border-radius: 4px; padding: 3px; -webkit-background-clip: padding-box; } +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))); @@ -19,21 +20,25 @@ input { font-size: 14px; border: 1px solid #777; box-sha background: -o-radial-gradient(center, ellipse cover, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 70%); background: -ms-radial-gradient(center, ellipse cover, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 70%); background: radial-gradient(center, ellipse cover, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 70%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#80ffffff', endColorstr='#00ffffff',GradientType=1 ); } -#footer { text-align: center; 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 rgba(0, 0, 0, 0.15); box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.2); vertical-align: top; } +#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; } -.clr { overflow: hidden; } +#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; box-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); } +.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); } /* configuration */ .configuration { margin-top: 20px; } @@ -43,11 +48,15 @@ input { font-size: 14px; border: 1px solid #777; box-sha .configuration .section label { display: inline-block; width: 112px; } .configuration .size, -.configuration .border_width { width: 28px; text-align: right; } +.configuration .border_width { width: 28px; text-align: right; } + +.configuration .color { width: 65px; text-align: center } -.configuration .color { width: 65px; text-align: center } +.configuration .unit { font-size: 14px; color: rgba(0, 0, 0, 0.4); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); margin-left: 5px; } -.configuration .unit { font-size: 14px; color: rgba(0, 0, 0, 0.4); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); margin-left: 5px; } +/* 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); } -/* result_code */ -.result_code { position: relative; white-space: pre; padding: 10px; float: right; width: 380px; font-size: 12px; font-family: 'Courier new'; font-weight: bold; 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 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); } +/* 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/img/clippy.png b/public/img/clippy.png new file mode 100644 index 0000000..7a462e1 Binary files /dev/null and b/public/img/clippy.png differ diff --git a/public/img/fork.png b/public/img/fork.png new file mode 100644 index 0000000..aba9b39 Binary files /dev/null and b/public/img/fork.png differ diff --git a/public/index.html b/public/index.html index 679af2a..196123e 100644 --- a/public/index.html +++ b/public/index.html @@ -3,14 +3,16 @@