GaMMA

Digging in my archives I found a backup of my personal home page from 2000 to 2003, and through a little work of archeology and restoration, I made it work in our modern world of 2023.

Back in those days, I was starting my exploration of computer history, and the NeXT computer appeared everywhere as one of the major inspirations of our modern computing experience. Hence, I decided to create GaMMA1, a website I described as “a DHTML experience”.

This ancestor of single-page applications emulates a NeXT desktop environment2, where people could click on the menu or on the dock icons, and then the content would dynamically load and appear on a draggable <div> object. This was 5 years before AJAX was born!

How did this work? Well, I used a hidden <iframe> to load children pages, who would call some JavaScript on the parent page to indicate that they had finished loading. And it worked!

Back to the Future

But running GaMMA in our current world was a bit problematic:

ns4 = (document.layers)? true:false
ie4 = (document.all)? true:false

So how did I restore this website?

1. Removing Server-Side Code

I started by the easiest part, removing all the VBScript and Classic ASP code. I thought about rewriting that in PHP, but it was just used to store information about the number of visitors and to read and write data in the guestbook (remember guestbooks?)

So instead, I just stripped all of that server code away. And I removed the Microsoft Access databases from the equation, too. I renamed the files from *.asp to *.html.

2. Replacing Dreamweaver-Generated Code

I had to rewrite and replace quite a bit of the client-side JavaScript code. First, I decided to simplify those boolean values at the top of the script:

//ns4 = (document.layers)? true:false
//ie4 = (document.all)? true:false
ns4 = true
ie4 = false

Yes, I use Firefox, which it is technically a direct heir to Netscape, so ns4 = true and voilà.

The biggest part of the JavaScript code was automatically generated by Dreamweaver, and it depended on either the document.layers property of Netscape or the document.all property of Internet Explorer… both of which are deprecated today.

But this is important: both were dictionaries, not functions, and both roughly worked like the document.getElementById() function nowadays. So the trick was to use an ES2015 Proxy object to create the polyfill of the year:

// Courtesy of
// https://stackoverflow.com/a/20147219/133764
document.layers = new Proxy({}, {
    get: function(target, name, receiver) {
        if (!(name in target)) {
            return document.getElementById(name);
        }
    }
});

There’s also an autogenerated function called MM_findObj() used by Dreamweaver, quite literally doing what document.getElementById() does nowadays, so:

/*
function MM_findObj(n, d) { //v3.0
    var p,i,x;  if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
    d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
    if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
    for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=MM_findObj(n,d.layers[i].document); return x;
}
*/
function MM_findObj(n, d) {
    return document.getElementById(n);
}

Other functions, like MM_showHideLayers(), worked without problems; some parts of the standards we use today haven’t changed in 24 years.

3. Dynamically Loading Data

If you click on menus or dock items, the page loads its contents dynamically from the server. In the original GaMMA, there were two APIs at play:

//Load external HTML file in specific layer
function loadSource(id,nestref,url) {
    if(!loadingRightNow) {
        loadingRightNow = true;
        MM_showHideLayers('loading','','show');
        if (ns4) {
            var lyr = (nestref)? eval('document.'+nestref+'.document.'+id) : document.layers[id]
            lyr.load(url,lyr.clip.width)
        }
        else if (ie4) {
            parent.bufferFrame.document.location = url
        }
        loadingRightNow = false;
    }
}

(Pay attention to the loadingRightNow variable and its feeble attempts to prevent this code from stepping on its own feet.)

When the child page had loaded into the <iframe>, it would call the loadSourceFinish() function on the parent, which is something only Internet Explorer needed.

//End of External HTML loading
function loadSourceFinish(id) {
    if (ie4) document.all[id].innerHTML = parent.bufferFrame.document.body.innerHTML;
    MM_showHideLayers('loading','','hide');
    MM_showHideLayers('winMini','','hide','win','','show','contents','','show');
}

But we’re in 2023, and nowadays such an operation is trivial, thanks to the fetch() API and the async and await keywords, coupled with the DOMParser API and its capacity to parse content from text strings:

async function loadSource(id, nestref, url) {
    MM_showHideLayers('loading','','show')
    const data = await fetch(url)
    const html = await data.text()
    let parser = new DOMParser()
    let doc = parser.parseFromString(html, "text/html")
    document.win.innerHTML = doc.querySelector('body').innerHTML
    MM_showHideLayers('loading','','hide')
    MM_showHideLayers('winMini','','hide','win','','show','contents','','show')
}

Gotta love being a web developer in 2023. Even the semicolons are gone. And let’s be honest, the snippet above feels a lot like HTMX or Hotwire at work.

4. Drag-and-Drop

Let us now talk about the horrendous MM_dragLayer() function that Dreamweaver inserted in pages to enable drag and drop of <div> and <layer> objects in 1999. I’m including here the first few lines just for historical purposes:

function MM_dragLayer(objName,x,hL,hT,hW,hH,toFront,dropBack,cU,cD,cL,cR,targL,targT,tol,dropJS,et,dragJS) { //v3.0
  //Copyright 1998 Macromedia, Inc. All rights reserved.
  var i,j,aLayer,retVal,curDrag=null,NS=(navigator.appName=='Netscape'), curLeft, curTop;
  if (!document.all && !document.layers) return false;
  retVal = true; if(!NS && event) event.returnValue = true;
  if (MM_dragLayer.arguments.length > 1) {
    curDrag = MM_findObj(objName); if (!curDrag) return false;
    if (!document.allLayers) { document.allLayers = new Array();
      with (document) if (NS) { for (i=0; i<layers.length; i++) allLayers[i]=layers[i];
        for (i=0; i<allLayers.length; i++) if (allLayers[i].document && allLayers[i].document.layers)
          with (allLayers[i].document) for (j=0; j<layers.length; j++) allLayers[allLayers.length]=layers[j];
      } else for (i=0;i<all.length;i++) if (all[i].style&&all[i].style.position) allLayers[allLayers.length]=all[i];}
    curDrag.MM_dragOk=true; curDrag.MM_targL=targL; curDrag.MM_targT=targT;
// …

Yes, it’s minified and everything. And, yeah, the complete function is about 5 times the length of the snippet above. Good luck understanding what’s going on.

(Kids, this was 7 years before jQuery, Prototype, script.aculo.us, and even 5 years before Firebug introduced console.log() to the world. Back in those days, we could only alert() our way into code.)

Instead, today we would add the draggable="true" attribute to the <div>s we want to move, and then use the Drag & Drop API. I found this code to make things even simpler:

function dragElement(elmnt) {
  var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0, elementMoving;
  if (document.getElementById(elmnt.id + "header")) {
    // if present, the header is where you move the DIV from:
    document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
  } else {
    // otherwise, move the DIV from anywhere inside the DIV:
    elmnt.onmousedown = dragMouseDown;
  }

  function dragMouseDown(e) {
    e = e || window.event;
    e.preventDefault();
    // get the mouse cursor position at startup:
    pos3 = e.clientX;
    pos4 = e.clientY;
    document.onmouseup = closeDragElement;
    // call a function whenever the cursor moves:
    document.onmousemove = elementDrag;
  }

  function elementDrag(e) {
    e = e || window.event;
    e.preventDefault();
    // calculate the new cursor position:
    pos1 = pos3 - e.clientX;
    pos2 = pos4 - e.clientY;
    pos3 = e.clientX;
    pos4 = e.clientY;
    // set the element's new position:
    elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
    elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
  }

  function closeDragElement(event) {
    // stop moving when mouse button is released:
    document.onmouseup = null;
    document.onmousemove = null;
    if (elmnt.id === 'winMini') {
      repositionWinBar('winMini', 'win');
    }
    if (elmnt.id === 'win') {
      repositionWinBar('win', 'winMini');
    }
  }
}

The new drag-and-drop code is not only easier to read, it is also much more flexible, allowing users to drag and drop not only by holding the title bar, but any part of the <div> they want.

5. Flash!

And here comes the most incredible part. Back in those days, I produced a lot of Macromedia Flash movies, and I still have plenty of those .swf files around. The problem is… well, Flash is completely gone:

Since Adobe no longer supports Flash Player after December 31, 2020, and blocked Flash content from running in Flash Player beginning January 12, 2021, Adobe strongly recommends all users immediately uninstall Flash Player to help protect their systems.

Those movies are lost. Or are they? Enter the Ruffle project. It is a little marvel of modern technology: written in Rust, it generates WebAssembly code that emulates a Flash player. You read right. And to prove it, I’ve re-enabled the Flash movies page in GaMMA completely3.

One More Thing

Actually the original GaMMA included a live web chat application, using a Microsoft Access database (again) and live-reloading messages in spite of the fact that the WebSockets and Server Sent Events APIs were literally decades away.

How did it work? Well, it simply refreshed the page periodically:

<META HTTP-EQUIV="Refresh" CONTENT="30; URL=bottom.asp">

People would write messages on the top part of the <frameset> and the bottom would refresh itself every 30 seconds. Boom, you have yourself a chat. Maybe I’ll rewrite it in the future, but not now.

Conclusion

No wonder, my introduction in the GaMMA website reads:

I hope this will be the last HTML page I’ll ever do… even if this one is more of a JavaScript-driven one… Maybe “Delta” will be a Flash website?

Famous last words. Actually, Flash went the way of the Dodo, and thankfully web standards evolved in wonderful ways. Yes, it’s still a PITA to create websites, but when I look at the past, I’m thankful for the many wonderful APIs we have on our browsers these days.

GaMMA is available in this very website for your viewing and clicking pleasure.

Update, 2024-03-29: If you like NeXT and would like to use the actual thing, instead of this puny recreation, head to Infinite Mac instead!


  1. The name “GaMMA” means it was my third personal homepage, and the lowercase “a” is a nod to the “e” in NeXT. ↩︎

  2. Be mindful that I had never used a NeXT computer at that moment, so my recreation of the UI and its interactivity was based on my perceptions after seeing screenshots found all over the web. It was not meant as a faithful recreation, but more as an exercise in style, and a technical challenge in web development. ↩︎

  3. I’ll write more about Ruffle and Flash in an upcoming article↩︎