Problem statement
As you might already know, this website's back-end is a self-developped CMS. While it is not near as powerful and polished as many of the available alternatives, creating it yourself is a great opportunity for a starter to learn the basics and experience the possibilities and limits of the languages. One of the core functionalities of a CMS is of course to manage your content and more specifically to create and alter web pages.
The website uses a templating system to create pages based on a content database. The goal is create a tool in the CMS, which allows to change the order in which the database items appear on the page by simply dragging them around. Besides that, pages also have columns, so the tool should allow to easily drag the items in or out of a column (nested lists!).
Let's take a look at the features this article wants to achieve in this demo:
- Create a draggable interface, which allows the user to change the look of a page by dragging around items as they appear on the page.
- All items should be draggable, whether they are part of the main parent, or part of a column.
- For this example the columns themselves are not draggable. This means they can not be removed or made by dragging (e.g. a block with 2 column's will not get more or less columns by dragging elements. There are two reasons for this:
- First, how columns look is CSS related. 2 columns might be 50%-50%, but also 70%-30% etc. Implementing this in the code makes it more complex in code, as well as for the user.
- Secondly, as you'll see the drag behaviour is not perfect. Including draggable columns requires quite a bit of tweaks to make it work well.
- Finally, the code should output the list tree, so the newly sorted list can be processed and the order of items saved.
Because the items (and column wraps) are draggable, but the columns themselves are not, the code has to allow some levels of the nested lists to be blocked from dragging, while allowing their childs to be draggable again.
HTML template
If you look at the source code of this page, you'll see the template that is used to create a web page. Parts of the page are created in block elements (for our example we'll use divs). To create a column section, the template creates a parent wrapper, then the two columns and finally inside these columns items can be added inside divs. The code belows gives a simplified example, which in total has 10 visible items. Everything is then wrapped in a main parent, which might be your <body>
tag or your content section.
<div class="main-parent"> <div>Item 1</div> <div class="columns-parent"> <!-- Parent P1 --> <div class="column"> <!-- Sub-Parent C1 --> <div>Item 2 - P1-C1-K1</div> <!-- Child(kid) K1 --> <div>Item 3 - P1-C1-K2</div> <!-- Child(kid) K2 --> </div> <div class="column"> <!-- Sub-Parent C2 --> <div>Item 4 - P1-C2-K1</div> <div>Item 5 - P1-C2-K2</div> </div> </div> <div>Item 6</div> <div class="columns-parent"> <!-- Parent P2 --> <div class="column"> <div>Item 7 - P2-C1-K1</div> <div>Item 9 - P2-C2-K1</div> </div> <div class="column"> <div>Item 9 - P2-C2-K1</div> <div>Item 10 - P2-C2-K2</div> </div> </div> </div>
Because the website uses a templating system, the content of each items is stored but not the <div>
tags temselves. Taking advantage of this system the same output is easily generated using list items <ol>
and <li>
. The items inside the columns are now items of the list column, which is nested inside the columns-parent list, which in turn is nested inside the main-parent list. Note that the nested lists are placed inside a <li>
element!
One of the goals is to output the list tree. For this the HTML id
property will be used and added for each <li>
item.
The example below is also used in the demo.
<ol class="main-parent"> <li id="1">Item 1</li> <li id="P1"><ol class="columns-parent"> <!-- Parent P1 --> <li id="P1-C1"><ol class="column"> <!-- Sub-Parent C1 --> <li id="2">Item 2 - P1-C1-K1</li> <!-- Child(kid) K1 --> <li id="3">Item 3 - P1-C1-K2</li> <!-- Child(kid) K2 --> </ol></li> <li id="P1-C2"><ol class="column"> <!-- Sub-Parent C2 --> <li id="4">Item 4 - P1-C2-K1</li> <li id="5">Item 5 - P1-C2-K2</li> </ol></li> </ol></li> <li id="6">Item 6</li> <li id="P2"><ol class="columns-parent"> <!-- Parent P2 --> <li id="P2-C1"><ol class="column"> <li id="7">Item 7 - P2-C1-K1</li> <li id="8">Item 9 - P2-C2-K1</li> </ol></li> <li id="P2-C2"><ol class="column"> <li id="9">Item 9 - P2-C2-K1</li> <li id="10">Item 10 - P2-C2-K2</li> </ol></li> </ol></li> </ol>
CSS Styling
The CSS should correspond to the CSS of the webpage you want to change. For this example we'll take a simplified CSS. To make the columns, the flex
property is used. Next, the code belows adds some styilng to make the different parent/childs more visible.
body{width:100%;} ol{list-style:none;margin:0;padding:0;} li{padding:0;margin:0;} .main-parent{ max-width:600px; } .main-parent li{ min-height:40px; background-color:#dbdcef; margin:10px; padding:10px; border:2px dashed #737ae3; } .columns-parent{ display:flex; } .columns-parent > li { flex:1; background-color:#e6c39b; border-color:#d98628; } .column li{ background-color:#b0f08c; border-color:#6fe02d; }
JQuery Sortable
There is quite a wide variety in Javascript/JQuery libraries that allow for drag 'n dropping and sorting of lists. An honorable mention goes to Nestable, which was my first go-to library and has a nice, full example. Sadly it is not maintained and does not support nested lists, with some levels not being draggable (columns).
Luckily the JQueryUI sortable function provides all the flexibility we need. To load the library on our page, we need to include both the JQuery and the JQueryUI libraries:
<script src="https://code.jquery.com/jquery-1.12.4.js"></script> <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
The basic usage of Sortable is easy, simply initialize the function on a list. As discussed, we want to make the lists with classes .main-parent
and .column
sortable, while not allowing the .columns-parent
do be dragged, so we only initialize the function on the first two. The code below is placed inside new <script>
tags. At the same time, we pass the function the variable connectWith, which tells the function to which other lists sorting is allowed. As there are multiple .column
lists, we also have to connect these to each other.
$(function() { $('ol.main-parent').sortable({ connectWith: 'ol.column', }); $('ol.column').sortable({ connectWith: 'ol.main-parent,ol.column', }); });
It's this simple to get the sorting behaviour we wanted. We can make this a little more user-friendly by adding a 'move' cursor while dragging and showing a placeholder. For the placeholder, we'll tell the code to add the class .drag-placeholder
and add some code to make the placeholder the same size as the element we're dragging. This is done by calculating the height of the dragged element at the moment the dragging starts:
$(function() { $('ol.main-parent').sortable({ connectWith: 'ol.column', placeholder: 'drag-placeholder', cursor: 'move', start: function(e, ui){ ui.placeholder.height(ui.item.height()); }, }); $('ol.column').sortable({ connectWith: 'ol.main-parent,ol.column', placeholder: 'drag-placeholder', cursor: 'move', start: function(e, ui){ ui.placeholder.height(ui.item.height()); }, }); });
Of course we can also style the placeholder in the CSS:
.drag-placeholder{ background-color:#f9f979 !important; border-color:#f0f01d; box-sizing: border-box; -moz-box-sizing: border-box; min-height:30px; min-width:30px; }
Outputting the list tree
The dragging was easily implemented using JQueryUI Sortable. The last step is to output the list tree after it was changed. For this example, we create a textarea with id #output
, which will store the list tree.
<textarea id="output" name="output"></textarea>
To change the output, we use the Sortable update
event to change the textarea's value. To create the list tree, a function FetchChild
is added, which loops through the .main-parent
list and creates a JSON output in the buildJSON
function. These last two functions were taken from an online example, which sadly I've lost the reference to. Finally, we call this function outside sortable to output the initial list tree. The code belows shows the full JQuery code, which was also used in the demo:
$(function() { $('ol.main-parent').sortable({ connectWith: 'ol.column', placeholder: 'drag-placeholder', cursor: 'move', start: function(e, ui){ ui.placeholder.height(ui.item.height()); }, update: function(e, ui) { $("#output").val(JSON.stringify(FetchChild(), null, 2)); } }); $('ol.column').sortable({ connectWith: 'ol.main-parent,ol.column', placeholder: 'drag-placeholder', cursor: 'move', start: function(e, ui){ ui.placeholder.height(ui.item.height()); }, update: function(e, ui) { $("#output").val(JSON.stringify(FetchChild(), null, 2)); } }); function FetchChild(){ var data =[]; $('ol.main-parent > li').each(function(){ data.push(buildJSON($(this))); }); return data; } function buildJSON($li) { var subObj = { "id": $li.attr('id') }; $li.children('ol').children().each(function() { if (!subObj.children) { subObj.children = []; } subObj.children.push(buildJSON($(this))); }); return subObj; } // output initial serialised data $("#output").val(JSON.stringify(FetchChild(), null, 2)); });