Garbage Burrito

Inside DK: Content Management

Inside DK: Content Management
Ben Kittrell - 02 22, 2007 @ 11:40AM
Comments: 1

One of the core features I wanted to implement when I started designing doodlekit was the Content Management.  CMS Software can get very complicated, very quickly.  You have approval workflows, publishing, asset management, and security.  We were targeting small businesses and families that would probably end up in tears faced with this kind of complexity.  So I started thinking, what's the easiest way to edit a web page?  What if you could go to that page, click edit, change something,  click save, and see your change right there and then?  That's about as simple as it gets.

The concept is simple, you have a link at the bottom of the page that says "edit this page".  When that link is clicked, a form containing an editor is loaded through Ajax and displayed over the content.  After the content is modified you click save and the data is saved through another Ajax call.  The form is hidden and the content dynamically reloaded, all without a page refresh.  

Quick Edits

This idea spawned what would define the majority of doodlekit's interface, we call it the Quick Edit.  These are used in all areas of the site to give users a way to quickly change things without a complicated admin interface.  

We start out with a two divs, the overlay, and the quick_form.  These are hidden by default and displayed when necessary.  The overlay div is used to place a semi-transparent veil over the entire page. Some CSS trickery caters to IE6 and it's lack of alpha PNG support.  The quick_form div is centered horizontally using CSS, and vertically using Javascript.  This is because the height of the div stretches based on the content loaded.  Writing all this down is giving me some ideas on how to improve it; but for now, it aint broke.  

The actual CSS and javascript is too verbose to include in this post.  Here is an example html file that I whipped up real quick like.

The Ajax link code was getting pretty gnarly, so I moved it into a helper

  def edit_page_link
    link_to_remote("edit this page",
      { :update => 'quick_edit_content',
        :url => { :action => 'quick_edit', :id => @page.id },
        :complete => "showQuickForm();addTiny('page[content]');afterQuickForm();" },
      :class  => 'crud_link',
      :id => 'quick_edit_link')
  end
 
Pretty basic Rails/Prototype Ajax stuff.  You'll see that after the content is loaded I show the quick form and overlay, load up the TinyMCE editor, and the after callback will center the div vertically.  The order is important.

The form code was also moved to a helper.

  def edit_page_form
    form_remote_tag(:update => 'main_content',
        :url => { :controller => 'pages', :action => 'quick_save' },
        :before => "hideQuickForm();removeTiny('page[content]');",
        :complete => "new Effect.Highlight('quick_content');")
  end
 
Again, pretty basic.  The quick_save action saves and returns the rendered content.  It hides the quick form and overlay and removes the TinyMCE control.  After the content is dynamically updated it highlights it to imply that it's changed.
 
WYSIWYG

If this tool is going to be used by laymen, then it pretty much has to have a WYSIWYG.  I had experience with TinyMCE, and was impressed, so I went with it.  Right off the bat I had troubles loading it through Ajax.  The way the quick edit works is by loading the form dynamically through Ajax into a hidden div, and then displaying that div.  TinyMCE assumes that the form is already on the page.  The fix was easy, but not easy to find.

I setup the editor with the normal tinyMCE.init( method, however the 'mode' should be set to...

  mode : "specific_textareas",
    
This way it doesn't try to automatically apply the editor to all textareas on the page.  Then I added the following javascript method. This is called after the Ajax load is complete. Don't ask me to explain it.  

  function addTiny(el) {
    if (window.tinyMCE) {
        tinyMCE.idCounter=0;
        tinyMCE.execCommand("mceAddControl",false,el);
        tinyMCE.execInstanceCommand(el, 'mceFocus');
    }
  }
 
That looked good, but when I hit save, it didn't work.  This is my theory on what's happening (yes I'm too lazy to verify it).  TinyMCE uses hooks in the form to transfer the data from the editor to the actual textarea when the form is submitted.  I'm assuming that Prototypes Ajax form submission somehow bypasses these hooks.  So I just have to manually call this callback.
 
  function saveTiny() {
    if (window.tinyMCE) {
        tinyMCE.triggerSave();
    }
    return true;
  }
 
Then the submit button...

  <%= submit_tag 'Save', :onclick => "return saveTiny();" %>
 
I also wrote a GZip Compressor for TinyMCE.

URLs

I'm a stickler for pretty URLs, especially if it's a link that people are likely to send to others.  I wanted each page to have a nice URL, instead of ?page=1.  

Each page record in the database has a 'handle', which is dynamically generated from the user provided page title.  If same page title already exists, then an incrementing number is placed at the end.  Every site has a Home page that cannot be deleted.  It's handle is automatically set to 'index'.

Here's how it works

    @page.handle = @page.page_name.gsub(/[^\w|\s]+/, "").gsub(/\s+/, "_").downcase

    count = 1;
    while Page.find_by_handle(@page.handle, @site)
      @page.handle = name + count.to_s
      count += 1
    end
    
I take the page name, strip anything that's not a word character or space, change spaces to underscores, and make it lowercase.  I start searching for another page with the same handle, and add a number to the end if one is found.

The routing for this is very simple.  I just add this to my routes.rb file.

  map.connect '/home/:id', :controller => "pages"

Now I can go to http://garbageburrito.com/home/about, and it will forward me to the pages controller with the id parameter set to 'about'.

The action in the pages controller looks like this.

  def show
    @handle = params[:id] || "index"
    @page = Page.find_by_handle(@handle, @site)
    if @page.private
      check_login
    end
  end
 
If there's no handle provided then default to index.  Then just look up the page by the handle and check do a security check.  Notice that the site object is passed to the query to ensure you can only pull pages within this site's scope.  For more on that see Inside DK: Multi-site Rails Applications.

I would have liked to cover asset management and the menu system, however I think trying to squeeze those into this article would be too much.

Doodlekit's content management is so fluid and simple I don't even like to call it CMS, I prefer dynamic pages.  This is the core concept that led the other features such as the blog and forums to fruition.  I can only hope that we can constrain new features to be this elegant.

Comments: 1

Comments

1. Thao Nguyen - 05 15, 2007 @ 09:20PM

Searching for Best Local Tour Operator in Vietnam, Budget Vietnam Travel,Luxury Vietnam Travel,Luxury Vietnam,Vietnam Best Tours,Luxury Package

Post a Comment




powered by : Doodlekit Online Free Website Builder : developed by : Doodlebit™ Website Company