Commits

Luke Plant committed 0f39f05

"New row" and "New column" buttons moved, and new row/col represented in main window

Comments (0)

Files changed (10)

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

     // presentation_info: a dictionary of { sect_id : [PresentationInfo] }
     this.presentation_info = {};
 
-    this.setup_controls(jQuery(wym._box).find(".wym_area_bottom"));
+    // an array of dictionaries corresponding to PresentationInfo objects for commands.
+    this.commands = new Array();
+    // a dictionary mapping command name to PresentationInfo object.
+    this.command_dict = {};
+
+
+    this.setup_controls(jQuery(wym._bvox).find(".wym_area_bottom"));
 }
 
 function escapeHtml(html) {
     // Create elements
     container.after(
         "<div class=\"prescontrol\">" +
-        "<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>" +
     jQuery("body").append("<div style=\"position: absolute; display: none\" class=\"previewbox\" id=\"" + previewbox_id + "\">");
 
     this.classlist = jQuery(this.wym._options.classesSelector).find("ul");
+    this.commandlist = jQuery(this.wym._options.layoutCommandsSelector).find("ul");
     this.headingscontrol = jQuery('#' + headingsbox_id);
     this.headingsfilter = jQuery('#' + headingsfilter_id);
     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.newrowbutton = jQuery('#' + newrowbutton_id);
+    //this.newcolbutton = jQuery('#' + newcolbutton_id);
+    //this.removebutton = jQuery('#' + removebutton_id);
     this.cleanhtmlbutton = jQuery('#' + cleanhtmlbutton_id);
 
     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(function() {
+                                   // these must come after separate_presentation
+                                   self.insert_command_divs();
+                                   self.update_all_style_display();
+                               });
 
     // Event handlers
     this.headingscontrol.change(function(event) {
                                    self.update_active_heading_list();
                                    self.update_headingbox();
                                });
-    this.newrowbutton.click(function(event) {
+/*    this.newrowbutton.click(function(event) {
                                 self.insert_row();
                                 return false;
                             });
     this.removebutton.click(function(event) {
                                 self.remove_layout_command();
                                 return false;
-                            });
+                            });*/
     this.cleanhtmlbutton.click(function(event) {
                                    self.clean_html();
                                    return false;
                   self.form_submit(event);
               });
 
+    // 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) {
                 }, "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.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();
+
     // 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: {
 
 PresentationControls.prototype.build_classlist = function() {
     this.classlist.empty();
-    // Remove any tooltips
-    jQuery(".orbitaltooltip-simplebox").unbind().remove();
 
     var self = this;
     jQuery.each(this.available_styles, function(i, item) {
                      self.update_classlist_item(btn, style);
                   });
 
-        // Attach tooltip to label we just added:
+        // 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.
+        // Assign an id, because orbitaltooltip doesn't work without it.
         btn.attr('id', 'id_classlist_' + i);
         setTimeout(function() {
                        btn.orbitaltooltip({
                        });
         }, 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();
-                                              });
 
-    // Fix height of classlist.
-    var h = jQuery(" .wym_area_main").height() -
-        jQuery(this.wym._options.containersSelector).height() -
-        jQuery(this.wym._options.layoutCommandsSelector).height() -
-        jQuery(this.wym._options.classesSelector + " h2").height() -
-        20; // 20 is a fudge value, probably equal to some marings/paddings
-    this.classlist.css("height", h.toString() + "px");
 
 };
 
-PresentationControls.prototype.unbind_classlist = function() {
-    // Remove existing event handlers, reset state
-    this.classlist.find("a").unbind().addClass("disabled");
+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() {
     return false;
 };
 
-PresentationControls.prototype.assign_id = function(node) {
+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 tagName = node.tagName.toLowerCase();
     var i = 1;
     var id = "";
     while (true) {
     }
 };
 
-PresentationControls.prototype.register_section = function(sect_id, tag) {
-    // This is a temporary fix to make sure variables are in consistent
-    // state. We have to do a server side call to get proper values,
-    // but we delay that until needed.
+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);
+        }
+    }
+};
 
-    var structureItem = {
-        sect_id: sect_id,
-        tag: tag,
-        level: undefined, // Difficult to calculate client side
-        name: undefined // Difficult to calculate client side
-    };
-    this.stored_headings.push(structureItem);
-    this.presentation_info[sect_id] = Array();
+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;
-    // TODO - images for new row/column
-    // var container = (wym._selected_image ? wym._selected_image 
-    //                 : jQuery(wym.selected()));
-    // TODO - limit selection to allowed elements for style - fix for 'row', 'col'
-
-    var expr = style.allowed_elements.join(",");
+    var self = this;
+    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 = first.id;
-        if (id == undefined || id == "") {
-            id = this.assign_id(first);
-            this.register_section(id, first.tagName.toLowerCase());
-            first.id = id;
-        }
+        var id = this.ensure_id(first);
         return id;
     }
     return undefined;
     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.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]);
+            }
+        }
+    }
+};
+
 PresentationControls.prototype.update_classlist_item = function(btn, style) {
     var self = this;
     var sect_id = this.get_current_section(style);
     var style_list = jQuery.map(styles, function(s, i) {
                                     return self.get_verbose_style_name(s.name);
                                 }).join(", ");
-    if (sect_id.match(/^nerow_/)) {
-        // TODO
-    } else if (sect_id.match(/^newcol_/)) {
-        // TODO
-    } else {
-        this.add_css_rule("#" + sect_id + ":before", 'content: "' + style_list + '"');
-    }
+    this.add_css_rule("#" + sect_id + ":before", 'content: "' + style_list + '"');
 };
 
 PresentationControls.prototype.update_all_style_display = function() {
-    for (var i=0; i < this.stored_headings.length; i++) {
-        this.update_style_display(this.stored_headings[i].sect_id);
+    for (var key in this.presentation_info) {
+        this.update_style_display(key);
     }
 };
 
             return styles[i].verbose_name;
         }
     }
-    if (stylename == "newrow") {
-        return "New row";
-    } else if (stylename == "new column") {
-        return "New column";
+    var commands = this.commands;
+    for (var i2 = 0; i2 < commands; i2++) {
+        if (commands[i2].name == stylename) {
+            return commands[i2].verbose_name;
+        }
     }
     return undefined; // shouldn't get here
 };
 
 PresentationControls.prototype.insert_row = function() {
     // Insert a new row command into outline above selected item
+    var sect_id = get_current_section(this.newrow_style);
     var headingIndex = this.get_heading_index();
     if (headingIndex == null) return;
     var sect_id = this.active_heading_list[headingIndex].sect_id;
             // 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();
+            this.presentation_info['newrow_' + sect_id] = new Array();
             break;
         }
     }
             // 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();
+            this.presentation_info['newcol_' + sect_id] = new Array();
             break;
         }
     }
                    });
 };
 
+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

         .wym_skin_semanticeditor .wym_buttons li.wym_tools_preview a       { background-position: 0 -408px;}
 
 /*CLASSES*/
-        .wym_skin_semanticeditor .wym_classes ul             { overflow-y: auto; }
+        .wym_skin_semanticeditor .wym_classes ul             { overflow-y: auto;
+                                                               overflow-x: visible; }
 
 
 /*DECORATION*/

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

         // 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/></div>');
+        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,
 
 
   p:before,
+  div:before,
   h1:before,
   h2:before,
   h3:before,
   }
 
 
+  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);

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