Excuse the title.

In my last post, I talked about using Markdown instead of a WYSIWYG editor. But one thing that TinyMCE and CKEditor does provide (with extensions) is the ability to upload images into the content area of a site. If a client requires the ability to upload images in to articles, a WYSIWYG editor usually manages this much better than a Markdown solution.

A brilliant Symphony extension Subsection Manager allows a site editor to upload an image which can be dragged and dropped into a Mardown textarea. Although this extension is much more flexible than using the image uploader in a WYSIWYG editor, it doesn’t allow a site editor to resize images, and to float them left and right. All images that have uploaded using this method appear in-line, meaning text does not wrap around the image.

I have read some discussion boards where people argued that developers should not provide the facility to upload images into content, since this could result in the design breaking. Although it is true we should do as much as possible to prevent a user from breaking a site’s design, preventing them from uploading images into content is to the detriment of the site.

A post in the same discussion suggested we could create a section in Symphony called “Images”, then use a “select box link” (similar to Node Reference field in Drupal) to associate those images with entries. But this is very inflexible - all images would have to appear together at the top or at the bottom of the content. Imagine writing about a diagram that appears 8 paragraphs below.

In fact all other major platforms seem to have a good mechanism for this, and Symphony should be no exception. There needs to be a way of uploading images into content, without breaking the design and providing the ability to float those images left and right.

The solution

The solution is to enhance the Subsection manager to enable content editors to upload images, in one of a pre-defined set of sizes (so they can’t easily break the design), and to provide the ability to float the images to the left or right, just like they can in a WYSIWYG editor.

Subsection Manager

There are a number of pre-requisites for this to work:

Markdown

The anatomy of a Markdown image tag is as follows:


![Alt Text Attribute](/path/to/image "Title attribute")

Since the Title attribute is hardly ever used, I hijacked it in the Markdown extension to look for “left” or “right”. If one of these appear, it sets the image class instead of the title attribute in the resulting HTML. If you like to use the Title attribute, or you need to provide your client with ability to set the title attribute, I suggest you alter the Markdown syntax further. I needed a very quick solution for a client, but perhaps you could implement something like:


![Alt Text Attribute](/path/to/image "Title attribute" "Class attribute")

Anyway, to achieve this, at line 889 of /extensions/markdown/lib/php-markdown-extra-1.2.4/markdown.php, I changed:


if (isset($title)) {
    $title = $this->encodeAttribute($title);
    $result .=  " title="$title""; # $title already quoted
}

to:


if (isset($title)) {
    $title = $this->encodeAttribute($title);
    if (in_array($title, array('left', 'right'))) {
        $result .=  " class="$title"";
    }
    else {
        $result .=  " title="$title""; # $title already quoted
    }
}

So in order to float an image using Markdown, we can add:


![Alt text](/path/to/image.png "right")

If we don’t specify “left” or “right”, the image will be displayed in-line.

Finally, we need to add our “left” and “right” classes in our CSS file:


.left { float: left; }
.right { float: right; }

Subsection Manager

The next step was to update Subsection Manager to include the float and resize options. I did this by making a few changes to /extensions/subsectionmanager/lib/class.subsectionmanager.php.

class.subsectionmanager.php

Firstly, I added a variable to the SubsectionManager class at line 8. So immediately after the $_Items declaration on line 12, I added:


private $_Sizes = array(
    'Large (640 wide)' => array('width' => 640, 'height' => 0),
    'Medium (480 wide)' => array('width' => 480, 'height' => 0),
    'Small (320 wide)' => array('width' => 320, 'height' => 0),
    'Square (200x200)' => array('width' => 200, 'height' => 200)
);

This array contains the resizing options you want to make available to the user, and should be configured based on the site’s design. If your site’s main content area is 640 pixels wide, that should be the largest possible image size. The array key is the name displayed in the select box, and each value is an array containing the width and height the image should be resized to.

To stretch images to a specific size, specify both the height and width. In most cases, I think you would want to set the image width only - so the image is scaled down proportionally to fit within the content area of your site. To scale the image proportionally by width, set the height to 0.

We now need to add the HTML which will allow users to align and resize images. This is done in the function “__layoutSubsection”.

The first line in this function sets an array called “$templates”, which sets up the HTML for the image preview in Subsection Manager. I changed this declaration from:


'image' => '<li value="{$value}" class="preview"><img src="' . URL . '/image/2/40/40/5{$preview}" width="40" height="40" /><a href="{$href}" class="image file">{$caption}</a></li>',

to:


'image' => '<li value="{$value}" class="preview image">
        <img src="' . URL . '/image/2/40/40/5{$preview}" width="40" height="40" />
        <a href="{$href}" class="image file">{$caption}</a>
        <div class="imgcontrols clearfix">
            <div class="align">
                <label>Left: <input type="radio" name="class-{$value}" value="left" /></label>
                <label>Right: <input type="radio" name="class-{$value}" value="right" /></label>
            </div>
            {$sizes}
        </div>
    </li>',

You should be able to see from all the variable in this code, that the string goes through a process of substitution, replacing all the variables with values.

The radio button HTML for the image alignment options appears in this code - it will be the same for all images. But since the resize options will be configured on a per image basis the variable “$size” is used. This is because an image uploaded by the user could be smaller than our pre-defined size options, so it would not look good to resize a 60x60 pixel image to 640x480. So it is much better to provide a set of resize options based on the original image’s size.

A bit further on in the function, there is a loop for each file in Subsection Manager. A line of code sets up the image preview (line 203 of the original file):


$href = URL . '/workspace' . $preview;

Firstly, I removed “URL” from this variable assignment to create a relative path. If absolute paths are used and I eventually have to move the site to another domain, I would have to go through every post (or find and replace though a SQL dump) to change the domain on every referenced file - so using a relative path is preferable. “http://jamesmorrish.co.uk/path/to/image.jpg” becomes “/path/to/image.jpg”.

By replacing this line of code with the following, the user has the ability to resize an image to one of our predefined options. The resize options provided are based on the original size of the image.


$href = '/workspace' . $preview;
$imgsize = getimagesize(URL . $href);
$image_sizes = '<label class="sizecont">Size:<select class="size"><option value="' . $imgsize[0] . 'x' . $imgsize[1] . '" >Original (' . $imgsize[0] . 'x' . $imgsize[1] . ')</option>';

foreach ($this->_Sizes as $name => $value) {

    if (($value['width'] <= $imgsize[0] || $value['width'] == 0) && ($value['height'] <= $imgsize[1] || $value['height'] == 0)) {
        $image_sizes .= '<option value="' . $value['width'] . 'x' . $value['height'] . '" >'. $name . '</option>';
    }
}
$image_sizes .= '</select>';

Now that the HTML has been set up for the image, we then need to pass it through the string substitution process I mentioned earlier. At line 220 (of the original file), after:


// Apply template
if($type == 'image') {
    $template = str_replace('{$preview}', $preview, $templates[$mode]['image']);
    $template = str_replace('{$href}', $href, $template);
    $template = str_replace('{$value}', $entry['id'], $template);

add:


$template = str_replace('{$sizes}', $image_sizes, $template);   

symphony.subsectionmanager.css

To style the new HTML, I added the following to the bottom of /extensions/subsectionmanager/assets/symphony.subsectionmanager.css


div.draghelper div.imgcontrols {
    display: none;
}

div.field-subsectionmanager div.imgcontrols {
    background: none repeat scroll 0 0 #E4E3E3;
    border-top: 1px solid #A3A3A3;
    display: none;
    margin-bottom: 14px;
    margin-top: 14px;
    padding: 12px 4%;
}

div.field-subsectionmanager div.imgcontrols:hover {
    cursor: default;
}

div.field-subsectionmanager .sizecont {
    float: right;
}

div.field-subsectionmanager select.size {
    display: inline;
    margin-left: 10px;
    width: auto;
}

div.field-subsectionmanager div.align {
    clear: both;
    float: left;
}

div.field-subsectionmanager div.align label {
    width: 68px; 
    margin-bottom: 6px;
}

div.field-subsectionmanager div.align input {
    float: right;
}

.clearfix:after {
    content: "."; 
    display: block; 
    height: 0; 
    clear: both; 
    visibility: hidden;
}

.clearfix {
    display: inline-block;
}

symphony.subsectionmanager.js

So finally we have to alter the behaviour of Subsection Manager to enable our new functionality.

Firstly we need to alter the drag/drop functionality. When an image is dropped into a text area we need to add “left” or “right” to the Markdown depending on what the user has selected, and using JIT Image Manipulation we need to create an image URL that will resize the image.

This is done by editing /extensions/subsectionmanager/assets/symphony.subsectionmanager.js.

On line 40 I altered the Markdown formatting settings to allow a class to be added to the image, by changing:


markdown: {
    image: '![{@text}]({@path})',
    file: '[{@text}]({@path})'
}

to:


markdown: {
    image: '![{@text}]({@path}{@class})',
    file: '[{@text}]({@path})'
}

Now we need to set the class to left or right, and set the image path to use JIT Image Manipulation. On line 327 of the original file, I replaced:


if(file.hasClass('image')) type = 'image';

with:


if(file.hasClass('image')) {
    type = 'image';
    matches.class = jQuery('.field-subsectionmanager .image[value=' + item.data('uid') + '] .imgcontrols input[name^=class]:checked').val();
    if (matches.class == undefined) matches.class = '';
    else matches.class = ' "' + matches.class + '"';
    // Determine the selected alignment (left or right) and set the class replacement variable

    var resize = false;
    if (item.data('uid')) {
        resize = jQuery('.field-subsectionmanager .image[value=' + item.data('uid') + '] .imgcontrols .size option:selected').val();

        var width, height;

        if(resize != -1) {
            width = resize.substr(0, resize.indexOf('x'));
            height = resize.substr(resize.indexOf('x') + 1, resize.length);

            matches.path = '/image/1/' + width + '/' + height + matches.path.substr(10, matches.path.length);
        }
    }
    // Determine the resizing options and reset the path to use JIT Image Manipulation
}

For the last step, we need to alter the show/hide behaviour of our new controls. I only wanted them to display when the Subsection Manager “drawer” is open, so the controls can be easy hidden away. The Subsection Manager has a “droppable” setting which enables drag/drop onto text areas. We only want to show the image controls if Subsection Manager images are droppable.

Immediately after line 93 of the original file:


template.insertAfter(item).slideUp(0).slideDown(settings.speed);

insert the following to show the image controls:


if (item.closest('.stage').hasClass('droppable')){
    item.find('.imgcontrols').slideDown(settings.speed);
}

Immediately after line 277 of the original file:


var item = object.find(settings.template).clone().removeClass('template').addClass('new').insertBefore(empty).slideDown(settings.speed);

insert the following to hide the image controls:


jQuery('.imgcontrols', stage).slideUp(settings.speed);

Conclusions

We now have an image aligning and resizing solution.

Subsection Manager

One potential issue is how to prevent your users uploading 10MB images directly from their digital cameras. Using this in conjunction with the Advanced Upload Field will prevent your users uploading such large files.

As I mentioned earlier, this was a quick solution to meet the needs of a client - there are lots of things I would like to do to enhance this further. I think it would be really nice to implement an uploading/cropping tool using jCrop, and combine that with JIT so that cropped images are resized proportionally so they’re safe to display.

« Back