Adding a TinyMCE image browser the CakePHP way
If your cake app requieres some sort of WYSIWYG editor, integrating TinyMCE is in most cases the way to go. The only problem with TinyMCE is that there is no image browser included for free. So we have to build our own one.
Introduction
This article is meant as a follow up to this one http://bakery.cakephp.org/articles/view/using-tinymce-with-cakephp (see comment no. 4), as it will not explain how to integrate TinyMCE into your app, but how to integrate an image browser into your already working TinyMCE installation.Please check the linked article to see how to integrate TinyMCE into your app.
For the sake of simplicity, all my images will reside in one folder. The user can browse the folder in form of a list and upload new images.
Step 1: Create the uploads folder
Create folder app/webroot/uploads and copy some image files into the new folder.Step 2: Create the Image model
The image model will be responsible to read in all the images in the uploads folder and will also take care that only valid files can be uploaded.Filename: app/models/image.php
Model Class:
Download code
<?php
class Image extends AppModel {
var $name = 'Image';
var $validate = array(
'image' => array(
'rule' => array(
'validFile',
array(
'required' => true,
'extensions' => array(
'jpg',
'jpeg',
'gif',
'png'
)
)
)
)
);
var $useTable = false;
function readFolder($folderName = null) {
$folder = new Folder($folderName);
$images = $folder->read(
true,
array(
'.',
'..',
'Thumbs.db'
),
true
);
$images = $images[1]; // We are only interested in files
// Get more infos about the images
$retVal = array();
foreach ($images as $the_image)
{
$the_image = new File($the_image);
$retVal[] = array_merge(
$the_image->info(),
array(
'size' => $the_image->size(),
'last_changed' => $the_image->lastChange()
)
);
}
return $retVal;
}
function upload($data = null) {
$this->set($data);
if(empty($this->data)) {
return false;
}
// Validation
if(!$this->validates()) {
return false;
}
// Move the file to the uploads folder
if(!move_uploaded_file($this->data['Image']['image']['tmp_name'], APP.WEBROOT_DIR.DS.'uploads'.DS.$this->data['Image']['image']['name'])) {
return false;
}
return true;
}
function validFile($check, $settings) {
$_default = array(
'required' => false,
'extensions' => array(
'jpg',
'jpeg',
'gif',
'png'
)
);
$_settings = array_merge(
$_default,
ife(
is_array($settings),
$settings,
array()
)
);
// Remove first level of Array
$_check = array_shift($check);
if($_settings['required'] == false && $_check['size'] == 0) {
return true;
}
// No file uploaded.
if($_settings['required'] && $_check['size'] == 0) {
return false;
}
// Check for Basic PHP file errors.
if($_check['error'] !== 0) {
return false;
}
// Use PHPs own file validation method.
if(is_uploaded_file($_check['tmp_name']) == false) {
return false;
}
// Valid extension
return Validation::extension(
$_check,
$_settings['extensions']
);
}
}
?>
Step 3: Create the images controller
Filename: app/controllers/images_controller.phpController Class:
Download code
<?php
class ImagesController extends AppController {
var $name = 'Images';
var $uses = array('Image');
var $helpers = array(
'Html',
'Form',
'Javascript',
'Number' // Used to show readable filesizes
);
function index() {
$this->set(
'images',
$this->Image->readFolder(APP.WEBROOT_DIR.DS.'uploads')
);
}
function upload() {
// Upload an image
if (!empty($this->data)) {
// Validate and move the file
if($this->Image->upload($this->data)) {
$this->Session->setFlash('The image was successfully uploaded.');
} else {
$this->Session->setFlash('There was an error with the uploaded file.');
}
$this->redirect(
array(
'action' => 'index'
)
);
} else {
$this->redirect(
array(
'action' => 'index'
)
);
}
}
}
?>
Step 4: Create a view to show the images and upload new ones
Filename: app/views/images/index.ctpView Template:
Download code
<?php
echo $javascript->codeBlock(
"function selectURL(url) {
if (url == '') return false;
url = '".Helper::url('/uploads/')."' + url;
field = window.top.opener.browserWin.document.forms[0].elements[window.top.opener.browserField];
field.value = url;
if (field.onchange != null) field.onchange();
window.top.close();
window.top.opener.browserWin.focus();
}"
);
?>
<?php
echo $form->create(
null,
array(
'type' => 'file',
'url' => array(
'action' => 'upload'
)
)
);
echo $form->label(
'Image.image',
'Upload image'
);
echo $form->file(
'Image.image'
);
echo $form->end('Upload');
?>
<?php if(isset($images[0])) {
$tableCells = array();
foreach($images As $the_image) {
$tableCells[] = array(
$html->link(
$the_image['basename'],
'#',
array(
'onclick' => 'selectURL("'.$the_image['basename'].'");'
)
),
$number->toReadableSize($the_image['size']),
date('m/d/Y H:i', $the_image['last_changed'])
);
}
echo $html->tag(
'table',
$html->tableHeaders(
array(
'File name',
'Size',
'Date created'
)
).$html->tableCells(
$tableCells
)
);
} ?>
If you now open the http://example.com/images you should see a list with all the files you copied into the uploads folder. You should also be able to upload a new image.
Step 5: Integrate the image browser into TinyMCE
Filename: app/views/elements/tinymce.ctpView Template:
Download code
<?php echo $javascript->link("tiny_mce/tiny_mce.js"); ?>
<?php
echo $javascript->codeBlock(
"function fileBrowserCallBack(field_name, url, type, win) {
browserField = field_name;
browserWin = win;
window.open('".Helper::url(array('controller' => 'images'))."', 'browserWindow', 'modal,width=600,height=400,scrollbars=yes');
}"
);
?>
<?php
echo $javascript->codeBlock(
"tinyMCE.init({
mode : 'textareas',
theme : 'advanced',
theme_advanced_buttons1 : 'forecolor, bold,italic,underline,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,undo,redo,|,link,unlink,|,image,emotions,code',
theme_advanced_buttons2 : '',
theme_advanced_buttons3 : '',
theme_advanced_toolbar_location : 'top',
theme_advanced_toolbar_align : 'left',
theme_advanced_path_location : 'bottom',
extended_valid_elements : 'a[name|href|target|title|onclick],img[class|src|border=0|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name],hr[class|width|size|noshade],font[face|size|color|style],span[class|align|style]',
file_browser_callback: 'fileBrowserCallBack',
width: '620',
height: '480',
relative_urls : false
});"
);
?>
Summary
As you could see, integrating the image browser into your TinyMCE installation is actually quiete easy. If you need a more advanced image browser, the view is first place you should tweak.Happy baking!
Written by Markus Henke
http://braindead1.de
Comments
Comment
1 Field error messages
Thanks for this work, it exactly what I needed!
Just one thing, this code can't display validation error messages (messages displayed close of fields)
To display error messages :
# in the controller :
remove upload function, and modify index() with this one
Controller Class:
<?php
function index() {
if (!empty($this->data)) {
// Validate and move the file
if($this->Image->upload($this->data)) {
$this->Session->setFlash('The image was successfully uploaded.');
} else {
$this->Session->setFlash('There was an error with the uploaded file');
}
}
$this->set(
'images',
$this->Image->readFolder('your_upload_folder')
);
}
?>
# in the view
Set the action to index an remplace $form->file, by $form->input and specify type to file :
View Template:
<?php
echo $form->create('Image',array('type' => 'file', 'url' => array('action' => 'index')));
echo $form->input('image', array('label' =>___('Upload image')." :", 'type' => 'file'));
echo $form->end('Upload');
?>
With this code you will be able to dissociate extention validation to the custom file validation and add other validation rules if needed :
Model Class:
<?php
<?php
var $validate = array(
'image' => array(
'extension' => array('rule' => array( 'extension', array('jpg', 'jpeg', 'gif', 'png')),
'message' => 'Invalid file or extention (should be jpg, jpeg, png or gif'
),
'validFile' => array('rule' => array('validFile', true), 'message' => 'Invalid file'))
)
);
function validFile($check, $required = true) {
// Remove first level of Array
$_check = array_shift($check);
// No file uploaded.
if(($_check['size'] == 0) && $required) {
return false;
}
// Check for Basic PHP file errors.
if($_check['error']) {
return false;
}
// Use PHPs own file validation method.
if(!is_uploaded_file($_check['tmp_name'])) {
return false;
}
// Valid extension
return true;
}
?>
?>
Question
2 Integrating image browser with TinyMCE helper?
the lack of an imagebrowser was basically the only thing preventing me from using TinyMCE over FCKEditor. Now that this article has been published, I see no reason not to go for TinyMCE.
However, this article is written with the assumption of using the TinyMCE Element. I however, have used the TinyMCE helper, explained at http://bakery.cakephp.org/articles/view/tinymce-helper-1
I was trying to figure out how to add the image upload functionality to the helper, and it turned out to be quite simple. I'll describe the implementation below, assuming you have the helper up and running.
First of all, copy the whole code provided at step 5 from this main article. Then, in every view you want the tinymce helper applied on textareas, insert the element into your view page
<?php echo $this->element('your_element_name'); ?>Now, this will add the tinymce editor to all the textareas in your view. I can imagine that that isn't supposed to happen in most cases...
This is caused by a parameter in the step 5 code. In the javascript
TinyMCE.init()function, there is a parameter called mode, which can have the following 4 values: textareas, specific_textareas, exact and none. I'm not going to go too deep into this, it basically determines which textfields are affected by the javascript code.I haven't managed to fix this problem with the mode parameter, but I've found a workaround just as easy (or even easier). The "problem" is caused by the FormHelper::input method. This method generates an input field based on the datatype of the corresponding field in the database. TEXT, CHAR, and VARCHAR types will cause the formhelper to generate a textarea. This can be manipulated by determing the input type at the formhelper. It just involves adding a simple extra parameter, the 'type' parameter.
So, to keep it simple:
<?php echo $form->input('title', array('type' => 'text')); ?>will actually generate an input field instead of a textarea, whatever the database data type is. Thus, no editor will be applied to these kind of fields, only to textareas. Maybe not the right workaround, but certainly the easiest...