Как-то так:
http://jsfiddle.net/Octane/WWkkm/ – во фреймах срабатывает, если не больше чем за 20 пикселей от границы отпустить мышь.
<!DOCTYPE html>
<html>
<head>
<meta charser="utf-8">
<title>Windows Snap</title>
<style>
html, body {
height: 100%;
}
body {
margin: 0;
}
.window {
position: fixed;
background: #53bdde;
outline: 1px solid #1d5e6d;
}
.window-title {
font: 12px/24px sans-serif;
color: #373e45;
text-align: center;
cursor: move;
-ms-user-select: none;
}
.window-content {
overflow: scroll;
position: absolute;
top: 24px;
right: 5px;
bottom: 5px;
left: 5px;
background: #efefef;
border: 1px solid #569cb6;
}
</style>
</head>
<body>
<div class="window" style="top:50px; left:50px; width:320px; height:240px;">
<div class="window-title">Snappable Window</div>
<div class="window-content"></div>
</div>
<script>
var obj = {}, arr = {}, fn = {}, ui = {};
arr.from = Array.prototype.slice.call.bind(Array.prototype.slice);
obj.extend = function (target/*, sources*/) {
arr.from(arguments, 1).forEach(function (src) {
Object.keys(src).forEach(function (prop) {
target[prop] = src[prop];
});
});
return target;
};
fn.inherit = function (cns, cls) {
cns.prototype = Object.create(cls.prototype);
cns.prototype.constructor = cns;
return cns;
}
obj.Observable = new function () {
function Observable() {
}
Observable.prototype = {
constructor: Observable,
_getObservers: function (eventType) {
var observers;
if (!("_observers" in this)) {
this._observers = {};
}
observers = this._observers;
if (!(eventType in observers)) {
observers[eventType] = [];
}
observers = observers[eventType];
return observers;
},
on: function (eventType, observer) {
var observers;
if ("addEventListener" in this) {
this.addEventListener(eventType, observer, false);
}
else {
observers = this._getObservers(eventType);
observers.push(observer);
}
return this;
},
fire: function (eventType) {
var observers = this._getObservers(eventType),
i = 0, length = observers.length;
while (i < length) {
observers[i].call(this, {type: eventType})
i++;
}
return this;
}
};
return Observable;
};
ui.Draggable = new function () {
function Draggable() {
}
obj.extend(fn.inherit(Draggable, obj.Observable).prototype, {
container: null,
activeElement: null,
_x: 0,
_y: 0,
_offsetX: 0,
_offsetY: 0,
_dragging: false,
_dragTimeout: 0,
enableDragging: function () {
this._onDragging = this._onDragging.bind(this);
this._saveCoords = this._saveCoords.bind(this);
this._onDragStop = this._onDragStop.bind(this);
this.activeElement.addEventListener("mousedown", this._onDragStart.bind(this), false);
},
_onDragStart: function (event) {
var rect = this.container.getBoundingClientRect();
this._offsetY = event.pageY - rect.top;
this._offsetX = event.pageX - rect.left;
this._dragging = true;
this._saveCoords(event);
document.addEventListener("mousemove", this._saveCoords, false);
document.addEventListener("mouseup", this._onDragStop, false);
this.fire("drag.beforeStart");
this._onDragging();
this.fire("drag.start");
},
_onDragStop: function () {
clearTimeout(this._dragTimeout);
this._dragging = false;
document.removeEventListener("mousemove", this._saveCoords, false);
document.removeEventListener("mouseup", this._onDragStop, false);
this.fire("drag.stop");
},
_onDragging: function () {
this._move();
if (this._dragging) {
this._dragTimeout = setTimeout(this._onDragging, 50);
}
},
_saveCoords: function (event) {
this._x = event.pageX;
this._y = event.pageY;
},
_move: function () {
var style = this.container.style;
style.top = this._y - this._offsetY + "px";
style.left = this._x - this._offsetX + "px";
}
});
return Draggable;
};
ui.Snappable = new function () {
function Snappable() {
}
obj.extend(fn.inherit(Snappable, ui.Draggable).prototype, {
container: null,
_x: 0,
_y: 0,
_snapping: false,
_snapTimeout: 0,
_maxX: 0,
_needSnap: false,
_snapTo: "",
_snapped: false,
_width: 0,
_height: 0,
enableSnapping: function () {
this._onSnapping = this._onSnapping.bind(this);
this.on("drag.start", this._onSpanStart.bind(this));
this.on("drag.stop", this._onSnapStop.bind(this));
this.on("drag.beforeStart", this._unsnap.bind(this));
},
_onSpanStart: function () {
this._maxX = document.documentElement.offsetWidth;
this._snapping = true;
this._saveBounds();
this._onSnapping();
},
_onSnapStop: function () {
clearTimeout(this._snapTimeout);
this._snapping = false;
if (this._needSnap) {
this._needSnap = false;
this._snap();
}
},
_onSnapping: function () {
this._ckeckSnapping();
if (this._snapping) {
this._snapTimeout = setTimeout(this._onSnapping, 100);
}
},
_ckeckSnapping: function () {
if ((this._x <= 20) || (this._x >= this._maxX - 20) || (this._y <= 20)) {
this._needSnap = true;
if (this._x <= 0) {
this._snapTo = "left";
}
else if (this._x >= this._maxX) {
this._snapTo = "right";
}
else if (this._y <= 0) {
this._snapTo = "top";
}
}
else {
this._needSnap = false;
}
},
_saveBounds: function () {
this._width = this.container.offsetWidth;
this._height = this.container.offsetHeight;
},
_snap: function () {
var style = this.container.style;
style.top = 0;
style.height = "100%";
if (this._snapTo == "left") {
style.left = 0;
style.width = "50%";
}
else if (this._snapTo == "right") {
style.left = "50%";
style.width = "50%";
}
else {
style.left = 0;
style.width = "100%";
}
this._snapped = true;
},
_unsnap: function () {
var style;
if (this._snapped) {
this._snapped = false;
this._offsetX = Math.ceil(this._width / 2);
style = this.container.style;
style.top = this._y - this._offsetY + "px";
style.left = this._x - this._offsetX + "px";
style.width = this._width + "px";
style.height = this._height + "px";
}
}
});
return Snappable;
};
ui.Window = new function () {
function Window(container) {
if (!(this instanceof ui.Window)) {
return new ui.Window(container);
}
this.container = container;
this.activeElement = container.querySelector(".window-title");
this.enableDragging();
this.enableSnapping();
}
obj.extend(fn.inherit(Window, ui.Snappable).prototype, {
container: null,
activeElement: null,
});
return Window;
};
arr.from(document.querySelectorAll(".window")).forEach(ui.Window);
</script>
</body>
</html>
Кросс-браузерность допилите, проверял только в IE10.