diff --git a/demos/tree/Tree.html b/demos/tree/Tree.html index eb6db59..efc1dc0 100644 --- a/demos/tree/Tree.html +++ b/demos/tree/Tree.html @@ -1,209 +1,279 @@ - - - - - - -Tree Demo - - - - - - - - - - - - - - - - - - - - -

Tree Demo

-

Simple demo of the tree control. See the source code for this page for more -information on how the component is instantiated and stylized. Open the web -console to see some of the events.

- - -
- - - - \ No newline at end of file + + + + +Tree Demo + + + + + + + + + + + + + + + + + + + + +

Tree Demo

+

The trees below are rendered with two different styles. The tree on the +left uses the component's default style, which is defined inline +(no stylesheet required). The tree on the right uses a stylesheet-based +style backed by an image sprite (tree.png). Open the web console to see events.

+ + +
+ +
+

Default style

+

Inline SVG icons — no stylesheet needed

+
+
+ +
+

Windows classic style

+

CSS classes + tree.png image sprite

+
+
+ +
+ + +

JavaXT Themes

+

The same tree styled with the JavaXT themes (themes/default.css and +themes/dark.css). The nodes, leaves and connectors are drawn entirely with CSS +(no image sprite) and follow the active theme. Each theme is shown in its own +iframe so the two stylesheets don't collide.

+ + +
+ +
+

Default theme

+

themes/default.css — javaxt-tree

+ +
+ +
+

Dark theme

+

themes/dark.css — javaxt-tree

+ +
+ +
+ + + diff --git a/demos/tree/tree-theme.html b/demos/tree/tree-theme.html new file mode 100644 index 0000000..6bf2fff --- /dev/null +++ b/demos/tree/tree-theme.html @@ -0,0 +1,69 @@ + + + + +Themed Tree + + + + + + + + + + + + + +
+ + diff --git a/src/button/Button.js b/src/button/Button.js index 7209e8e..81f977d 100644 --- a/src/button/Button.js +++ b/src/button/Button.js @@ -1,678 +1,775 @@ -if(!javaxt) var javaxt={}; -if(!javaxt.dhtml) javaxt.dhtml={}; - -//****************************************************************************** -//** Button Class -//****************************************************************************** -/** - * Custom button control. The button has 3 parts: icon, label, and an arrow. - * - ******************************************************************************/ - -javaxt.dhtml.Button = function(parent, config) { - this.className = "javaxt.dhtml.Button"; - - var me = this; - var defaultConfig = { - - /** Text label for the button. - */ - label: null, - - - /** If true, the button will initially appear in a selected state and the - * isSelected() method will return true. Default is false. - */ - selected: false, - - - /** If true, the button will initially appear disabled and the - * isDisabled() method will return true. Default is false. - */ - disabled: false, - - - /** If true, the button will be rendered as a toggle button. Default is - * false, unless the button has a menu. - */ - toggle: false, - - - /** If true, will create a drop-down menu for the button. Components such - * as buttons or custom DOM elements can be added directly to the menu. - * See getMenuPanel() for more information. - */ - menu: false, - - - /** Sets the position of the menu panel. Options are "bottom" or "right". - */ - menuAlign: "bottom", - - - - /** Used to set the "display" style attribute for the outer DOM element. - * This is not commonly ued. Default is "inline-block". - */ - display: "inline-block", - - - /** Used to set the width of the button. This property is optional. - */ - width: null, - - - /** Used to set the height of the button. This property is optional. - */ - height: null, - - - /** Used to set the icon position relative to the button label. Options - * are "left" or "right". Note that the icon style is set in the style - * config. The icon style should not be used to control whether the - * icon appears to the left or right of the label. Use this config - * instead. - */ - iconAlign: "left", - - - /** Style for individual elements within the component. Note that you can - * provide CSS class names instead of individual style definitions. - */ - style:{ - - button: { - border: "1px solid #cccccc", - borderRadius: "3px", - background: "#F6F6F6", - cursor: "pointer", - padding: "3px 7px", - margin: "0px", - color: "#2b2b2b" - }, - - label: { - fontFamily: "helvetica,arial,verdana,sans-serif", - fontSize: "14px", - whiteSpace: "nowrap" - }, - - icon: { - - }, - - arrow: { - - }, - - select: { - background: "#007FFF", - border: "1px solid #003EFF", - color: "#FFFFFF" - }, - - hover: { - background: "#ededed" - }, - - - menu: { - border: "1px solid #cccccc", - background: "#F6F6F6", - cursor: "pointer", - padding: "3px 3px", - zIndex: "1" - } - - }, - - - /** Sound to play when the button is clicked - */ - sound: null - }; - - var mainDiv; - var mask, menu; - var icon, label, arrow; - - - //************************************************************************** - //** Constructor - //************************************************************************** - var init = function(){ - - if (typeof parent === "string"){ - parent = document.getElementById(parent); - } - if (!parent) return; - - - //Clone the config so we don't modify the original config object - var clone = {}; - merge(clone, config); - - - //Merge clone with default config - merge(clone, defaultConfig); - config = clone; - - - //Get icon alignment - var iconAlignment = config.style.iconAlign; //legacy - if (!iconAlignment) iconAlignment = config.iconAlign; //preferred - if (iconAlignment!=="right") iconAlignment = "left"; - - - //Get menu alignment - var menuAlignment = config.style.menuAlignment; //legacy - if (!menuAlignment) menuAlignment = config.menuAlign; //preferred - if (menuAlignment!=="right") menuAlignment = "bottom"; - - - //Update arrow style as needed - if (config.menu===true){ - var arrowDefined = false; - for (var key in config.style.arrow){ - if (config.style.arrow.hasOwnProperty(key)){ - arrowDefined = true; - break; - } - } - - if (!arrowDefined){ - if (menuAlignment==="bottom"){ - config.style.arrow = { - width: 0, - height: 0, - borderLeft: "5px solid transparent", - borderRight: "5px solid transparent", - borderTop: "5px solid #575757", - marginLeft: "10px" - }; - config.style.arrowSelect = { - borderTop: "5px solid #FFFFFF" - }; - } - } - } - - - - //Create outer div used to hold the button, mask, and menu - var outerDiv = createElement('div', parent); - outerDiv.className = "javaxt-button"; - outerDiv.style.display = config.display; - if (config.width){ - if (typeof config.width === "string"){ - outerDiv.style.width = config.width; - } - else{ - outerDiv.style.width = config.width + "px"; - } - } - if (config.height){ - if (typeof config.height === "string"){ - outerDiv.style.height = config.height; - } - else{ - outerDiv.style.height = config.height + "px"; - } - } - outerDiv.style.position = "relative"; - if (config.hidden===true){ //legacy config... - outerDiv.style.visibility = 'hidden'; - outerDiv.style.display = 'none'; - } - me.el = outerDiv; - - - - - //The button is implemented using a simple HTML table with 3 columns. The - //left and right columns are for icons and the center column is for the - //button label. The width of the center column is set to 100% and the - //width of the left and right columns are defined by the "icon" and - //"arrow" styles. Unfortunately, some browsers seem to have issues - //rendering the table correctly inside of a div when the outerDiv's - //display style set to "inline-block". For example, Mobile Safari will - //completely ignore the "inline-block" style and stretch the button to - //100% of the available width. In Chrome, if the left or right columns has - //a width, the "inline-block" style is ignored and the button is stretched - //to 100% of the available width. As a workaround, it looks like we can - //wrap the button div in another div with the display style set to "table". - var tableDiv = createElement('div', outerDiv); - if (outerDiv.style.display==="inline-block"){ - tableDiv.style.display = "table"; - if (config.width) tableDiv.style.width = outerDiv.style.width; - } - tableDiv.style.height = "100%"; - - - //Create main div used to represent the button - mainDiv = createElement('div', tableDiv, config.style.button); - mainDiv.setAttribute("desc", "button"); - addEventHandlers(mainDiv); - - - - var table = createTable(mainDiv); - table.style.fontFamily = "inherit"; - table.style.textAlign = "inherit"; - table.style.color = "inherit"; - var tr = table.addRow(); - var td; - - - //Add icon (or label) - td = tr.addColumn(); - if (iconAlignment==="left"){ - icon = createElement("div", td, config.style.icon); - } - else{ - arrow = createElement("div", td, config.style.arrow); - } - - - //Add label - td = tr.addColumn({width: "100%"}); - label = createElement("div", td); - setStyle(label, "label"); - if (config.label) label.innerHTML = config.label; - - - //Add arrow (or icon) - td = tr.addColumn(); - if (iconAlignment==="left"){ - arrow = createElement("div", td, config.style.arrow); - } - else{ - icon = createElement("div", td, config.style.icon); - } - - - - //Create menu panel as needed - if (config.menu===true){ - config.toggle = true; - menu = createElement('div', outerDiv, config.style.menu); - menu.setAttribute("desc", "menu"); - menu.style.position = "absolute"; - menu.style.visibility = "hidden"; - - - var hideMenu = function(e){ - if (!mainDiv.contains(e.target)){ - menu.style.visibility = "hidden"; - me.deselect(); - } - }; - - //Hide menu if the client clicks outside of the menu - window.addEventListener('click', hideMenu); - - - //Create logic to process touch events - var touchStartTime, touchEndTime; - var x1, x2, y1, y2; - - window.addEventListener('touchstart', function(e){ - x1 = e.changedTouches[0].pageX; - y1 = e.changedTouches[0].pageY; - touchStartTime = new Date().getTime(); - touchEndTime = null; - }); - - window.addEventListener('touchend', function(e){ - - touchEndTime= new Date().getTime(); - x2 = e.changedTouches[0].pageX; - y2 = e.changedTouches[0].pageY; - - var distance = Math.sqrt( (x2-=x1)*x2 + (y2-=y1)*y2 ); - if (distance<0) distance = -distance; - var duration = touchEndTime - touchStartTime; - - if ((duration <= 500 && distance <= 10) || //Quick tap - (duration > 500 && distance <= 10)) { //Long press - hideMenu(e); - } - }); - - } - - - //Set button state - if (config.disabled===true) me.disable(); - if (config.selected===true) me.select(); - - - //Add public show/hide methods - addShowHide(me); - }; - - - //************************************************************************** - //** addEventHandlers - //************************************************************************** - var addEventHandlers = function(div){ - - //Disable text selection - div.unselectable="on"; - div.onselectstart=function(){return false;}; - - - //Create onclick function - var onclick = function(e){ - e.stopPropagation(); - if (config.sound!=null) config.sound.play(); - - - if (config.toggle===true){ - if (menu){ - - if (isTouch){ - me.toggle(); - } - else{ - //Do nothing - button is toggled on mouse down... - } - } - else{ - me.toggle(); - } - } - else{ - setStyle(mainDiv,"button"); - setStyle(icon,"icon"); - setStyle(arrow,"arrow"); - } - - - me.onClick(); - }; - - - //Create logic to process touch events - var touchStartTime; - var touchEndTime; - var x1, x2, y1, y2; - var isTouch = false; - - div.ontouchstart = function(e) { - isTouch = true; - - e.preventDefault(); - x1 = e.changedTouches[0].pageX; - y1 = e.changedTouches[0].pageY; - touchStartTime = new Date().getTime(); - touchEndTime = null; - - if (div.selected!==true){ - addStyle(div, "hover"); - addStyle(icon, "iconHover"); - addStyle(arrow, "arrowHover"); - } - }; - - div.ontouchend = function(e) { - - touchEndTime= new Date().getTime(); - x2 = e.changedTouches[0].pageX; - y2 = e.changedTouches[0].pageY; - - var distance = Math.sqrt( (x2-=x1)*x2 + (y2-=y1)*y2 ); - if (distance<0) distance = -distance; - var duration = touchEndTime - touchStartTime; - - if ((duration <= 500 && distance <= 10) || //Quick tap - (duration > 500 && distance <= 10)) { //Long press - onclick(e); - } - else{ - setStyle(div, "button"); - setStyle(icon, "icon"); - setStyle(arrow, "arrow"); - } - }; - - - - //Logic to process mouse events - if (!isTouch){ - div.onmousedown=function(){ - - addStyle(div, "select"); - addStyle(icon, "iconSelect"); - addStyle(arrow, "arrowSelect"); - - if (menu){ - me.toggle(); - //TODO: Add mouseup events to buttons in the menu - } - - //return false; - }; - div.onclick = function(e){ - onclick(e); - }; - div.onmouseover = function(){ - - if (div.selected!==true){ - addStyle(div, "hover"); - addStyle(icon, "iconHover"); - addStyle(arrow, "arrowHover"); - } - - }; - div.onmouseout = function(){ - - if (div.selected!==true){ - setStyle(div, "button"); - setStyle(icon, "icon"); - setStyle(arrow, "arrow"); - } - - }; - } - }; - - - //************************************************************************** - //** click - //************************************************************************** - /** Used to click the button and fire onClick event - */ - this.click = function(){ - mainDiv.click(); - }; - - - //************************************************************************** - //** onClick - //************************************************************************** - /** Called whenever the button is clicked. - */ - this.onClick = function(){}; - - - //************************************************************************** - //** getText - //************************************************************************** - /** Returns the button label. Same as getLabel(). - */ - this.getText = function(){ - return me.getLabel(); - }; - - - //************************************************************************** - //** getLabel - //************************************************************************** - /** Returns the button label. - */ - this.getLabel = function(){ - return label.innerText; - }; - - - //************************************************************************** - //** setLabel - //************************************************************************** - /** Used to update the button label. - */ - this.setLabel = function(str){ - label.innerText = str+""; - }; - - - //************************************************************************** - //** enable - //************************************************************************** - /** Used to enable the button. - */ - this.enable = function(){ - var outerDiv = me.el; - outerDiv.style.opacity = ""; - if (mask) mask.style.visibility = "hidden"; - }; - - - //************************************************************************** - //** disable - //************************************************************************** - /** Used to disable the button. - */ - this.disable = function(){ - - var outerDiv = me.el; - outerDiv.style.opacity = "0.5"; - - if (mask){ - mask.style.visibility = "visible"; - } - else{ - mask = createElement('div',{ - position: "absolute", - zIndex: "1", - width: "100%", - height: "100%" - }); - mask.setAttribute("desc", "mask"); - outerDiv.insertBefore(mask, outerDiv.firstChild); - } - }; - - - //************************************************************************** - //** isEnabled - //************************************************************************** - /** Returns true if the button is enabled (i.e. not disabled). - */ - this.isEnabled = function(){ - return !me.isDisabled(); - }; - - - //************************************************************************** - //** isDisabled - //************************************************************************** - /** Returns true if the button is disabled. - */ - this.isDisabled = function(){ - if (mask){ - if (mask.style.visibility !== "hidden") return true; - } - return false; - }; - - - //************************************************************************** - //** select - //************************************************************************** - /** Used to update the "selected" state of the button. - */ - this.select = function(){ - if (mainDiv.selected===true) return; - mainDiv.selected = true; - setStyle(mainDiv,"button"); - setStyle(icon,"icon"); - setStyle(arrow,"arrow"); - addStyle(mainDiv,"select"); - addStyle(icon,"iconSelect"); - addStyle(arrow,"arrowSelect"); - }; - - - //************************************************************************** - //** deselect - //************************************************************************** - /** Used to update the "selected" state of the button. - */ - this.deselect = function(){ - if (mainDiv.selected===true){ - mainDiv.selected = false; - setStyle(mainDiv,"button"); - setStyle(icon,"icon"); - setStyle(arrow,"arrow"); - } - }; - - - //************************************************************************** - //** isSelected - //************************************************************************** - /** Returns true if the button is selected (e.g. depressed) - */ - this.isSelected = function(){ - return (mainDiv.selected===true); - }; - - - //************************************************************************** - //** toggle - //************************************************************************** - /** Used to toggle the button's selection state. - */ - this.toggle = function(){ - if (config.toggle===true){ - if (mainDiv.selected===true){ - me.deselect(); - if (menu) menu.style.visibility = "hidden"; - } - else{ - me.select(); - if (menu) menu.style.visibility = "visible"; - } - } - }; - - - //************************************************************************** - //** getMenuPanel - //************************************************************************** - /** Returns the DOM element associated with the menu panel. Typically, this - * is used to render menu options (i.e. buttons). - */ - this.getMenuPanel = function(){ - return menu; - }; - - - //************************************************************************** - //** Utils - //************************************************************************** - var merge = javaxt.dhtml.utils.merge; - var createTable = javaxt.dhtml.utils.createTable; - var createElement = javaxt.dhtml.utils.createElement; - var addShowHide = javaxt.dhtml.utils.addShowHide; - var setStyle = function(el, style){ - javaxt.dhtml.utils.setStyle(el, config.style[style]); - }; - var addStyle = function(el, style){ - javaxt.dhtml.utils.addStyle(el, config.style[style]); - }; - - - init(); +if(!javaxt) var javaxt={}; +if(!javaxt.dhtml) javaxt.dhtml={}; + +//****************************************************************************** +//** Button Class +//****************************************************************************** +/** + * Custom button control. The button has 3 parts: icon, label, and an arrow. + * + ******************************************************************************/ + +javaxt.dhtml.Button = function(parent, config) { + this.className = "javaxt.dhtml.Button"; + + var me = this; + var defaultConfig = { + + /** Text label for the button. + */ + label: null, + + + /** If true, the button will initially appear in a selected state and the + * isSelected() method will return true. Default is false. + */ + selected: false, + + + /** If true, the button will initially appear disabled and the + * isDisabled() method will return true. Default is false. + */ + disabled: false, + + + /** If true, the button will be rendered as a toggle button. Default is + * false, unless the button has a menu. + */ + toggle: false, + + + /** If true, will create a drop-down menu for the button. Components such + * as buttons or custom DOM elements can be added directly to the menu. + * See getMenuPanel() for more information. + */ + menu: false, + + + /** Sets the position of the menu panel. Options are "bottom" or "right". + */ + menuAlign: "bottom", + + + + /** Used to set the "display" style attribute for the outer DOM element. + * This is not commonly ued. Default is "inline-block". + */ + display: "inline-block", + + + /** Used to set the width of the button. This property is optional. + */ + width: null, + + + /** Used to set the height of the button. This property is optional. + */ + height: null, + + + /** Used to set the icon position relative to the button label. Options + * are "left" or "right". Note that the icon style is set in the style + * config. The icon style should not be used to control whether the + * icon appears to the left or right of the label. Use this config + * instead. + */ + iconAlign: "left", + + + /** Used to set spacing between the button icon and the button label. + * Default is "5px". + */ + iconPadding: "5px", + + + /** Style for individual elements within the component. Note that you can + * provide CSS class names instead of individual style definitions. + */ + style:{ + + button: { + border: "1px solid #cccccc", + borderRadius: "3px", + background: "#F6F6F6", + cursor: "pointer", + padding: "3px 7px", + margin: "0px", + color: "#2b2b2b" + }, + + label: { + fontFamily: "helvetica,arial,verdana,sans-serif", + fontSize: "14px", + whiteSpace: "nowrap" + }, + + icon: { + + }, + + arrow: { + + }, + + select: { + background: "#007FFF", + border: "1px solid #003EFF", + color: "#FFFFFF" + }, + + hover: { + background: "#ededed" + }, + + + menu: { + border: "1px solid #cccccc", + background: "#F6F6F6", + cursor: "pointer", + padding: "3px 3px", + zIndex: "1" + } + + }, + + + /** Sound to play when the button is clicked + */ + sound: null + }; + + var mainDiv; + var mask, menu; + var icon, label, arrow; + + + //************************************************************************** + //** Constructor + //************************************************************************** + var init = function(){ + + if (typeof parent === "string"){ + parent = document.getElementById(parent); + } + if (!parent) return; + + + //Clone the config so we don't modify the original config object + var clone = {}; + merge(clone, config); + + + //Merge clone with default config + merge(clone, defaultConfig); + config = clone; + + + //Get icon alignment + var iconAlignment = config.style.iconAlign; //legacy + if (!iconAlignment) iconAlignment = config.iconAlign; //preferred + if (iconAlignment!=="right") iconAlignment = "left"; + config.iconAlign = iconAlignment; + + + //Get menu alignment + var menuAlignment = config.style.menuAlignment; //legacy + if (!menuAlignment) menuAlignment = config.menuAlign; //preferred + if (menuAlignment!=="right") menuAlignment = "bottom"; + + + //Update arrow style as needed + if (config.menu===true){ + var arrowDefined = false; + if (typeof config.style.arrow === "string"){ + arrowDefined = true; + } + else{ + for (var key in config.style.arrow){ + if (config.style.arrow.hasOwnProperty(key)){ + arrowDefined = true; + break; + } + } + } + + if (!arrowDefined){ + if (menuAlignment==="bottom"){ + config.style.arrow = { + width: 0, + height: 0, + borderLeft: "5px solid transparent", + borderRight: "5px solid transparent", + borderTop: "5px solid #575757", + marginLeft: "10px" + }; + config.style.arrowSelect = { + borderTop: "5px solid #FFFFFF" + }; + } + } + } + + + + //Create outer div used to hold the button, mask, and menu + var outerDiv = createElement('div', parent); + outerDiv.className = "javaxt-button"; + + + //Only set display if there's a mismatch (cleaner dom) + var cs = window.getComputedStyle(outerDiv); + if (cs.display!=config.display){ + outerDiv.style.display = config.display; + } + + //Simlarly, only set position as needed + if (cs.position!="relative"){ + outerDiv.style.position = "relative"; + } + + //Set width/height as needed + if (config.width){ + if (typeof config.width === "string"){ + outerDiv.style.width = config.width; + } + else{ + outerDiv.style.width = config.width + "px"; + } + } + if (config.height){ + if (typeof config.height === "string"){ + outerDiv.style.height = config.height; + } + else{ + outerDiv.style.height = config.height + "px"; + } + } + if (config.hidden===true){ //legacy config... + outerDiv.style.visibility = 'hidden'; + outerDiv.style.display = 'none'; + } + me.el = outerDiv; + + + + + //The button is implemented using a simple HTML table with 3 columns. The + //left and right columns are for icons and the center column is for the + //button label. The width of the center column is set to 100% and the + //width of the left and right columns are defined by the "icon" and + //"arrow" styles. Unfortunately, some browsers seem to have issues + //rendering the table correctly inside of a div when the outerDiv's + //display style set to "inline-block". For example, Mobile Safari will + //completely ignore the "inline-block" style and stretch the button to + //100% of the available width. In Chrome, if the left or right columns has + //a width, the "inline-block" style is ignored and the button is stretched + //to 100% of the available width. As a workaround, it looks like we can + //wrap the button div in another div with the display style set to "table". + var tableDiv = createElement('div', outerDiv); + if (outerDiv.style.display==="inline-block" || cs.display==="inline-block"){ + tableDiv.style.display = "table"; + if (config.width) tableDiv.style.width = outerDiv.style.width; + } + tableDiv.style.height = "100%"; + + + //Create main div used to represent the button + mainDiv = createElement('div', tableDiv, config.style.button); + mainDiv.setAttribute("desc", "button"); + addEventHandlers(mainDiv); + + + + var table = createTable(mainDiv); + table.style.fontFamily = "inherit"; + table.style.textAlign = "inherit"; + table.style.color = "inherit"; + var tr = table.addRow(); + var td; + + + //Add icon (or label) + td = tr.addColumn(); + if (iconAlignment==="left"){ + icon = createElement("div", td, config.style.icon); + } + else{ + arrow = createElement("div", td, config.style.arrow); + } + + + //Add label + td = tr.addColumn({width: "100%"}); + label = createElement("div", td); + setStyle(label, "label"); + + + + //Add arrow (or icon) + td = tr.addColumn(); + if (iconAlignment==="left"){ + arrow = createElement("div", td, config.style.arrow); + } + else{ + icon = createElement("div", td, config.style.icon); + } + + + + //Add show/hide to components + addShowHide(label); + addShowHide(icon); + addShowHide(arrow); + + + + //Hide icon as needed + var hideIcon = true; + if (typeof config.style.icon === "string"){ + hideIcon = false; + } + else{ + for (var key in config.style.icon){ + if (config.style.icon.hasOwnProperty(key)){ + hideIcon = false; + break; + } + } + } + if (hideIcon) icon.hide(); + + + + + //Create menu panel as needed + if (config.menu===true){ + config.toggle = true; + menu = createElement('div', outerDiv, config.style.menu); + menu.setAttribute("desc", "menu"); + menu.style.position = "absolute"; + menu.style.visibility = "hidden"; + + + var hideMenu = function(e){ + if (!mainDiv.contains(e.target)){ + menu.style.visibility = "hidden"; + me.deselect(); + } + }; + + //Hide menu if the client clicks outside of the menu + window.addEventListener('click', hideMenu); + + + //Create logic to process touch events + var touchStartTime, touchEndTime; + var x1, x2, y1, y2; + + window.addEventListener('touchstart', function(e){ + x1 = e.changedTouches[0].pageX; + y1 = e.changedTouches[0].pageY; + touchStartTime = new Date().getTime(); + touchEndTime = null; + }); + + window.addEventListener('touchend', function(e){ + + touchEndTime= new Date().getTime(); + x2 = e.changedTouches[0].pageX; + y2 = e.changedTouches[0].pageY; + + var distance = Math.sqrt( (x2-=x1)*x2 + (y2-=y1)*y2 ); + if (distance<0) distance = -distance; + var duration = touchEndTime - touchStartTime; + + if ((duration <= 500 && distance <= 10) || //Quick tap + (duration > 500 && distance <= 10)) { //Long press + hideMenu(e); + } + }); + + } + + + //Set button label + me.setLabel(config.label); + + + //Set button state + if (config.disabled===true) me.disable(); + if (config.selected===true) me.select(); + + + //Add public show/hide methods + addShowHide(me); + }; + + + //************************************************************************** + //** addEventHandlers + //************************************************************************** + var addEventHandlers = function(div){ + + //Disable text selection + div.unselectable="on"; + div.onselectstart=function(){return false;}; + + + //Create onclick function + var onclick = function(e){ + e.stopPropagation(); + if (config.sound!=null) config.sound.play(); + + + if (config.toggle===true){ + if (menu){ + + if (isTouch){ + me.toggle(); + } + else{ + //Do nothing - button is toggled on mouse down... + } + } + else{ + me.toggle(); + } + } + else{ + setDefaultStyle(mainDiv); + } + + + me.onClick(); + }; + + + //Create logic to process touch events + var touchStartTime; + var touchEndTime; + var x1, x2, y1, y2; + var isTouch = false; + + div.ontouchstart = function(e) { + isTouch = true; + + e.preventDefault(); + x1 = e.changedTouches[0].pageX; + y1 = e.changedTouches[0].pageY; + touchStartTime = new Date().getTime(); + touchEndTime = null; + + if (div.selected!==true){ + setHoverStyle(div); + } + }; + + div.ontouchend = function(e) { + + touchEndTime= new Date().getTime(); + x2 = e.changedTouches[0].pageX; + y2 = e.changedTouches[0].pageY; + + var distance = Math.sqrt( (x2-=x1)*x2 + (y2-=y1)*y2 ); + if (distance<0) distance = -distance; + var duration = touchEndTime - touchStartTime; + + if ((duration <= 500 && distance <= 10) || //Quick tap + (duration > 500 && distance <= 10)) { //Long press + onclick(e); + } + else{ + setDefaultStyle(div); + } + }; + + + + //Logic to process mouse events + if (!isTouch){ + div.onmousedown=function(){ + + setSelectStyle(div); + + if (menu){ + me.toggle(); + //TODO: Add mouseup events to buttons in the menu + } + + //return false; + }; + div.onclick = function(e){ + onclick(e); + }; + div.onmouseover = function(){ + if (div.selected!==true){ + setHoverStyle(div); + } + }; + div.onmouseout = function(){ + if (div.selected!==true){ + setDefaultStyle(div); + } + }; + } + }; + + + //************************************************************************** + //** click + //************************************************************************** + /** Used to click the button and fire onClick event + */ + this.click = function(){ + mainDiv.click(); + }; + + + //************************************************************************** + //** onClick + //************************************************************************** + /** Called whenever the button is clicked. + */ + this.onClick = function(){}; + + + //************************************************************************** + //** getText + //************************************************************************** + /** Returns the button label. Same as getLabel(). + */ + this.getText = function(){ + return me.getLabel(); + }; + + + //************************************************************************** + //** getLabel + //************************************************************************** + /** Returns the button label. + */ + this.getLabel = function(){ + return label.innerText; + }; + + + //************************************************************************** + //** setLabel + //************************************************************************** + /** Used to update the button label. + */ + this.setLabel = function(str){ + if (typeof str === 'undefined' || str===null || str.length===0){ + label.innerText = ""; + label.hide(); + } + else{ + str = str+""; + label.innerText = str; + label.show(); + addLabelPadding(str); //Explicitely pass the label (bug fix) + } + }; + + + //************************************************************************** + //** enable + //************************************************************************** + /** Used to enable the button. + */ + this.enable = function(){ + var outerDiv = me.el; + outerDiv.style.opacity = ""; + if (mask) mask.style.visibility = "hidden"; + }; + + + //************************************************************************** + //** disable + //************************************************************************** + /** Used to disable the button. + */ + this.disable = function(){ + + var outerDiv = me.el; + outerDiv.style.opacity = "0.5"; + + if (mask){ + mask.style.visibility = "visible"; + } + else{ + mask = createElement('div',{ + position: "absolute", + zIndex: "1", + width: "100%", + height: "100%" + }); + mask.setAttribute("desc", "mask"); + outerDiv.insertBefore(mask, outerDiv.firstChild); + } + }; + + + //************************************************************************** + //** isEnabled + //************************************************************************** + /** Returns true if the button is enabled (i.e. not disabled). + */ + this.isEnabled = function(){ + return !me.isDisabled(); + }; + + + //************************************************************************** + //** isDisabled + //************************************************************************** + /** Returns true if the button is disabled. + */ + this.isDisabled = function(){ + if (mask){ + if (mask.style.visibility !== "hidden") return true; + } + return false; + }; + + + //************************************************************************** + //** select + //************************************************************************** + /** Used to update the "selected" state of the button. + */ + this.select = function(){ + if (mainDiv.selected===true) return; + mainDiv.selected = true; + setDefaultStyle(mainDiv); + setSelectStyle(mainDiv); + }; + + + //************************************************************************** + //** deselect + //************************************************************************** + /** Used to update the "selected" state of the button. + */ + this.deselect = function(){ + if (mainDiv.selected===true){ + mainDiv.selected = false; + setDefaultStyle(mainDiv); + } + }; + + + //************************************************************************** + //** isSelected + //************************************************************************** + /** Returns true if the button is selected (e.g. depressed) + */ + this.isSelected = function(){ + return (mainDiv.selected===true); + }; + + + //************************************************************************** + //** toggle + //************************************************************************** + /** Used to toggle the button's selection state. + */ + this.toggle = function(){ + if (config.toggle===true){ + if (mainDiv.selected===true){ + me.deselect(); + if (menu) menu.style.visibility = "hidden"; + } + else{ + me.select(); + if (menu) menu.style.visibility = "visible"; + } + } + }; + + + //************************************************************************** + //** getMenuPanel + //************************************************************************** + /** Returns the DOM element associated with the menu panel. Typically, this + * is used to render menu options (i.e. buttons). + */ + this.getMenuPanel = function(){ + return menu; + }; + + + //************************************************************************** + //** setDefaultStyle + //************************************************************************** + var setDefaultStyle = function(div){ + setStyle(div, "button"); + setStyle(icon, "icon"); + setStyle(arrow, "arrow"); + addLabelPadding(); + }; + + + //************************************************************************** + //** setHoverStyle + //************************************************************************** + var setHoverStyle = function(div){ + addStyle(div, "hover"); + addStyle(icon, "iconHover"); + addStyle(arrow, "arrowHover"); + addLabelPadding(); + }; + + + //************************************************************************** + //** setSelectStyle + //************************************************************************** + var setSelectStyle = function(div){ + addStyle(div, "select"); + addStyle(icon, "iconSelect"); + addStyle(arrow, "arrowSelect"); + addLabelPadding(); + }; + + + //************************************************************************** + //** addLabelPadding + //************************************************************************** + var addLabelPadding = function(){ + if (icon.isVisible()){ + var str = arguments.length>0 ? arguments[0] : me.getLabel(); + if (!(typeof str === 'undefined' || str===null || str.length===0)){ + if (config.iconAlign==="left"){ + icon.style.marginRight = config.iconPadding; + } + else{ + icon.style.marginLeft = config.iconPadding; + } + } + } + }; + + + //************************************************************************** + //** Utils + //************************************************************************** + var merge = javaxt.dhtml.utils.merge; + var isEmpty = javaxt.dhtml.utils.isEmpty; + var createTable = javaxt.dhtml.utils.createTable; + var createElement = javaxt.dhtml.utils.createElement; + var addShowHide = javaxt.dhtml.utils.addShowHide; + var setStyle = function(el, style){ + + //Don't set empty style. Especially for hidden elements (e.g. icon) + var s = config.style[style]; + if (isEmpty(s)) return; + + javaxt.dhtml.utils.setStyle(el, s); + }; + var addStyle = function(el, style){ + javaxt.dhtml.utils.addStyle(el, config.style[style]); + }; + + + init(); }; \ No newline at end of file diff --git a/src/calendar/Calendar.css b/src/calendar/Calendar.css deleted file mode 100644 index fce839c..0000000 --- a/src/calendar/Calendar.css +++ /dev/null @@ -1,276 +0,0 @@ - -/**************************************************************************/ -/** Header -/**************************************************************************/ - -/* style for the header row */ -.javaxt-cal-header { - border: 1px solid #99BBE8; - background: none repeat scroll 0 0 #EEEEEE; - height: 25px; - -} - -/* style for individual cells in the header */ -.javaxt-cal-header-col { - border-left: 1px solid #99BBE8; - border-right: 1px solid #99BBE8; - text-align: center; - font-family: tahoma,arial,verdana,sans-serif; - font-size: 11px; - padding-top: 5px; -} - - - -/**************************************************************************/ -/** Multiday Event Header -/**************************************************************************/ - -/* style for the header row */ -.javaxt-cal-multiday-header { - border-bottom: 1px solid #99BBE8; - border-left: 1px solid #000000; - border-right: 1px solid #000000; - background-color: #FFFFFF; -} - -/* style for individual cells in the header */ -.javaxt-cal-multiday-col { - border-left: 1px solid #000000; - border-right: 1px solid #000000; - padding-top: 0px; - vertical-align: top; -} - -/* style for the left cell in the multiday header */ -.javaxt-cal-multiday-col-spacer { - -} - - -/**************************************************************************/ -/** Body -/**************************************************************************/ - -/* style for the body row */ -.javaxt-cal-body { - border-right: 1px solid #000000; - border-bottom: 1px solid #000000; - border-left: 1px solid #000000; -} - - -/**************************************************************************/ -/** Cells -/**************************************************************************/ - -/* Style for individual cells in the body. Note that the cell header and - footer are set using seperate css classes -*/ -.javaxt-cal-cell { - border-right: 1px solid #000000; - border-left: 1px solid #000000; - vertical-align: top; -} - -/* Style for headers inside individual cells (e.g. numbers inside the month view)*/ -.javaxt-cal-cell-header { - height: 15px; - padding:0 2px 0 0; - text-align: right; - font-family: tahoma,arial,verdana,sans-serif; - font-size: 11px; -} - -.javaxt-cal-cell-footer { - border-bottom: 1px solid #000000; -} - - -/* background color for days before and after current month */ -.javaxt-cal-cell-prev-month, .javaxt-cal-cell-next-month { - background: none repeat scroll 0 0 #EFF9FC; - border-right: 1px solid #000000; - border-left: 1px solid #000000; -} - - - -/**************************************************************************/ -/** Hours -/**************************************************************************/ -/** Style for horizonal lines within the cells used to deliniate hours and - * half hours. Note that the heights are very important. Make sure the - * height of the half-hour is exactly 1/2 the height of an hour. - */ - -.javaxt-cal-hour { - height: 55px; /* 56px-1px for border */ - border-top: 1px solid #99BBE8; -} - -.javaxt-cal-half-hour { - height: 27px; /* 28px-1px for border */ - border-top: 1px solid #99BBE8; -} - -.javaxt-cal-half-hour-sep { - border-top: 1px solid #D7E8FF; -} - -.javaxt-cal-hour-last { - border-bottom: 1px solid #99BBE8; -} - - -/**************************************************************************/ -/** Hour Labels -/**************************************************************************/ -/** Style for the hour labels that appear in the left column of the day and - * week views. - */ - -.javaxt-cal-label-hour { - font-family:"Times New Roman", Times, serif; - font-size: 18pt; - vertical-align: top; -} - -.javaxt-cal-label-meridian { - font-family: tahoma,arial,verdana,sans-serif; - font-size: 11px; - padding: 0px 0px 0px 3px; - vertical-align: top; -} - - -/**************************************************************************/ -/** Current Time Indicator -/**************************************************************************/ -/** Style for the horizonal line used to indicate the current time in the - * day and week views. - */ - -.javaxt-cal-current-time-indicator { - border-top: 1px solid #FFA1A1; -} - - -/**************************************************************************/ -/** Events -/**************************************************************************/ -/** Style for individual events. */ - -.javaxt-cal-event { - - font-family: tahoma,arial,verdana,sans-serif; - font-size: 11px; - padding-left: 4px; /* Padding inside the event div */ - white-space: nowrap; - - cursor: pointer; - - - border: 1px solid #803D5E; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - - background-color: #E2C0D0; - background: linear-gradient(#F1E2E9, #E2C0D0); - background: -ms-linear-gradient(#F1E2E9, #E2C0D0); - background: -webkit-linear-gradient(#F1E2E9, #E2C0D0); - background: -moz-linear-gradient(center top, #F1E2E9, #E2C0D0); -} - - -/* Left border for multi-day events that extend beyond the first cell in a - row or view -*/ -.javaxt-cal-event-continue-left { - -moz-border-radius: 0px; - -webkit-border-radius: 0px 3px 3px 0px; - border-radius: 0px 3px 3px 0px; - border-left: 0px; -} - - -/* Right border for multi-day events that extend beyond the last cell in a - row or view -*/ -.javaxt-cal-event-continue-right { - -moz-border-radius: 0px; - -webkit-border-radius: 3px 0px 0px 3px; - border-radius: 3px 0px 0px 3px; - border-right: 0px; -} - - -/* Left and Right borders for multi-day events that extend beyond the first and - last cell in a row/view -*/ -.javaxt-cal-event-continue-left.javaxt-cal-event-continue-right { - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - border-radius: 0px; -} - - -/* Style for events as they are being dragged */ -.javaxt-cal-event-drag { - cursor: move; - box-shadow: 0 12px 14px 0 rgba(0, 0, 0, 0.2), 0 13px 20px 0 rgba(0, 0, 0, 0.2); -} - -/**************************************************************************/ -/** Alternative Colors for Events -/**************************************************************************/ - -.javaxt-cal-event-blue { - border: 1px solid #6986BE; - background-color: #DBE1EF; - - background: linear-gradient(#FFFFFF, #DBE1EF); - background: -ms-linear-gradient(#FFFFFF, #DBE1EF); - background: -webkit-linear-gradient(#FFFFFF, #DBE1EF); - background: -moz-linear-gradient(center top, #FFFFFF, #DBE1EF); -} - -.javaxt-cal-event-red { - border: 1px solid #A81D24; - background-color: #E55F66; - /*background-image: -moz-linear-gradient(center top , #F2B6B6, #E55F66);*/ - - background: linear-gradient(#FFFFFF, #E55F66); - background: -ms-linear-gradient(#FFFFFF, #E55F66); - background: -webkit-linear-gradient(#FFFFFF, #E55F66); - background: -moz-linear-gradient(center top, #FFFFFF, #E55F66); -} - - -.javaxt-cal-event-yellow { - border: 1px solid #9D7925; - background-color: #FFFF83; - /*background-image: -moz-linear-gradient(center top , #FFFDD6, #FFFF83);*/ - - background: linear-gradient(#FFFFFF, #FFFF83); - background: -ms-linear-gradient(#FFFFFF, #FFFF83); - background: -webkit-linear-gradient(#FFFFFF, #FFFF83); - background: -moz-linear-gradient(center top, #FFFFFF, #FFFF83); -} - - - -/**************************************************************************/ -/** Misc -/**************************************************************************/ - -.javaxt-noselect { - -webkit-user-select: none; - -moz-user-select: none; - -o-user-select: none; - -ms-user-select: none; - -khtml-user-select: none; - user-select: none; -} \ No newline at end of file diff --git a/src/calendar/Calendar.js b/src/calendar/Calendar.js index 8482c27..4582a74 100644 --- a/src/calendar/Calendar.js +++ b/src/calendar/Calendar.js @@ -1,292 +1,445 @@ -if(!javaxt) var javaxt={}; -if(!javaxt.dhtml) javaxt.dhtml={}; - -//****************************************************************************** -//** Calendar Class -//*****************************************************************************/ -/** - * Used to render events for a given month/week/day - * - ******************************************************************************/ - -javaxt.dhtml.Calendar = function(parent, config) { - - this.className = "javaxt.dhtml.Calendar"; - - var me = this; - var view = null; - var views = {}; - var currView; - - - var rendered = false; - var deferredEvents = []; - var _listeners = {}; - - - var supportedViews = { - day: javaxt.dhtml.calendar.Day, - week: javaxt.dhtml.calendar.Week, - month: javaxt.dhtml.calendar.Month - }; - - - //************************************************************************** - //** Constructor - //************************************************************************** - /** Creates a new instance of this class. */ - - var init = function(){ - - //Clone the config so we don't modify the original config object - var clone = {}; - merge(clone, config); - config = clone; - - - //Set store - if (config.eventStore==null) config.eventStore = new javaxt.dhtml.calendar.EventStore(); - - - - //Replace listeners with local callbacks so that the events appear to - //be fired from this class instead of the views - var listeners = config.listeners; - var beforerender = null; - var afterrender = null; - if (listeners!=null){ - - for (var listenerName in listeners) { - if (listeners.hasOwnProperty(listenerName)) { - var listener = listeners[listenerName]; - - - if (listenerName=='beforerender'){ - beforerender = listener; - delete listeners[listenerName]; - } - else if (listenerName=='afterrender'){ - afterrender = listener; - delete listeners[listenerName]; - } - else{ - _listeners[listenerName] = listener; - (function(listenerName) { - - listeners[listenerName] = function(){ - //console.log("** fire " + listenerName + "? " + rendered); - - - var args = []; - for (var i=0; i -1; - var isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || - window.navigator.userAgent.indexOf("Trident/") > -1; - - - - //************************************************************************** - //** Constructor - //************************************************************************** - /** Creates a new instance of the calendar control. */ - - var init = function(){ - - //Call super - new javaxt.dhtml.calendar.View(me, config); - - //Set store - store = config.eventStore==null ? new javaxt.dhtml.calendar.EventStore() : config.eventStore; - - //Set number of days to render - if (config.days!=null) days = config.days; - - //Set event size, padding, and spacing - function isNumeric(n){ return !isNaN(parseFloat(n)) && isFinite(n); } - if (isNumeric(config.eventHeight)) eventHeight = parseInt(config.eventHeight); - if (isNumeric(config.eventPadding)) eventPadding = parseInt(config.eventPadding); - - - //Specify function used to get current date - getCurrentDate = config.getCurrentDate==null ? - getCurrentDate = function(){return new Date();} : config.getCurrentDate; - - - - //Configure renderers - if (config.renderers){ - for (var rendererName in config.renderers) { - if (config.renderers.hasOwnProperty(rendererName)) { - if (me[rendererName]){ - - //Override the default renderer - (function(rendererName) { - me[rendererName] = function(){ - var renderer = config.renderers[rendererName]; - return renderer.apply(me, arguments); - }; - - })(rendererName); - } - } - } - } - - - //Call the beforerender callback - rendered = false; - var listener = me.getListener('beforerender'); - if (listener!=null) listener.callback.apply(listener.scope, [me]); - - //Set date and render the calendar - me.setDate(config.date); - - //Call the afterrender callback - listener = me.getListener('afterrender'); - if (listener!=null) listener.callback.apply(listener.scope, [me]); - rendered = true; - - //Call the update callback - listener = me.getListener('update'); - if (listener!=null) listener.callback.apply(listener.scope, [me]); - }; - - - //************************************************************************** - //** hasHours - //************************************************************************** - this.hasHours = function(){ - return true; - }; - - - //************************************************************************** - //** show - //************************************************************************** - this.show = function(){ - parent.appendChild(el); - }; - - - //************************************************************************** - //** hide - //************************************************************************** - this.hide = function(){ - parent.removeChild(el); - }; - - - //************************************************************************** - //** showFooter - //************************************************************************** - this.showFooter = function(){ - footerRow.style.display = ''; - }; - - - //************************************************************************** - //** hideFooter - //************************************************************************** - this.hideFooter = function(){ - footerRow.style.display = "none"; - }; - - - //************************************************************************** - //** enableTouch - //************************************************************************** - this.enableTouch = function(){ - scrollable = true; - }; - - - //************************************************************************** - //** disableTouch - //************************************************************************** - this.disableTouch = function(){ - scrollable = false; - }; - - - //************************************************************************** - //** renderTable - //************************************************************************** - /** Used to render a new table for the current date. */ - - var renderTable = function(){ - - - //Remove any previously rendered table - if (el!=null){ - for (var i=0; i1) text = text.substring(0,3); - //else text+= ", " + javaxt.dhtml.calendar.Utils.monthNames[d.getMonth()] + " " + d.getDate(); - innerDiv.innerHTML = text; - - - outerDiv.appendChild(innerDiv); - return outerDiv; - }; - - - - this.createColumnFooter = function(i){ - return document.createElement('div'); - }; - - - - //************************************************************************** - //** createHourLabel - //************************************************************************** - /** Returns a div used to indicate the hour of day (e.g. 12pm). This method - * can be safely overridden to generate custom labels for hours. - */ - this.createHourLabel = function(hour){ - var div = document.createElement('div'); - div.style.float = "right"; - div.style.padding = "0px 7px 0px 7px"; - - - //Update hour and set meridian - var meridian = 'AM'; - if (hour==0) hour = 12; - else if (hour==12) meridian = 'PM'; - else if (hour>12){ - hour = (hour-12); - meridian = 'PM'; - } - - //Create table to render the hour and meridian - var hdr = document.createElement('table'); - hdr.cellSpacing = 0; - hdr.cellPadding = 0; - div.appendChild(hdr); - var t = document.createElement('tbody'); - hdr.appendChild(t); - var tr = document.createElement('tr'); - t.appendChild(tr); - var td = document.createElement('td'); - td.className = "javaxt-cal-label-hour"; - td.innerHTML = hour; - tr.appendChild(td); - td = document.createElement('td'); - td.className = "javaxt-cal-label-meridian"; - td.innerHTML = meridian; - tr.appendChild(td); - - return div; - }; - - - //************************************************************************** - //** updateCurrTime - //************************************************************************** - /** Used to update the position of the current time indicator. - */ - var updateCurrTime = function(){ - - var date = getCurrentDate(); - var cellID = (date.getMonth()+1) + "-" + date.getDate() + "-" + date.getFullYear(); - var cell = cells[cellID]; - if (cell){ - - var hours = date.getHours(); - var h1 = getVerticalOffset(hours, cell); - var h2 = getVerticalOffset(hours+1, cell); - - var pixelsPerMinute = (h2-h1)/60; - var h = h1+(date.getMinutes()*pixelsPerMinute); - - currTimeDiv.style.display = ''; - currTimeDiv.style.top = h + "px"; - } - else{ - currTimeDiv.style.display = "none"; - } - }; - - - //************************************************************************** - //** scrollTo - //************************************************************************** - /** Used to scroll to a specific time of day. - * @param hour Time of day, specified as a decimal (e.g. 6.5 for 6:30 AM) - */ - this.scrollTo = function(hour){ - var cellID = (date.getMonth()+1) + "-" + date.getDate() + "-" + date.getFullYear(); - var cell = cells[cellID]; - var offset = getVerticalOffset(hour, cell); - bodyDiv.scrollTop = offset+1; //+1 for border - }; - - - //************************************************************************** - //** getScrollDiv - //************************************************************************** - this.getScrollDiv = function(){ - return bodyDiv; - }; - - - //************************************************************************** - //** getVerticalOffset - //************************************************************************** - /** Returns the relative, vertical offset of a row in a cell. - */ - var getVerticalOffset = function(hour, cell){ - - //Compute row heights and cache the values for subsequent use. Caching - //the row heights improves load times by approx 500ms in IE and 1000ms - //in FF. Caching assumes that row heights are the same accross all cells - //and that the row heights do not change. - if (rowHeights.length==0){ - var tbody = cell.childNodes[0].childNodes[0]; - var rows = tbody.childNodes; - for (var i=0; i=hour){ - return (offset-rowHeight); - } - } - return 0; - }; - - - //************************************************************************** - //** getEventStore - //************************************************************************** - this.getEventStore = function(){ - return store; - }; - - - //************************************************************************** - //** addEvent - //************************************************************************** - this.addEvent = function(event){ - addEvent(event); - }; - - - //************************************************************************** - //** addEvent - //************************************************************************** - /** Private method used to add an event to the view. - * @param deferUpdates Option to postpone or defer updating the event - * width and position when there are overlapping events. This option is - * provided for optimization. - */ - var addEvent = function(event, deferUpdates){ - - log("Adding " + event.getSubject() + "..."); - var numDays = event.numDays(); - - - //Check if we've already rendered the given event - if (numDays>=1){ - for (var i=1; i0){ - var div = td.childNodes[0]; - if (div.event.equals(event)){ - return; - } - } - } - } - - } - else{ - var divs = getDivs(event.getStartDate()); - for (var i=0; i=1){ - addMultiDayEvent(event); - } - else{ - addSingleDayEvent(event, deferUpdates); - } - - - //Update the event store - store.add(event); - }; - - - //************************************************************************** - //** addEvents - //************************************************************************** - /** Used to add multiple events to the view. This method is recommended for - * bulk loading and is significantly faster than calling addEvent multiple - * times. - */ - this.addEvents = function(events){ - - - //Add events but defer updating position and widths of overlapping events - for (var i=0; i0){ - - //Compute number of columns - var numColumns = 2; - if (overlappingEvents.length>1){ - - //The following logic is somewhat flawed and may yeild - //a column count slightly higher than expected... - for (var j=0; j0){ - var _numColumns = 1 + 1 + numIntersects; - if (_numColumns>numColumns) numColumns = _numColumns; - } - - } - } - - - var width = (Math.floor((1/(numColumns))*100)); - log("Update " + event.getSubject() + " width to " + width + "%"); - var outerDiv = getDiv(event); - outerDiv.style.width = width + "%"; - - - for (var j=0; j0){ - - for (var j=0; j1){ - - var left = parseInt(outerDiv.style.left); - var colIndex = Math.floor(left/width)+1; - - - log(event.getSubject() + " is in column " + colIndex + "/" + numColumns); - - if (colIndex===(numColumns-1)){ - var overlappingEvents = getOverlappingEvents(event); - var nextEvent = overlappingEvents[overlappingEvents.length-1]; - var nextDiv = getDiv(nextEvent); - var orgWidth = parseInt(outerDiv.style.width); - var newWidth = parseInt(nextDiv.style.left) - left; - if (newWidth>orgWidth){ - log(" ++ Updating " + event.getSubject() + " width to " + newWidth + "% (was " + orgWidth + "%)"); - outerDiv.style.width = newWidth + "%"; - } - } - - } - } - - }; - - - //************************************************************************** - //** removeEvent - //************************************************************************** - - this.removeEvent = function(event){ - - //Update event store - store.remove(event); - - - //Remove div - if (event.numDays()>=1){ - - for (var i=1; i0){ - var div = td.childNodes[0]; - if (div.event.equals(event)){ - - //Remove event div - td.removeChild(div); - - - //Remove colspan - var colSpan = td.colSpan; - if (colSpan>=2){ - td.colSpan = 1; - var nextSibling = td.nextSibling; - var clone = td.cloneNode(true); - //var tr = td.parentNode; - for (var k=0; k1){ - var hasEvents = false; - for (var k=1; k0){ - hasEvents = true; - break; - } - } - - if (!hasEvents){ - multidayEventsTable.removeChild(tr); - } - } - - - //Update table height - var maxHeight = (multidayEventsTable.childNodes.length-1)*(eventHeight+eventPadding); - if (maxHeight==0){ - multidayRow.style.display = "none"; - multidayRow.style.visibility = "hidden"; - } - else{ - multidayEventsTable.parentNode.parentNode.parentNode.style.height = (maxHeight+(eventPadding)) + "px"; - } - - - return; - } - } - } - } - - } - else{ - - //Remove event div - var divs = getDivs(event.getStartDate()); - for (var i=0; i0){ - - //Remove overlapping events - for (var i=0; i1 && d.getDay()>0) d.setDate(d.getDate()-d.getDay()); - for (var i=0; i0){ - var div = td.childNodes[0]; - var event = div.event; - - var addEvent = true; - for (var k=0; k1 && d.getDay()>0) d.setDate(d.getDate()-d.getDay()); - for (var i=0; i0){ - var div = td.childNodes[0]; - var event = div.event; - store.remove(event); - } - } - - multidayEventsTable.removeChild(tr); - } - multidayRow.style.display = "none"; - multidayRow.style.visibility = "hidden"; - }; - - - //************************************************************************** - //** refresh - //************************************************************************** - /** Used to re-render all the events in the cell. - */ - this.refresh = function(){ - var events = me.getEvents(); - me.clear(); - me.addEvents(events); - me.scrollTo(6.5); - }; - - - //************************************************************************** - //** addSingleDayEvent - //************************************************************************** - /** Used to render events that start and end on the same day. - */ - var addSingleDayEvent = function(event, deferUpdates){ - - if (deferUpdates!=true) deferUpdates = false; - - //Find cell used to render event - var d = event.getStartDate(); - var cellID = (d.getMonth()+1) + "-" + d.getDate() + "-" + d.getFullYear(); - var cell = cells[cellID]; - if (cell==null) return; - - - - var width = 100; - var overlappingEvents, rects, leftCoords; - - - - //Update position and width of any overlapping events - if (!deferUpdates){ - - //Get bounding rectangles of the overlapping events - rects = []; - var overlappingEvents = getOverlappingEvents(event); - for (var i=0; i0){ - - //Update position of the new div as needed - log("Update " + event.getSubject() + "?"); - updatePosition(outerDiv, event, width); - - - //If there are any divs to the right, ensure that the width spans - //to the closest div - var rightDiv; - var r1 = _getRect(outerDiv); - var _right = r1.right; - for (var i=0; i=_right){ - - - for (var j=0; jendDate) return; - - - var startColID, endColID; - var continueLeft = false; - var continueRight = false; - var a = javaxt.dhtml.calendar.Utils.getDaysBetween(startDate, event.getStartDate()); - var b = javaxt.dhtml.calendar.Utils.getDaysBetween(startDate, event.getEndDate()); - //console.log(event.getSubject() + " " + a + " --> " + b); - - startColID = a; - if (a<0){ - startColID = 0; - continueLeft = true; - } - startColID = Math.floor(startColID); - - - - endColID = startColID+b; - if (endColID>days-1){ - endColID = days-1; - continueRight = true; - } - endColID = Math.floor(endColID); - - - //console.log("use cols " + startColID + " - " + endColID); - - - - //Find start/end columns in the multiday events table - var startCol, endCol; - for (var i=1; i0){ - startCol = endCol = null; - break; - } - - previousCol = previousCol.previousSibling; - } - } - } - - - i = multidayEventsTable.childNodes.length; - break; - } - - - var colSpan = td.colSpan; - if (colSpan>=2) idx+=colSpan; - else idx++; - } - } - - - - //Add new row to the multidayEventsTable as needed - if (!startCol || !endCol){ - var tr = document.createElement("tr"); - multidayEventsTable.appendChild(tr); - var td = document.createElement("td"); - td.className = "javaxt-cal-multiday-col-spacer"; - tr.appendChild(td); - - - //Remove height from spacer col of previous row and set current col height - if (multidayEventsTable.childNodes.length>1){ - tr.previousSibling.childNodes[0].style.height = ''; - } - td.style.height = "100%"; - - - - //Add days - for (var i=0; i=2){ - javaxt.dhtml.calendar.Utils.addColSpan(startCol, colSpan); - } - - - //Add event to the startCol - var outerDiv = document.createElement('div'); - outerDiv.style.width = "100%"; - outerDiv.style.height = "100%"; - outerDiv.style.position = "absolute"; - - var innerDiv = document.createElement('div'); - innerDiv.style.height = "100%"; - var paddingLeft = continueLeft ? "0px" : eventPadding + "px"; - var paddingRight = continueRight ? "0px" : eventPadding + "px"; - innerDiv.style.padding = "0px " + paddingRight + " 0px " + paddingLeft; //Horizontal padding - innerDiv.style.position = "relative"; - outerDiv.appendChild(innerDiv); - - var div = event.createDiv(continueLeft, continueRight); - div.style.height = "100%"; - innerDiv.appendChild(div); - - - //Wrap the outerdiv to ensure proper overflow - var wrapper = document.createElement('div'); - wrapper.style.width = "100%"; - wrapper.style.height = eventHeight + "px"; - wrapper.style.position = "relative"; - wrapper.style.marginTop = (multidayEventsTable.childNodes.length>2 ? (eventPadding*2) : 0) + "px"; - wrapper.appendChild(outerDiv); - wrapper.event = event; - wrapper.onclick = function(e){ - var listener = me.getListener('eventclick'); - if (listener!=null){ - var callback = listener.callback; - var scope = listener.scope; - callback.apply(scope, [this.event, this, me, e]); - } - }; - - startCol.appendChild(wrapper); - - - //Update the visibility of the multiday row - multidayRow.style.display = ''; - multidayRow.style.visibility = "visible"; - - - //Update table height - var numMultiDayEvents = multidayEventsTable.childNodes.length-1; - var h = numMultiDayEvents*(eventHeight+eventPadding); - h = (h+(eventPadding*2)); - var multidayEventsDiv = multidayEventsTable.parentNode.parentNode.parentNode; - multidayEventsDiv.style.height = h + "px"; - - - - - //Check whether the columns are aligned. For some browsers (e.g. Firefox), - //the vertical scroll bar doesn't show up until the overflow container - //reaches a certain height. Without the scroll bar, the multiday event - //columns become misaligned with the cells. - var h1 = multidayEventsTable.childNodes[0].childNodes[1]; - var c1; - for (var id in cells) { - if (cells.hasOwnProperty(id)) { - c1 = cells[id].parentNode.parentNode; - break; - } - } - - //Update the height of the vertical scroll bar until it becomes visible. - //This should work assuming the multidayEventsTable and the header - var orgHeight = h; - while (h1.offsetWidth>c1.offsetWidth){ - if (h1 && startDate.getDay()>0) startDate.setDate(startDate.getDate()-startDate.getDay()); - - endDate = new Date(startDate); - endDate.setDate(endDate.getDate()+days); - - - renderTable(); - loadEvents(); - }; - - - //************************************************************************** - //** getDate - //************************************************************************** - this.getDate = function(){ - return date; - }; - - - //************************************************************************** - //** getDateRange - //************************************************************************** - /** Returns the start/end dates represented by this view. */ - - this.getDateRange = function(){ - return { - startDate: new Date(startDate), - endDate: new Date(endDate) - }; - }; - - - //************************************************************************** - //** getTitle - //************************************************************************** - /** Returns a title for the current view. */ - - this.getTitle = function(){ - if (days==1){ - var month = javaxt.dhtml.calendar.Utils.monthNames[date.getMonth()]; - return (month + " " + date.getDate() + ", " + date.getFullYear()); - } - else{ - var range = me.getDateRange(); - var startDate = range.startDate; - var endDate = range.endDate; - endDate.setDate(endDate.getDate()-1); - - var startMonth = javaxt.dhtml.calendar.Utils.monthNames[startDate.getMonth()]; - var endMonth = javaxt.dhtml.calendar.Utils.monthNames[endDate.getMonth()]; - - var a = startMonth + " " + startDate.getDate(); - var b = endDate.getDate() + ", " + endDate.getFullYear(); - if (startDate.getMonth()==endDate.getMonth() && startDate.getFullYear()==endDate.getFullYear()){ - return (a + " - " + b); - } - else{ - if (startDate.getFullYear()==endDate.getFullYear()){ - return (a + " - " + endMonth + " " + b); - } - else{ - return (a + ", " + startDate.getFullYear() + " - " + endMonth + " " + b); - } - } - } - }; - - - //************************************************************************** - //** loadEvents - //************************************************************************** - var loadEvents = function(){ - - var events = store.getEvents(); - for (var i=0; i=startDate.getTime()){ - me.addEvent(events[i]); - } - } - }; - - - //************************************************************************** - //** updatePosition - //************************************************************************** - /** Function used to move a div from left to right until we find an area - * that doesn't intersect another div. - */ - var updatePosition = function(div, _event, width){ - - //Get bounding rectangle of the div. Subtract 1 pixel for intesection test. - var r1 = _getRect(div); - r1 = { - left: r1.left+1, - right: r1.right-1, - top: r1.top+1, - bottom: r1.bottom-1 - }; - - - //Check whether the div intersect any other divs. Shift div - //to the right until we find a - var divs = getDivs(_event.getStartDate()); - var x = 0; - while (x<=100){ - - var shiftRight = false; - for (var j=0; j0){ - el = el.childNodes[0]; - if (el.tagName.toUpperCase()==="TD"){ - td = el; - break; - } - } - - if (td){ - var left = parseInt(outerDiv.style.left); - if (left===0){ - td.style.paddingLeft = eventPadding+"px"; - } - else{ - td.style.paddingLeft = "0px"; - } - } - }; - - - //************************************************************************** - //** sortEvents - //************************************************************************** - /** Used to sort events by order of appearance in the cell - left to right, - * top to bottom. - */ - var sortEvents = function(events){ - - var _events = []; - for (var i=0; i=30) startRow+=1; - return startRow; - }; - - - events.sort(function(event1, event2){ - var y1 = getRow(event1); - var y2 = getRow(event2); - - var div1, div2; - for (var i=0; i<_events.length; i++){ - if (_events[i].equals(event1)){ - div1 = divs[i]; - } - if (_events[i].equals(event2)){ - div2 = divs[i]; - } - - if (div1!=null && div2!=null) break; - } - var x1 = parseInt(div1.style.left); - var x2 = parseInt(div2.style.left); - - var a = x1 + (y1*1000); - var b = x2 + (y2*1000); - return a-b; - }); - - return events; - }; - - - //************************************************************************** - //** getOverlappingEvents - //************************************************************************** - /** Returns a list of events that overlap a given event. This method ignores - * multi-day events and events that are not in the current view. - */ - var getOverlappingEvents = function(event){ - - var arr = []; - var overlappingEvents = store.getOverlappingEvents(event); - for (var i=0; i=1){ - //TODO: Find multiday event div... - } - else{ - var divs = getDivs(event.getStartDate()); - for (var i=0; i r1.right || - r2.right < r1.left || - r2.top > r1.bottom || - r2.bottom < r1.top); - }; - - - //************************************************************************** - //** createTable - //************************************************************************** - var createTable = function(){ - var table = document.createElement('table'); - table.style.width = "100%"; - table.style.height = "100%"; - table.cellSpacing = 0; - table.cellPadding = 0; - table.style.borderCollapse = "collapse"; - var tbody = document.createElement('tbody'); - table.appendChild(tbody); - return tbody; - }; - - - var _getRect = javaxt.dhtml.calendar.Utils.getRect; - var initDrag = javaxt.dhtml.calendar.Utils.initDrag; - var log = function(str){if(debug)console.log(str);}; - - - init(); +if(!javaxt) var javaxt={}; +if(!javaxt.dhtml) javaxt.dhtml={}; +if(!javaxt.dhtml.calendar) javaxt.dhtml.calendar={}; + +//****************************************************************************** +//** Day View +//*****************************************************************************/ +/** + * Used to render a day + * + ******************************************************************************/ + +javaxt.dhtml.calendar.Day = function(parent, config) { + this.className = "javaxt.dhtml.calendar.Day"; + + var me = this; + var defaultConfig = { + + }; + + + //DOM elements + var el; + var bodyDiv; + var footerRow; + var multidayRow, multidayEventsTable; + var currTimeDiv, getCurrentDate; + + + //Class variables + var rendered; + var startDate, endDate; + var cells = {}; + var widths = {}; + var scrollWidth; + var scrollable = true; + + + //Config options + var days = 1; + var date; + var store; + var eventHeight = 17; //Only applies to multiday events + var eventPadding = 2; + var holdDelay = 500; + var debug = false; + + + //Browser detection used to adjust event padding + var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + var isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || + window.navigator.userAgent.indexOf("Trident/") > -1; + + + + //************************************************************************** + //** Constructor + //************************************************************************** + /** Creates a new instance of the calendar control. */ + + var init = function(){ + + //Clone the config so we don't modify the original config object + var clone = {}; + merge(clone, config); + + + //Merge clone with default config + merge(clone, defaultConfig); + config = clone; + + + //Ensure the "javaxt-noselect" style rule is present in the document + javaxt.dhtml.utils.addNoSelectRule(); + + + + //Call super + new javaxt.dhtml.calendar.View(me, config); + + //Set store + store = config.eventStore==null ? new javaxt.dhtml.calendar.EventStore() : config.eventStore; + + //Set number of days to render + if (config.days!=null) days = config.days; + + //Set event size, padding, and spacing + var isNumber = javaxt.dhtml.utils.isNumber; + if (isNumber(config.eventHeight)) eventHeight = parseInt(config.eventHeight); + if (isNumber(config.eventPadding)) eventPadding = parseInt(config.eventPadding); + + + //Specify function used to get current date + getCurrentDate = config.getCurrentDate==null ? + getCurrentDate = function(){return new Date();} : config.getCurrentDate; + + + + //Configure renderers + if (config.renderers){ + for (var rendererName in config.renderers) { + if (config.renderers.hasOwnProperty(rendererName)) { + if (me[rendererName]){ + + //Override the default renderer + (function(rendererName) { + me[rendererName] = function(){ + var renderer = config.renderers[rendererName]; + return renderer.apply(me, arguments); + }; + + })(rendererName); + } + } + } + } + + + //Call the beforerender callback + rendered = false; + var listener = me.getListener('beforerender'); + if (listener!=null) listener.callback.apply(listener.scope, [me]); + + //Set date and render the calendar + me.setDate(config.date); + + //Call the afterrender callback + listener = me.getListener('afterrender'); + if (listener!=null) listener.callback.apply(listener.scope, [me]); + rendered = true; + + //Call the update callback + listener = me.getListener('update'); + if (listener!=null) listener.callback.apply(listener.scope, [me]); + + + addResizeListener(parent, onResize); + }; + + + //************************************************************************** + //** hasHours + //************************************************************************** + this.hasHours = function(){ + return true; + }; + + + //************************************************************************** + //** show + //************************************************************************** + this.show = function(){ + parent.appendChild(el); + }; + + + //************************************************************************** + //** hide + //************************************************************************** + this.hide = function(){ + parent.removeChild(el); + }; + + + //************************************************************************** + //** showFooter + //************************************************************************** + this.showFooter = function(){ + footerRow.style.display = ''; + }; + + + //************************************************************************** + //** hideFooter + //************************************************************************** + this.hideFooter = function(){ + footerRow.style.display = "none"; + }; + + + //************************************************************************** + //** enableTouch + //************************************************************************** + this.enableTouch = function(){ + scrollable = true; + }; + + + //************************************************************************** + //** disableTouch + //************************************************************************** + this.disableTouch = function(){ + scrollable = false; + }; + + + //************************************************************************** + //** onResize + //************************************************************************** + /** Recomputes event positions after the view is resized. + */ + var onResize = function(){ + if (!rendered) return; + if (!el || !el.parentNode) return; //view is hidden (not the active view) + + //Re-lay-out the events (their positions are measured from the grid, which + //stretches/shrinks with the view). Scroll position is preserved. + var scrollTop = bodyDiv ? bodyDiv.scrollTop : null; + var events = me.getEvents(); + me.clear(); + me.addEvents(events); + if (scrollTop!=null) bodyDiv.scrollTop = scrollTop; + }; + + + + //************************************************************************** + //** renderTable + //************************************************************************** + /** Used to render a new table for the current date. + */ + var renderTable = function(){ + + + //Remove any previously rendered table + if (el!=null){ + for (var i=0; i1) text = text.substring(0,3); + //else text+= ", " + config.monthNames[d.getMonth()] + " " + d.getDate(); + innerDiv.innerHTML = text; + + + return outerDiv; + }; + + + + this.createColumnFooter = function(i){ + return createElement('div'); + }; + + + + //************************************************************************** + //** createHourLabel + //************************************************************************** + /** Returns a div used to indicate the hour of day (e.g. 12pm). This method + * can be safely overridden to generate custom labels for hours. + */ + this.createHourLabel = function(hour){ + var div = createElement('div', { + float: "right", + padding: "0px 7px 0px 7px" + }); + + + //Update hour and set meridian + var meridian = 'AM'; + if (hour==0) hour = 12; + else if (hour==12) meridian = 'PM'; + else if (hour>12){ + hour = (hour-12); + meridian = 'PM'; + } + + //Create table to render the hour and meridian + var hdr = createTable(div); + var tr = hdr.addRow(); + var td = tr.addColumn(); + addStyle(td, "labelHour"); + td.innerHTML = hour; + td = tr.addColumn(); + addStyle(td, "labelMeridian"); + td.innerHTML = meridian; + + return div; + }; + + + //************************************************************************** + //** updateCurrTime + //************************************************************************** + /** Used to update the position of the current time indicator. + */ + var updateCurrTime = function(){ + + var date = getCurrentDate(); + var cellID = (date.getMonth()+1) + "-" + date.getDate() + "-" + date.getFullYear(); + var cell = cells[cellID]; + if (cell){ + + var hours = date.getHours(); + var h1 = getVerticalOffset(hours, cell); + var h2 = getVerticalOffset(hours+1, cell); + + var pixelsPerMinute = (h2-h1)/60; + var h = h1+(date.getMinutes()*pixelsPerMinute); + + currTimeDiv.style.display = ''; + currTimeDiv.style.top = h + "px"; + } + else{ + currTimeDiv.style.display = "none"; + } + }; + + + //************************************************************************** + //** scrollTo + //************************************************************************** + /** Used to scroll to a specific time of day. + * @param hour Time of day, specified as a decimal (e.g. 6.5 for 6:30 AM) + */ + this.scrollTo = function(hour){ + var cellID = (date.getMonth()+1) + "-" + date.getDate() + "-" + date.getFullYear(); + var cell = cells[cellID]; + var offset = getVerticalOffset(hour, cell); + bodyDiv.scrollTop = offset+1; //+1 for border + }; + + + //************************************************************************** + //** getScrollDiv + //************************************************************************** + this.getScrollDiv = function(){ + return bodyDiv; + }; + + + //************************************************************************** + //** getVerticalOffset + //************************************************************************** + /** Returns the vertical offset (relative to the cell) of a given hour. The + * offset is measured directly from the rendered grid rows rather than summed + * from cached row heights - the rows stretch to fill the available space + * (e.g. on large screens) and fractional heights would otherwise accumulate + * rounding error, causing events to drift away from the hour lines. + */ + var getVerticalOffset = function(hour, cell){ + var tbody = cell.childNodes[0].childNodes[0]; + var rows = tbody.childNodes; + if (rows.length==0) return 0; + + var cellTop = _getRect(cell).top; + + //There are 2 rows per hour; find the row that starts at (or just after) + //the requested hour and return the top of that row relative to the cell. + var i = Math.ceil(hour*2); + if (i < rows.length) return _getRect(rows[i]).top - cellTop; + + //At/after the end of the day - use the bottom of the last row. + return _getRect(rows[rows.length-1]).bottom - cellTop; + }; + + + //************************************************************************** + //** getEventStore + //************************************************************************** + this.getEventStore = function(){ + return store; + }; + + + //************************************************************************** + //** addEvent + //************************************************************************** + this.addEvent = function(event){ + addEvent(event); + }; + + + //************************************************************************** + //** addEvent + //************************************************************************** + /** Private method used to add an event to the view. + * @param deferUpdates Option to postpone or defer updating the event + * width and position when there are overlapping events. This option is + * provided for optimization. + */ + var addEvent = function(event, deferUpdates){ + + log("Adding " + event.getSubject() + "..."); + var numDays = event.numDays(); + + + //Check if we've already rendered the given event + if (numDays>=1){ + for (var i=1; i0){ + var div = td.childNodes[0]; + if (div.event.equals(event)){ + return; + } + } + } + } + + } + else{ + var divs = getDivs(event.getStartDate()); + for (var i=0; i=1){ + addMultiDayEvent(event); + } + else{ + addSingleDayEvent(event, deferUpdates); + } + + + //Update the event store + store.add(event); + }; + + + //************************************************************************** + //** addEvents + //************************************************************************** + /** Used to add multiple events to the view. This method is recommended for + * bulk loading and is significantly faster than calling addEvent multiple + * times. + */ + this.addEvents = function(events){ + + + //Add events but defer updating position and widths of overlapping events + for (var i=0; i0){ + + //Compute number of columns + var numColumns = 2; + if (overlappingEvents.length>1){ + + //The following logic is somewhat flawed and may yeild + //a column count slightly higher than expected... + for (var j=0; j0){ + var _numColumns = 1 + 1 + numIntersects; + if (_numColumns>numColumns) numColumns = _numColumns; + } + + } + } + + + var width = (Math.floor((1/(numColumns))*100)); + log("Update " + event.getSubject() + " width to " + width + "%"); + var outerDiv = getDiv(event); + outerDiv.style.width = width + "%"; + + + for (var j=0; j0){ + + for (var j=0; j1){ + + var left = parseInt(outerDiv.style.left); + var colIndex = Math.floor(left/width)+1; + + + log(event.getSubject() + " is in column " + colIndex + "/" + numColumns); + + if (colIndex===(numColumns-1)){ + var overlappingEvents = getOverlappingEvents(event); + var nextEvent = overlappingEvents[overlappingEvents.length-1]; + var nextDiv = getDiv(nextEvent); + var orgWidth = parseInt(outerDiv.style.width); + var newWidth = parseInt(nextDiv.style.left) - left; + if (newWidth>orgWidth){ + log(" ++ Updating " + event.getSubject() + " width to " + newWidth + "% (was " + orgWidth + "%)"); + outerDiv.style.width = newWidth + "%"; + } + } + + } + } + + }; + + + //************************************************************************** + //** removeEvent + //************************************************************************** + + this.removeEvent = function(event){ + + //Update event store + store.remove(event); + + + //Remove div + if (event.numDays()>=1){ + + for (var i=1; i0){ + var div = td.childNodes[0]; + if (div.event.equals(event)){ + + //Remove event div + td.removeChild(div); + + + //Remove colspan + var colSpan = td.colSpan; + if (colSpan>=2){ + td.colSpan = 1; + var nextSibling = td.nextSibling; + var clone = td.cloneNode(true); + //var tr = td.parentNode; + for (var k=0; k1){ + var hasEvents = false; + for (var k=1; k0){ + hasEvents = true; + break; + } + } + + if (!hasEvents){ + multidayEventsTable.removeChild(tr); + } + } + + + //Update table height + var maxHeight = (multidayEventsTable.childNodes.length-1)*(eventHeight+eventPadding); + if (maxHeight==0){ + multidayRow.style.display = "none"; + multidayRow.style.visibility = "hidden"; + } + else{ + multidayEventsTable.parentNode.parentNode.parentNode.style.height = (maxHeight+(eventPadding)) + "px"; + } + + + return; + } + } + } + } + + } + else{ + + //Remove event div + var divs = getDivs(event.getStartDate()); + for (var i=0; i0){ + + //Remove overlapping events + for (var i=0; i1 && d.getDay()>0) d.setDate(d.getDate()-d.getDay()); + for (var i=0; i0){ + var div = td.childNodes[0]; + var event = div.event; + + var addEvent = true; + for (var k=0; k1 && d.getDay()>0) d.setDate(d.getDate()-d.getDay()); + for (var i=0; i0){ + var div = td.childNodes[0]; + var event = div.event; + store.remove(event); + } + } + + multidayEventsTable.removeChild(tr); + } + multidayRow.style.display = "none"; + multidayRow.style.visibility = "hidden"; + }; + + + //************************************************************************** + //** refresh + //************************************************************************** + /** Used to re-render all the events in the cell. + */ + this.refresh = function(){ + var events = me.getEvents(); + me.clear(); + me.addEvents(events); + me.scrollTo(6.5); + }; + + + //************************************************************************** + //** addSingleDayEvent + //************************************************************************** + /** Used to render events that start and end on the same day. + */ + var addSingleDayEvent = function(event, deferUpdates){ + + if (deferUpdates!=true) deferUpdates = false; + + //Find cell used to render event + var d = event.getStartDate(); + var cellID = (d.getMonth()+1) + "-" + d.getDate() + "-" + d.getFullYear(); + var cell = cells[cellID]; + if (cell==null) return; + + + + var width = 100; + var overlappingEvents, rects, leftCoords; + + + + //Update position and width of any overlapping events + if (!deferUpdates){ + + //Get bounding rectangles of the overlapping events + rects = []; + var overlappingEvents = getOverlappingEvents(event); + for (var i=0; i0){ + + //Update position of the new div as needed + log("Update " + event.getSubject() + "?"); + updatePosition(outerDiv, event, width); + + + //If there are any divs to the right, ensure that the width spans + //to the closest div + var rightDiv; + var r1 = _getRect(outerDiv); + var _right = r1.right; + for (var i=0; i=_right){ + + + for (var j=0; jendDate) return; + + + var startColID, endColID; + var continueLeft = false; + var continueRight = false; + var a = javaxt.dhtml.calendar.Utils.getDaysBetween(startDate, event.getStartDate()); + var b = javaxt.dhtml.calendar.Utils.getDaysBetween(startDate, event.getEndDate()); + //console.log(event.getSubject() + " " + a + " --> " + b); + + startColID = a; + if (a<0){ + startColID = 0; + continueLeft = true; + } + startColID = Math.floor(startColID); + + + + endColID = startColID+b; + if (endColID>days-1){ + endColID = days-1; + continueRight = true; + } + endColID = Math.floor(endColID); + + + //console.log("use cols " + startColID + " - " + endColID); + + + + //Find start/end columns in the multiday events table + var startCol, endCol; + for (var i=1; i0){ + startCol = endCol = null; + break; + } + + previousCol = previousCol.previousSibling; + } + } + } + + + i = multidayEventsTable.childNodes.length; + break; + } + + + var colSpan = td.colSpan; + if (colSpan>=2) idx+=colSpan; + else idx++; + } + } + + + + //Add new row to the multidayEventsTable as needed + if (!startCol || !endCol){ + var tr = createElement("tr", multidayEventsTable); + var td = createElement("td", tr); + addStyle(td, "multidayColSpacer"); + + + //Remove height from spacer col of previous row and set current col height + if (multidayEventsTable.childNodes.length>1){ + tr.previousSibling.childNodes[0].style.height = ''; + } + td.style.height = "100%"; + + + + //Add days + for (var i=0; i=2){ + javaxt.dhtml.calendar.Utils.addColSpan(startCol, colSpan); + } + + + //Add event to the startCol + var outerDiv = createElement('div', { + width: "100%", + height: "100%", + position: "absolute" + }); + + var paddingLeft = continueLeft ? "0px" : eventPadding + "px"; + var paddingRight = continueRight ? "0px" : eventPadding + "px"; + var innerDiv = createElement('div', outerDiv, { + height: "100%", + padding: "0px " + paddingRight + " 0px " + paddingLeft, //Horizontal padding + position: "relative" + }); + + var div = event.createDiv(continueLeft, continueRight); + div.style.height = "100%"; + innerDiv.appendChild(div); + + + //Wrap the outerdiv to ensure proper overflow + var wrapper = createElement('div', { + width: "100%", + height: eventHeight + "px", + position: "relative", + marginTop: (multidayEventsTable.childNodes.length>2 ? (eventPadding*2) : 0) + "px" + }); + wrapper.appendChild(outerDiv); + wrapper.event = event; + wrapper.onclick = function(e){ + var listener = me.getListener('eventclick'); + if (listener!=null){ + var callback = listener.callback; + var scope = listener.scope; + callback.apply(scope, [this.event, this, me, e]); + } + }; + + startCol.appendChild(wrapper); + + + //Update the visibility of the multiday row + multidayRow.style.display = ''; + multidayRow.style.visibility = "visible"; + + + //Update table height + var numMultiDayEvents = multidayEventsTable.childNodes.length-1; + var h = numMultiDayEvents*(eventHeight+eventPadding); + h = (h+(eventPadding*2)); + var multidayEventsDiv = multidayEventsTable.parentNode.parentNode.parentNode; + multidayEventsDiv.style.height = h + "px"; + + + + + //Check whether the columns are aligned. For some browsers (e.g. Firefox), + //the vertical scroll bar doesn't show up until the overflow container + //reaches a certain height. Without the scroll bar, the multiday event + //columns become misaligned with the cells. + var h1 = multidayEventsTable.childNodes[0].childNodes[1]; + var c1; + for (var id in cells) { + if (cells.hasOwnProperty(id)) { + c1 = cells[id].parentNode.parentNode; + break; + } + } + + //Update the height of the vertical scroll bar until it becomes visible. + //This should work assuming the multidayEventsTable and the header + var orgHeight = h; + while (h1.offsetWidth>c1.offsetWidth){ + if (h1 && startDate.getDay()>0) startDate.setDate(startDate.getDate()-startDate.getDay()); + + endDate = new Date(startDate); + endDate.setDate(endDate.getDate()+days); + + + renderTable(); + loadEvents(); + }; + + + //************************************************************************** + //** getDate + //************************************************************************** + this.getDate = function(){ + return date; + }; + + + //************************************************************************** + //** getDateRange + //************************************************************************** + /** Returns the start/end dates represented by this view. */ + + this.getDateRange = function(){ + return { + startDate: new Date(startDate), + endDate: new Date(endDate) + }; + }; + + + //************************************************************************** + //** getTitle + //************************************************************************** + /** Returns a title for the current view. */ + + this.getTitle = function(){ + if (days==1){ + var month = config.monthNames[date.getMonth()]; + return (month + " " + date.getDate() + ", " + date.getFullYear()); + } + else{ + var range = me.getDateRange(); + var startDate = range.startDate; + var endDate = range.endDate; + endDate.setDate(endDate.getDate()-1); + + var startMonth = config.monthNames[startDate.getMonth()]; + var endMonth = config.monthNames[endDate.getMonth()]; + + var a = startMonth + " " + startDate.getDate(); + var b = endDate.getDate() + ", " + endDate.getFullYear(); + if (startDate.getMonth()==endDate.getMonth() && startDate.getFullYear()==endDate.getFullYear()){ + return (a + " - " + b); + } + else{ + if (startDate.getFullYear()==endDate.getFullYear()){ + return (a + " - " + endMonth + " " + b); + } + else{ + return (a + ", " + startDate.getFullYear() + " - " + endMonth + " " + b); + } + } + } + }; + + + //************************************************************************** + //** loadEvents + //************************************************************************** + var loadEvents = function(){ + + var events = store.getEvents(); + for (var i=0; i=startDate.getTime()){ + me.addEvent(events[i]); + } + } + }; + + + //************************************************************************** + //** updatePosition + //************************************************************************** + /** Function used to move a div from left to right until we find an area + * that doesn't intersect another div. + */ + var updatePosition = function(div, _event, width){ + + //Get bounding rectangle of the div. Subtract 1 pixel for intesection test. + var r1 = _getRect(div); + r1 = { + left: r1.left+1, + right: r1.right-1, + top: r1.top+1, + bottom: r1.bottom-1 + }; + + + //Check whether the div intersect any other divs. Shift div + //to the right until we find a + var divs = getDivs(_event.getStartDate()); + var x = 0; + while (x<=100){ + + var shiftRight = false; + for (var j=0; j0){ + el = el.childNodes[0]; + if (el.tagName.toUpperCase()==="TD"){ + td = el; + break; + } + } + + if (td){ + var left = parseInt(outerDiv.style.left); + if (left===0){ + td.style.paddingLeft = eventPadding+"px"; + } + else{ + td.style.paddingLeft = "0px"; + } + } + }; + + + //************************************************************************** + //** sortEvents + //************************************************************************** + /** Used to sort events by order of appearance in the cell - left to right, + * top to bottom. + */ + var sortEvents = function(events){ + + var _events = []; + for (var i=0; i=30) startRow+=1; + return startRow; + }; + + + events.sort(function(event1, event2){ + var y1 = getRow(event1); + var y2 = getRow(event2); + + var div1, div2; + for (var i=0; i<_events.length; i++){ + if (_events[i].equals(event1)){ + div1 = divs[i]; + } + if (_events[i].equals(event2)){ + div2 = divs[i]; + } + + if (div1!=null && div2!=null) break; + } + var x1 = parseInt(div1.style.left); + var x2 = parseInt(div2.style.left); + + var a = x1 + (y1*1000); + var b = x2 + (y2*1000); + return a-b; + }); + + return events; + }; + + + //************************************************************************** + //** getOverlappingEvents + //************************************************************************** + /** Returns a list of events that overlap a given event. This method ignores + * multi-day events and events that are not in the current view. + */ + var getOverlappingEvents = function(event){ + + var arr = []; + var overlappingEvents = store.getOverlappingEvents(event); + for (var i=0; i=1){ + //TODO: Find multiday event div... + } + else{ + var divs = getDivs(event.getStartDate()); + for (var i=0; idate.getMonth()) td.className+='-next-month'; - - - - - //Update list of cells - var id = (d.getMonth()+1) + "-" + d.getDate() + "-" + d.getFullYear(); - var cell = cells[id]; - if (cell==null){ - cells[id] = [td]; - } - else{ - cells[id].push(td); - } - - - - //Add event listener - td.date = new Date(d); - if (x==1){ - td.onclick = function(e){ - var el = this; - var clickedEvent = false; - var div = el.childNodes[0]; - if (div.childNodes.length>0){ - var firstEvent = _getRect(div.childNodes[0]); - var lastEvent = div.childNodes.length==1 ? - firstEvent : _getRect(div.childNodes[div.childNodes.length-1]); - var y = e.clientY; - if (ylastEvent.bottom){} - else clickedEvent = true; - } - - - if (!clickedEvent){ - var _date = new Date(el.date); - var listener = me.getListener('cellclick'); - if (listener!=null){ - var callback = listener.callback; - var scope = listener.scope; - callback.apply(scope, [_date, el, me, e]); - } - } - }; - } - else{ - td.onclick = function(e){ - var el = this; - var _date = new Date(el.date); - var listener = me.getListener('cellclick'); - if (listener!=null){ - var callback = listener.callback; - var scope = listener.scope; - callback.apply(scope, [_date, el, me, e]); - } - }; - } - - - td.ontouchstart = function(e) { - - //Disable select/highlight behaviour - e.preventDefault(); - - - //Call onclick function - if (touchEnabled){ - var touch = e.touches[0]; - var x = touch.pageX; - var y = touch.pageY; - this.onclick.apply(this, [{ - clientX: x, - clientY: y - }]); - } - }; - - - - if (x==0){ - td.className += " javaxt-cal-cell-header"; - td.appendChild(me.createCellHeader(new Date(d), i, j)); - } - else if (x==1){ - - if (k==0){ - multiDayCols[id] = [td]; - td.style.height = "1px"; - } - else{ - div = document.createElement("div"); - div.style.width="100%"; - div.style.height = "100%"; - div.style.position = "relative"; - td.appendChild(div); - singleDayCols[id] = td; - } - } - else if (x==2){ - td.className += " javaxt-cal-cell-footer"; - td.style.height = "1px"; - td.appendChild(me.createCellFooter(new Date(d), i, j)); - } - - d.setDate(d.getDate()+1); - - - - - //Remove borders as needed. The borders for these - //specific cells should be set by javaxt-cal-body - if (j==0){ - td.style.borderLeft = "0px"; - } - if (j==days.length-1){ - td.style.borderRight = "0px"; - } - if (i==0){ - td.style.borderTop = "0px"; - } - if (i==numWeeks-1){ - td.style.borderBottom = "0px"; - } - - - - tr.appendChild(td); - } - } - } - } - - body.appendChild(tbody.parentNode); - parent.appendChild(table); - - - - - //Call the update callback - if (rendered){ - var listener = me.getListener('update'); - if (listener!=null) listener.callback.apply(listener.scope, [me]); - } - }; - - - //************************************************************************** - //** createColumnHeader - //************************************************************************** - /** Returns a div used to indicate the day of the week. The div is inserted - * into a given column header. This method can be safely overridden to - * generate custom headers. - */ - this.createColumnHeader = function(i){ - var outerDiv = document.createElement('div'); - outerDiv.style.width = "100%"; - outerDiv.style.height = "100%"; - outerDiv.style.position = "relative"; - var innerDiv = document.createElement('div'); - innerDiv.style.width = "100%"; - innerDiv.style.height = "100%"; - innerDiv.style.position = "absolute"; - innerDiv.style.whiteSpace = 'nowrap'; - innerDiv.style.overflow = 'hidden'; - innerDiv.innerHTML = days[i]; - - outerDiv.appendChild(innerDiv); - return outerDiv; - }; - - - //************************************************************************** - //** createCellHeader - //************************************************************************** - /** Returns a div used to indicate the date within an individual cell. This - * method can be safely overridden to generate custom cell headers. - */ - this.createCellHeader = function(date, i, j){ - var text = date.getDate(); - var monthName = javaxt.dhtml.calendar.Utils.monthNames[date.getMonth()].substring(0,3) + " "; - if (i==0 && j==0) text = monthName + text; - else if (text==1) text = monthName + text; - var div = document.createElement("div"); - div.innerHTML = text; - return div; - }; - - - //************************************************************************** - //** createCellFooter - //************************************************************************** - /** Returns an div empty which is enserted into the cell footer. This method - * can be safely overridden to generate custom cell footers. - */ - this.createCellFooter = function(date, i, j){ - var div = document.createElement("div"); - return div; - }; - - - //************************************************************************** - //** getCells - //************************************************************************** - /** Returns an array of cells - one for each day in the view. A cell is - * defined by a date/id and a bounding rectangle. These cells are used - * when dragging events. - */ - this.getCells = function(){ - var arr = []; - for (var id in cells) { - if (cells.hasOwnProperty(id)) { - var cols = cells[id]; - - var r1 = _getRect(cols[0]); - var r2 = _getRect(cols[cols.length-1]); - var x1 = r1.left; - var x2 = r1.right; - var y1 = r1.top; - var y2 = r2.bottom; - - - var rect = { - left: x1, - right: x2, - top: y1, - bottom: y2, - width: x2-x1, - height: y2-y1 - }; - - arr.push({ - id: id, - rect: rect - }); - } - } - return arr; - }; - - - //************************************************************************** - //** getEventStore - //************************************************************************** - this.getEventStore = function(){ - return store; - }; - - - //************************************************************************** - //** addEvent - //************************************************************************** - this.addEvent = function(event){ - - - log("Adding " + event.getSubject() + "..."); - - - //Check if we've already rendered the given event - var numDays = event.numDays(); - if (numDays>=1){ - for (var id in multiDayCols) { - if (multiDayCols.hasOwnProperty(id)) { - var arr = multiDayCols[id]; - if (arr!=null){ - for (var i=1; i=1){ - addMultiDayEvent(event); - } - else{ - addSingleDayEvent(event); - } - - - //Update the event store - store.add(event); - }; - - - - //************************************************************************** - //** removeEvent - //************************************************************************** - - this.removeEvent = function(event){ - - - //Remove the event from the store - store.remove(event); - - - //Update view - if (event.numDays()>=1){ //Multiday Event - - - //Remove div - var rows = []; - for (var id in multiDayCols) { - if (multiDayCols.hasOwnProperty(id)) { - var arr = multiDayCols[id]; - for (var i=1; i=2){ - var str = id.split("-"); - var month = parseInt(str[0]); - var day = parseInt(str[1]); - var year = parseInt(str[2]); - var date = new Date(year, month-1, day); - for (var x=1; x1){ - var td = arr[1]; - if (td.parentNode!=null){ - if (td.parentNode.previousSibling==firstRow){ - var eventDiv = td.firstChild; - if (eventDiv!=null){ - eventDiv.style.marginTop = "0px"; - } - } - } - } - } - } - - } - } - - - - alignEvents(); - - } - else{ //Single day event - - var d = event.getStartDate(); - var id = (d.getMonth()+1) + "-" + d.getDate() + "-" + d.getFullYear(); - var td = singleDayCols[id]; - var div = td.childNodes[0]; - for (var i=0; i=event.getStartDate().getTime()){ - - div.insertBefore(wrapper, div.childNodes[i]); - addedEvent = true; - break; - } - } - } - if (!addedEvent){ - div.appendChild(wrapper); - } - - }; - - - //************************************************************************** - //** addMultiDayEvent - //************************************************************************** - /** Used to render multi-day events - */ - var addMultiDayEvent = function(event){ - - - //Find cells to span - var cols = []; - for (var x=0; x0) spans.push(span); - - - //Iterate through the spans and render events - for (var i=0; i1){ - - for (k=1; k0 || _col.parentNode==null){ - spanInUse = true; - break; - } - - } - - if (!spanInUse){ - col = cols[0][k]; - break; - } - } - } - - - //If a suitable column was not found, add a new row and select a - //column from the new row - if (col==null){ - - //Insert row - var currRow = cols[0][cols[0].length-1].parentNode; - var nextRow = currRow.nextSibling; - var newRow = cols[0][0].parentNode.cloneNode(true); - currRow.parentNode.insertBefore(newRow, nextRow); - - //Update the multiDayCols array - for (var j=0; j0 && col.previousSibling==null; - var continueRight = col.nextSibling==null && (spans.length>1 && i1) wrapper.style.marginTop = (eventSpacing*2) + "px"; //Vertical padding - wrapper.appendChild(outerDiv); - wrapper.event = event; - wrapper.onclick = function(e){ - var listener = me.getListener('eventclick'); - if (listener!=null){ - var callback = listener.callback; - var scope = listener.scope; - callback.apply(scope, [this.event, this, me, e]); - } - }; - - - col.appendChild(wrapper); - col.style.height = eventHeight + "px"; - mutiDayEventDivs.push(wrapper); - } - - - - //Move the event divs up to fill the gap created by multi-events - alignEvents(); - }; - - - //************************************************************************** - //** alignEvents - //************************************************************************** - /** Used to vertically align events to fill any gaps created by multi-events. - * If there are multiday events that span a cell, events are aligned with - * the last multiday event in the cell. Otherwise, events are aligned with - * the cell header - */ - var alignEvents = function(){ - for (var id in singleDayCols) { - if (singleDayCols.hasOwnProperty(id)) { - var td = singleDayCols[id]; - var div = td.childNodes[0]; - var cols = multiDayCols[id]; - var offset = 0; - - - - var numUsedCols = 0; - var lastUsedCol = null; - for (var i=1; i0 || cols[i].parentNode==null){ - numUsedCols++; - lastUsedCol = i; - } - } - - - //The following logic doesn't work for FireFox: - //var offset = (((cols.length-1)-numUsedCols)*(eventHeight)); - //if (numUsedCols==0) offset+=eventSpacing; - //As a workaround, we need to compute offset using the DOM - var tr = td.parentNode; - var y2 = _getRect(tr).top; - var y1; - if (lastUsedCol!=null){ - var lastUsedRow = tr.parentNode.childNodes[lastUsedCol]; - y1 = _getRect(lastUsedRow).bottom; - offset = y2-y1; - } - else{ - y1 = _getRect(tr.parentNode.childNodes[0]).top; - offset = ((y2-y1)+((eventSpacing*2)-1)); //-1px for spacer row? - } - - - div.style.marginTop = -offset + "px"; - } - } - }; - - - //************************************************************************** - //** createTable - //************************************************************************** - var createTable = function(){ - var table = document.createElement('table'); - table.style.width = "100%"; - table.style.height = "100%"; - table.cellSpacing = 0; - table.cellPadding = 0; - table.style.borderCollapse = "collapse"; - var tbody = document.createElement('tbody'); - table.appendChild(tbody); - return tbody; - }; - - - - - - //************************************************************************** - //** getDOM - //************************************************************************** - this.getDOM = function(){ - return table; - }; - - - //************************************************************************** - //** next - //************************************************************************** - this.next = function(){ - date.setMonth(date.getMonth()+1); - computeRange(date); - renderTable(); - loadEvents(); - }; - - - //************************************************************************** - //** back - //************************************************************************** - this.back = function(){ - date.setMonth(date.getMonth()-1); - computeRange(date); - renderTable(); - loadEvents(); - }; - - - //************************************************************************** - //** setDate - //************************************************************************** - this.setDate = function(d){ - if (d==null) d = new Date(); - - - if (date!=null){ - if (d.getFullYear()==date.getFullYear() && d.getMonth()==date.getMonth()){ - date = d; - return; - } - } - - - date = d; - computeRange(date); - renderTable(); - loadEvents(); - }; - - - //************************************************************************** - //** getDate - //************************************************************************** - this.getDate = function(){ - return date; - }; - - - //************************************************************************** - //** computeRange - //************************************************************************** - var computeRange = function(d){ - - //Compute number of rows to render. Credit: - //http://stackoverflow.com/a/2485172 - var year = d.getFullYear(); - var month = d.getMonth()+1; - var firstOfMonth = new Date(year, month-1, 1); - var lastOfMonth = new Date(year, month, 0); - numWeeks = Math.ceil( (firstOfMonth.getDay() + lastOfMonth.getDate()) / 7); - - startDate = new Date(firstOfMonth); - startDate.setDate(startDate.getDate()-firstOfMonth.getDay()); - - endDate = new Date(lastOfMonth); - endDate.setDate(endDate.getDate()+(6-lastOfMonth.getDay())); - }; - - - //************************************************************************** - //** getDateRange - //************************************************************************** - /** Returns the start/end dates represented by this view. */ - - this.getDateRange = function(){ - return { - startDate: new Date(startDate), - endDate: new Date(endDate) - }; - }; - - - //************************************************************************** - //** getTitle - //************************************************************************** - /** Returns a title for the current view. */ - - this.getTitle = function(){ - return (javaxt.dhtml.calendar.Utils.monthNames[date.getMonth()] + " " + date.getFullYear()); - }; - - - //************************************************************************** - //** loadEvents - //************************************************************************** - var loadEvents = function(){ - var events = store.getEvents(); - for (var i=0; idate.getMonth()) addStyle(td, "cellNextMonth"); + else addStyle(td, "cell"); + + + + + //Update list of cells + var id = (d.getMonth()+1) + "-" + d.getDate() + "-" + d.getFullYear(); + var cell = cells[id]; + if (cell==null){ + cells[id] = [td]; + } + else{ + cells[id].push(td); + } + + + + //Add event listener + td.date = new Date(d); + if (x==1){ + td.onclick = function(e){ + var el = this; + var clickedEvent = false; + var div = el.childNodes[0]; + if (div.childNodes.length>0){ + var firstEvent = _getRect(div.childNodes[0]); + var lastEvent = div.childNodes.length==1 ? + firstEvent : _getRect(div.childNodes[div.childNodes.length-1]); + var y = e.clientY; + if (ylastEvent.bottom){} + else clickedEvent = true; + } + + + if (!clickedEvent){ + var _date = new Date(el.date); + var listener = me.getListener('cellclick'); + if (listener!=null){ + var callback = listener.callback; + var scope = listener.scope; + callback.apply(scope, [_date, el, me, e]); + } + } + }; + } + else{ + td.onclick = function(e){ + var el = this; + var _date = new Date(el.date); + var listener = me.getListener('cellclick'); + if (listener!=null){ + var callback = listener.callback; + var scope = listener.scope; + callback.apply(scope, [_date, el, me, e]); + } + }; + } + + + td.ontouchstart = function(e) { + + //Disable select/highlight behaviour + e.preventDefault(); + + + //Call onclick function + if (touchEnabled){ + var touch = e.touches[0]; + var x = touch.pageX; + var y = touch.pageY; + this.onclick.apply(this, [{ + clientX: x, + clientY: y + }]); + } + }; + + + + if (x==0){ + addStyle(td, "cellHeader"); + td.appendChild(me.createCellHeader(new Date(d), i, j)); + } + else if (x==1){ + + if (k==0){ + multiDayCols[id] = [td]; + td.style.height = "1px"; + } + else{ + div = createElement("div", td, { + width: "100%", + height: "100%", + position: "relative" + }); + singleDayCols[id] = td; + } + } + else if (x==2){ + addStyle(td, "cellFooter"); + td.style.height = "1px"; + td.appendChild(me.createCellFooter(new Date(d), i, j)); + } + + d.setDate(d.getDate()+1); + + + + + //Remove borders as needed. The borders for these + //specific cells should be set by javaxt-cal-body + if (j==0){ + td.style.borderLeft = "0px"; + } + if (j==days.length-1){ + td.style.borderRight = "0px"; + } + if (i==0){ + td.style.borderTop = "0px"; + } + if (i==numWeeks-1){ + td.style.borderBottom = "0px"; + } + + + + tr.appendChild(td); + } + } + } + } + + + + + + //Call the update callback + if (rendered){ + var listener = me.getListener('update'); + if (listener!=null) listener.callback.apply(listener.scope, [me]); + } + }; + + + //************************************************************************** + //** createColumnHeader + //************************************************************************** + /** Returns a div used to indicate the day of the week. The div is inserted + * into a given column header. This method can be safely overridden to + * generate custom headers. + */ + this.createColumnHeader = function(i){ + var outerDiv = createElement('div', { + width: "100%", + height: "100%", + position: "relative" + }); + var innerDiv = createElement('div', outerDiv, { + width: "100%", + height: "100%", + position: "absolute", + whiteSpace: 'nowrap', + overflow: 'hidden' + }); + innerDiv.innerHTML = days[i]; + return outerDiv; + }; + + + //************************************************************************** + //** createCellHeader + //************************************************************************** + /** Returns a div used to indicate the date within an individual cell. This + * method can be safely overridden to generate custom cell headers. + */ + this.createCellHeader = function(date, i, j){ + var text = date.getDate(); + var monthName = config.monthNames[date.getMonth()].substring(0,3) + " "; + if (i==0 && j==0) text = monthName + text; + else if (text==1) text = monthName + text; + var div = createElement("div"); + div.innerHTML = text; + return div; + }; + + + //************************************************************************** + //** createCellFooter + //************************************************************************** + /** Returns an div empty which is enserted into the cell footer. This method + * can be safely overridden to generate custom cell footers. + */ + this.createCellFooter = function(date, i, j){ + var div = createElement("div"); + return div; + }; + + + //************************************************************************** + //** getCells + //************************************************************************** + /** Returns an array of cells - one for each day in the view. A cell is + * defined by a date/id and a bounding rectangle. These cells are used + * when dragging events. + */ + this.getCells = function(){ + var arr = []; + for (var id in cells) { + if (cells.hasOwnProperty(id)) { + var cols = cells[id]; + + var r1 = _getRect(cols[0]); + var r2 = _getRect(cols[cols.length-1]); + var x1 = r1.left; + var x2 = r1.right; + var y1 = r1.top; + var y2 = r2.bottom; + + + var rect = { + left: x1, + right: x2, + top: y1, + bottom: y2, + width: x2-x1, + height: y2-y1 + }; + + arr.push({ + id: id, + rect: rect + }); + } + } + return arr; + }; + + + //************************************************************************** + //** getEventStore + //************************************************************************** + this.getEventStore = function(){ + return store; + }; + + + //************************************************************************** + //** addEvent + //************************************************************************** + this.addEvent = function(event){ + + log("Adding " + event.getSubject() + "..."); + + + //Check if we've already rendered the given event + var numDays = event.numDays(); + if (numDays>=1){ + for (var id in multiDayCols) { + if (multiDayCols.hasOwnProperty(id)) { + var arr = multiDayCols[id]; + if (arr!=null){ + for (var i=1; i=1){ + addMultiDayEvent(event); + } + else{ + addSingleDayEvent(event); + } + + + //Update the event store + store.add(event); + }; + + + + //************************************************************************** + //** removeEvent + //************************************************************************** + + this.removeEvent = function(event){ + + + //Remove the event from the store + store.remove(event); + + + //Update view + if (event.numDays()>=1){ //Multiday Event + + + //Remove div + var rows = []; + for (var id in multiDayCols) { + if (multiDayCols.hasOwnProperty(id)) { + var arr = multiDayCols[id]; + for (var i=1; i=2){ + var str = id.split("-"); + var month = parseInt(str[0]); + var day = parseInt(str[1]); + var year = parseInt(str[2]); + var date = new Date(year, month-1, day); + for (var x=1; x1){ + var td = arr[1]; + if (td.parentNode!=null){ + if (td.parentNode.previousSibling==firstRow){ + var eventDiv = td.firstChild; + if (eventDiv!=null){ + eventDiv.style.marginTop = "0px"; + } + } + } + } + } + } + + } + } + + + + alignEvents(); + + } + else{ //Single day event + + var d = event.getStartDate(); + var id = (d.getMonth()+1) + "-" + d.getDate() + "-" + d.getFullYear(); + var td = singleDayCols[id]; + var div = td.childNodes[0]; + for (var i=0; i=event.getStartDate().getTime()){ + + div.insertBefore(wrapper, div.childNodes[i]); + addedEvent = true; + break; + } + } + } + if (!addedEvent){ + div.appendChild(wrapper); + } + + + //Ensure a consistent gap between stacked events + spaceEvents(div); + + }; + + + //************************************************************************** + //** addMultiDayEvent + //************************************************************************** + /** Used to render multi-day events + */ + var addMultiDayEvent = function(event){ + + + //Find cells to span + var cols = []; + for (var x=0; x0) spans.push(span); + + + //Iterate through the spans and render events + for (var i=0; i1){ + + for (k=1; k0 || _col.parentNode==null){ + spanInUse = true; + break; + } + + } + + if (!spanInUse){ + col = cols[0][k]; + break; + } + } + } + + + //If a suitable column was not found, add a new row and select a + //column from the new row + if (col==null){ + + //Insert row + var currRow = cols[0][cols[0].length-1].parentNode; + var nextRow = currRow.nextSibling; + var newRow = cols[0][0].parentNode.cloneNode(true); + currRow.parentNode.insertBefore(newRow, nextRow); + + //Update the multiDayCols array + for (var j=0; j0 && col.previousSibling==null; + var continueRight = col.nextSibling==null && (spans.length>1 && i1) wrapper.style.marginTop = (eventSpacing*2) + "px"; //Vertical padding + wrapper.appendChild(outerDiv); + wrapper.event = event; + wrapper.onclick = function(e){ + var listener = me.getListener('eventclick'); + if (listener!=null){ + var callback = listener.callback; + var scope = listener.scope; + callback.apply(scope, [this.event, this, me, e]); + } + }; + + + col.appendChild(wrapper); + col.style.height = eventHeight + "px"; + mutiDayEventDivs.push(wrapper); + } + + + + //Move the event divs up to fill the gap created by multi-events + alignEvents(); + }; + + + //************************************************************************** + //** alignEvents + //************************************************************************** + /** Used to vertically align events to fill any gaps created by multi-events. + * If there are multiday events that span a cell, events are aligned with + * the last multiday event in the cell. Otherwise, events are aligned with + * the cell header + */ + var alignEvents = function(){ + for (var id in singleDayCols) { + if (singleDayCols.hasOwnProperty(id)) { + var td = singleDayCols[id]; + var div = td.childNodes[0]; + var cols = multiDayCols[id]; + var offset = 0; + + + + var numUsedCols = 0; + var lastUsedCol = null; + for (var i=1; i0 || cols[i].parentNode==null){ + numUsedCols++; + lastUsedCol = i; + } + } + + + //The following logic doesn't work for FireFox: + //var offset = (((cols.length-1)-numUsedCols)*(eventHeight)); + //if (numUsedCols==0) offset+=eventSpacing; + //As a workaround, we need to compute offset using the DOM + var tr = td.parentNode; + var y2 = _getRect(tr).top; + var y1; + if (lastUsedCol!=null){ + var lastUsedRow = tr.parentNode.childNodes[lastUsedCol]; + y1 = _getRect(lastUsedRow).bottom; + offset = y2-y1; + } + else{ + y1 = _getRect(tr.parentNode.childNodes[0]).top; + offset = ((y2-y1)+((eventSpacing*2)-1)); //-1px for spacer row? + } + + + div.style.marginTop = -offset + "px"; + + + //Keep single-day events clear of the multiday bar and each other + spaceEvents(div); + } + } + }; + + + //************************************************************************** + //** spaceEvents + //************************************************************************** + /** Ensures a small, consistent vertical gap between the events stacked in a + * cell. The event divs can render slightly taller than their wrapper (border + * + padding), so aligning to the table rows leaves overlaps. Instead we + * measure the actual geometry with getRect() and nudge each event's + * margin-top so it clears the event above it - including any multiday event + * spanning the cell (which the single-day events must sit below). + */ + var eventSpacer = eventSpacing + 1; //desired gap between events (px) + + var nudgeBelow = function(wrapper, aboveRect){ + var curr = getEventDiv(wrapper); + if (curr==null || aboveRect==null) return; + var gap = _getRect(curr).top - aboveRect.bottom; + if (gap < eventSpacer){ + var margin = parseInt(wrapper.style.marginTop); + if (isNaN(margin)) margin = 0; + wrapper.style.marginTop = (margin + (eventSpacer - gap)) + "px"; + } + }; + + var spaceEvents = function(container){ + if (container==null) return; + + //Collect the single-day event wrappers in render order + var wrappers = []; + for (var i=0; i tdRect.left && mr.left < tdRect.right){ //same column + if (boundary==null || mr.bottom > boundary) boundary = mr.bottom; + } + } + if (boundary!=null) nudgeBelow(wrappers[0], {bottom: boundary}); + + //Walk top-to-bottom, pushing each event down until it clears the one + //above it. Adjusting a wrapper also shifts the ones below it, so we + //re-measure on each iteration. + for (var i=1; i0){ + el = el.childNodes[0]; + if (el.isCalEvent===true) return el; + } + return null; + }; + + + //************************************************************************** + //** getDOM + //************************************************************************** + this.getDOM = function(){ + return table; + }; + + + //************************************************************************** + //** next + //************************************************************************** + this.next = function(){ + date.setMonth(date.getMonth()+1); + computeRange(date); + renderTable(); + loadEvents(); + }; + + + //************************************************************************** + //** back + //************************************************************************** + this.back = function(){ + date.setMonth(date.getMonth()-1); + computeRange(date); + renderTable(); + loadEvents(); + }; + + + //************************************************************************** + //** setDate + //************************************************************************** + this.setDate = function(d){ + if (d==null) d = new Date(); + + + if (date!=null){ + if (d.getFullYear()==date.getFullYear() && d.getMonth()==date.getMonth()){ + date = d; + return; + } + } + + + date = d; + computeRange(date); + renderTable(); + loadEvents(); + }; + + + //************************************************************************** + //** getDate + //************************************************************************** + this.getDate = function(){ + return date; + }; + + + //************************************************************************** + //** computeRange + //************************************************************************** + var computeRange = function(d){ + + //Compute number of rows to render. Credit: + //http://stackoverflow.com/a/2485172 + var year = d.getFullYear(); + var month = d.getMonth()+1; + var firstOfMonth = new Date(year, month-1, 1); + var lastOfMonth = new Date(year, month, 0); + numWeeks = Math.ceil( (firstOfMonth.getDay() + lastOfMonth.getDate()) / 7); + + startDate = new Date(firstOfMonth); + startDate.setDate(startDate.getDate()-firstOfMonth.getDay()); + + endDate = new Date(lastOfMonth); + endDate.setDate(endDate.getDate()+(6-lastOfMonth.getDay())); + }; + + + //************************************************************************** + //** getDateRange + //************************************************************************** + /** Returns the start/end dates represented by this view. + */ + this.getDateRange = function(){ + return { + startDate: new Date(startDate), + endDate: new Date(endDate) + }; + }; + + + //************************************************************************** + //** getTitle + //************************************************************************** + /** Returns a title for the current view. + */ + this.getTitle = function(){ + return (config.monthNames[date.getMonth()] + " " + date.getFullYear()); + }; + + + //************************************************************************** + //** loadEvents + //************************************************************************** + var loadEvents = function(){ + var events = store.getEvents(); + for (var i=0; i10){ - holdActive = false; - return; - } - - - - holdActive = true; - - - - - //Initiate drag - startDrag({ - clientX: x, - clientY: y - }); - - - //Add "touchmove" event listener - if (document.addEventListener) { - div.addEventListener("touchmove", onTouchMove); - } - else if (document.attachEvent) { - div.attachEvent("ontouchmove", onTouchMove); - } - - - }, holdDelay); - }; - - //End touch (similar to "onmouseup") - div.ontouchend = function(e) { - - - //Enable scrolling in the view - view.enableTouch(); - - - //If the mouse is released immediately (i.e., a click), before the - //holdStarter runs, then cancel the holdStarter and do the click - if (holdStarter) { - clearTimeout(holdStarter); - - //run click-only operation here - //console.log("Click!"); - var listener = view.getListener('eventclick'); - if (listener!=null){ - var callback = listener.callback; - var scope = listener.scope; - - var touch = e.changedTouches[0]; - var x = touch.pageX; - var y = touch.pageY; - - callback.apply(scope, [div.event, div, view, { - clientX: x, - clientY: y - }]); - } - } - - //Otherwise, if the mouse was being held, end the hold - else if (holdActive) { - holdActive = false; - moveDiv(div); - - - - //Remove javaxt-cal-event-drag class from the event div - var innerDiv = getInnerDiv(div); - innerDiv.className = innerDiv.className.replace( /(?:^|\s)javaxt-cal-event-drag(?!\S)/g , '' ); - - - //Remove z-index - div.style.zIndex = ''; - - - //Remove "touchmove" event listener - if (document.removeEventListener) { - div.removeEventListener("touchmove", onTouchMove); - } - else if (document.detachEvent) { - div.detachEvent("ontouchmove", onTouchMove); - } - - } - - }; - - - - - - - var onMouseMove = function(e){ - var x = e.clientX; - var y = e.clientY; - div.style.left = (x-div.xOffset) + 'px'; - div.style.top = (y-div.yOffset) + 'px'; - }; - - var onTouchMove = function(e) { - e.preventDefault(); - var touch = e.touches[0]; - var x = touch.pageX; - var y = touch.pageY; - - onMouseMove({ - clientX: x, - clientY: y - }); - }; - - - var startDrag = function(e){ - var x = e.clientX; - var y = e.clientY; - - var rect = _getRect(div); - var top = rect.top-parseInt(div.style.marginTop); - - var xOffset = x-rect.left; - var yOffset = view.hasHours() ? (rect.height/2) : y-top; - - - var parentNode = div.parentNode; - - var orgStyle = { - width: div.style.width, - left: div.style.left, - top: div.style.top, - rect: rect - }; - div.orgStyle = orgStyle; - div.orgParent = parentNode; - div.xOffset = xOffset; - div.yOffset = yOffset; - - - //Disable text selection in the entire document - very important! - var body = document.getElementsByTagName('body')[0]; - if (!body.className.match(/(?:^|\s)javaxt-noselect(?!\S)/) ){ - body.className += (body.className.length==0 ? "" : " ") + "javaxt-noselect"; - } - - - //Remove div from the current cell and append it to the body - var nextSibling = div.nextSibling; - parentNode.removeChild(div); - body.appendChild(div); - - - //Add placeholder div as needed (e.g. month view) - if (view.hasHours()==false && nextSibling!=null){ - var placeHolderDiv = document.createElement("div"); - placeHolderDiv.style.width = "100%"; - placeHolderDiv.style.height = rect.height + "px"; - placeHolderDiv.style.marginTop = div.style.marginTop; - parentNode.insertBefore(placeHolderDiv, nextSibling); - placeHolderDiv.event = div.event; - } - - - - div.style.position = "absolute"; - div.style.width = rect.width + 'px'; - div.style.left = rect.left + 'px'; - div.style.top = (y-yOffset) + 'px'; - div.style.cursor = 'move'; - div.style.zIndex = getNextHighestZindex(); - - - - //Add javaxt-cal-event-drag class to the event div - var innerDiv = getInnerDiv(div); - innerDiv.className += " javaxt-cal-event-drag"; - }; - - - - - /** Used to move an event from cell to cell. */ - var moveDiv = function(div){ - - - //Compute geometry of the div - var rect = _getRect(div); - var minY = rect.top; - - - //Generate list of cells that intersect the four corners of the div - var cells = getCells(rect); - - - //If the div doesn't intersect any cells in the view, return the div - //to its original location. - if (cells.length==0){ - returnDiv(div); - return; - } - - - //Select cell to move to - var cell; - if (cells.length==1){ - cell = cells[0]; - } - else{ - - //Find the cell with the largest area of intersection - var intersections = {}; - var keys = []; - for (var i=0; i0){ - var firstChild = el.childNodes[0]; - - if (firstChild.className.match(/(?:^|\s)javaxt-cal-event(?!\S)/) ){ - return firstChild; - } - else{ - el = firstChild; - } - } - }; - - - /** Returns a list of cells that intersect the four corners of a given rectangle. */ - var getCells = function(rect){ - - var minX = rect.left; - var maxX = rect.right; - var minY = rect.top; - var maxY = rect.bottom; - - //Generate list of cells that intersect the four corners of the div - var cells = []; - var addCell = function(cell){ - if (cell==null) return; - for (var i=0; i=minX && x<=maxX){ - var minY = rect.top; - var maxY = rect.bottom; - if (y>=minY && y<=maxY){ - return cell; - } - } - } - return null; - }; - - - - /** Returns the total area that a given rectangle intersects a cell. */ - var getAreaOfIntersection = function(r1, cell){ - - var rect = cell.rect; - var minX = rect.left; - var maxX = rect.right; - var minY = rect.top; - var maxY = rect.bottom; - - var left = r1.left; - var right = r1.right; - var top = r1.top; - var bottom = r1.bottom; - - if (leftmaxX) right=maxX; - if (topmaxY) bottom=maxY; - - var w = right-left; - var h = bottom-top; - return w*h; - }; - - - - - /** Returns the time represented by a given y coordinate in the view. - * The returned value is a decimal (e.g. 9.5 representing 9:30 AM). */ - var getTime = function(y, div){ - - - var rows = []; - var parentNode = div.orgParent; - for (var i=0; i=minY && y<=maxY){ - return (i/2); - } - - } - return null; - }; - - - - /** Used to update a given start and end date with a given time. The time - * variable must be a decimal value (e.g. 9.5 representing 9:30 AM). */ - var setTime = function(time, startDate, endDate){ - var diff = time - (startDate.getHours() + (startDate.getMinutes()/60)); - var h = Math.floor(diff); - var m = Math.abs((diff % 1)*60); - - startDate.setHours(startDate.getHours()+h); - startDate.setMinutes(startDate.getMinutes()+m); - - endDate.setHours(endDate.getHours()+h); - endDate.setMinutes(endDate.getMinutes()+m); - }; - - - - var _getRect = javaxt.dhtml.calendar.Utils.getRect; - - - - - var getNextHighestZindex = function(obj){ - var highestIndex = 0; - var currentIndex = 0; - var elArray = Array(); - if(obj){elArray = obj.getElementsByTagName('*');}else{elArray = document.getElementsByTagName('*');} - for(var i=0; i < elArray.length; i++){ - if (elArray[i].currentStyle){ - currentIndex = parseFloat(elArray[i].currentStyle['zIndex']); - }else if(window.getComputedStyle){ - currentIndex = parseFloat(document.defaultView.getComputedStyle(elArray[i],null).getPropertyValue('z-index')); - } - if(!isNaN(currentIndex) && currentIndex > highestIndex){highestIndex = currentIndex;} - } - return(highestIndex+1); - }; - - } +if(!javaxt) var javaxt={}; +if(!javaxt.dhtml) javaxt.dhtml={}; +if(!javaxt.dhtml.calendar) javaxt.dhtml.calendar={}; +javaxt.dhtml.calendar.Utils = { + + + //************************************************************************** + //** getDaysBetween + //************************************************************************** + /** Returns the number of days between 2 dates. Returns a decimal value. + */ + getDaysBetween : function(startDate, endDate){ + + function treatAsUTC(date) { + var result = new Date(date); + result.setMinutes(result.getMinutes() - result.getTimezoneOffset()); + return result; + } + + var millisecondsPerDay = 24 * 60 * 60 * 1000; + return (treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay; + }, + + + //************************************************************************** + //** getStyle + //************************************************************************** + /** Used to get or create a style object for a given event + */ + getStyle: function(event, config){ + var style = event.getStyle(); + if (!style) style = {}; + if (!style.event) style.event = config.style.event; + if (!style.eventContinueLeft) style.eventContinueLeft = config.style.eventContinueLeft; + if (!style.eventContinueRight) style.eventContinueRight = config.style.eventContinueRight; + return style; + }, + + + //************************************************************************** + //** addColSpan + //************************************************************************** + /** Used to add a colspan to a given column. + */ + addColSpan : function(td, newSpan){ + if (newSpan<2) return; + + //Add colspan + td.colSpan = newSpan; + + + + var deleteCells = function(startCol, numCells){ + if (startCol==null) return; + var td = startCol; + var tr = td.parentNode; + var x = 0; + var del = []; + while (x10){ + holdActive = false; + return; + } + + + + holdActive = true; + + + + + //Initiate drag + startDrag({ + clientX: x, + clientY: y + }); + + + //Add "touchmove" event listener + if (document.addEventListener) { + div.addEventListener("touchmove", onTouchMove); + } + else if (document.attachEvent) { + div.attachEvent("ontouchmove", onTouchMove); + } + + + }, holdDelay); + }; + + //End touch (similar to "onmouseup") + div.ontouchend = function(e) { + + + //Enable scrolling in the view + view.enableTouch(); + + + //If the mouse is released immediately (i.e., a click), before the + //holdStarter runs, then cancel the holdStarter and do the click + if (holdStarter) { + clearTimeout(holdStarter); + + //run click-only operation here + //console.log("Click!"); + var listener = view.getListener('eventclick'); + if (listener!=null){ + var callback = listener.callback; + var scope = listener.scope; + + var touch = e.changedTouches[0]; + var x = touch.pageX; + var y = touch.pageY; + + callback.apply(scope, [div.event, div, view, { + clientX: x, + clientY: y + }]); + } + } + + //Otherwise, if the mouse was being held, end the hold + else if (holdActive) { + holdActive = false; + moveDiv(div); + + + + //Remove the "drag" style from the event div + var innerDiv = getInnerDiv(div); + removeStyle(innerDiv, dragStyle); + innerDiv.style.cursor = "pointer"; + + + //Remove z-index + div.style.zIndex = ''; + + + //Remove "touchmove" event listener + if (document.removeEventListener) { + div.removeEventListener("touchmove", onTouchMove); + } + else if (document.detachEvent) { + div.detachEvent("ontouchmove", onTouchMove); + } + + } + + }; + + + + + + + var onMouseMove = function(e){ + var x = e.clientX; + var y = e.clientY; + div.style.left = (x-div.xOffset-div.scopeLeft) + 'px'; + div.style.top = (y-div.yOffset-div.scopeTop) + 'px'; + }; + + var onTouchMove = function(e) { + e.preventDefault(); + var touch = e.touches[0]; + var x = touch.pageX; + var y = touch.pageY; + + onMouseMove({ + clientX: x, + clientY: y + }); + }; + + + var startDrag = function(e){ + var x = e.clientX; + var y = e.clientY; + + var rect = _getRect(div); + var top = rect.top-parseInt(div.style.marginTop); + + var xOffset = x-rect.left; + var yOffset = view.hasHours() ? (rect.height/2) : y-top; + + + var parentNode = div.parentNode; + + var orgStyle = { + width: div.style.width, + left: div.style.left, + top: div.style.top, + rect: rect + }; + div.orgStyle = orgStyle; + div.orgParent = parentNode; + div.xOffset = xOffset; + div.yOffset = yOffset; + + + //Find the calendar container to drag within. Dragging within the + //container (instead of document.body) keeps any scoped styles (e.g. + //".javaxt-calendar .javaxt-cal-event") applied to the event while it + //is being dragged. Positions below are made relative to this element. + var scope = parentNode; + while (scope && !(scope.className && (""+scope.className).indexOf("javaxt-calendar")>-1)){ + scope = scope.parentNode; + } + var scopeRect = scope ? _getRect(scope) : {left: 0, top: 0}; + if (!scope) scope = document.getElementsByTagName('body')[0]; + div.scopeLeft = scopeRect.left; + div.scopeTop = scopeRect.top; + + + //Disable text selection in the entire document - very important! + var body = document.getElementsByTagName('body')[0]; + if (!body.className.match(/(?:^|\s)javaxt-noselect(?!\S)/) ){ + body.className += (body.className.length==0 ? "" : " ") + "javaxt-noselect"; + } + + + //Remove div from the current cell and append it to the calendar scope + var nextSibling = div.nextSibling; + parentNode.removeChild(div); + scope.appendChild(div); + + + //Add placeholder div as needed (e.g. month view) + if (view.hasHours()==false && nextSibling!=null){ + var placeHolderDiv = createElement("div", { + width: "100%", + height: rect.height + "px", + marginTop: div.style.marginTop + }); + parentNode.insertBefore(placeHolderDiv, nextSibling); + placeHolderDiv.event = div.event; + } + + + + div.style.position = "absolute"; + div.style.width = rect.width + 'px'; + div.style.left = (rect.left-div.scopeLeft) + 'px'; + div.style.top = (y-yOffset-div.scopeTop) + 'px'; + div.style.cursor = 'move'; + div.style.zIndex = getNextHighestZindex(); + + + + //Apply the "drag" style to the event div + var innerDiv = getInnerDiv(div); + addStyle(innerDiv, dragStyle); + }; + + + + + /** Used to move an event from cell to cell. */ + var moveDiv = function(div){ + + + //Compute geometry of the div + var rect = _getRect(div); + var minY = rect.top; + + + //Generate list of cells that intersect the four corners of the div + var cells = getCells(rect); + + + //If the div doesn't intersect any cells in the view, return the div + //to its original location. + if (cells.length==0){ + returnDiv(div); + return; + } + + + //Select cell to move to + var cell; + if (cells.length==1){ + cell = cells[0]; + } + else{ + + //Find the cell with the largest area of intersection + var intersections = {}; + var keys = []; + for (var i=0; i0){ + var firstChild = el.childNodes[0]; + + if (firstChild.isCalEvent===true){ + return firstChild; + } + else{ + el = firstChild; + } + } + }; + + + /** Returns a list of cells that intersect the four corners of a given rectangle. */ + var getCells = function(rect){ + + var minX = rect.left; + var maxX = rect.right; + var minY = rect.top; + var maxY = rect.bottom; + + //Generate list of cells that intersect the four corners of the div + var cells = []; + var addCell = function(cell){ + if (cell==null) return; + for (var i=0; i=minX && x<=maxX){ + var minY = rect.top; + var maxY = rect.bottom; + if (y>=minY && y<=maxY){ + return cell; + } + } + } + return null; + }; + + + + + + /** Returns the time represented by a given y coordinate in the view. + * The returned value is a decimal (e.g. 9.5 representing 9:30 AM). */ + var getTime = function(y, div){ + + + var rows = []; + var parentNode = div.orgParent; + for (var i=0; i=minY && y<=maxY){ + return (i/2); + } + + } + return null; + }; + + + + /** Used to update a given start and end date with a given time. The time + * variable must be a decimal value (e.g. 9.5 representing 9:30 AM). */ + var setTime = function(time, startDate, endDate){ + var diff = time - (startDate.getHours() + (startDate.getMinutes()/60)); + var h = Math.floor(diff); + var m = Math.abs((diff % 1)*60); + + startDate.setHours(startDate.getHours()+h); + startDate.setMinutes(startDate.getMinutes()+m); + + endDate.setHours(endDate.getHours()+h); + endDate.setMinutes(endDate.getMinutes()+m); + }; + + + + var _getRect = javaxt.dhtml.utils.getRect; + var getNextHighestZindex = javaxt.dhtml.utils.getNextHighestZindex; + var getAreaOfIntersection = javaxt.dhtml.utils.getAreaOfIntersection; + + } }; \ No newline at end of file diff --git a/src/callout/Callout.js b/src/callout/Callout.js index 3fd5aeb..54081d8 100644 --- a/src/callout/Callout.js +++ b/src/callout/Callout.js @@ -1,531 +1,531 @@ -if(!javaxt) var javaxt={}; -if(!javaxt.dhtml) javaxt.dhtml={}; - - -//****************************************************************************** -//** Callout Class -//****************************************************************************** -/** - * Used to create simple tooltip/popup boxes with an arrow. - * - ******************************************************************************/ - -javaxt.dhtml.Callout = function(parent, config) { - this.className = "javaxt.dhtml.Callout"; - - var me = this; - var div, innerDiv, callout, notch, notchBorder; - var opening = false; - - var defaultConfig = { - - position: "absolute", - - - /** Style for individual elements within the component. Note that you can - * provide CSS class names instead of individual style definitions. - */ - style: { - - panel: { - border: "1px solid #c5d9e8", - backgroundColor: "#eef4f9", - borderRadius: "6px", - boxShadow: "0 12px 14px 0 rgba(0, 0, 0, 0.2), 0 13px 20px 0 rgba(0, 0, 0, 0.2)" - }, - - arrow: { - - //Only backgroundColor, borderColor, width, height, and padding - //are considered. All other properties are ignored. - borderColor: "#c5d9e8", - backgroundColor: "#eef4f9", - width: "10px", - height: "10px", - padding: "10px" - } - - } - }; - - - //************************************************************************** - //** Constructor - //************************************************************************** - var init = function(){ - - //Clone the config so we don't modify the original config object - var clone = {}; - merge(clone, config); - - - //Merge clone with default config - merge(clone, defaultConfig); - config = clone; - - - - - //Create outer div - div = createElement("div", parent); - div.setAttribute("desc", me.className); - if (config.position==="absolute"){ - div.style.display = "none"; - div.style.position = "absolute"; - div.style.top = div.style.left = 0; - } - me.el = div; - - - //Create callout box - callout = createElement("div", div, config.style.panel); - callout.style.position = "relative"; - callout.style.margin = 0; - callout.style.padding = 0; - callout.style.borderWidth = "1px"; //notch assumes the border is 1px. See showAt() method... - - - - //Create content div - innerDiv = createElement("div", callout, { - width: "100%", - height: "100%" - }); - - - - //Create temporary div to get arrow style - var temp = createElement("div", config.style.arrow); - temp.style.position = "absolute"; - temp.style.visibility = 'hidden'; - temp.style.display = 'block'; - var body = document.getElementsByTagName("body")[0]; - body.appendChild(temp); - var style = temp.currentStyle || window.getComputedStyle(temp); - var getStyle = function(prop){ - - var _getStyle = function(prop){ - if (style.getPropertyValue){ - var val = style.getPropertyValue(prop); - if (val && val.length>0) return val; - prop = prop.replace( /([a-z])([A-Z])/g, '$1-$2' ).toLowerCase(); - return style.getPropertyValue(prop); - } - else{ - return style[prop]; - } - }; - - if (prop instanceof Array){ - var arr = prop; - for (var i=0; i0){ - return val; - } - } - } - else{ - return _getStyle(prop); - } - - }; - - config.arrow = { - backgroundColor: getStyle("backgroundColor"), - borderColor: getStyle(["borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor"]), - paddingTop: parseInt(getStyle("paddingTop")), - paddingBottom: parseInt(getStyle("paddingBottom")), - paddingLeft: parseInt(getStyle("paddingLeft")), - paddingRight: parseInt(getStyle("paddingRight")) - }; - temp.style.border = 0; - temp.style.padding = 0; - temp.style.margin = 0; - config.arrow.width = temp.offsetWidth; - config.arrow.height = temp.offsetHeight; - body.removeChild(temp); - temp = null; - - - - //Create notch (triangle) - notch = createElement("b"); - notch.setAttribute("desc","notch"); - notch.style.position = "absolute"; - notch.style.top=0; - notch.style.left=0; - notch.style.margin=0; - notch.style.padding=0; - notch.style.width=0; - notch.style.height=0; - notch.style.fontSize=0; - notch.style.lineHeight=0; - - - - //Create border for the notch - notchBorder = createElement("b", div); - notchBorder.setAttribute("desc","notchBorder"); - notchBorder.style.position="absolute"; - notchBorder.style.top=0; - notchBorder.style.left=0; - notchBorder.style.margin=0; - notchBorder.style.padding=0; - notchBorder.style.width=0; - notchBorder.style.height=0; - notchBorder.style.fontSize=0; - notchBorder.style.lineHeight=0; - - - // ie6 transparent fix - //_border-right-color: pink; - //_border-left-color: pink; - //_filter: chroma(color=pink); - - - - div.appendChild(notch); - - - - //Add event listeners - if (config.position==="absolute"){ - - var onresize = function(){ - me.hide(); - }; - - var onclick = function(e){ - var x = e.clientX; - var y = e.clientY; - hideIfOutside(x, y); - }; - - var ontouchstart = function(e){ - var x = e.changedTouches[0].pageX; - var y = e.changedTouches[0].pageY; - hideIfOutside(x, y); - }; - - - if (document.addEventListener) { // For all major browsers, except IE 8 and earlier - document.addEventListener("click", onclick); - document.addEventListener("touchstart", ontouchstart); - window.addEventListener("resize", onresize); - } - else if (document.attachEvent) { // For IE 8 and earlier versions - document.attachEvent("onclick", onclick); - document.attachEvent("ontouchstart", ontouchstart); - window.attachEvent("onresize", onresize); - } - } - }; - - - //************************************************************************** - //** hideIfOutside - //************************************************************************** - var hideIfOutside = function(x, y){ - - if (opening) return; - - if (div.style.display === 'block'){ - - var x1 = parseInt(div.style.left); - var x2 = x1+div.offsetWidth; - var y1 = parseInt(div.style.top); - var y2 = y1+div.offsetHeight; - - if (xx2){ - me.hide(); - } - else{ - if (yy2){ - me.hide(); - } - } - } - }; - - - //************************************************************************** - //** getInnerDiv - //************************************************************************** - /** Returns the content div inside the callout that can be populated with - * text, html, menu buttons, etc. - */ - this.getInnerDiv = function(){ - return innerDiv; - }; - - - //************************************************************************** - //** getSize - //************************************************************************** - /** Returns the width and height of the callout. - */ - this.getSize = function(){ - var size; - - if (div.style.display === 'none'){ - div.style.visibility = 'hidden'; - div.style.display = 'block'; - size = { - width: div.offsetWidth, - height: div.offsetHeight - }; - div.style.visibility = ''; - div.style.display = 'none'; - } - else{ - size = { - width: div.offsetWidth, - height: div.offsetHeight - }; - } - return size; - }; - - - //************************************************************************** - //** show - //************************************************************************** - /** Used to render the callout. - */ - this.show = function(){ - opening = true; - - div.style.zIndex = getNextHighestZindex(); - div.style.display = 'block'; - me.onShow(); - - setTimeout(function() { - opening = false; - }, 500); - }; - - - //************************************************************************** - //** showAt - //************************************************************************** - /** Used to render the callout at a specific coordinate. The tip of the - * arrow associated with the callout will appear at the given coordinate. - * - * @param position Where to place the callout box relative to the given - * coordinate. Options include left, right, above, and below. - * - * @param align Options include left, right, center if the "position" is - * above or below. Otherwise, options are top, bottom, or middle. - */ - this.showAt = function(x, y, position, align){ - opening = true; - - - //Hack to get div width/height BEFORE making the div visible - div.style.visibility = 'hidden'; - div.style.display = 'block'; - - - var backgroundColor = config.arrow.backgroundColor; - var borderColor = config.arrow.borderColor; - - - var notchSize = Math.max(config.arrow.width, config.arrow.height); - var notchOffset = 0; - var notchCenter = notchSize; - var notchHeight = notchSize; - - - - var halign = function(){ - if (align==="left"){ - notchOffset = config.arrow.paddingLeft; - div.style.left = (x-(notchOffset+notchCenter)) + "px"; - notch.style.left=notchBorder.style.left=notchOffset + "px"; - } - else if (align==="right"){ - notchOffset = config.arrow.paddingRight; - div.style.left = ((x-div.offsetWidth) + (notchOffset+notchCenter)) + "px"; - notch.style.left=notchBorder.style.left= (div.offsetWidth-(notchOffset+(notchCenter*2))) + "px"; - } - else if (align==="center" || align==="middle"){ - var center = div.offsetWidth/2; - div.style.left = (x-center) + "px"; - notch.style.left=notchBorder.style.left= (center-notchCenter) + "px"; - } - else{ - return; - } - }; - - - var valign = function(){ - callout.style.top = "0px"; - - if (align==="top"){ - notchOffset = config.arrow.paddingTop; - div.style.top = (y-(notchOffset+notchCenter)) + "px"; - notch.style.top=notchBorder.style.top=notchOffset + "px"; - } - else if (align==="bottom"){ - notchOffset = config.arrow.paddingBottom; - div.style.top = ((y-div.offsetWidth) + (notchOffset+notchCenter)) + "px"; - notch.style.top = notchBorder.style.top = (div.offsetHeight-(notchOffset+(notchCenter*2))) + "px"; - } - else if (align==="middle" || align==="center"){ - var center = div.offsetHeight/2; - div.style.top = (y-center) + "px"; - notch.style.top = notchBorder.style.top = (center-notchCenter) + "px"; - } - else{ - return; - } - }; - - - //Update notch style align elements. Notch style is based on a CSS triangle - //described here: https://css-tricks.com/snippets/css/css-triangle/ - if (position==="above"){ - - //Update notch style so the arrow is pointing down - notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid " + backgroundColor; - notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid transparent"; - notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid transparent"; - notch.style.borderBottom=notchBorder.style.borderBottom=0; - notchBorder.style.borderTopColor=borderColor; //<--Make sure this appears after all other border definitions! - - - //Set vertical position of the notch, div, and callout - div.style.left = x + "px"; - div.style.top = ((y-div.offsetHeight)-notchHeight) + "px"; - callout.style.top = "0px"; - notch.style.top = (div.offsetHeight-1) + "px"; //-1 for the border width - notchBorder.style.top = div.offsetHeight + "px"; - - - //Set horizontal alignment of the notch and div - halign(); - } - else if (position==="below"){ - - //Update notch style so the arrow is pointing up - notch.style.borderTop=notchBorder.style.borderTop=0; - notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid transparent"; - notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid transparent"; - notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid " + backgroundColor; - notchBorder.style.borderBottomColor=borderColor; //<--Make sure this appears after all other border definitions! - - - //Set vertical position of the notch, div, and callout - div.style.left = x + "px"; - div.style.top = y + "px"; - callout.style.top = notchHeight + "px"; - notch.style.top = "1px"; //+1 for border width - notchBorder.style.top = "0px"; - - - //Set horizontal position of the notch and div - halign(); - } - else if (position==="left"){ - - //Update notch style so the arrow is pointing right - notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid transparent"; - notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid " + backgroundColor; - notch.style.borderRight=notchBorder.style.borderRight=0; - notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid transparent"; - notchBorder.style.borderLeftColor=borderColor; //<--Make sure this appears after all other border definitions! - - - //Set horizontal position - div.style.left = (x-(div.offsetWidth+notchHeight)) + "px"; - callout.style.left = "0px"; - notch.style.left = (div.offsetWidth-1) + "px"; - notchBorder.style.left = div.offsetWidth + "px"; - - - //Set vertical position of the notch and div - valign(); - } - else if (position==="right"){ - - - //Update notch style so the arrow is pointing left - notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid transparent"; - notch.style.borderLeft=notchBorder.style.borderLeft=0; - notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid " + backgroundColor; - notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid transparent"; - notchBorder.style.borderRightColor=borderColor; //<--Make sure this appears after all other border definitions! - - - //Set horizontal position - div.style.left = x + "px"; - callout.style.left = notchHeight + "px"; - notch.style.left = "1px"; - notchBorder.style.left = "0px"; - - - //Set vertical position of the notch and div - valign(); - } - else{ - return; - } - - - div.style.visibility = ""; - - me.show(); - }; - - - //************************************************************************** - //** hide - //************************************************************************** - /** Used to hide the callout. - */ - this.hide = function(){ - div.style.display = 'none'; - opening = false; - me.onHide(); - }; - - - //************************************************************************** - //** isVisible - //************************************************************************** - /** Returns true of the callout is visible. - */ - this.isVisible = function(){ - return (div.style.display !== 'none'); - }; - - - //************************************************************************** - //** onShow - //************************************************************************** - /** Called whenever the callout is made visible. - */ - this.onShow = function(){}; - - - //************************************************************************** - //** onHide - //************************************************************************** - /** Called whenever the callout is hidden. - */ - this.onHide = function(){}; - - - - //************************************************************************** - //** Utils - //************************************************************************** - var merge = javaxt.dhtml.utils.merge; - var getNextHighestZindex = javaxt.dhtml.utils.getNextHighestZindex; - var createElement = javaxt.dhtml.utils.createElement; - - - init(); +if(!javaxt) var javaxt={}; +if(!javaxt.dhtml) javaxt.dhtml={}; + + +//****************************************************************************** +//** Callout Class +//****************************************************************************** +/** + * Used to create simple tooltip/popup boxes with an arrow. + * + ******************************************************************************/ + +javaxt.dhtml.Callout = function(parent, config) { + this.className = "javaxt.dhtml.Callout"; + + var me = this; + var div, innerDiv, callout, notch, notchBorder; + var opening = false; + + var defaultConfig = { + + position: "absolute", + + + /** Style for individual elements within the component. Note that you can + * provide CSS class names instead of individual style definitions. + */ + style: { + + panel: { + border: "1px solid #c5d9e8", + backgroundColor: "#eef4f9", + borderRadius: "6px", + boxShadow: "0 12px 14px 0 rgba(0, 0, 0, 0.2), 0 13px 20px 0 rgba(0, 0, 0, 0.2)" + }, + + arrow: { + + //Only backgroundColor, borderColor, width, height, and padding + //are considered. All other properties are ignored. + borderColor: "#c5d9e8", + backgroundColor: "#eef4f9", + width: "10px", + height: "10px", + padding: "10px" + } + + } + }; + + + //************************************************************************** + //** Constructor + //************************************************************************** + var init = function(){ + + //Clone the config so we don't modify the original config object + var clone = {}; + merge(clone, config); + + + //Merge clone with default config + merge(clone, defaultConfig); + config = clone; + + + + + //Create outer div + div = createElement("div", parent, "javaxt-callout"); + if (config.position==="absolute"){ + div.style.display = "none"; + div.style.position = "absolute"; + div.style.top = div.style.left = 0; + } + me.el = div; + + + //Create callout box + callout = createElement("div", div, config.style.panel); + callout.style.position = "relative"; + callout.style.margin = 0; + callout.style.padding = 0; + callout.style.borderWidth = "1px"; //notch assumes the border is 1px. See showAt() method... + + + + //Create content div + innerDiv = createElement("div", callout, { + width: "100%", + height: "100%" + }); + + + + //Create temporary div to get arrow style + var temp = createElement("div", "javaxt-callout"); + temp.style.position = "absolute"; + temp.style.visibility = 'hidden'; + temp.style.display = 'block'; + var body = document.getElementsByTagName("body")[0]; + body.appendChild(temp); + temp = createElement("div", temp, config.style.arrow); + var style = temp.currentStyle || window.getComputedStyle(temp); + var getStyle = function(prop){ + + var _getStyle = function(prop){ + if (style.getPropertyValue){ + var val = style.getPropertyValue(prop); + if (val && val.length>0) return val; + prop = prop.replace( /([a-z])([A-Z])/g, '$1-$2' ).toLowerCase(); + return style.getPropertyValue(prop); + } + else{ + return style[prop]; + } + }; + + if (prop instanceof Array){ + var arr = prop; + for (var i=0; i0){ + return val; + } + } + } + else{ + return _getStyle(prop); + } + + }; + + config.arrow = { + backgroundColor: getStyle("backgroundColor"), + borderColor: getStyle(["borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor"]), + paddingTop: parseInt(getStyle("paddingTop")), + paddingBottom: parseInt(getStyle("paddingBottom")), + paddingLeft: parseInt(getStyle("paddingLeft")), + paddingRight: parseInt(getStyle("paddingRight")) + }; + temp.style.border = 0; + temp.style.padding = 0; + temp.style.margin = 0; + config.arrow.width = temp.offsetWidth; + config.arrow.height = temp.offsetHeight; + body.removeChild(temp.parentNode); + temp = null; + + + + //Create notch (triangle) + notch = createElement("b"); + notch.setAttribute("desc","notch"); + notch.style.position = "absolute"; + notch.style.top=0; + notch.style.left=0; + notch.style.margin=0; + notch.style.padding=0; + notch.style.width=0; + notch.style.height=0; + notch.style.fontSize=0; + notch.style.lineHeight=0; + + + + //Create border for the notch + notchBorder = createElement("b", div); + notchBorder.setAttribute("desc","notchBorder"); + notchBorder.style.position="absolute"; + notchBorder.style.top=0; + notchBorder.style.left=0; + notchBorder.style.margin=0; + notchBorder.style.padding=0; + notchBorder.style.width=0; + notchBorder.style.height=0; + notchBorder.style.fontSize=0; + notchBorder.style.lineHeight=0; + + + // ie6 transparent fix + //_border-right-color: pink; + //_border-left-color: pink; + //_filter: chroma(color=pink); + + + + div.appendChild(notch); + + + + //Add event listeners + if (config.position==="absolute"){ + + var onresize = function(){ + me.hide(); + }; + + var onclick = function(e){ + var x = e.clientX; + var y = e.clientY; + hideIfOutside(x, y); + }; + + var ontouchstart = function(e){ + var x = e.changedTouches[0].pageX; + var y = e.changedTouches[0].pageY; + hideIfOutside(x, y); + }; + + + if (document.addEventListener) { // For all major browsers, except IE 8 and earlier + document.addEventListener("click", onclick); + document.addEventListener("touchstart", ontouchstart); + window.addEventListener("resize", onresize); + } + else if (document.attachEvent) { // For IE 8 and earlier versions + document.attachEvent("onclick", onclick); + document.attachEvent("ontouchstart", ontouchstart); + window.attachEvent("onresize", onresize); + } + } + }; + + + //************************************************************************** + //** hideIfOutside + //************************************************************************** + var hideIfOutside = function(x, y){ + + if (opening) return; + + if (div.style.display === 'block'){ + + var x1 = parseInt(div.style.left); + var x2 = x1+div.offsetWidth; + var y1 = parseInt(div.style.top); + var y2 = y1+div.offsetHeight; + + if (xx2){ + me.hide(); + } + else{ + if (yy2){ + me.hide(); + } + } + } + }; + + + //************************************************************************** + //** getInnerDiv + //************************************************************************** + /** Returns the content div inside the callout that can be populated with + * text, html, menu buttons, etc. + */ + this.getInnerDiv = function(){ + return innerDiv; + }; + + + //************************************************************************** + //** getSize + //************************************************************************** + /** Returns the width and height of the callout. + */ + this.getSize = function(){ + var size; + + if (div.style.display === 'none'){ + div.style.visibility = 'hidden'; + div.style.display = 'block'; + size = { + width: div.offsetWidth, + height: div.offsetHeight + }; + div.style.visibility = ''; + div.style.display = 'none'; + } + else{ + size = { + width: div.offsetWidth, + height: div.offsetHeight + }; + } + return size; + }; + + + //************************************************************************** + //** show + //************************************************************************** + /** Used to render the callout. + */ + this.show = function(){ + opening = true; + + div.style.zIndex = getNextHighestZindex(); + div.style.display = 'block'; + me.onShow(); + + setTimeout(function() { + opening = false; + }, 500); + }; + + + //************************************************************************** + //** showAt + //************************************************************************** + /** Used to render the callout at a specific coordinate. The tip of the + * arrow associated with the callout will appear at the given coordinate. + * + * @param position Where to place the callout box relative to the given + * coordinate. Options include left, right, above, and below. + * + * @param align Options include left, right, center if the "position" is + * above or below. Otherwise, options are top, bottom, or middle. + */ + this.showAt = function(x, y, position, align){ + opening = true; + + + //Hack to get div width/height BEFORE making the div visible + div.style.visibility = 'hidden'; + div.style.display = 'block'; + + + var backgroundColor = config.arrow.backgroundColor; + var borderColor = config.arrow.borderColor; + + + var notchSize = Math.max(config.arrow.width, config.arrow.height); + var notchOffset = 0; + var notchCenter = notchSize; + var notchHeight = notchSize; + + + + var halign = function(){ + if (align==="left"){ + notchOffset = config.arrow.paddingLeft; + div.style.left = (x-(notchOffset+notchCenter)) + "px"; + notch.style.left=notchBorder.style.left=notchOffset + "px"; + } + else if (align==="right"){ + notchOffset = config.arrow.paddingRight; + div.style.left = ((x-div.offsetWidth) + (notchOffset+notchCenter)) + "px"; + notch.style.left=notchBorder.style.left= (div.offsetWidth-(notchOffset+(notchCenter*2))) + "px"; + } + else if (align==="center" || align==="middle"){ + var center = div.offsetWidth/2; + div.style.left = (x-center) + "px"; + notch.style.left=notchBorder.style.left= (center-notchCenter) + "px"; + } + else{ + return; + } + }; + + + var valign = function(){ + callout.style.top = "0px"; + + if (align==="top"){ + notchOffset = config.arrow.paddingTop; + div.style.top = (y-(notchOffset+notchCenter)) + "px"; + notch.style.top=notchBorder.style.top=notchOffset + "px"; + } + else if (align==="bottom"){ + notchOffset = config.arrow.paddingBottom; + div.style.top = ((y-div.offsetWidth) + (notchOffset+notchCenter)) + "px"; + notch.style.top = notchBorder.style.top = (div.offsetHeight-(notchOffset+(notchCenter*2))) + "px"; + } + else if (align==="middle" || align==="center"){ + var center = div.offsetHeight/2; + div.style.top = (y-center) + "px"; + notch.style.top = notchBorder.style.top = (center-notchCenter) + "px"; + } + else{ + return; + } + }; + + + //Update notch style align elements. Notch style is based on a CSS triangle + //described here: https://css-tricks.com/snippets/css/css-triangle/ + if (position==="above"){ + + //Update notch style so the arrow is pointing down + notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid " + backgroundColor; + notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid transparent"; + notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid transparent"; + notch.style.borderBottom=notchBorder.style.borderBottom=0; + notchBorder.style.borderTopColor=borderColor; //<--Make sure this appears after all other border definitions! + + + //Set vertical position of the notch, div, and callout + div.style.left = x + "px"; + div.style.top = ((y-div.offsetHeight)-notchHeight) + "px"; + callout.style.top = "0px"; + notch.style.top = (div.offsetHeight-1) + "px"; //-1 for the border width + notchBorder.style.top = div.offsetHeight + "px"; + + + //Set horizontal alignment of the notch and div + halign(); + } + else if (position==="below"){ + + //Update notch style so the arrow is pointing up + notch.style.borderTop=notchBorder.style.borderTop=0; + notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid transparent"; + notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid transparent"; + notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid " + backgroundColor; + notchBorder.style.borderBottomColor=borderColor; //<--Make sure this appears after all other border definitions! + + + //Set vertical position of the notch, div, and callout + div.style.left = x + "px"; + div.style.top = y + "px"; + callout.style.top = notchHeight + "px"; + notch.style.top = "1px"; //+1 for border width + notchBorder.style.top = "0px"; + + + //Set horizontal position of the notch and div + halign(); + } + else if (position==="left"){ + + //Update notch style so the arrow is pointing right + notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid transparent"; + notch.style.borderLeft=notchBorder.style.borderLeft=notchSize+"px solid " + backgroundColor; + notch.style.borderRight=notchBorder.style.borderRight=0; + notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid transparent"; + notchBorder.style.borderLeftColor=borderColor; //<--Make sure this appears after all other border definitions! + + + //Set horizontal position + div.style.left = (x-(div.offsetWidth+notchHeight)) + "px"; + callout.style.left = "0px"; + notch.style.left = (div.offsetWidth-1) + "px"; + notchBorder.style.left = div.offsetWidth + "px"; + + + //Set vertical position of the notch and div + valign(); + } + else if (position==="right"){ + + + //Update notch style so the arrow is pointing left + notch.style.borderTop=notchBorder.style.borderTop=notchSize+"px solid transparent"; + notch.style.borderLeft=notchBorder.style.borderLeft=0; + notch.style.borderRight=notchBorder.style.borderRight=notchSize+"px solid " + backgroundColor; + notch.style.borderBottom=notchBorder.style.borderBottom=notchSize+"px solid transparent"; + notchBorder.style.borderRightColor=borderColor; //<--Make sure this appears after all other border definitions! + + + //Set horizontal position + div.style.left = x + "px"; + callout.style.left = notchHeight + "px"; + notch.style.left = "1px"; + notchBorder.style.left = "0px"; + + + //Set vertical position of the notch and div + valign(); + } + else{ + return; + } + + + div.style.visibility = ""; + + me.show(); + }; + + + //************************************************************************** + //** hide + //************************************************************************** + /** Used to hide the callout. + */ + this.hide = function(){ + div.style.display = 'none'; + opening = false; + me.onHide(); + }; + + + //************************************************************************** + //** isVisible + //************************************************************************** + /** Returns true of the callout is visible. + */ + this.isVisible = function(){ + return (div.style.display !== 'none'); + }; + + + //************************************************************************** + //** onShow + //************************************************************************** + /** Called whenever the callout is made visible. + */ + this.onShow = function(){}; + + + //************************************************************************** + //** onHide + //************************************************************************** + /** Called whenever the callout is hidden. + */ + this.onHide = function(){}; + + + + //************************************************************************** + //** Utils + //************************************************************************** + var merge = javaxt.dhtml.utils.merge; + var getNextHighestZindex = javaxt.dhtml.utils.getNextHighestZindex; + var createElement = javaxt.dhtml.utils.createElement; + + + init(); }; \ No newline at end of file diff --git a/src/carousel/Carousel.js b/src/carousel/Carousel.js index 20b358d..d97ed1e 100644 --- a/src/carousel/Carousel.js +++ b/src/carousel/Carousel.js @@ -100,12 +100,18 @@ javaxt.dhtml.Carousel = function(parent, config) { slideOver: false, - /** If true, will allow touchscreen users to slide back and forth through - * the panels using touch gestures. + /** If true, will allow users to slide back and forth through the panels + * using touch gestures. */ drag: true, + /** Cursor style when dragging. Only applicable when "drag" is set to + * true. + */ + dragCursor: "ew-resize", //grabbing + + /** Currently unused */ visiblePanels: 1, @@ -844,6 +850,24 @@ javaxt.dhtml.Carousel = function(parent, config) { this.beforeChange = function(currPanel, nextPanel, direction){}; + //************************************************************************** + //** onDragStart + //************************************************************************** + /** Called when a client begins to drag a panel in the carousel + * @param currPanel Content of the active panel + */ + this.onDragStart = function(currPanel){}; + + + //************************************************************************** + //** onDragEnd + //************************************************************************** + /** Called when a client completes a drag event + * @param currPanel Content of the active panel + */ + this.onDragEnd = function(currPanel){}; + + //************************************************************************** //** getPanels //************************************************************************** @@ -930,13 +954,15 @@ javaxt.dhtml.Carousel = function(parent, config) { var startX, offsetX; var prevPanel; + var notify = true; + //Function called when a drag is initiated var onDragStart = function(e){ + startX = e.clientX; offsetX = parseInt(innerDiv.style.left); - //Disable text selection in the entire document - very important! var body = document.getElementsByTagName('body')[0]; if (!body.className.match(/(?:^|\s)javaxt-noselect(?!\S)/) ){ @@ -945,7 +971,6 @@ javaxt.dhtml.Carousel = function(parent, config) { prevPanel = currPanel; - innerDiv.style.cursor = 'move'; }; @@ -957,6 +982,19 @@ javaxt.dhtml.Carousel = function(parent, config) { //Otherwise, client is sliding to the left. + //Fire onDragStart event once we actually have some movement + if (notify){ + if (d!==0){ + me.onDragStart(currPanel.childNodes[0].childNodes[0]); + notify = false; + } + } + + + //Update cursor style + innerDiv.style.cursor = config.dragCursor; + + var left = offsetX-d; innerDiv.style.left = left + 'px'; @@ -1031,6 +1069,7 @@ javaxt.dhtml.Carousel = function(parent, config) { //Function called when the user stops dragging the div var onDragEnd = function(){ + notify = true; var rect = _getRect(outerDiv); var minX = rect.x; @@ -1082,10 +1121,17 @@ javaxt.dhtml.Carousel = function(parent, config) { if (animationSteps<0) animationSteps = -animationSteps; //console.log(start + "/" + end + " --> move " + (start-end) + "px in " + animationSteps + "ms"); + var direction = (start-end)>0 ? "next" : "back"; + + + slide(innerDiv, start, end, new Date().getTime(), 100, function(){ currPanel = innerDiv.childNodes[visiblePanel]; + var nextPanel = currPanel.childNodes[0].childNodes[0]; + me.onDragEnd(nextPanel); if (currPanel!=prevPanel){ - me.onChange(currPanel.childNodes[0].childNodes[0], prevPanel.childNodes[0].childNodes[0]); + me.beforeChange(prevPanel.childNodes[0].childNodes[0], nextPanel, direction); + me.onChange(nextPanel, prevPanel.childNodes[0].childNodes[0]); } }); @@ -1108,6 +1154,7 @@ javaxt.dhtml.Carousel = function(parent, config) { //MouseDown innerDiv.onmousedown = function(e){ if (sliding) return; + if (e.button>0) return; // Do not take any immediate action - just set the holdStarter // to wait for the predetermined delay, and then begin a hold @@ -1162,7 +1209,7 @@ javaxt.dhtml.Carousel = function(parent, config) { document.detachEvent("onmousemove", onDrag); document.detachEvent("onmouseup", onMouseUp); } - innerDiv.style.cursor = 'pointer'; + innerDiv.style.cursor = ''; onDragEnd(); //Remove the "javaxt-noselect" class diff --git a/src/checkbox/Checkbox.js b/src/checkbox/Checkbox.js index 28bac19..03575f6 100644 --- a/src/checkbox/Checkbox.js +++ b/src/checkbox/Checkbox.js @@ -1,426 +1,442 @@ -if(!javaxt) var javaxt={}; -if(!javaxt.dhtml) javaxt.dhtml={}; - -//****************************************************************************** -//** Checkbox Class -//****************************************************************************** -/** - * Form input that can be either checked or unchecked. The input consists of - * a square box with a checkmark when selected. - *
- * Here's a simple example of how to instantiate a checkbox using an existing - * div (DOM element) and a minimal config. See config settings for a full - * range of options. -
-    var checkbox = new javaxt.dhtml.Checkbox(div, {
-        label: "I Agree",
-        checked: false
-    });
- 
- * Once the checkbox is instantiated you can call any of the public methods. - * You can also add event listeners by overriding any of the public "on" or - * "before" methods like this: -
-    checkbox.onClick = function(checked){
-        console.log("I Agree", checked);
-    };
-
- * - ******************************************************************************/ - -javaxt.dhtml.Checkbox = function(parent, config) { - this.className = "javaxt.dhtml.Checkbox"; - - var me = this; - var outerDiv; - var box, check, mask, label; - - - var defaultConfig = { - - /** Label for the checkbox (optional). Accepts either a string or a DOM - * element. - */ - label: null, - - - /** Value associated with the checkbox (optional). Note that this is - * different than the checkbox state. Use the isChecked() method to - * determine whether the checkbox is checked. - */ - value: null, - - - /** If true, the checkbox will initially render with a check. Default is - * false. - */ - checked: false, - - - /** If true, the component will be disabled when rendered. Default is - * false. - */ - disabled: false, - - - /** Style for individual elements within the component. Note that you can - * provide CSS class names instead of individual style definitions. - */ - style:{ - - panel: { - display: "inline-block", - position: "relative" - }, - - box: { - width: "13px", - height: "13px", - border: "1px solid #cccccc", - borderRadius: "3px", - backgroundColor: "#F6F6F6", - cursor: "pointer", - margin: "0px", - color: "#2b2b2b" - }, - - label: { - fontFamily: "helvetica,arial,verdana,sans-serif", - fontSize: "14px", - whiteSpace: "nowrap", - cursor: "pointer", - padding: "1px 0 0 5px" - }, - - check: { - content: "", - display: "block", - width: "3px", - height: "6px", - border: "solid #ffffff", - borderWidth: "0 2px 2px 0", - transform: "rotate(45deg)", - margin: "1px 0 0 4px" - }, - - select: { - backgroundColor: "#007FFF", - border: "1px solid #003EFF", - color: "#FFFFFF" - }, - - disable: { - backgroundColor: "#ffffff", - border: "1px solid #ffffff", - borderRadius: "3px", - cursor: "pointer", - opacity: "0.5" - }, - - hover: { - backgroundColor: "#ededed" - } - } - }; - - - //************************************************************************** - //** Constructor - //************************************************************************** - var init = function(){ - - if (typeof parent === "string"){ - parent = document.getElementById(parent); - } - if (!parent) return; - - - //Clone the config so we don't modify the original config object - var clone = {}; - merge(clone, config); - - - //Merge clone with default config - merge(clone, defaultConfig); - config = clone; - - - //Create container - outerDiv = createElement('div', parent, config.style.panel); - if (config.display){ - console.warn( - "The 'display' config in the javaxt.dhtml.Checkbox " + - "class has been deprecated. Use panel style instead"); - outerDiv.style.display = config.display; - } - me.el = outerDiv; - addShowHide(me); - - - //Create checkbox - if (config.label){ - - //Create table with 2 columns - one for the checkbox - //and a column for the checkbox label. - var table = createTable(outerDiv); - table.style.fontFamily = "inherit"; - table.style.textAlign = "inherit"; - table.style.color = "inherit"; - var tr = table.addRow(); - - - box = createElement('div', tr.addColumn(), config.style.box); - label = createElement("div", tr.addColumn(), config.style.label); - if (isElement(config.label)){ - label.appendChild(config.label); - } - else{ - label.innerHTML = config.label; - } - - addEventHandlers(table); - } - else{ - - //Create checkbox with no label - box = createElement('div', outerDiv, config.style.box); - addEventHandlers(box); - } - - - //Set button state - if (config.disabled===true) me.disable(); - if (config.checked===true) me.select(); - }; - - - //************************************************************************** - //** addEventHandlers - //************************************************************************** - var addEventHandlers = function(div){ - - //Disable text selection - div.unselectable="on"; - div.onselectstart=function(){return false;}; - - - div.onmousedown=function(){ - addStyle(box, "select"); - return false; - }; - - - //Create onclick function - var onclick = function(){ - if (config.sound!=null) config.sound.play(); - me.toggle(); - me.onClick(box.checked); - }; - - - //Create logic to process touch events - var touchStartTime; - var touchEndTime; - var x1, x2, y1, y2; - var isTouch = false; - - div.ontouchstart = function(e) { - isTouch = true; - - e.preventDefault(); - x1 = e.changedTouches[0].pageX; - y1 = e.changedTouches[0].pageY; - touchStartTime = new Date().getTime(); - touchEndTime = null; - - if (box.checked!==true){ - addStyle(box, "hover"); - } - }; - - div.ontouchend = function(e) { - - touchEndTime= new Date().getTime(); - x2 = e.changedTouches[0].pageX; - y2 = e.changedTouches[0].pageY; - - var distance = Math.sqrt( (x2-=x1)*x2 + (y2-=y1)*y2 ); - if (distance<0) distance = -distance; - var duration = touchEndTime - touchStartTime; - - if ((duration <= 500 && distance <= 10) || //Quick tap - (duration > 500 && distance <= 10)) { //Long press - onclick(); - } - }; - - - - //Logic to process mouse events - div.onclick = function(){ - if (!isTouch) onclick(); - }; - div.onmouseover = function(){ - - if (box.checked!==true){ - addStyle(box, "hover"); - } - - }; - div.onmouseout = function(){ - - if (box.checked!==true){ - setStyle(box, "box"); - } - - }; - }; - - - //************************************************************************** - //** onClick - //************************************************************************** - /** Called whenever the checkbox is clicked. - */ - this.onClick = function(checked){}; - - - //************************************************************************** - //** onChange - //************************************************************************** - /** Called whenever the checkbox value is changed. - */ - this.onChange = function(checked){}; - - - //************************************************************************** - //** enable - //************************************************************************** - /** Used to enable the checkbox. - */ - this.enable = function(){ - mask.style.visibility = "hidden"; - }; - - - //************************************************************************** - //** disable - //************************************************************************** - /** Used to disable the checkbox. - */ - this.disable = function(){ - - if (mask){ - mask.style.visibility = "visible"; - } - else{ - - mask = createElement('div', config.style.disable); - mask.style.position = "absolute"; - mask.style.zIndex = "1"; - mask.style.width = "100%"; - mask.style.height = "100%"; - - var outerDiv = me.el; - outerDiv.insertBefore(mask, outerDiv.firstChild); - } - }; - - - //************************************************************************** - //** select - //************************************************************************** - /** Used to add a check to the checkbox. - */ - this.select = function(silent){ - if (box.checked===true) return; - box.checked = true; - addStyle(box,"select"); - - if (check){ - check.style.visibility = "visible"; - } - else{ - check = createElement('div', box, config.style.check); - } - - if (silent===true) return; - me.onChange(true); - }; - - - //************************************************************************** - //** deselect - //************************************************************************** - /** Used to remove the check from the checkbox. - */ - this.deselect = function(silent){ - if (box.checked===true){ - box.checked = false; - setStyle(box,"box"); - check.style.visibility = "hidden"; - - if (silent===true) return; - me.onChange(false); - } - }; - - - //************************************************************************** - //** toggle - //************************************************************************** - /** Used to toggle the checkbox state. - */ - this.toggle = function(){ - if (box.checked===true){ - me.deselect(); - } - else{ - me.select(); - } - }; - - - //************************************************************************** - //** getValue - //************************************************************************** - /** Returns the value associated with the checkbox. Note that this is - * different than the checkbox state. Use the isChecked() method to - * determine whether the checkbox is checked. The value is defined in - * the config used to instantiate this class. - */ - this.getValue = function(){ - if (config.value==null) return me.isChecked(); - return config.value; - }; - - - //************************************************************************** - //** isChecked - //************************************************************************** - /** Returns true if the checkbox is checked. - */ - this.isChecked = function(){ - return box.checked===true; - }; - - - //************************************************************************** - //** Utils - //************************************************************************** - var createElement = javaxt.dhtml.utils.createElement; - var createTable = javaxt.dhtml.utils.createTable; - var addShowHide = javaxt.dhtml.utils.addShowHide; - var isElement = javaxt.dhtml.utils.isElement; - var merge = javaxt.dhtml.utils.merge; - - var setStyle = function(el, style){ - javaxt.dhtml.utils.setStyle(el, config.style[style]); - }; - var addStyle = function(el, style){ - javaxt.dhtml.utils.addStyle(el, config.style[style]); - }; - - - init(); +if(!javaxt) var javaxt={}; +if(!javaxt.dhtml) javaxt.dhtml={}; + +//****************************************************************************** +//** Checkbox Class +//****************************************************************************** +/** + * Form input that can be either checked or unchecked. The input consists of + * a square box with a checkmark when selected. + *
+ * Here's a simple example of how to instantiate a checkbox using an existing + * div (DOM element) and a minimal config. See config settings for a full + * range of options. +
+    var checkbox = new javaxt.dhtml.Checkbox(div, {
+        label: "I Agree",
+        checked: false
+    });
+ 
+ * Once the checkbox is instantiated you can call any of the public methods. + * You can also add event listeners by overriding any of the public "on" or + * "before" methods like this: +
+    checkbox.onClick = function(checked){
+        console.log("I Agree", checked);
+    };
+
+ * + ******************************************************************************/ + +javaxt.dhtml.Checkbox = function(parent, config) { + this.className = "javaxt.dhtml.Checkbox"; + + var me = this; + var box, check, mask, label; + + + var defaultConfig = { + + /** Label for the checkbox (optional). Accepts either a string or a DOM + * element. + */ + label: null, + + + /** Value associated with the checkbox (optional). Note that this is + * different than the checkbox state. Use the isChecked() method to + * determine whether the checkbox is checked. + */ + value: null, + + + /** If true, the checkbox will initially render with a check. Default is + * false. + */ + checked: false, + + + /** If true, the component will be disabled when rendered. Default is + * false. + */ + disabled: false, + + + /** Style for individual elements within the component. Note that you can + * provide CSS class names instead of individual style definitions. + */ + style:{ + + panel: { + display: "inline-block" + }, + + /** Style for the checkbox + */ + box: { + width: "13px", + height: "13px", + border: "1px solid #ccc", + borderRadius: "3px", + backgroundColor: "#F6F6F6", + cursor: "pointer", + margin: "0px", + color: "#2b2b2b" + }, + + /** Style for the label next to the checkbox + */ + label: { + fontFamily: "helvetica,arial,verdana,sans-serif", + fontSize: "14px", + whiteSpace: "nowrap", + cursor: "pointer", + padding: "1px 0 0 5px" + }, + + /** Style for the checkmark inside the checkbox + */ + check: { + content: "", + display: "block", + width: "3px", + height: "6px", + border: "solid #fff", + borderWidth: "0 2px 2px 0", + transform: "rotate(45deg)", + margin: "1px 0 0 4px" + }, + + /** Style for the checkbox when it is selected/checked + */ + select: { + backgroundColor: "#007FFF", + border: "1px solid #003EFF", + color: "#fff" + }, + + /** Style for the mask used to disable the checkbox + */ + disable: { + cursor: "default" + }, + + /** Style for the checkbox when mouse hovers over + */ + hover: { + backgroundColor: "#ededed" + } + } + }; + + + //************************************************************************** + //** Constructor + //************************************************************************** + var init = function(){ + + if (typeof parent === "string"){ + parent = document.getElementById(parent); + } + if (!parent) return; + + + //Clone the config so we don't modify the original config object + var clone = {}; + merge(clone, config); + + + //Merge clone with default config + merge(clone, defaultConfig); + config = clone; + + + //Create container + me.el = createElement("span", parent, "javaxt-checkbox"); + addShowHide(me); + + + var innerDiv = createElement('div', me.el, config.style.panel); + innerDiv.style.position = "relative"; + if (config.display){ + console.warn( + "The 'display' config in the javaxt.dhtml.Checkbox " + + "class has been deprecated. Use panel style instead"); + innerDiv.style.display = config.display; + } + + + + //Create checkbox + if (config.label){ + + //Create table with 2 columns - one for the checkbox + //and a column for the checkbox label. + var table = createTable(innerDiv); + table.style.fontFamily = "inherit"; + table.style.textAlign = "inherit"; + table.style.color = "inherit"; + table.style.width = ""; + table.style.height = ""; + var tr = table.addRow(); + + + box = createElement('div', tr.addColumn(), config.style.box); + label = createElement("div", tr.addColumn(), config.style.label); + if (isElement(config.label)){ + label.appendChild(config.label); + } + else{ + label.innerHTML = config.label; + } + + addEventHandlers(table); + } + else{ + + //Create checkbox with no label + box = createElement('div', innerDiv, config.style.box); + addEventHandlers(box); + } + + + //Set button state + if (config.disabled===true) me.disable(); + if (config.checked===true) me.select(); + }; + + + //************************************************************************** + //** addEventHandlers + //************************************************************************** + var addEventHandlers = function(div){ + + //Disable text selection + div.unselectable="on"; + div.onselectstart=function(){return false;}; + + + div.onmousedown=function(){ + addStyle(box, "select"); + return false; + }; + + + //Create onclick function + var onclick = function(){ + if (config.sound!=null) config.sound.play(); + me.toggle(); + me.onClick(box.checked); + }; + + + //Create logic to process touch events + var touchStartTime; + var touchEndTime; + var x1, x2, y1, y2; + var isTouch = false; + + div.ontouchstart = function(e) { + isTouch = true; + + e.preventDefault(); + x1 = e.changedTouches[0].pageX; + y1 = e.changedTouches[0].pageY; + touchStartTime = new Date().getTime(); + touchEndTime = null; + + if (box.checked!==true){ + addStyle(box, "hover"); + } + }; + + div.ontouchend = function(e) { + + touchEndTime= new Date().getTime(); + x2 = e.changedTouches[0].pageX; + y2 = e.changedTouches[0].pageY; + + var distance = Math.sqrt( (x2-=x1)*x2 + (y2-=y1)*y2 ); + if (distance<0) distance = -distance; + var duration = touchEndTime - touchStartTime; + + if ((duration <= 500 && distance <= 10) || //Quick tap + (duration > 500 && distance <= 10)) { //Long press + onclick(); + } + }; + + + + //Logic to process mouse events + div.onclick = function(){ + if (!isTouch) onclick(); + }; + div.onmouseover = function(){ + + if (box.checked!==true){ + addStyle(box, "hover"); + } + + }; + div.onmouseout = function(){ + + if (box.checked!==true){ + setStyle(box, "box"); + } + + }; + }; + + + //************************************************************************** + //** onClick + //************************************************************************** + /** Called whenever the checkbox is clicked. + */ + this.onClick = function(checked){}; + + + //************************************************************************** + //** onChange + //************************************************************************** + /** Called whenever the checkbox value is changed. + */ + this.onChange = function(checked){}; + + + //************************************************************************** + //** enable + //************************************************************************** + /** Used to enable the checkbox. + */ + this.enable = function(){ + mask.style.visibility = "hidden"; + box.style.opacity = ""; + label.style.opacity = ""; + }; + + + //************************************************************************** + //** disable + //************************************************************************** + /** Used to disable the checkbox. + */ + this.disable = function(){ + + if (mask){ + mask.style.visibility = "visible"; + } + else{ + + mask = createElement('div', config.style.disable); + mask.style.position = "absolute"; + mask.style.zIndex = "1"; + mask.style.width = "100%"; + mask.style.height = "100%"; + + var innerDiv = me.el.firstChild; + innerDiv.insertBefore(mask, innerDiv.firstChild); + } + box.style.opacity = "0.5"; + label.style.opacity = "0.5"; + }; + + + //************************************************************************** + //** select + //************************************************************************** + /** Used to add a check to the checkbox. + */ + this.select = function(silent){ + if (box.checked===true) return; + box.checked = true; + addStyle(box,"select"); + + if (check){ + check.style.visibility = "visible"; + } + else{ + check = createElement('div', box, config.style.check); + } + + if (silent===true) return; + me.onChange(true); + }; + + + //************************************************************************** + //** deselect + //************************************************************************** + /** Used to remove the check from the checkbox. + */ + this.deselect = function(silent){ + if (box.checked===true){ + box.checked = false; + setStyle(box,"box"); + check.style.visibility = "hidden"; + + if (silent===true) return; + me.onChange(false); + } + }; + + + //************************************************************************** + //** toggle + //************************************************************************** + /** Used to toggle the checkbox state. + */ + this.toggle = function(){ + if (box.checked===true){ + me.deselect(); + } + else{ + me.select(); + } + }; + + + //************************************************************************** + //** getValue + //************************************************************************** + /** Returns the value associated with the checkbox. Note that this is + * different than the checkbox state. Use the isChecked() method to + * determine whether the checkbox is checked. The value is defined in + * the config used to instantiate this class. + */ + this.getValue = function(){ + if (config.value==null) return me.isChecked(); + return config.value; + }; + + + //************************************************************************** + //** isChecked + //************************************************************************** + /** Returns true if the checkbox is checked. + */ + this.isChecked = function(){ + return box.checked===true; + }; + + + //************************************************************************** + //** Utils + //************************************************************************** + var createElement = javaxt.dhtml.utils.createElement; + var createTable = javaxt.dhtml.utils.createTable; + var addShowHide = javaxt.dhtml.utils.addShowHide; + var isElement = javaxt.dhtml.utils.isElement; + var merge = javaxt.dhtml.utils.merge; + + var setStyle = function(el, style){ + javaxt.dhtml.utils.setStyle(el, config.style[style]); + }; + var addStyle = function(el, style){ + javaxt.dhtml.utils.addStyle(el, config.style[style]); + }; + + + init(); }; \ No newline at end of file diff --git a/src/datagrid/DataGrid.js b/src/datagrid/DataGrid.js index 8bccdf4..630c49c 100644 --- a/src/datagrid/DataGrid.js +++ b/src/datagrid/DataGrid.js @@ -302,6 +302,8 @@ javaxt.dhtml.DataGrid = function(parent, config) { //Update "header" setting in the column config var header = column.header; if (header==='x' && !checkboxHeader){ + clone.sortable = false; + clone.align = "center"; clone.header = createCheckbox(); var checkbox = clone.header.checkbox; checkboxHeader = { @@ -364,6 +366,27 @@ javaxt.dhtml.DataGrid = function(parent, config) { //Override the update method rows[i].update = function(){ config.update(this, this.record); + + //Fill in any empty cells using the column config. Columns + //with a "field" attribute are populated using values from + //the record. This also ensures that the checkbox column is + //rendered, even if the update function never called + //row.set('x'). + for (var j=0; j=0 && key-1){ + + //Wrap value in an overflow div as requested + if (config.columns[idx].wrap===true) val = wrap(val); + + this._set(idx, val); + } + else{ + + //Pass the key through as-is and let the underlying + //table try to resolve it (e.g. numeric strings) + this._set(key, val); + } } }; @@ -420,13 +474,15 @@ javaxt.dhtml.DataGrid = function(parent, config) { //Check if the client clicked inside a checkbox var insideCheckbox = false; var checkboxCol = this.childNodes[checkboxHeader.idx]; - var checkbox = checkboxCol.getContent().checkbox; - var rect = _getRect(checkbox.el); - var clientX = e.clientX; - var clientY = e.clientY; - if (clientX>=rect.left && clientX<=rect.right){ - if (clientY>=rect.top && clientY<=rect.bottom){ - insideCheckbox = true; + var checkbox = getCheckbox(checkboxCol.getContent()); + if (checkbox){ + var rect = _getRect(checkbox.el); + var clientX = e.clientX; + var clientY = e.clientY; + if (clientX>=rect.left && clientX<=rect.right){ + if (clientY>=rect.top && clientY<=rect.bottom){ + insideCheckbox = true; + } } } @@ -443,7 +499,8 @@ javaxt.dhtml.DataGrid = function(parent, config) { for (var i=1; i0){ - url += "&" + key + "=" + str; - } - } - } - } - } - } + //Build URL using the shared buildURL function so that the params, + //filter, and orderby are identical to the URLs used by load(). + //A consistent orderby is particularly important here because we + //use an offset to fetch records beyond what has been loaded. + var url = buildURL({ + fields: fieldNames, + count: false, + offset: currPage*config.limit + }); //Execute service request and process response @@ -1151,10 +1193,25 @@ javaxt.dhtml.DataGrid = function(parent, config) { var arr = []; var records = config.parseResponse.apply(me, [request]); for (var i=0; i0) fieldNames += ","; - fieldNames += config.fields[i]; - } - - - //Fire "beforeLoad" event - me.beforeLoad(page); - + /** Returns the checkbox associated with the content of a cell. Returns + * null if the cell is empty or does not contain a checkbox (e.g. the + * update function never called row.set('x') for the checkbox column). + * Note that cell content may be a string or null (see getContent in the + * Table class) so we need to check for the checkbox property carefully. + */ + var getCheckbox = function(content){ + if (content && content.checkbox) return content.checkbox; + return null; + }; - //Create params for the querystring - var params = { - page: page, - limit: config.limit, - fields: fieldNames - }; + //************************************************************************** + //** buildURL + //************************************************************************** + /** Used to construct a URL to fetch records from the server. Merges the + * given params with config.params and the current filter. + */ + var buildURL = function(params){ + if (!params) params = {}; //Add config.params to the querystring as needed @@ -1334,11 +1372,6 @@ javaxt.dhtml.DataGrid = function(parent, config) { } - //Add count to the querystring - if (config.count==true && page==1) params.count = true; - else params.count = false; - - //Add filter to the querystring var orderby = ""; if (filter){ @@ -1381,12 +1414,67 @@ javaxt.dhtml.DataGrid = function(parent, config) { } - //Build URL var url = config.url; if (url.indexOf("?")==-1) url+= "?"; url += encodeParams(params); url += orderby; + return url; + }; + + + //************************************************************************** + //** load + //************************************************************************** + var load = function(page, callback){ + + if (pageRequests[page+""]) return; + if (loading) return; + loading = true; + + mask.show(); + pageRequests[page+""] = page; + + + //Parse page + if (page){ + page = parseInt(page); + if (isNaN(page) || page<1) page = 1; + } + else{ + page = 1; + } + if (page==1) eof = false; + + + //Generate list of fields + var fieldNames = ""; + for (var i=0; i0) fieldNames += ","; + fieldNames += config.fields[i]; + } + + + //Fire "beforeLoad" event + me.beforeLoad(page); + + + + //Create params for the querystring + var params = { + page: page, + limit: config.limit, + fields: fieldNames + }; + + + //Add count to the querystring + if (config.count==true && page==1) params.count = true; + else params.count = false; + + + //Build URL + var url = buildURL(params); diff --git a/src/datepicker/DateInput.js b/src/datepicker/DateInput.js index 76eb24c..230a5aa 100644 --- a/src/datepicker/DateInput.js +++ b/src/datepicker/DateInput.js @@ -1,405 +1,425 @@ -if(!javaxt) var javaxt={}; -if(!javaxt.dhtml) javaxt.dhtml={}; - -//****************************************************************************** -//** DateInput Class -//****************************************************************************** -/** - * Form input used to specify a date. The input consists of a text field and - * a button. When the user clicks on the button, a javaxt.dhtml.DatePicker - * will appear below the input. - * - ******************************************************************************/ - -javaxt.dhtml.DateInput = function(parent, config) { - this.className = "javaxt.dhtml.DateInput"; - - - var me = this; - var datePicker, menu, input, button, mask; - var formatDate; - - var defaultConfig = { - - - /** Initial date/value for the input. Supports both strings and dates. - */ - date: null, - - - /** If true, the calendar menu will appear whenever the text field has - * focus (e.g. mouse click). Default is false. - */ - showMenuOnFocus: false, - - - /** Style for individual elements within the component. Note that you can - * provide CSS class names instead of individual style definitions. - */ - style: { - - input: { - color: "#363636", - fontSize: "14px", - height: "24px", - lineHeight: "24px", - padding: "0px 4px", - verticalAlign: "middle", - transition: "border 0.2s linear 0s, box-shadow 0.2s linear 0s", - backgroundColor: "#fff", - border: "1px solid #ccc", - borderRight: "0 none", - boxShadow: "0 1px 1px rgba(0, 0, 0, 0.075) inset" - }, - - button: { - color: "#363636", - height: "24px", - width: "24px", - border: "1px solid #b4b4b4", - cursor: "pointer", - - backgroundImage: "url(data:image/png;base64,iVBORw0KGgoAAAANSUh"+ - "EUgAAABAAAAAQCAYAAAAf8/9hAAAAlklEQVQ4jWNgGAUowNvbW9PBwUEAjxJGHx"+ - "8f/dDQUB4MGV9f3+TAwMD/AQEB97y8vOSxafb392+BqrlkbGzMhSLr7++/MTAw8"+ - "D8OQ+CaYdjb21sT3flKAQEBj7AYgqHZz8+vBqsHPT09ldENCQgImIysOSAgoBZP"+ - "GGEaQpJmfIYQrRmbITj9TAg4ODgIeHp6apGleegAAME5Y+rCcN+AAAAAAElFTkSuQmCC)", - backgroundPosition: "3px 3px", - backgroundRepeat: "no-repeat", - backgroundColor: "#e4e4e4" - }, - - menu: { - backgroundColor: "#fff", - border: "1px solid #ccc", - borderTop: "0 none" - }, - - datePicker: { - - } - }, - - /** Function used to format date for display. Returns "M/D/YYYY" by default. - */ - formatDate: function(date){ - return (date.getMonth()+1) + "/" + date.getDate() + "/" + date.getFullYear(); - } - }; - - - //************************************************************************** - //** Constructor - //************************************************************************** - var init = function(){ - - if (typeof parent === "string"){ - parent = document.getElementById(parent); - } - if (!parent) return; - - - //Clone the config so we don't modify the original config object - var clone = {}; - merge(clone, config); - - - //Merge clone with default config - merge(clone, defaultConfig); - config = clone; - - - formatDate = config.formatDate; - - - //Create main div - var mainDiv = createElement("div", parent, { - position: "relative", - display: "inline-block" - }); - mainDiv.setAttribute("desc", me.className); - me.el = mainDiv; - - - //Create table with 2 columns - var table = createTable(mainDiv); - table.style.height = ""; - var tr = table.addRow(); - - - //Create input in the first column - input = createElement('input', tr.addColumn({width: "100%"}), config.style.input); - input.style.width="100%"; - input.type = "text"; - - input.onkeydown = function(e){ - if (e.keyCode===9){ - e.preventDefault(); - } - }; - input.onkeyup = function(e){ - if (e.keyCode===9){ //tab - me.hideMenu(); - focusNext(); - } - else if (e.keyCode===40){ //down arrow - me.showMenu(); - } - }; - - if (config.showMenuOnFocus){ - input.onfocus = function(){ - me.showMenu(); - }; - } - - - me.setDate(config.date, true); - - - input.oninput = function(){ - if (me.isDisabled()===true) return; - - var val = this.value; - if (isDate(val)){ - var date = new Date(val); - if (menu){ - if (menu.style.visibility === "hidden"){} - else{ - datePicker.setDate(date); - } - } - me.onChange(date); - } - }; - input.onpaste = input.oninput; - input.onpropertychange = input.oninput; - - - - - //Create button in the second column - button = createElement('input', tr.addColumn(), config.style.button); - button.type = "button"; - button.onclick = function(){ - - if (menu){ - if (menu.style.visibility === "hidden"){ - me.showMenu(); - } - else{ - menu.style.visibility = "hidden"; - } - } - else{ - me.showMenu(); - } - }; - - - //Add public show/hide methods - addShowHide(me); - }; - - - //************************************************************************** - //** isDisabled - //************************************************************************** - /** Returns true if the compnentent has been disabled. - */ - this.isDisabled = function(){ - return me.el.disabled; - }; - - - //************************************************************************** - //** enable - //************************************************************************** - /** Used to enable the input allowing users to interact with the component. - */ - this.enable = function(){ - var outerDiv = me.el; - if (mask){ - outerDiv.style.opacity = ""; - mask.style.visibility = "hidden"; - } - outerDiv.disabled = false; - }; - - - //************************************************************************** - //** disable - //************************************************************************** - /** Used to disable the input preventing users from interacting with the - * component. - */ - this.disable = function(){ - me.hideMenu(); - - var outerDiv = me.el; - outerDiv.style.opacity = "0.6"; - - if (mask){ - mask.style.visibility = "visible"; - } - else{ - mask = createElement('div', { - width: "100%", - height: "100%", - position: "absolute", - zIndex: 1 - }); - mask.setAttribute("desc", "mask"); - outerDiv.insertBefore(mask, outerDiv.firstChild); - } - outerDiv.disabled = true; - }; - - - //************************************************************************** - //** onChange - //************************************************************************** - this.onChange = function(currDate, prevDate){}; - - - //************************************************************************** - //** showMenu - //************************************************************************** - this.showMenu = function(){ - - var date = me.getValue(); - if (!date) date = new Date(); - - if (datePicker){ - datePicker.setDate(date); - menu.style.visibility = ''; - } - else{ - - var mainDiv = me.el; - - menu = createElement('div', mainDiv, config.style.menu); - menu.setAttribute("desc", "menu"); - menu.style.width = "100%"; - menu.style.position = "absolute"; - menu.style.zIndex = 1; - //menu.style.visibility = "hidden"; - - - - - //Hide menu if the client clicks outside of the menu - var hideMenu = function(e){ - if (!mainDiv.contains(e.target)){ - menu.style.visibility = "hidden"; - } - }; - if (document.addEventListener) { // For all major browsers, except IE 8 and earlier - document.addEventListener("click", hideMenu); - } - else if (document.attachEvent) { // For IE 8 and earlier versions - document.attachEvent("onclick", hideMenu); - } - - - - datePicker = new javaxt.dhtml.DatePicker(menu, { - date: date, - style: config.style.datePicker - }); - datePicker.select(); - datePicker.onClick = function(date){ - me.hideMenu(); - me.setValue(date); - }; - } - - - //TODO: adjust position of the menu if it's not visible - - }; - - - //************************************************************************** - //** hideMenu - //************************************************************************** - this.hideMenu = function(){ - if (menu) menu.style.visibility = "hidden"; - }; - - - //************************************************************************** - //** setValue - //************************************************************************** - this.setValue = function(date, silent){ - var prevDate = me.getValue(); - - if (isDate(date)){ - if (!date.getTime) date = new Date(date); - input.value = formatDate(date); - } - else{ - date = null; - input.value = ""; - } - - if (silent===true) return; - me.onChange(date, prevDate); - }; - - - //************************************************************************** - //** getValue - //************************************************************************** - this.getValue = function(){ - var date = new Date(input.value); - if (isNaN( date.getTime() )) date = null; - return date; - }; - - - //************************************************************************** - //** setDate - //************************************************************************** - this.setDate = function(date, silent){ - me.setValue(date, silent); - }; - - - //************************************************************************** - //** getDate - //************************************************************************** - this.getDate = function(){ - me.getValue(); - }; - - - //************************************************************************** - //** focusNext - //************************************************************************** - /** Used to focus on the next available form element. - */ - var focusNext = function(){ - var form = input.form; - if (form){ - for (var i=0; ix) console.log(d + " vs " + x); - if (d>x) d = x; - - me.render(new Date(y, m, d)); - }; - - - //************************************************************************** - //** next - //************************************************************************** - /** Used to render the next month. - */ - this.next = function(){ - - var m = currDate.getMonth()+1; - var d = currDate.getDate(); - var y = currDate.getFullYear(); - - var x = new Date(y, m+1, 0).getDate(); - if (d>x) console.log(d + " vs " + x); - if (d>x) d = x; - - me.render(new Date(y, m, d)); - }; - - - //************************************************************************** - //** getMonth - //************************************************************************** - /** Returns the month rendered in the date picker (0-11) - */ - this.getMonth = function(){ - return currDate.getMonth(); - }; - - - //************************************************************************** - //** getYear - //************************************************************************** - /** Returns the year rendered in the date picker. - */ - this.getYear = function(){ - return currDate.getFullYear(); - }; - - - //************************************************************************** - //** computeRange - //************************************************************************** - /** Computes several key variables used to render the calndar including - * start/end date and the total number of rows to render. Credit: - * http://stackoverflow.com/a/2485172 - */ - var computeRange = function(d){ - - var year = d.getFullYear(); - var month = d.getMonth()+1; - var firstOfMonth = new Date(year, month-1, 1); - var lastOfMonth = new Date(year, month, 0); - var numWeeks = Math.ceil( (firstOfMonth.getDay() + lastOfMonth.getDate()) / 7); - - var startDate = new Date(firstOfMonth); - startDate.setDate(startDate.getDate()-firstOfMonth.getDay()); - - var endDate = new Date(lastOfMonth); - endDate.setDate(endDate.getDate()+(6-lastOfMonth.getDay())); - - - return { - numWeeks: numWeeks, - startDate: startDate, - endDate: endDate - }; - }; - - - - - //************************************************************************** - //** Utils - //************************************************************************** - var merge = javaxt.dhtml.utils.merge; - var createElement = javaxt.dhtml.utils.createElement; - var createTable = javaxt.dhtml.utils.createTable; - var addStyle = function(el, style){ - javaxt.dhtml.utils.addStyle(el, config.style[style]); - }; - - - - init(); +if(!javaxt) var javaxt={}; +if(!javaxt.dhtml) javaxt.dhtml={}; + +//****************************************************************************** +//** DatePicker Class +//****************************************************************************** +/** + * Calendar component used select dates + * + ******************************************************************************/ + +javaxt.dhtml.DatePicker = function(parent, config) { + this.className = "javaxt.dhtml.DatePicker"; + + var me = this; + var defaultConfig = { + + /** Used to set the initial view. All we need is a month and a year. + */ + date: new Date(), + + /** Used to set the selection mode. Options are "day" or "week". + */ + selectionMode: "day", + + + /** Day names or abbreviations to use in the column headers + */ + daysOfWeek: ["S","M","T","W","T","F","S"], + + + /** Month names or abbreviations used in the header + */ + months : ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ], + + + /** If true, allows users to deselect the current selection via mouse + * click + */ + allowDeselect: true, + + + /** Style for individual elements within the component. Note that you can + * provide CSS class names instead of individual style definitions. + */ + style: { + + + //Panel Style + panel: { + fontFamily: "helvetica,arial,verdana,sans-serif", + backgroundColor: "#ffffff", + border: "1px solid #b4cbdd", + display: "inline-block" + }, + + + header: { + backgroundColor: "#d9e7f8", + height: "25px", + lineHeight: "25px" + }, + + //Title area + title: { + position: "absolute", + width: "100%", + whiteSpace: "nowrap", + //fontFamily: "helvetica,arial,verdana,sans-serif", + fontSize: "14px", + fontWeight: "bold", + color: "#555555", + textAlign: "center", + cursor: "default" + }, + + next: { + float: "right", + borderRight: "2px solid #5e8be0", + borderBottom: "2px solid #5e8be0", + width: "7px", + height: "7px", + transform: "rotate(-45deg)", + margin: "7px 10px 0 0", + cursor: "pointer" + }, + + back: { + float: "left", + borderRight: "2px solid #5e8be0", + borderBottom: "2px solid #5e8be0", + width: "7px", + height: "7px", + transform: "rotate(135deg)", + margin: "7px 0 0 10px", + cursor: "pointer" + }, + + + cell: { + width: "18px", + height: "18px", + lineHeight: "18px", + textAlign: "right", + padding: "2px 4px 1px 0px", + fontSize: "11px", + color: "#000000", + cursor: "pointer", + border: "1px solid #ffffff", + margin: "1px" + }, + + + cellHeader: { //overrides cell style for header cells + color: "#233d6d", + fontSize: "10px", + lineHeight: "10px", + paddingBottom: "0px", + paddingTop: "0px", + border: "0px", + borderTop: "1px solid #bbccff", + borderBottom: "1px solid #bbccff" + }, + + + previousMonth: { //overrides cell style for previous month + color: "#aaaaaa" + }, + + nextMonth: { //overrides cell style for next month + color: "#aaaaaa" + }, + + today: { + width: "22px", + height: "21px", + border: "1px solid #FF7373", + top: "-1px", + left: "-1px" + }, + + + selectedRow: { + + }, + + selectedCell: { + color: "#000000", + fontWeight: "bold", + backgroundColor: "#fff4bf", + border: "1px solid #bfa52f" + } + + } + }; + + var currDate; + var startDate; + var mainDiv; + var todayHighlightDiv; + var cells = []; + var selectionMode; + + + //************************************************************************** + //** Constructor + //************************************************************************** + /** Creates a new instance of this class. */ + + var init = function(){ + + if (typeof parent === "string"){ + parent = document.getElementById(parent); + } + if (!parent) return; + + + //Clone the config so we don't modify the original config object + var clone = {}; + merge(clone, config); + + + //Merge clone with default config + merge(clone, defaultConfig); + config = clone; + + + //Update style config keyword for legacy apps + if (config.style.selected && !config.style.selectedCell){ + config.style.selectedCell = config.style.selected; + } + + + //Get selection mode + selectionMode = config.selectionMode; + + + //Create container + var div = createElement("div", parent, "javaxt-datepicker"); + me.el = div; + + + //Create main div + mainDiv = createElement("div", div, config.style.panel); + mainDiv.style.display = "table"; + + + //Disable text selection + mainDiv.unselectable="on"; + mainDiv.onselectstart=function(){return false;}; + mainDiv.onmousedown=function(){return false;}; + + + //Create highlight div + todayHighlightDiv = createElement('div', config.style.today); + todayHighlightDiv.style.position = "absolute"; + + + //Render month + me.setDate(config.date); + }; + + + //************************************************************************** + //** setDate + //************************************************************************** + /** Used to update the calendar and select the given date. + */ + this.setDate = function(date){ + date = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + + //Render date + me.render(date); + + //Deselect current selection + me.deselect(); + + //Select date + for (var i=0; ix) console.log(d + " vs " + x); + if (d>x) d = x; + + me.render(new Date(y, m, d)); + }; + + + //************************************************************************** + //** next + //************************************************************************** + /** Used to render the next month. + */ + this.next = function(){ + + var m = currDate.getMonth()+1; + var d = currDate.getDate(); + var y = currDate.getFullYear(); + + var x = new Date(y, m+1, 0).getDate(); + if (d>x) console.log(d + " vs " + x); + if (d>x) d = x; + + me.render(new Date(y, m, d)); + }; + + + //************************************************************************** + //** getMonth + //************************************************************************** + /** Returns the month rendered in the date picker (0-11) + */ + this.getMonth = function(){ + return currDate.getMonth(); + }; + + + //************************************************************************** + //** getYear + //************************************************************************** + /** Returns the year rendered in the date picker. + */ + this.getYear = function(){ + return currDate.getFullYear(); + }; + + + //************************************************************************** + //** computeRange + //************************************************************************** + /** Computes several key variables used to render the calndar including + * start/end date and the total number of rows to render. Credit: + * http://stackoverflow.com/a/2485172 + */ + var computeRange = function(d){ + + var year = d.getFullYear(); + var month = d.getMonth()+1; + var firstOfMonth = new Date(year, month-1, 1); + var lastOfMonth = new Date(year, month, 0); + var numWeeks = Math.ceil( (firstOfMonth.getDay() + lastOfMonth.getDate()) / 7); + + var startDate = new Date(firstOfMonth); + startDate.setDate(startDate.getDate()-firstOfMonth.getDay()); + + var endDate = new Date(lastOfMonth); + endDate.setDate(endDate.getDate()+(6-lastOfMonth.getDay())); + + + return { + numWeeks: numWeeks, + startDate: startDate, + endDate: endDate + }; + }; + + + + + //************************************************************************** + //** Utils + //************************************************************************** + var merge = javaxt.dhtml.utils.merge; + var createElement = javaxt.dhtml.utils.createElement; + var createTable = javaxt.dhtml.utils.createTable; + var addStyle = function(el, style){ + javaxt.dhtml.utils.addStyle(el, config.style[style]); + }; + + + + init(); }; \ No newline at end of file diff --git a/src/panel/Panel.js b/src/panel/Panel.js new file mode 100644 index 0000000..9621ffc --- /dev/null +++ b/src/panel/Panel.js @@ -0,0 +1,150 @@ +if(!javaxt) var javaxt={}; +if(!javaxt.dhtml) javaxt.dhtml={}; + +//****************************************************************************** +//** Panel +//****************************************************************************** +/** + * General purpose container with an optional header, toolbar, and footer. + * + ******************************************************************************/ + + +javaxt.dhtml.Panel = function (parent, config) { + + var me = this; + var defaultConfig = { + + /** CSS class name for the container. + */ + className: null, + + /** Style for individual elements within the component. Note that you can + * provide CSS class names instead of individual style definitions. + */ + style: { + header: {}, + toolbar: {}, + body: {}, + footer: {} + } + }; + + var table, header, toolbar, body, footer; + + + //************************************************************************** + //** Constructor + //************************************************************************** + var init = function(){ + + if (typeof parent === "string"){ + parent = document.getElementById(parent); + } + if (!parent) return; + + + //Clone the config so we don't modify the original config object + var clone = {}; + merge(clone, config); + + + //Merge clone with default config + merge(clone, defaultConfig); + config = clone; + + + //Create main div + var mainDiv = createElement("div", parent, { + width: "100%", + height: "100%", + position: "relative" + }); + mainDiv.className = "javaxt-panel"; + if (config.className) mainDiv.className += " " + config.className; + + me.el = mainDiv; + addShowHide(me); + + + //Create body + table = createTable(mainDiv); + body = table.addRow().addColumn(config.style.body); + body.style.height = "100%"; + }; + + + //************************************************************************** + //** getHeader + //************************************************************************** + /** Returns the header element. + */ + this.getHeader = function(){ + if (!header){ + + var tr = createElement("tr"); + if (toolbar){ + var tbody = toolbar.parentNode.parentNode; + tbody.insertBefore(tr, toolbar.parentNode); + } + else{ + var tbody = body.parentNode.parentNode; + tbody.insertBefore(tr, body.parentNode); + } + + + header = createElement("td", tr, config.style.header); + } + return header; + }; + + + //************************************************************************** + //** getToolbar + //************************************************************************** + /** Returns the toolbar element. + */ + this.getToolbar = function(){ + if (!toolbar){ + var tr = createElement("tr"); + var tbody = body.parentNode.parentNode; + tbody.insertBefore(tr, body.parentNode); + toolbar = createElement("td", tr, config.style.toolbar); + } + return toolbar; + }; + + + //************************************************************************** + //** getBody + //************************************************************************** + /** Returns the body element. + */ + this.getBody = function(){ + return body; + }; + + + //************************************************************************** + //** getFooter + //************************************************************************** + /** Returns the footer element. + */ + this.getFooter = function(){ + if (!footer){ + footer = table.addRow().addColumn(config.style.footer); + } + return footer; + }; + + + //************************************************************************** + //** Utils + //************************************************************************** + var createElement = javaxt.dhtml.utils.createElement; + var createTable = javaxt.dhtml.utils.createTable; + var addShowHide = javaxt.dhtml.utils.addShowHide; + var merge = javaxt.dhtml.utils.merge; + + init(); +}; \ No newline at end of file diff --git a/src/switch/Switch.js b/src/switch/Switch.js index 1e2ac0a..13a6eec 100644 --- a/src/switch/Switch.js +++ b/src/switch/Switch.js @@ -80,17 +80,21 @@ javaxt.dhtml.Switch = function(parent, config) { config = clone; - groove = createElement("div", parent, config.style.groove); + //Create container + var mainDiv = createElement("span", parent); + mainDiv.className = "javaxt-switch"; + me.el = mainDiv; + addShowHide(me); + + + //Create slider + groove = createElement("div", mainDiv, config.style.groove); groove.onclick = function(){ me.setValue(!me.getValue()); }; handle = createElement("div", groove, config.style.handle); me.setValue(config.value, true); - me.el = groove; - - //Add public show/hide methods - addShowHide(me); }; diff --git a/src/tabpanel/TabPanel.js b/src/tabpanel/TabPanel.js index abc149d..3f7c3e0 100644 --- a/src/tabpanel/TabPanel.js +++ b/src/tabpanel/TabPanel.js @@ -1,414 +1,414 @@ -if(!javaxt) var javaxt={}; -if(!javaxt.dhtml) javaxt.dhtml={}; - -//****************************************************************************** -//** TabPanel -//****************************************************************************** -/** - * Standard tab control used to show/hide individual panels. - * - ******************************************************************************/ - -javaxt.dhtml.TabPanel = function(parent, config) { - this.className = "javaxt.dhtml.TabPanel"; - - var me = this; - var tabList; - var tabContent; - - var defaultConfig = { - - /** If true, will insert a "close" icon into the tab that will allow - * users to close/remove the tab from the tab panel. - */ - closable: false, - - /** Style for individual elements within the component. Note that you can - * provide CSS class names instead of individual style definitions. - */ - style : { - tabBar: { - border: "1px solid #ccc", - backgroundColor: "#eaeaea", - height: "30px", - borderBottom: "0px" - }, - activeTab: { - lineHeight: "30px", - padding: "0 7px", - backgroundColor: "#fafafa", - cursor: "default", - borderRight: "1px solid #ccc", - borderBottom: "1px solid #fafafa" - }, - inactiveTab: { - lineHeight: "30px", - padding: "0 7px", - cursor: "pointer", - borderRight: "1px solid #ccc", - borderBottom: "0px" - }, - tabBody: { - border: "1px solid #ccc", - verticalAlign: "top" - }, - closeIcon: { - - } - } - }; - - - //************************************************************************** - //** Constructor - //************************************************************************** - var init = function(){ - - if (typeof parent === "string"){ - parent = document.getElementById(parent); - } - if (!parent) return; - - - //Clone the config so we don't modify the original config object - var clone = {}; - merge(clone, config); - - - //Merge clone with default config - merge(clone, defaultConfig); - config = clone; - - - //Create main table - var table = createTable(parent); - table.setAttribute("desc", me.className); - - - - //Row 1 - var td = table.addRow().addColumn(); - setStyle(td, "tabBar"); - td.style.width = "100%"; - - tabList = createElement("ul", td, { - listStyle: "none outside none", - height: "100%", - padding: 0, - margin: 0 - }); - - - - //Row 2 - var td = table.addRow().addColumn(); - setStyle(td, "tabBody"); - td.style.width = "100%"; - td.style.height = "100%"; - - tabContent = createElement("div", td, { - width: "100%", - height: "100%", - position: "relative" - }); - - - me.el = table; - }; - - - //************************************************************************** - //** addTab - //************************************************************************** - /** Used to add a new tab to the panel. - * @param label Tab title. - * @param el Tab contents. Rendered when the tab is active. Accepts strings, - * DOM elements, and nulls - */ - this.addTab = function(label, el){ - - var div = createElement("div", tabContent, { - width: "100%", - height: "100%", - position: "absolute" - }); - - - if (el==null) div.innerHTML = ""; - else{ - if (isElement(el)){ - var p = el.parentNode; - if (p) p.removeChild(el); - div.appendChild(el); - } - else{ - if (typeof el === "string"){ - div.innerHTML = el; - } - } - } - - - var tab = createElement("li", tabList); - setStyle(tab, "inactiveTab"); - tab.style.position = "relative"; - tab.style.float = "left"; - tab.style.height = "100%"; - tab.innerHTML = label; - tab.el = div; - tab.onclick = function(){ - raiseTab(this); - }; - tab.onselectstart = function () {return false;}; - tab.onmousedown = function () {return false;}; - for (var i=0; i - *
  • name: Name of the tab and tab label
  • - *
  • header: DOM element for the tab header
  • - *
  • body: DOM element for the tab content
  • - *
  • hidden: Boolean
  • - *
  • active: Boolean
  • - * - */ - this.getTabs = function(){ - var tabs = []; - for (var i=0; i