Tangled in the Threads

Jon Udell, August 1, 2001

The Universal Canvas Revisited

Approaches to live editing of Web pages

Readers write to share their methods of experimentation and their results

An earlier column entitled The Universal Canvas has drawn some really interesting commentary from readers. Clearly there's strong interest in live editing of Web pages, and there are a number of different R&D efforts underway. As several folks pointed out, the DHTML edit control, which was the basis of the solutions described in the first column, has been superseded in IE 5.5 by capabilities that are embedded natively within the browser.

Andrew Mayo:

Your article on the DHTML editor was fascinating but were you aware that this functionality has largely been subsumed into the browser as of IE 5.5?

By simply marking any HTML component, such as a DIV (or even a SPAN) with "contenteditable=true", you will find that it becomes editable. Standard Word shortcuts such as ctrl-b, ctrl-i etc. allow you to bold and italicise text.

By embedding the IE browser ActiveX control in an application you can then provide the user with an immensely rich UI as part of a larger application. You create the HTML to render the page 'on the fly' inside a VB application, for example, and then you simply insert this dynamically into the body of the page being shown by the browser control.

It is true that you can also build these sorts of applications using just JScript inside the browser but debugging is much weaker than a hybrid VB-browser solution, where the browser effectively provides the display tier only.

Behaviours are useful in this regard because they allow you to then add editing functionality within the browser canvas in a modular and object-oriented way.

Consider, for example, the following page:

<html>
<body>
<div style=behavior:url("resize.htc")>this is resizable</div>
</body>
</html>

The behaviour "resize.htc", shown below, permits this div to be dynamically repositioned and resized, thus making the browser its own editor. To see this, double-click on the div and then resize and reposition with the mouse.

Behaviours can be combined to provide modular enhancements to any HTML element. For example, I have a drag behaviour that allows an element (and its children) to be copied by dragging and dropping either within the same page or a different page.

The resize and drag behaviours can be applied either separately or together to any element, or via CSS and style sheets.

You can see that with a combination of behaviours and contenteditable, you can build web pages that are 'self-editing' in all sorts of sophisticated ways. Since no ActiveX controls are used, this technology will work over firewall boundaries.

This technology is, if anything, even more fascinating than add-on components because it is 100% natively supported inside IE.

PS: behaviours also make excellent UI components. I have developed several of these, including a full treeview and a tabbed dialogue box that are completely DHTML and need no ActiveX components. Using VML it is also possible to do some amazing graphical stuff, such as ERDs etc, with smart routing lines which always join via the shortest path, and so on.

Here's the code that Andrew sent me. To try it, save it as the file resize.htc in the same directory as the test page which contains the above HTML fragment. Notes Andrew: "Some of the code supports functionality which is not used in the simple test page. A canonical resize behaviour could be somewhat simpler and smaller than this example."

Andrew Mayo's resize.htc, a "behavior" for moving and resizing HTML elements

<public:component name=resize urn=resize id=resize>
     <attach event = "onmousedown" onevent="mdown()"/>
     <attach event = "onmousemove" onevent="mmove()"/>
     <attach event = "onmouseup" onevent="mup()"/>
     <attach event = "ondblclick" onevent="mdblclick()"/>
     <attach event = "ondragstart" onevent="mdrag()"/>
     <property name ="locked" put="SetLocked"/>
     <property name ="nodrag" put="SetDraggable"/>
     <method name="select" internalname="SetSelected"/>
     <event name = "onselect" id="evtSelect"/>
</public>

<script>
     var ldx;
     var rdx;
     var tdy;
     var bdy;
     var edge;
     var srcElt;
     var mlastX;
     var mlastY;
     var bLock;
     var bNoDrag;
     var oBorderStyle;
     var oBorderWidth;
     var oOverFlow;
     var dragImage;
     var eltClone;
     var me;

public_description = new Create();

function Create()

     {
     mlastX = null;
     mlastY = null;

     window.document.attachEvent("onmouseup",wmup);
     me=element;
     me.sel=false;
     }

function SetLocked(bLockFlag)
     {
     if (bLockFlag)
          {
          bLock=true;
          }
     else
          {
          bLock=false;
          }
     }

function SetDraggable(bDragFlag)
     {
     if (bDragFlag)
          {
          bNoDrag=true;
          }
     else
          {
          bNoDrag=false;
          }
     }

function mdown()
     {
     if (bLock || !me.sel || window.event.button == 2) return;
     // check for either resize or drag operation, depending on mouse button.
     if (window.event.button == 1)
          {
          mlastX = window.event.clientX;
          mlastY = window.event.clientY;
          srcElt=element;
          srcElt.setCapture();
          }


     }
function mmove()
     {
     if (bLock || !me.sel) return;
     if (mlastX)
          // move or resize. If edge is set, resize else move
          {

          switch (edge)
               {
               case "sw":
                    //south resize

srcElt.style.posHeight+=parseInt(window.event.clientY)-parseInt(mlastY);
                    if (edge != "sw") break;

               case "w":
               case "nw":
                    // west resize

srcElt.style.posLeft+=parseInt(window.event.clientX)-parseInt(mlastX);

srcElt.style.posWidth-=parseInt(window.event.clientX)-parseInt(mlastX);
                    if (edge != "nw") break;

               case "n":
               case "ne":
                    // north resize

srcElt.style.posTop+=parseInt(window.event.clientY)-parseInt(mlastY);

srcElt.style.posHeight-=parseInt(window.event.clientY)-parseInt(mlastY);
                    if (edge != "ne") break;

               case "e":
               case "se":
                    // east resize

srcElt.style.posWidth+=parseInt(window.event.clientX)-parseInt(mlastX);
                    if (edge != "se") break;

               case "s":
                    //south resize

srcElt.style.posHeight+=parseInt(window.event.clientY)-parseInt(mlastY);
                    break;

               default:
                    //move

srcElt.style.posLeft+=parseInt(window.event.clientX)-parseInt(mlastX);

srcElt.style.posTop+=parseInt(window.event.clientY)-parseInt(mlastY);
               }
               mlastX=window.event.clientX;
               mlastY=window.event.clientY;
          }
     else
          {
          if (srcElt == null)
               {
               srcElt = element;
               }
          // if moving with button up, set size of sweet spot
          spot = 12;
          // set cursor according to edge proximity. Note we use offset and client
coords because we want the
          // physical object size, not its logical size.
          tdy =
(Math.abs(window.event.clientY-srcElt.offsetTop+window.document.body.scrollTop) <
spot) ? "n" : "";
          bdy=
(Math.abs(window.event.clientY-(srcElt.offsetTop-window.document.body.scrollTop+srcElt.clientHeight))
< spot) ? "s" : "" ;
          ldx =
(Math.abs(window.event.clientX-srcElt.offsetLeft+window.document.body.scrollLeft) <
spot) ? "w" : "";
          rdx =
(Math.abs(window.event.clientX-(srcElt.offsetLeft-window.document.body.scrollLeft+srcElt.clientWidth))
< spot) ? "e" : "" ;
          // set edge variable if we are at any resize edge.
          edge = tdy + bdy + ldx + rdx;
          // next operation might fail, so trap
          try
               {
               srcElt.style.cursor=(edge != "") ? tdy + bdy + ldx + rdx + "-resize" :
"hand";
               }
          catch(e)
               {
               // just don't do anything
               }
          }
     }

function mup()
     {
     if (bLock || !me.sel) return;
     window.document.releaseCapture();
     srcElt=null;
     element.style.cursor="default";
     mlastX = null;
     mlastY = null;
     }
function wmup()
     {
     mup();
     }

function mdblclick()
     {
     // toggle resize mode on/off
     if (bLock) return;
     SetSelected(!me.sel);
     // fire onselect event for calling programs to use
     o = createEventObject();
     evtSelect.fire(o);
     window.event.cancelBubble=true;
     }

function SetSelected(bSel)
     {
     me.sel=bSel;
     if (bSel)
          {
          // copy the object as it is now. The cloneNode method proved unreliable
          // if the object contains VML children so we clone the outer container,
          // which must be a DIV, then fill its innerHTML with the container's
          // innerHTML.
          // If the selected element is a line, we place a DIV around it
          // so that it can be handled like a normal 2D element. When we deselect
          // the line, we remove the DIV
          eltClone = me.cloneNode(false);
          eltClone.innerHTML=me.innerHTML;
          // set expando property 'resizing' so that other behaviours can cooperate
          window.document.resizing=true;
          // get current element properties
          oBorderStyle=me.style.borderStyle;
          oBorderWidth=me.style.borderWidth;
          oOverFlow=me.style.overflow;
          // we get rid of the scrollbars because they can cause mouse capture problems
          me.style.borderStyle="solid";
          me.style.borderWidth=3;
          me.style.overflow='hidden';
          // check for any positional params that are missing and set them. Also make position absolute
          if (me.style.position != 'absolute') me.style.position='absolute';
          if (me.style.posLeft != me.offsetLeft) me.style.posLeft = me.offsetLeft;
          if (me.style.posTop != me.offsetTop) me.style.posTop = me.offsetTop;
          if (me.style.posWidth != me.offsetWidth) me.style.posWidth =
me.offsetWidth;
          if (me.style.posHeight != me.offsetHeight) me.style.posHeight =
me.offsetHeight;
          if (!dragImage && !bNoDrag)
               {
               // insert the drag image if not already inserted
               dragImage = document.createElement("<img id=dragimg
style='z-index:1;position:absolute;left:0;top:0;height:100%;width:100%' src
='pix1.gif'>");
               me.insertBefore(dragImage);
               }
          }
     else
          {
          me.style.borderStyle=oBorderStyle;
          me.style.borderWidth=oBorderWidth;
          me.style.overflow=oOverFlow;
          window.document.resizing=false;
          if (dragImage)
               {
               dragImage.removeNode();
               dragImage = null;
               }
          }
     }


function mdrag()
     {
     // drag the selected element. Set the clone element's dimensions first, in case
we modified the width/height
     // while we were in reposition mode. Note we check there *is* a clone element
     // - someone could have tried dragging a piece of text instead!
     // If this is a 'step' element (flowchart) then we create an empty new element
     // rather than literally copying the HTML. The step behaviour will fill in the
rest

     if (eltClone)
          {
          eltClone.style.posWidth=element.style.posWidth;
          eltClone.style.posHeight=element.style.posHeight;
          window.event.dataTransfer.effectAllowed="copy";
          if (eltClone.className == 'step')
               {
               var s = '<DIV class=step
style="position:absolute;height:'+eltClone.style.posHeight+';width:'+eltClone.style.posWidth+'"></DIV>';
               window.event.dataTransfer.setData("text",s);
               }
          else
               {
               window.event.dataTransfer.setData("text",eltClone.outerHTML);
               }
          }
     }
</script>

Here's a sequence of screenshots that demonstrate how this works.

1. Initial state

2. Double-clicking activates resize (and move) capability

3. Element moved to new location

Thanks, Andrew, for sharing your insights and for this intriguing demo!

DHTML editing control vs. MSHTML editor

I'll admit to having been unclear about the distinction between the older DHTML editing control, and the newer IE 5.5 capabilities that Andrew demonstrates. Here's what Microsoft says about it:

What Ever Happened to the DHTML Edit Control?

The DHTML editing component is just a wrapper for the MSHTML editor. It was developed back in the days of Internet Explorer 4.0 to supplement the limited features available in the editor at that time. The DHTML edit control is a Microsoft® ActiveX® control that wraps the DHTML editing component and provides some additional functionality such as saving and loading documents.

The MSHTML editor has evolved a lot since Internet Explorer 4.0, and for most hosts, the DHTML editing component is no longer necessary. Going forward, the DHTML editor will no longer be updated, so hosts are encouraged to use the MSHTML editor directly unless they require a specific feature that’s not yet supported. The only remaining features provided by the DHTML editing component that aren’t supported in the MSHTML editor in Internet Explorer 5.5 are source preservation, z-index operations, and some table operations such as insertion and deletion of tables, rows, and columns. Source preservation is only required by Web authoring applications that provide an HTML Source View, and z-index operations are easily performed through the DOM. In Internet Explorer 5.5, hosts can write their own custom extensions to perform whatever table editing operations their application requires.

In newsgroups where these technologies are discussed, there's some reluctance to abandon the DHTML editing component. Partly that's because it delivered a set of high-level capabilities -- table editing, loading and saving documents -- which must otherwise be reimplemented in script. And partly it's because the MSHTML documentation is roundly criticized as inadequate. "I got past frustrated and irritated over a month ago," groused one person. "I'm working on frothing at the mouth this week. I think I'll just microwave my MSDN subscription DVD next week." Said another: "With some deep digging, you can do all that you need to do. But you're right, the docs are not clear AT ALL."

Of course, the DHTML editing component wasn't trouble-free either. As I noted in the earlier column, Ektron worked hard to improve its basic capabilities, adding support for things like XHTML output and internationalization. Another correspondent tackled some of these same issues when building a WYSIWYG editor for Zope.

Satheesh Babu:

For Joe User, Zope as a CMS is still daunting. So I had to write an application where people get to choose templates and fill out the content. For this, I made use of DHTML editor. But it has some problems. The insert image/link controls actually browse the client's PC. So, I had to do lot of customizations to develop custom controls that browse from the server.

It also produces horrible HTML, by the way. You need to clean up the IMG tags, because the control puts STYLE="WIDTH X; HEIGHT Y" instead of height and width attributess, and Netscape 4.x can't understand this.

Finally, I hooked it to a Tidy SOAP service that can Tidy your HTML real time. That's very useful, since most of our users copy-paste from MS Word, and I really needed to get all the font tags removed so that our CSS gets applied.

In the end, Satheesh decided to focus future effort on MSHTML.

Satheesh Babu:

I think MSHTML is the way to go because:

  1. It is much more light (whereas DEC can result in a really big page)
  2. MS says it is not going to support DEC any more
  3. Its API is simpler, so programming is easy (or more difficult as you see it).
  4. After 8 months of production use of our CMS, we are finding that more and more people - though they liked WYSIWYG initially - are getting tired of having to format their own content.

Here's what I'm thinking of now:

  1. XML source documents/templates
  2. XML schema attached to that
  3. Users pick a template and a generic editor screen pops up by parsing the XML Schema and shows a form where they enter title, keywords, property X... and some divisions/paragraphs. We plan to provide only very basic formatting in these divisions (mostly B,I,U,A HREF, UL/OL, LI. (This makes the MSHTML component very light weight, and for people with other browsers, it is not that bad).
  4. People select the XSL they want to use and that's it. (MSXML parser really works well from Zope via pythoncom).

Lightweight collaborative editing

Another take on the universal canvas came from Michael Rees, who referred me to his paper, Implementing Responsive Lightweight In-page Editing, and to an implementation that you can try online, or download and explore. Note that if you do download the code, you'll discover that it uses encryption to obfuscate the JavaScript code, which resides in a comment that begins //**Start Encode**. Curious about this mechanism, which I hadn't seen before, I searched Google and found -- on the first page -- both a description of the tool used to do the encryption, and also pointers to several tools that defeat it. I asked Michael about this, and he replied: "Please don't hold back using my stuff because others can now decode it. I am quite happy for people to see my scripts, and hopefully improve upon them. The feedback would be useful."

As I mentioned in the earlier column, you can create multiple instances of Ektron's eWebEditPro on a single web page. Michael's approach works in a similar fashion. The page becomes a framework that contains a set of editable parts. It's even more interesting that that, though, because each mini-editor isn't just restricted to its point of activation. As the basic editing demo illustrates, you can also create and reorder elements. Michael's paper uses the term i-grain (for "information grain") to describe the nuggets of content that can be edited and rearranged in this structure.

Why is this kind of thing important? As those of us who advocate for a two-way Web never tire of pointing out, the Web was originally meant to be a writable as well as a readable surface. It was also originally meant to be a groupware application, enabling teams to collaborate in direct and immediate ways.

None of us has yet to really inhabit the kind of of environment that Tim Berners-Lee foresaw when he invented the Web. Although live editing of shared documents will, I am sure, play a central role in that environment, we've yet to confront -- let alone resolve -- the many issues this technology will raise. But there's only one way to find out: do the experiment. That's exactly what Andrew, Satheesh, and Michael are up to. Thanks for sharing your research and your results so far. I can't wait to see how this will all turn out!


Jon Udell (http://udell.roninhouse.com/) was BYTE Magazine's executive editor for new media, the architect of the original www.byte.com, and author of BYTE's Web Project column. He is the author of Practical Internet Groupware, from O'Reilly and Associates. Jon now works as an independent Web/Internet consultant. His recent BYTE.com columns are archived at http://www.byte.com/index/threads

Creative Commons License
This work is licensed under a Creative Commons License.