It is currently Mon Mar 18, 2024 9:16 pm



Reply to topic  [ 6 posts ] 
js window object 
Author Message
Felix Rex
User avatar

Joined: Fri Mar 28, 2003 6:01 pm
Posts: 16646
Location: On a slope
Reply with quote
Post js window object
So, in a lot of my web projects I find myself using javascript to create windows that mimic that mimic the windows in, well, Windows. This may be a popup window that looks like an explorer window or a right-click menu that looks like a context menu. Recoding the whole thing every time is a pain in the butt, so I wanted to write a library of sorts. I've also been wanting to learn OO in javascript since it's so monstrously different than OO in php and c#.

Anyway, this is where I am. Here's a link to a demo. It's not done... it's not even close to done. I've run into a lot of serious problems. But more on that later.

demo: http://clankiller.com/jsWindow/
zip download: http://clankiller.com/jsWindow/js_windowObject.zip

Click on "create" to dynamically create a window. The location, size and content are all static, this is just a demo. But notice it's possible to drag the window around even to resize it. Also, dragging and resizing is 'buggy'. I guess I should post the code first.

This is the class and the meat of the monster.
Code:
var o = new Array();

function _window(objectid){
   /* objects */
   this.thewindow;
   this.thecontent;
   this.thetitle;
   this.thestatus;
   this.windowControls;
   this.row1;   this.row2;   this.row3;
   this.r1c1;   this.r1c2;   this.r1c3;
   this.r2c1;   this.r2c2;   this.r2c3;
   this.r3c1;   this.r3c2;   this.r3c3;
   var id = objectid;
   
   /* window options  */
   this.windowOptions = new Array();
   this.windowOptions['ok'] = true;
   this.windowOptions['cancel'] = true;
   this.windowOptions['close'] = true;
   this.windowOptions['titlebar'] = true;
   this.windowOptions['drag'] = 'titlebar';
   this.windowOptions['status'] = true;
   this.windowOptions['resize'] = true;
   
   /* object properties */
   this.timers = new Array();
   this.offset;
   this.dodrag = false;
   this.doresize = false;
   
   this.setOptions = function(option, setting){
      this.windowOptions[option] = setting;
   }
   
   this.create = function(style, x, y){
      if(o.length > 1){
         //multiple windows, reset z and styles.
      }
      this.thewindow = document.createElement('div');
      
      switch(style){
         case 'normal':
            //build the entire window (table)
            this.thewindow = document.createElement('table');
            this.row1 = document.createElement('tr');
            this.row2 = document.createElement('tr');
            this.row3 = document.createElement('tr');
            this.r1c1 = document.createElement('td');
            this.r1c2 = document.createElement('th');
            this.r1c3 = document.createElement('td');
            this.r2c1 = document.createElement('td');
            this.r2c2 = document.createElement('td');
            this.r2c3 = document.createElement('td');
            this.r3c1 = document.createElement('td');
            this.r3c2 = document.createElement('td');
            this.r3c3 = document.createElement('td');
            
            this.row1.appendChild(this.r1c1);   this.row1.appendChild(this.r1c2);   this.row1.appendChild(this.r1c3);   
            this.row2.appendChild(this.r2c1);   this.row2.appendChild(this.r2c2);   this.row2.appendChild(this.r2c3);   
            this.row3.appendChild(this.r3c1);   this.row3.appendChild(this.r3c2);   this.row3.appendChild(this.r3c3);
            
            if(this.windowOptions['titlebar']){ this.thewindow.appendChild(this.row1); }
            this.thewindow.appendChild(this.row2);
            if(this.windowOptions['status']){ this.thewindow.appendChild(this.row3); }
            
            //build the controls (table)
            if(this.windowOptions['titlebar']){
               this.windowControls = document.createElement('table');
               var r = document.createElement('tr');
               var c = document.createElement('td');
               this.thetitle = c;
               c.style.color = '#ffffff';
               r.appendChild(c);
               if(close){
                  c = document.createElement('td');
                  c.style.width = '21px';
                  var i = document.createElement('img');
                  i.src = 'close_x_up.png';
                  i.onclick = function(e){
                     o[id].destroy();
                  }
                  i.onmouseover = function() { this.src='close_x_over.png'; }
                  i.onmouseout = function() { this.src='close_x_up.png'; }
                  c.appendChild(i);
                  r.appendChild(c);
               }
               this.windowControls.appendChild(r);
               this.r1c2.appendChild(this.windowControls);
            }

            //create status bar
            if(this.windowOptions['status']){
               this.thestatus = document.createElement('span');
               this.r3c2.appendChild(this.thestatus);
            }

            //apply classes
            this.setStyle(style, 'active');
            //apply events
            this.setEvents();
            
            this.thecontent = this.r2c2;
            document.body.appendChild(this.thewindow);
            break;
      }
      
      this.thewindow.style.position = 'absolute';
      this.thewindow.style.left = x;
      this.thewindow.style.top = y;
      this.thewindow.style.zIndex = 1000;
   }
   this.content = function(content){
      this.thecontent.innerHTML = content;
   }
   this.title = function(title){
      this.thetitle.innerHTML = title;
   }
   this.status = function(status){
      this.thestatus.innerHTML = status;
   }
   this.zIndex = function(z){
      this.thewindow.style.zIndex = z;
   }
   this.setStyle = function(style, position){
      switch(style){
         case 'normal':
            switch(position){
               case 'active':
                  this.thewindow.className = 'windowTable';
                  this.windowControls.className = 'titleInside';
                  this.r1c1.className = 'leftTitle';
                  this.r1c2.className = 'title';
                  this.r1c3.className = 'rightTitle';
                  this.r2c1.className = 'leftside';
                  this.r2c2.className = 'content';
                  this.r2c3.className = 'rightside';
                  this.r3c1.className = 'leftStatus';
                  this.r3c2.className = 'status';
                  this.r3c2.style.verticalAlign = 'middle';
                  this.r3c3.className = (this.windowOptions['resize'] == true)? 'rightStatusHandle' : 'rightStatus';
                  this.thestatus.className = 'statusText';
                  break;
               case 'inactive':
               
                  break;
            }
            break;
      }
   }
   this.setEvents = function(){
      switch(this.windowOptions['drag']){
         case 'titlebar':
            this.row1.onmousedown = function(e){
               //get element position
               var oElement = o[id].thewindow;
               var eLeft = 0;   var eTop = 0; var posx; var posy;
               while(oElement != null){
                  eLeft += oElement.offsetLeft;
                  eTop += oElement.offsetTop;
                  oElement = oElement.offsetParent;
               }
               //get mouse position
               if (!e) var e = window.event;
               if (e.pageX || e.pageY)    {
                  posx = e.pageX;
                  posy = e.pageY;
               }
               else if (e.clientX || e.clientY)    {
                  posx = e.clientX + document.body.scrollLeft
                     + document.documentElement.scrollLeft;
                  posy = e.clientY + document.body.scrollTop
                     + document.documentElement.scrollTop;
               }
               //get pointer offset
               this.offset = new Array();
               this.offset['x'] = posx - eLeft;
               this.offset['y'] = posy - eTop;
               this.dodrag = true;
               return false;
            };
            this.row1.onmouseup = function(){
               this.dodrag = false;
            };
            this.row1.onmousemove = function(e){
               if(!this.dodrag){ return; }
               var posx;
               var posy;
               if (!e) var e = window.event;
               if (e.pageX || e.pageY)    {
                  posx = e.pageX;
                  posy = e.pageY;
               }
               else if (e.clientX || e.clientY)    {
                  posx = e.clientX + document.body.scrollLeft
                     + document.documentElement.scrollLeft;
                  posy = e.clientY + document.body.scrollTop
                     + document.documentElement.scrollTop;
               }
               o[id].thewindow.style.left = posx - this.offset['x'];
               o[id].thewindow.style.top = posy - this.offset['y'];
               return false;
            }
            break;
         case 'window':
            this.thewindow.onmousedown = function(e){
               //get element position
               var oElement = o[id].thewindow;
               var eLeft = 0;   var eTop = 0; var posx; var posy;
               while(oElement != null){
                  eLeft += oElement.offsetLeft;
                  eTop += oElement.offsetTop;
                  oElement = oElement.offsetParent;
               }
               //get mouse position
               if (!e) var e = window.event;
               if (e.pageX || e.pageY)    {
                  posx = e.pageX;
                  posy = e.pageY;
               }
               else if (e.clientX || e.clientY)    {
                  posx = e.clientX + document.body.scrollLeft
                     + document.documentElement.scrollLeft;
                  posy = e.clientY + document.body.scrollTop
                     + document.documentElement.scrollTop;
               }
               //get pointer offset
               this.offset = new Array();
               this.offset['x'] = posx - eLeft;
               this.offset['y'] = posy - eTop;
               this.dodrag = true;
            };
            this.thewindow.onmouseup = function(){
               this.dodrag = false;
            };
            this.thewindow.onmousemove = function(e){
               if(!this.dodrag){ return; }
               var posx;
               var posy;
               if (!e) var e = window.event;
               if (e.pageX || e.pageY)    {
                  posx = e.pageX;
                  posy = e.pageY;
               }
               else if (e.clientX || e.clientY)    {
                  posx = e.clientX + document.body.scrollLeft
                     + document.documentElement.scrollLeft;
                  posy = e.clientY + document.body.scrollTop
                     + document.documentElement.scrollTop;
               }
               o[id].thewindow.style.left = posx - this.offset['x'];
               o[id].thewindow.style.top = posy - this.offset['y'];
            }         
            break;
      }
      if(this.windowOptions['resize']){
         this.r3c3.onmousedown = function(e){
            //should I dyanmically spawn a transparent element to catch when the mouse pops outside the boundary?
            this.doresize = true;
            return false;
         }
         this.r3c3.onmouseup = function(){
            this.doresize = false;
         }
         this.r3c3.onmousemove = function(e){
            if(!this.doresize){   return; }
            var posx;
            var posy;
            if (!e) var e = window.event;
            if (e.pageX || e.pageY)    {
               posx = e.pageX;
               posy = e.pageY;
            }
            else if (e.clientX || e.clientY)    {
               posx = e.clientX + document.body.scrollLeft
                  + document.documentElement.scrollLeft;
               posy = e.clientY + document.body.scrollTop
                  + document.documentElement.scrollTop;
            }
            //get element position
            var oElement = o[id].thewindow;
            var eLeft = 0;   var eTop = 0; var posx; var posy;
            while(oElement != null){
               eLeft += oElement.offsetLeft;
               eTop += oElement.offsetTop;
               oElement = oElement.offsetParent;
            }            
            o[id].thewindow.style.width = posx - eLeft + 5;
            o[id].thewindow.style.height = posy - eTop + 5;
            return false;
         }
      }
   }
   
   this.destroy = function(){
      if(this.thewindow == null){
         alert('object does not exist');
         return;
      }
      this.thewindow.parentNode.removeChild(this.thewindow);
      this.thewindow = null;
      o[this.id] = null;
      // if(o[id].thewindow == null){
         // alert('object does not exist');
         // return;
      // }
      // o[id].thewindow.parentNode.removeChild(o[id].thewindow);
      // o[id].thewindow = null;
   }
}


this is the function that creates the object and sets its variables. This is the part a user would have to mess with.
Code:
function create(){
   var item = o.length;
   o[item] = new _window(item);
   o[item].create('normal', 300, 200);
   o[item].title('Window Title');
   o[item].status('Ready');
   o[item].content('This is some content.  We like content.<br>  I may put in an nl2br equivalent.');
}


And for completeness here's my html
Code:
<html>
<head>
   <title>Javascript - Object Window Test</title>
   <script src='js_window.js'></script>
   <link rel='stylesheet' type='text/css' href='main.css'>
</head>
<body>

<a href='javascript: create();'>create</a><br><br>
<div id='test'></div>
</body>
</html>


And css
Code:
.windowTable{      
   border-collapse: collapse;
   position: absolute;
}
.windowTable td,
.windowTable th{      
   padding: 0px 0px 0px 0px;   
   vertical-align: top;
}
.leftTitle{      
   height: 30px;   
   width: 14px;   
   background-image: url('left_title.png');
}
.title{            
   background-image: url('bg_title.png');
   cursor: default;
}
.titleInside{      
   width: 100%;   
   line-height: 26px;   
   vertical-align: middle;
}
.rightTitle{      
   height: 30px;   
   width: 14px;   
   background-image: url('right_title.png');
}
.content{
   background-color: #ffffff;
}
.leftside{
   width: 14px;
   background-image: url('bg_leftside.gif');
}
.rightside{
   width: 14px;
   background-image: url('bg_rightside.gif');
}
.leftStatus{
   height: 30px;
   width: 14px;
   background-image: url('left_status.png');
}
.status{
   background-image: url('bg_status.png');
   height: 30px;
}
.statusText{
   border-left: 1px solid #888888;
   padding: 0px 20px 0px 5px;
   float: left;
}
.rightStatus{
   height: 30px;
   width: 14px;
   background-image: url('right_status.png');
}
.rightStatusHandle{
   height: 30px;
   width: 14px;
   background-image: url('right_status_handle.png');
   cursor: se-resize;
}


Here are the stumbling blocks.

1. After I instantiate an object, how do I associate an onclick event handler (or any other js event) with that specific instance of the object. The this keyword wouldn't work, since that would just refer to the element the event occured against. What I ended up doing was storing all the objects in an array (the o array). I pass an object's id (its position in the o array) to the object which then stores it as a local variable. I also store everything that can be acted on (the window, the title bar, the status bar) as object references inside the object. Then with the onclick, for instance, I just reference objectarray[objectid].element, like o[id].status.innerHTML = "This is the status".

I think that's a pretty big hack. The more elegant way would be for the event to automatically reference the object. Still, even though it's a hack the only thing external to the object is the array. So I guess that's a good thing.

2. Dragging.. for the window resizing and window moving, what I've done is add onmousedown, onmouseup and onmousemove event handlers to the specific sections. The onmouseup sets a local variable that says "drag" or "resize". Without those local cars set the onmousemove just returns... with it set it then does its magic of sampling the location of the window in respect to the mouse cursor and moves/resizes the window object as appropriate.

But it's not very reliable. The problem, I think, is the sampling rate of the onmousemove event. Say it samples even 100ms.. if your mouse traverses off the edge of the onmousemove element in that 100ms the events stop firing. In effect, if you move your mouse too fast, or even if your mouse is just too close to the edge of the element, you lose your "grip". Then when you mouse back over, regardless of the mouse button position, it snaps back on.

There are a few things wrong with this. 1, the onmousemove event handler is always on, it just immediately returns false. This is probably excessive. I'm thinking about dynamically adding the onmousemove event handler when onmousedown happens. Regarding the sample rate problem, I'm thinking that onmousedown I might just dynamically create an element, make it completely transparent and like 200px x 200px, then center it on the mouse. The onmousemove event handler would be attached to this element. Then onmouseup I just remove that element. This gives a bigger hit box.

so anyway, that's where I am. Those are my big problems. It lacks elegance and it acts a bit wonky... the wonky bit at least I think I can fix. It's also pretty limited right now... just one window type. I plan on adding a context menu type window as well. Between that and the fact you can turn on and off various elements (like the status and menu bars) should make it pretty useful. Oh, and right now I don't statically set the height and width but I think I will. This'll allow long, wrapping text without just making the window retardedly wide.

If anyone has any input or improvements I'd love to hear it.

_________________
They who can give up essential liberty to obtain a little temporary safety, deserve neither liberty nor safety.


Fri Apr 25, 2008 7:00 am
Profile WWW
Duke
User avatar

Joined: Mon Mar 31, 2003 8:59 am
Posts: 1358
Location: right behind you
Reply with quote
Post 
You and I have different philosophies on some areas of development. I would much rather use someone else's code than sort all that shit out myself and screw the learning experience. ExtJS is a nice framework. Even if you want to roll your own, it might be worthwhile to look at the interfaces to their GUIs to see how they approached t.


Fri Apr 25, 2008 5:09 pm
Profile YIM WWW
Felix Rex
User avatar

Joined: Fri Mar 28, 2003 6:01 pm
Posts: 16646
Location: On a slope
Reply with quote
Post 
Funny you should mention that, I actually checked it out very recently. Now that my knowledge of js is increasing I can make sense of some of this stuff more. It still drives me nuts when people remove all the newlines from inside the js or have huge compound ternary operations.

Anyway, extjs is cool, but you realize it went GPL lately. So you better keep it physically separated from your own code or your code just got viralled into GPL.

Bleh, to get back to the point, yea, I get told that a lot. I like to learn the stuff myself and build it myself while most people prefer just using what someone else has. I dunno, stuff like phpbb and even worse stuff kinda soured me to 3rd party code. I just feel like I can't trust it without auditing it... and if I'm auditing it I might as well write it. :p

On another note, extjs doesn't actually support what I'm trying to do, I don't think. I mean, it does a LOT of stuff, including some really neat stuff, but I don't think it supports like right-click context menus which I'm starting to get into. Why have an "edit" button for an admin user for every item on a page when I can just set an onrightlick event handler to the element?

Anyway, hi Pig! BTW, I hope you don't mind, but I used you and Ox in my c# tutorial as examples. :twisted:

_________________
They who can give up essential liberty to obtain a little temporary safety, deserve neither liberty nor safety.


Sat Apr 26, 2008 1:22 pm
Profile WWW
Duke
User avatar

Joined: Mon Mar 31, 2003 8:59 am
Posts: 1358
Location: right behind you
Reply with quote
Post 
Grabby grabby


Sat Apr 26, 2008 9:46 pm
Profile YIM WWW
Emperor
User avatar

Joined: Wed Apr 16, 2003 1:25 am
Posts: 2560
Reply with quote
Post 
Satis wrote:
Now that my knowledge of js is increasing I can make sense of some of this stuff more.

The next thing to do is to learn to make code more compact and easier to read. We spoke on that matter on several occasions. Here I do not mean on removing all LFs or using huge ternary ?: operators.

_________________
++


Thu Jun 26, 2008 3:57 am
Profile WWW
Felix Rex
User avatar

Joined: Fri Mar 28, 2003 6:01 pm
Posts: 16646
Location: On a slope
Reply with quote
Post 
I actually haven't touched this code in quite awhile. Perhaps I'll post the code for the IM app I've been working on. Though I did learn something recently about some issues with js object and the this keyword.

Meh...without my code in front of me, I can't elaborate too much, but something like

var that = this;

in the class properties you can easily reference your own object without needing to resort to external arrays.

_________________
They who can give up essential liberty to obtain a little temporary safety, deserve neither liberty nor safety.


Thu Jun 26, 2008 9:31 pm
Profile WWW
Display posts from previous:  Sort by  
Reply to topic   [ 6 posts ] 

Who is online

Users browsing this forum: No registered users and 2 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Jump to:  
cron
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group.
Designed by STSoftware.