Commits

Zak Patterson committed a339740

fixing issue #40

Comments (0)

Files changed (2)

semanticeditor/static/semanticeditor/javascript/wymeditor/plugins/semantic/wymeditor.semantic.js

  * elements for each section.  Depends on a server-side backend to do
  * parsing and provide list of allowed CSS classes.
  */
+(function(jQuery){
+  function PresentationControls(wym, opts) {
+      this.wym = wym;
+      this.opts = opts;
+      this.name = wym._element.get(0).name;
+      // availableStyles: an array of dictionaries corresponding to PresentationInfo objects
+      // (PresentationClass objects specifically)
+      this.availableStyles = new Array();
+      // presentationInfo: a dictionary of { sectId : [PresentationInfo] }
+      this.presentationInfo = {};
 
-function PresentationControls(wym, opts) {
-    this.wym = wym;
-    this.opts = opts;
-    this.name = wym._element.get(0).name;
-    // availableStyles: an array of dictionaries corresponding to PresentationInfo objects
-    // (PresentationClass objects specifically)
-    this.availableStyles = new Array();
-    // presentationInfo: a dictionary of { sectId : [PresentationInfo] }
-    this.presentationInfo = {};
+      // commands: an array of dictionaries corresponding to PresentationInfo objects
+      // (PresentationCommand objects specifically)
+      this.commands = new Array();
+      // commandDict: a dictionary mapping command name to PresentationInfo object.
+      this.commandDict = {};
 
-    // commands: an array of dictionaries corresponding to PresentationInfo objects
-    // (PresentationCommand objects specifically)
-    this.commands = new Array();
-    // commandDict: a dictionary mapping command name to PresentationInfo object.
-    this.commandDict = {};
+      // a selector which matches any command block.  Filled in later.
+      this.allCommandSelectors = "";
 
-    // a selector which matches any command block.  Filled in later.
-    this.allCommandSelectors = "";
+      // the node of the current block level element being edited.
+      this.currentNode = null;
 
-    // the node of the current block level element being edited.
-    this.currentNode = null;
+      // Need to sync with presentation.py
+      this.blockdefSelector = "h1,h2,h3,h4,h5,h6,p,ol,ul,blockquote,li,pre";
 
-    // Need to sync with presentation.py
-    this.blockdefSelector = "h1,h2,h3,h4,h5,h6,p,ol,ul,blockquote,li,pre";
+      this.setupControls(jQuery(wym._box).find(".wym_area_right"));
+  }
 
-    this.setupControls(jQuery(wym._box).find(".wym_area_right"));
-}
+  // ---- Setup and loading ----
 
-// ---- Setup and loading ----
+  // -- Setup static controls
 
-// -- Setup static controls
+  PresentationControls.prototype.setupControls = function(container) {
+      var idPrefix = "id_prescontrol_" + this.name + "_";
+      var previewButtonId = idPrefix + 'previewbutton';
+      var showStylesButtonId = idPrefix + 'showstyles';
+      var previewBoxId = idPrefix + 'previewbox';
+      var hidePreviewButtonId = idPrefix + 'hidepreviewbutton';
+      var cleanHtmlButtonId = idPrefix + 'cleanhtmlbutton';
+      var editorNewTabId = idPrefix + 'editornewtab';
+      var self = this;
 
-PresentationControls.prototype.setupControls = function(container) {
-    var idPrefix = "id_prescontrol_" + this.name + "_";
-    var previewButtonId = idPrefix + 'previewbutton';
-    var showStylesButtonId = idPrefix + 'showstyles';
-    var previewBoxId = idPrefix + 'previewbox';
-    var hidePreviewButtonId = idPrefix + 'hidepreviewbutton';
-    var cleanHtmlButtonId = idPrefix + 'cleanhtmlbutton';
-    var editorNewTabId = idPrefix + 'editornewtab';
-    var self = this;
+      // Create elements
+      container.prepend(
+          "<div class=\"prescontrol wym_panel\">" +
+              "<h2>Semantic editor</h2>" +
+              "<input type=\"submit\" id=\"" + showStylesButtonId + "\" /><br/>" +
+              "<input type=\"submit\" value=\"Clean pasted HTML\" id=\"" + cleanHtmlButtonId  +  "\" /><br/>" +
+              "<input type=\"submit\" value=\"Preview\" id=\"" + previewButtonId + "\" /><br/>" +
+              "<input type=\"submit\" value=\"Edit in new tab\" id=\"" + editorNewTabId + "\" /<br/>" +
+              "<div class=\"prescontrolerror\" id=\"" + idPrefix + "errorbox" + "\"></div>" +
+          "</div>");
 
-    // Create elements
-    container.prepend(
-        "<div class=\"prescontrol wym_panel\">" +
-            "<h2>Semantic editor</h2>" +
-            "<input type=\"submit\" id=\"" + showStylesButtonId + "\" /><br/>" +
-            "<input type=\"submit\" value=\"Clean pasted HTML\" id=\"" + cleanHtmlButtonId  +  "\" /><br/>" +
-            "<input type=\"submit\" value=\"Preview\" id=\"" + previewButtonId + "\" /><br/>" +
-            "<input type=\"submit\" value=\"Edit in new tab\" id=\"" + editorNewTabId + "\" /<br/>" +
-            "<div class=\"prescontrolerror\" id=\"" + idPrefix + "errorbox" + "\"></div>" +
-        "</div>");
+      jQuery("body").append("<div style=\"position: absolute; display: none\" class=\"previewbox\" id=\"" +  previewBoxId + "\">" +
+                            "<div class=\"closebar\">" +
+                            "<input type=\"submit\" value=\"Close\" id=\"" + hidePreviewButtonId + "\">" +
+                            "</div>" +
+                            "<div class=\"content\"></div>" +
+                            "</div>");
 
-    jQuery("body").append("<div style=\"position: absolute; display: none\" class=\"previewbox\" id=\"" +  previewBoxId + "\">" +
-                          "<div class=\"closebar\">" +
-                          "<input type=\"submit\" value=\"Close\" id=\"" + hidePreviewButtonId + "\">" +
-                          "</div>" +
-                          "<div class=\"content\"></div>" +
-                          "</div>");
+      this.classList = jQuery(this.wym._options.classesSelector).find("ul");
+      this.commandList = jQuery(this.wym._options.layoutCommandsSelector).find("ul");
+      this.errorBox = jQuery('#' + idPrefix + "errorbox");
+      this.previewButton = jQuery('#' + previewButtonId);
+      this.previewBox = jQuery('#' + previewBoxId);
+      this.hidePreviewButton = jQuery('#' + hidePreviewButtonId);
+      this.showStylesButton = jQuery('#' + showStylesButtonId);
+      this.cleanHtmlButton = jQuery('#' + cleanHtmlButtonId);
+      this.editorNewTabButton = jQuery('#' + editorNewTabId);
 
-    this.classList = jQuery(this.wym._options.classesSelector).find("ul");
-    this.commandList = jQuery(this.wym._options.layoutCommandsSelector).find("ul");
-    this.errorBox = jQuery('#' + idPrefix + "errorbox");
-    this.previewButton = jQuery('#' + previewButtonId);
-    this.previewBox = jQuery('#' + previewBoxId);
-    this.hidePreviewButton = jQuery('#' + hidePreviewButtonId);
-    this.showStylesButton = jQuery('#' + showStylesButtonId);
-    this.cleanHtmlButton = jQuery('#' + cleanHtmlButtonId);
-    this.editorNewTabButton = jQuery('#' + editorNewTabId);
+      this.setupCssStorage();
 
-    this.setupCssStorage();
+       // Initial set up
 
-     // Initial set up
+      // retrieveCommands() must come before retrieveStyles(), due to use
+      // of calculateSelectors() which needs .commands to be set.
+      this.retrieveCommands(); // async=False
+      this.retrieveStyles(); // async=False
+      this.separatePresentation();
 
-    // retrieveCommands() must come before retrieveStyles(), due to use
-    // of calculateSelectors() which needs .commands to be set.
-    this.retrieveCommands(); // async=False
-    this.retrieveStyles(); // async=False
-    this.separatePresentation();
-
-    this.previewButton.click(function(event) {
-                                 self.showPreview();
-                                 return false;
-                              });
-    this.hidePreviewButton.click(function(event) {
-                                     self.previewBox.hide();
+      this.previewButton.click(function(event) {
+                                   self.showPreview();
+                                   return false;
+                                });
+      this.hidePreviewButton.click(function(event) {
+                                       self.previewBox.hide();
+                                       return false;
+                                   });
+      // start with styles hidden
+      this.showStyles(false);
+      this.showStylesButton.toggle(function(event) {
+                                       self.showStyles(true);
+                                       return false;
+                                   },
+                                   function(event) {
+                                       self.showStyles(false);
+                                       return false;
+                                   });
+      this.cleanHtmlButton.click(function(event) {
+                                     self.cleanHtml();
                                      return false;
                                  });
-    // start with styles hidden
-    this.showStyles(false);
-    this.showStylesButton.toggle(function(event) {
-                                     self.showStyles(true);
-                                     return false;
-                                 },
-                                 function(event) {
-                                     self.showStyles(false);
-                                     return false;
-                                 });
-    this.cleanHtmlButton.click(function(event) {
-                                   self.cleanHtml();
-                                   return false;
-                               });
-    this.editorNewTabButton.click(function(event) {
-                                      var url = self.wym._box.get(0).ownerDocument.URL;
-                                      window.open(url, '_blank');
-                                      return false;
+      this.editorNewTabButton.click(function(event) {
+                                        var url = self.wym._box.get(0).ownerDocument.URL;
+                                        window.open(url, '_blank');
+                                        return false;
+                                    });
+      jQuery(this.wym._doc)
+          .bind("keyup", function(evt) {
+                    // this style of binding gives docKeyup
+                    // access to 'this' PresentationControl
+                    self.docKeyup(evt);
+                })
+          .bind("keydown", function(evt) {
+                    self.docKeydown(evt);
+                })
+          .bind("mouseup", function(evt) {
+                    // In case the user clicked somewhere else
+                    // in the document, we have to update class list
+                    self.updateAppliedButtons();
+                });
+      jQuery(this.wym._options.containersSelector).find("a")
+          .bind("mousedown", function(evt) {
+                    // Need to save element id before the 'click' event
+                    // handler for the button runs and changes the id.
+                    self.saveCurrentElemId();
+                })
+          .bind("click", function(evt) {
+                    // Need to restore the element id before anything else
+                    // runs and calls ensureId
+                    self.restoreCurrentElemId();
+                    self.updateAppliedButtons();
+                });
+
+      // Insert rewriting of HTML before the WYMeditor updates the textarea.
+      jQuery(this.wym._options.updateSelector)
+          .bind(this.wym._options.updateEvent, function(event) {
+                    self.formSubmit(event);
+                });
+  };
+
+  // Setup document - splits the HTML into 'content HTML' and 'presentation'
+  PresentationControls.prototype.separatePresentation = function() {
+      var self = this;
+      jQuery.post(this.opts.separatePresentationUrl, { html: self.wym.xhtml() } ,
+                  function(data) {
+                      self.withGoodData(data,
+                          function(value) {
+                              // Store the presentation
+                              self.presentationInfo = value.presentation;
+                              // Update the HTML
+                              self.setHtml(value.html);
+                              // Update presentation of HTML
+                              self.updateAfterLoading();
+                          });
+                  }, "json");
+  };
+
+  PresentationControls.prototype.updateAfterLoading = function() {
+      this.insertCommandBlocks();
+      this.updateAllStyleDisplay();
+  };
+
+  PresentationControls.prototype.retrieveCommands = function() {
+      var self = this;
+      // Needs async=false, since separatePresentation depends on data.
+      var res = jQuery.ajax({
+                    type: "GET",
+                    data: {},
+                    url: this.opts.retrieveCommandsUrl,
+                    dataType: "json",
+                    async: false
+      }).responseText;
+      var data = JSON.parse(res);
+
+      self.withGoodData(data, function(value) {
+                              self.commands = data.value;
+                              for (var i = 0; i < self.commands.length; i++) {
+                                  var c = self.commands[i];
+                                  self.commandDict[c.name] = c;
+                              }
+                              self.calculateSelectors(self.commands);
+                              self.allCommandSelectors = jQuery.map(self.commands,
+                                                                      function(c, i) { return self.tagnameToSelector(c.name); }
+                                                                      ).join(",");
+                              self.buildCommandList();
+                              jQuery.each(self.commands,
+                                  function(i, c) {
+                                      self.addCssRule(self.tagnameToSelector(c.name) + ":after",
+                                                      'content: "' + c.verbose_name + '"');
                                   });
-    jQuery(this.wym._doc)
-        .bind("keyup", function(evt) {
-                  // this style of binding gives docKeyup
-                  // access to 'this' PresentationControl
-                  self.docKeyup(evt);
-              })
-        .bind("keydown", function(evt) {
-                  self.docKeydown(evt);
-              })
-        .bind("mouseup", function(evt) {
-                  // In case the user clicked somewhere else
-                  // in the document, we have to update class list
-                  self.updateAppliedButtons();
-              });
-    jQuery(this.wym._options.containersSelector).find("a")
-        .bind("mousedown", function(evt) {
-                  // Need to save element id before the 'click' event
-                  // handler for the button runs and changes the id.
-                  self.saveCurrentElemId();
-              })
-        .bind("click", function(evt) {
-                  // Need to restore the element id before anything else
-                  // runs and calls ensureId
-                  self.restoreCurrentElemId();
-                  self.updateAppliedButtons();
-              });
+                          });
+  };
 
-    // Insert rewriting of HTML before the WYMeditor updates the textarea.
-    jQuery(this.wym._options.updateSelector)
-        .bind(this.wym._options.updateEvent, function(event) {
-                  self.formSubmit(event);
-              });
-};
+  PresentationControls.prototype.retrieveStyles = function() {
+      var self = this;
+      // Needs async=false, since separatePresentation depends on data.
+      var res = jQuery.ajax({
+                    type: "GET",
+                    data: {
+                        'template':this.opts.template,
+                        'page_id':this.opts.pageId
+                    },
+                    url: this.opts.retrieveStylesUrl,
+                    dataType: "json",
+                    async: false
+      }).responseText;
+      var data = JSON.parse(res);
 
-// Setup document - splits the HTML into 'content HTML' and 'presentation'
-PresentationControls.prototype.separatePresentation = function() {
-    var self = this;
-    jQuery.post(this.opts.separatePresentationUrl, { html: self.wym.xhtml() } ,
-                function(data) {
-                    self.withGoodData(data,
-                        function(value) {
-                            // Store the presentation
-                            self.presentationInfo = value.presentation;
-                            // Update the HTML
-                            self.setHtml(value.html);
-                            // Update presentation of HTML
-                            self.updateAfterLoading();
-                        });
-                }, "json");
-};
+      self.withGoodData(data, function(value) {
+          self.availableStyles = data.value;
+          self.calculateSelectors(self.availableStyles);
+          self.buildClassList();
+      });
+  };
 
-PresentationControls.prototype.updateAfterLoading = function() {
-    this.insertCommandBlocks();
-    this.updateAllStyleDisplay();
-};
+  PresentationControls.prototype.calculateSelectors = function(stylelist) {
+      // Given a list of styles, add a 'allowed_elements_selector' attribute
+      // on the basis of the 'allowed_elements' attribute.
 
-PresentationControls.prototype.retrieveCommands = function() {
-    var self = this;
-    // Needs async=false, since separatePresentation depends on data.
-    var res = jQuery.ajax({
-                  type: "GET",
-                  data: {},
-                  url: this.opts.retrieveCommandsUrl,
-                  dataType: "json",
-                  async: false
-    }).responseText;
-    var data = JSON.parse(res);
+      // This is just an optimisation to avoid doing this work multiple times.
 
-    self.withGoodData(data, function(value) {
-                            self.commands = data.value;
-                            for (var i = 0; i < self.commands.length; i++) {
-                                var c = self.commands[i];
-                                self.commandDict[c.name] = c;
-                            }
-                            self.calculateSelectors(self.commands);
-                            self.allCommandSelectors = jQuery.map(self.commands,
-                                                                    function(c, i) { return self.tagnameToSelector(c.name); }
-                                                                    ).join(",");
-                            self.buildCommandList();
-                            jQuery.each(self.commands,
-                                function(i, c) {
-                                    self.addCssRule(self.tagnameToSelector(c.name) + ":after",
-                                                    'content: "' + c.verbose_name + '"');
-                                });
-                        });
-};
+      var self = this;
+      for (var i = 0; i < stylelist.length; i++) {
+          var style = stylelist[i];
+          style.allowed_elements_selector =
+              jQuery.map(style.allowed_elements,
+                         function(t,j) { return self.tagnameToSelector(t); }
+                        ).join(",");
+      }
+  };
 
-PresentationControls.prototype.retrieveStyles = function() {
-    var self = this;
-    // Needs async=false, since separatePresentation depends on data.
-    var res = jQuery.ajax({
-                  type: "GET",
-                  data: {
-                      'template':this.opts.template,
-                      'page_id':this.opts.pageId
-                  },
-                  url: this.opts.retrieveStylesUrl,
-                  dataType: "json",
-                  async: false
-    }).responseText;
-    var data = JSON.parse(res);
+  PresentationControls.prototype.tagnameToSelector = function(name) {
+      // Convert a tag name into a selector used to match elements that represent
+      // that tag.
+      //
+      // A 'tag' is either an HTML element name or a command name.
+      //
+      // This function accounts for the use of p elements to represent
+      // commands in the document.
+      var c = this.commandDict[name];
+      if (c == null) {
+          // not a command
+          if (name == "p") {
+              // Need to ensure we don't match commands
+              var selector = "p";
+              for (var i = 0; i < this.commands.length; i++) {
+                  selector = selector + "[class!='secommand secommand-" + this.commands[i].name + "']";
+              }
+              return selector;
+          } else {
+              return name;
+          }
+      } else {
+          return "p.secommand-" + name;
+      }
+  };
 
-    self.withGoodData(data, function(value) {
-        self.availableStyles = data.value;
-        self.calculateSelectors(self.availableStyles);
-        self.buildClassList();
-    });
-};
+  PresentationControls.prototype.setupCssStorage = function() {
+      // We need a place to store custom CSS rules. (Existing jQuery plugins don't
+      // allow for our need of changing style sheet of document in an iframe, and
+      // rolling our own is easier than fixing them).
 
-PresentationControls.prototype.calculateSelectors = function(stylelist) {
-    // Given a list of styles, add a 'allowed_elements_selector' attribute
-    // on the basis of the 'allowed_elements' attribute.
+      // code partly copied from jquery.cssRule.js
+      var cssStorageNode = jQuery('<style rel="stylesheet" type="text/css">').appendTo(jQuery(this.wym._doc).find("head"))[0];
+      this.stylesheet = this.wym._doc.styleSheets[0];
+  };
 
-    // This is just an optimisation to avoid doing this work multiple times.
+  // ---- Saving data ----
 
-    var self = this;
-    for (var i = 0; i < stylelist.length; i++) {
-        var style = stylelist[i];
-        style.allowed_elements_selector =
-            jQuery.map(style.allowed_elements,
-                       function(t,j) { return self.tagnameToSelector(t); }
-                      ).join(",");
-    }
-};
 
-PresentationControls.prototype.tagnameToSelector = function(name) {
-    // Convert a tag name into a selector used to match elements that represent
-    // that tag.
-    //
-    // A 'tag' is either an HTML element name or a command name.
-    //
-    // This function accounts for the use of p elements to represent
-    // commands in the document.
-    var c = this.commandDict[name];
-    if (c == null) {
-        // not a command
-        if (name == "p") {
-            // Need to ensure we don't match commands
-            var selector = "p";
-            for (var i = 0; i < this.commands.length; i++) {
-                selector = selector + "[class!='secommand secommand-" + this.commands[i].name + "']";
-            }
-            return selector;
-        } else {
-            return name;
-        }
-    } else {
-        return "p.secommand-" + name;
-    }
-};
+  PresentationControls.prototype.prepareData = function() {
+      // Prepare data/html for sending server side.
 
-PresentationControls.prototype.setupCssStorage = function() {
-    // We need a place to store custom CSS rules. (Existing jQuery plugins don't
-    // allow for our need of changing style sheet of document in an iframe, and
-    // rolling our own is easier than fixing them).
+      // We need to ensure all elements have ids, *and* that command block
+      // elements have correct ids (which is a side effect of the below).
+      this.ensureAllIds();
+      // this.presentationInfo might have old info, if command blocks have
+      // been removed.  Need to clean.
+      this.cleanPresentationInfo();
+  };
 
-    // code partly copied from jquery.cssRule.js
-    var cssStorageNode = jQuery('<style rel="stylesheet" type="text/css">').appendTo(jQuery(this.wym._doc).find("head"))[0];
-    this.stylesheet = this.wym._doc.styleSheets[0];
-};
+  PresentationControls.prototype.cleanHtml = function() {
+      this.prepareData();
+      var self = this;
+      var html = this.wym.xhtml();
+      jQuery.post(this.opts.cleanHtmlUrl, {'html':html},
+                  function(data) {
+                      self.withGoodData(data, function(value) {
+                                              self.setHtml(value.html);
+                                              self.updateAfterLoading();
+                                          });
+                  }, "json");
+  };
 
-// ---- Saving data ----
+  // ---- Utility functions ----
 
+  PresentationControls.prototype.escapeHtml = function(html) {
+      return html.replace(/&/g,"&amp;")
+          .replace(/\"/g,"&quot;")
+          .replace(/</g,"&lt;")
+          .replace(/>/g,"&gt;");
+  };
 
-PresentationControls.prototype.prepareData = function() {
-    // Prepare data/html for sending server side.
+  PresentationControls.prototype.flattenPresStyle = function(pres) {
+      // Convert a PresentationInfo object to a string
+      return pres.prestype + ":" + pres.name;
+  };
 
-    // We need to ensure all elements have ids, *and* that command block
-    // elements have correct ids (which is a side effect of the below).
-    this.ensureAllIds();
-    // this.presentationInfo might have old info, if command blocks have
-    // been removed.  Need to clean.
-    this.cleanPresentationInfo();
-};
+  // If data contains an error message, display to the user,
+  // otherwise clear the displayed error and perform the callback
+  // with the value in the data.
+  PresentationControls.prototype.withGoodData = function(data, callback) {
+      if (data.result == 'ok') {
+          this.clearError();
+          callback(data.value);
+      } else {
+          // TODO - perhaps distinguish between a server error
+          // and a user error
+          this.showError(data.message);
+      }
+  };
 
-PresentationControls.prototype.cleanHtml = function() {
-    this.prepareData();
-    var self = this;
-    var html = this.wym.xhtml();
-    jQuery.post(this.opts.cleanHtmlUrl, {'html':html},
-                function(data) {
-                    self.withGoodData(data, function(value) {
-                                            self.setHtml(value.html);
-                                            self.updateAfterLoading();
-                                        });
-                }, "json");
-};
+  // ---- DOM utility ----
 
-// ---- Utility functions ----
+  PresentationControls.prototype.nextElementSibling = function(node) {
+      var next = node;
+      while (true) {
+          if (next == null) {
+              return null;
+          }
+          if (next.nextSibling == null) {
+              next = next.parentNode;
+          } else {
+              next = next.nextSibling;
+              if (next.nodeName != "#text") {
+                  return next;
+              }
+          }
+      }
+  };
 
-PresentationControls.prototype.escapeHtml = function(html) {
-    return html.replace(/&/g,"&amp;")
-        .replace(/\"/g,"&quot;")
-        .replace(/</g,"&lt;")
-        .replace(/>/g,"&gt;");
-};
+  PresentationControls.prototype.previousElementSibling = function(node) {
+      var prev = node;
+      while (true) {
+          if (prev == null) {
+              return null;
+          }
+          if (prev.previousSibling == null) {
+              prev = prev.parentNode;
+          } else {
+              prev = prev.previousSibling;
+              if (prev.nodeName != "#text") {
+                  return prev;
+              }
+          }
+      }
+  };
 
-PresentationControls.prototype.flattenPresStyle = function(pres) {
-    // Convert a PresentationInfo object to a string
-    return pres.prestype + ":" + pres.name;
-};
+  // ---- CSS utility ----
 
-// If data contains an error message, display to the user,
-// otherwise clear the displayed error and perform the callback
-// with the value in the data.
-PresentationControls.prototype.withGoodData = function(data, callback) {
-    if (data.result == 'ok') {
-        this.clearError();
-        callback(data.value);
-    } else {
-        // TODO - perhaps distinguish between a server error
-        // and a user error
-        this.showError(data.message);
-    }
-};
+  PresentationControls.prototype.addCssRule = function(selector, rule) {
+      var ss = this.stylesheet;
+      var rules = ss.rules ? 'rules' : 'cssRules';
+      // should work with IE and Firefox, don't know about Safari
+      if (ss.addRule)
+          ss.addRule(selector, rule||'x:y' );//IE won't allow empty rules
+      else if (ss.insertRule)
+          ss.insertRule(selector + '{'+ rule +'}', ss[rules].length);
+  };
 
-// ---- DOM utility ----
+  // ---- Event handlers ----
 
-PresentationControls.prototype.nextElementSibling = function(node) {
-    var next = node;
-    while (true) {
-        if (next == null) {
-            return null;
-        }
-        if (next.nextSibling == null) {
-            next = next.parentNode;
-        } else {
-            next = next.nextSibling;
-            if (next.nodeName != "#text") {
-                return next;
-            }
-        }
-    }
-};
+  // Long event handlers below, most are defined inline.
 
-PresentationControls.prototype.previousElementSibling = function(node) {
-    var prev = node;
-    while (true) {
-        if (prev == null) {
-            return null;
-        }
-        if (prev.previousSibling == null) {
-            prev = prev.parentNode;
-        } else {
-            prev = prev.previousSibling;
-            if (prev.nodeName != "#text") {
-                return prev;
-            }
-        }
-    }
-};
+  PresentationControls.prototype.docKeyup = function(evt) {
+      // Some browsers (Firefox at least) will insert a new paragraph with the
+      // same ID as the old one when the user presses 'Enter'. This needs fixing
+      // for our styling to work. It's possible that multiple 'p' elements can be inserted
+      // for one keyup event, so we handle that. This appears to happen for any
+      // elements that are created by pressing 'Enter'
 
-// ---- CSS utility ----
+      var node = this.getCurrentContainerNode();
+      var container;
+      if (node == null) {
+          return;
+      } else {
+          container = jQuery(node);
+      }
+      if (evt.keyCode == 13 && !evt.shiftKey) {
+          if (container.is("p[id],li[id]")) {
+              // Need to clear id on all elem's with that id except the first.
+              // (hoping that jQuery will return them in document order, which it
+              // seems to)
+              jQuery(this.wym._doc).find(container.get(0).tagName + "#" +  container.attr("id")).
+                  each(function(i){
+                           var node = this;
+                           if (i > 0) { // skip the first
+                                jQuery(node).removeAttr('id');
+                           }
+                       });
+          }
+      }
+      // We also need to update the disable/enabled state of the classList and commandList
+      this.updateAppliedButtons(container);
+  };
 
-PresentationControls.prototype.addCssRule = function(selector, rule) {
-    var ss = this.stylesheet;
-    var rules = ss.rules ? 'rules' : 'cssRules';
-    // should work with IE and Firefox, don't know about Safari
-    if (ss.addRule)
-        ss.addRule(selector, rule||'x:y' );//IE won't allow empty rules
-    else if (ss.insertRule)
-        ss.insertRule(selector + '{'+ rule +'}', ss[rules].length);
-};
+  PresentationControls.prototype.docKeydown = function(evt) {
+      // Need to intercept backspace and delete, to stop command blocks
+      // being deleted. (The system copes fairly well with them being
+      // deleted, but it can be very confusing for the user.)
+      var isBackspace = (evt.keyCode == 8);
+      var isDelete = (evt.keyCode == 46);
+      var target = null;
 
-// ---- Event handlers ----
+      if (isBackspace || isDelete) {
 
-// Long event handlers below, most are defined inline.
+          var s = this.wym._iframe.contentWindow.getSelection();
+          if (s.anchorNode != s.focusNode) {
+              // an extended selection.  It doesn't matter if this
+              // contains command blocks - it doesn't confuse the user
+              // if these are deleted wholesale.
+              return;
+          }
+          if (s.focusNode == null) {
+              return;
+          }
 
-PresentationControls.prototype.docKeyup = function(evt) {
-    // Some browsers (Firefox at least) will insert a new paragraph with the
-    // same ID as the old one when the user presses 'Enter'. This needs fixing
-    // for our styling to work. It's possible that multiple 'p' elements can be inserted
-    // for one keyup event, so we handle that. This appears to happen for any
-    // elements that are created by pressing 'Enter'
+          var node = s.focusNode;
+          if (node.nodeName == "#text") {
+              // always true?
+              node = node.parentNode;
+          }
+          if (isBackspace) {
+              if (s.focusOffset != 0) {
+                  return; // not at first character within text node
+              }
+              // need to check *previous* node
+              target = this.previousElementSibling(node);
+          }
+          if (isDelete) {
+              if (s.focusOffset != s.focusNode.textContent.length) {
+                  return; // not at last character within text node
+              }
+              // need to check *next* node
+              target = this.nextElementSibling(node);
+          }
+          if (target == null) {
+              return;
+          }
 
-    var node = this.getCurrentContainerNode();
-    var container;
-    if (node == null) {
-        return;
-    } else {
-        container = jQuery(node);
-    }
-    if (evt.keyCode == 13 && !evt.shiftKey) {
-        if (container.is("p[id],li[id]")) {
-            // Need to clear id on all elem's with that id except the first.
-            // (hoping that jQuery will return them in document order, which it
-            // seems to)
-            jQuery(this.wym._doc).find(container.get(0).tagName + "#" +  container.attr("id")).
-                each(function(i){
-                         var node = this;
-                         if (i > 0) { // skip the first
-                              jQuery(node).removeAttr('id');
+          if (jQuery(target).is(this.allCommandSelectors)) {
+              // stop backspace or delete from working.
+              evt.preventDefault();
+          }
+      }
+  };
+
+  PresentationControls.prototype.formSubmit = function(event) {
+      this.prepareData();
+      // Since we are in the middle of submitting the page, an asynchronous
+      // request will be too late! So we block instead.
+
+      // TODO - exception handling.  If this fails, the default
+      // handler will post the form, causing all formatting to be lost.
+      var res = jQuery.ajax({
+                    type: "POST",
+                    data: {
+                        html: this.wym.xhtml(),
+                        presentation: JSON.stringify(this.presentationInfo)
+                    },
+                    url: this.opts.combinePresentationUrl,
+                    dataType: "json",
+                    async: false
+      }).responseText;
+      var data = JSON.parse(res);
+      if (data.result == 'ok') {
+          // Replace existing HTML with combined.
+          this.setHtml(data.value.html);
+          // In case the normal WYMeditor update got called *before* this
+          // event handler, we do another update.
+          this.wym.update();
+      } else {
+          event.preventDefault();
+          this.showError(data.message);
+          alert("Data in " + this.name + " can't be saved - see error message.");
+      }
+  };
+
+  PresentationControls.prototype.saveCurrentElemId = function() {
+
+      // When a container tag is clicked, the tag of the current node is changed,
+      // and the 'id' attribute is removed. This causes the styling and commands
+      // to be lost. So we save the id and restore after the change.  This can
+      // result in strange ids e.g. an h2 element with the id='h1_1'.  However,
+      // this doesn't matter at all - it only matters that ids are unique
+      var nodeId = null;
+      var node = this.getCurrentContainerNode();
+      if (node != null) {
+          nodeId = node.id;
+      }
+      this._savedNodeId = nodeId;
+  };
+
+  PresentationControls.prototype.restoreCurrentElemId = function() {
+
+      if (this._savedNodeId == null) {
+          return;
+      }
+      var node = this.getCurrentContainerNode();
+      if (node != null &&
+          (node.id == null || node.id == "")) {
+          node.id = this._savedNodeId;
+      }
+      this._savedNodeId = null;
+  };
+
+  // ---- Manipulation of edited document ----
+
+  PresentationControls.prototype.setHtml = function(html) {
+      // WUMEditor ought to call .listen() after setting
+      // the HTML (to rebind events for images),
+      // but it doesn't at the moment, so we work around it by wrapping.
+      this.wym.html(html);
+      this.wym.listen();
+  };
+
+  PresentationControls.prototype.ensureAllIds = function() {
+      // We check all block level elements (or all elements that can have commands
+      // applied to them)
+
+      var self = this;
+      var elems = [];
+      for (var i = 0; i < this.commands.length; i++) {
+          elems = jQuery.merge(elems, this.commands[i].allowed_elements);
+      }
+      elems = jQuery.unique(elems);
+      jQuery(this.wym._doc).find(elems.join(",")).each(function(i) {
+                                                           self.ensureId(this);
+                                                       });
+  };
+
+  PresentationControls.prototype.ensureId = function(node) {
+      var id = node.id;
+
+      if (id == undefined || id == "") {
+          id = this.nextId(node.tagName.toLowerCase());
+          node.id = id;
+          this.registerSection(id);
+      }
+      return id;
+  };
+
+  PresentationControls.prototype.nextId = function(tagName) {
+      // All sections that can receive styles need a section ID.
+      // For the initial HTML, this is assigned server-side when the
+      // HTML is split into 'semantic HTML' and 'presentation info'.
+      // For newly added HTML, however, we need to add it ourself
+
+      var i = 1;
+      var id = "";
+      while (true) {
+          id = tagName + "_" + i.toString();
+          if (jQuery(this.wym._doc).find('#' + id).is(tagName)) {
+              i++;
+          } else {
+              return id;
+          }
+      }
+  };
+
+  PresentationControls.prototype.commandBlockId = function(sectId, command) {
+      // Returns the ID to use for a command block that appears before
+      // section sectId for a given command.  This is also the key used
+      // in this.presentationInfo
+      return command.name + "_" + sectId;
+  };
+
+  PresentationControls.prototype.insertCommandBlock = function(sectId, command) {
+      // the spaces help make the command a decent target in the editor for Firefox (and we also use z-index: 1 in the CSS to bring them forward) - otherwise we can't hit them reliably with the mouse. Safari has not had a problem with this for some reason.
+      var newelem = jQuery("<p class=\"secommand secommand-" + command.name + "\">&nbsp;</p>");
+      var elem = jQuery(this.wym._doc).find("#" + sectId);
+
+      // Commands have an order they should appear in, which is defined by layout_order.
+      // We can use this to put the block in the right place.
+      var loopAgain = true;
+      var self = this;
+      while(loopAgain) {
+          loopAgain = false;
+          jQuery.each(this.commands,
+                     function(i, c) {
+                         var prev = elem.prev();
+                         if (prev.is(self.tagnameToSelector(c.name)) &&
+                                     c.layout_order > command.layout_order) {
+                             elem = prev;
+                             loopAgain = true;
                          }
                      });
-        }
-    }
-    // We also need to update the disable/enabled state of the classList and commandList
-    this.updateAppliedButtons(container);
-};
+      }
 
-PresentationControls.prototype.docKeydown = function(evt) {
-    // Need to intercept backspace and delete, to stop command blocks
-    // being deleted. (The system copes fairly well with them being
-    // deleted, but it can be very confusing for the user.)
-    var isBackspace = (evt.keyCode == 8);
-    var isDelete = (evt.keyCode == 46);
-    var target = null;
+      elem.before(newelem);
+      var newId = this.commandBlockId(sectId, command);
+      newelem.attr('id', newId);
+      return newId;
+  };
 
-    if (isBackspace || isDelete) {
+  PresentationControls.prototype.insertCommandBlocks = function() {
+      // This is run once, after loading HTML.  We can rely on 'ids' being
+      // present, since the HTML is all sent by the server.
+      for (var key in this.presentationInfo) {
+          var presinfos = this.presentationInfo[key];
+          for (var i = 0; i < presinfos.length; i++) {
+              var pi = presinfos[i];
+              if (pi.prestype == 'command') {
+                  // key = e.g. 'newrow_p_1', we need 'p_1'
+                  var id = key.split('_').slice(1).join('_');
+                  this.insertCommandBlock(id, this.commandDict[pi.name]);
+              }
+          }
+      }
+  };
 
-        var s = this.wym._iframe.contentWindow.getSelection();
-        if (s.anchorNode != s.focusNode) {
-            // an extended selection.  It doesn't matter if this
-            // contains command blocks - it doesn't confuse the user
-            // if these are deleted wholesale.
-            return;
-        }
-        if (s.focusNode == null) {
-            return;
-        }
+  PresentationControls.prototype.isCommandBlock = function(node) {
+      // command blocks are like <p id="newrow_p_1" class="secommand secommand-[commandname]">
+      return (node != undefined && node.tagName != undefined &&
+              node.tagName.toLowerCase() == 'p' && node.className != undefined &&
+              node.className.match(/\bsecommand\b/));
+  };
 
-        var node = s.focusNode;
-        if (node.nodeName == "#text") {
-            // always true?
-            node = node.parentNode;
-        }
-        if (isBackspace) {
-            if (s.focusOffset != 0) {
-                return; // not at first character within text node
-            }
-            // need to check *previous* node
-            target = this.previousElementSibling(node);
-        }
-        if (isDelete) {
-            if (s.focusOffset != s.focusNode.textContent.length) {
-                return; // not at last character within text node
-            }
-            // need to check *next* node
-            target = this.nextElementSibling(node);
-        }
-        if (target == null) {
-            return;
-        }
+  // ---- Display of style information on document.
 
-        if (jQuery(target).is(this.allCommandSelectors)) {
-            // stop backspace or delete from working.
-            evt.preventDefault();
-        }
-    }
-};
+  PresentationControls.prototype.updateStyleDisplay = function(sectId) {
+      var styles = this.presentationInfo[sectId];
+      var self = this;
+      var stylelist = jQuery.map(styles, function(s, i) {
+                                     return self.getPresentationInfo(s.name);
+                                 });
+      // Group by category:
+      var cmpString = function(a, b) {
+          if (a == null && b == null) {
+              return 0;
+          }
+          if (a == null) {
+              return -1;
+          }
+          if (b == null) {
+              return 1;
+          }
+          if (a < b) {
+              return -1;
+          }
+          if (a > b) {
+              return 1;
+          }
+          return 0;
+      };
+      stylelist.sort(function(s1, s2) {
+                         if (s1.category == s2.category) {
+                             return cmpString(s1.verbose_name, s2.verbose_name);
+                         } else {
+                             return cmpString(s1.category, s2.category);
+                         }
+                     });
 
-PresentationControls.prototype.formSubmit = function(event) {
-    this.prepareData();
-    // Since we are in the middle of submitting the page, an asynchronous
-    // request will be too late! So we block instead.
+      // Now build the caption.
+      var caption = [];
+      var currentCategory = null;
+      jQuery.each(stylelist, function(i, item) {
+          if (caption.length > 0) {
+              caption.push(", ");
+          }
+          if (currentCategory != item.category) {
+              // NB - due to sorting, uncategorized items always appear at
+              // start, which is what we want.
+              if (item.category != null) {
+                  caption.push(item.category + ": ");
+              }
+              currentCategory = item.category;
+          }
+          caption.push(item.verbose_name);
+      });
 
-    // TODO - exception handling.  If this fails, the default
-    // handler will post the form, causing all formatting to be lost.
-    var res = jQuery.ajax({
-                  type: "POST",
-                  data: {
-                      html: this.wym.xhtml(),
-                      presentation: JSON.stringify(this.presentationInfo)
-                  },
-                  url: this.opts.combinePresentationUrl,
-                  dataType: "json",
-                  async: false
-    }).responseText;
-    var data = JSON.parse(res);
-    if (data.result == 'ok') {
-        // Replace existing HTML with combined.
-        this.setHtml(data.value.html);
-        // In case the normal WYMeditor update got called *before* this
-        // event handler, we do another update.
-        this.wym.update();
-    } else {
-        event.preventDefault();
-        this.showError(data.message);
-        alert("Data in " + this.name + " can't be saved - see error message.");
-    }
-};
+      // put a non-breaking space into the style list, so it always exists
+      this.addCssRule("#" + sectId + ":before", 'content: "' + caption.join("") + '\\00a0' + '"');
+  };
 
-PresentationControls.prototype.saveCurrentElemId = function() {
+  PresentationControls.prototype.updateAllStyleDisplay = function() {
+      for (var key in this.presentationInfo) {
+          this.updateStyleDisplay(key);
+      }
+  };
 
-    // When a container tag is clicked, the tag of the current node is changed,
-    // and the 'id' attribute is removed. This causes the styling and commands
-    // to be lost. So we save the id and restore after the change.  This can
-    // result in strange ids e.g. an h2 element with the id='h1_1'.  However,
-    // this doesn't matter at all - it only matters that ids are unique
-    var nodeId = null;
-    var node = this.getCurrentContainerNode();
-    if (node != null) {
-        nodeId = node.id;
-    }
-    this._savedNodeId = nodeId;
-};
+  PresentationControls.prototype.getPresentationInfo = function(stylename) {
+      // Full PresentationInfo information is not stored against individual DOM nodes, only
+      // the prestype and name. This function gets the full info from the name.
 
-PresentationControls.prototype.restoreCurrentElemId = function() {
+      var styles = this.availableStyles;
+      for (var i = 0; i < styles.length; i++) {
+          if (styles[i].name == stylename) {
+              return styles[i];
+          }
+      }
+      var commands = this.commands;
+      for (var i2 = 0; i2 < commands; i2++) {
+          if (commands[i2].name == stylename) {
+              return commands[i2];
+          }
+      }
+      return undefined; // shouldn't get here
+  };
 
-    if (this._savedNodeId == null) {
-        return;
-    }
-    var node = this.getCurrentContainerNode();
-    if (node != null &&
-        (node.id == null || node.id == "")) {
-        node.id = this._savedNodeId;
-    }
-    this._savedNodeId = null;
-};
+  // ---- Manipulation of class and command list ----
 
-// ---- Manipulation of edited document ----
+  PresentationControls.prototype.buildClassList = function() {
+      var self = this;
+      this.classList.empty();
+      // For each new category encountered, we have an li that contains
+      // a div with a heading, and a ul with all the actual classes.
+      // NB the category could be 'null' so care must be taken. Also,
+      // the list is already sorted by category and verbose_name
+      var state = {start:true,
+                   category:null,
+                   categoryList:null
+                  };
+      jQuery.each(self.availableStyles, function(i, item) {
+          if (state.start || (state.category != item.category)) {
+              var category;
+              if (item.category == null) {
+                  category = "Uncategorized";
+              } else {
+                  category = item.category;
+              }
+              var classListItem = jQuery("<li class=\"class-category\"><h3 class=\"categoryHeading\">" + self.escapeHtml(category) + ':</h3></li>');
+              classListItem.appendTo(self.classList);
+              state.categoryList = jQuery("<ul></ul>");
+              state.categoryList.appendTo(classListItem);
+          }
+          var btn = jQuery("<li><a href='#'>" + self.escapeHtml(item.verbose_name) + "</a></li>").appendTo(state.categoryList).find("a");
+          // event handlers
+          var style = self.availableStyles[i];
 
-PresentationControls.prototype.setHtml = function(html) {
-    // WUMEditor ought to call .listen() after setting
-    // the HTML (to rebind events for images),
-    // but it doesn't at the moment, so we work around it by wrapping.
-    this.wym.html(html);
-    this.wym.listen();
-};
+          btn.click(function(event) {
+                        self.toggleStyle(style, btn);
+                        event.preventDefault();
+                    });
+          state.start = false;
+          state.category = item.category;
+      });
+  };
 
-PresentationControls.prototype.ensureAllIds = function() {
-    // We check all block level elements (or all elements that can have commands
-    // applied to them)
+  PresentationControls.prototype.buildCommandList = function () {
+      var self = this;
+      self.commandList.empty();
+      jQuery.each(self.commands, function(i, item) {
+          var btn = jQuery("<li><a href='#'>" + self.escapeHtml(item.verbose_name) + "</a></li>").appendTo(self.commandList).find("a");
+          // event handlers
+          var command = self.commands[i];
 
-    var self = this;
-    var elems = [];
-    for (var i = 0; i < this.commands.length; i++) {
-        elems = jQuery.merge(elems, this.commands[i].allowed_elements);
-    }
-    elems = jQuery.unique(elems);
-    jQuery(this.wym._doc).find(elems.join(",")).each(function(i) {
-                                                         self.ensureId(this);
-                                                     });
-};
+          btn.click(function(event) {
+                        self.doCommand(command, btn);
+                        event.preventDefault();
+                    });
+      });
+  };
 
-PresentationControls.prototype.ensureId = function(node) {
-    var id = node.id;
+  PresentationControls.prototype.updateClassListItem = function(btn, style) {
+      var sectId = this.getCurrentSection(style);
+      if (sectId == undefined) {
+          // Can't use it.
+          if (style.prestype == "command") {
+              // Don't want the class list (displayed below command list)
+              // to jump up and down
+              btn.addClass("disabled");
+              btn.removeClass("used");
+          } else {
+              btn.hide();
+              btn.addClass('disabled');
+          }
+      } else {
+          if (style.prestype == "command") {
+              btn.removeClass("disabled");
+              if (this.hasCommand(sectId, style)) {
+                  btn.addClass("used");
+              } else {
+                  btn.removeClass("used");
+              }
+          } else {
+              btn.removeClass('disabled');
+              btn.show();
+              if (this.hasStyle(sectId, style)) {
+                  btn.addClass("used");
+              } else {
+                  btn.removeClass("used");
+              }
+          }
+      }
+  };
 
-    if (id == undefined || id == "") {
-        id = this.nextId(node.tagName.toLowerCase());
-        node.id = id;
-        this.registerSection(id);
-    }
-    return id;
-};
+  PresentationControls.prototype.updateAppliedButtons = function(curContainer) {
+      var self = this;
+      var node;
+      if (curContainer == undefined) {
+          node = this.getCurrentContainerNode();
+      } else {
+          node = curContainer.get(0);
+      }
+      if (node != undefined && node != this.currentNode) {
+          // if current node has changed, might need to update list
+          this.currentNode = node;
+          // Sets enabled/disabled on all items in classList and commands
+          var pairs = [[this.availableStyles, this.classList],
+                       [this.commands, this.commandList]];
 
-PresentationControls.prototype.nextId = function(tagName) {
-    // All sections that can receive styles need a section ID.
-    // For the initial HTML, this is assigned server-side when the
-    // HTML is split into 'semantic HTML' and 'presentation info'.
-    // For newly added HTML, however, we need to add it ourself
+          for (var i = 0; i < pairs.length; i++) {
+              var styles = pairs[i][0];
+              var btncontainer = pairs[i][1];
+              btncontainer.find("a").each(function(k) {
+                                              self.updateClassListItem(jQuery(this), styles[k]);
+                                          });
+          }
+          this.classList.find('li.class-category').each(function(k) {
+              var category = jQuery(this);
+              var total = category.find('li a').length;
+              var hidden = category.find('li a.disabled').length;
+              if ((total - hidden) > 0) {
+                  category.show();
+              } else {
+                  category.hide();
+              }
+          });
 
-    var i = 1;
-    var id = "";
-    while (true) {
-        id = tagName + "_" + i.toString();
-        if (jQuery(this.wym._doc).find('#' + id).is(tagName)) {
-            i++;
-        } else {
-            return id;
-        }
-    }
-};
+          // We also update the container list to set the class="used" for
+          // consistent styling between that list and the command/class lists.
+          jQuery(this.wym._options.containersSelector).find("a").each(function(k) {
+              var name = this.name; // stores 'P', 'H1' etc
+              if (name.toLowerCase() == node.tagName.toLowerCase() &&
+                  !self.isCommandBlock(node)) {
+                  jQuery(this).addClass("used");
+              } else {
+                  jQuery(this).removeClass("used");
+              }
+          });
 
-PresentationControls.prototype.commandBlockId = function(sectId, command) {
-    // Returns the ID to use for a command block that appears before
-    // section sectId for a given command.  This is also the key used
-    // in this.presentationInfo
-    return command.name + "_" + sectId;
-};
+      }
+  };
 
-PresentationControls.prototype.insertCommandBlock = function(sectId, command) {
-    // the spaces help make the command a decent target in the editor for Firefox (and we also use z-index: 1 in the CSS to bring them forward) - otherwise we can't hit them reliably with the mouse. Safari has not had a problem with this for some reason.
-    var newelem = jQuery("<p class=\"secommand secommand-" + command.name + "\">&nbsp;</p>");
-    var elem = jQuery(this.wym._doc).find("#" + sectId);
+  // ---- Manipulation/query of stored presentation info ----
 
-    // Commands have an order they should appear in, which is defined by layout_order.
-    // We can use this to put the block in the right place.
-    var loopAgain = true;
-    var self = this;
-    while(loopAgain) {
-        loopAgain = false;
-        jQuery.each(this.commands,
-                   function(i, c) {
-                       var prev = elem.prev();
-                       if (prev.is(self.tagnameToSelector(c.name)) &&
-                                   c.layout_order > command.layout_order) {
-                           elem = prev;
-                           loopAgain = true;
-                       }
-                   });
-    }
+  PresentationControls.prototype.registerSection = function(sectId) {
+      // Make sure we have somewhere to store styles for a section.
+      this.presentationInfo[sectId] = new Array();
+  };
 
-    elem.before(newelem);
-    var newId = this.commandBlockId(sectId, command);
-    newelem.attr('id', newId);
-    return newId;
-};
+  PresentationControls.prototype.getCurrentContainerNode = function() {
+      // Returns the node of the closest container element to the selection
+      var container = jQuery(this.wym.selected()).parentsOrSelf(this.blockdefSelector);
+      if (container.is(this.blockdefSelector)) {
+          return container.get(0);
+      } else {
+          return null;
+      }
+  };
 
-PresentationControls.prototype.insertCommandBlocks = function() {
-    // This is run once, after loading HTML.  We can rely on 'ids' being
-    // present, since the HTML is all sent by the server.
-    for (var key in this.presentationInfo) {
-        var presinfos = this.presentationInfo[key];
-        for (var i = 0; i < presinfos.length; i++) {
-            var pi = presinfos[i];
-            if (pi.prestype == 'command') {
-                // key = e.g. 'newrow_p_1', we need 'p_1'
-                var id = key.split('_').slice(1).join('_');
-                this.insertCommandBlock(id, this.commandDict[pi.name]);
-            }
-        }
-    }
-};
+  PresentationControls.prototype.getCurrentSection = function(style) {
+      // Returns the section ID of the current section that is applicable
+      // to the given style, or undefined if there is none.
+      // Since sections can be nested, if more than one section is possible
+      // for the given style, the first section found (starting from the
+      // current selection and moving up the HTML tree) will be returned.
+      // This function has the side effect of adding a section ID if one
+      // was not defined already.
+      var wym = this.wym;
+      var self = this;
+      var expr = style.allowed_elements_selector;
+      var container = jQuery(wym.selected()).parentsOrSelf(expr);
+      if (container.is(expr)) {
+          var first = container.get(0);
+          var id = this.ensureId(first);
+          return id;
+      }
+      return undefined;
+  };
 
-PresentationControls.prototype.isCommandBlock = function(node) {
-    // command blocks are like <p id="newrow_p_1" class="secommand secommand-[commandname]">
-    return (node != undefined && node.tagName != undefined &&
-            node.tagName.toLowerCase() == 'p' && node.className != undefined &&
-            node.className.match(/\bsecommand\b/));
-};
+  PresentationControls.prototype.hasStyle = function(sectId, style) {
+      var styles = this.presentationInfo[sectId];
+      if (styles == undefined)
+          return false;
+      for (var i = 0; i < styles.length; i++) {
+          if (this.flattenPresStyle(styles[i]) == this.flattenPresStyle(style)) {
+              return true;
+          };
+      }
+      return false;
+  };
 
-// ---- Display of style information on document.
+  PresentationControls.prototype.hasCommand = function(sectId, command) {
+      // Returns true if the section has the command before it.
+      return (this.presentationInfo[this.commandBlockId(sectId, command)] != undefined);
+  };
 
-PresentationControls.prototype.updateStyleDisplay = function(sectId) {
-    var styles = this.presentationInfo[sectId];
-    var self = this;
-    var stylelist = jQuery.map(styles, function(s, i) {
-                                   return self.getPresentationInfo(s.name);
-                               });
-    // Group by category:
-    var cmpString = function(a, b) {
-        if (a == null && b == null) {
-            return 0;
-        }
-        if (a == null) {
-            return -1;
-        }
-        if (b == null) {
-            return 1;
-        }
-        if (a < b) {
-            return -1;
-        }
-        if (a > b) {
-            return 1;
-        }
-        return 0;
-    };
-    stylelist.sort(function(s1, s2) {
-                       if (s1.category == s2.category) {
-                           return cmpString(s1.verbose_name, s2.verbose_name);
-                       } else {
-                           return cmpString(s1.category, s2.category);
-                       }
-                   });
+  PresentationControls.prototype.addStyle = function(sectId, presinfo) {
+      var styles = this.presentationInfo[sectId];
+      styles.push(presinfo);
+      this.presentationInfo[sectId] = jQuery.unique(styles);
+  };
 
-    // Now build the caption.
-    var caption = [];
-    var currentCategory = null;
-    jQuery.each(stylelist, function(i, item) {
-        if (caption.length > 0) {
-            caption.push(", ");
-        }
-        if (currentCategory != item.category) {
-            // NB - due to sorting, uncategorized items always appear at
-            // start, which is what we want.
-            if (item.category != null) {
-                caption.push(item.category + ": ");
-            }
-            currentCategory = item.category;
-        }
-        caption.push(item.verbose_name);
-    });
+  PresentationControls.prototype.removeStyle = function(sectId, presinfo) {
+      var styles = this.presentationInfo[sectId];
+      styles = jQuery.grep(styles, function(item, i) {
+                               return !(item.prestype == presinfo.prestype
+                                        && item.name == presinfo.name);
+                           });
+      this.presentationInfo[sectId] = styles;
+  };
 
-    // put a non-breaking space into the style list, so it always exists
-    this.addCssRule("#" + sectId + ":before", 'content: "' + caption.join("") + '\\00a0' + '"');
-};
+  PresentationControls.prototype.toggleStyle = function(style, btn) {
+      // What section are we on?
+      var sectId = this.getCurrentSection(style);
+      if (sectId == undefined) {
+          // No allowed to use it there
+          alert("Cannot use this style on current element.");
+          return;
+      }
 
-PresentationControls.prototype.updateAllStyleDisplay = function() {
-    for (var key in this.presentationInfo) {
-        this.updateStyleDisplay(key);
-    }
-};
+      if (this.hasStyle(sectId, style)) {
+          this.removeStyle(sectId, style);
+      } else {
+          this.addStyle(sectId, style);
+      }
+      this.updateStyleDisplay(sectId);
+      this.updateClassListItem(btn, style);
+  };
 
-PresentationControls.prototype.getPresentationInfo = function(stylename) {
-    // Full PresentationInfo information is not stored against individual DOM nodes, only
-    // the prestype and name. This function gets the full info from the name.
+  PresentationControls.prototype.doCommand = function(command, btn) {
+      // What section are we on?
+      var sectId = this.getCurrentSection(command);
+      if (sectId == undefined) {
+          // Not allowed to use it there
+          alert("Cannot use this command on current element.");
+          return;
+      }
+      // newrow and newcol are the only commands at the moment.
+      // We handle both using inserted blocks, and both commands
+      // act as toggles (remove if already present).
+      if (this.hasCommand(sectId, command)) {
+          var id = this.commandBlockId(sectId, command);
+          jQuery(this.wym._doc).find('#' + id).remove();
+          delete this.presentationInfo[id];
+      } else {
+          var newId = this.insertCommandBlock(sectId, command);
+          this.registerSection(newId);
+          this.presentationInfo[newId].push(command);
+          this.updateStyleDisplay(newId);
+      }
+      this.updateClassListItem(btn, command);
+  };
 
-    var styles = this.availableStyles;
-    for (var i = 0; i < styles.length; i++) {
-        if (styles[i].name == stylename) {
-            return styles[i];
-        }
-    }
-    var commands = this.commands;
-    for (var i2 = 0; i2 < commands; i2++) {
-        if (commands[i2].name == stylename) {
-            return commands[i2];
-        }
-    }
-    return undefined; // shouldn't get here
-};
+  PresentationControls.prototype.cleanPresentationInfo = function() {
+      // clear out orphaned items
+      var orphaned = [];
+      for (var key in this.presentationInfo) {
+          var sel = "#" + key;
+          if (!jQuery(this.wym._doc).find(sel).is(sel)) {
+              orphaned.push(key);
+          }
+      }
+      for (var i = 0; i < orphaned.length; i++) {
+          delete this.presentationInfo[orphaned[i]];
+      }
+  };
 
-// ---- Manipulation of class and command list ----
+  // ---- Error box -----
 
-PresentationControls.prototype.buildClassList = function() {
-    var self = this;
-    this.classList.empty();
-    // For each new category encountered, we have an li that contains
-    // a div with a heading, and a ul with all the actual classes.
-    // NB the category could be 'null' so care must be taken. Also,
-    // the list is already sorted by category and verbose_name
-    var state = {start:true,
-                 category:null,
-                 categoryList:null
-                };
-    jQuery.each(self.availableStyles, function(i, item) {
-        if (state.start || (state.category != item.category)) {
-            var category;
-            if (item.category == null) {
-                category = "Uncategorized";
-            } else {
-                category = item.category;
-            }
-            var classListItem = jQuery("<li class=\"class-category\"><h3 class=\"categoryHeading\">" + self.escapeHtml(category) + ':</h3></li>');
-            classListItem.appendTo(self.classList);
-            state.categoryList = jQuery("<ul></ul>");
-            state.categoryList.appendTo(classListItem);
-        }
-        var btn = jQuery("<li><a href='#'>" + self.escapeHtml(item.verbose_name) + "</a></li>").appendTo(state.categoryList).find("a");
-        // event handlers
-        var style = self.availableStyles[i];
+  PresentationControls.prototype.clearError = function() {
+      this.errorBox.empty();
+  };
 
-        btn.click(function(event) {
-                      self.toggleStyle(style, btn);
-                      event.preventDefault();
-                  });
-        state.start = false;
-        state.category = item.category;
-    });
-};
+  PresentationControls.prototype.showError = function(message) {
+      this.clearError();
+      this.errorBox.append(this.escapeHtml(message));
+  };
+  // ---- Preview ---
 
-PresentationControls.prototype.buildCommandList = function () {
-    var self = this;
-    self.commandList.empty();
-    jQuery.each(self.commands, function(i, item) {
-        var btn = jQuery("<li><a href='#'>" + self.escapeHtml(item.verbose_name) + "</a></li>").appendTo(self.commandList).find("a");
-        // event handlers
-        var command = self.commands[i];
+  PresentationControls.prototype.showPreview = function() {
+      this.prepareData();
+      var self = this;
+      jQuery.post(this.opts.previewUrl, {'html': self.wym.xhtml(),
+                                         'presentation': JSON.stringify(this.presentationInfo)
+                                        },
+                  function(data) {
+                      self.withGoodData(data,
+                          function(value) {