14 min read

 

CMS Design Using PHP and jQuery

CMS Design Using PHP and jQuery

Build and improve your in-house PHP CMS by enhancing it with jQuery

  • Create a completely functional and a professional looking CMS
  • Add a modular architecture to your CMS and create template-driven web designs
  • Use jQuery plugins to enhance the “feel” of your CMS
  • A step-by-step explanatory tutorial to get your hands dirty in building your own CMS
        Read more about this book      

(For more resources on this subject, see here.)

Dates

Dates are annoying. The scheme I prefer is to enter dates the same way MySQL accepts them—yyyy-mm-dd hh:mm:ss. From left to right, each subsequent element is smaller than the previous. It’s logical, and can be sorted sensibly using a simple numeric sorter.

Unfortunately, most people don’t read or write dates in that format. They’d prefer something like 08/07/06.

Dates in that format do not make sense. Is it the 8th day of the 7th month of 2006, or the 7th day of the 8th month of 2006, or even the 6th day of the 7th month of 2008? Date formats are different all around the world.

Therefore, you cannot trust human administrators to enter the dates manually.

A very quick solution is to use the jQuery UI’s datepicker plugin.

Temporarily (we’ll remove it in a minute) add the highlighted lines to /ww.admin/pages/pages.js:

other_GET_params:currentpageid
});
$('.date-human').datepicker({
'dateFormat':'yy-mm-dd'
});
});

When the date field is clicked, this appears:

It’s a great calendar, but there’s still a flaw: Before you click on the date field, and even after you select the date, the field is still in yyyy-mm-dd format.

While MySQL will thank you for entering the date in a sane format, you will have people asking you why the date is not shown in a humanly readable way.

We can’t simply change the date format to accept something more reasonable such as “May 23rd, 2010”, because we would then need to ensure that we can understand this on the server-side, which might take more work than we really want to do.

So we need to do something else.

The datepicker plugin has an option which lets you update two fields at the same time. This is the solution—we will display a dummy field which is humanly readable, and when that’s clicked, the calendar will appear and you will be able to choose a date, which will then be set in the human-readable field and in the real form field.

Don’t forget to remove that temporary code from /ww.admin/pages/pages.js.

Because this is a very useful feature, which we will use throughout the admin area whenever a date is needed, we will add a global JavaScript file which will run on all pages.

Edit /ww.admin/header.php and add the following highlighted line:

<script src="/j/jquery.remoteselectoptions
/jquery.remoteselectoptions.js"></script>
<script src="/ww.admin/j/admin.js"></script>
<link rel="stylesheet" href="http://ajax.googleapis.com
/ajax/libs/jqueryui/1.8.0/themes/south-street
/jquery-ui.css" type="text/css" />

And then we’ll create the /ww.admin/j/ directory and a file named /ww.admin/j/admin.js:

function convert_date_to_human_readable(){
var $this=$(this);
var id='date-input-'+Math.random().toString()
.replace(/\./,'');
var dparts=$this.val().split(/-/);
$this
.datepicker({
dateFormat:'yy-mm-dd',
modal:true,
altField:'#'+id,
altFormat:'DD, d MM, yy',
onSelect:function(dateText,inst){
this.value=dateText;
}
});
var $wrapper=$this.wrap(
'<div style="position:relative" />');
var $input=$('<input id="'+id+'" class="date-human-readable"
value="'+date_m2h($this.val())+'" />');
$input.insertAfter($this);
$this.css({
'position':'absolute',
'opacity':0
});
$this
.datepicker(
'setDate', new Date(dparts[0],dparts[1]-1,dparts[2])
);
}
$(function(){
$('input.date-human').each(convert_date_to_human_readable);
});

This takes the computer-readable date input and creates a copy of it, but in a human-readable format.

The original date input box is then made invisible and laid across the new one. When it is clicked, the date is updated on both of them, but only the human-readable one is shown.

Much better. Easy for a human to read, and also usable by the server.

Saving the page

We created the form, and except for making the body textarea more user-friendly, it’s just about finished. Let’s do that now.

When you click on the Insert Page Details button (or Update Page Details, if an ID was provided), the form data is posted to the server.

We need to perform these actions before the page menu is displayed, so it is up-to-date.

Edit /ww.admin/pages.php, and add the following highlighted lines before the load menu section:

echo '<h1>Pages</h1>';
// { perform any actions
if(isset($_REQUEST['action'])){
if($_REQUEST['action']=='Update Page Details'
|| $_REQUEST['action']=='Insert Page Details'){
require 'pages/action.edit.php';
}
else if($_REQUEST['action']=='delete'){
'pages/action.delete.php';
}
}
// }
// { load menu

If an action parameter is sent to the server, then the server will use this block to decide whether you want to edit or delete the page. We’ll handle deletes later in the article.

Notice that we are handling inserts and updates with the same file, action.edit.php—in the database, there is almost no difference between the two when using MySQL.

So, let’s create that file now. We’ll do it a bit at a time, like how we did the form, as it’s a bit long.

Create /ww.admin/pages/action.edit.php with this code:

<?php
function pages_setup_name($id,$pid){
$name=trim($_REQUEST['name']);
if(dbOne('select id from pages where
name="'.addslashes($name).'" and parent='.$pid.'
and id!='.$id,'id')){
$i=2;
while(dbOne('select id from pages where
name="'.addslashes($name.$i).'" and parent='.$pid.'
and id!='.$id,'id'))$i++;
echo '<em>A page named "'.htmlspecialchars($name).'"
already exists. Page name amended to "'
.htmlspecialchars($name.$i).'".</em>';
$name=$name.$i;
}
return $name;
}

The first piece is a function which tests the submitted page name. If that name is the same as another page which has the same parent, then a number is added to the end and a message is shown explaining this.

Here’s an example, creating a page named “Home” in the top level (we already have a page named “Home”):

Next we’ll create a function for testing the inputted special variable. Add this to the same file:

function pages_setup_specials($id=0){
$special=0;
$specials=isset($_REQUEST['special'])
?$_REQUEST['special']:array();
foreach($specials as $a=>$b)
$special+=pow(2,$a);
$homes=dbOne("SELECT COUNT(id) AS ids FROM pages
WHERE (special&1) AND id!=$id",'ids');
if($special&1){ // there can be only one homepage
if($homes!=0){
dbQuery("UPDATE pages SET special=special-1
WHERE special&1");
}
}
else{
if($homes==0){
$special+=1;
echo '<em>This page has been marked as the site\'s
Home Page, because there must always be one.</em>';
}
}
return $special;
}

In this function, we build up the special variable, which is a bit field.

A bit field is a number which uses binary math to combine a few “yes/no” answers into one single value. It’s good for saving space and fields in the database.
Each value has a value assigned to it which is a power of two. The interesting thing to note about powers of two is that in binary, they’re always represented as a 1 with some 0s after it. For example, 1 is represented as 00000001, 2 is 00000010, 4 is 00000100, and so on.
When you have a bit field such as 00000011 (each number here is a bit), it’s easy to see that this is composed of the values 1 and 2 combined, which are 20 and 21 respectively.
The & operator lets us check quickly if a certain bit is turned on (is 1) or not. For example, 00010011 & 16 is true, and 00010011 & 32 is false, because the 16 bit is on and the 32 bit is off.

In the database, we set a bit for the homepage, which we say has a value of 1. In the previous function, we need to make sure that after inserting or updating a page, there is always exactly one homepage.

The only other one we’ve set so far is “does not appear in navigation menu”, which we’ve given the value 2. If we added a third bit flag (“is a 404 handler”, for example), it would have the value 4, then 8, and so on.

Okay—now we will set up our variables. Add this to the same file:

// { set up common variables
$id =(int)$_REQUEST['id'];
$pid =(int)$_REQUEST['parent'];
$keywords =$_REQUEST['keywords'];
$description =$_REQUEST['description'];
$associated_date =$_REQUEST['associated_date'];
$title =$_REQUEST['title'];
$name =pages_setup_name($id,$pid);
$body =$_REQUEST['body'];
$special =pages_setup_specials($id);
if(isset($_REQUEST['page_vars']))
$vars =json_encode($_REQUEST['page_vars']);
else $vars='[]';
// }

Then we will add the main body of the page update SQL to the same file:

// { create SQL
$q='edate=now(),type="'.addslashes($_REQUEST['type']).'",
associated_date="'.addslashes($associated_date).'",
keywords="'.addslashes($keywords).'",
description="'.addslashes($description).'",
name="'.addslashes($name).'",
title="'.addslashes($title).'",
body="'.addslashes($body).'",parent='.$pid.',
special='.$special.',vars="'.addslashes($vars).'"';
// }

This is SQL which is common to both creating and updating a page.

Finally we run the actual query and perform the action. Add this to the same file:


// { run the query
if($_REQUEST['action']=='Update Page Details'){
$q="update pages set $q where id=$id";
dbQuery($q);
}
else{
$q="insert into pages set cdate=now(),$q";
dbQuery($q);
$_REQUEST['id']=dbLastInsertId();
}
// }
echo '<em>Page Saved</em>';

In the first case, we simply run an update.

In the second, we run an insert, adding the creation date to the query, and then setting $_REQUEST[‘id’] to the ID of the entry that we just created.

Creating new top-level pages

If you’ve been trying all this, you’ll have noticed that you can create a top-level page simply by clicking on the admin area’s Pages link in the top menu, and then you’re shown an empty Insert Page Details form.

If you’ve been trying all this, you’ll have noticed that you can create a top-level page simply by clicking on the admin area’s Pages link in the top menu, and then you’re shown an empty Insert Page Details form.

It makes sense, though, to also have it available from the pagelist on the left-hand side.

So, let’s make that add main page button useful.

If you remember, we created a pages_add_main_page function in the menu.js file, just as a placeholder until we got everything else done.

Open up that file now, /ww.admin/pages/menu.js, and replace that function with the following two new functions:

function pages_add_main_page(){
pages_new(0);
}
function pages_new(p){
$('<form id="newpage_dialog" action="/ww.admin/pages.php"
method="post">
<input type="hidden" name="action"
value="Insert Page Details" />
<input type="hidden" name="special[1]"
value="1" />
<input type="hidden" name="parent" value="'+p+'" />
<table>
<tr><th>Name</th><td><input name="name" /></td></tr>
<tr><th>Page Type</th><td><select name="type">
<option value="0">normal</option>
</select></td></tr>
<tr><th>Associated Date</th><td>
<input name="associated_date" class="date-human"
id="newpage_date" /></td></tr>
</table>
</form>')
.dialog({
modal:true,
buttons:{
'Create Page': function() {
$('#newpage_dialog').submit();
},
'Cancel': function() {
$(this).dialog('destroy');
$(this).remove();
}
}
});
$('#newpage_date').each(convert_date_to_human_readable);
return false;
}

When the add main page button is clicked, a dialog box is created asking some basic information about the page to create:

We include a few hidden inputs.

  • action: To tell the server this is an Insert Page Details action.
  • special: When Create Page is clicked, the page will be saved in the database, but we should hide it initially so that front-end readers don’t see a half-finished page. So, the special[1] flag is set (21 == 2, which is the value for hiding a page).
  • parent: Note that this is a variable. We can use the same dialog to create sub-pages.

When the dialog has been created, the date input box is converted to human-readable.

Creating new sub-pages

We will add sub-pages by using context menus on the page list. Note that we have a message saying right-click for options under the list.

First, add this function to the /ww.admin/pages/menu.js file:

function pages_add_subpage(node,tree){
var p=node[0].id.replace(/.*_/,'');
pages_new(p);
}

We will now need to activate the context menu. This is done by adding a contextmenu plugin to the jstree plugin . Luckily, it comes with the download, so you’ve already installed it. Add it to the page by editing /ww.admin/pages/menu.php and add this highlighted line:

<script src="/j/jquery.jstree/jquery.tree.js"></script>
<script src=
"/j/jquery.jstree/plugins/jquery.tree.contextmenu.js">
</script>
<script src="/ww.admin/pages/menu.js"></script>

And now, we edit the .tree() call in /ww.admin/menu.js to tell it what to do:

$('#pages-wrapper').tree({
callback:{
// SKIPPED FOR BREVITY - DO NOT DELETE THESE LINES
},
plugins:{
'contextmenu':{
'items':{
'create' : {
'label' : "Create Page",
'icon' : "create",
'visible' : function (NODE, TREE_OBJ) {
if(NODE.length != 1) return 0;
return TREE_OBJ.check("creatable", NODE);
},
'action':pages_add_subpage,
'separator_after' : true
},
'rename':false,
'remove':false
}
}
}
});

By default, the contextmenu has three links: create, rename, and remove. You need to turn off any you’re not currently using by setting them to false.

Now if you right-click on any page name in the pagelist, you will have a choice to create a sub-page under it.

Deleting pages

We will add deletions in the same way, using the context menu.

Edit the same file, and this time in the contextmenu code, replace the remove: false line with these:


'remove' : {
'label' : "Delete Page",
'icon' : "remove",
'visible' : function (NODE, TREE_OBJ) {
if(NODE.length != 1) return 0;
return TREE_OBJ.check("deletable", NODE);
},
'action':pages_delete,
'separator_after' : true
}

And add the pages_delete function to the same file:

function pages_delete(node,tree){
if(!confirm(
"Are you sure you want to delete this page?"))return;
$.getJSON('/ww.admin/pages/delete.php?id='
+node[0].id.replace(/.*_/,''),function(){
document.location=document.location.toString();
});
}

One thing to always keep in mind is whenever creating any code that deletes something in your CMS, you must ask the administrator if he/she is sure, just to make sure it wasn’t an accidental click. If the administrator confirms that the click was intentional, then it’s not your fault if something important was deleted.

So, the pages_delete function first checks for this, and then calls the server to remove the file. The page is then refreshed because this may significantly change the page list tree, as we’ll see now.

Create the /ww.admin/pages/delete.php file:

<?php
require '../admin_libs.php';
$id=(int)$_REQUEST['id'];
if(!$id)exit;
$r=dbRow("SELECT COUNT(id) AS pagecount FROM pages");
if($r['pagecount']<2){
die('cannot delete - there must always be one page');
}
else{
$pid=dbOne("select parent from pages
where id=$id",'parent');
dbQuery("delete from pages where id=$id");
dbQuery("update pages set parent=$pid where parent=$id");
}
echo 1;

First, we ensure that there is always at least one page in the database. If deleting this page would empty the table, then we refuse to do it. There is no need to add an alert explaining this, as it should be clear to anyone that deleting the last remaining page in a website leaves the website with no content at all. Simply refusing the deletion should be enough.

Next, we delete the page.

Finally, any pages which were contained within that page (pages which had this one as their parent), are moved up in the tree so they are contained in the deleted page’s old parent.

For example, if you had a page, page1>page2>page3 (where > indicates the hierarchy), and you removed page2, then page3 would then be in the position page1>page3.

This can cause a large difference in the treestructure if there were quite a few pages contained under the deleted one, so the page needs to be refreshed.

LEAVE A REPLY

Please enter your comment!
Please enter your name here