Invitation card creator with Prototype and Scriptaculous


I’ve created a demo demonstrating the feasibility earlier, the first one using jQuery and jQuery UI actually, although at the end I never used jQuery since Inviclub is created with Symphony and Symphony is using Prototype.

Due to the fact that Prototype is monkey patching Javascript it will play badly with for instance jQuery, some stuff works but some don’t because of the patching. That’s the reason we’re using Prototype and Scriptaculous for this.

Let’s walk the final js from top to bottom:

var elCount = 0;
var curEl = null;
var workWith = 'element';

function displayZindex(){
	$('zindexviewer').writeAttribute({value: $(curEl).getStyle('zIndex')});
}

function changeZindex(chg){
	var curZ = $(curEl).getStyle('zIndex');
	$(curEl).setStyle({zIndex: parseInt(curZ) + chg})
	displayZindex();
}
function decrease_zindex(){ changeZindex(-1); }
function increase_zindex(){ changeZindex(1); }

First some globals, elCount will keep track of how many elements we have in the workspace, curEl will keep track of which element is selected and workWith keeps track of whether we are working with the workspace or an element. The reason for that last one is that the user can set the background color and picture of the workspace, the border can also be changed. It will form the very “surface” of our digital invitation cards, ie if you change the background to red then the background of the final card will be red so it’s more than just a workspace, it’s the background element too.

Then we have some functions keeping track of the z-index of the elements, we can for instance create an image element and then a text element and drag the text to be on top of the image. DisplayZindex() will as the name implies, output the current z-index of the selected element so the user can keep track of what is where in that dimension.

changeZindex() will change the z-index of the current element by an arbitrary amount, the two final decrease and increase functions are simply shortcuts to this function.

function child(p, ch){
	var c 		= typeof p == 'string' ? $(p).childNodes : p.childNodes;
	var type 	= ch.charAt(0);
	var ch 		= ch.slice(1, ch.length);
	for(i = 0; i < c.length; i++){
		if(type == '#'){
			if(c[i].id == ch) return c[i];
		}else if(type == '.'){
			if(c[i].className == ch) return c[i];
		}
	}
	return false;
}

This is a utility function, if I remember correctly I think Prototype already has the functionality that this function creates. Anyway, we will check if a child (ch) is the child of a parent (p). When it comes to p (the parent) input can either be the id string or the element itself. The child (ch) we are looking for is either an id string or a class string, ie #child_id or .child_class. We return the child if we find it, otherwise false.

function stayInBox(box, el, resize){
	var thisPos 		= $(el).viewportOffset();
	var boxPos 			= $(box).viewportOffset();
	var diff_right 	= (thisPos.left + $(el).getWidth()) 	- (boxPos.left + $(box).getWidth());
	var diff_bottom = (thisPos.top 	+ $(el).getHeight()) 	- (boxPos.top + $(box).getHeight());
	var diff_left 	= boxPos.left 	- thisPos.left;
	var diff_top 		= boxPos.top 		- thisPos.top;

	if(resize){
    if(diff_right > 0)
      $(el).setStyle({'width': parseInt($(el).getWidth() - diff_right) + "px"});
      
    if(diff_bottom > 0)
      $(el).setStyle({'height': parseInt($(el).getHeight() - diff_bottom) + "px"});
	}else{
		if(diff_right > 0)
		  $(el).setStyle({'left': parseInt(thisPos.left - boxPos.left - diff_right) + 'px'});
		  
		if(diff_bottom > 0)
		  $(el).setStyle({'top': parseInt(thisPos.top - boxPos.top - diff_bottom) + 'px'});
	}
	
	if(diff_left > 0)
		$(el).setStyle({'left': parseInt(thisPos.left + diff_left) + "px"});
		
	if(diff_top > 0)
		$(el).setStyle({'top': parseInt(thisPos.top + diff_top) + "px"});
}

This one has already been explained, albeit in the jQuery demo, but the principle is the same. If you compare it to the earlier version this one is bigger, some bugs have been removed etc, I’ll leave it up to you to determine the stuff that’s been fixed and so on.

Anyway, as the name implies, it will make sure the elements can’t be dragged outside of the workspace.

function setDimensions(el, cEl){
	var dimArr = el.size.split('x');
	cEl.setStyle({'width': dimArr[0] + 'px', 'height': dimArr[1] + 'px'});
}

function setPos(el, cEl){
	var posArr = el.pos.split('x');
	cEl.setStyle({'left': posArr[0] + 'px', 'top': posArr[1] + 'px'});
}

Positions and sizes are set as for instance 640×480 in the database, the above are two convenience functions for simply passing these strings and having the elements react to the values.

function styleStrToHsh(styleStr){
	var result 				= new Hash();
	var styleArr 			= styleStr.split(';');
	styleArr.each(function(st){
		var tmp 				= st.split(':');
		if(tmp[0] != undefined || tmp[1] != undefined)
			result.set([tmp[0]], tmp[1]);
	});
	return result;
}

function getStyles(keepArr, styleStr){
	var result 		= '';
	var styleObj 	= styleStrToHsh(styleStr);
	keepArr.each(function(val){
		result += val+":"+styleObj.get(val)+";"; 
	});
	return result;
}

Again, as the name implies styleStrToHsh() will take a style string (that’s how the user’s work is stored in the database) and convert them to a hash that can be used with getStyles().

GetStyles() will take an array with styles that we want to keep and the style string from the database and output a new string containing only the values we want to keep.

function loadObj(cEl, el){
	cEl = $(cEl);
	cEl.writeAttribute({'style': el.style});

	setDimensions(el, cEl);
	setPos(el, cEl);
	
	if(el.type != 'workspace' && cEl.getStyle('border') != null){
		var cBorder = cEl.getStyle('border');
		if(cBorder.search(/none/) != -1 || (Prototype.Browser.IE && cBorder.match(/px$/))){
			cEl.setStyle({'border': ''});
			cEl.writeAttribute({'class': 'default_border'});
		}
	}
}

This one puts an object on the workspace based on info stored in the database, if no border style can be found we have to take special action. The reason for this is that the border of a selected element is supposed to be grey striped when not selected and black striped when selected. We need something to start with if we are supposed to toggle this feature when different objects are selected. As it happens, some IE bullshit was necessary here. Note that we are using the class attribute instead of the border style to accomplish the grey/black toggling.

function evJSON(str){
	return eval("(" + str + ")");
}

function loadWorkspace(res){
	
	$('el_chooser').insert('<option value="0" >Välj</option>');
	
	if(res == 'inner'){
		var els 		= evJSON($('workspace').innerHTML);
		$('workspace').update('');
	}else
		var els 		= evJSON(res.responseText);
	
	var wSpace 	= els.find(function(el){ return el.type == 'workspace' });
	loadObj($('workspace'), wSpace);
	els.each(function(el){
		if(el.type == 'text'){
			createTxtObject(el.data, el);
		}else if(el.type == 'image') 
			createImageObject(el.data, el);
	});
	selOption($('el_chooser'), '0');
}

Here we load a whole workspace that has been saved in the database in the form of JSON. We can either create the workspace from scratch or replace it with something else (typically templates already saved in the database and selected on the template page).

First we loop through the elements and get the workspace, create it and then put the rest of the objects on it.

function newCommon(tpl_id, sub_class){
	var newEl = $(tpl_id).cloneNode(true);
	var newId = tpl_id + elCount;
	
	$('el_chooser').insert('<option value="'+newId+'" >'+newId+'</option>');
	
	$(newEl).setStyle({zIndex: elCount + 1}).writeAttribute({id: newId})
	$('workspace').appendChild(newEl);
	new Draggable(newEl, {handle: 'dragger', snap: function(x,y,draggable) {
      function constrain(n, lower, upper) {
        if (n > upper) return upper;
        else if (n < lower) return lower;
        else return n;
      }
     
      element_dimensions = Element.getDimensions(draggable.element);
      parent_dimensions = Element.getDimensions(draggable.element.parentNode);
      return[
        constrain(x, 0, parent_dimensions.width - element_dimensions.width),
        constrain(y, 0, parent_dimensions.height - element_dimensions.height)];
	  }
	});
	
	Event.observe(child(newEl, '.delete'), 'click', function(event){
		var toDel = $(this.getOffsetParent());
		removeFromSel($('el_chooser'), toDel.readAttribute('id'));
		toDel.remove();
		curEl = null;
	});
	
	Event.observe(child(newEl, '.dragger'), 'mousedown', function(event){
		var toSelect = $(this.getOffsetParent());
		selectCur(toSelect, sub_class);
		sub_class == "txt_area_start" ? toggleRight('textMenu') : toggleRight('imageMenu');;
	});
	
	elCount++;
	curEl = newEl;
	allDefaultBorder();
	return newEl;
}

So here we use the Scriptaculous Draggable to initate both image and text elements (draggable divs containing image elements or text area elements respectively). This part has already been explained too in the earlier demo mentioned above. Here we have added stuff and fixed some bugs though.

function removeFromSel(select_box, val){
	for(var i = 0; i < select_box.options.length; i++){
		if(select_box.options[i].value == val){
			$(select_box.options[i]).remove();
			break;
		}
	}
}

function selOption(select_box, sel_val){
	for(var i = 0; i < select_box.options.length; i++){
		if(select_box.options[i].value == sel_val){
			select_box.selectedIndex = i;
			break;
		}
	}
}

Some select box manipulation, removeFromSel will remove an element with value val, selOption works the same way but will instead select it.

function selectCur(toSelect, sub_class){
	var elType 	= sub_class == "txt_area_start" ? "text" : "image";
	chooseElement(toSelect, elType);
	selOption($('el_chooser'), toSelect.readAttribute('id'));
}

function writeClass(el, cls){
	el.removeAttribute('class');
	el.writeAttribute({'class': cls});
}

function chooseElement(toSelect, elType){
	var oldEl 	= curEl;
	curEl 	= toSelect;
	toggleMenu(elType);
	displayZindex();
	var bClass = curEl.readAttribute('class');
	if(bClass == 'default_border' || bClass == 'img_start' || bClass == 'txt_start'){
		writeClass(curEl, 'black_border');
		if(curEl.readAttribute('id') != oldEl.readAttribute('id'))
			writeClass(oldEl, 'default_border');
	}else if(curEl.readAttribute('class') == 'black_border'){
		//do nothing
	}else if(oldEl.readAttribute('class') == 'black_border'){
		writeClass(oldEl, 'default_border');
	}
}

This logic takes care of toggling the selected element the user wants to work with on and the previously selected element to off (remember the grey/black border?). ToggleMenu() will take care of displaying the correct panels and buttons depending on which type of element has been chosen (text or image), different types of elements have different tools associated with them. That part is outside the scope of this tutorial though.

First we check, has the element in question got the default border (it’s the gray one) or has it just been created in the workspace? If any of these possibilities is true we give the element the black border (it’s selected). After that we give the prior selected element the gray border.

Note that writeClass() is just an IE hack.

function resizeImg(img, parentDiv){
	var bWidth = parentDiv.getStyle('border') ? parseInt(parentDiv.getStyle('border').match(/(\d+)px/)[1]) : 2;
	img.setStyle({
		'width': parseInt($(parentDiv).getWidth('width') - (bWidth * 2)) + "px",
		'height': parseInt($(parentDiv).getHeight('height') - (bWidth * 2)) + "px"
	});
}

function createImageObject(path, el){
	var newEl = newCommon('Bild', 'img_img_start');
	var img = $(child(newEl, ".img_img_start"));
	if(el != null)
		loadObj(newEl, el);

	child(newEl, ".img_img_start").writeAttribute({src: path});
	new Resizeable(newEl, 
		{'top': 0, 'left': 0, bottom: 10, right: 10,
		duringresize: function(el){
			stayInBox($('workspace'), el, true);
			resizeImg(img, el);
		}
	});

	resizeImg(img, newEl);
	toggleRight('imageMenu');
	return newEl;
}

So here we setup the image resizing (the dragging can be setup in the same way for all kind of elements, that’s why it’s located in newCommon). We make sure the image is contained in the box while resizing, we also need to account for the fact that the border of the containing div might be of varying thickness so we don’t draw the image bigger than the container.

function resizeTxtArea(txtArea, parentDiv){
	stayInBox($('workspace'), parentDiv, true);
	txtArea.setStyle({
		'width': parseInt($(parentDiv).getWidth('width') - 20) + "px",
		'height': parseInt($(parentDiv).getHeight('height') - 20) + "px"
	});
}

function createTxtObject(txt, el){
	var newEl = newCommon('Text', 'txt_area_start');
	var txtArea = $(child(newEl, ".txt_area_start"));
	if(el != null)
		loadObj(newEl, el);
		
	new Resizeable(newEl, 
		{'top': 0, 'left': 0, bottom: 10, right: 10, 
		duringresize: function(el){
			resizeTxtArea(txtArea, el);
		}
	});
	txtArea.update(txt);
	
	if(el != null){
		var keepArr = ['font-family', 'font-size', 'color', 'text-decoration', 'font-style', 'font-weight'];
		var styles = getStyles(keepArr, el.style);
		txtArea.writeAttribute({'style': styles});
	}
	toggleRight('textMenu');
	return newEl;
}

And the same thing for the text area we just did for the image element. Note update(txt), I think it’s the equivalent of html(txt) in jQuery.

Event.observe(window, 'load', function() {
	$('templates').hide();
	Event.observe('textButton', 'click', function(event){
		createTxtObject("Text har.", null);
	});
	
	Event.observe('el_chooser', 'change', function(event){
		var chooser = $('el_chooser');
		var curId = chooser.options[chooser.selectedIndex].value;
		if(curId != 0){
			var elType 	= curId.search(/Text/) != -1 ? "text" : "image";
			chooseElement($(curId), elType);
		}
	});
});

This is the main entrypoint, we observe a button that will create text elements and a select box that will select elements based on what we select in it. That select box is the one we manipulate with the select box functions described above, each time an object is created we have to add its id to the box and when we remove an object we have to remove it from the select box of course.

function setTxtStyle(hsh){
	$(curEl).setStyle(hsh);
	$(child(curEl, ".txt_area_start")).setStyle(hsh);
}

function setFont(){
	var fChooser = $('fontchooser');
	var fFamily = fChooser.options[fChooser.selectedIndex].value;
	setTxtStyle({fontFamily: fFamily});
}

function setFontSize(){
	var fSizeEl = $('fontsize');
	var fSize = fSizeEl.options[fSizeEl.selectedIndex].value;
	setTxtStyle({fontSize: fSize});
}

function setFontColor(colorValue)	{ setTxtStyle({'color': 		 colorValue}); }
function setItalic()				{ setTxtStyle({'fontStyle': 	 'italic'}); 	}
function setBold()					{ setTxtStyle({'fontWeight': 	 'bold'}); 		}
function setUnderline()				{ setTxtStyle({'textDecoration': 'underline'});}

function resetText(){
	setTxtStyle({'fontStyle': 			''});
	setTxtStyle({'fontWeight': 			''});
	setTxtStyle({'textDecoration': 	''});
}

function setTextAttributes(){
	setFont();
  setFontSize();
}

Some text related functions, selecting fonts and stuff.

function setImgStyle(hsh){
	$(curEl).setStyle(hsh);
	$(child(curEl, ".img_img_start")).setStyle(hsh);
}

function setBackgroundColor()			{ $(curEl).setStyle({backgroundColor: $('backgroundcolorchooser').value}); }
function setBackgroundColor(color){ $(curEl).setStyle({backgroundColor: color}); }
function setWorkspaceColor(color)	{ $('workspace').setStyle({backgroundColor: color}); }

function setWspaceImgCommon(imgRep, imgUrl, bkgPos){
	removeWorkspaceImage();
	$('workspace').setStyle({
		'backgroundRepeat': imgRep,
		'backgroundImage': imgUrl,
		'backgroundPosition': bkgPos
	});
}

function setBackgroundImage(){
    var imageChooser = $("backgroundImageChooser");
    if(imageChooser.value == null)
        return;
    child(curEl, ".img_img_start").writeAttribute({src: imageChooser.value});
}

function setWorkspaceImage(){
    var imageChooser = $('workspaceImageChooser');
    var settingChooser = $('workspaceSettingChooser');
    var workspaceElement = $('workspace');
		
    if(imageChooser.value == null || imageChooser.value == "")
        return;
    if(settingChooser.value == null || settingChooser.value == "")
        return;
		
    if(settingChooser.value == 'center')
			setWspaceImgCommon("no-repeat", "url(" + imageChooser.value + ")", "center center");
    else if(settingChooser.value == 'repeat')
			setWspaceImgCommon("repeat", "url(" + imageChooser.value + ")", "top left");
    else if(settingChooser.value == 'custom'){
			setWspaceImgCommon("no-repeat", "", "top left");
      setBackgroundImageSource(imageChooser.value);
    }else
    	alert("Ingen bakgrundsinställning hittades!");
}

function setBackgroundImageSource(imageSource){
	var wspace = $('workspace');
  var img = $("workspace_image");
  if(img == null){
  	img = document.createElement("IMG");
		wspace.appendChild(img);
	}
  
  $(img).writeAttribute({
		id: "workspace_image",
		src: imageSource
	});
	
	$(img).setStyle({
		position: 'relative',
		'top': '0px',
		'left': '0px',
		zIndex: $(wspace).getStyle('zIndex'), 
		'width': '100%', 
		'height': '100%'
	});
}

function removeWorkspaceImage(){
    if($("workspace_image") != null)
        $("workspace").removeChild($("workspace_image"));
    $("workspace").setStyle({'backgroundImage': "none"});
}

function updateWorkspaceImageChooser(){
    var workspaceImageChooser = document.getElementById("workspaceImageChooser");
    var backgroundImageChooser = document.getElementById("backgroundImageChooser");
    workspaceImageChooser.innerHTML = backgroundImageChooser.innerHTML;
}

Image and workspace related functions, setting background colors and background images and so on.

function setBorder(borderColor){
	var settings = {};
  	if(borderColor.match('no color') == null)
		settings['borderColor'] = borderColor;
  	settings['borderStyle'] = $('borderstylechooser').value;
  	settings['borderWidth'] = $('bordersizechooser').value;
	if(workWith == 'workspace')
		$('workspace').setStyle(settings);
	else
		$(curEl).setStyle(settings);
		
}

function allDefaultBorder(){
	var els = $('workspace').childElements();
	els.each(function(el){
		var cur = $(el);
		if(cur.readAttribute('class') == 'black_border')
			cur.writeAttribute({'class': 'default_border'});
	});
}

Setting a specific border and setting all borders to the default.

function getAllAsJSON(){
	var els = $('workspace').childElements();
	els.push($('workspace'));
	var toStore = [];
	var stylesToStore = [
		'font-family', 'font-size', 'padding', 'color', 
		'text-decoration', 'font-style', 'font-weight', 'background-color', 'z-index',
		'background-image', 'background-repeat', 'background-position'
	];
	
	els.each(function(el){
		var isWspace 	= el.readAttribute('id') == 'workspace' ? true : false;
		var child 		= isWspace ? el : $(el.firstDescendant());
		var div 		= $(el);
		curToStore 	= {};
		var curStyle 	= "";
		for(var i = 0; i < stylesToStore.length; i++){
			var curToGet = div.getStyle(stylesToStore[i]);
			if(curToGet != null)
				curStyle += stylesToStore[i] + ":" + curToGet + ";";
		}
		
		
		var bStyle 			= div.getStyle('borderStyle').split(' ').shift();
		if(bStyle != ""){
			var borderStyle 	= bStyle == "" ? "none" : bStyle;
			var bStyle 		= div.getStyle('borderWidth').split(' ').shift();
			var borderWidth 	= bStyle == "" ? "1px" : bStyle;
			var bStyle 		= div.getStyle('borderColor').split(')').shift() + ')';
			var borderColor 	= bStyle == ")" ? "rgb(0, 0, 0)" : bStyle;
			curStyle += "border:" + borderStyle + " " + borderWidth + " " + borderColor + ";";
		}
		
		curToStore['style'] 	= curStyle;
		curToStore['position'] 	= div.positionedOffset().left + "x" + div.positionedOffset().top;
		curToStore['size'] 		= div.getWidth() + "x" + div.getHeight();
		curToStore['id'] 		= div.readAttribute('id');
		
		if(child.hasClassName('txt_area_start')){
			curToStore['data'] 		= child.value;
			curToStore['type'] 		= 'text';
		}else if(child.hasClassName('img_img_start')){
			curToStore['data'] 		= child.readAttribute('src');
			curToStore['type'] 		= 'image';
		}else{
			curToStore['data'] 		= "";
			curToStore['type'] 		= 'workspace';
		}
		
		toStore.push(curToStore);
	});
	return toStore.toJSON();
}

Saving to the database and making sure the style attribute is consistent and will work in all browsers.

function setWorkspaceSize(){
    var sizeChooser = $('sizechooser');
    var sizeString 	= sizeChooser.value;
    var sizeArray 	= sizeString.split('x');
		$('workspace').setStyle({'width': sizeArray[0] + "px", 'height': sizeArray[1] + "px" });
		$('workspace').childElements().each(function(el){ 
			stayInBox($('workspace'), el, false); 
		});
}

If the size of the workspace changes we need to make sure all elements move so that they don’t end up being outside of the box.

function toggleRight(showMe){
	['backgroundMenu', 'imageMenu', 'textMenu'].each(function(elId){
		if(showMe == elId)
			$(elId).show();
		else
			$(elId).hide();
	});
}

function toggleTabs(clsName, elStyle){
	$('folder_templetes').className = clsName;
	$('templetes').setStyle({'display': elStyle});
	$('template_selector').setStyle({'display': elStyle});
	$('hide_icon').setStyle({'display': elStyle});
}

function toggleImgs(elStyle){
	$('image_selector').setStyle({'display': elStyle});
	$('hide_icon').setStyle({'display': elStyle});
	$('images').setStyle({'display': elStyle});
}

function hide_image_selector(){
	show_folder('contents');
	toggleRight('imageMenu');
}

function show_folder(element){
	$('folder_contents').className = (element == 'contents' || element == 'images') ? 'selected' : '';
	
	if(element == 'templetes') 		toggleTabs('selected', 'block');
	else							toggleTabs('', 'none');
	
	if(element == 'preferences')	togglePrefs('selected', 'block');
	else							togglePrefs('', 'none');
	
	if(element == 'contents')			$('contents').setStyle({'display': 'block'});
	else								$('contents').setStyle({'display': 'none'});
	
	if(element == 'images')				toggleImgs('block');
	else								toggleImgs('none');
}

Here we toggle various GUI elements on and off depending on what the user is working with, images, texts or the workspace.

function menuHandler(element_to_show, caller_element, workspace_q){
	workWith = workspace_q == true ? 'workspace' : 'element';
  	var caller = $(caller_element);
  	var element = $(element_to_show);
  	if(!element.visible())
    	    menu_show(element, caller);
  	else
    	    menu_hide(element);
}

function menu_show(element, caller){
	var newLeft = caller.positionedOffset().left + "px";
  	var newTop = (caller.positionedOffset().top + 22) + "px";
	element.setStyle({'left': newLeft, 'top': newTop});
	element.show();
}

function menu_hide(element){ element.hide(); }

Handling the menu used to work with the objects, turning stuff on and off depending on what the user is working with again.

Related Posts

Tags: , , , , ,