├── docs ├── .gitignore ├── demo.gif ├── drop.gif ├── hero.png ├── icon.gif └── panel.gif ├── wrap ├── end.frag └── start.frag ├── test ├── images │ ├── db.png │ ├── bird.png │ ├── box.png │ ├── pen.png │ ├── siatka10.png │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png └── main.js ├── src ├── jtop.js ├── offset.js ├── template.js ├── grid.js ├── item.js ├── class.js ├── tooltip.js ├── item.icon.js ├── text.js ├── drag.js ├── popupmenu.js ├── core.js ├── scrollview.js ├── tween.js └── item.panel.js ├── css ├── jtop.tooltip.icon.css ├── jtop.tooltip.info.css └── jtop.popupmenu.css ├── index.html ├── LICENSE ├── README.md ├── lib ├── domReady.js ├── signals.js └── underscore.js └── references └── photos.html /docs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /wrap/end.frag: -------------------------------------------------------------------------------- 1 | window.jtop = require('jtop'); 2 | }()); -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /docs/drop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/docs/drop.gif -------------------------------------------------------------------------------- /docs/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/docs/hero.png -------------------------------------------------------------------------------- /docs/icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/docs/icon.gif -------------------------------------------------------------------------------- /docs/panel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/docs/panel.gif -------------------------------------------------------------------------------- /test/images/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/test/images/db.png -------------------------------------------------------------------------------- /test/images/bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/test/images/bird.png -------------------------------------------------------------------------------- /test/images/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/test/images/box.png -------------------------------------------------------------------------------- /test/images/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/test/images/pen.png -------------------------------------------------------------------------------- /test/images/siatka10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/test/images/siatka10.png -------------------------------------------------------------------------------- /test/images/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/test/images/glyphicons-halflings.png -------------------------------------------------------------------------------- /test/images/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderitual/jtop/HEAD/test/images/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /wrap/start.frag: -------------------------------------------------------------------------------- 1 | /* 2 | * Jtop library v.1.0.1 3 | * JS library provides desktop gui functionality for web. 4 | * 5 | * skowronkow_at_gmail.com 6 | * http://coderitual.com 7 | */ 8 | 9 | (function() { -------------------------------------------------------------------------------- /src/jtop.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'require', 3 | 'exports', 4 | 'module', 5 | './item.icon', 6 | './item.panel', 7 | './popupmenu', 8 | './tooltip', 9 | './scrollview' 10 | ], 11 | 12 | function( 13 | require, 14 | exports, 15 | module 16 | ) { 17 | 18 | var core = require('./core'), 19 | PopupMenu = require('./popupmenu'); 20 | 21 | module.exports = { 22 | init: function(element, options) { return new core.Core(element, options); }, 23 | popupmenu: function(options) { return new PopupMenu(options); } 24 | } 25 | }); -------------------------------------------------------------------------------- /css/jtop.tooltip.icon.css: -------------------------------------------------------------------------------- 1 | .jt-tooltip { 2 | margin: 10px; 3 | box-shadow: 0px 0px 5px #000; 4 | width: 210px; 5 | font-family: Tahoma, Arial; 6 | font-size: 11px; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | background-color: #fff; 11 | } 12 | 13 | .jt-tooltip .image { 14 | height: 150px; 15 | width: 210px; 16 | } 17 | 18 | .jt-tooltip .title, .jt-tooltip .description, .jt-tooltip .field { 19 | padding: 8px; 20 | } 21 | 22 | .jt-tooltip .title { 23 | border-bottom: 1px solid #bebebe; 24 | font-weight: bold; 25 | } 26 | 27 | .jt-tooltip .description { 28 | color: #333; 29 | height: 50px; 30 | } 31 | 32 | .jt-tooltip .field { 33 | color: #999; 34 | padding: 5px 10px; 35 | background-color: #EEE; 36 | font-size: 10px; 37 | } -------------------------------------------------------------------------------- /css/jtop.tooltip.info.css: -------------------------------------------------------------------------------- 1 | 2 | .jt-tooltip-info { 3 | margin: 10px; 4 | font-family: Tahoma, Arial; 5 | box-shadow: 0px 0px 4px #000; 6 | font-size: 9px; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | background-color: #FFF; 11 | border-radius: 2px; 12 | background: #999; /* for non-css3 browsers */ 13 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#dddddd'); /* for IE */ 14 | background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ddd)); /* for webkit browsers */ 15 | background: -moz-linear-gradient(top, #fff, #ddd); /* for firefox 3.6+ */ 16 | } 17 | 18 | .jt-tooltip-info .title { 19 | text-shadow: 1px 1px 2px #FFF; 20 | font-weight: bold; 21 | padding: 4px; 22 | margin-right: 10px; 23 | color: #333; 24 | } 25 | 26 | .jt-tooltip-info .title .name{ 27 | text-shadow: 1px 1px 2px #FFF; 28 | color: #196391; 29 | } 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jtop 1.0.1 5 | 6 | 7 | 8 | 9 | 10 | 35 | 36 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /src/offset.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | //jQuery offset implementation using getBoundingClientRect 4 | function offset(element) { 5 | 6 | var docElem, 7 | win, 8 | box = { top: 0, left: 0 }, 9 | elem = element, 10 | doc = elem && elem.ownerDocument; 11 | 12 | if ( !doc ) { 13 | return; 14 | } 15 | 16 | docElem = doc.documentElement; 17 | 18 | // If we don't have gBCR, just use 0,0 rather than error 19 | // BlackBerry 5, iOS 3 (original iPhone) 20 | if ( typeof elem.getBoundingClientRect !== "undefined" ) { 21 | box = elem.getBoundingClientRect(); 22 | } 23 | 24 | win = getWindow(doc); 25 | 26 | return { 27 | top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ), 28 | left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ) 29 | }; 30 | }; 31 | 32 | function getWindow( elem ) { 33 | return elem.nodeType === 9 ? 34 | elem.defaultView || elem.parentWindow :false; 35 | } 36 | 37 | module.exports = offset; 38 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2017 coderitual 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/template.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | // Simple JavaScript Templating 4 | // John Resig - http://ejohn.org/ - MIT Licensed 5 | 6 | var cache = {}, 7 | 8 | tmpl = function tmpl(str, data){ 9 | // Figure out if we're getting a template, or if we need to 10 | // load the template - and be sure to cache the result. 11 | var fn = !/\W/.test(str) ? 12 | cache[str] = cache[str] || 13 | tmpl(document.getElementById(str).innerHTML) : 14 | 15 | // Generate a reusable function that will serve as a template 16 | // generator (and which will be cached). 17 | new Function("obj", 18 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 19 | 20 | // Introduce the data as local variables using with(){} 21 | "with(obj){p.push('" + 22 | 23 | // Convert the template into pure JavaScript 24 | str 25 | .replace(/[\r\t\n]/g, " ") 26 | .split("<%").join("\t") 27 | .replace(/((^|%>)[^\t]*)'/g, "$1\r") 28 | .replace(/\t=(.*?)%>/g, "',$1,'") 29 | .split("\t").join("');") 30 | .split("%>").join("p.push('") 31 | .split("\r").join("\\'") 32 | + "');}return p.join('');"); 33 | 34 | // Provide some basic currying to the user 35 | return data ? fn( data ) : fn; 36 | }; 37 | 38 | module.exports = tmpl 39 | }); -------------------------------------------------------------------------------- /src/grid.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Class = require('./class'); 5 | 6 | var Grid = Class.extend({ 7 | 8 | init: function() { 9 | this._grid = []; 10 | }, 11 | 12 | setValue: function(x, y, val) { 13 | if (!this._grid[y]) this._grid[y] = []; 14 | if (!this._grid[y][x]) this._grid[y][x] = []; 15 | this._grid[y][x].push(val); 16 | }, 17 | 18 | removeValue: function(x, y, val) { 19 | if (!this._grid[y] || !this._grid[y][x]) return null; 20 | this._grid[y][x].splice(this._grid[y][x].indexOf(val), 1); 21 | }, 22 | 23 | getValue: function(x, y) { 24 | if (!this._grid[y] || !this._grid[y][x]) return null; 25 | // get top most value 26 | return this._grid[y][x][this._grid[y][x].length - 1] || null; 27 | }, 28 | 29 | getValues: function(x, y) { 30 | if (!this._grid[y] || !this._grid[y][x]) return null; 31 | // get all values 32 | return this._grid[y][x] || null; 33 | }, 34 | 35 | getRowCount: function() { 36 | 37 | var resultSet = []; 38 | 39 | _.each(this._grid, function(arr, idx) { 40 | if(_.any(arr, function(a) { return _.size(a) > 0})) { 41 | resultSet.push(idx + 1); 42 | } else { 43 | resultSet.push(0); 44 | } 45 | }); 46 | 47 | return _.max(resultSet) || 0; 48 | }, 49 | 50 | getColumnCount: function() { 51 | 52 | var resultSet = []; 53 | 54 | _.each(this._grid, function(arr) { 55 | _.each(arr, function(a, idx) { 56 | if(_.size(a) > 0) { 57 | resultSet.push(idx + 1); 58 | } 59 | }); 60 | }); 61 | 62 | return _.max(resultSet) || 0; 63 | } 64 | 65 | }); 66 | 67 | module.exports = Grid; 68 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jtop 2 | 3 | Builld beautiful UI similar to real desktop. 4 | 5 | ![Demo](docs/hero.png) 6 | 7 | ## Getting Started 8 | 9 | See the **[live version](http://coderitual.github.io/jtop/ "jtop")**. 10 | 11 | ## Features 12 | - ✊ **Drag & drop** for desktop elements 13 | - 📦 **Basic elements** included: `Icon`, `Panel`, `Tooltip`, `Menu` 14 | - 📝 **SVG Text** with drop shadow and ellispis 15 | - ✏️ **Inline** text editing (Panels) 16 | - ↕️ **Resizable** elements 17 | 18 | ### Example 19 | 20 | ```js 21 | const desktop = jtop.init('jtop', { 22 | scrollView: { 23 | initY: 25 24 | } 25 | }); 26 | 27 | const tooltop = desktop.tooltip({ 28 | offsetLeft: 30, 29 | offsetTop: -120 30 | }); 31 | 32 | const menu = jtop.popupmenu().addMenuElement( 33 | 'open project', 34 | null, 35 | sender => { 36 | console.log(`open project ${sender.title}`); 37 | }, 38 | 'edit-item' 39 | ); 40 | 41 | const icon = desktop 42 | .icon({ title: 'Icon', image: 'test/images/db.png', gridX: 1, gridY: 1 }) 43 | .menu(cMenuProject) 44 | .tooltip(iconTooltip); 45 | 46 | ``` 47 | 48 | For more, visit the example page inside `test` directory and look into `main.js`. 49 | 50 | ## Built With 51 | 52 | * [SVG](https://developer.mozilla.org/pl/docs/Web/SVG) - Custom graphics and effects 53 | * [require.js](http://requirejs.org/) - Module Loader 54 | * [js-signals](http://millermedeiros.github.io/js-signals/) - Pub/Sub system 55 | * [underscore.js](https://underscorejs.org/) - Functional programming helpers 56 | 57 | ## Authors 58 | 59 | * **Mike Skowronek** - *Initial work* - [coderitual](https://twitter.com/coderitual) 60 | 61 | ## License 62 | 63 | Jtop is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 64 | -------------------------------------------------------------------------------- /src/item.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Signal = require('./lib/signals'), 5 | Class = require('./class'), 6 | TWEEN = require('./tween'); 7 | 8 | var settings = { 9 | transformAnimDuration: 500, 10 | transformAnimTween: TWEEN.Easing.Elastic.Out 11 | }; 12 | 13 | function buildTransform(t) { 14 | return ['translate(', t.x, ',', t.y, ') rotate(', t.r, ') scale(', t.s, ')'].join(''); 15 | }; 16 | 17 | function tweenTransformUpdate() { 18 | this.node.setAttribute('transform', buildTransform(this.transform)); 19 | }; 20 | 21 | var Item = Class.extend({ 22 | 23 | id: null, 24 | type: null, 25 | node: null, 26 | parent: null, 27 | manager: null, 28 | 29 | init: function(options) { 30 | 31 | // transform matrix 32 | this.transform = { 33 | x: 0, 34 | y: 0, 35 | r: 0, 36 | s: 1 37 | }; 38 | 39 | //events container 40 | this.on = { 41 | add: new Signal(), 42 | remove: new Signal() 43 | }; 44 | 45 | if(options) { 46 | this.id = options.id || null; 47 | } 48 | 49 | this.elements = {}; 50 | this.tweens = {}; 51 | this.settings = {}; 52 | this._bbox = { width: 0, height: 0 }; 53 | 54 | // tween transforms 55 | this.tweens.transform = new TWEEN.Tween(this.transform) 56 | .easing(TWEEN.Easing.Elastic.Out) 57 | .onUpdate(_.bind(tweenTransformUpdate, this)); 58 | }, 59 | 60 | remove: function() { 61 | if(!(_.isNull(this.id) || _.isNull(this.node) || _.isNull(this.parent))) { 62 | this.onRemove(this); 63 | 64 | this.parent && this.parent.node.removeChild(this.node); 65 | delete this; 66 | } 67 | }, 68 | 69 | pos: function(x, y) { 70 | this.tweens.transform.stop(); 71 | this.transform.x = x; 72 | this.transform.y = y; 73 | this.node.setAttribute('transform', buildTransform(this.transform)); 74 | return this; 75 | }, 76 | 77 | posAnim: function(x, y, duration, easing) { 78 | this.tweens.transform.stop().to({ 79 | x: x, 80 | y: y 81 | }, duration || settings.transformAnimDuration) 82 | .easing(easing || settings.transformAnimTween) 83 | .start(); 84 | return this; 85 | }, 86 | 87 | invalidate: function() { 88 | // defer calculations to next tick because of some borwsers (Opera) 89 | _.defer(function(sender) { 90 | var bbox = sender.node.getBoundingClientRect(); 91 | sender._bbox.width = bbox.width; 92 | sender._bbox.height = bbox.height; 93 | }, this); 94 | }, 95 | 96 | getBoundingBox: function() { 97 | return { 98 | x: this.transform.x, 99 | y: this.transform.y, 100 | w: this._bbox.width, 101 | h: this._bbox.height 102 | }; 103 | }, 104 | 105 | onAdd: function() { 106 | this.on.add.dispatch(this); 107 | }, 108 | 109 | onRemove: function() { 110 | this.on.remove.dispatch(this); 111 | } 112 | }); 113 | 114 | module.exports = Item; 115 | }); -------------------------------------------------------------------------------- /src/class.js: -------------------------------------------------------------------------------- 1 | // John Resig class implementation with Dominik Szablewski's modifications 2 | 3 | define(function(require, exports, module) { 4 | 5 | var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 6 | // The base Class implementation (does nothing) 7 | var Class = function(){}; 8 | 9 | // Inject class 10 | var inject = function (prop) { 11 | var proto = this.prototype; 12 | var _super = {}; 13 | for (var name in prop) { 14 | if (typeof (prop[name]) == "function" && typeof (proto[name]) == "function" && fnTest.test(prop[name])) { 15 | _super[name] = proto[name]; 16 | proto[name] = (function (name, fn) { 17 | return function () { 18 | var tmp = this._super; 19 | this._super = _super[name]; 20 | var ret = fn.apply(this, arguments); 21 | this._super = tmp; 22 | return ret; 23 | }; 24 | })(name, prop[name]); 25 | } else { 26 | proto[name] = prop[name]; 27 | } 28 | } 29 | }; 30 | 31 | // Create a new Class that inherits from this class 32 | Class.extend = function(prop) { 33 | var _super = this.prototype; 34 | 35 | // Instantiate a base class (but only create the instance, 36 | // don't run the init constructor) 37 | initializing = true; 38 | var prototype = new this(); 39 | initializing = false; 40 | 41 | // Copy the properties over onto the new prototype 42 | for (var name in prop) { 43 | // Check if we're overwriting an existing function 44 | prototype[name] = typeof prop[name] == "function" && 45 | typeof _super[name] == "function" && fnTest.test(prop[name]) ? 46 | (function(name, fn){ 47 | return function() { 48 | var tmp = this._super; 49 | 50 | // Add a new ._super() method that is the same method 51 | // but on the super-class 52 | this._super = _super[name]; 53 | 54 | // The method only need to be bound temporarily, so we 55 | // remove it when we're done executing 56 | var ret = fn.apply(this, arguments); 57 | this._super = tmp; 58 | 59 | return ret; 60 | }; 61 | })(name, prop[name]) : 62 | prop[name]; 63 | } 64 | 65 | // The dummy class constructor 66 | function Class() { 67 | // All construction is actually done in the init method 68 | if ( !initializing && this.init ) 69 | this.init.apply(this, arguments); 70 | } 71 | 72 | // Populate our constructed prototype object 73 | Class.prototype = prototype; 74 | 75 | // Enforce the constructor to be what we expect 76 | Class.prototype.constructor = Class; 77 | 78 | // And make this class extendable 79 | Class.extend = arguments.callee; 80 | Class.inject = inject; 81 | 82 | return Class; 83 | }; 84 | 85 | module.exports = Class; 86 | }); -------------------------------------------------------------------------------- /css/jtop.popupmenu.css: -------------------------------------------------------------------------------- 1 | /* 2 | * jtop context menu style sheet 3 | */ 4 | 5 | .jtop-popupmenu { 6 | background: #F9F9F9; 7 | box-shadow: 0px 0px 5px #000; 8 | 9 | display: none; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | list-style: none; 14 | margin: 0; 15 | padding: 5px 0 5px 0; 16 | min-width: 150px; 17 | font-family: "Lucida Grande", Tahoma, Helvetica, Arial, Verdana; 18 | font-size: 11px; 19 | } 20 | 21 | .jtop-popupmenu li { 22 | position: relative; 23 | margin-top: 0; 24 | margin-bottom: 0; 25 | padding-left: 5px; 26 | cursor: pointer; 27 | } 28 | 29 | .jtop-popupmenu li:hover { 30 | background: #196391; 31 | /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0d78b0', endColorstr='#196391'); /* for IE */ 32 | background: -webkit-gradient(linear, left top, left bottom, from(#0d78b0), to(#196391)); /* for webkit browsers */ 33 | background: -moz-linear-gradient(top, #0d78b0, #196391); /* for firefox 3.6+ */ 34 | } 35 | 36 | .jtop-popupmenu li .img { 37 | display: inline-block; 38 | width: 16px; 39 | height: 16px; 40 | vertical-align: middle; 41 | margin: 2px 3px; 42 | } 43 | 44 | .jtop-popupmenu hr { 45 | border: 0; 46 | height: 0; 47 | border-top: 1px solid #bebebe; 48 | border-bottom: none; 49 | } 50 | 51 | /* 52 | .jtop-popupmenu li:hover .img { 53 | background-position: bottom; 54 | } 55 | */ 56 | 57 | .jtop-popupmenu a { 58 | color: #000; 59 | display: inline; 60 | padding: 5px; 61 | text-decoration: none; 62 | width: 85%; 63 | } 64 | 65 | .jtop-popupmenu li:hover a { 66 | color: #f9f9f9; 67 | } 68 | 69 | .jtop-popupmenu li:last-child a { 70 | border-bottom: none; 71 | } 72 | 73 | /* 74 | * Context menu arrow 75 | */ 76 | 77 | .jtop-popupmenu-arrow { 78 | border-color: #f9f9f9 transparent transparent transparent; 79 | border-style: solid; 80 | border-width: 7px; 81 | height:0; 82 | width:0; 83 | position:absolute; 84 | bottom:-14px; 85 | left:42%; 86 | 87 | /* ie6 ? ;) */ 88 | 89 | _border-left-color: pink; 90 | _border-bottom-color: pink; 91 | _border-right-color: pink; 92 | _filter: chroma(color=pink); 93 | } 94 | 95 | .jtop-popupmenu-arrow-border { 96 | border-style: solid; 97 | border-width: 7px; 98 | height:0; 99 | width:0; 100 | position:absolute; 101 | left:42%; 102 | } 103 | 104 | /* Arrow up */ 105 | 106 | .jtop-arrowup { 107 | border-color: #FFF transparent transparent transparent; 108 | bottom:-14px; 109 | top: auto;; 110 | } 111 | 112 | .jtop-arrowdown { 113 | border-color: transparent transparent #FFF transparent; 114 | top:-14px; 115 | bottom: auto; 116 | } 117 | 118 | /* Arrow down */ 119 | 120 | .jtop-arrowup-border { 121 | border-color: #323232 transparent transparent transparent; 122 | bottom:-15px; 123 | top: auto;; 124 | } 125 | 126 | .jtop-arrowdown-border { 127 | border-color: transparent transparent #323232 transparent; 128 | top:-15px; 129 | bottom: auto; 130 | } 131 | 132 | /* 133 | pridesk menu 134 | *********************************************** 135 | */ 136 | 137 | .jtop-popupmenu li .img { 138 | background-image: url('../test/images/glyphicons-halflings.png'); 139 | background-repeat: none; 140 | } 141 | 142 | .jtop-popupmenu li:hover .img { 143 | background-image: url('../test/images/glyphicons-halflings-white.png'); 144 | background-repeat: none; 145 | } 146 | 147 | .jtop-popupmenu li.edit-item .img { 148 | background-position:-96px -72px; 149 | } 150 | 151 | .jtop-popupmenu li.remove .img { 152 | background-position:-312px 0; 153 | } 154 | 155 | .jtop-popupmenu li.open-link .img { 156 | background-position:-120px -72px; 157 | } 158 | 159 | .jtop-popupmenu li.preview-project .img { 160 | background-position:-96px -119px; 161 | } 162 | 163 | .jtop-popupmenu li.edit-project .img { 164 | background-position:0 -72px; 165 | } 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /lib/domReady.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS domReady 2.0.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/domReady for details 5 | */ 6 | /*jslint */ 7 | /*global require: false, define: false, requirejs: false, 8 | window: false, clearInterval: false, document: false, 9 | self: false, setInterval: false */ 10 | 11 | 12 | define(function () { 13 | 'use strict'; 14 | 15 | var isTop, testDiv, scrollIntervalId, 16 | isBrowser = typeof window !== "undefined" && window.document, 17 | isPageLoaded = !isBrowser, 18 | doc = isBrowser ? document : null, 19 | readyCalls = []; 20 | 21 | function runCallbacks(callbacks) { 22 | var i; 23 | for (i = 0; i < callbacks.length; i += 1) { 24 | callbacks[i](doc); 25 | } 26 | } 27 | 28 | function callReady() { 29 | var callbacks = readyCalls; 30 | 31 | if (isPageLoaded) { 32 | //Call the DOM ready callbacks 33 | if (callbacks.length) { 34 | readyCalls = []; 35 | runCallbacks(callbacks); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Sets the page as loaded. 42 | */ 43 | function pageLoaded() { 44 | if (!isPageLoaded) { 45 | isPageLoaded = true; 46 | if (scrollIntervalId) { 47 | clearInterval(scrollIntervalId); 48 | } 49 | 50 | callReady(); 51 | } 52 | } 53 | 54 | if (isBrowser) { 55 | if (document.addEventListener) { 56 | //Standards. Hooray! Assumption here that if standards based, 57 | //it knows about DOMContentLoaded. 58 | document.addEventListener("DOMContentLoaded", pageLoaded, false); 59 | window.addEventListener("load", pageLoaded, false); 60 | } else if (window.attachEvent) { 61 | window.attachEvent("onload", pageLoaded); 62 | 63 | testDiv = document.createElement('div'); 64 | try { 65 | isTop = window.frameElement === null; 66 | } catch (e) {} 67 | 68 | //DOMContentLoaded approximation that uses a doScroll, as found by 69 | //Diego Perini: http://javascript.nwbox.com/IEContentLoaded/, 70 | //but modified by other contributors, including jdalton 71 | if (testDiv.doScroll && isTop && window.external) { 72 | scrollIntervalId = setInterval(function () { 73 | try { 74 | testDiv.doScroll(); 75 | pageLoaded(); 76 | } catch (e) {} 77 | }, 30); 78 | } 79 | } 80 | 81 | //Check if document already complete, and if so, just trigger page load 82 | //listeners. Latest webkit browsers also use "interactive", and 83 | //will fire the onDOMContentLoaded before "interactive" but not after 84 | //entering "interactive" or "complete". More details: 85 | //http://dev.w3.org/html5/spec/the-end.html#the-end 86 | //http://stackoverflow.com/questions/3665561/document-readystate-of-interactive-vs-ondomcontentloaded 87 | //Hmm, this is more complicated on further use, see "firing too early" 88 | //bug: https://github.com/requirejs/domReady/issues/1 89 | //so removing the || document.readyState === "interactive" test. 90 | //There is still a window.onload binding that should get fired if 91 | //DOMContentLoaded is missed. 92 | if (document.readyState === "complete") { 93 | pageLoaded(); 94 | } 95 | } 96 | 97 | /** START OF PUBLIC API **/ 98 | 99 | /** 100 | * Registers a callback for DOM ready. If DOM is already ready, the 101 | * callback is called immediately. 102 | * @param {Function} callback 103 | */ 104 | function domReady(callback) { 105 | if (isPageLoaded) { 106 | callback(doc); 107 | } else { 108 | readyCalls.push(callback); 109 | } 110 | return domReady; 111 | } 112 | 113 | domReady.version = '2.0.1'; 114 | 115 | /** 116 | * Loader Plugin API method 117 | */ 118 | domReady.load = function (name, req, onLoad, config) { 119 | if (config.isBuild) { 120 | onLoad(null); 121 | } else { 122 | domReady(onLoad); 123 | } 124 | }; 125 | 126 | /** END OF PUBLIC API **/ 127 | 128 | return domReady; 129 | }); -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | baseUrl: './src', 3 | paths: { 4 | 'lib': '../lib', 5 | 'domReady': '../lib/domReady' 6 | }, 7 | 8 | shim: { 9 | 'lib/underscore': { exports: "_"} 10 | } 11 | 12 | }); 13 | 14 | require(['domReady', 'jtop'], function(domReady, jtop) { 15 | 16 | domReady(function() { 17 | var desktop = jtop.init('jtop', { 18 | scrollView: { 19 | initY: 25 20 | } 21 | }); 22 | 23 | var cMenuProject = jtop.popupmenu() 24 | .addMenuElement('open project', null, function(sender) { 25 | alert('open project ' + sender.title); 26 | }, 'edit-item') 27 | .addMenuSeparator() 28 | .addMenuElement('preview project', null, function(sender) { 29 | alert('open project ' + sender.title); 30 | }, 'preview-project') 31 | .addMenuElement('remove', null, function(sender) { 32 | if(sender.parent.type === 'PANEL' && _.keys(sender.parent.items).length == 1) { 33 | sender.parent.remove(); 34 | } 35 | sender.remove(); 36 | }, 'remove'); 37 | 38 | var iconTooltip = desktop.tooltip({ 39 | offsetLeft: 30, 40 | offsetTop: -120, 41 | }) 42 | .addTemplate('<%if(image) {%><%}%>' + 43 | '
<%=title%>
' + 44 | '
<%=description%>
' + 45 | '
<%=field%>
') 46 | .addTemplate(' '); 47 | 48 | iconTooltip.on.show.add(function(sender, values) { 49 | values.title = sender.settings.title; 50 | values.image = 'http://ns3002439.ovh.net/thumbs/rozne/thumb-2274542.jpg'; 51 | values.description = sender.settings.title; 52 | values.field = 'Jtop project'; 53 | }); 54 | 55 | var icons = []; 56 | 57 | icons[0] = desktop.icon({title: 'Some very long text applied to this icon can be seen here it is good i hope', image: 'test/images/db.png', gridX: 1, gridY: 1}) 58 | .menu(cMenuProject). 59 | tooltip(iconTooltip);; 60 | 61 | icons[1] = desktop.icon({title: 'Secon element. Shorten text applied.', image: 'test/images/box.png', gridX: 2, gridY: 1}) 62 | .menu(cMenuProject) 63 | .tooltip(iconTooltip); 64 | 65 | icons[2] = desktop.icon({title: 'Short named element', image: 'test/images/bird.png', gridX: 3, gridY: 1}) 66 | .menu(cMenuProject) 67 | .tooltip(iconTooltip); 68 | 69 | icons[3] = desktop.icon({title: 'Different element', image: 'test/images/box.png', gridX: 2, gridY: 2}) 70 | .menu(cMenuProject) 71 | .tooltip(iconTooltip); 72 | 73 | icons[4] = desktop.icon({title: 'Test project, some very long text.', image: 'test/images/db.png', gridX: 1, gridY: 2}) 74 | .menu(cMenuProject) 75 | .tooltip(iconTooltip); 76 | 77 | icons[5] = desktop.icon({title: 'Secon element. Shorten text applied.', image: 'test/images/box.png', gridX: 3, gridY: 3}) 78 | .menu(cMenuProject) 79 | .tooltip(iconTooltip); 80 | 81 | icons[6] = desktop.icon({title: 'Short named element', image: 'test/images/bird.png', gridX: 3, gridY: 2}) 82 | .menu(cMenuProject) 83 | .tooltip(iconTooltip); 84 | 85 | icons[7] = desktop.icon({title: 'Different element', image: 'test/images/box.png', gridX: 2, gridY: 3}) 86 | .menu(cMenuProject) 87 | .tooltip(iconTooltip); 88 | 89 | 90 | 91 | var p = desktop.panel({title: 'Some very long text with this custom panel item'}).pos(500, 100); 92 | var r = desktop.panel({title: 'Different panel', width: 200, height: 160}).pos(800, 100); 93 | 94 | p.addItem(icons[7], 0,0, true); 95 | 96 | // automated create panels functionality 97 | var panelTooltip = desktop.tooltip({ 98 | className: 'jt-tooltip-info', 99 | offsetLeft: 0, 100 | offsetTop: 0, 101 | toOpacity: 1 102 | }) 103 | .addTemplate('
<%=name%> <%=title%>
'); 104 | 105 | panelTooltip.on.show.add(function(sender, values) { 106 | values.name = '+ Create panel'; 107 | values.title = sender.settings.title; 108 | }); 109 | 110 | desktop.on.dragOverItem.add(function(item, itemBelow, x, y) { 111 | panelTooltip.show(itemBelow, x, y); 112 | }); 113 | 114 | desktop.on.dragOutItem.add(function(item) { 115 | panelTooltip.hide(); 116 | }); 117 | 118 | desktop.on.dropInItem.add(function(item, itemBelow) { 119 | panelTooltip.hide(); 120 | var newPanel = desktop.panel({title: itemBelow.settings.title, width: 200, height: 80}).pos(itemBelow.transform.x, itemBelow.transform.y + 25); 121 | 122 | desktop.grid.removeValue(itemBelow.settings.gridX, itemBelow.settings.gridY, itemBelow); 123 | desktop.grid.removeValue(item.settings.gridX, item.settings.gridY, item); 124 | 125 | newPanel.addItem(item, 1, 0, true); 126 | newPanel.addItem(itemBelow, 0, 0, true); 127 | 128 | return true; 129 | }); 130 | 131 | // automated remove panels functionality 132 | var panelToRemove = null; 133 | desktop.on.dragStart.add(function(item, x, y) { 134 | if(item.parent.type === 'PANEL' && _.keys(item.parent.items).length == 0) { 135 | panelToRemove = item.parent; 136 | } else { 137 | panelToRemove = null; 138 | } 139 | }); 140 | 141 | desktop.on.dragEnd.add(function(item, x, y) { 142 | if(item.parent !== panelToRemove) { 143 | panelToRemove && panelToRemove.remove(); 144 | } 145 | }); 146 | 147 | }); 148 | 149 | }); -------------------------------------------------------------------------------- /src/tooltip.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Class = require('./class'), 5 | Signal = require('./lib/signals'), 6 | core = require('./core'), 7 | Icon = require('./item.icon'), 8 | Template = require('./template'), 9 | TWEEN = require('./tween'); 10 | 11 | function hasClass(ele,cls) { 12 | return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); 13 | }; 14 | 15 | function addClass(ele,cls) { 16 | if (!hasClass(ele,cls)) ele.className += " "+cls; 17 | }; 18 | 19 | function removeClass(ele,cls) { 20 | if (hasClass(ele,cls)) { 21 | var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); 22 | ele.className = ele.className.replace(reg,' '); 23 | } 24 | }; 25 | 26 | function windowToElement(el, x, y) { 27 | var bbox = el.getBoundingClientRect(); 28 | return { x: x + bbox.left, y: y + bbox.top}; 29 | }; 30 | 31 | var Tooltip = Class.extend({ 32 | 33 | _tooltips: [], 34 | 35 | init: function(options, desktop) { 36 | 37 | if(!(desktop instanceof core.Core)) return; 38 | 39 | var o = this.settings = _.extend({ 40 | marginTop: 20, 41 | marginBottom: 20, 42 | offsetLeft: 0, 43 | offsetTop: 0, 44 | showDelay: 0, 45 | hideDelay: 0, 46 | fadeInSpeed: 200, 47 | fadeOutSpeed: 0, 48 | toOpacity: 1, 49 | className: 'jt-tooltip' 50 | }, options), 51 | 52 | self = this, 53 | body = document.getElementsByTagName('body')[0], 54 | tooltip = document.createElement('div'); 55 | 56 | addClass(tooltip, o.className); 57 | body.appendChild(tooltip); 58 | 59 | this.active = false; 60 | this._style = { opacity: 0 }; 61 | this._template = null; 62 | this.on = { 63 | show: new Signal() 64 | }; 65 | 66 | document.addEventListener('mousemove', _.bind(function(e) { 67 | e.preventDefault(); 68 | if(this.active) { 69 | var loc = windowToElement(desktop.parent, e.clientX, e.clientY); 70 | this.pos(e.clientX, e.clientY); 71 | } 72 | }, this)); 73 | 74 | document.addEventListener('mousedown', _.bind(function(e) { 75 | if(this.active) { 76 | this.hide(); 77 | } 78 | }, this)); 79 | 80 | this._tweenOpacity = new TWEEN.Tween(this._style) 81 | .onUpdate(function() { 82 | tooltip.style.opacity = this.opacity; 83 | }) 84 | .onComplete(function() { 85 | if(!self.active) tooltip.style.display = 'none'; 86 | self._style.opacity = parseFloat(tooltip.style.opacity); 87 | }); 88 | 89 | this._tooltips.push(this); 90 | this._getTooltipHtml = function() { 91 | return tooltip; 92 | } 93 | }, 94 | 95 | addTemplate: function(templ) { 96 | if(_.isString(templ)) { 97 | this._template = Template(templ); 98 | } 99 | 100 | return this; 101 | }, 102 | 103 | show: function(sender, x, y) { 104 | var tooltip = this._getTooltipHtml(), 105 | self = this, 106 | o = this.settings, 107 | values = {}; 108 | 109 | this.active = true; 110 | this.on.show.dispatch(sender, values); 111 | tooltip.innerHTML = this._template(values); 112 | 113 | if(tooltip.style.display == 'none') tooltip.style.display = 'block'; 114 | this.pos(x, y); 115 | 116 | this._tweenOpacity 117 | .stop() 118 | .to({'opacity': o.toOpacity}, o.fadeInSpeed) 119 | .delay(o.showDelay) 120 | .start(); 121 | 122 | return this; 123 | }, 124 | 125 | pos: function(x, y) { 126 | var tooltip = this._getTooltipHtml(), 127 | o = this.settings, 128 | self = this, 129 | posX = x + o.offsetLeft, 130 | posY = y + o.offsetTop; 131 | 132 | _.extend(tooltip.style, { 133 | top: posY + 'px', 134 | left: posX + 'px' 135 | }); 136 | 137 | var bbox = tooltip.getBoundingClientRect(), 138 | dy = 0, 139 | dx = 0; 140 | 141 | if(bbox.top - o.marginTop < 0) { 142 | dy = -(bbox.top - o.marginTop); 143 | } else if (bbox.bottom + o.marginBottom > window.innerHeight) { 144 | dy = -Math.abs(bbox.bottom + o.marginBottom - window.innerHeight); 145 | } 146 | 147 | if(bbox.left < 0) { 148 | dx = -bbox.left; 149 | } else if (bbox.right > window.innerWidth) { 150 | dx = -bbox.width - 2 * o.offsetLeft; 151 | } 152 | 153 | _.extend(tooltip.style, { 154 | top: posY + dy + 'px', 155 | left: posX + dx + 'px' 156 | }); 157 | }, 158 | 159 | /** 160 | * Hides tooltip 161 | * @return {tooltip} 162 | */ 163 | hide: function() { 164 | var tooltip = this._getTooltipHtml(), 165 | o = this.settings, 166 | that = this; 167 | 168 | this.active = false; 169 | 170 | this._tweenOpacity 171 | .stop() 172 | .to({'opacity': 0}, o.fadeOutSpeed) 173 | .delay(o.hideDelay) 174 | .start(); 175 | 176 | return this; 177 | } 178 | }); 179 | 180 | // Icon inject 181 | Icon.inject({ 182 | tooltip: function(tooltip) { 183 | if(!(tooltip instanceof Tooltip)) return; 184 | 185 | this._tooltip = tooltip; 186 | 187 | this.node.addEventListener('mouseover', _.bind(function(e) { 188 | e.preventDefault(); 189 | if(!this.manager._drag.dragging) { 190 | var loc = windowToElement(this.manager.parent, e.clientX, e.clientY); 191 | this._tooltip.show(this, e.clientX, e.clientY); 192 | } 193 | 194 | }, this)); 195 | 196 | this.node.addEventListener('mouseout', _.bind(function(e) { 197 | e.preventDefault(); 198 | this._tooltip.hide(); 199 | }, this)); 200 | 201 | return this; 202 | } 203 | }); 204 | 205 | // Core inject 206 | core.Core.inject({ 207 | tooltip: function(options) { 208 | return new Tooltip(options, this); 209 | } 210 | }); 211 | 212 | module.exports = Tooltip; 213 | }); -------------------------------------------------------------------------------- /src/item.icon.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Signal = require('./lib/signals'), 5 | core = require('./core'), 6 | Item = require('./item'), 7 | Text = require('./text'), 8 | Drag = require('./drag'), 9 | TWEEN = require('./tween'); 10 | 11 | var XLINK = 'http://www.w3.org/1999/xlink', 12 | _type = 'ICON'; 13 | 14 | var settings = { 15 | fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana', 16 | fontSize: 11, 17 | mouseOverSpeed: 100, 18 | mouseOutSpeed: 100, 19 | mouseOverRotation: 10, 20 | mouseOutRotation: 0, 21 | titleMargin: 5, 22 | titleMaxLines: 2 23 | }; 24 | 25 | function measureElements() { 26 | var node = this.node, 27 | icon = this.elements.icon, 28 | title = this.elements.title, 29 | handle = this.elements.handle, 30 | titleTop = this.elements._titleTop, 31 | titleShadow = this.elements._titleShadow, 32 | o = this.settings; 33 | 34 | icon.setAttribute('x', (o.maxWidth - o.width) / 2); 35 | icon.setAttribute('y', o.offsetTop); 36 | 37 | var iconBBox = icon.getBBox(); 38 | icon.cx = o.maxWidth / 2; 39 | icon.cy = o.offsetTop + o.height / 2; 40 | 41 | title.setAttribute('transform', 'translate(' + (o.maxWidth / 2 - 1) + ', ' + (iconBBox.height + iconBBox.y + o.fontSize + o.textOffsetTop) + ')'); 42 | 43 | Text.addTextFlow(o.title, titleTop, o.maxWidth - o.titleMargin * 2, 0, o.fontSize, false, o.titleMaxLines, true); 44 | Text.addTextFlow(o.title, titleShadow, o.maxWidth - o.titleMargin * 2, 0, o.fontSize, false, o.titleMaxLines, true); 45 | 46 | var bbox = node.getBBox(); 47 | core.svgSetXYWH(handle, 0, 0, o.maxWidth, bbox.height); 48 | }; 49 | 50 | function onMouseOver() { 51 | var icon = this.elements.icon; 52 | this.tweens.hover.stop().to({ r: settings.mouseOverRotation }, settings.mouseOverSpeed).start(); 53 | 54 | if(!this.drag.dragging) { 55 | this.manager.node.style.cursor = 'pointer'; 56 | } 57 | 58 | }; 59 | 60 | function onMouseOut() { 61 | var icon = this.elements.icon; 62 | this.tweens.hover.stop().to({ r: settings.mouseOutRotation }, settings.mouseOutSpeed).start(); 63 | 64 | if(!this.drag.dragging) { 65 | this.manager.node.style.cursor = 'default'; 66 | } 67 | }; 68 | 69 | function onClickEvent(e) { 70 | this.on.click.dispatch(this, e); 71 | }; 72 | 73 | var Icon = Item.extend({ 74 | 75 | init: function(options) { 76 | this._super(options); 77 | 78 | var o = this.settings = _.extend({ 79 | image: '', 80 | title: '', 81 | width: 38, 82 | height: 38, 83 | maxWidth: 100, 84 | maxHeight: 80, 85 | offsetTop: 12, 86 | textOffsetTop: 2, 87 | fontFamily: settings['fontFamily'], 88 | fontSize: settings['fontSize'], 89 | titleMargin: settings['titleMargin'], 90 | titleMaxLines: settings['titleMaxLines'] 91 | }, options); 92 | 93 | this.type = _type; 94 | 95 | _.extend(this.on, { 96 | click: new Signal() 97 | }); 98 | 99 | var g = core.createSVGElement('g'), 100 | icon = core.createSVGElement('image'), 101 | title = core.createSVGElement('g'), 102 | handle = core.createSVGElement('rect'), 103 | titleTop = core.createSVGElement('text'), 104 | titleShadow = core.createSVGElement('text'); 105 | 106 | icon.setAttributeNS(XLINK, "xlink:href", o.image); 107 | icon.setAttribute('width', o.width); 108 | icon.setAttribute('height', o.height); 109 | 110 | titleTop.setAttribute('font-family', o.fontFamily); 111 | titleTop.setAttribute('font-size', o.fontSize); 112 | titleTop.setAttribute('fill', '#FFF'); 113 | titleTop.setAttribute('text-anchor', 'middle'); 114 | 115 | titleShadow.setAttribute('font-family', o.fontFamily); 116 | titleShadow.setAttribute('font-size', o.fontSize); 117 | titleShadow.setAttribute('fill', '#000'); 118 | titleShadow.setAttribute('stroke', '#000'); 119 | titleShadow.setAttribute('stroke-width', 2.6); 120 | titleShadow.setAttribute('stroke-opacity', 0.5); 121 | titleShadow.setAttribute('text-anchor', 'middle'); 122 | titleShadow.setAttribute('transform', 'translate(1, 1)'); 123 | 124 | title.setAttribute('class', 'text'); 125 | title.style['-webkit-user-select'] = 'none'; 126 | title.style['-moz-user-select'] = 'none'; 127 | title.appendChild(titleShadow); 128 | title.appendChild(titleTop); 129 | 130 | handle.setAttribute('fill', '#FFF'); 131 | handle.setAttribute('opacity', '0.0'); 132 | handle.setAttribute('rx', '5'); 133 | handle.setAttribute('ry', '5'); 134 | 135 | g.appendChild(icon); 136 | g.appendChild(title); 137 | g.appendChild(handle); 138 | 139 | g.addEventListener('mouseover', _.bind(onMouseOver, this)); 140 | g.addEventListener('mouseout', _.bind(onMouseOut, this)); 141 | g.addEventListener('click', _.bind(onClickEvent, this)); 142 | 143 | this.node = g; 144 | this.elements = { 145 | icon: icon, 146 | title: title, 147 | handle: handle, 148 | _titleTop: titleTop, 149 | _titleShadow: titleShadow 150 | }; 151 | 152 | Drag.asDragElement.call(this); 153 | }, 154 | 155 | title: function(val) { 156 | if(_.isString(val)) { 157 | this.settings.title = val; 158 | this.invalidate(); 159 | } 160 | 161 | return this; 162 | }, 163 | 164 | image: function(val) { 165 | if(_.isString(val)) { 166 | this.settings.image = val; 167 | this.elements.icon.setAttributeNS(XLINK, "xlink:href", val); 168 | } 169 | 170 | return this; 171 | }, 172 | 173 | onAdd: function(desk) { 174 | this.invalidate(); 175 | 176 | // hover effect 177 | var icon = this.elements.icon; 178 | this.tweens.hover = new TWEEN.Tween({ r: 0 }) 179 | .easing(TWEEN.Easing.Linear.None) 180 | .onUpdate(function() { 181 | icon.rotate = this.r; 182 | icon.setAttribute('transform', 'rotate(' + icon.rotate + ', ' + icon.cx + ',' + icon.cy + ')'); 183 | }); 184 | 185 | this._super(); 186 | }, 187 | 188 | invalidate: function() { 189 | measureElements.call(this); 190 | this._super(); 191 | }, 192 | 193 | dragStart: function() { 194 | this.node.style.opacity = 0.8; 195 | }, 196 | dragEnd: function() { 197 | this.node.style.opacity = 1; 198 | } 199 | }); 200 | 201 | core.Core.inject({ 202 | icon: function(options) { 203 | return this.addItem(new Icon(options), options.gridX, options.gridY); 204 | } 205 | }); 206 | 207 | module.exports = Icon; 208 | }); -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var core = require('./core'); 4 | 5 | function autoEllipseText(element, text, width) { 6 | element.textContent = text; 7 | var textLength = element.getComputedTextLength(); 8 | if(textLength > width) { 9 | var i = 1; 10 | element.textContent = ''; 11 | while(element.getComputedTextLength() < (width) && i < text.length) { 12 | element.textContent = text.substr(0, i) + '...'; 13 | i++; 14 | } 15 | return element.textContent; 16 | } 17 | element.textContent = text; 18 | return text; 19 | }; 20 | 21 | function textFlow(myText, textToAppend, maxWidth, x, ddy, justified, maxLines, ellipsis) { 22 | //extract and add line breaks for start 23 | textToAppend.textContent = ''; 24 | var dashArray = []; 25 | var dashFound = true; 26 | var indexPos = 0; 27 | var cumulY = 0; 28 | while (dashFound == true) { 29 | var result = myText.indexOf("-",indexPos); 30 | if (result == -1) { 31 | //could not find a dash 32 | dashFound = false; 33 | } 34 | else { 35 | dashArray.push(result); 36 | indexPos = result + 1; 37 | } 38 | } 39 | //split the text at all spaces and dashes 40 | var words = myText.split(/[\s-]/); 41 | var line = ""; 42 | var dy = 0; 43 | var lines = 0; 44 | var curNumChars = 0; 45 | var computedTextLength = 0; 46 | var myTextNode; 47 | var tspanEl; 48 | var lastLineBreak = 0; 49 | 50 | for (i=0;i maxWidth || i == 0) { 54 | 55 | if (computedTextLength > maxWidth) { 56 | var tempText = tspanEl.firstChild.nodeValue; 57 | tempText = tempText.slice(0,(tempText.length - words[i-1].length - 2)); //the -2 is because we also strip off white space 58 | tspanEl.firstChild.nodeValue = tempText; 59 | if (justified) { 60 | //determine the number of words in this line 61 | var nrWords = tempText.split(/\s/).length; 62 | computedTextLength = tspanEl.getComputedTextLength(); 63 | var additionalWordSpacing = (maxWidth - computedTextLength) / (nrWords - 1); 64 | tspanEl.setAttributeNS(null,"word-spacing",additionalWordSpacing); 65 | //alternatively one could use textLength and lengthAdjust, however, currently this is not too well supported in SVG UA's 66 | } 67 | } 68 | lines++; 69 | tspanEl = core.createSVGElement("tspan"); 70 | tspanEl.setAttributeNS(null,"x",x); 71 | tspanEl.setAttributeNS(null,"dy",dy); 72 | myTextNode = document.createTextNode(line); 73 | tspanEl.appendChild(myTextNode); 74 | textToAppend.appendChild(tspanEl); 75 | 76 | if(checkDashPosition(dashArray,curNumChars-1)) { 77 | line = word + "-"; 78 | } 79 | else { 80 | line = word + " "; 81 | } 82 | if (i != 0) { 83 | line = words[i-1] + " " + line; 84 | } 85 | dy = ddy; 86 | cumulY += dy; 87 | 88 | } else { 89 | if(checkDashPosition(dashArray,curNumChars-1)) { 90 | line += word + "-"; 91 | } 92 | else { 93 | line += word + " "; 94 | } 95 | } 96 | 97 | tspanEl.firstChild.nodeValue = line; 98 | 99 | if(maxLines && lines >= maxLines) { 100 | if (ellipsis === true) { 101 | 102 | line = line.slice(0, -1); 103 | if(i < words.length - 1) line += ' ' + word; 104 | autoEllipseText(tspanEl, line, maxWidth); // 3 dots place 105 | } 106 | return; 107 | } 108 | 109 | computedTextLength = tspanEl.getComputedTextLength(); 110 | if (i == words.length - 1) { 111 | if (computedTextLength > maxWidth) { 112 | var tempText = tspanEl.firstChild.nodeValue; 113 | tspanEl.firstChild.nodeValue = tempText.slice(0,(tempText.length - words[i].length - 1)); 114 | tspanEl = core.createSVGElement("tspan"); 115 | tspanEl.setAttributeNS(null,"x",x); 116 | tspanEl.setAttributeNS(null,"dy",dy); 117 | myTextNode = document.createTextNode(words[i]); 118 | tspanEl.appendChild(myTextNode); 119 | textToAppend.appendChild(tspanEl); 120 | } 121 | 122 | } 123 | } 124 | return cumulY; 125 | }; 126 | 127 | //this function checks if there should be a dash at the given position, instead of a blank 128 | function checkDashPosition(dashArray,pos) { 129 | var result = false; 130 | for (var i=0;i this.parent.offsetWidth) newposx = this.parent.offsetWidth - crect.w; 126 | 127 | if (crect.y + yd < 0) newposy -= (crect.y + yd); 128 | else if (crect.y + yd + crect.h > this.parent.offsetHeight) newposy += this.parent.offsetHeight - (crect.y + yd + crect.h); 129 | } 130 | 131 | item.pos(newposx, newposy); 132 | 133 | this._drag.x = loc.x; 134 | this._drag.y = loc.y; 135 | this._drag.cx = cx; 136 | this._drag.cy = cy; 137 | 138 | if(item.drag.checkDragOver && _.isNull(this._drag.dragCheckTimer)) { 139 | this._drag.dragCheckTimer = setTimeout(_.bind(dragOverElement, this), settings.checkDragOverDelay); 140 | } 141 | 142 | this.drag(item, loc.x, loc.y); 143 | item.dragMove(loc.x, loc.y); 144 | }; 145 | 146 | function mouseup(e) { 147 | e.preventDefault(); 148 | if(_.isNull(this._drag.item)) return; 149 | 150 | if(this._drag.dragging) { 151 | 152 | var item = this._drag.item, 153 | loc = windowToElement(this.parent, e.clientX, e.clientY); 154 | xd = loc.x - this._drag.x, 155 | yd = loc.y - this._drag.y, 156 | newposx = item.transform.x + xd, 157 | newposy = item.transform.y + yd; 158 | 159 | item.pos(newposx, newposy); 160 | item.drag.dragging = false; 161 | 162 | // restore previous cursor 163 | this.node.style.cursor = this._drag.cursor; 164 | 165 | if(item.drag.checkDropOver) 166 | dropOverElement.call(this, loc.x, loc.y); 167 | 168 | this.dragEnd(item, loc.x, loc.y); 169 | item.dragEnd(loc.x, loc.y); 170 | } 171 | 172 | this._drag = { 173 | item: null, 174 | x: null, 175 | y: null, 176 | dragging: false, 177 | dragCheckTimer: null, 178 | itemStartDrag: { 179 | parent: null, 180 | x: 0, 181 | y: 0 182 | } 183 | }; 184 | }; 185 | 186 | module.exports = { 187 | asDragElement: function(options) { 188 | 189 | this.drag = _.extend({ 190 | handle: this.node, 191 | distance: 5, 192 | enabled: true, 193 | dragging: false, 194 | checkDragOver: true, 195 | checkDropOver: true, 196 | boundingBox: true 197 | }, options || {}); 198 | 199 | _.defaults(this, { 200 | dragStart: function(x, y) {}, 201 | dragEnd: function(x, y) {}, 202 | dragMove: function(x, y) {} 203 | }); 204 | }, 205 | 206 | asDropElement: function(options) { 207 | 208 | this.drop = _.extend({ 209 | handle: this.node, 210 | enabled: true 211 | }, options || {}); 212 | 213 | this.drop.handle.className.baseVal += ' droppable'; 214 | 215 | _.defaults(this, { 216 | dragOver: function(item, x, y) {}, 217 | dragOut: function(item, x, y) {}, 218 | dropIn: function(item, x, y) {}, 219 | dropOut: function(item, x, y) {} 220 | }); 221 | }, 222 | 223 | asDragManager: function(handler) { 224 | 225 | this.on.addItem.add(_.bind(itemAdded, this)); 226 | 227 | _.defaults(this, { 228 | _drag: { 229 | item: null, 230 | x: null, 231 | y: null, 232 | dragging: false, 233 | dragCheckTimer: null, 234 | dropItem: null, 235 | cursor: 'default', 236 | itemStartDrag: { 237 | parent: null, 238 | x: 0, 239 | y: 0 240 | } 241 | }, 242 | 243 | dragStart: function(item, x, y) {}, 244 | dragEnd: function(item, x, y) {}, 245 | drag: function(item, x, y) {}, 246 | getDropableItem: function() {}, 247 | registerDragItem: function(item) { itemAdded.call(this, item); } 248 | }); 249 | 250 | handler.addEventListener('mousemove', _.bind(mousemove, this)); 251 | handler.addEventListener('mouseup', _.bind(mouseup, this)); 252 | } 253 | } 254 | }); 255 | -------------------------------------------------------------------------------- /src/popupmenu.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Class = require('./class'), 5 | Signal = require('./lib/signals'), 6 | core = require('./core'), 7 | Item = require('./item'), 8 | Icon = require('./item.icon'), 9 | TWEEN = require('./tween'); 10 | 11 | function hasClass(ele,cls) { 12 | return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); 13 | }; 14 | 15 | function addClass(ele,cls) { 16 | if (!hasClass(ele,cls)) ele.className += " "+cls; 17 | }; 18 | 19 | function removeClass(ele,cls) { 20 | if (hasClass(ele,cls)) { 21 | var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); 22 | ele.className = ele.className.replace(reg,' '); 23 | } 24 | }; 25 | 26 | function windowToElement(el, x, y) { 27 | var bbox = el.getBoundingClientRect(); 28 | return { x: x - bbox.left, y: y - bbox.top}; 29 | }; 30 | 31 | // Popup menu definition 32 | var PopupMenu = Class.extend({ 33 | 34 | _sender: null, 35 | _menus: [], 36 | 37 | init: function(options) { 38 | 39 | var o = this.settings = _.extend({ 40 | width: 130, 41 | defaultSide: 'down', 42 | showArrow: true, 43 | offsetTop: 70, 44 | offsetBottom: 20, 45 | iconWidth: 16, 46 | iconHeight: 16, 47 | elements: {}, 48 | delay: 0 49 | }, options), 50 | 51 | body = document.getElementsByTagName('body')[0], 52 | htmlMenuList = document.createElement('ul'), 53 | htmlMenuElement = document.createElement('li'), 54 | htmlMenuDiv = document.createElement('div'), 55 | htmlMenuImg = document.createElement('div'), 56 | htmlMenuTitle = document.createElement('a'), 57 | 58 | htmlArrow = document.createElement('div'), 59 | htmlArrowBorder = document.createElement('div'); 60 | 61 | htmlMenuDiv.appendChild(htmlMenuImg); 62 | htmlMenuDiv.appendChild(htmlMenuTitle); 63 | htmlMenuElement.appendChild(htmlMenuDiv); 64 | 65 | htmlMenuList.appendChild(htmlArrowBorder); 66 | htmlMenuList.appendChild(htmlArrow); 67 | 68 | body.appendChild(htmlMenuList); 69 | 70 | addClass(htmlMenuList, 'jtop-popupmenu'); 71 | addClass(htmlArrow, 'jtop-popupmenu-arrow'); 72 | addClass(htmlArrowBorder, 'jtop-popupmenu-arrow-border'); 73 | addClass(htmlMenuImg, 'img'); 74 | 75 | this._style = { opacity: 0 }, 76 | this._tweenOpacity = new TWEEN.Tween(this._style) 77 | .onUpdate(function() { 78 | htmlMenuList.style.opacity = this.opacity; 79 | }) 80 | .onComplete(function() { 81 | if(this.opacity <= 0) htmlMenuList.style.display = 'none'; 82 | }); 83 | 84 | this._menus.push(this); 85 | 86 | this._getElementTemplate = function() { 87 | return htmlMenuElement.cloneNode(true); 88 | } 89 | 90 | this._getMenuHtml = function() { 91 | return htmlMenuList; 92 | } 93 | 94 | this._getArrow = function() { 95 | return htmlArrow; 96 | } 97 | 98 | this._getArrowBorder = function() { 99 | return htmlArrowBorder; 100 | } 101 | 102 | this._createSeparator = function() { 103 | return document.createElement('hr'); 104 | } 105 | 106 | // Hide popupmenus except mouse actions 107 | document.addEventListener('mousedown', _.bind(function(e) { 108 | this.hide(); 109 | }, this)); 110 | 111 | htmlMenuList.addEventListener('mousedown', _.bind(function(e) { 112 | e.stopPropagation(); 113 | e.preventDefault(); 114 | }, this)); 115 | 116 | htmlMenuList.addEventListener('click', _.bind(function(e) { 117 | e.stopPropagation(); 118 | e.preventDefault(); 119 | this.hide(); 120 | }, this)); 121 | }, 122 | 123 | /** 124 | * Adds element position to menu 125 | * @param {string} title Title of element (it will be shown in the menu position). 126 | * @param {string} icon Path to the element icon (optional). 127 | * @param {function} handler Handler function for element click event.. 128 | */ 129 | addMenuElement: function(title, icon, handler, className) { 130 | var menu = this._getMenuHtml(), 131 | el = this._getElementTemplate(), 132 | div = el.getElementsByTagName('div')[0], 133 | text = div.getElementsByTagName('a')[0], 134 | img = div.getElementsByClassName('img')[0]; 135 | 136 | var self = this; 137 | 138 | text.href = '#'; 139 | text.innerHTML = title; 140 | 141 | if(_.isFunction(handler)) { 142 | el.addEventListener('click', function(e) { 143 | e.preventDefault(); 144 | handler.call(self, self._sender); 145 | return false; 146 | }); 147 | } 148 | 149 | if(_.isString(icon)) { 150 | img.style.backgroundImage = 'url(' + icon + ')'; 151 | } 152 | 153 | if(className && className.length > 0) { 154 | addClass(el, className); 155 | } 156 | 157 | menu.appendChild(el); 158 | return this; 159 | }, 160 | 161 | /** 162 | * Adds separator element to menu 163 | */ 164 | addMenuSeparator: function() { 165 | var menu = this._getMenuHtml(), 166 | sep = this._createSeparator(); 167 | 168 | menu.appendChild(sep); 169 | return this; 170 | }, 171 | 172 | show: function(sender, x, y) { 173 | var menu = this._getMenuHtml(), 174 | that = this, 175 | arrow = this._getArrow(), 176 | arrowBorder = this._getArrowBorder(), 177 | o = this.settings, 178 | posX, 179 | posY, 180 | arrowX, 181 | arrowY, 182 | canUpPos; 183 | 184 | this._sender = sender; 185 | 186 | // Display first to calculate bbox 187 | _.extend(menu.style, { 188 | display: 'block', 189 | opacity: '0', 190 | top: y + 'px', 191 | left: x + 'px' 192 | }); 193 | 194 | // Calculate menu position 195 | var width = menu.offsetWidth, 196 | height = menu.offsetHeight; 197 | 198 | posX = x - width / 2; 199 | 200 | if(posX - window.pageXOffset < 0) { 201 | posX += (window.pageXOffset - posX) + 10; 202 | } else if ((window.pageXOffset + window.innerWidth) - (posX + width) < 0) { 203 | posX -= (posX + width) - (window.pageXOffset + window.innerWidth) + 25; 204 | } 205 | 206 | arrowX = x - posX - 7; 207 | 208 | canUpPos = (((o.defaultSide === 'up') && (y - height - o.offsetBottom - window.pageYOffset) > 0) 209 | || ((o.defaultSide === 'down') && (window.pageYOffset + window.innerHeight) - (y + height + o.offsetTop) < 0) ) 210 | 211 | menu.style.left = posX + 'px'; 212 | menu.style.top = canUpPos 213 | ? y - height - o.offsetBottom + 'px' 214 | : y + o.offsetTop + 'px'; 215 | 216 | // calculate arrow position 217 | if(canUpPos) { // above 218 | 219 | removeClass(arrow, 'jtop-arrowdown'); 220 | addClass(arrow, 'jtop-arrowup'); 221 | 222 | _.extend(arrow.style, { 223 | 'left': arrowX + 'px', 224 | }); 225 | 226 | removeClass(arrowBorder, 'jtop-arrowdown-border'); 227 | addClass(arrowBorder, 'jtop-arrowup-border'); 228 | 229 | _.extend(arrowBorder.style, { 230 | 'left': arrowX + 'px', 231 | }); 232 | } else { // below 233 | 234 | removeClass(arrow, 'jtop-arrowup'); 235 | addClass(arrow, 'jtop-arrowdown'); 236 | 237 | _.extend(arrow.style, { 238 | 'left': arrowX + 'px', 239 | }); 240 | 241 | removeClass(arrowBorder, 'jtop-arrowup-border'); 242 | addClass(arrowBorder, 'jtop-arrowdown-border'); 243 | 244 | _.extend(arrowBorder.style, { 245 | 'left': arrowX + 'px', 246 | }); 247 | } 248 | 249 | // hide all other menus 250 | for(var i = 0, len = this._menus.length; i < len; i++) if(this._menus[i] !== this) { 251 | this._menus[i].hide(); 252 | } 253 | 254 | // Tween menu show 255 | this._tweenOpacity.stop().to({'opacity': '1'}, 100).delay(o.delay).start(); 256 | 257 | return this; 258 | }, 259 | 260 | hide: function() { 261 | var menu = this._getMenuHtml(), 262 | that = this; 263 | 264 | if(menu.style.opacity <= 0) 265 | return this; 266 | 267 | this._tweenOpacity.stop().to({'opacity': '0'}, 100).start(); 268 | 269 | return this; 270 | } 271 | }); 272 | 273 | // Icon inject 274 | Icon.inject({ 275 | menu: function(popupmenu) { 276 | if(!(popupmenu instanceof PopupMenu)) return; 277 | 278 | this.menu = { 279 | menu: popupmenu, 280 | click: false 281 | }; 282 | 283 | this.node.addEventListener('mousedown', _.bind(function(e) { 284 | this.menu.click = true; 285 | }, this)); 286 | 287 | this.node.addEventListener('mouseup', _.bind(function(e) { 288 | e.preventDefault(); 289 | if(!this.drag.dragging && this.menu.click) { 290 | 291 | this.menu.click = false; 292 | var bbox = this.elements.icon.getBoundingClientRect(), 293 | loc = windowToElement(this.manager.parent, bbox.left, bbox.bottom); 294 | popupmenu.show(this, bbox.left + bbox.width / 2, bbox.top); 295 | } 296 | 297 | }, this)); 298 | 299 | return this; 300 | } 301 | }); 302 | 303 | // Module export 304 | module.exports = PopupMenu; 305 | }); -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Signal = require('./lib/signals'), 5 | Class = require('./class'), 6 | TWEEN = require('./tween'), 7 | Item = require('./item'), 8 | Drag = require('./drag'), 9 | Grid = require('./grid'); 10 | 11 | var SVG = "http://www.w3.org/2000/svg", 12 | desktopIdCounter = 1, 13 | idPrefix = 'jtop_desktop', 14 | type = 'DESKTOP'; 15 | 16 | function disableSelection(target){ 17 | target.onmousedown=function(){return false} 18 | target.style.cursor = "default"; 19 | }; 20 | 21 | function getIEVersion() { 22 | var rv = -1; // Return value assumes failure. 23 | if (navigator.appName == 'Microsoft Internet Explorer') 24 | { 25 | var ua = navigator.userAgent; 26 | var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); 27 | if (re.exec(ua) != null) 28 | rv = parseFloat( RegExp.$1 ); 29 | } 30 | return rv; 31 | }; 32 | 33 | var events = { 34 | create: new Signal() 35 | }; 36 | 37 | function createIndicator(x, y, w, h, persist) { 38 | var p = document.createElementNS(SVG, "rect"); 39 | svgSetXYWH(p, x, y, w, h); 40 | p.setAttribute('fill', 'rgba(255,255,255,1)'); 41 | p.style.opacity = 0.6; 42 | this.layers.indicatorLayer.appendChild(p); 43 | if (!persist) rampOpacityDownEx(p); 44 | return p; 45 | }; 46 | 47 | function rampOpacityDownEx(g) { 48 | var rampFunc = function () { 49 | var o = parseFloat(g.style.opacity) - 0.04; 50 | g.style.opacity = o; 51 | if (o > 0) 52 | setTimeout(rampFunc, 10); 53 | else 54 | g.parentNode.removeChild(g); 55 | 56 | } 57 | rampFunc(); 58 | }; 59 | 60 | // private Core functions 61 | function itemRemoved(item) { 62 | if(item) { 63 | item.on.remove.remove(itemRemoved); 64 | this.grid.removeValue(item.settings.gridX, item.settings.gridY, item); 65 | this.items[item.id] = null; 66 | delete this.items[item.id]; 67 | } 68 | }; 69 | 70 | var Core = Class.extend({ 71 | 72 | init: function(element, options) { 73 | 74 | var id = idPrefix + '_' + desktopIdCounter++, 75 | parent, 76 | node, 77 | indicatorLayer; 78 | 79 | this._settings = _.extend({ 80 | gridW: 100, 81 | gridH: 100 82 | }, options); 83 | 84 | if(_.isString(element)) { 85 | parent = document.getElementById(element); 86 | } else if(_.isElement(element)) { 87 | parent = element; 88 | } 89 | 90 | if(_.isNull(parent)) { 91 | console.error('Cannot create desktop. Specified DOM element does not exists.'); 92 | return; 93 | } 94 | 95 | node = document.createElementNS(SVG, 'svg'); 96 | node.setAttribute('xlmns', SVG); 97 | node.setAttribute('xmlns:xlink', SVG); 98 | node.setAttribute('id', id); 99 | node.style.width = '100%'; 100 | node.style.height = '100%'; 101 | parent.appendChild(node); 102 | 103 | indicatorLayer = document.createElementNS(SVG, 'g'); 104 | indicatorLayer.setAttribute('class', 'indicator-layer'); 105 | node.appendChild(indicatorLayer); 106 | 107 | disableSelection(parent); 108 | 109 | this.id = id; 110 | this.node = node; 111 | this.parent = parent; 112 | this.items = {}; 113 | this.type = type; 114 | this.grid = new Grid(); 115 | 116 | this.on = { 117 | addItem: new Signal(), 118 | removeItem: new Signal(), 119 | settings: new Signal(), 120 | change: new Signal(), 121 | dragOverItem: new Signal(), 122 | dragOutItem: new Signal(), 123 | dropInItem: new Signal(), 124 | dragStart: new Signal(), 125 | dragEnd: new Signal() 126 | }; 127 | 128 | this.layers = { 129 | indicatorLayer: indicatorLayer 130 | } 131 | 132 | Drag.asDragManager.call(this, node); 133 | Drag.asDropElement.call(this); 134 | this._drag.gridX = -1; 135 | this._drag.gridY = -1; 136 | events.create.dispatch(this); 137 | }, 138 | 139 | addItem: function(item, gridX, gridY) { 140 | if(item instanceof Item && _.isString(item.type) && !_.has(this.items, item.id)) { 141 | 142 | var o = this._settings; 143 | 144 | item.id = item.id || _.uniqueId('jtop_' + item.type + '_'); 145 | this.items[item.id] = item; 146 | 147 | if(_.isElement(item.node)) { 148 | addClass(item.node, 'ITEM'); 149 | addClass(item.node, item.type); 150 | item.node.setAttribute('id', item.id); 151 | item.parent = this; 152 | item.manager = this; 153 | item.on.remove.add(itemRemoved, this); 154 | this.node.appendChild(item.node); 155 | } 156 | 157 | if(_.isNumber(gridX) && _.isNumber(gridY)) { 158 | this.grid.setValue(gridX, gridY, item); 159 | item.settings.gridX = gridX; 160 | item.settings.gridY = gridY; 161 | item.pos(item.settings.gridX * o.gridW, item.settings.gridY * o.gridH); 162 | } 163 | 164 | this.on.addItem.dispatch(item); 165 | item.onAdd.call(item, this); 166 | 167 | return item; 168 | } 169 | }, 170 | 171 | removeItem: function(id) { 172 | var item; 173 | if(_.has(this.items, id)) 174 | item = this.items[id]; 175 | else if(id instanceof Item) 176 | item = id; 177 | 178 | if(item) { 179 | item.remove(); 180 | } 181 | }, 182 | 183 | dragStart: function(item, x, y) { 184 | 185 | if(!_.has(this.items, item.id)) return; 186 | this.on.dragStart.dispatch(item, x, y); 187 | 188 | if(item.type === 'ICON') { 189 | if(item.parent.id !== this.id) 190 | item.pos(item.transform.x + item.parent.transform.x, item.transform.y + item.parent.transform.y); 191 | 192 | item.parent.node.removeChild(item.node); 193 | item.parent = this; 194 | item.parent.node.appendChild(item.node); 195 | } 196 | }, 197 | 198 | dragEnd: function(item, x, y) { 199 | if(!_.has(this.items, item.id)) return; 200 | this.on.dragEnd.dispatch(item, x, y); 201 | }, 202 | 203 | dragOver: function(item, x, y) { 204 | if(item.type === 'ICON') { 205 | var self = this, 206 | o = this._settings, 207 | newposx = Math.floor(x / o.gridW), 208 | newposy = Math.floor(y / o.gridH); 209 | 210 | if(newposx !== this._drag.gridX || newposy !== this._drag.gridY) { 211 | 212 | this.on.dragOutItem.dispatch(item); 213 | var belowItem = this.grid.getValue(newposx, newposy); 214 | if(belowItem) { 215 | this.on.dragOverItem.dispatch(item, belowItem, x, y); 216 | } 217 | 218 | createIndicator.call(self, newposx * o.gridW, newposy * o.gridH, o.gridW, o.gridH); 219 | 220 | this._drag.gridX = newposx; 221 | this._drag.gridY = newposy; 222 | } 223 | } 224 | }, 225 | 226 | dragOut: function(item, x, y) { 227 | this.on.dragOutItem.dispatch(item); 228 | this._drag.gridX = -1; 229 | this._drag.gridY = -1; 230 | }, 231 | 232 | dropIn: function(item, x, y, dropBack) { 233 | if(item.type === 'ICON') { 234 | var o = this._settings, 235 | grid = this.grid, 236 | gridposx = Math.floor(x / o.gridW), 237 | gridposy = Math.floor(y / o.gridH); 238 | 239 | var belowItem = this.grid.getValue(gridposx, gridposy); 240 | grid.setValue(gridposx, gridposy, item); 241 | item.settings.gridX = gridposx; 242 | item.settings.gridY = gridposy; 243 | 244 | if(!dropBack) { 245 | item.posAnim(gridposx * o.gridW, gridposy * o.gridH); 246 | if(belowItem) { 247 | this.on.dropInItem.dispatch(item, belowItem); 248 | } 249 | } else { 250 | item.posAnim(gridposx * o.gridW, gridposy * o.gridH, 150, TWEEN.Easing.Linear.None); 251 | } 252 | 253 | return true; 254 | } 255 | }, 256 | 257 | dropOut: function(item, x, y) { 258 | if(item.type === 'ICON') { 259 | var o = this._settings, 260 | grid = this.grid; 261 | grid.removeValue(item.settings.gridX, item.settings.gridY, item); 262 | } 263 | }, 264 | 265 | getDropableItem: function(id) { 266 | if(this.id === id) 267 | return this; 268 | else 269 | return this.getItemById(id); 270 | }, 271 | 272 | getItemById: function(id) { 273 | if(_.has(this.items, id)) 274 | return this.items[id]; 275 | }, 276 | 277 | settings: function(val) { 278 | _.extend(this._settings, val); 279 | this.on.settings.dispatch(this._settings); 280 | }, 281 | 282 | destroy: function() { 283 | this.parent.removeChild(this.node); 284 | } 285 | }); 286 | 287 | // core helper functions 288 | function createSVGElement(type) { 289 | return document.createElementNS(SVG, type); 290 | }; 291 | 292 | function svgSetXYWH(el, x, y, w, h) { 293 | el.setAttribute("x", x); 294 | el.setAttribute("y", y); 295 | el.setAttribute("width", w); 296 | el.setAttribute("height", h); 297 | }; 298 | 299 | function pathRoundedRectangle(x, y, w, h, r1, r2, r3, r4) { 300 | var array = []; 301 | array = array.concat(["M",x,r1+y, "Q",x,y, x+r1,y]); //A 302 | array = array.concat(["L",x+w-r2,y, "Q",x+w,y, x+w,y+r2]); //B 303 | array = array.concat(["L",x+w,y+h-r3, "Q",x+w,y+h, x+w-r3,y+h]); //C 304 | array = array.concat(["L",x+r4,y+h, "Q",x,y+h, x,y+h-r4, "Z"]); //D 305 | 306 | return array.join(' '); 307 | }; 308 | 309 | function pathCross(x, y, w, h, width) { 310 | var array = []; 311 | array = array.concat(["M",0, (h - width) / 2, "L", (w - width) / 2, (h - width) / 2]); // 1 312 | array = array.concat(["L", (w - width) / 2, 0, "L", (w + width) / 2, 0]); // 2,3 313 | array = array.concat(["L", (w + width) / 2, (h - width) / 2, "L", w, (h - width) / 2]); // 4,5 314 | array = array.concat(["L", w, (h + width) / 2, "L", (w + width) / 2, (h + width) / 2]); // 5,6 315 | array = array.concat(["L", (w + width) / 2, h, "L", (w - width) / 2, h]); // 7,8 316 | array = array.concat(["L", (w - width) / 2, (h + width) / 2, "L", 0, (h + width) / 2, 'Z']); // 8,9 317 | return array.join(' '); 318 | }; 319 | 320 | function hasClass(ele,cls) { 321 | return ele.className.baseVal.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); 322 | }; 323 | 324 | function addClass(ele,cls) { 325 | if (!hasClass(ele,cls)) ele.className.baseVal += " "+cls; 326 | }; 327 | 328 | function removeClass(ele,cls) { 329 | if (hasClass(ele,cls)) { 330 | var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); 331 | ele.className = ele.className.baseVal.replace(reg,' '); 332 | } 333 | }; 334 | 335 | module.exports = { 336 | Core: Core, 337 | on: events, 338 | getIEVersion: getIEVersion, 339 | svgSetXYWH: svgSetXYWH, 340 | createSVGElement: createSVGElement, 341 | addClass: addClass, 342 | hasClass: hasClass, 343 | removeClass: removeClass, 344 | path: { 345 | roundedRectangle: pathRoundedRectangle, 346 | cross: pathCross 347 | } 348 | }; 349 | 350 | }); -------------------------------------------------------------------------------- /src/scrollview.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Signal = require('./lib/signals'), 5 | Class = require('./class'), 6 | core = require('./core'), 7 | TWEEN = require('./tween'); 8 | 9 | var settings = { 10 | activationArea: 40, 11 | transformAnimDuration: 500, 12 | transformAnimTween: TWEEN.Easing.Back.Out 13 | }; 14 | 15 | function hasClass(ele,cls) { 16 | return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); 17 | }; 18 | 19 | function addClass(ele,cls) { 20 | if (!hasClass(ele,cls)) ele.className += " "+cls; 21 | }; 22 | 23 | function removeClass(ele,cls) { 24 | if (hasClass(ele,cls)) { 25 | var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); 26 | ele.className = ele.className.replace(reg,' '); 27 | } 28 | }; 29 | 30 | function windowToElement(el, x, y) { 31 | var bbox = el.getBoundingClientRect(); 32 | return { x: x - bbox.left, y: y - bbox.top}; 33 | }; 34 | 35 | function onWindowResize(e) { 36 | 37 | var self = this, 38 | bbox = this.layer.getBoundingClientRect(); 39 | this.viewportWidth = window.innerWidth; 40 | this.viewportHeight = window.innerHeight; 41 | this.maxOffsetX = _.max([bbox.right - this.viewportWidth, 0]); 42 | this.maxOffsetY = _.max([bbox.bottom - this.viewportHeight, 0]); 43 | 44 | 45 | if(this.maxOffsetX > 0) { 46 | this.elements.htmlArrowLeft.style.display = 'none'; 47 | this.elements.htmlArrowRight.style.display = 'block'; 48 | } else { 49 | this.elements.htmlArrowLeft.style.display = 'none'; 50 | this.elements.htmlArrowRight.style.display = 'none'; 51 | } 52 | 53 | if(this.maxOffsetY > 0) { 54 | this.elements.htmlArrowUp.style.display = 'none'; 55 | this.elements.htmlArrowDown.style.display = 'block'; 56 | } else { 57 | this.elements.htmlArrowUp.style.display = 'none'; 58 | this.elements.htmlArrowDown.style.display = 'none'; 59 | } 60 | 61 | _.extend(this.elements.htmlArrowUp.style, { 62 | top: settings.activationArea / 2 + this.initY + 'px', 63 | left: (self.viewportWidth / 2 - 15) + 'px', 64 | }); 65 | 66 | _.extend(this.elements.htmlArrowDown.style, { 67 | bottom: settings.activationArea / 2 + 'px', 68 | left: (self.viewportWidth / 2 - 15) + 'px', 69 | }); 70 | 71 | _.extend(this.elements.htmlArrowLeft.style, { 72 | left: settings.activationArea + 'px', 73 | top: (this.viewportHeight / 2 - 15) + 'px', 74 | }); 75 | 76 | _.extend(this.elements.htmlArrowRight.style, { 77 | left: this.viewportWidth - settings.activationArea + 'px', 78 | top: (this.viewportHeight / 2 - 15) + 'px', 79 | }); 80 | 81 | this.on.resize.dispatch(this); 82 | }; 83 | 84 | function onMouseMove(e) { 85 | 86 | if(!this.enabled) return; 87 | var self = this; 88 | 89 | this.arrowsOpacityAnim(0.5); 90 | 91 | clearTimeout(this.hideTimeout); 92 | this.hideTimeout = setTimeout(function() { 93 | self.arrowsOpacityAnim(0); 94 | }, 1000); 95 | 96 | if(this.maxOffsetX > 0) { 97 | if(e.clientX >= 0 && e.clientX <= settings.activationArea + this.initY) { 98 | this.on.scroll.dispatch(this, this.SCROLL_DIRECTION.LEFT); 99 | this.posAnim(this.initX, this.transform.y, this.SCROLL_DIRECTION.LEFT); // left 100 | this.elements.htmlArrowLeft.style.display = 'none'; 101 | this.elements.htmlArrowRight.style.display = 'block'; 102 | } else if(e.clientX > (this.viewportWidth - settings.activationArea) && e.clientX <= this.viewportWidth) { 103 | this.on.scroll.dispatch(this, this.SCROLL_DIRECTION.RIGHT); 104 | this.posAnim(this.initX - this.maxOffsetX, this.transform.y, this.SCROLL_DIRECTION.RIGHT); // right 105 | this.elements.htmlArrowLeft.style.display = 'block'; 106 | this.elements.htmlArrowRight.style.display = 'none'; 107 | } 108 | } 109 | 110 | if(this.maxOffsetY > 0) { 111 | if(e.clientY >= 0 && e.clientY <= settings.activationArea + this.initY) { 112 | this.on.scroll.dispatch(this, this.SCROLL_DIRECTION.TOP); 113 | this.posAnim(this.transform.x, this.initY, this.SCROLL_DIRECTION.TOP); // top 114 | this.elements.htmlArrowUp.style.display = 'none'; 115 | this.elements.htmlArrowDown.style.display = 'block'; 116 | } else if(e.clientY > (this.viewportHeight - settings.activationArea) && e.clientY <= this.viewportHeight) { 117 | this.on.scroll.dispatch(this, this.SCROLL_DIRECTION.BOTTOM); 118 | this.posAnim(this.transform.x, this.initY - this.maxOffsetY, this.SCROLL_DIRECTION.BOTTOM); // bottom 119 | this.elements.htmlArrowUp.style.display = 'block'; 120 | this.elements.htmlArrowDown.style.display = 'none'; 121 | } 122 | } 123 | }; 124 | 125 | var ScrollView = Class.extend({ 126 | 127 | SCROLL_DIRECTION: { 128 | TOP: 1, 129 | LEFT: 2, 130 | BOTTOM: 3, 131 | RIGHT: 4, 132 | }, 133 | 134 | init: function(desktop) { 135 | 136 | var self = this; 137 | 138 | this.settings = _.defaults(desktop._settings.scrollView || {}, { 139 | initX: 0, 140 | initY: 0 141 | }); 142 | 143 | this.desktop = desktop; 144 | this.layer = desktop.parent; 145 | this.layers = []; 146 | this.enabled = true; 147 | 148 | window.addEventListener('resize', _.bind(onWindowResize, this)); 149 | window.addEventListener('mousemove', _.bind(onMouseMove, this)); 150 | 151 | var bbox = this.layer.getBoundingClientRect(); 152 | this.viewportWidth = window.innerWidth; 153 | this.viewportHeight = window.innerHeight; 154 | this.maxOffsetX = bbox.right - this.viewportWidth; 155 | this.maxOffsetY = bbox.bottom - this.viewportHeight; 156 | this.initX = this.settings.initX; 157 | this.initY = this.settings.initY; 158 | 159 | this.transform = { 160 | x: this.maxOffsetX > 0 ? bbox.left : self.initX, 161 | y: this.maxOffsetY > 0 ? bbox.top : self.initY 162 | }; 163 | 164 | this.on = { 165 | scroll: new Signal, 166 | resize: new Signal 167 | }; 168 | 169 | this.tweens = {}; 170 | this.posAnimState = false; 171 | 172 | this.tweens.pos = new TWEEN.Tween(this.transform) 173 | .easing(TWEEN.Easing.Elastic.Out) 174 | .onComplete(function() { 175 | self.posAnimState = false; 176 | }) 177 | .onUpdate(function() { 178 | if(self.maxOffsetX > 0) self.layer.style.left = this.x + 'px'; 179 | if(self.maxOffsetY > 0) self.layer.style.top = this.y + 'px'; 180 | 181 | for (var i = self.layers.length - 1; i >= 0; i--) { 182 | if(self.maxOffsetX > 0) self.layers[i].style.left = this.x + 'px'; 183 | if(self.maxOffsetY > 0) self.layers[i].style.top = this.y + 'px'; 184 | }; 185 | }); 186 | 187 | // arrows indicators 188 | this.elements = { 189 | htmlArrowUp: document.createElement('div'), 190 | htmlArrowDown: document.createElement('div'), 191 | htmlArrowLeft: document.createElement('div'), 192 | htmlArrowRight: document.createElement('div') 193 | } 194 | 195 | _.extend(this.elements.htmlArrowUp.style, { 196 | position: 'fixed', 197 | top: settings.activationArea / 2 + this.initY + 'px', 198 | left: (this.viewportWidth / 2 - 15) + 'px', 199 | 'border-color': 'transparent transparent #FFF transparent', 200 | 'opacity': 0 201 | }); 202 | 203 | _.extend(this.elements.htmlArrowDown.style, { 204 | position: 'fixed', 205 | bottom: settings.activationArea / 2 + 'px', 206 | left: (this.viewportWidth / 2 - 15) + 'px', 207 | 'border-color': '#FFF transparent transparent transparent', 208 | 'opacity': 0 209 | }); 210 | 211 | _.extend(this.elements.htmlArrowLeft.style, { 212 | position: 'fixed', 213 | left: settings.activationArea + 'px', 214 | top: (this.viewportHeight / 2 - 15) + 'px', 215 | 'border-color': 'transparent #FFF transparent transparent', 216 | 'opacity': 0 217 | }); 218 | 219 | _.extend(this.elements.htmlArrowRight.style, { 220 | position: 'fixed', 221 | left: this.viewportWidth - settings.activationArea + 'px', 222 | top: (this.viewportHeight / 2 - 15) + 'px', 223 | 'border-color': 'transparent transparent transparent #FFF', 224 | 'opacity': 0 225 | }); 226 | 227 | addClass(this.elements.htmlArrowUp, 'jtop-popupmenu-arrow'); 228 | addClass(this.elements.htmlArrowDown, 'jtop-popupmenu-arrow'); 229 | addClass(this.elements.htmlArrowLeft, 'jtop-popupmenu-arrow'); 230 | addClass(this.elements.htmlArrowRight, 'jtop-popupmenu-arrow'); 231 | 232 | document.body.appendChild(this.elements.htmlArrowUp); 233 | document.body.appendChild(this.elements.htmlArrowDown); 234 | document.body.appendChild(this.elements.htmlArrowLeft); 235 | document.body.appendChild(this.elements.htmlArrowRight); 236 | 237 | this.arrows = { 238 | opacity: 0 239 | }; 240 | 241 | this.arrowsShowing = false; 242 | this.arrowsHiding = false; 243 | this.hideTimeout = null; 244 | 245 | this.tweens.arrowOpacity = new TWEEN.Tween(this.arrows) 246 | .easing(TWEEN.Easing.Elastic.Out) 247 | .onComplete(function() { 248 | self.arrowsShowing = false; 249 | self.arrowsHiding = false; 250 | }) 251 | .onUpdate(function() { 252 | self.elements.htmlArrowUp.style.opacity = this.opacity; 253 | self.elements.htmlArrowDown.style.opacity = this.opacity; 254 | self.elements.htmlArrowLeft.style.opacity = this.opacity; 255 | self.elements.htmlArrowRight.style.opacity = this.opacity; 256 | }); 257 | 258 | onWindowResize.call(this); 259 | }, 260 | 261 | arrowsOpacityAnim: function(value, duration, easing) { 262 | 263 | if(this.arrowsHiding && value === 0) return; 264 | if(this.arrowsShowing && value > 0) return; 265 | 266 | if(value > 0) { 267 | this.arrowsHiding = false; 268 | this.arrowsShowing = true; 269 | } else { 270 | this.arrowsHiding = true; 271 | this.arrowsShowing = false; 272 | } 273 | 274 | this.tweens.arrowOpacity.stop().to({ 275 | opacity: value, 276 | }, duration || settings.transformAnimDuration) 277 | .easing(easing || settings.transformAnimTween) 278 | .start(); 279 | 280 | return this; 281 | }, 282 | 283 | posAnim: function(x, y, state, duration, easing) { 284 | 285 | if(this.posAnimState === state) return; 286 | this.posAnimState = state; 287 | 288 | this.tweens.pos.stop().to({ 289 | x: x, 290 | y: y 291 | }, duration || settings.transformAnimDuration) 292 | .easing(easing || settings.transformAnimTween) 293 | .start(); 294 | return this; 295 | }, 296 | 297 | addLayer: function(id, initX, initY) { 298 | 299 | var layer = document.getElementById(id); 300 | if(!layer) return; 301 | 302 | var bbox = layer.getBoundingClientRect(); 303 | layer.initX = initX || 0; 304 | layer.initY = initY || 0; 305 | 306 | this.layers.push(layer); 307 | onWindowResize.call(this); 308 | 309 | return layer; 310 | } 311 | 312 | }); 313 | 314 | core.Core.inject({ 315 | init: function(element, options) { 316 | this._super(element, options); 317 | this.scrollView = new ScrollView(this); 318 | } 319 | }); 320 | 321 | }); -------------------------------------------------------------------------------- /src/tween.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | // Eric Moller poyfill rAF 4 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 5 | 6 | (function() { 7 | var lastTime = 0; 8 | var vendors = ['ms', 'moz', 'webkit', 'o']; 9 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 10 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 11 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 12 | || window[vendors[x]+'CancelRequestAnimationFrame']; 13 | } 14 | 15 | if (!window.requestAnimationFrame) 16 | window.requestAnimationFrame = function(callback, element) { 17 | var currTime = new Date().getTime(); 18 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 19 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 20 | timeToCall); 21 | lastTime = currTime + timeToCall; 22 | return id; 23 | }; 24 | 25 | if (!window.cancelAnimationFrame) 26 | window.cancelAnimationFrame = function(id) { 27 | clearTimeout(id); 28 | }; 29 | }()); 30 | 31 | /** 32 | * @author sole / http://soledadpenades.com 33 | * @author mr.doob / http://mrdoob.com 34 | * @author Robert Eisele / http://www.xarg.org 35 | * @author Philippe / http://philippe.elsass.me 36 | * @author Robert Penner / http://www.robertpenner.com/easing_terms_of_use.html 37 | * @author Paul Lewis / http://www.aerotwist.com/ 38 | * @author lechecacharro 39 | * @author Josh Faul / http://jocafa.com/ 40 | * @author egraether / http://egraether.com/ 41 | */ 42 | var TWEEN = TWEEN || ( function () { 43 | var _tweens = []; 44 | return { 45 | REVISION: '6', 46 | getAll: function () { 47 | return _tweens; 48 | }, 49 | removeAll: function () { 50 | _tweens = []; 51 | }, 52 | add: function ( tween ) { 53 | _tweens.push( tween ); 54 | }, 55 | remove: function ( tween ) { 56 | var i = _tweens.indexOf( tween ); 57 | if ( i !== -1 ) { 58 | _tweens.splice( i, 1 ); 59 | } 60 | }, 61 | update: function ( time ) { 62 | var i = 0; 63 | var num_tweens = _tweens.length; 64 | var time = time !== undefined ? time : Date.now(); 65 | while ( i < num_tweens ) { 66 | if ( _tweens[ i ].update( time ) ) { 67 | i ++; 68 | } else { 69 | _tweens.splice( i, 1 ); 70 | num_tweens --; 71 | } 72 | } 73 | } 74 | }; 75 | } )(); 76 | TWEEN.Tween = function ( object ) { 77 | var _object = object; 78 | var _valuesStart = {}; 79 | var _valuesEnd = {}; 80 | var _duration = 1000; 81 | var _delayTime = 0; 82 | var _startTime = null; 83 | var _easingFunction = TWEEN.Easing.Linear.None; 84 | var _interpolationFunction = TWEEN.Interpolation.Linear; 85 | var _chainedTween = null; 86 | var _onUpdateCallback = null; 87 | var _onCompleteCallback = null; 88 | this.to = function ( properties, duration ) { 89 | if ( duration !== null ) { 90 | _duration = duration; 91 | } 92 | _valuesEnd = properties; 93 | return this; 94 | }; 95 | this.start = function ( time ) { 96 | TWEEN.add( this ); 97 | _startTime = time !== undefined ? time : Date.now(); 98 | _startTime += _delayTime; 99 | for ( var property in _valuesEnd ) { 100 | // This prevents the engine from interpolating null values 101 | if ( _object[ property ] === null ) { 102 | continue; 103 | } 104 | // check if an Array was provided as property value 105 | if ( _valuesEnd[ property ] instanceof Array ) { 106 | if ( _valuesEnd[ property ].length === 0 ) { 107 | continue; 108 | } 109 | // create a local copy of the Array with the start value at the front 110 | _valuesEnd[ property ] = [ _object[ property ] ].concat( _valuesEnd[ property ] ); 111 | } 112 | _valuesStart[ property ] = _object[ property ]; 113 | } 114 | return this; 115 | }; 116 | this.stop = function () { 117 | TWEEN.remove( this ); 118 | return this; 119 | }; 120 | this.delay = function ( amount ) { 121 | _delayTime = amount; 122 | return this; 123 | }; 124 | this.easing = function ( easing ) { 125 | _easingFunction = easing; 126 | return this; 127 | }; 128 | this.interpolation = function ( interpolation ) { 129 | _interpolationFunction = interpolation; 130 | return this; 131 | }; 132 | this.chain = function ( chainedTween ) { 133 | _chainedTween = chainedTween; 134 | return this; 135 | }; 136 | this.onUpdate = function ( onUpdateCallback ) { 137 | _onUpdateCallback = onUpdateCallback; 138 | return this; 139 | }; 140 | this.onComplete = function ( onCompleteCallback ) { 141 | _onCompleteCallback = onCompleteCallback; 142 | return this; 143 | }; 144 | this.update = function ( time ) { 145 | if ( time < _startTime ) { 146 | return true; 147 | } 148 | var elapsed = ( time - _startTime ) / _duration; 149 | elapsed = elapsed > 150 | 1 ? 1 : elapsed; 151 | var value = _easingFunction( elapsed ); 152 | for ( var property in _valuesStart ) { 153 | var start = _valuesStart[ property ]; 154 | var end = _valuesEnd[ property ]; 155 | if ( end instanceof Array ) { 156 | _object[ property ] = _interpolationFunction( end, value ); 157 | } else { 158 | _object[ property ] = start + ( end - start ) * value; 159 | } 160 | } 161 | if ( _onUpdateCallback !== null ) { 162 | _onUpdateCallback.call( _object, value ); 163 | } 164 | if ( elapsed == 1 ) { 165 | if ( _onCompleteCallback !== null ) { 166 | _onCompleteCallback.call( _object ); 167 | } 168 | if ( _chainedTween !== null ) { 169 | _chainedTween.start(); 170 | } 171 | return false; 172 | } 173 | return true; 174 | }; 175 | }; 176 | TWEEN.Easing = { 177 | Linear: { 178 | None: function ( k ) { 179 | return k; 180 | } 181 | }, 182 | Quadratic: { 183 | In: function ( k ) { 184 | return k * k; 185 | }, 186 | Out: function ( k ) { 187 | return k * ( 2 - k ); 188 | }, 189 | InOut: function ( k ) { 190 | if ( ( k *= 2 ) 191 | < 1 ) return 0.5 * k * k; 192 | return - 0.5 * ( --k * ( k - 2 ) - 1 ); 193 | } 194 | }, 195 | Cubic: { 196 | In: function ( k ) { 197 | return k * k * k; 198 | }, 199 | Out: function ( k ) { 200 | return --k * k * k + 1; 201 | }, 202 | InOut: function ( k ) { 203 | if ( ( k *= 2 ) < 1 ) return 0.5 * k * k * k; 204 | return 0.5 * ( ( k -= 2 ) * k * k + 2 ); 205 | } 206 | }, 207 | Quartic: { 208 | In: function ( k ) { 209 | return k * k * k * k; 210 | }, 211 | Out: function ( k ) { 212 | return 1 - --k * k * k * k; 213 | }, 214 | InOut: function ( k ) { 215 | if ( ( k *= 2 ) < 1) return 0.5 * k * k * k * k; 216 | return - 0.5 * ( ( k -= 2 ) * k * k * k - 2 ); 217 | } 218 | }, 219 | Quintic: { 220 | In: function ( k ) { 221 | return k * k * k * k * k; 222 | }, 223 | Out: function ( k ) { 224 | return --k * k * k * k * k + 1; 225 | }, 226 | InOut: function ( k ) { 227 | if ( ( k *= 2 ) < 1 ) return 0.5 * k * k * k * k * k; 228 | return 0.5 * ( ( k -= 2 ) * k * k * k * k + 2 ); 229 | } 230 | }, 231 | Sinusoidal: { 232 | In: function ( k ) { 233 | return 1 - Math.cos( k * Math.PI / 2 ); 234 | }, 235 | Out: function ( k ) { 236 | return Math.sin( k * Math.PI / 2 ); 237 | }, 238 | InOut: function ( k ) { 239 | return 0.5 * ( 1 - Math.cos( Math.PI * k ) ); 240 | } 241 | }, 242 | Exponential: { 243 | In: function ( k ) { 244 | return k === 0 ? 0 : Math.pow( 1024, k - 1 ); 245 | }, 246 | Out: function ( k ) { 247 | return k === 1 ? 1 : 1 - Math.pow( 2, - 10 * k ); 248 | }, 249 | InOut: function ( k ) { 250 | if ( k === 0 ) return 0; 251 | if ( k === 1 ) return 1; 252 | if ( ( k *= 2 ) < 1 ) return 0.5 * Math.pow( 1024, k - 1 ); 253 | return 0.5 * ( - Math.pow( 2, - 10 * ( k - 1 ) ) + 2 ); 254 | } 255 | }, 256 | Circular: { 257 | In: function ( k ) { 258 | return 1 - Math.sqrt( 1 - k * k ); 259 | }, 260 | Out: function ( k ) { 261 | return Math.sqrt( 1 - --k * k ); 262 | }, 263 | InOut: function ( k ) { 264 | if ( ( k *= 2 ) < 1) return - 0.5 * ( Math.sqrt( 1 - k * k) - 1); 265 | return 0.5 * ( Math.sqrt( 1 - ( k -= 2) * k) + 1); 266 | } 267 | }, 268 | Elastic: { 269 | In: function ( k ) { 270 | var s, a = 0.1, p = 0.4; 271 | if ( k === 0 ) return 0; 272 | if ( k === 1 ) return 1; 273 | if ( !a || a < 1 ) { a = 1; s = p / 4; } 274 | else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); 275 | return - ( a * Math.pow( 2, 10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) ); 276 | }, 277 | Out: function ( k ) { 278 | var s, a = 2, p = 0.4; 279 | if ( k === 0 ) return 0; 280 | if ( k === 1 ) return 1; 281 | if ( !a || a < 1 ) { a = 1; s = p / 4; } 282 | else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); 283 | return ( a * Math.pow( 2, - 10 * k) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) + 1 ); 284 | }, 285 | InOut: function ( k ) { 286 | var s, a = 0.1, p = 0.4; 287 | if ( k === 0 ) return 0; 288 | if ( k === 1 ) return 1; 289 | if ( !a || a < 1 ) { a = 1; s = p / 4; } 290 | else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); 291 | if ( ( k *= 2 ) < 1 ) return - 0.5 * ( a * Math.pow( 2, 10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) ); 292 | return a * Math.pow( 2, -10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) * 0.5 + 1; 293 | } 294 | }, 295 | Back: { 296 | In: function ( k ) { 297 | var s = 1.70158; 298 | return k * k * ( ( s + 1 ) * k - s ); 299 | }, 300 | Out: function ( k ) { 301 | var s = 1.70158; 302 | return --k * k * ( ( s + 1 ) * k + s ) + 1; 303 | }, 304 | InOut: function ( k ) { 305 | var s = 1.70158 * 1.525; 306 | if ( ( k *= 2 ) < 1 ) return 0.5 * ( k * k * ( ( s + 1 ) * k - s ) ); 307 | return 0.5 * ( ( k -= 2 ) * k * ( ( s + 1 ) * k + s ) + 2 ); 308 | } 309 | }, 310 | Bounce: { 311 | In: function ( k ) { 312 | return 1 - TWEEN.Easing.Bounce.Out( 1 - k ); 313 | }, 314 | Out: function ( k ) { 315 | if ( k < ( 1 / 2.75 ) ) { 316 | return 7.5625 * k * k; 317 | } else if ( k < ( 2 / 2.75 ) ) { 318 | return 7.5625 * ( k -= ( 1.5 / 2.75 ) ) * k + 0.75; 319 | } else if ( k < ( 2.5 / 2.75 ) ) { 320 | return 7.5625 * ( k -= ( 2.25 / 2.75 ) ) * k + 0.9375; 321 | } else { 322 | return 7.5625 * ( k -= ( 2.625 / 2.75 ) ) * k + 0.984375; 323 | } 324 | }, 325 | InOut: function ( k ) { 326 | if ( k < 0.5 ) return TWEEN.Easing.Bounce.In( k * 2 ) * 0.5; 327 | return TWEEN.Easing.Bounce.Out( k * 2 - 1 ) * 0.5 + 0.5; 328 | } 329 | } 330 | }; 331 | TWEEN.Interpolation = { 332 | Linear: function ( v, k ) { 333 | var m = v.length - 1, f = m * k, i = Math.floor( f ), fn = TWEEN.Interpolation.Utils.Linear; 334 | if ( k < 0 ) return fn( v[ 0 ], v[ 1 ], f ); 335 | if ( k > 336 | 1 ) return fn( v[ m ], v[ m - 1 ], m - f ); 337 | return fn( v[ i ], v[ i + 1 > m ? m : i + 1 ], f - i ); 338 | }, 339 | Bezier: function ( v, k ) { 340 | var b = 0, n = v.length - 1, pw = Math.pow, bn = TWEEN.Interpolation.Utils.Bernstein, i; 341 | for ( i = 0; i 342 | <= n; i++ ) { 343 | b += pw( 1 - k, n - i ) * pw( k, i ) * v[ i ] * bn( n, i ); 344 | } 345 | return b; 346 | }, 347 | CatmullRom: function ( v, k ) { 348 | var m = v.length - 1, f = m * k, i = Math.floor( f ), fn = TWEEN.Interpolation.Utils.CatmullRom; 349 | if ( v[ 0 ] === v[ m ] ) { 350 | if ( k < 0 ) i = Math.floor( f = m * ( 1 + k ) ); 351 | return fn( v[ ( i - 1 + m ) % m ], v[ i ], v[ ( i + 1 ) % m ], v[ ( i + 2 ) % m ], f - i ); 352 | } else { 353 | if ( k < 0 ) return v[ 0 ] - ( fn( v[ 0 ], v[ 0 ], v[ 1 ], v[ 1 ], -f ) - v[ 0 ] ); 354 | if ( k > 355 | 1 ) return v[ m ] - ( fn( v[ m ], v[ m ], v[ m - 1 ], v[ m - 1 ], f - m ) - v[ m ] ); 356 | return fn( v[ i ? i - 1 : 0 ], v[ i ], v[ m 357 | < i + 1 ? m : i + 1 ], v[ m < i + 2 ? m : i + 2 ], f - i ); 358 | } 359 | }, 360 | Utils: { 361 | Linear: function ( p0, p1, t ) { 362 | return ( p1 - p0 ) * t + p0; 363 | }, 364 | Bernstein: function ( n , i ) { 365 | var fc = TWEEN.Interpolation.Utils.Factorial; 366 | return fc( n ) / fc( i ) / fc( n - i ); 367 | }, 368 | Factorial: ( function () { 369 | var a = [ 1 ]; 370 | return function ( n ) { 371 | var s = 1, i; 372 | if ( a[ n ] ) return a[ n ]; 373 | for ( i = n; i > 374 | 1; i-- ) s *= i; 375 | return a[ n ] = s; 376 | } 377 | } )(), 378 | CatmullRom: function ( p0, p1, p2, p3, t ) { 379 | var v0 = ( p2 - p0 ) * 0.5, v1 = ( p3 - p1 ) * 0.5, t2 = t * t, t3 = t * t2; 380 | return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 + ( - 3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 + v0 * t + p1; 381 | } 382 | } 383 | }; 384 | 385 | (function animate() { 386 | requestAnimationFrame(animate); 387 | TWEEN.update(); 388 | })(); 389 | 390 | module.exports = TWEEN; 391 | }); -------------------------------------------------------------------------------- /references/photos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | \ 359 | -------------------------------------------------------------------------------- /lib/signals.js: -------------------------------------------------------------------------------- 1 | /*jslint onevar:true, undef:true, newcap:true, regexp:true, bitwise:true, maxerr:50, indent:4, white:false, nomen:false, plusplus:false */ 2 | /*global define:false, require:false, exports:false, module:false, signals:false */ 3 | 4 | /** @license 5 | * JS Signals 6 | * Released under the MIT license 7 | * Author: Miller Medeiros 8 | * Version: 0.8.1 - Build: 266 (2012/07/31 03:33 PM) 9 | */ 10 | 11 | (function(global){ 12 | 13 | // SignalBinding ------------------------------------------------- 14 | //================================================================ 15 | 16 | /** 17 | * Object that represents a binding between a Signal and a listener function. 18 | *
- This is an internal constructor and shouldn't be called by regular users. 19 | *
- inspired by Joa Ebert AS3 SignalBinding and Robert Penner's Slot classes. 20 | * @author Miller Medeiros 21 | * @constructor 22 | * @internal 23 | * @name SignalBinding 24 | * @param {Signal} signal Reference to Signal object that listener is currently bound to. 25 | * @param {Function} listener Handler function bound to the signal. 26 | * @param {boolean} isOnce If binding should be executed just once. 27 | * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 28 | * @param {Number} [priority] The priority level of the event listener. (default = 0). 29 | */ 30 | function SignalBinding(signal, listener, isOnce, listenerContext, priority) { 31 | 32 | /** 33 | * Handler function bound to the signal. 34 | * @type Function 35 | * @private 36 | */ 37 | this._listener = listener; 38 | 39 | /** 40 | * If binding should be executed just once. 41 | * @type boolean 42 | * @private 43 | */ 44 | this._isOnce = isOnce; 45 | 46 | /** 47 | * Context on which listener will be executed (object that should represent the `this` variable inside listener function). 48 | * @memberOf SignalBinding.prototype 49 | * @name context 50 | * @type Object|undefined|null 51 | */ 52 | this.context = listenerContext; 53 | 54 | /** 55 | * Reference to Signal object that listener is currently bound to. 56 | * @type Signal 57 | * @private 58 | */ 59 | this._signal = signal; 60 | 61 | /** 62 | * Listener priority 63 | * @type Number 64 | * @private 65 | */ 66 | this._priority = priority || 0; 67 | } 68 | 69 | SignalBinding.prototype = { 70 | 71 | /** 72 | * If binding is active and should be executed. 73 | * @type boolean 74 | */ 75 | active : true, 76 | 77 | /** 78 | * Default parameters passed to listener during `Signal.dispatch` and `SignalBinding.execute`. (curried parameters) 79 | * @type Array|null 80 | */ 81 | params : null, 82 | 83 | /** 84 | * Call listener passing arbitrary parameters. 85 | *

If binding was added using `Signal.addOnce()` it will be automatically removed from signal dispatch queue, this method is used internally for the signal dispatch.

86 | * @param {Array} [paramsArr] Array of parameters that should be passed to the listener 87 | * @return {*} Value returned by the listener. 88 | */ 89 | execute : function (paramsArr) { 90 | var handlerReturn, params; 91 | if (this.active && !!this._listener) { 92 | params = this.params? this.params.concat(paramsArr) : paramsArr; 93 | handlerReturn = this._listener.apply(this.context, params); 94 | if (this._isOnce) { 95 | this.detach(); 96 | } 97 | } 98 | return handlerReturn; 99 | }, 100 | 101 | /** 102 | * Detach binding from signal. 103 | * - alias to: mySignal.remove(myBinding.getListener()); 104 | * @return {Function|null} Handler function bound to the signal or `null` if binding was previously detached. 105 | */ 106 | detach : function () { 107 | return this.isBound()? this._signal.remove(this._listener, this.context) : null; 108 | }, 109 | 110 | /** 111 | * @return {Boolean} `true` if binding is still bound to the signal and have a listener. 112 | */ 113 | isBound : function () { 114 | return (!!this._signal && !!this._listener); 115 | }, 116 | 117 | /** 118 | * @return {Function} Handler function bound to the signal. 119 | */ 120 | getListener : function () { 121 | return this._listener; 122 | }, 123 | 124 | /** 125 | * Delete instance properties 126 | * @private 127 | */ 128 | _destroy : function () { 129 | delete this._signal; 130 | delete this._listener; 131 | delete this.context; 132 | }, 133 | 134 | /** 135 | * @return {boolean} If SignalBinding will only be executed once. 136 | */ 137 | isOnce : function () { 138 | return this._isOnce; 139 | }, 140 | 141 | /** 142 | * @return {string} String representation of the object. 143 | */ 144 | toString : function () { 145 | return '[SignalBinding isOnce:' + this._isOnce +', isBound:'+ this.isBound() +', active:' + this.active + ']'; 146 | } 147 | 148 | }; 149 | 150 | 151 | /*global SignalBinding:false*/ 152 | 153 | // Signal -------------------------------------------------------- 154 | //================================================================ 155 | 156 | function validateListener(listener, fnName) { 157 | if (typeof listener !== 'function') { 158 | throw new Error( 'listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName) ); 159 | } 160 | } 161 | 162 | /** 163 | * Custom event broadcaster 164 | *
- inspired by Robert Penner's AS3 Signals. 165 | * @name Signal 166 | * @author Miller Medeiros 167 | * @constructor 168 | */ 169 | function Signal() { 170 | /** 171 | * @type Array. 172 | * @private 173 | */ 174 | this._bindings = []; 175 | this._prevParams = null; 176 | } 177 | 178 | Signal.prototype = { 179 | 180 | /** 181 | * Signals Version Number 182 | * @type String 183 | * @const 184 | */ 185 | VERSION : '0.8.1', 186 | 187 | /** 188 | * If Signal should keep record of previously dispatched parameters and 189 | * automatically execute listener during `add()`/`addOnce()` if Signal was 190 | * already dispatched before. 191 | * @type boolean 192 | */ 193 | memorize : false, 194 | 195 | /** 196 | * @type boolean 197 | * @private 198 | */ 199 | _shouldPropagate : true, 200 | 201 | /** 202 | * If Signal is active and should broadcast events. 203 | *

IMPORTANT: Setting this property during a dispatch will only affect the next dispatch, if you want to stop the propagation of a signal use `halt()` instead.

204 | * @type boolean 205 | */ 206 | active : true, 207 | 208 | /** 209 | * @param {Function} listener 210 | * @param {boolean} isOnce 211 | * @param {Object} [listenerContext] 212 | * @param {Number} [priority] 213 | * @return {SignalBinding} 214 | * @private 215 | */ 216 | _registerListener : function (listener, isOnce, listenerContext, priority) { 217 | 218 | var prevIndex = this._indexOfListener(listener, listenerContext), 219 | binding; 220 | 221 | if (prevIndex !== -1) { 222 | binding = this._bindings[prevIndex]; 223 | if (binding.isOnce() !== isOnce) { 224 | throw new Error('You cannot add'+ (isOnce? '' : 'Once') +'() then add'+ (!isOnce? '' : 'Once') +'() the same listener without removing the relationship first.'); 225 | } 226 | } else { 227 | binding = new SignalBinding(this, listener, isOnce, listenerContext, priority); 228 | this._addBinding(binding); 229 | } 230 | 231 | if(this.memorize && this._prevParams){ 232 | binding.execute(this._prevParams); 233 | } 234 | 235 | return binding; 236 | }, 237 | 238 | /** 239 | * @param {SignalBinding} binding 240 | * @private 241 | */ 242 | _addBinding : function (binding) { 243 | //simplified insertion sort 244 | var n = this._bindings.length; 245 | do { --n; } while (this._bindings[n] && binding._priority <= this._bindings[n]._priority); 246 | this._bindings.splice(n + 1, 0, binding); 247 | }, 248 | 249 | /** 250 | * @param {Function} listener 251 | * @return {number} 252 | * @private 253 | */ 254 | _indexOfListener : function (listener, context) { 255 | var n = this._bindings.length, 256 | cur; 257 | while (n--) { 258 | cur = this._bindings[n]; 259 | if (cur._listener === listener && cur.context === context) { 260 | return n; 261 | } 262 | } 263 | return -1; 264 | }, 265 | 266 | /** 267 | * Check if listener was attached to Signal. 268 | * @param {Function} listener 269 | * @param {Object} [context] 270 | * @return {boolean} if Signal has the specified listener. 271 | */ 272 | has : function (listener, context) { 273 | return this._indexOfListener(listener, context) !== -1; 274 | }, 275 | 276 | /** 277 | * Add a listener to the signal. 278 | * @param {Function} listener Signal handler function. 279 | * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 280 | * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) 281 | * @return {SignalBinding} An Object representing the binding between the Signal and listener. 282 | */ 283 | add : function (listener, listenerContext, priority) { 284 | validateListener(listener, 'add'); 285 | return this._registerListener(listener, false, listenerContext, priority); 286 | }, 287 | 288 | /** 289 | * Add listener to the signal that should be removed after first execution (will be executed only once). 290 | * @param {Function} listener Signal handler function. 291 | * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). 292 | * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) 293 | * @return {SignalBinding} An Object representing the binding between the Signal and listener. 294 | */ 295 | addOnce : function (listener, listenerContext, priority) { 296 | validateListener(listener, 'addOnce'); 297 | return this._registerListener(listener, true, listenerContext, priority); 298 | }, 299 | 300 | /** 301 | * Remove a single listener from the dispatch queue. 302 | * @param {Function} listener Handler function that should be removed. 303 | * @param {Object} [context] Execution context (since you can add the same handler multiple times if executing in a different context). 304 | * @return {Function} Listener handler function. 305 | */ 306 | remove : function (listener, context) { 307 | validateListener(listener, 'remove'); 308 | 309 | var i = this._indexOfListener(listener, context); 310 | if (i !== -1) { 311 | this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal 312 | this._bindings.splice(i, 1); 313 | } 314 | return listener; 315 | }, 316 | 317 | /** 318 | * Remove all listeners from the Signal. 319 | */ 320 | removeAll : function () { 321 | var n = this._bindings.length; 322 | while (n--) { 323 | this._bindings[n]._destroy(); 324 | } 325 | this._bindings.length = 0; 326 | }, 327 | 328 | /** 329 | * @return {number} Number of listeners attached to the Signal. 330 | */ 331 | getNumListeners : function () { 332 | return this._bindings.length; 333 | }, 334 | 335 | /** 336 | * Stop propagation of the event, blocking the dispatch to next listeners on the queue. 337 | *

IMPORTANT: should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.

338 | * @see Signal.prototype.disable 339 | */ 340 | halt : function () { 341 | this._shouldPropagate = false; 342 | }, 343 | 344 | /** 345 | * Dispatch/Broadcast Signal to all listeners added to the queue. 346 | * @param {...*} [params] Parameters that should be passed to each handler. 347 | */ 348 | dispatch : function (params) { 349 | if (! this.active) { 350 | return; 351 | } 352 | 353 | var paramsArr = Array.prototype.slice.call(arguments), 354 | n = this._bindings.length, 355 | bindings; 356 | 357 | if (this.memorize) { 358 | this._prevParams = paramsArr; 359 | } 360 | 361 | if (! n) { 362 | //should come after memorize 363 | return; 364 | } 365 | 366 | bindings = this._bindings.slice(); //clone array in case add/remove items during dispatch 367 | this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch. 368 | 369 | //execute all callbacks until end of the list or until a callback returns `false` or stops propagation 370 | //reverse loop since listeners with higher priority will be added at the end of the list 371 | do { n--; } while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false); 372 | }, 373 | 374 | /** 375 | * Forget memorized arguments. 376 | * @see Signal.memorize 377 | */ 378 | forget : function(){ 379 | this._prevParams = null; 380 | }, 381 | 382 | /** 383 | * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). 384 | *

IMPORTANT: calling any method on the signal instance after calling dispose will throw errors.

385 | */ 386 | dispose : function () { 387 | this.removeAll(); 388 | delete this._bindings; 389 | delete this._prevParams; 390 | }, 391 | 392 | /** 393 | * @return {string} String representation of the object. 394 | */ 395 | toString : function () { 396 | return '[Signal active:'+ this.active +' numListeners:'+ this.getNumListeners() +']'; 397 | } 398 | 399 | }; 400 | 401 | 402 | // Namespace ----------------------------------------------------- 403 | //================================================================ 404 | 405 | /** 406 | * Signals namespace 407 | * @namespace 408 | * @name signals 409 | */ 410 | var signals = Signal; 411 | 412 | /** 413 | * Custom event broadcaster 414 | * @see Signal 415 | */ 416 | // alias for backwards compatibility (see #gh-44) 417 | signals.Signal = Signal; 418 | 419 | 420 | 421 | //exports to multiple environments 422 | if(typeof define === 'function' && define.amd){ //AMD 423 | define(function () { return signals; }); 424 | } else if (typeof module !== 'undefined' && module.exports){ //node 425 | module.exports = signals; 426 | } else { //browser 427 | //use string because of Google closure compiler ADVANCED_MODE 428 | /*jslint sub:true */ 429 | global['signals'] = signals; 430 | } 431 | 432 | }(this)); 433 | -------------------------------------------------------------------------------- /src/item.panel.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | var _ = require('./lib/underscore'), 4 | Signal = require('./lib/signals'), 5 | core = require('./core'), 6 | Item = require('./item'), 7 | Text = require('./text'), 8 | Drag = require('./drag'), 9 | TWEEN = require('./tween'), 10 | Grid = require('./grid'), 11 | Offset = require('./offset'); 12 | 13 | var XLINK = 'http://www.w3.org/1999/xlink', 14 | _type = 'PANEL'; 15 | 16 | var settings = { 17 | fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana', 18 | fontSize: 15, 19 | mouseOverSpeed: 200, 20 | mouseOutSpeed: 200, 21 | mouseOverOpacity: 0.75, 22 | mouseOutOpacity: 0.6, 23 | textMargin: 25, 24 | alignDistance: 10, 25 | alignIndicatorDelay: 70, 26 | alignIndicator: false 27 | }; 28 | 29 | function buildTransform(t) { 30 | return ['translate(', t.x, ',', t.y, ') rotate(', t.r, ') scale(', t.s, ')'].join(''); 31 | }; 32 | 33 | function buildTransformEx(x, y, r, s) { 34 | return ['translate(', x, ',', y, ') rotate(', r, ') scale(', s, ')'].join(''); 35 | }; 36 | 37 | function windowToElement(el, x, y) { 38 | var bbox = el.getBoundingClientRect(); 39 | return { x: x - bbox.left, y: y - bbox.top}; 40 | }; 41 | 42 | function hasClass(ele,cls) { 43 | return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); 44 | }; 45 | 46 | function addClass(ele,cls) { 47 | if (!hasClass(ele,cls)) ele.className += " "+cls; 48 | }; 49 | 50 | function removeClass(ele,cls) { 51 | if (hasClass(ele,cls)) { 52 | var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); 53 | ele.className = ele.className.replace(reg,' '); 54 | } 55 | }; 56 | 57 | function createIndicator(el, x1, y1, x2, y2) { 58 | var line = core.createSVGElement('line'); 59 | line.setAttribute('x1', x1); 60 | line.setAttribute('y1', y1); 61 | line.setAttribute('x2', x2); 62 | line.setAttribute('y2', y2); 63 | line.setAttribute('stroke', 'rgba(255,255,255,1)'); 64 | line.setAttribute('stroke-width', 2); 65 | line.setAttribute('stroke-dasharray', 3); 66 | line.style.opacity = 0.7; 67 | el.appendChild(line); 68 | rampOpacityDownEx(line); 69 | }; 70 | 71 | function rampOpacityDownEx(g) { 72 | var rampFunc = function () { 73 | var o = parseFloat(g.style.opacity) - 0.03; 74 | g.style.opacity = o; 75 | if (o > 0) 76 | setTimeout(rampFunc, 10); 77 | else 78 | g.parentNode.removeChild(g); 79 | 80 | } 81 | rampFunc(); 82 | }; 83 | 84 | // Resizer 85 | var Resizer = Item.extend({ 86 | 87 | init: function(options) { 88 | 89 | this._super(); 90 | 91 | var o = this.settings = _.defaults(options || {}, { 92 | radius: 10 93 | }); 94 | 95 | _.extend(this.on, { 96 | resizeStart: new Signal, 97 | resize: new Signal, 98 | resizeEnd: new Signal 99 | }); 100 | 101 | var g = core.createSVGElement('g'); 102 | handle = core.createSVGElement('circle'); 103 | 104 | handle.setAttribute('r', o.radius); 105 | handle.setAttribute('fill', '#000'); 106 | handle.setAttribute('stroke', '#000'); 107 | handle.setAttribute('stroke-width', 1); 108 | handle.setAttribute('opacity', 0); 109 | handle.setAttribute('transform', 'translate(' + (-o.radius) + ',' + (0) + ')'); 110 | 111 | g.appendChild(handle); 112 | g.style.cursor = 'nw-resize'; 113 | 114 | this.node = g; 115 | 116 | Drag.asDragElement.call(this, { 117 | handle: g, 118 | checkDragOver: false, 119 | checkDropOver: false 120 | }); 121 | }, 122 | 123 | dragStart: function(x, y) { 124 | this.on.resizeStart.dispatch(x, y, x - this.parent.transform.x, y - this.parent.transform.y); 125 | this.dragManager.node.style.cursor = 'nw-resize'; 126 | }, 127 | 128 | dragEnd: function(x, y) { 129 | this.on.resizeEnd.dispatch(x, y, x - this.parent.transform.x, y - this.parent.transform.y); 130 | }, 131 | 132 | dragMove: function(x, y) { 133 | this.on.resize.dispatch(x, y, x - this.parent.transform.x, y - this.parent.transform.y); 134 | this.dragManager.node.style.cursor = 'nw-resize'; 135 | } 136 | 137 | }); 138 | 139 | // Panel 140 | function measureElements() { 141 | var node = this.node, 142 | container = this.elements.container, 143 | topPanel = this.elements.topPanel, 144 | title = this.elements.title, 145 | titleTop = this.elements.titleTop, 146 | titleShadow = this.elements.titleShadow, 147 | bottomPanel = this.elements.bottomPanel, 148 | containerBackground = this.elements.containerBackground, 149 | topBackground = this.elements.topBackground, 150 | bottomBackground = this.elements.bottomBackground, 151 | resizer = this.resizer, 152 | o = this.settings; 153 | 154 | core.svgSetXYWH(containerBackground, 0, 0, o.width, o.height); 155 | topBackground.setAttribute('d', core.path.roundedRectangle(0, 0, o.width, o.topPanelHeight, 6, 6, 0, 0)); 156 | bottomBackground.setAttribute('d', core.path.roundedRectangle(0, 0, o.width, o.bottomPanelHeight, 0, 0, 6, 6)); 157 | 158 | Text.addEllipseText(titleTop, o.title, o.width - 2 * o.textMargin); 159 | titleShadow.textContent = titleTop.textContent; 160 | title.setAttribute('transform', 'translate(' + o.width / 2 + ', ' + o.textOffsetTop + ')'); 161 | topPanel.setAttribute('transform', 'translate(0, ' + (-o.topPanelHeight) + ')'); 162 | bottomPanel.setAttribute('transform', 'translate(0, ' + (o.height) + ')'); 163 | resizer.pos(o.width, o.height); 164 | }; 165 | 166 | function onMouseOver() { 167 | var topBackground = this.elements.topBackground; 168 | this.tweens.hover.stop().to({ o: settings.mouseOverOpacity }, settings.mouseOverSpeed).start(); 169 | }; 170 | 171 | function onMouseOut() { 172 | var topBackground = this.elements.topBackground; 173 | this.tweens.hover.stop().to({ o: settings.mouseOutOpacity }, settings.mouseOutSpeed).start(); 174 | }; 175 | 176 | function onInlineEditInit(e) { 177 | 178 | var title = this.elements.title, 179 | topPanel = this.elements.topPanel, 180 | o = this.settings, 181 | textValue = o.title, 182 | self = this, 183 | input, 184 | bbox, 185 | bboxText, 186 | loc, 187 | locText; 188 | 189 | if(o.inlineEdit !== true) return; 190 | 191 | // prevent edit after drag (Fiirefox) 192 | if(this.drag._afterDrag === true) { 193 | this.drag._afterDrag = false; 194 | return; 195 | } 196 | 197 | bbox = topPanel.getBoundingClientRect(); 198 | loc = windowToElement(this.manager.parent, bbox.left, bbox.top); 199 | bboxText = title.getBoundingClientRect(); 200 | locText = windowToElement(this.manager.parent, bboxText.left, bboxText.top); 201 | 202 | input = this._inlineInput = document.createElement("input"); 203 | input.type = "text"; 204 | input.className = 'jt-panel-edit-inline'; 205 | input.maxLength = 50; 206 | input.value = o.title; 207 | 208 | this.on.changed.active = false; 209 | this.title(''); 210 | this.settings.title = input.value; 211 | this.on.changed.active = true; 212 | 213 | input.addEventListener('keydown', function(e) { 214 | if (e.keyCode == 13) { 215 | onInlineEditEnd.call(self, e); 216 | } else if (e.keyCode == 27) { 217 | this.value = textValue; 218 | onInlineEditEnd.call(self, e); 219 | } 220 | }); 221 | 222 | _.extend(input.style, { 223 | display: 'block', 224 | backgroundColor: 'transparent', 225 | outline: 'none', 226 | position: 'absolute', 227 | left: loc.x + o.textMargin + 'px', 228 | top: locText.y + 'px', 229 | border: 'none', 230 | width: o.width - 2 * o.textMargin + 'px', 231 | color: '#FFF', 232 | fontFamily: o.fontFamily, 233 | fontSize: o.fontSize + 'px', 234 | textAlign: 'center', 235 | margin: 0, 236 | padding: 0 237 | }); 238 | 239 | this.manager.parent.appendChild(input); 240 | input.focus(); 241 | input.select(); 242 | }; 243 | 244 | function onInlineEditEnd(e) { 245 | 246 | if(this.settings.inlineEdit !== true) return; 247 | 248 | if(this._inlineInput) { 249 | var input = this._inlineInput; 250 | 251 | if(this.settings.title !== input.value && input.value !== '') { 252 | this.title(input.value); 253 | } else { 254 | this.on.changed.active = false; 255 | this.title(this.settings.title); 256 | this.on.changed.active = true; 257 | } 258 | 259 | this.manager.parent.removeChild(input); 260 | this._inlineInput = null; 261 | } 262 | } 263 | 264 | var Panel = Item.extend({ 265 | 266 | panels: [], // static panels reference 267 | 268 | init: function(options) { 269 | this._super(options); 270 | 271 | var o = _.extend(this.settings, { 272 | title: '', 273 | width: 200, 274 | height: 160, 275 | minWidh: 100, 276 | minHeight: 80, 277 | gridW: 100, 278 | gridH: 80, 279 | fontFamily: settings['fontFamily'], 280 | fontSize: settings['fontSize'], 281 | topPanelHeight: 25, 282 | bottomPanelHeight: 6, 283 | textOffsetTop: 18, 284 | textMargin: settings['textMargin'], 285 | inlineEdit: true 286 | }, options); 287 | 288 | this.type = _type; 289 | this.items = {}; 290 | this.grid = new Grid(); 291 | 292 | _.extend(this.on, { 293 | changed: new Signal() 294 | }); 295 | 296 | // panel basic svg elements 297 | var g = core.createSVGElement('g'), 298 | container = core.createSVGElement('g'), 299 | containerBackground = core.createSVGElement('rect'), 300 | topPanel = core.createSVGElement('g'), 301 | topBackground = core.createSVGElement('path'), 302 | bottomPanel = core.createSVGElement('g'), 303 | bottomBackground = core.createSVGElement('path'), 304 | title = core.createSVGElement('g'), 305 | titleTop = core.createSVGElement('text'), 306 | titleShadow = core.createSVGElement('text'); 307 | 308 | core.svgSetXYWH(containerBackground, 0, 0, o.width, o.height); 309 | containerBackground.setAttribute('fill', '#000'); 310 | containerBackground.setAttribute('opacity', '0.4'); 311 | container.appendChild(containerBackground); 312 | 313 | titleTop.setAttribute('font-family', o.fontFamily); 314 | titleTop.setAttribute('font-size', o.fontSize); 315 | titleTop.setAttribute('fill', '#FFF'); 316 | titleTop.setAttribute('text-anchor', 'middle'); 317 | 318 | titleShadow.setAttribute('font-family', o.fontFamily); 319 | titleShadow.setAttribute('font-size', o.fontSize); 320 | titleShadow.setAttribute('fill', '#000'); 321 | titleShadow.setAttribute('stroke', '#000'); 322 | titleShadow.setAttribute('stroke-width', 2.6); 323 | titleShadow.setAttribute('stroke-opacity', 0.5); 324 | titleShadow.setAttribute('text-anchor', 'middle'); 325 | titleShadow.setAttribute('transform', 'translate(1, 1)'); 326 | 327 | title.setAttribute('class', 'text'); 328 | title.style['-webkit-user-select'] = 'none'; 329 | title.style['-moz-user-select'] = 'none'; 330 | title.appendChild(titleShadow); 331 | title.appendChild(titleTop); 332 | 333 | topBackground.setAttribute('d', core.path.roundedRectangle(0, 0, o.width, o.topPanelHeight, 6, 6, 0, 0)); 334 | topBackground.setAttribute('fill', '#000'); 335 | topBackground.setAttribute('opacity', settings.mouseOutOpacity); 336 | topBackground.opacity = settings.mouseOutOpacity; // tween value 337 | topPanel.appendChild(topBackground); 338 | topPanel.appendChild(title); 339 | 340 | bottomBackground.setAttribute('d', core.path.roundedRectangle(0, 0, o.width, o.bottomPanelHeight, 0, 0, 6, 6)); 341 | bottomBackground.setAttribute('fill', '#000'); 342 | bottomBackground.setAttribute('opacity', '0.4'); 343 | bottomPanel.appendChild(bottomBackground); 344 | 345 | // drag indicators 346 | var indicators = core.createSVGElement('g'), 347 | indicator = core.createSVGElement('path'); 348 | 349 | indicator.setAttribute('stroke', '#000'); 350 | indicator.setAttribute('stroke-width', 1.0); 351 | indicator.setAttribute('stroke-opacity', 0); 352 | indicator.setAttribute('fill', '#000'); 353 | indicator.setAttribute('fill-opacity', 0.3); 354 | //indicator.setAttribute('d', core.path.roundedRectangle(0, 0, o.gridW, o.gridH, 0, 0, 0, 0)); 355 | //indicator.setAttribute('d', core.path.cross(0, 0, o.gridW / 3, o.gridH / 3, 10)); 356 | //indicator.setAttribute('transform', 'translate(' + (o.gridW - o.gridW / 3) / 2 + ', ' + (o.gridH - o.gridH / 3) / 2 + ')'); 357 | indicator.setAttribute('opacity', '0'); 358 | indicators.setAttribute('transform-origin', '50 50'); 359 | indicators.appendChild(indicator); 360 | 361 | g.appendChild(container); 362 | g.appendChild(bottomPanel); 363 | g.appendChild(topPanel); 364 | g.appendChild(indicators); 365 | 366 | //events handling 367 | g.addEventListener('mouseover', _.bind(onMouseOver, this)); 368 | g.addEventListener('mouseout', _.bind(onMouseOut, this)); 369 | 370 | document.addEventListener('mousedown', _.bind(onInlineEditEnd, this)); 371 | topPanel.addEventListener('click', _.bind(onInlineEditInit, this)); 372 | 373 | this.node = g; 374 | this.elements = { 375 | container: container, 376 | containerBackground: containerBackground, 377 | topPanel: topPanel, 378 | topBackground: topBackground, 379 | bottomPanel: bottomPanel, 380 | bottomBackground: bottomBackground, 381 | title: title, 382 | titleTop: titleTop, 383 | titleShadow: titleShadow, 384 | indicators: indicators, 385 | indicator: indicator 386 | }; 387 | 388 | //resizer 389 | var resizer = this.resizer = new Resizer(); 390 | 391 | resizer.parent = this; 392 | g.appendChild(resizer.node); 393 | resizer.on.resizeStart.add(_.bind(this._resizeStart, this)); 394 | resizer.on.resizeEnd.add(_.bind(this._resizeEnd, this)); 395 | resizer.on.resize.add(_.bind(this._resize, this)); 396 | 397 | Drag.asDragElement.call(this, { 398 | handle: topPanel, 399 | checkDragOver: false, 400 | checkDropOver: false 401 | }); 402 | Drag.asDropElement.call(this, { 403 | gridX: -1, 404 | gridY: -1 405 | }); 406 | 407 | this.panels.push(this); 408 | }, 409 | 410 | title: function(val) { 411 | if(_.isString(val)) { 412 | this.settings.title = val; 413 | this.invalidate(); 414 | this.on.changed.dispatch({ key: 'title', value: val }); 415 | } 416 | 417 | return this; 418 | }, 419 | 420 | resize: function(enabled) { 421 | 422 | if(_.isUndefined(enabled)) return this.resizer.drag.enabled; 423 | 424 | if(enabled === true) { 425 | this.resizer.drag.enabled = true; 426 | this.resizer.node.style.cursor = 'nw-resize'; 427 | } else { 428 | this.resizer.drag.enabled = false; 429 | this.resizer.node.style.cursor = 'default'; 430 | } 431 | }, 432 | 433 | getBoundingBox: function() { 434 | return { 435 | x: this.transform.x, 436 | y: this.transform.y - this.settings.topPanelHeight, 437 | w: this._bbox.width, 438 | h: this._bbox.height 439 | }; 440 | }, 441 | 442 | onAdd: function(manager) { 443 | manager.registerDragItem(this.resizer); 444 | this.invalidate(); 445 | 446 | // hover effect 447 | var topBackground = this.elements.topBackground; 448 | this.tweens.hover = new TWEEN.Tween({ o: topBackground.opacity }) 449 | .easing(TWEEN.Easing.Linear.None) 450 | .onUpdate(function() { 451 | topBackground.opacity = this.o; 452 | topBackground.setAttribute('opacity', topBackground.opacity); 453 | }); 454 | 455 | // drag indicator animation 456 | var indicator = this.elements.indicator; 457 | indicator.t = { 458 | o: 0.2, 459 | s: 1, 460 | r: 0 461 | }; 462 | this.tweens.indicator = new TWEEN.Tween(indicator.t) 463 | .to({ s: 1.5, o: 1, r: 360 }, 300) 464 | .easing(TWEEN.Easing.Linear.None) 465 | .onUpdate(function() { 466 | indicator.setAttribute('opacity', this.o); 467 | }); 468 | this.tweens.indicatorBack = new TWEEN.Tween(indicator.t) 469 | .to({ s: 1, o: 0.2, r: 0}, 300) 470 | .easing(TWEEN.Easing.Linear.None) 471 | .onUpdate(function() { 472 | indicator.setAttribute('opacity', this.o); 473 | }); 474 | 475 | this.tweens.indicatorBack.chain(this.tweens.indicator); 476 | this.tweens.indicator.chain(this.tweens.indicatorBack); 477 | }, 478 | 479 | dragOver: function(item, x, y) { 480 | if(item.type === 'ICON') { 481 | 482 | var o = this.settings, 483 | newposx = Math.floor((x - this.transform.x) / o.gridW), 484 | newposy = Math.floor((y - this.transform.y) / o.gridH), 485 | indicator = this.elements.indicator, 486 | indicators = this.elements.indicators; 487 | 488 | newposx = newposx < 0 ? 0 : (newposx * o.gridW >= o.width ? o.width / o.gridW - 1 : newposx); 489 | newposy = newposy < 0 ? 0 : (newposy * o.gridH >= o.height ? o.height / o.gridH - 1 : newposy); 490 | 491 | if(newposx !== this.drop.gridX || newposy !== this.drop.gridY) { 492 | 493 | // indicators.setAttribute('opacity', '1'); 494 | // indicators.setAttribute('transform', 'translate(' + newposx * o.gridW + ', ' + newposy * o.gridH + ')'); 495 | // indicator.t.o = 0.2; 496 | // this.tweens.indicator.start(); 497 | this.drop.gridX = newposx; 498 | this.drop.gridY = newposy; 499 | } 500 | 501 | var belowItem = this.grid.getValue(newposx, newposy); 502 | if(belowItem) { 503 | this.manager.node.style.cursor = 'not-allowed'; // item not accepted cursor 504 | } else { 505 | this.manager.node.style.cursor = 'move'; 506 | } 507 | 508 | this.tweens.hover.stop().to({ o: settings.mouseOverOpacity }, settings.mouseOverSpeed).start(); 509 | } 510 | }, 511 | 512 | dragOut: function(item, x, y) { 513 | if(item.type === 'ICON') { 514 | 515 | var indicators = this.elements.indicators; 516 | this.drop.gridX = -1; 517 | this.drop.gridY = -1; 518 | // indicators.setAttribute('opacity', '0'); 519 | // this.tweens.indicator.stop(); 520 | // this.tweens.indicatorBack.stop(); 521 | 522 | this.manager.node.style.cursor = 'move'; // cursor not allowed clear 523 | this.tweens.hover.stop().to({ o: settings.mouseOutOpacity }, settings.mouseOutSpeed).start(); 524 | } 525 | }, 526 | 527 | dropIn: function(item, x, y) { 528 | if(item.type === 'ICON') { 529 | var o = this.settings 530 | indicators = this.elements.indicators, 531 | relx = x - this.transform.x, 532 | rely = y - this.transform.y, 533 | newposx = Math.floor((x - this.transform.x) / o.gridW), 534 | newposy = Math.floor((y - this.transform.y) / o.gridH); 535 | 536 | newposx = newposx < 0 ? 0 : (newposx * o.gridW >= o.width ? o.width / o.gridW - 1 : newposx); 537 | newposy = newposy < 0 ? 0 : (newposy * o.gridH >= o.height ? o.height / o.gridH - 1 : newposy); 538 | 539 | //measure item position 540 | var belowItem = this.grid.getValue(newposx, newposy); 541 | if(belowItem) return false; // item not accepted 542 | 543 | this.drop.gridX = newposx; 544 | this.drop.gridY = newposy; 545 | 546 | item.pos(item.transform.x - this.transform.x, item.transform.y - this.transform.y); 547 | item.posAnim(this.drop.gridX * o.gridW, this.drop.gridY * o.gridH, 150, TWEEN.Easing.Linear.None); 548 | this.addItem(item, this.drop.gridX, this.drop.gridY, false); 549 | 550 | // reset 551 | this.drop.gridX = -1; 552 | this.drop.gridY = -1; 553 | indicators.setAttribute('opacity', '0'); 554 | this.tweens.indicator.stop(); 555 | this.tweens.indicatorBack.stop(); 556 | 557 | return true; 558 | } 559 | }, 560 | 561 | dropOut: function(item, x, y) { 562 | var o = this.settings; 563 | if(item.type === 'ICON') { 564 | this.removeItem(item); 565 | } 566 | }, 567 | 568 | dragStart: function(x, y) { 569 | this.drag.alignX = false; 570 | this.drag.alignY = false; 571 | }, 572 | 573 | dragEnd: function(x, y) { 574 | this.pos(this.drag.alignX ? this.drag.alignPosX : this.transform.x, 575 | this.drag.alignY ? this.drag.alignPosY : this.transform.y); 576 | 577 | // set this flag to prevent inline edit after drag in some browsers (Firefox) 578 | var self = this; 579 | this.drag._afterDrag = true; 580 | _.defer(function() { 581 | self.drag._afterDrag = false; // set after drag to false on next thick (Prevent force to double click to edit in other than FF browsers) 582 | }); 583 | }, 584 | 585 | dragMove: function(x, y) { 586 | // panel alignment 587 | var self = this; 588 | 589 | var relatePanelY = _.find(this.panels, function(panel) { 590 | return panel !== self && Math.abs(self.transform.y - panel.transform.y) < settings.alignDistance 591 | }); 592 | 593 | if(!_.isUndefined(relatePanelY)) { 594 | 595 | if(!this.drag.alignY) { 596 | this.drag.alignY = true; 597 | settings.alignIndicator && setTimeout(function() { 598 | self.drag.alignY && createIndicator(self.manager.node, 599 | 0, relatePanelY.transform.y - relatePanelY.settings.topPanelHeight, 600 | self.manager.parent.offsetWidth, relatePanelY.transform.y - relatePanelY.settings.topPanelHeight); 601 | }, settings.alignIndicatorDelay); 602 | } 603 | 604 | this.drag.alignPosY = relatePanelY.transform.y; 605 | } else { 606 | this.drag.alignY = false; 607 | } 608 | 609 | var relatePanelX = _.find(this.panels, function(panel) { 610 | return panel !== self && Math.abs(self.transform.x - panel.transform.x) < settings.alignDistance 611 | }); 612 | 613 | if(!_.isUndefined(relatePanelX)) { 614 | 615 | if(!this.drag.alignX) { 616 | this.drag.alignX = true; 617 | settings.alignIndicator && setTimeout(function() { 618 | self.drag.alignX && createIndicator(self.manager.node, 619 | relatePanelX.transform.x, 0, 620 | relatePanelX.transform.x, self.manager.parent.offsetHeight); 621 | }, settings.alignIndicatorDelay); 622 | } 623 | 624 | this.drag.alignX = true; 625 | this.drag.alignPosX = relatePanelX.transform.x; 626 | } else { 627 | this.drag.alignX = false; 628 | } 629 | 630 | var posx = this.drag.alignX ? this.drag.alignPosX : this.transform.x, 631 | posy = this.drag.alignY ? this.drag.alignPosY : this.transform.y; 632 | 633 | this.node.setAttribute('transform', buildTransform({ 634 | x: posx, 635 | y: posy, 636 | s: self.transform.s, 637 | r: self.transform.r 638 | })); 639 | }, 640 | 641 | addItem: function(item, gridX, gridY, changePos) { 642 | if(!_.has(this.items, item.id)) { 643 | this.items[item.id] = item; 644 | item.on.remove.add(this.removeItem, this); 645 | 646 | if(item.parent.grid instanceof Grid) { 647 | item.parent.grid.removeValue(item.settings.gridX, item.settings.gridY, item); 648 | } 649 | 650 | item.parent.node.removeChild(item.node); 651 | item.parent = this; 652 | item.parent.node.appendChild(item.node); 653 | 654 | this.grid.setValue(gridX, gridY, item); 655 | item.settings.gridX = _.defaults(gridX, item.settings.gridX); 656 | item.settings.gridY = _.defaults(gridY, item.settings.gridY); 657 | 658 | var o = this.settings, 659 | maxX = this.grid.getColumnCount(), 660 | maxY = this.grid.getRowCount(), 661 | changePos = _.defaults(changePos, true); 662 | 663 | o.minWidh = o.gridW * maxX; 664 | o.minHeight = o.gridH * maxY; 665 | 666 | if(changePos) { 667 | item.pos(item.settings.gridX * o.gridW, item.settings.gridY * o.gridH); 668 | } 669 | } 670 | }, 671 | 672 | removeItem: function(item) { 673 | if(_.has(this.items, item.id)) { 674 | item.on.remove.remove(this.removeItem); 675 | this.grid.removeValue(item.settings.gridX, item.settings.gridY, item); 676 | 677 | var o = this.settings, 678 | maxX = this.grid.getColumnCount(), 679 | maxY = this.grid.getRowCount(); 680 | 681 | o.minWidh = maxX > 0 ? o.gridW * maxX : o.gridW; 682 | o.minHeight = maxY > 0 ? o.gridH * maxY : o.gridH; 683 | 684 | this.items[item.id] = null; 685 | delete this.items[item.id]; 686 | } 687 | }, 688 | 689 | _resizeStart: function(x, y, ix, iy) {}, 690 | 691 | _resizeEnd: function(x, y, ix, iy) { 692 | var self = this, 693 | o = this.settings, 694 | resizer = this.resizer; 695 | 696 | ix = _.max([o.minWidh, ix]); 697 | iy = _.max([o.minHeight, iy]); 698 | 699 | ix = Math.floor((ix + o.gridW / 2) / o.gridW) * o.gridW; 700 | iy = Math.floor((iy + o.gridH / 2) / o.gridH) * o.gridH; 701 | 702 | o.width = ix; 703 | o.height = iy; 704 | 705 | this.invalidate(); 706 | this.on.changed.dispatch({ key: 'size', value: {width: self.settings.width, height: self.settings.height }}); 707 | }, 708 | 709 | _resize: function(x, y, ix, iy) { 710 | 711 | var o = this.settings; 712 | 713 | ix = _.max([o.minWidh, ix]); 714 | iy = _.max([o.minHeight, iy]); 715 | 716 | o.width = ix; 717 | o.height = iy; 718 | 719 | this.invalidate(); 720 | }, 721 | 722 | invalidate: function() { 723 | measureElements.call(this); 724 | this._super(); 725 | } 726 | 727 | }); 728 | 729 | core.Core.inject({ 730 | panel: function(options) { 731 | return this.addItem(new Panel(options)); 732 | } 733 | }); 734 | 735 | module.exports = Panel; 736 | 737 | }); -------------------------------------------------------------------------------- /lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.3 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | 9 | (function() { 10 | 11 | // Baseline setup 12 | // -------------- 13 | 14 | // Establish the root object, `window` in the browser, or `global` on the server. 15 | var root = this; 16 | 17 | // Save the previous value of the `_` variable. 18 | var previousUnderscore = root._; 19 | 20 | // Establish the object that gets returned to break out of a loop iteration. 21 | var breaker = {}; 22 | 23 | // Save bytes in the minified (but not gzipped) version: 24 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 25 | 26 | // Create quick reference variables for speed access to core prototypes. 27 | var slice = ArrayProto.slice, 28 | unshift = ArrayProto.unshift, 29 | toString = ObjProto.toString, 30 | hasOwnProperty = ObjProto.hasOwnProperty; 31 | 32 | // All **ECMAScript 5** native function implementations that we hope to use 33 | // are declared here. 34 | var 35 | nativeForEach = ArrayProto.forEach, 36 | nativeMap = ArrayProto.map, 37 | nativeReduce = ArrayProto.reduce, 38 | nativeReduceRight = ArrayProto.reduceRight, 39 | nativeFilter = ArrayProto.filter, 40 | nativeEvery = ArrayProto.every, 41 | nativeSome = ArrayProto.some, 42 | nativeIndexOf = ArrayProto.indexOf, 43 | nativeLastIndexOf = ArrayProto.lastIndexOf, 44 | nativeIsArray = Array.isArray, 45 | nativeKeys = Object.keys, 46 | nativeBind = FuncProto.bind; 47 | 48 | // Create a safe reference to the Underscore object for use below. 49 | var _ = function(obj) { return new wrapper(obj); }; 50 | 51 | // Export the Underscore object for **Node.js**, with 52 | // backwards-compatibility for the old `require()` API. If we're in 53 | // the browser, add `_` as a global object via a string identifier, 54 | // for Closure Compiler "advanced" mode. 55 | if (typeof exports !== 'undefined') { 56 | if (typeof module !== 'undefined' && module.exports) { 57 | exports = module.exports = _; 58 | } 59 | exports._ = _; 60 | } else { 61 | root['_'] = _; 62 | } 63 | 64 | // Current version. 65 | _.VERSION = '1.3.3'; 66 | 67 | // Collection Functions 68 | // -------------------- 69 | 70 | // The cornerstone, an `each` implementation, aka `forEach`. 71 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 72 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 73 | var each = _.each = _.forEach = function(obj, iterator, context) { 74 | if (obj == null) return; 75 | if (nativeForEach && obj.forEach === nativeForEach) { 76 | obj.forEach(iterator, context); 77 | } else if (obj.length === +obj.length) { 78 | for (var i = 0, l = obj.length; i < l; i++) { 79 | if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; 80 | } 81 | } else { 82 | for (var key in obj) { 83 | if (_.has(obj, key)) { 84 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 85 | } 86 | } 87 | } 88 | }; 89 | 90 | // Return the results of applying the iterator to each element. 91 | // Delegates to **ECMAScript 5**'s native `map` if available. 92 | _.map = _.collect = function(obj, iterator, context) { 93 | var results = []; 94 | if (obj == null) return results; 95 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 96 | each(obj, function(value, index, list) { 97 | results[results.length] = iterator.call(context, value, index, list); 98 | }); 99 | if (obj.length === +obj.length) results.length = obj.length; 100 | return results; 101 | }; 102 | 103 | // **Reduce** builds up a single result from a list of values, aka `inject`, 104 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 105 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 106 | var initial = arguments.length > 2; 107 | if (obj == null) obj = []; 108 | if (nativeReduce && obj.reduce === nativeReduce) { 109 | if (context) iterator = _.bind(iterator, context); 110 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 111 | } 112 | each(obj, function(value, index, list) { 113 | if (!initial) { 114 | memo = value; 115 | initial = true; 116 | } else { 117 | memo = iterator.call(context, memo, value, index, list); 118 | } 119 | }); 120 | if (!initial) throw new TypeError('Reduce of empty array with no initial value'); 121 | return memo; 122 | }; 123 | 124 | // The right-associative version of reduce, also known as `foldr`. 125 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 126 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 127 | var initial = arguments.length > 2; 128 | if (obj == null) obj = []; 129 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 130 | if (context) iterator = _.bind(iterator, context); 131 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 132 | } 133 | var reversed = _.toArray(obj).reverse(); 134 | if (context && !initial) iterator = _.bind(iterator, context); 135 | return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); 136 | }; 137 | 138 | // Return the first value which passes a truth test. Aliased as `detect`. 139 | _.find = _.detect = function(obj, iterator, context) { 140 | var result; 141 | any(obj, function(value, index, list) { 142 | if (iterator.call(context, value, index, list)) { 143 | result = value; 144 | return true; 145 | } 146 | }); 147 | return result; 148 | }; 149 | 150 | // Return all the elements that pass a truth test. 151 | // Delegates to **ECMAScript 5**'s native `filter` if available. 152 | // Aliased as `select`. 153 | _.filter = _.select = function(obj, iterator, context) { 154 | var results = []; 155 | if (obj == null) return results; 156 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 157 | each(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) results[results.length] = value; 159 | }); 160 | return results; 161 | }; 162 | 163 | // Return all the elements for which a truth test fails. 164 | _.reject = function(obj, iterator, context) { 165 | var results = []; 166 | if (obj == null) return results; 167 | each(obj, function(value, index, list) { 168 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 169 | }); 170 | return results; 171 | }; 172 | 173 | // Determine whether all of the elements match a truth test. 174 | // Delegates to **ECMAScript 5**'s native `every` if available. 175 | // Aliased as `all`. 176 | _.every = _.all = function(obj, iterator, context) { 177 | var result = true; 178 | if (obj == null) return result; 179 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 180 | each(obj, function(value, index, list) { 181 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 182 | }); 183 | return !!result; 184 | }; 185 | 186 | // Determine if at least one element in the object matches a truth test. 187 | // Delegates to **ECMAScript 5**'s native `some` if available. 188 | // Aliased as `any`. 189 | var any = _.some = _.any = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = false; 192 | if (obj == null) return result; 193 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if a given value is included in the array or object using `===`. 201 | // Aliased as `contains`. 202 | _.include = _.contains = function(obj, target) { 203 | var found = false; 204 | if (obj == null) return found; 205 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 206 | found = any(obj, function(value) { 207 | return value === target; 208 | }); 209 | return found; 210 | }; 211 | 212 | // Invoke a method (with arguments) on every item in a collection. 213 | _.invoke = function(obj, method) { 214 | var args = slice.call(arguments, 2); 215 | return _.map(obj, function(value) { 216 | return (_.isFunction(method) ? method || value : value[method]).apply(value, args); 217 | }); 218 | }; 219 | 220 | // Convenience version of a common use case of `map`: fetching a property. 221 | _.pluck = function(obj, key) { 222 | return _.map(obj, function(value){ return value[key]; }); 223 | }; 224 | 225 | // Return the maximum element or (element-based computation). 226 | _.max = function(obj, iterator, context) { 227 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.max.apply(Math, obj); 228 | if (!iterator && _.isEmpty(obj)) return -Infinity; 229 | var result = {computed : -Infinity}; 230 | each(obj, function(value, index, list) { 231 | var computed = iterator ? iterator.call(context, value, index, list) : value; 232 | computed >= result.computed && (result = {value : value, computed : computed}); 233 | }); 234 | return result.value; 235 | }; 236 | 237 | // Return the minimum element (or element-based computation). 238 | _.min = function(obj, iterator, context) { 239 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.min.apply(Math, obj); 240 | if (!iterator && _.isEmpty(obj)) return Infinity; 241 | var result = {computed : Infinity}; 242 | each(obj, function(value, index, list) { 243 | var computed = iterator ? iterator.call(context, value, index, list) : value; 244 | computed < result.computed && (result = {value : value, computed : computed}); 245 | }); 246 | return result.value; 247 | }; 248 | 249 | // Shuffle an array. 250 | _.shuffle = function(obj) { 251 | var shuffled = [], rand; 252 | each(obj, function(value, index, list) { 253 | rand = Math.floor(Math.random() * (index + 1)); 254 | shuffled[index] = shuffled[rand]; 255 | shuffled[rand] = value; 256 | }); 257 | return shuffled; 258 | }; 259 | 260 | // Sort the object's values by a criterion produced by an iterator. 261 | _.sortBy = function(obj, val, context) { 262 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 263 | return _.pluck(_.map(obj, function(value, index, list) { 264 | return { 265 | value : value, 266 | criteria : iterator.call(context, value, index, list) 267 | }; 268 | }).sort(function(left, right) { 269 | var a = left.criteria, b = right.criteria; 270 | if (a === void 0) return 1; 271 | if (b === void 0) return -1; 272 | return a < b ? -1 : a > b ? 1 : 0; 273 | }), 'value'); 274 | }; 275 | 276 | // Groups the object's values by a criterion. Pass either a string attribute 277 | // to group by, or a function that returns the criterion. 278 | _.groupBy = function(obj, val) { 279 | var result = {}; 280 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 281 | each(obj, function(value, index) { 282 | var key = iterator(value, index); 283 | (result[key] || (result[key] = [])).push(value); 284 | }); 285 | return result; 286 | }; 287 | 288 | // Use a comparator function to figure out at what index an object should 289 | // be inserted so as to maintain order. Uses binary search. 290 | _.sortedIndex = function(array, obj, iterator) { 291 | iterator || (iterator = _.identity); 292 | var low = 0, high = array.length; 293 | while (low < high) { 294 | var mid = (low + high) >> 1; 295 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 296 | } 297 | return low; 298 | }; 299 | 300 | // Safely convert anything iterable into a real, live array. 301 | _.toArray = function(obj) { 302 | if (!obj) return []; 303 | if (_.isArray(obj)) return slice.call(obj); 304 | if (_.isArguments(obj)) return slice.call(obj); 305 | if (obj.toArray && _.isFunction(obj.toArray)) return obj.toArray(); 306 | return _.values(obj); 307 | }; 308 | 309 | // Return the number of elements in an object. 310 | _.size = function(obj) { 311 | return _.isArray(obj) ? obj.length : _.keys(obj).length; 312 | }; 313 | 314 | // Array Functions 315 | // --------------- 316 | 317 | // Get the first element of an array. Passing **n** will return the first N 318 | // values in the array. Aliased as `head` and `take`. The **guard** check 319 | // allows it to work with `_.map`. 320 | _.first = _.head = _.take = function(array, n, guard) { 321 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 322 | }; 323 | 324 | // Returns everything but the last entry of the array. Especcialy useful on 325 | // the arguments object. Passing **n** will return all the values in 326 | // the array, excluding the last N. The **guard** check allows it to work with 327 | // `_.map`. 328 | _.initial = function(array, n, guard) { 329 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 330 | }; 331 | 332 | // Get the last element of an array. Passing **n** will return the last N 333 | // values in the array. The **guard** check allows it to work with `_.map`. 334 | _.last = function(array, n, guard) { 335 | if ((n != null) && !guard) { 336 | return slice.call(array, Math.max(array.length - n, 0)); 337 | } else { 338 | return array[array.length - 1]; 339 | } 340 | }; 341 | 342 | // Returns everything but the first entry of the array. Aliased as `tail`. 343 | // Especially useful on the arguments object. Passing an **index** will return 344 | // the rest of the values in the array from that index onward. The **guard** 345 | // check allows it to work with `_.map`. 346 | _.rest = _.tail = function(array, index, guard) { 347 | return slice.call(array, (index == null) || guard ? 1 : index); 348 | }; 349 | 350 | // Trim out all falsy values from an array. 351 | _.compact = function(array) { 352 | return _.filter(array, function(value){ return !!value; }); 353 | }; 354 | 355 | // Return a completely flattened version of an array. 356 | _.flatten = function(array, shallow) { 357 | return _.reduce(array, function(memo, value) { 358 | if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); 359 | memo[memo.length] = value; 360 | return memo; 361 | }, []); 362 | }; 363 | 364 | // Return a version of the array that does not contain the specified value(s). 365 | _.without = function(array) { 366 | return _.difference(array, slice.call(arguments, 1)); 367 | }; 368 | 369 | // Produce a duplicate-free version of the array. If the array has already 370 | // been sorted, you have the option of using a faster algorithm. 371 | // Aliased as `unique`. 372 | _.uniq = _.unique = function(array, isSorted, iterator) { 373 | var initial = iterator ? _.map(array, iterator) : array; 374 | var results = []; 375 | // The `isSorted` flag is irrelevant if the array only contains two elements. 376 | if (array.length < 3) isSorted = true; 377 | _.reduce(initial, function (memo, value, index) { 378 | if (isSorted ? _.last(memo) !== value || !memo.length : !_.include(memo, value)) { 379 | memo.push(value); 380 | results.push(array[index]); 381 | } 382 | return memo; 383 | }, []); 384 | return results; 385 | }; 386 | 387 | // Produce an array that contains the union: each distinct element from all of 388 | // the passed-in arrays. 389 | _.union = function() { 390 | return _.uniq(_.flatten(arguments, true)); 391 | }; 392 | 393 | // Produce an array that contains every item shared between all the 394 | // passed-in arrays. (Aliased as "intersect" for back-compat.) 395 | _.intersection = _.intersect = function(array) { 396 | var rest = slice.call(arguments, 1); 397 | return _.filter(_.uniq(array), function(item) { 398 | return _.every(rest, function(other) { 399 | return _.indexOf(other, item) >= 0; 400 | }); 401 | }); 402 | }; 403 | 404 | // Take the difference between one array and a number of other arrays. 405 | // Only the elements present in just the first array will remain. 406 | _.difference = function(array) { 407 | var rest = _.flatten(slice.call(arguments, 1), true); 408 | return _.filter(array, function(value){ return !_.include(rest, value); }); 409 | }; 410 | 411 | // Zip together multiple lists into a single array -- elements that share 412 | // an index go together. 413 | _.zip = function() { 414 | var args = slice.call(arguments); 415 | var length = _.max(_.pluck(args, 'length')); 416 | var results = new Array(length); 417 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 418 | return results; 419 | }; 420 | 421 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 422 | // we need this function. Return the position of the first occurrence of an 423 | // item in an array, or -1 if the item is not included in the array. 424 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 425 | // If the array is large and already in sort order, pass `true` 426 | // for **isSorted** to use binary search. 427 | _.indexOf = function(array, item, isSorted) { 428 | if (array == null) return -1; 429 | var i, l; 430 | if (isSorted) { 431 | i = _.sortedIndex(array, item); 432 | return array[i] === item ? i : -1; 433 | } 434 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 435 | for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; 436 | return -1; 437 | }; 438 | 439 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 440 | _.lastIndexOf = function(array, item) { 441 | if (array == null) return -1; 442 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 443 | var i = array.length; 444 | while (i--) if (i in array && array[i] === item) return i; 445 | return -1; 446 | }; 447 | 448 | // Generate an integer Array containing an arithmetic progression. A port of 449 | // the native Python `range()` function. See 450 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 451 | _.range = function(start, stop, step) { 452 | if (arguments.length <= 1) { 453 | stop = start || 0; 454 | start = 0; 455 | } 456 | step = arguments[2] || 1; 457 | 458 | var len = Math.max(Math.ceil((stop - start) / step), 0); 459 | var idx = 0; 460 | var range = new Array(len); 461 | 462 | while(idx < len) { 463 | range[idx++] = start; 464 | start += step; 465 | } 466 | 467 | return range; 468 | }; 469 | 470 | // Function (ahem) Functions 471 | // ------------------ 472 | 473 | // Reusable constructor function for prototype setting. 474 | var ctor = function(){}; 475 | 476 | // Create a function bound to a given object (assigning `this`, and arguments, 477 | // optionally). Binding with arguments is also known as `curry`. 478 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 479 | // We check for `func.bind` first, to fail fast when `func` is undefined. 480 | _.bind = function bind(func, context) { 481 | var bound, args; 482 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 483 | if (!_.isFunction(func)) throw new TypeError; 484 | args = slice.call(arguments, 2); 485 | return bound = function() { 486 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 487 | ctor.prototype = func.prototype; 488 | var self = new ctor; 489 | var result = func.apply(self, args.concat(slice.call(arguments))); 490 | if (Object(result) === result) return result; 491 | return self; 492 | }; 493 | }; 494 | 495 | // Bind all of an object's methods to that object. Useful for ensuring that 496 | // all callbacks defined on an object belong to it. 497 | _.bindAll = function(obj) { 498 | var funcs = slice.call(arguments, 1); 499 | if (funcs.length == 0) funcs = _.functions(obj); 500 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 501 | return obj; 502 | }; 503 | 504 | // Memoize an expensive function by storing its results. 505 | _.memoize = function(func, hasher) { 506 | var memo = {}; 507 | hasher || (hasher = _.identity); 508 | return function() { 509 | var key = hasher.apply(this, arguments); 510 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 511 | }; 512 | }; 513 | 514 | // Delays a function for the given number of milliseconds, and then calls 515 | // it with the arguments supplied. 516 | _.delay = function(func, wait) { 517 | var args = slice.call(arguments, 2); 518 | return setTimeout(function(){ return func.apply(null, args); }, wait); 519 | }; 520 | 521 | // Defers a function, scheduling it to run after the current call stack has 522 | // cleared. 523 | _.defer = function(func) { 524 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 525 | }; 526 | 527 | // Returns a function, that, when invoked, will only be triggered at most once 528 | // during a given window of time. 529 | _.throttle = function(func, wait) { 530 | var context, args, timeout, throttling, more, result; 531 | var whenDone = _.debounce(function(){ more = throttling = false; }, wait); 532 | return function() { 533 | context = this; args = arguments; 534 | var later = function() { 535 | timeout = null; 536 | if (more) func.apply(context, args); 537 | whenDone(); 538 | }; 539 | if (!timeout) timeout = setTimeout(later, wait); 540 | if (throttling) { 541 | more = true; 542 | } else { 543 | result = func.apply(context, args); 544 | } 545 | whenDone(); 546 | throttling = true; 547 | return result; 548 | }; 549 | }; 550 | 551 | // Returns a function, that, as long as it continues to be invoked, will not 552 | // be triggered. The function will be called after it stops being called for 553 | // N milliseconds. If `immediate` is passed, trigger the function on the 554 | // leading edge, instead of the trailing. 555 | _.debounce = function(func, wait, immediate) { 556 | var timeout; 557 | return function() { 558 | var context = this, args = arguments; 559 | var later = function() { 560 | timeout = null; 561 | if (!immediate) func.apply(context, args); 562 | }; 563 | if (immediate && !timeout) func.apply(context, args); 564 | clearTimeout(timeout); 565 | timeout = setTimeout(later, wait); 566 | }; 567 | }; 568 | 569 | // Returns a function that will be executed at most one time, no matter how 570 | // often you call it. Useful for lazy initialization. 571 | _.once = function(func) { 572 | var ran = false, memo; 573 | return function() { 574 | if (ran) return memo; 575 | ran = true; 576 | return memo = func.apply(this, arguments); 577 | }; 578 | }; 579 | 580 | // Returns the first function passed as an argument to the second, 581 | // allowing you to adjust arguments, run code before and after, and 582 | // conditionally execute the original function. 583 | _.wrap = function(func, wrapper) { 584 | return function() { 585 | var args = [func].concat(slice.call(arguments, 0)); 586 | return wrapper.apply(this, args); 587 | }; 588 | }; 589 | 590 | // Returns a function that is the composition of a list of functions, each 591 | // consuming the return value of the function that follows. 592 | _.compose = function() { 593 | var funcs = arguments; 594 | return function() { 595 | var args = arguments; 596 | for (var i = funcs.length - 1; i >= 0; i--) { 597 | args = [funcs[i].apply(this, args)]; 598 | } 599 | return args[0]; 600 | }; 601 | }; 602 | 603 | // Returns a function that will only be executed after being called N times. 604 | _.after = function(times, func) { 605 | if (times <= 0) return func(); 606 | return function() { 607 | if (--times < 1) { return func.apply(this, arguments); } 608 | }; 609 | }; 610 | 611 | // Object Functions 612 | // ---------------- 613 | 614 | // Retrieve the names of an object's properties. 615 | // Delegates to **ECMAScript 5**'s native `Object.keys` 616 | _.keys = nativeKeys || function(obj) { 617 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 618 | var keys = []; 619 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 620 | return keys; 621 | }; 622 | 623 | // Retrieve the values of an object's properties. 624 | _.values = function(obj) { 625 | return _.map(obj, _.identity); 626 | }; 627 | 628 | // Return a sorted list of the function names available on the object. 629 | // Aliased as `methods` 630 | _.functions = _.methods = function(obj) { 631 | var names = []; 632 | for (var key in obj) { 633 | if (_.isFunction(obj[key])) names.push(key); 634 | } 635 | return names.sort(); 636 | }; 637 | 638 | // Extend a given object with all the properties in passed-in object(s). 639 | _.extend = function(obj) { 640 | each(slice.call(arguments, 1), function(source) { 641 | for (var prop in source) { 642 | obj[prop] = source[prop]; 643 | } 644 | }); 645 | return obj; 646 | }; 647 | 648 | // Return a copy of the object only containing the whitelisted properties. 649 | _.pick = function(obj) { 650 | var result = {}; 651 | each(_.flatten(slice.call(arguments, 1)), function(key) { 652 | if (key in obj) result[key] = obj[key]; 653 | }); 654 | return result; 655 | }; 656 | 657 | // Fill in a given object with default properties. 658 | _.defaults = function(obj) { 659 | each(slice.call(arguments, 1), function(source) { 660 | for (var prop in source) { 661 | if (obj[prop] == null) obj[prop] = source[prop]; 662 | } 663 | }); 664 | return obj; 665 | }; 666 | 667 | // Create a (shallow-cloned) duplicate of an object. 668 | _.clone = function(obj) { 669 | if (!_.isObject(obj)) return obj; 670 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 671 | }; 672 | 673 | // Invokes interceptor with the obj, and then returns obj. 674 | // The primary purpose of this method is to "tap into" a method chain, in 675 | // order to perform operations on intermediate results within the chain. 676 | _.tap = function(obj, interceptor) { 677 | interceptor(obj); 678 | return obj; 679 | }; 680 | 681 | // Internal recursive comparison function. 682 | function eq(a, b, stack) { 683 | // Identical objects are equal. `0 === -0`, but they aren't identical. 684 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 685 | if (a === b) return a !== 0 || 1 / a == 1 / b; 686 | // A strict comparison is necessary because `null == undefined`. 687 | if (a == null || b == null) return a === b; 688 | // Unwrap any wrapped objects. 689 | if (a._chain) a = a._wrapped; 690 | if (b._chain) b = b._wrapped; 691 | // Invoke a custom `isEqual` method if one is provided. 692 | if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); 693 | if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); 694 | // Compare `[[Class]]` names. 695 | var className = toString.call(a); 696 | if (className != toString.call(b)) return false; 697 | switch (className) { 698 | // Strings, numbers, dates, and booleans are compared by value. 699 | case '[object String]': 700 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 701 | // equivalent to `new String("5")`. 702 | return a == String(b); 703 | case '[object Number]': 704 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 705 | // other numeric values. 706 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 707 | case '[object Date]': 708 | case '[object Boolean]': 709 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 710 | // millisecond representations. Note that invalid dates with millisecond representations 711 | // of `NaN` are not equivalent. 712 | return +a == +b; 713 | // RegExps are compared by their source patterns and flags. 714 | case '[object RegExp]': 715 | return a.source == b.source && 716 | a.global == b.global && 717 | a.multiline == b.multiline && 718 | a.ignoreCase == b.ignoreCase; 719 | } 720 | if (typeof a != 'object' || typeof b != 'object') return false; 721 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 722 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 723 | var length = stack.length; 724 | while (length--) { 725 | // Linear search. Performance is inversely proportional to the number of 726 | // unique nested structures. 727 | if (stack[length] == a) return true; 728 | } 729 | // Add the first object to the stack of traversed objects. 730 | stack.push(a); 731 | var size = 0, result = true; 732 | // Recursively compare objects and arrays. 733 | if (className == '[object Array]') { 734 | // Compare array lengths to determine if a deep comparison is necessary. 735 | size = a.length; 736 | result = size == b.length; 737 | if (result) { 738 | // Deep compare the contents, ignoring non-numeric properties. 739 | while (size--) { 740 | // Ensure commutative equality for sparse arrays. 741 | if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; 742 | } 743 | } 744 | } else { 745 | // Objects with different constructors are not equivalent. 746 | if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; 747 | // Deep compare objects. 748 | for (var key in a) { 749 | if (_.has(a, key)) { 750 | // Count the expected number of properties. 751 | size++; 752 | // Deep compare each member. 753 | if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; 754 | } 755 | } 756 | // Ensure that both objects contain the same number of properties. 757 | if (result) { 758 | for (key in b) { 759 | if (_.has(b, key) && !(size--)) break; 760 | } 761 | result = !size; 762 | } 763 | } 764 | // Remove the first object from the stack of traversed objects. 765 | stack.pop(); 766 | return result; 767 | } 768 | 769 | // Perform a deep comparison to check if two objects are equal. 770 | _.isEqual = function(a, b) { 771 | return eq(a, b, []); 772 | }; 773 | 774 | // Is a given array, string, or object empty? 775 | // An "empty" object has no enumerable own-properties. 776 | _.isEmpty = function(obj) { 777 | if (obj == null) return true; 778 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 779 | for (var key in obj) if (_.has(obj, key)) return false; 780 | return true; 781 | }; 782 | 783 | // Is a given value a DOM element? 784 | _.isElement = function(obj) { 785 | return !!(obj && obj.nodeType == 1); 786 | }; 787 | 788 | // Is a given value an array? 789 | // Delegates to ECMA5's native Array.isArray 790 | _.isArray = nativeIsArray || function(obj) { 791 | return toString.call(obj) == '[object Array]'; 792 | }; 793 | 794 | // Is a given variable an object? 795 | _.isObject = function(obj) { 796 | return obj === Object(obj); 797 | }; 798 | 799 | // Is a given variable an arguments object? 800 | _.isArguments = function(obj) { 801 | return toString.call(obj) == '[object Arguments]'; 802 | }; 803 | if (!_.isArguments(arguments)) { 804 | _.isArguments = function(obj) { 805 | return !!(obj && _.has(obj, 'callee')); 806 | }; 807 | } 808 | 809 | // Is a given value a function? 810 | _.isFunction = function(obj) { 811 | return toString.call(obj) == '[object Function]'; 812 | }; 813 | 814 | // Is a given value a string? 815 | _.isString = function(obj) { 816 | return toString.call(obj) == '[object String]'; 817 | }; 818 | 819 | // Is a given value a number? 820 | _.isNumber = function(obj) { 821 | return toString.call(obj) == '[object Number]'; 822 | }; 823 | 824 | // Is a given object a finite number? 825 | _.isFinite = function(obj) { 826 | return _.isNumber(obj) && isFinite(obj); 827 | }; 828 | 829 | // Is the given value `NaN`? 830 | _.isNaN = function(obj) { 831 | // `NaN` is the only value for which `===` is not reflexive. 832 | return obj !== obj; 833 | }; 834 | 835 | // Is a given value a boolean? 836 | _.isBoolean = function(obj) { 837 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 838 | }; 839 | 840 | // Is a given value a date? 841 | _.isDate = function(obj) { 842 | return toString.call(obj) == '[object Date]'; 843 | }; 844 | 845 | // Is the given value a regular expression? 846 | _.isRegExp = function(obj) { 847 | return toString.call(obj) == '[object RegExp]'; 848 | }; 849 | 850 | // Is a given value equal to null? 851 | _.isNull = function(obj) { 852 | return obj === null; 853 | }; 854 | 855 | // Is a given variable undefined? 856 | _.isUndefined = function(obj) { 857 | return obj === void 0; 858 | }; 859 | 860 | // Has own property? 861 | _.has = function(obj, key) { 862 | return hasOwnProperty.call(obj, key); 863 | }; 864 | 865 | // Utility Functions 866 | // ----------------- 867 | 868 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 869 | // previous owner. Returns a reference to the Underscore object. 870 | _.noConflict = function() { 871 | root._ = previousUnderscore; 872 | return this; 873 | }; 874 | 875 | // Keep the identity function around for default iterators. 876 | _.identity = function(value) { 877 | return value; 878 | }; 879 | 880 | // Run a function **n** times. 881 | _.times = function (n, iterator, context) { 882 | for (var i = 0; i < n; i++) iterator.call(context, i); 883 | }; 884 | 885 | // Escape a string for HTML interpolation. 886 | _.escape = function(string) { 887 | return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); 888 | }; 889 | 890 | // If the value of the named property is a function then invoke it; 891 | // otherwise, return it. 892 | _.result = function(object, property) { 893 | if (object == null) return null; 894 | var value = object[property]; 895 | return _.isFunction(value) ? value.call(object) : value; 896 | }; 897 | 898 | // Add your own custom functions to the Underscore object, ensuring that 899 | // they're correctly added to the OOP wrapper as well. 900 | _.mixin = function(obj) { 901 | each(_.functions(obj), function(name){ 902 | addToWrapper(name, _[name] = obj[name]); 903 | }); 904 | }; 905 | 906 | // Generate a unique integer id (unique within the entire client session). 907 | // Useful for temporary DOM ids. 908 | var idCounter = 0; 909 | _.uniqueId = function(prefix) { 910 | var id = idCounter++; 911 | return prefix ? prefix + id : id; 912 | }; 913 | 914 | // By default, Underscore uses ERB-style template delimiters, change the 915 | // following template settings to use alternative delimiters. 916 | _.templateSettings = { 917 | evaluate : /<%([\s\S]+?)%>/g, 918 | interpolate : /<%=([\s\S]+?)%>/g, 919 | escape : /<%-([\s\S]+?)%>/g 920 | }; 921 | 922 | // When customizing `templateSettings`, if you don't want to define an 923 | // interpolation, evaluation or escaping regex, we need one that is 924 | // guaranteed not to match. 925 | var noMatch = /.^/; 926 | 927 | // Certain characters need to be escaped so that they can be put into a 928 | // string literal. 929 | var escapes = { 930 | '\\': '\\', 931 | "'": "'", 932 | 'r': '\r', 933 | 'n': '\n', 934 | 't': '\t', 935 | 'u2028': '\u2028', 936 | 'u2029': '\u2029' 937 | }; 938 | 939 | for (var p in escapes) escapes[escapes[p]] = p; 940 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 941 | var unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g; 942 | 943 | // Within an interpolation, evaluation, or escaping, remove HTML escaping 944 | // that had been previously added. 945 | var unescape = function(code) { 946 | return code.replace(unescaper, function(match, escape) { 947 | return escapes[escape]; 948 | }); 949 | }; 950 | 951 | // JavaScript micro-templating, similar to John Resig's implementation. 952 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 953 | // and correctly escapes quotes within interpolated code. 954 | _.template = function(text, data, settings) { 955 | settings = _.defaults(settings || {}, _.templateSettings); 956 | 957 | // Compile the template source, taking care to escape characters that 958 | // cannot be included in a string literal and then unescape them in code 959 | // blocks. 960 | var source = "__p+='" + text 961 | .replace(escaper, function(match) { 962 | return '\\' + escapes[match]; 963 | }) 964 | .replace(settings.escape || noMatch, function(match, code) { 965 | return "'+\n_.escape(" + unescape(code) + ")+\n'"; 966 | }) 967 | .replace(settings.interpolate || noMatch, function(match, code) { 968 | return "'+\n(" + unescape(code) + ")+\n'"; 969 | }) 970 | .replace(settings.evaluate || noMatch, function(match, code) { 971 | return "';\n" + unescape(code) + "\n;__p+='"; 972 | }) + "';\n"; 973 | 974 | // If a variable is not specified, place data values in local scope. 975 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 976 | 977 | source = "var __p='';" + 978 | "var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" + 979 | source + "return __p;\n"; 980 | 981 | var render = new Function(settings.variable || 'obj', '_', source); 982 | if (data) return render(data, _); 983 | var template = function(data) { 984 | return render.call(this, data, _); 985 | }; 986 | 987 | // Provide the compiled function source as a convenience for build time 988 | // precompilation. 989 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + 990 | source + '}'; 991 | 992 | return template; 993 | }; 994 | 995 | // Add a "chain" function, which will delegate to the wrapper. 996 | _.chain = function(obj) { 997 | return _(obj).chain(); 998 | }; 999 | 1000 | // The OOP Wrapper 1001 | // --------------- 1002 | 1003 | // If Underscore is called as a function, it returns a wrapped object that 1004 | // can be used OO-style. This wrapper holds altered versions of all the 1005 | // underscore functions. Wrapped objects may be chained. 1006 | var wrapper = function(obj) { this._wrapped = obj; }; 1007 | 1008 | // Expose `wrapper.prototype` as `_.prototype` 1009 | _.prototype = wrapper.prototype; 1010 | 1011 | // Helper function to continue chaining intermediate results. 1012 | var result = function(obj, chain) { 1013 | return chain ? _(obj).chain() : obj; 1014 | }; 1015 | 1016 | // A method to easily add functions to the OOP wrapper. 1017 | var addToWrapper = function(name, func) { 1018 | wrapper.prototype[name] = function() { 1019 | var args = slice.call(arguments); 1020 | unshift.call(args, this._wrapped); 1021 | return result(func.apply(_, args), this._chain); 1022 | }; 1023 | }; 1024 | 1025 | // Add all of the Underscore functions to the wrapper object. 1026 | _.mixin(_); 1027 | 1028 | // Add all mutator Array functions to the wrapper. 1029 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1030 | var method = ArrayProto[name]; 1031 | wrapper.prototype[name] = function() { 1032 | var wrapped = this._wrapped; 1033 | method.apply(wrapped, arguments); 1034 | var length = wrapped.length; 1035 | if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; 1036 | return result(wrapped, this._chain); 1037 | }; 1038 | }); 1039 | 1040 | // Add all accessor Array functions to the wrapper. 1041 | each(['concat', 'join', 'slice'], function(name) { 1042 | var method = ArrayProto[name]; 1043 | wrapper.prototype[name] = function() { 1044 | return result(method.apply(this._wrapped, arguments), this._chain); 1045 | }; 1046 | }); 1047 | 1048 | // Start chaining a wrapped Underscore object. 1049 | wrapper.prototype.chain = function() { 1050 | this._chain = true; 1051 | return this; 1052 | }; 1053 | 1054 | // Extracts the result from a wrapped and chained object. 1055 | wrapper.prototype.value = function() { 1056 | return this._wrapped; 1057 | }; 1058 | 1059 | }).call(this); --------------------------------------------------------------------------------