Luke Plant avatar Luke Plant committed f0c0333 Merge

Merge bug fix

Comments (0)

Files changed (10)

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

     this.name = wym._element.get(0).name;
     // available_styles: an array of dictionaries corresponding to PresentationInfo objects
     this.available_styles = new Array();
-    // stored_headings: array of StructureItem objects (attributes .level, .name, .sect_id, .tag)
-    this.stored_headings = new Array();
-    // active_heading_list - a possibly filtered version of stored_headings
-    this.active_heading_list = new Array();
     // presentation_info: a dictionary of { sect_id : [PresentationInfo] }
     this.presentation_info = {};
 
-    this.setup_controls(jQuery(wym._box).find(".wym_area_bottom"));
+    // commands: an array of dictionaries corresponding to PresentationInfo objects for commands.
+    this.commands = new Array();
+    // command_dict: a dictionary mapping command name to PresentationInfo object.
+    this.command_dict = {};
+
+    this.setup_controls(jQuery(wym._bvox).find(".wym_area_bottom"));
 }
 
 function escapeHtml(html) {
     return html.replace(/&/g,"&")
-	.replace(/\"/g,""")
-	.replace(/</g,"&lt;")
-	.replace(/>/g,"&gt;");
+        .replace(/\"/g,"&quot;")
+        .replace(/</g,"&lt;")
+        .replace(/>/g,"&gt;");
 }
 
 function flattenPresStyle(pres) {
     // Convert a string to PresentationInfo object
     var comps = presstr.match(/^([^:]+):(.*)$/);
     return { prestype: comps[1],
-	     name: comps[2] };
+             name: comps[2] };
 }
 
+
+PresentationControls.prototype.setup_css_storage = 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).
+
+    // 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.add_css_rule = 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.setup_controls = function(container) {
     var id_prefix = "id_prescontrol_" + this.name + "_";
-    var headingsbox_id = id_prefix + 'headings';
-    var optsbox_id = id_prefix + 'optsbox';
-    var headingsfilter_id = id_prefix + 'headingsfilter';
     var previewbutton_id = id_prefix + 'previewbutton';
     var previewbox_id = id_prefix + 'previewbox';
-    var refresh_id = id_prefix + 'refresh';
-    var newrowbutton_id = id_prefix + 'newrowbutton';
-    var newcolbutton_id = id_prefix + 'newcolbutton';
-    var removebutton_id = id_prefix + 'removebutton';
     var cleanhtmlbutton_id = id_prefix + 'cleanhtmlbutton';
     var self = this;
 
     // Create elements
     container.after(
-	"<div class=\"prescontrol\">" +
-	"<div class=\"prescontroloptsboxcont\">Presentation choices:<div class=\"prescontroloptsbox\" id=\"" + optsbox_id + "\"></div>" +
-        "<input type=\"submit\" value=\"New row\" id=\"" + newrowbutton_id  +"\" />" +
-        "<input type=\"submit\" value=\"New column\" id=\"" + newcolbutton_id  +"\" />" +
-        "<input type=\"submit\" value=\"Remove\" id=\"" + removebutton_id  +"\" />" +
-        "</div>" +
-    "<div class=\"prescontrolheadings\" style=\"margin-right: 260px;\">Document structure:<br/><select size=\"15\" id=\"" + headingsbox_id + "\"></select>" +
-	"<br/><label><input type=\"checkbox\" id=\"" + headingsfilter_id + "\"> Headings only</label></div>" +
-
-	"<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr>" +
+        "<div class=\"prescontrol\">" +
+        "<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr>" +
             "<td><input type=\"submit\" value=\"Clean pasted HTML\" id=\"" + cleanhtmlbutton_id  +  "\" /></td>" +
-	    "<td><input type=\"submit\" value=\"Refresh structure\" id=\"" + refresh_id + "\" /></td>" +
-	    "<td><input type=\"submit\" value=\"Preview\" id=\"" + previewbutton_id + "\" /></td>" +
+            "<td><input type=\"submit\" value=\"Preview\" id=\"" + previewbutton_id + "\" /></td>" +
             "<td width=\"100%\"><div class=\"prescontrolerror\" id=\"" + id_prefix + "errorbox" + "\"></div></td></tr></table>" +
         "</div>");
 
     jQuery("body").append("<div style=\"position: absolute; display: none\" class=\"previewbox\" id=\"" + previewbox_id + "\">");
 
-    this.optsbox = jQuery('#' + optsbox_id);
-    this.headingscontrol = jQuery('#' + headingsbox_id);
-    this.headingsfilter = jQuery('#' + headingsfilter_id);
+    this.classlist = jQuery(this.wym._options.classesSelector).find("ul");
+    this.commandlist = jQuery(this.wym._options.layoutCommandsSelector).find("ul");
     this.errorbox = jQuery('#' + id_prefix + "errorbox");
     this.previewbutton = jQuery('#' + previewbutton_id);
     this.previewbox = jQuery('#' + previewbox_id);
-    this.refreshbutton = jQuery('#' + refresh_id);
-    this.newrowbutton = jQuery('#' + newrowbutton_id);
-    this.newcolbutton = jQuery('#' + newcolbutton_id);
-    this.removebutton = jQuery('#' + removebutton_id);
     this.cleanhtmlbutton = jQuery('#' + cleanhtmlbutton_id);
-    this.optsbox.css('height', this.headingscontrol.get(0).clientHeight.toString() + "px");
 
-    // Initial set up
+    this.setup_css_storage();
+
+     // Initial set up
+    // Remove any tooltips
+    jQuery(".orbitaltooltip-simplebox").unbind().remove();
+
+    this.retrieve_commands();
     this.retrieve_styles();
-    // refresh_headings must come after separate_presentation
-    this.separate_presentation(function() { self.refresh_headings(); });
+    this.separate_presentation();
 
-    // Event handlers
-    this.headingscontrol.change(function(event) {
-				    self.update_optsbox();
-    });
-    this.refreshbutton.click(function(event) {
-				 self.refresh_headings();
-				 self.headingscontrol.get(0).focus();
-				 return false;
-			     });
     this.previewbutton.toggle(function(event) {
-				 self.show_preview();
-				 return false;
-			      },
-			      function(event) {
-				  self.previewbox.hide();
-			      });
-    this.headingsfilter.change(function(event) {
-				   self.update_active_heading_list();
-				   self.update_headingbox();
-			       });
-    this.newrowbutton.click(function(event) {
-                                self.insert_row();
-				return false;
-                            });
-    this.newcolbutton.click(function(event) {
-                                self.insert_column();
-				return false;
-                            });
-    this.removebutton.click(function(event) {
-                                self.remove_layout_command();
-				return false;
-                            });
+                                 self.show_preview();
+                                 return false;
+                              },
+                              function(event) {
+                                  self.previewbox.hide();
+                              });
     this.cleanhtmlbutton.click(function(event) {
                                    self.clean_html();
                                    return false;
                                });
     // Insert rewriting of HTML before the WYMeditor updates the textarea.
     jQuery(this.wym._options.updateSelector)
-	.bind(this.wym._options.updateEvent, function(event) {
-		  self.form_submit(event);
-	      });
+        .bind(this.wym._options.updateEvent, function(event) {
+                  self.form_submit(event);
+              });
 
-    // Other event handlers added in update_optsbox
+    // Fix height of classlist (in timeout to give command list time to load)
+    setTimeout(function() {
+                   var h = jQuery(" .wym_area_main").height() -
+                       jQuery(self.wym._options.containersSelector).height() -
+                       jQuery(self.wym._options.layoutCommandsSelector).height() -
+                       jQuery(self.wym._options.classesSelector + " h2").height() -
+                       20; // 20 is a fudge value, probably equal to some marings/paddings
+                   self.classlist.css("height", h.toString() + "px");
+               }, 1000);
+
+    // Stop the tooltips from getting in the way - dismiss with a click.
+    // (in timeout to give them time to be created)
+    setTimeout(function() {
+                   jQuery(".orbitaltooltip-simplebox").click(function(event) {
+                                                                 jQuery(this).hide();
+                                                             });
+               }, 1000);
+
 };
 
 PresentationControls.prototype.set_html = function(html) {
 };
 
 // Splits the HTML into 'content HTML' and 'presentation'
-PresentationControls.prototype.separate_presentation = function(andthen) {
+PresentationControls.prototype.separate_presentation = function() {
     // 'andthen' is a function to do afterwards.  This is a nasty
     // hack to get things to work.
     var self = this;
     jQuery.post(this.opts.separate_presentation_url, { html: self.wym.xhtml() } ,
-		function(data) {
-		    self.with_good_data(data,
-			function(value) {
-			    // Store the presentation
-			    self.presentation_info = value.presentation;
-			    // Update the HTML
+                function(data) {
+                    self.with_good_data(data,
+                        function(value) {
+                            // Store the presentation
+                            self.presentation_info = value.presentation;
+                            // Update the HTML
                             self.set_html(value.html);
-                            if (andthen != null) {
-                                andthen();
-                            }
-			});
-		}, "json");
+                            // Update presentation of HTML
+                            self.update_after_loading();
+                        });
+                }, "json");
+};
+
+PresentationControls.prototype.ensure_all_ids = function() {
+    // First there might be some divs that need fixing up.  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.ensure_id(this);
+                                                     });
+};
+
+PresentationControls.prototype.clean_presentation_info = function() {
+    // clear out orphaned items
+    var orphaned = [];
+    for (var key in this.presentation_info) {
+        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.presentation_info[orphaned[i]];
+    }
 };
 
 PresentationControls.prototype.form_submit = function(event) {
+    // We need to ensure all elements have ids, *and* that div elements have
+    // correct ids (which is a side effect of the below).
+    this.ensure_all_ids();
+    // this.presentation_info might have old info, if command divs have
+    // been removed.  Need to clean.
+    this.clean_presentation_info();
+
     // 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.presentation_info)
-		  },
-		  url: this.opts.combine_presentation_url,
-		  dataType: "json",
-		  async: false
+                  type: "POST",
+                  data: {
+                      html: this.wym.xhtml(),
+                      presentation: JSON.stringify(this.presentation_info)
+                  },
+                  url: this.opts.combine_presentation_url,
+                  dataType: "json",
+                  async: false
     }).responseText;
     var data = JSON.parse(res);
     if (data.result == 'ok') {
-	// Replace existing HTML with combined.
+        // Replace existing HTML with combined.
         this.set_html(data.value.html);
-	// In case the normal WYMeditor update got called *before* this
-	// event handler, we do another update.
-	this.wym.update();
+        // In case the normal WYMeditor update got called *before* this
+        // event handler, we do another update.
+        this.wym.update();
     } else {
-	event.preventDefault();
-	this.show_error(data.message);
+        event.preventDefault();
+        this.show_error(data.message);
         alert("Data in " + this.name + " can't be saved - see error message.");
     }
 };
 
-PresentationControls.prototype.build_optsbox = function() {
-    this.optsbox.empty();
-    // Remove any tooltips
-    jQuery(".orbitaltooltip-simplebox").unbind().remove();
+PresentationControls.prototype.build_classlist = function() {
+    this.classlist.empty();
 
     var self = this;
     jQuery.each(this.available_styles, function(i, item) {
-	var val = flattenPresStyle(item);
-	self.optsbox.append("<div><label><input type=\"checkbox\" value=\"" +
-			    escapeHtml(val) + "\" /> " +
-			    escapeHtml(item.verbose_name) +
-			    "</label></div>");
-	// Attach tooltip to label we just added:
-	self.optsbox.find("input[value='" + val + "']").parent().each(function() {
-	    var help = item.description;
-	    if (help == "") {
-		help = "(No help available)";
-	    }
-	    help = "<h1>" + escapeHtml(item.verbose_name) + "</h1>" + help;
-            help = help + '<br/><hr/><p>Can be used on these elements:</p><p>' + item.allowed_elements.join(' ') + '</p>';
-	    // Assign an id, because orbitaltooltip
-            // doesn't work without it.
-	    $(this).attr('id', 'id_optsbox_label_' + i);
-            var inputitem = this;
-            setTimeout(function() {
-	        $(inputitem).orbitaltooltip({
-	            orbitalPosition: 270,
-		    // Small spacing means we can move onto the tooltip
-                    // in order to scroll it if the help text has
-		    // produced scroll bars.
-		    spacing:         8,
-		    tooltipClass: 	 'orbitaltooltip-simplebox',
-		    html:            help
-	        });
-            }, 1000); // Delay, otherwise tooltips can end up in wrong position.
-	});
+        var btn = jQuery("<li><a href='#'>" + escapeHtml(item.verbose_name) + "</a></li>").appendTo(self.classlist).find("a");
+        // event handlers
+        var style = self.available_styles[i];
+        btn.click(function(event) {
+                      self.toggle_style(style);
+                  });
+        btn.hover(function(event) {
+                     self.update_classlist_item(btn, style);
+                  });
+
+        // Attach tooltip to button we just added:
+        var help = item.description;
+        if (help == "") {
+            help = "(No help available)";
+        }
+        help = "<h1>" + escapeHtml(item.verbose_name) + "</h1>" + help;
+        help = help + '<br/><hr/><p>Can be used on these elements:</p><p>' + item.allowed_elements.join(' ') + '</p>';
+        // Assign an id, because orbitaltooltip doesn't work without it.
+        btn.attr('id', 'id_classlist_' + i);
+        setTimeout(function() {
+                       btn.orbitaltooltip({
+                           orbitalPosition: 270,
+                           // Small spacing means we can move onto the tooltip
+                           // in order to scroll it if the help text has
+                           // produced scroll bars.
+                           spacing:         8,
+                           tooltipClass:         'orbitaltooltip-simplebox',
+                           html:            help
+                       });
+        }, 1000); // Delay, otherwise tooltips can end up in wrong position.
     });
-    // Stop the tooltips from getting in the way
-    // - dismiss with a click.
-    jQuery(".orbitaltooltip-simplebox").click(function(event) {
-						  jQuery(this).hide();
-					      });
+
+
 };
 
-PresentationControls.prototype.unbind_optsbox = function() {
-    // Remove existing event handlers, reset state
-    this.optsbox.find("input").unbind().removeAttr("checked").attr("disabled", true);
+PresentationControls.prototype.build_commandlist = function() {
+    this.commandlist.empty();
+
+    var self = this;
+    jQuery.each(this.commands, function(i, item) {
+        var btn = jQuery("<li><a href='#'>" + escapeHtml(item.verbose_name) + "</a></li>").appendTo(self.commandlist).find("a");
+        // event handlers
+        var command = self.commands[i];
+
+        btn.click(function(event) {
+                      self.do_command(command);
+                  });
+        btn.hover(function(event) {
+                      // update_classlist_item works for
+                      // commands as well as classes.
+                     self.update_classlist_item(btn, command);
+                  });
+
+        // Attach tooltip to label we just added:
+        var help = item.description;
+        if (help == "") {
+            help = "(No help available)";
+        }
+        help = "<h1>" + escapeHtml(item.verbose_name) + "</h1>" + help;
+        help = help + '<br/><hr/><p>Can be inserted on these elements:</p><p>' + item.allowed_elements.join(' ') + '</p>';
+        // Assign an id, because orbitaltooltip doesn't work without it.
+        btn.attr('id', 'id_commandlist_' + i);
+        setTimeout(function() {
+                       btn.orbitaltooltip({
+                           orbitalPosition: 270,
+                           // Small spacing means we can move onto the tooltip
+                           // in order to scroll it if the help text has
+                           // produced scroll bars.
+                           spacing:         8,
+                           tooltipClass:         'orbitaltooltip-simplebox',
+                           html:            help
+                       });
+        }, 1000); // Delay, otherwise tooltips can end up in wrong position.
+    });
+
 };
 
-PresentationControls.prototype.get_heading_index = function() {
-    var selected = this.headingscontrol.get(0).value;
-    if (!selected) {
-	return null;
+PresentationControls.prototype.has_style = function(sect_id, style) {
+    var styles = this.presentation_info[sect_id];
+    for (var i = 0; i < styles.length; i++) {
+        if (flattenPresStyle(styles[i]) == flattenPresStyle(style)) {
+            return true;
+        };
     }
-    return parseInt(selected, 10);
+    return false;
 };
 
-PresentationControls.prototype.update_optsbox = function() {
+PresentationControls.prototype.next_id = 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.update_command_divs = function(node, id, max) {
+    if (max == 0) {
+        return;
+    };
+    var prev = node.previousSibling;
+    if (prev && prev.tagName.toLowerCase() == 'div') {
+        // command divs are like <div id="newrow_p_1" class="newrow">
+        // check we've got a command div.
+        var className = prev.className;
+        var prev_id = prev.id;
+        if (prev_id && prev_id.match(eval("/^" + className + "/"))) {
+            prev.id = className + "_" + id;
+            // need to update presentation_info as well
+            this.presentation_info[prev.id] = this.presentation_info[prev_id];
+            delete this.presentation_info[prev_id];
+            // and then the visible indicators
+            this.update_style_display(prev.id);
+            // do the next one.
+            this.update_command_divs(prev, id, max - 1);
+        }
+    }
+};
+
+PresentationControls.prototype.assign_id = function(node, id) {
+
+    // Assign an ID to an element.  This can be tricky, because if the section
+    // has a command div before it, we need to make sure that the id of those
+    // divs are changed, since the 'position' of the command divs are only stored
+    // by giving them the right id.
+    node.id = id;
+    // Need to change (up to) the two previous siblings.
+    this.update_command_divs(node, id, 2);
+};
+
+PresentationControls.prototype.register_section = function(sect_id) {
+    // Make sure we have somewhere to store styles for a section.
+    this.presentation_info[sect_id] = new Array();
+};
+
+PresentationControls.prototype.tagname_to_selector = function(name) {
+    // Convert a tag name into a selector used to match
+    // elements that represent that tag.  This function accounts
+    // for the use of div elemennts to represent commands.
+    var c = this.command_dict[name];
+    if (c == null) {
+        // not a command
+        return name;
+    } else {
+        return "div." + c.name;
+    }
+};
+
+PresentationControls.prototype.ensure_id = function(node) {
+    var id = node.id;
+
+    if (id == undefined || id == "") {
+        id = this.next_id(node.tagName.toLowerCase());
+        this.assign_id(node, id);
+        this.register_section(id);
+    }
+    return id;
+};
+
+PresentationControls.prototype.get_current_section = function(style) {
+    var wym = this.wym;
     var self = this;
-    this.unbind_optsbox();
-    // Currently selected heading?
-    var headingIndex = self.get_heading_index();
-    if (headingIndex == null) return;
-    var sect = this.active_heading_list[headingIndex];
-    var sect_id = sect.sect_id;
-    var styles = this.presentation_info[sect_id];
-    if (styles == null) {
-	styles = new Array();
-	this.presentation_info[sect_id] = styles;
+    var expr = jQuery.map(style.allowed_elements,
+                          function(t,i) { return self.tagname_to_selector(t); }
+                         ).join(",");
+    var container = jQuery(wym.selected()).parentsOrSelf(expr);
+    if (container.is(expr)) {
+        var first = container.get(0);
+        var id = this.ensure_id(first);
+        return id;
     }
-    this.optsbox.find("input").each(
-	function(i, input) {
-	    // Set state
-	    var val = input.value;
-            if (jQuery.inArray(sect.tag.toLowerCase(), self.available_styles[i].allowed_elements) != -1) {
-                input.disabled = false;
-            } else {
-                input.disabled = true;
+    return undefined;
+};
+
+PresentationControls.prototype.toggle_style = function(style) {
+    // What section are we on?
+    var sect_id = this.get_current_section(style);
+    if (sect_id == undefined) {
+        // No allowed to use it there
+        alert("Cannot use this style on current element.");
+        return;
+    }
+
+    if (this.has_style(sect_id, style)) {
+        this.remove_style(sect_id, style);
+    } else {
+        this.add_style(sect_id, style);
+    }
+    this.update_style_display(sect_id);
+};
+
+PresentationControls.prototype.insert_command_div = function(sect_id, command) {
+    var newelem = jQuery("<div class=\"" + command.name + "\">*</div>");
+    var elem = jQuery(this.wym._doc).find("#" + sect_id);
+    // New row should appear before new col
+    if (elem.prev().is("div.newcol") && command.name == 'newrow') {
+        elem = elem.prev();
+    }
+    elem.before(newelem);
+    var new_id = command.name + "_" + sect_id; // duplication with do_command
+    newelem.attr('id', new_id);
+    this.update_style_display(new_id);
+    return new_id;
+};
+
+PresentationControls.prototype.do_command = function(command) {
+    // What section are we on?
+    var sect_id = this.get_current_section(command);
+    if (sect_id == undefined) {
+        // No allowed to use it there
+        alert("Cannot use this command on current element.");
+        return;
+    }
+    var new_id = command.name + "_" + sect_id; // duplication with insert_command_div
+    this.register_section(new_id);
+    this.presentation_info[new_id].push(command);
+    // newrow and newcol are the only commands at the moment.
+    // We handle both using inserted divs and images.
+    this.insert_command_div(sect_id, command);
+};
+
+PresentationControls.prototype.insert_command_divs = 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.presentation_info) {
+        var presinfos = this.presentation_info[key];
+        for (var i = 0; i < presinfos.length; i++) {
+            var pi = presinfos[i];
+            if (pi.prestype == 'command') {
+                var id = key.split('_').slice(1).join('_');
+                this.insert_command_div(id, this.command_dict[pi.name]);
             }
-	    jQuery.each(styles,
-		function (j, style) {
-		    if (flattenPresStyle(style) == val) {
-			input.checked = true;
-		    }
-		});
-	    // Add event handlers
-	    // Change
-	    jQuery(this).change(function(event) {
-				    var style = expandPresStyle(input.value);
-				    if (input.checked) {
-					self.add_style(sect_id, style);
-				    } else {
-					self.remove_style(sect_id, style);
-				    }
-				    self.headingscontrol.get(0).focus();
-			 });
-	});
+        }
+    }
+};
+
+PresentationControls.prototype.update_classlist_item = function(btn, style) {
+    var self = this;
+    var sect_id = this.get_current_section(style);
+    if (sect_id == undefined) {
+        // Can't use it.
+        btn.addClass("disabled");
+    } else {
+        btn.removeClass("disabled");
+    }
 };
 
 PresentationControls.prototype.show_preview = function() {
     var self = this;
+    this.ensure_all_ids();
+    this.clean_presentation_info();
     jQuery.post(this.opts.preview_url, { 'html': self.wym.xhtml(),
-					 'presentation': JSON.stringify(this.presentation_info)
-					},
-		function(data) {
-		    self.with_good_data(data,
-			function(value) {
-			    var btn = self.previewbutton;
-			    var box = self.previewbox;
-			    var pos = btn.offset();
-			    box.html(value.html);
-			    var height = box.height() + parseInt(box.css('padding-top')) + parseInt(box.css('padding-bottom')) +
-				parseInt(box.css('border-top-width')) + parseInt(box.css('border-bottom-width'));
-			    box.css("top", pos.top - height).css("left", pos.left);
-			    box.show();
-			});
-		}, "json");
+                                         'presentation': JSON.stringify(this.presentation_info)
+                                        },
+                function(data) {
+                    self.with_good_data(data,
+                        function(value) {
+                            var btn = self.previewbutton;
+                            var box = self.previewbox;
+                            var pos = btn.offset();
+                            box.html(value.html);
+                            var height = box.height() + parseInt(box.css('padding-top')) + parseInt(box.css('padding-bottom')) +
+                                parseInt(box.css('border-top-width')) + parseInt(box.css('border-bottom-width'));
+                            box.css("top", pos.top - height).css("left", pos.left);
+                            box.show();
+                        });
+                }, "json");
     return false;
 };
 
 PresentationControls.prototype.remove_style = function(sect_id, presinfo) {
     var styles = this.presentation_info[sect_id];
     styles = jQuery.grep(styles, function(item, i) {
-			     return !(item.prestype == presinfo.prestype
-				      && item.name == presinfo.name);
-			 });
+                             return !(item.prestype == presinfo.prestype
+                                      && item.name == presinfo.name);
+                         });
     this.presentation_info[sect_id] = styles;
 };
 
-PresentationControls.prototype.insert_row = function() {
-    // Insert a new row command into outline above selected item
-    var headingIndex = this.get_heading_index();
-    if (headingIndex == null) return;
-    var sect_id = this.active_heading_list[headingIndex].sect_id;
-    if (sect_id.match(/^newrow_/)) {
-        alert("Can't insert new row here");
-        return;
+PresentationControls.prototype.update_style_display = function(sect_id) {
+    var styles = this.presentation_info[sect_id];
+    var self = this;
+    var style_list = jQuery.map(styles, function(s, i) {
+                                    return self.get_verbose_style_name(s.name);
+                                }).join(", ");
+    this.add_css_rule("#" + sect_id + ":before", 'content: "' + style_list + '"');
+};
+
+PresentationControls.prototype.update_all_style_display = function() {
+    for (var key in this.presentation_info) {
+        this.update_style_display(key);
     }
+};
 
-    for (var i=0; i < this.stored_headings.length; i++) {
-        if (this.stored_headings[i].sect_id == sect_id) {
-            // Update the headings:
-            this.insert_row_command(this.stored_headings, i, sect_id);
-            // Update presentation info:
-            // An empty array will do, don't actually need a
-            // command in there (and the GUI will overwrite any
-            // anything we put in it)
-            this.presentation_info['newrow_' + sect_id] = Array();
-            break;
+PresentationControls.prototype.update_after_loading = function() {
+    this.insert_command_divs();
+    this.update_all_style_display();
+};
+
+PresentationControls.prototype.get_verbose_style_name = function(stylename) {
+    // Full style information is not stored against individual headings, only
+    // the type and name. So sometimes we need to go from one to the other.
+
+    var styles = this.available_styles;
+    for (var i = 0; i < styles.length; i++) {
+        if (styles[i].name == stylename) {
+            return styles[i].verbose_name;
         }
     }
-
-    // Refresh GUI
-    this.update_active_heading_list();
-    this.update_headingbox();
-
-    // select what was selected before
-    this.headingscontrol.get(0).value = headingIndex;
-};
-
-PresentationControls.prototype.insert_row_command = function(arr, i, sect_id) {
-    arr.splice(i, 0, {level:1,
-                      name:"New row",
-                      tag:"row",
-                      sect_id:"newrow_" + sect_id
-                     });
-};
-
-
-PresentationControls.prototype.insert_column = function() {
-    // Insert a new row command into outline above selected item
-    var headingIndex = this.get_heading_index();
-    if (headingIndex == null) return;
-    var sect_id = this.active_heading_list[headingIndex].sect_id;
-    if (sect_id.match(/^newrow_/) || sect_id.match(/^newcol_/)) {
-        alert("Can't insert new column here");
-        return;
-    }
-
-    for (var i=0; i < this.stored_headings.length; i++) {
-        if (this.stored_headings[i].sect_id == sect_id) {
-            // Update the headings:
-            this.insert_column_command(this.stored_headings, i, sect_id);
-            // Update presentation info:
-            // An empty array will do, don't actually need a
-            // command in there (and the GUI will overwrite any
-            // anything we put in it)
-            this.presentation_info['newcol_' + sect_id] = Array();
-            break;
+    var commands = this.commands;
+    for (var i2 = 0; i2 < commands; i2++) {
+        if (commands[i2].name == stylename) {
+            return commands[i2].verbose_name;
         }
     }
-    // Refresh GUI
-    this.update_active_heading_list();
-    this.update_headingbox();
-
-    // select what was selected before
-    this.headingscontrol.get(0).value = headingIndex;
-};
-
-PresentationControls.prototype.insert_column_command = function(arr, i, sect_id) {
-    arr.splice(i, 0, {level:1,
-                      name:"New column",
-                      tag:"column",
-                      sect_id:"newcol_" + sect_id
-                     });
-};
-
-PresentationControls.prototype.remove_layout_command = function() {
-    var headingIndex = this.get_heading_index();
-    if (headingIndex == null) return;
-    var sect_id = this.active_heading_list[headingIndex].sect_id;
-    if (!sect_id.match(/^newrow_/) && !sect_id.match(/^newcol_/)) {
-        alert("Can only use 'remove' on new row or column commands");
-        return;
-    }
-    // Update heading list
-    this.stored_headings.splice(headingIndex, 1);
-    // Update presentation info
-    delete this.presentation_info[sect_id];
-    // Refresh GUI
-    this.update_active_heading_list();
-    this.update_headingbox();
-
-    if (headingIndex > 0) {
-        this.headingscontrol.get(0).value = headingIndex - 1;
-    }
+    return undefined; // shouldn't get here
 };
 
 PresentationControls.prototype.clean_html = function() {
                 function(data) {
                     self.with_good_data(data, function(value) {
                                             self.set_html(value.html);
+                                            self.update_after_loading();
                                         });
                 }, "json");
 };
 // with the value in the data.
 PresentationControls.prototype.with_good_data = function(data, callback) {
     if (data.result == 'ok') {
-	this.clear_error();
-	callback(data.value);
+        this.clear_error();
+        callback(data.value);
     } else {
-	// TODO - perhaps distinguish between a server error
-	// and a user error
-	this.show_error(data.message);
+        // TODO - perhaps distinguish between a server error
+        // and a user error
+        this.show_error(data.message);
     }
 };
 
     this.errorbox.append(escapeHtml(message));
 };
 
-PresentationControls.prototype.refresh_headings = function() {
-    var self = this;
-    var html = this.wym.xhtml();
-    jQuery.post(this.opts.extract_structure_url, { 'html':html },
-	function(data) {
-	    self.with_good_data(data, function(value) {
-		self.stored_headings = value;
-                self.add_layout_to_headings();
-		self.update_active_heading_list();
-		self.update_headingbox();
-	    });
-	}, "json");
-};
-
-PresentationControls.prototype.add_layout_to_headings = function() {
-    // Row/Column info is transmitted as part of presentation info
-    // but it is displayed in the headings list, because it represents
-    // divs that are inserted (and can be styled).  So we need to
-    // update this.stored_headings using this.presentation_info
-    var first_sect_id;
-    if (this.stored_headings.length > 0) {
-        first_sect_id = this.stored_headings[0].sect_id;
-    } else {
-        first_sect_id = '';
-    }
-    for (var i = 0; i < this.stored_headings.length; i++) {
-        // Loop through, if any have corresponding newrow or newcol
-        // entries, then add them into list.
-        var sect_id = this.stored_headings[i].sect_id;
-        if (this.presentation_info['newrow_' + sect_id] != null) {
-            this.insert_row_command(this.stored_headings, i, sect_id);
-            i++;
-        }
-        if (this.presentation_info['newcol_' + sect_id] != null) {
-            this.insert_column_command(this.stored_headings, i, sect_id);
-            i++;
-        }
-    }
-
-    if (this.stored_headings.length > 0) {
-        // If after this process we don't have newrow and newcol commands
-        // at the beginning of the heading list, we add them.
-        if (this.stored_headings[0].tag != 'row') {
-            this.insert_row_command(this.stored_headings, 0, first_sect_id);
-        }
-        if (this.stored_headings[1].tag != 'column') {
-            this.insert_column_command(this.stored_headings, 1, first_sect_id);
-        }
-    }
-
-};
-
-
-PresentationControls.prototype.update_active_heading_list = function() {
-    var self = this;
-    var items;
-    if (this.headingsfilter.get(0).checked) {
-	items = jQuery.grep(self.stored_headings,
-                            function(item, idx) {
-				return (item.tag.toUpperCase()).match(/H\d/);
-			    });
-    } else {
-	items = self.stored_headings;
-    }
-    self.active_heading_list = items;
-};
-
-PresentationControls.prototype.update_headingbox = function() {
-    var self = this;
-    this.unbind_optsbox();
-    this.headingscontrol.empty();
-    jQuery.each(this.active_heading_list, function(i, item) {
-		    var spaces = (new Array((item.level - 1)*3)).join("&nbsp;");
-                    var caption = "";
-                    var tag = item.tag.toLowerCase();
-                    if (tag == 'row' || tag == 'column') {
-                        caption = escapeHtml(item.name);
-                    } else {
-                        caption = tag + ": " + escapeHtml(item.name);
-                    }
-		    self.headingscontrol.append("<option class ='" + tag + "' value='" + i.toString() + "'>" + spaces + caption + "</option>");
-    });
-};
-
 PresentationControls.prototype.retrieve_styles = function() {
     // Retrieve via AJAX
     var self = this;
     jQuery.getJSON(this.opts.retrieve_styles_url, {'template':this.opts.template,
                                                    'page_id':this.opts.page_id
                                                   },
-		   function (data) {
-		       self.with_good_data(data, function(value) {
-			   self.available_styles = data.value;
-			   self.build_optsbox();
-		       });
-		   });
+                   function (data) {
+                       self.with_good_data(data, function(value) {
+                           self.available_styles = data.value;
+                           self.build_classlist();
+                       });
+                   });
 };
 
+PresentationControls.prototype.retrieve_commands = function() {
+    var self = this;
+    jQuery.getJSON(this.opts.retrieve_commands_url, {},
+                  function (data) {
+                      self.with_good_data(data, function(value) {
+                                              self.commands = data.value;
+                                              for (var i = 0; i < self.commands.length; i++) {
+                                                  var c = self.commands[i];
+                                                  self.command_dict[c.name] = c;
+                                              }
+                                              self.build_commandlist();
+                                          });
+                  });
+};
 
 // The actual WYMeditor plugin:
 WYMeditor.editor.prototype.semantic = function(options) {

semanticeditor/media/semanticeditor/javascript/wymeditor/skins/semanticeditor/skin.css

             background: #cde8cd ;
         }
         .wym_skin_semanticeditor .wym_section a:hover    { background-color: #903; color: #cde8cd;}
+
+        .wym_skin_semanticeditor .wym_section a.disabled { color: #888; cursor: default; background-color: #dde8dd; }
+        .wym_skin_semanticeditor .wym_section a.disabled:hover    { color: #888; background-color: #dde8dd; }
       /*hide section titles by default*/
         .wym_skin_semanticeditor .wym_section h2         { display: none; }
       /*disable any margin-collapse*/
         .wym_skin_semanticeditor .wym_buttons li.wym_tools_html a          { background-position: 0 -193px;}
         .wym_skin_semanticeditor .wym_buttons li.wym_tools_preview a       { background-position: 0 -408px;}
 
+/*CLASSES*/
+        .wym_skin_semanticeditor .wym_classes ul             { overflow-y: auto;
+                                                               overflow-x: visible; }
+
+
 /*DECORATION*/
         .wym_skin_semanticeditor .wym_section h2             { background: #fff;}
         .wym_skin_semanticeditor .wym_section h2 span        { color: gray;}
             * html .wym_dialog div.row       { height: 1%; }
             .wym_dialog div.row              { display: block; }
             /* End hide from IE-mac */                
-            
+
 /*WYMEDITOR_LINK*/
         a.wym_wymeditor_link        { text-indent: -9999px; float: right; display: block; width: 50px; height: 15px; background: url(../wymeditor_icon.png);  overflow: hidden; text-decoration: none;  }
 
 	.prescontrol td             { padding: 0; }
 	.prescontrolheadings        { margin-right: 260px; }
 	.prescontrolheadings select { background: inherit; border: none; width: 100%;}
-	.prescontroloptsboxcont div { clear:left; }
-	.prescontroloptsbox         { overflow-y: scroll; border: none; }
+	.prescontrolclasslistcont div { clear:left; }
+	.prescontrolclasslist         { overflow-y: scroll; border: none; }
 	.prescontrolerror           { color: red; text-align: center; padding-top: 5px; }
-	.prescontroloptsboxcont {width: 220px; float: right;}
-	.prescontrol .prescontroloptsbox label {
+	.prescontrolclasslistcont {width: 220px; float: right;}
+	.prescontrol .prescontrolclasslist label {
 	    /* Override some stuff set by Django admin CSS*/
 	    display: inline;
 	    width: auto;

semanticeditor/media/semanticeditor/javascript/wymeditor/skins/semanticeditor/skin.js

 
     init: function(wym) {
 
-        //render following sections as panels
+        // render 'classes' as a panel, but remove contents
+        // (will be added later dynamically)
         jQuery(wym._box).find(wym._options.classesSelector)
-	    .remove();
+	    .addClass("wym_panel").find("li").remove();
+
+        // add an additional box for 'commands'.
+        // This will be filled in later in the 'semantic' plugin.
+        wym._options.layoutCommandsSelector = ".wym_layout_commands";
+        jQuery(wym._box).find(wym._options.classesSelector).before('<div class="wym_layout_commands wym_section wym_panel"><h2>Commands</h2><ul></ul></div>');
 
         //render following sections as buttons
         jQuery(wym._box).find(wym._options.toolsSelector)

semanticeditor/media/semanticeditor/wymeditor/iframe/default/wymiframe.css

  
 /* make HTML blocs visible */
   p,
+  div,
   h1,
   h2,
   h3,
   blockquote	{font-weight: light; color: #666;}
 
   p,
+  div,
   h1,
   h2,
   h3,
   ol,
   table,
   blockquote,
-  pre   {margin: 0; 
-      padding: .5em 0 .5em 2em;}
+  pre
+  {
+      margin: 0;
+      padding: 10px 0 .5em 2em;
+      position: relative; /* to establish containing block for ':before' elements */
+  }
+  li
+  {
+      position: relative;
+      padding-top: 10px;
+  }
+
+
+  p:before,
+  div:before,
+  h1:before,
+  h2:before,
+  h3:before,
+  h4:before,
+  h5:before,
+  h6:before,
+  ul:before,
+  ol:before,
+  li:before,
+  table:before,
+  blockquote:before,
+  pre:before
+  {
+      position: absolute;
+      left: 0px;
+      top: 0px;
+      font-size: 8px;
+      color: #888;
+  }
+
+  li:before
+  {
+      left: -2em;
+  }
+
+
+  div.newrow:after {
+      content:" New row";
+  }
+
+  div.newcol:after {
+      content:" New column";
+  }
+
+  div.newrow, div.newcol {
+      background-color: #f8f8f8;
+      font-size: 70%;
+      border-top: 1px solid #ffffff;
+      border-bottom: 1px solid #f0f0f0;
+  }
 
   ol,ul	{border: none;
 		padding: 1em 2em;
 		background-image: url(arkestra-lbl-list.png);
 	  }
 
-  li	{margin-left: 1em; 
-	padding-left: 1em;
-	list-style-position: outside;}
+  li
+  {
+      margin-left: 1em;
+      padding-left: 1em;
+      list-style-position: outside;
+  }

semanticeditor/models.py

 
     allowed_elements = models.CharField("Allowed HTML elements", max_length=255,
                                         help_text="A space separated list of HTML "
-                                        "element names.  Use 'row' or 'column' to indicate "
+                                        "element names.  Use 'newrow' or 'newcol' to indicate "
                                         "it can be applied to layout rows or columns ",
-                                        default="h1 h2 h3 h4 h5 h6 p blockquote ul li row column")
+                                        default="h1 h2 h3 h4 h5 h6 p blockquote ul li newrow newcol")
 
     column_equiv = models.IntegerField("Column count equivalent", null=True, blank=True,
                                        help_text="For classes designed to be applied to "

semanticeditor/templates/semanticeditor/editorwidget.html

             symanticopts = {
                 extract_structure_url: "{% url semantic.extract_structure %}",
                 retrieve_styles_url: "{% url semantic.retrieve_styles %}",
+                retrieve_commands_url: "{% url semantic.retrieve_commands %}",
                 separate_presentation_url: "{% url semantic.separate_presentation %}",
                 combine_presentation_url: "{% url semantic.combine_presentation %}",
                 clean_html_url: "{% url semantic.clean_html %}",

semanticeditor/tests.py

         LayoutDetails.max_columns = self._old_max_columns
         super(TestCase, self).tearDown()
 
+    def test_remove_command_divs(self):
+        self.assertEqual('<div class="row"><div><div><p>Test</p></div></div></div>', format_html('<div class="newrow">* </div><p>Test</p>', {}))
+
     def test_empty(self):
         self.assertEqual('<div class="row"/>', format_html('', {}));
 

semanticeditor/urls.py

 from django.conf.urls.defaults import patterns, url
-from semanticeditor.views import extract_structure_view, retrieve_styles, separate_presentation, combine_presentation, clean_html_view, preview
+from semanticeditor.views import *
 
 urlpatterns = patterns('',
     url(r'extract_structure/', extract_structure_view, name="semantic.extract_structure"),
     url(r'retrieve_styles/', retrieve_styles, name="semantic.retrieve_styles"),
+    url(r'retrieve_commands/', retrieve_commands, name="semantic.retrieve_commands"),
     url(r'separate_presentation/', separate_presentation, name="semantic.separate_presentation"),
     url(r'combine_presentation/', combine_presentation, name="semantic.combine_presentation"),
     url(r'clean_html/', clean_html_view, name="semantic.clean_html"),

semanticeditor/utils/presentation.py

     Shortcut for creating commands
     """
     return PresentationInfo(prestype="command",  name=name,
-                            verbose_name=verbose_name, description=description)
+                            verbose_name=verbose_name, description=description,
+                            allowed_elements=sorted(list(technical_blockdef)))
 
 NEWROW = PresentationCommand('newrow',
                              verbose_name = "New row",
             child.tag = 'p'
         _replace_block_elements(child)
 
+def _remove_command_divs(elem):
+    for child in reversed(elem.getchildren()):
+        _remove_command_divs(child)
+        if child.tag == 'div':
+            classes = set(_get_classes_for_node(child))
+            if NEWROW.name in classes or NEWCOL.name in classes:
+                elem.remove(child)
+
 def clean_tree(root):
     """
     Cleans dirty HTML from an ElementTree
     # If there is text directly in body, it needs wrapping in a block element.
     _promote_child_text(body, 'p')
 
+    # replace 'command' divs
+    _remove_command_divs(body)
+
     # First replace divs
     _replace_block_elements(body)
 

semanticeditor/views.py

     return success(map(PI_to_dict,retval))
 
 @json_view
+def retrieve_commands(request):
+    return success(map(PI_to_dict, [NEWROW, NEWCOL]))
+
+@json_view
 def separate_presentation(request):
     """
     Returns a JSON object:
 def combine_presentation(request):
     """
     Combines submitted 'html' and 'presentation' data,
-    returning a dictionary containg { html: <combined html> }
+    returning a dictionary containing { html: <combined html> }
     """
     html = request.POST.get('html', '')
     presentation = request.POST.get('presentation', '{}')
     presentation = simplejson.loads(presentation)
     presentation = _convert_pres(presentation)
-
     return graceful_errors(AllUserErrors, lambda: dict(html=format_html(html, presentation, pretty_print=True)))
 
 @json_view
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.