/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of "Scrivo" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* $Id: Window.js 783 2013-08-09 10:17:59Z geert $
*/
"use strict";
SUI.Window = SUI.defineClass(
/** @lends SUI.Window.prototype */{
/** @ignore */ baseClass: SUI.AnchorLayout,
/**
* @class
* SUI.Window is a component to create dialog boxes. Dialog boxes are
* container boxes that are displayed on top of the user interface. They
* can be either modal (disable the current interface) or modeless (still
* keeping the current user interface active). The windows are always
* movable and by default resizeable too.
*
* @augments SUI.AnchorLayout
*
* @description
* Create a window, use show to show it in modal mode or draw to display
* as it modeless window.
*
* @constructs
* @param see base class
* @param {boolean} arg.resizable Create a resizable window.
*/
initializer: function(arg) {
// the window stack is a static list of currently open windows, if
// it is not yet craeted, create it now.
if (!SUI.Window.windowStack) {
this._initWindowStack();
}
// set the default minimum size for a window
arg.minWidth = arg.minWidth || 200;
arg.minHeight = arg.minHeight || 100;
SUI.Window.initializeBase(this, arg);
// use visible overflow to allow the resizer element to become larger
// than the windows
this.el().style.overflow = "visible";
// if the top position was not explicitly set center the window
if (!arg.top) {
this.center();
}
// create a resizable window (default true) ?
this._resizable = arg.resizable ? true : false;
// create the boxes for the window and add the event handlers
this._buildControl(arg);
},
/**
* The inner border width.
*/
BORDER_WIDTH: 3,
/**
* Right offset of the close icon.
*/
CLOSE_RIGHT: 2,
/**
* Close icons size (width and height).
*/
CLOSE_SIZE: 20,
/**
* Top offset of the close icon.
*/
CLOSE_TOP: 1,
/**
* Size of the corner draggers.
*/
CORNER_SIZE: 16,
/**
* Border width of border around the the inner window (without the
* caption bar).
*/
INNER_BORDER_OUTLINE: 1,
/**
* Border width of border around the whole window.
*/
OUTER_BORDER_OUTLINE: 1,
/**
* Height of the caption bar (excluding borders).
*/
CAPTION_HEIGHT: 23,
/**
* Padding top of the caption text in the caption bar.
*/
CAPTION_PADDING_TOP: 3,
/**
* Padding left of the caption text in the caption bar.
*/
CAPTION_PADDING_LEFT: 5,
/**
* Add a box component to the window.
* @param {SUI.Box} child Child box to add
*/
add: function(child) {
SUI.Window.parentMethod(this, "add", child, this.body);
},
/**
* Set or get the caption text.
* @param {String} text The new caption text (null to use the method as
* getter)
* @return {String} The caption text (null if the method was used as
* setter)
*/
caption: function(text) {
if (text === undefined) {
return this._caption.el().innerHTML;
}
this._caption.el().innerHTML = text;
return null;
},
/**
* Set the top and left so that the window will be centered when it will
* be drawn.
*/
center: function() {
// set the top and left so that the window will be centered
// horizontally and vertically with slight offset to the top
this.top(SUI.browser.viewportScrollY() +
((SUI.browser.viewportHeight - this.height()) / 3 | 0));
this.left(SUI.browser.viewportScrollX() +
((SUI.browser.viewportWidth - this.width()) / 2 | 0));
// correct top and/or left if the window top and/or left are outside
// of the browser screen
if (this.top() < 0) {
this.top(0);
}
if (this.left() < 0) {
this.left(0);
}
},
/**
* Close a modal window that was created buy calling 'show'
*/
close: function() {
// This is an ie cursor patch. The is the cursor is in and
// contenteditable div on this window and this window is closed
// by the close icon causes problems with placing the cursor in
// other input fields. Selecting the current range when closing the
// window seems to 'free' the cursor.
// Moved this to the onfocus of editable elements
// if (SUI.browser.isIE) {
// document.selection.createRange().select();
// }
// remove the overlay from the DOM tree ...
document.body.removeChild(this._overlay);
// ... and the window itself ...
this.removeBox();
// ... and pop it of the stack
SUI.Window.windowStack.pop();
// if there are still windows on the stack ...
if (SUI.Window.windowStack.length) {
// ... set the previously removed style back on the overlay
var topI = SUI.Window.windowStack.length-1;
SUI.style.addClass(SUI.Window.windowStack[topI]._overlay,
"sui-overlay-disable");
}
// set the tab index back of all elements where it was removed
for (var i=0; i<this._disabledElements.length; i++) {
this._disabledElements[i].el.tabIndex =
this._disabledElements[i].ti.tabIndex;
}
// and clear the list
this._disabledElements = null;
},
/**
* Get the top, left, right and bottom offset of the client area
* relative to the outer dimensions of the window.
*/
clientAreaPosition: function() {
var w = this._draggerBorderWidth();
return { top: w+this.CAPTION_HEIGHT, left: w, right: w, bottom: w };
},
/**
* Display the window control. Set the CSS size and position of the
* window's boxes.
*/
display: function() {
// set the CSS dimensions of outer box
this.setDim();
// set the CSS dimensions of the header
this._caption.setDim();
// set the CSS dimensions of the client area
this._mainArea.setDim();
this.body.setDim();
// If the window is resizable ...
if (this._resizable) {
// ... set the CSS dimensions of the side draggers ...
this._n.setDim();
this._e.setDim();
this._s.setDim();
this._w.setDim();
// ... and of the corner draggers
this._nw.setDim();
this._ne.setDim();
this._se.setDim();
this._sw.setDim();
}
// display all the child controls
SUI.Window.parentMethod(this, "display");
},
/**
* Lay out the the window. Calculate the sizes and positions of all
* the window's elements.
*/
layOut: function() {
// set the size and position of the caption bar and icon
this._caption.setRect(0, 0, this.clientWidth(), this.CAPTION_HEIGHT);
SUI.style.setRect(this._closeIcon, this.CLOSE_TOP,
this.clientWidth() - this.CLOSE_SIZE - this.CLOSE_RIGHT,
this.CLOSE_SIZE, this.CLOSE_SIZE);
// set the size and position of the client area: note that 'body' is
// no child of '_mainArea'
this._mainArea.setRect(this.CAPTION_HEIGHT, 0, this.clientWidth(),
this.clientHeight() - this.CAPTION_HEIGHT);
this.body.setRect(
this._mainArea.top() + this._mainArea.border().top
+ this._mainArea.padding().top,
this._mainArea.left() + this._mainArea.border().left
+ this._mainArea.padding().left,
this._mainArea.clientWidth(), this._mainArea.clientHeight());
// If the window is resizable ...
if (this._resizable) {
var obo = this.OUTER_BORDER_OUTLINE;
var bw = this._draggerBorderWidth();
// ... set the size and positions of the side draggers ...
this._n.setRect(-obo, -obo, this.width(), bw);
this._e.setRect(-obo, this.width() - bw - obo, bw, this.height());
this._s.setRect(this.height() - bw - obo, -obo, this.width(), bw);
this._w.setRect(-obo, -obo, bw, this.height());
// ... and corner draggers
this._nw.setRect(-obo, -obo, this.CORNER_SIZE, this.CORNER_SIZE);
this._ne.setRect(-obo, this.clientWidth() + obo - this.CORNER_SIZE,
this.CORNER_SIZE, this.CORNER_SIZE);
this._se.setRect(this.clientHeight() + obo - this.CORNER_SIZE,
this.clientWidth() + obo - this.CORNER_SIZE, this.CORNER_SIZE,
this.CORNER_SIZE);
this._sw.setRect(this.clientHeight() + obo - this.CORNER_SIZE,
-obo, this.CORNER_SIZE, this.CORNER_SIZE);
}
// lay out all child boxes
SUI.Window.parentMethod(this, "layOut");
},
/**
* onClose event handler. This event handler is called when the
* user clicks on the 'close' button on the window's caption.
* @event
*/
onClose: function() {
},
/**
* Show a modal window. Before creating the window an overlay will be
* placed over the current window to disallow access to the other elements
* of the interface.
*/
show: function() {
// check if the window is already shown
if (this._disabledElements) {
return;
}
// start a new list ...
this._disabledElements = [];
// .. get all the elements of the DOM tree ...
var l = document.getElementsByTagName("*");
// ... and loop through them ...
for(var i; i<l.length; i++) {
// ... if the tab index was set ...
if (l[i].tabIndex !== undefined && l[i].tabIndex != -1) {
// ... store it
this._disabledElements.push({ti: l[i].tabIndex, el: l[i]});
l[i].tabIndex = -1;
}
}
// if there is an other overlay active ...
if (SUI.Window.windowStack.length) {
// ... remove the class so it not visible any more
var topI = SUI.Window.windowStack.length-1;
SUI.style.removeClass(SUI.Window.windowStack[topI]._overlay,
"sui-overlay-disable");
}
// create a new overlay for this window
this._overlay = SUI.browser.createElement();
SUI.style.setRect(this._overlay,
0, 0, SUI.browser.viewportWidth, SUI.browser.viewportHeight);
if (true) {
SUI.style.addClass(this._overlay, "sui-overlay-disable");
} else {
// for taking screenshots:
this._overlay.style.backgroundColor = "white";
this.el().style.webkitBoxShadow = "none";
this.el().style.boxShadow = "none";
}
this._overlay.style.position = "fixed";
// append the overlay to the document body
document.body.appendChild(this._overlay);
// add the window to the window stack ...
SUI.Window.windowStack.push(this);
// ... and append it to the document tree
this.parent({el: function() { return document.body; }});
// now draw the window
this.draw();
},
// storage for disabled elements under the window (we need to enable them
// later on)
_disabledElements: null,
// the overlay window that obscures the rest of the UI when the window
// is shown
_overlay: null,
// if it is a resizable window or not
_resizable: true,
/* Add _startDragBorder event handler on the onmousedown event of the
* element.
*/
_addResizeHandler: function(element, dir) {
var that = this;
// 'that' and 'dir' are two closure variables
SUI.browser.addEventListener(element, "mousedown",
function(e) {
if (!that._startDragBorder(new SUI.Event(this, e), dir)) {
SUI.browser.noPropagation(e);
}
}
);
},
/* And the event handlers for the close button and to move the window.
*/
_addWindowEvents: function() {
var that = this;
// add the 'move window' event handler on the onmousedown event of
// the caption bar
SUI.browser.addEventListener(this._caption.el(), "mousedown",
function(e) {
if (!that._startDragWindow(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
}
);
// close the window on the click event of the close icon
SUI.browser.addEventListener(this._closeIcon, "click",
function(e) {
// call the onclose listener before closing the form
that.callListener("onClose");
if (!that.close()) {
SUI.browser.noPropagation(e);
}
}
);
},
/* Make all required boxes for the control and set the event handlers.
*/
_buildControl: function(arg) {
// set the window's main style
this.addClass("sui-window-border");
this.border(new SUI.Border(this.OUTER_BORDER_OUTLINE));
// create the main area for the window
this._mainArea = new SUI.Box({parent: this});
this._mainArea.addClass("sui-window");
this._mainArea.border(
new SUI.Border(this.INNER_BORDER_OUTLINE));
this._mainArea.padding(new SUI.Padding(this.BORDER_WIDTH));
// create the caption bar
this._caption = new SUI.Box({parent: this});
this._caption.addClass("sui-window-caption sui-window-border");
this._caption.el().innerHTML = arg.caption || SUI.i18n.captionWindow;
this._caption.padding(new SUI.Padding(this.CAPTION_PADDING_TOP, 0, 0,
this.CAPTION_PADDING_LEFT));
this._caption.el().style.cursor = "move";
// if the window is resizable create the resize handlers
if (this._resizable) {
// create the the boxes for the side handles
this._n = new SUI.Box({parent: this});
this._e = new SUI.Box({parent: this});
this._s = new SUI.Box({parent: this});
this._w = new SUI.Box({parent: this});
// create the the boxes for the corner handles
this._nw = new SUI.Box({parent: this});
this._ne = new SUI.Box({parent: this});
this._se = new SUI.Box({parent: this});
this._sw = new SUI.Box({parent: this});
// set the cursors and resize event handlers for the handles
var arr = [this._n, this._e, this._s, this._w, this._nw,
this._ne, this._se, this._sw];
var curs = ["n", "e", "s", "w", "nw", "ne", "se", "sw"];
for (var i=0; i<arr.length; i++) {
arr[i].el().style.cursor = curs[i] + "-resize";
this._addResizeHandler(arr[i].el(), curs[i]);
}
}
// create the close icon
this._closeIcon = document.createElement("INPUT");
this._closeIcon.type = "image";
this._closeIcon.src = SUI.imgDir+"/"+SUI.resource.wnClose;
this._closeIcon.style.position = "absolute";
this.el().appendChild(this._closeIcon);
// create the client area of the window (don't add it to _mainArea
// because it has to lay over the corner draggers
this.body = new SUI.Box({parent: this});
this.body.addClass("body sui-window-body");
if (arg.padding) {
this.body.padding(arg.padding);
}
// add the event handlers
this._addWindowEvents();
},
/* The draggable border width equals the width of all the borders.
*/
_draggerBorderWidth: function() {
return this.OUTER_BORDER_OUTLINE + this.INNER_BORDER_OUTLINE +
this.BORDER_WIDTH;
},
/* End the dragging motion, set the window's size and/or position to
* the ones of the dragger.
*/
_endDrag: function(dragger) {
// set the window's size and/or position
this.setRect(this.top() + dragger.top(), this.left() + dragger.left(),
dragger.width(), dragger.height());
// remove the dragger from the document tree
dragger.removeBox();
// redraw the window
this.draw();
},
/* Initalize this static window stack.
*/
_initWindowStack: function() {
// if there is no static windowStack, create it. The windowStack is
// system global list of displayed modal windows.
if (!SUI.Window.windowStack) {
SUI.Window.windowStack = [];
// add an event handler to the window that resizes all overlays
// on the window resize event
SUI.browser.addEventListener(window, "resize",
function(event){
for (var i=0; i<SUI.Window.windowStack.length; i++) {
SUI.style.setRect(SUI.Window.windowStack[i]._overlay,
0, 0, SUI.browser.viewportWidth-0,
SUI.browser.viewportHeight-0);
}
SUI.browser.noPropagation(event);
}
);
// add an event handler to the window that catches some keycodes
// to close the window
// TODO
SUI.browser.addEventListener(window.document, "keydown",
function(event) {
var e = new SUI.Event(this, event);
if (SUI.Window.windowStack.length) {
var topI = SUI.Window.windowStack.length-1;
var win = SUI.Window.windowStack[topI];
// handle the esc key
if (e.event.keyCode == 27 ) {
// call the onclose listener before closing form
win.callListener("onClose");
win.close();
}
// handle the enter key
//if (e.event.keyCode == 13 ) {
//win.handleEnter(new SUI.Event(this, e));
//}
SUI.browser.noPropagation(event);
}
}
);
}
},
/* Start dragging of one of the borders of the window.
*/
_startDragBorder: function(event, dir) {
// create a dragger
var dragger = new SUI.Dragger({
parent: this,
width: this.width(),
height: this.height(),
border: new SUI.Border(this._draggerBorderWidth())
});
// set the style of the dragger
dragger.addClass("sui-window-dragger");
dragger.el().style.cursor = event.elListener.style.cursor;
// set the direction of the dragger
dragger.direction(dir);
// get the boundaries and the dragger
var xMax, yMax;
if (dir.indexOf("w") != -1 || dir.indexOf("n") != -1) {
xMax = this.left() + this.width();
yMax = this.top() + this.height();
} else {
xMax = SUI.browser.viewportWidth - this.left() - 1;
yMax = SUI.browser.viewportHeight - this.top() - 1;
}
if (xMax > this.maxWidth()) {
xMax = this.maxWidth();
}
if (yMax > this.maxHeight()) {
yMax = this.maxHeight();
}
// set the boundaries and direction of the dragger
dragger.xMin(this.minWidth());
dragger.yMin(this.minHeight());
dragger.xMax(xMax);
dragger.yMax(yMax);
// set CSS dimensions of the dragger
dragger.setDim();
var that = this;
// 'this' and 'dragger' are closure variables
dragger.addListener("onEndDrag",
function() {
that._endDrag(dragger);
}
);
// and start dragging
dragger.start(event, this);
},
/* Start dragging the window.
*/
_startDragWindow: function(event) {
// create a dragger ...
var dragger = new SUI.Dragger({
parent: this,
width: this.width(),
height: this.height(),
border: new SUI.Border(this._draggerBorderWidth())
});
// ... set the style ...
dragger.addClass("sui-window-dragger");
dragger.el().style.cursor = this._caption.el().style.cursor;
// ... and direction
dragger.direction(dragger.HORIZONTAL + dragger.VERTICAL);
// set the dragging boundaries
dragger.xMin(-this.left() - this.OUTER_BORDER_OUTLINE);
dragger.xMax(SUI.browser.viewportWidth - this.width() - this.left()
- this.OUTER_BORDER_OUTLINE);
dragger.yMin(-this.top() - this.OUTER_BORDER_OUTLINE);
dragger.yMax(SUI.browser.viewportHeight - this.height() - this.top()
- this.OUTER_BORDER_OUTLINE);
// set CSS dimensions of the dragger
dragger.setDim();
var that = this;
// 'this' and 'dragger' are closure variables
dragger.addListener("onEndDrag",
function() {
that._endDrag(dragger);
}
);
// and start dragging
dragger.start(event, this);
}
});