docs: function(attr, newValue, elems) {
  	var handle = svgroot.suspendRedraw(1000);
  	if (current_mode == 'pathedit') {
  		// Editing node
  		pathActions.moveNode(attr, newValue);
  	}
  	elems = elems || selectedElements;
  	var i = elems.length;
  	var no_xy_elems = ['g', 'polyline', 'path'];
  	var good_g_attrs = ['transform', 'opacity', 'filter'];
  	
  	while (i--) {
  		var elem = elems[i];
  		if (elem == null) {continue;}
  		
  		// Set x,y vals on elements that don't have them
  		if ((attr === 'x' || attr === 'y') && no_xy_elems.indexOf(elem.tagName) >= 0) {
  			var bbox = getStrokedBBox([elem]);
  			var diff_x = attr === 'x' ? newValue - bbox.x : 0;
  			var diff_y = attr === 'y' ? newValue - bbox.y : 0;
  			canvas.moveSelectedElements(diff_x*current_zoom, diff_y*current_zoom, true);
  			continue;
  		}
  		
  		// only allow the transform/opacity/filter attribute to change on <g> elements, slightly hacky
  		// TODO: FIXME: This doesn't seem right. Where's the body of this if statement?
  		if (elem.tagName === "g" && good_g_attrs.indexOf(attr) >= 0) {}
  		var oldval = attr === "#text" ? elem.textContent : elem.getAttribute(attr);
  		if (oldval == null) {oldval = "";}
  		if (oldval !== String(newValue)) {
  			if (attr == "#text") {
  				var old_w = svgedit.utilities.getBBox(elem).width;
  				elem.textContent = newValue;
  				
  				// FF bug occurs on on rotated elements
  				if (/rotate/.test(elem.getAttribute('transform'))) {
  					elem = ffClone(elem);
  				}
  				
  				// Hoped to solve the issue of moving text with text-anchor="start",
  				// but this doesn't actually fix it. Hopefully on the right track, though. -Fyrd
  				
  //					var box=getBBox(elem), left=box.x, top=box.y, width=box.width,
  //						height=box.height, dx = width - old_w, dy=0;
  //					var angle = svgedit.utilities.getRotationAngle(elem, true);
  //					if (angle) {
  //						var r = Math.sqrt( dx*dx + dy*dy );
  //						var theta = Math.atan2(dy,dx) - angle;
  //						dx = r * Math.cos(theta);
  //						dy = r * Math.sin(theta);
  //						
  //						elem.setAttribute('x', elem.getAttribute('x')-dx);
  //						elem.setAttribute('y', elem.getAttribute('y')-dy);
  //					}
  				
  			} else if (attr == "#href") {
  				setHref(elem, newValue);
  			}
  			else {elem.setAttribute(attr, newValue);}
  
  			// Go into "select" mode for text changes
  			// NOTE: Important that this happens AFTER elem.setAttribute() or else attributes like
  			// font-size can get reset to their old value, ultimately by svgEditor.updateContextPanel(),
  			// after calling textActions.toSelectMode() below
  			if (current_mode === "textedit" && attr !== "#text" && elem.textContent.length) {
  				textActions.toSelectMode(elem);
  			}
  
  //			if (i==0)
  //				selectedBBoxes[0] = svgedit.utilities.getBBox(elem);
  			// Use the Firefox ffClone hack for text elements with gradients or
  			// where other text attributes are changed. 
  			if (svgedit.browser.isGecko() && elem.nodeName === 'text' && /rotate/.test(elem.getAttribute('transform'))) {
  				if (String(newValue).indexOf('url') === 0 || (['font-size', 'font-family', 'x', 'y'].indexOf(attr) >= 0 && elem.textContent)) {
  					elem = ffClone(elem);
  				}
  			}
  			// Timeout needed for Opera & Firefox
  			// codedread: it is now possible for this function to be called with elements
  			// that are not in the selectedElements array, we need to only request a
  			// selector if the element is in that array
  			if (selectedElements.indexOf(elem) >= 0) {
  				setTimeout(function() {
  					// Due to element replacement, this element may no longer
  					// be part of the DOM
  					if (!elem.parentNode) {return;}
  					selectorManager.requestSelector(elem).resize();
  				}, 0);
  			}
  			// if this element was rotated, and we changed the position of this element
  			// we need to update the rotational transform attribute 
  			var angle = svgedit.utilities.getRotationAngle(elem);
  			if (angle != 0 && attr != "transform") {
  				var tlist = svgedit.transformlist.getTransformList(elem);
  				var n = tlist.numberOfItems;
  				while (n--) {
  					var xform = tlist.getItem(n);
  					if (xform.type == 4) {
  						// remove old rotate
  						tlist.removeItem(n);
  						
  						var box = svgedit.utilities.getBBox(elem);
  						var center = svgedit.math.transformPoint(box.x+box.width/2, box.y+box.height/2, svgedit.math.transformListToTransform(tlist).matrix);
  						var cx = center.x,
  							cy = center.y;
  						var newrot = svgroot.createSVGTransform();
  						newrot.setRotate(angle, cx, cy);
  						tlist.insertItemBefore(newrot, n);
  						break;
  					}
  				}
  			}
  		} // if oldValue != newValue
  	} // for each elem
  	svgroot.unsuspendRedraw(handle);	
  }