/** * Recalculate. * * Licensed under the MIT License * */ // Dependencies: // 1) jquery // 2) jquery-svg.js // 3) svgedit.js // 4) browser.js // 5) math.js // 6) history.js // 7) units.js // 8) svgtransformlist.js // 9) svgutils.js // 10) coords.js var svgedit = svgedit || {}; (function() { if (!svgedit.recalculate) { svgedit.recalculate = {}; } var NS = svgedit.NS; var context_; // Function: svgedit.recalculate.init svgedit.recalculate.init = function(editorContext) { context_ = editorContext; }; // Function: svgedit.recalculate.updateClipPath // Updates a s values based on the given translation of an element // // Parameters: // attr - The clip-path attribute value with the clipPath's ID // tx - The translation's x value // ty - The translation's y value svgedit.recalculate.updateClipPath = function(attr, tx, ty) { var path = getRefElem(attr).firstChild; var cp_xform = svgedit.transformlist.getTransformList(path); var newxlate = context_.getSVGRoot().createSVGTransform(); newxlate.setTranslate(tx, ty); cp_xform.appendItem(newxlate); // Update clipPath's dimensions svgedit.recalculate.recalculateDimensions(path); }; // Function: svgedit.recalculate.recalculateDimensions // Decides the course of action based on the element's transform list // // Parameters: // selected - The DOM element to recalculate // // Returns: // Undo command object with the resulting change svgedit.recalculate.recalculateDimensions = function(selected) { if (selected == null) return null; var svgroot = context_.getSVGRoot(); var tlist = svgedit.transformlist.getTransformList(selected); // remove any unnecessary transforms if (tlist && tlist.numberOfItems > 0) { var k = tlist.numberOfItems; while (k--) { var xform = tlist.getItem(k); if (xform.type === 0) { tlist.removeItem(k); } // remove identity matrices else if (xform.type === 1) { if (svgedit.math.isIdentity(xform.matrix)) { tlist.removeItem(k); } } // remove zero-degree rotations else if (xform.type === 4) { if (xform.angle === 0) { tlist.removeItem(k); } } } // End here if all it has is a rotation if (tlist.numberOfItems === 1 && svgedit.utilities.getRotationAngle(selected)) return null; } // if this element had no transforms, we are done if (!tlist || tlist.numberOfItems == 0) { // Chrome has a bug that requires clearing the attribute first. selected.setAttribute('transform', ''); selected.removeAttribute('transform'); return null; } // TODO: Make this work for more than 2 if (tlist) { var k = tlist.numberOfItems; var mxs = []; while (k--) { var xform = tlist.getItem(k); if (xform.type === 1) { mxs.push([xform.matrix, k]); } else if (mxs.length) { mxs = []; } } if (mxs.length === 2) { var m_new = svgroot.createSVGTransformFromMatrix(svgedit.math.matrixMultiply(mxs[1][0], mxs[0][0])); tlist.removeItem(mxs[0][1]); tlist.removeItem(mxs[1][1]); tlist.insertItemBefore(m_new, mxs[1][1]); } // combine matrix + translate k = tlist.numberOfItems; if (k >= 2 && tlist.getItem(k-2).type === 1 && tlist.getItem(k-1).type === 2) { var mt = svgroot.createSVGTransform(); var m = svgedit.math.matrixMultiply( tlist.getItem(k-2).matrix, tlist.getItem(k-1).matrix); mt.setMatrix(m); tlist.removeItem(k-2); tlist.removeItem(k-2); tlist.appendItem(mt); } } // If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned). switch ( selected.tagName ) { // Ignore these elements, as they can absorb the [M] case 'line': case 'polyline': case 'polygon': case 'path': break; default: if ((tlist.numberOfItems === 1 && tlist.getItem(0).type === 1) || (tlist.numberOfItems === 2 && tlist.getItem(0).type === 1 && tlist.getItem(0).type === 4)) { return null; } } // Grouped SVG element var gsvg = $(selected).data('gsvg'); // we know we have some transforms, so set up return variable var batchCmd = new svgedit.history.BatchCommand('Transform'); // store initial values that will be affected by reducing the transform list var changes = {}, initial = null, attrs = []; switch (selected.tagName) { case 'line': attrs = ['x1', 'y1', 'x2', 'y2']; break; case 'circle': attrs = ['cx', 'cy', 'r']; break; case 'ellipse': attrs = ['cx', 'cy', 'rx', 'ry']; break; case 'foreignObject': case 'rect': case 'image': attrs = ['width', 'height', 'x', 'y']; break; case 'use': case 'text': case 'tspan': attrs = ['x', 'y']; break; case 'polygon': case 'polyline': initial = {}; initial['points'] = selected.getAttribute('points'); var list = selected.points; var len = list.numberOfItems; changes['points'] = new Array(len); for (var i = 0; i < len; ++i) { var pt = list.getItem(i); changes['points'][i] = {x:pt.x, y:pt.y}; } break; case 'path': initial = {}; initial['d'] = selected.getAttribute('d'); changes['d'] = selected.getAttribute('d'); break; } // switch on element type to get initial values if (attrs.length) { changes = $(selected).attr(attrs); $.each(changes, function(attr, val) { changes[attr] = svgedit.units.convertToNum(attr, val); }); } else if (gsvg) { // GSVG exception changes = { x: $(gsvg).attr('x') || 0, y: $(gsvg).attr('y') || 0 }; } // if we haven't created an initial array in polygon/polyline/path, then // make a copy of initial values and include the transform if (initial == null) { initial = $.extend(true, {}, changes); $.each(initial, function(attr, val) { initial[attr] = svgedit.units.convertToNum(attr, val); }); } // save the start transform value too initial.transform = context_.getStartTransform() || ''; // if it's a regular group, we have special processing to flatten transforms if ((selected.tagName == 'g' && !gsvg) || selected.tagName == 'a') { var box = svgedit.utilities.getBBox(selected), oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2}, newcenter = svgedit.math.transformPoint(box.x+box.width/2, box.y+box.height/2, svgedit.math.transformListToTransform(tlist).matrix), m = svgroot.createSVGMatrix(); // temporarily strip off the rotate and save the old center var gangle = svgedit.utilities.getRotationAngle(selected); if (gangle) { var a = gangle * Math.PI / 180; if ( Math.abs(a) > (1.0e-10) ) { var s = Math.sin(a)/(1 - Math.cos(a)); } else { // FIXME: This blows up if the angle is exactly 0! var s = 2/a; } for (var i = 0; i < tlist.numberOfItems; ++i) { var xform = tlist.getItem(i); if (xform.type == 4) { // extract old center through mystical arts var rm = xform.matrix; oldcenter.y = (s*rm.e + rm.f)/2; oldcenter.x = (rm.e - s*rm.f)/2; tlist.removeItem(i); break; } } } var tx = 0, ty = 0, operation = 0, N = tlist.numberOfItems; if (N) { var first_m = tlist.getItem(0).matrix; } // first, if it was a scale then the second-last transform will be it if (N >= 3 && tlist.getItem(N-2).type == 3 && tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2) { operation = 3; // scale // if the children are unrotated, pass the scale down directly // otherwise pass the equivalent matrix() down directly var tm = tlist.getItem(N-3).matrix, sm = tlist.getItem(N-2).matrix, tmn = tlist.getItem(N-1).matrix; var children = selected.childNodes; var c = children.length; while (c--) { var child = children.item(c); tx = 0; ty = 0; if (child.nodeType == 1) { var childTlist = svgedit.transformlist.getTransformList(child); // some children might not have a transform (, , etc) if (!childTlist) continue; var m = svgedit.math.transformListToTransform(childTlist).matrix; // Convert a matrix to a scale if applicable // if (svgedit.math.hasMatrixTransform(childTlist) && childTlist.numberOfItems == 1) { // if (m.b==0 && m.c==0 && m.e==0 && m.f==0) { // childTlist.removeItem(0); // var translateOrigin = svgroot.createSVGTransform(), // scale = svgroot.createSVGTransform(), // translateBack = svgroot.createSVGTransform(); // translateOrigin.setTranslate(0, 0); // scale.setScale(m.a, m.d); // translateBack.setTranslate(0, 0); // childTlist.appendItem(translateBack); // childTlist.appendItem(scale); // childTlist.appendItem(translateOrigin); // } // } var angle = svgedit.utilities.getRotationAngle(child); var oldStartTransform = context_.getStartTransform(); var childxforms = []; context_.setStartTransform(child.getAttribute('transform')); if (angle || svgedit.math.hasMatrixTransform(childTlist)) { var e2t = svgroot.createSVGTransform(); e2t.setMatrix(svgedit.math.matrixMultiply(tm, sm, tmn, m)); childTlist.clear(); childTlist.appendItem(e2t); childxforms.push(e2t); } // if not rotated or skewed, push the [T][S][-T] down to the child else { // update the transform list with translate,scale,translate // slide the [T][S][-T] from the front to the back // [T][S][-T][M] = [M][T2][S2][-T2] // (only bringing [-T] to the right of [M]) // [T][S][-T][M] = [T][S][M][-T2] // [-T2] = [M_inv][-T][M] var t2n = svgedit.math.matrixMultiply(m.inverse(), tmn, m); // [T2] is always negative translation of [-T2] var t2 = svgroot.createSVGMatrix(); t2.e = -t2n.e; t2.f = -t2n.f; // [T][S][-T][M] = [M][T2][S2][-T2] // [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv] var s2 = svgedit.math.matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse()); var translateOrigin = svgroot.createSVGTransform(), scale = svgroot.createSVGTransform(), translateBack = svgroot.createSVGTransform(); translateOrigin.setTranslate(t2n.e, t2n.f); scale.setScale(s2.a, s2.d); translateBack.setTranslate(t2.e, t2.f); childTlist.appendItem(translateBack); childTlist.appendItem(scale); childTlist.appendItem(translateOrigin); childxforms.push(translateBack); childxforms.push(scale); childxforms.push(translateOrigin); // logMatrix(translateBack.matrix); // logMatrix(scale.matrix); } // not rotated batchCmd.addSubCommand( svgedit.recalculate.recalculateDimensions(child) ); // TODO: If any have this group as a parent and are // referencing this child, then we need to impose a reverse // scale on it so that when it won't get double-translated // var uses = selected.getElementsByTagNameNS(NS.SVG, 'use'); // var href = '#' + child.id; // var u = uses.length; // while (u--) { // var useElem = uses.item(u); // if (href == svgedit.utilities.getHref(useElem)) { // var usexlate = svgroot.createSVGTransform(); // usexlate.setTranslate(-tx,-ty); // svgedit.transformlist.getTransformList(useElem).insertItemBefore(usexlate,0); // batchCmd.addSubCommand( svgedit.recalculate.recalculateDimensions(useElem) ); // } // } context_.setStartTransform(oldStartTransform); } // element } // for each child // Remove these transforms from group tlist.removeItem(N-1); tlist.removeItem(N-2); tlist.removeItem(N-3); } else if (N >= 3 && tlist.getItem(N-1).type == 1) { operation = 3; // scale m = svgedit.math.transformListToTransform(tlist).matrix; var e2t = svgroot.createSVGTransform(); e2t.setMatrix(m); tlist.clear(); tlist.appendItem(e2t); } // next, check if the first transform was a translate // if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ] // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) && tlist.getItem(0).type == 2) { operation = 2; // translate var T_M = svgedit.math.transformListToTransform(tlist).matrix; tlist.removeItem(0); var M_inv = svgedit.math.transformListToTransform(tlist).matrix.inverse(); var M2 = svgedit.math.matrixMultiply( M_inv, T_M ); tx = M2.e; ty = M2.f; if (tx != 0 || ty != 0) { // we pass the translates down to the individual children var children = selected.childNodes; var c = children.length; var clipPaths_done = []; while (c--) { var child = children.item(c); if (child.nodeType == 1) { // Check if child has clip-path if (child.getAttribute('clip-path')) { // tx, ty var attr = child.getAttribute('clip-path'); if (clipPaths_done.indexOf(attr) === -1) { svgedit.recalculate.updateClipPath(attr, tx, ty); clipPaths_done.push(attr); } } var oldStartTransform = context_.getStartTransform(); context_.setStartTransform(child.getAttribute('transform')); var childTlist = svgedit.transformlist.getTransformList(child); // some children might not have a transform (, , etc) if (childTlist) { var newxlate = svgroot.createSVGTransform(); newxlate.setTranslate(tx, ty); if (childTlist.numberOfItems) { childTlist.insertItemBefore(newxlate, 0); } else { childTlist.appendItem(newxlate); } batchCmd.addSubCommand(svgedit.recalculate.recalculateDimensions(child)); // If any have this group as a parent and are // referencing this child, then impose a reverse translate on it // so that when it won't get double-translated var uses = selected.getElementsByTagNameNS(NS.SVG, 'use'); var href = '#' + child.id; var u = uses.length; while (u--) { var useElem = uses.item(u); if (href == svgedit.utilities.getHref(useElem)) { var usexlate = svgroot.createSVGTransform(); usexlate.setTranslate(-tx,-ty); svgedit.transformlist.getTransformList(useElem).insertItemBefore(usexlate, 0); batchCmd.addSubCommand( svgedit.recalculate.recalculateDimensions(useElem) ); } } context_.setStartTransform(oldStartTransform); } } } clipPaths_done = []; context_.setStartTransform(oldStartTransform); } } // else, a matrix imposition from a parent group // keep pushing it down to the children else if (N == 1 && tlist.getItem(0).type == 1 && !gangle) { operation = 1; var m = tlist.getItem(0).matrix, children = selected.childNodes, c = children.length; while (c--) { var child = children.item(c); if (child.nodeType == 1) { var oldStartTransform = context_.getStartTransform(); context_.setStartTransform(child.getAttribute('transform')); var childTlist = svgedit.transformlist.getTransformList(child); if (!childTlist) continue; var em = svgedit.math.matrixMultiply(m, svgedit.math.transformListToTransform(childTlist).matrix); var e2m = svgroot.createSVGTransform(); e2m.setMatrix(em); childTlist.clear(); childTlist.appendItem(e2m, 0); batchCmd.addSubCommand( svgedit.recalculate.recalculateDimensions(child) ); context_.setStartTransform(oldStartTransform); // Convert stroke // TODO: Find out if this should actually happen somewhere else var sw = child.getAttribute('stroke-width'); if (child.getAttribute('stroke') !== 'none' && !isNaN(sw)) { var avg = (Math.abs(em.a) + Math.abs(em.d)) / 2; child.setAttribute('stroke-width', sw * avg); } } } tlist.clear(); } // else it was just a rotate else { if (gangle) { var newRot = svgroot.createSVGTransform(); newRot.setRotate(gangle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } if (tlist.numberOfItems == 0) { selected.removeAttribute('transform'); } return null; } // if it was a translate, put back the rotate at the new center if (operation == 2) { if (gangle) { newcenter = { x: oldcenter.x + first_m.e, y: oldcenter.y + first_m.f }; var newRot = svgroot.createSVGTransform(); newRot.setRotate(gangle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } } // if it was a resize else if (operation == 3) { var m = svgedit.math.transformListToTransform(tlist).matrix; var roldt = svgroot.createSVGTransform(); roldt.setRotate(gangle, oldcenter.x, oldcenter.y); var rold = roldt.matrix; var rnew = svgroot.createSVGTransform(); rnew.setRotate(gangle, newcenter.x, newcenter.y); var rnew_inv = rnew.matrix.inverse(), m_inv = m.inverse(), extrat = svgedit.math.matrixMultiply(m_inv, rnew_inv, rold, m); tx = extrat.e; ty = extrat.f; if (tx != 0 || ty != 0) { // now push this transform down to the children // we pass the translates down to the individual children var children = selected.childNodes; var c = children.length; while (c--) { var child = children.item(c); if (child.nodeType == 1) { var oldStartTransform = context_.getStartTransform(); context_.setStartTransform(child.getAttribute('transform')); var childTlist = svgedit.transformlist.getTransformList(child); var newxlate = svgroot.createSVGTransform(); newxlate.setTranslate(tx, ty); if (childTlist.numberOfItems) { childTlist.insertItemBefore(newxlate, 0); } else { childTlist.appendItem(newxlate); } batchCmd.addSubCommand( svgedit.recalculate.recalculateDimensions(child) ); context_.setStartTransform(oldStartTransform); } } } if (gangle) { if (tlist.numberOfItems) { tlist.insertItemBefore(rnew, 0); } else { tlist.appendItem(rnew); } } } } // else, it's a non-group else { // FIXME: box might be null for some elements ( etc), need to handle this var box = svgedit.utilities.getBBox(selected); // Paths (and possbly other shapes) will have no BBox while still in , // but we still may need to recalculate them (see issue 595). // TODO: Figure out how to get BBox from these elements in case they // have a rotation transform if (!box && selected.tagName != 'path') return null; var m = svgroot.createSVGMatrix(), // temporarily strip off the rotate and save the old center angle = svgedit.utilities.getRotationAngle(selected); if (angle) { var oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2}, newcenter = svgedit.math.transformPoint(box.x+box.width/2, box.y+box.height/2, svgedit.math.transformListToTransform(tlist).matrix); var a = angle * Math.PI / 180; if ( Math.abs(a) > (1.0e-10) ) { var s = Math.sin(a)/(1 - Math.cos(a)); } else { // FIXME: This blows up if the angle is exactly 0! var s = 2/a; } for (var i = 0; i < tlist.numberOfItems; ++i) { var xform = tlist.getItem(i); if (xform.type == 4) { // extract old center through mystical arts var rm = xform.matrix; oldcenter.y = (s*rm.e + rm.f)/2; oldcenter.x = (rm.e - s*rm.f)/2; tlist.removeItem(i); break; } } } // 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition var operation = 0; var N = tlist.numberOfItems; // Check if it has a gradient with userSpaceOnUse, in which case // adjust it by recalculating the matrix transform. // TODO: Make this work in Webkit using svgedit.transformlist.SVGTransformList if (!svgedit.browser.isWebkit()) { var fill = selected.getAttribute('fill'); if (fill && fill.indexOf('url(') === 0) { var paint = getRefElem(fill); var type = 'pattern'; if (paint.tagName !== type) type = 'gradient'; var attrVal = paint.getAttribute(type + 'Units'); if (attrVal === 'userSpaceOnUse') { //Update the userSpaceOnUse element m = svgedit.math.transformListToTransform(tlist).matrix; var gtlist = svgedit.transformlist.getTransformList(paint); var gmatrix = svgedit.math.transformListToTransform(gtlist).matrix; m = svgedit.math.matrixMultiply(m, gmatrix); var m_str = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')'; paint.setAttribute(type + 'Transform', m_str); } } } // first, if it was a scale of a non-skewed element, then the second-last // transform will be the [S] // if we had [M][T][S][T] we want to extract the matrix equivalent of // [T][S][T] and push it down to the element if (N >= 3 && tlist.getItem(N-2).type == 3 && tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2) // Removed this so a with a given [T][S][T] would convert to a matrix. // Is that bad? // && selected.nodeName != 'use' { operation = 3; // scale m = svgedit.math.transformListToTransform(tlist, N-3, N-1).matrix; tlist.removeItem(N-1); tlist.removeItem(N-2); tlist.removeItem(N-3); } // if we had [T][S][-T][M], then this was a skewed element being resized // Thus, we simply combine it all into one matrix else if (N == 4 && tlist.getItem(N-1).type == 1) { operation = 3; // scale m = svgedit.math.transformListToTransform(tlist).matrix; var e2t = svgroot.createSVGTransform(); e2t.setMatrix(m); tlist.clear(); tlist.appendItem(e2t); // reset the matrix so that the element is not re-mapped m = svgroot.createSVGMatrix(); } // if we had [R][T][S][-T][M], then this was a rotated matrix-element // if we had [T1][M] we want to transform this into [M][T2] // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2] // down to the element else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) && tlist.getItem(0).type == 2) { operation = 2; // translate var oldxlate = tlist.getItem(0).matrix, meq = svgedit.math.transformListToTransform(tlist,1).matrix, meq_inv = meq.inverse(); m = svgedit.math.matrixMultiply( meq_inv, oldxlate, meq ); tlist.removeItem(0); } // else if this child now has a matrix imposition (from a parent group) // we might be able to simplify else if (N == 1 && tlist.getItem(0).type == 1 && !angle) { // Remap all point-based elements m = svgedit.math.transformListToTransform(tlist).matrix; switch (selected.tagName) { case 'line': changes = $(selected).attr(['x1', 'y1', 'x2', 'y2']); case 'polyline': case 'polygon': changes.points = selected.getAttribute('points'); if (changes.points) { var list = selected.points; var len = list.numberOfItems; changes.points = new Array(len); for (var i = 0; i < len; ++i) { var pt = list.getItem(i); changes.points[i] = {x:pt.x, y:pt.y}; } } case 'path': changes.d = selected.getAttribute('d'); operation = 1; tlist.clear(); break; default: break; } } // if it was a rotation, put the rotate back and return without a command // (this function has zero work to do for a rotate()) else { operation = 4; // rotation if (angle) { var newRot = svgroot.createSVGTransform(); newRot.setRotate(angle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } if (tlist.numberOfItems == 0) { selected.removeAttribute('transform'); } return null; } // if it was a translate or resize, we need to remap the element and absorb the xform if (operation == 1 || operation == 2 || operation == 3) { svgedit.coords.remapElement(selected, changes, m); } // if we are remapping // if it was a translate, put back the rotate at the new center if (operation == 2) { if (angle) { if (!svgedit.math.hasMatrixTransform(tlist)) { newcenter = { x: oldcenter.x + m.e, y: oldcenter.y + m.f }; } var newRot = svgroot.createSVGTransform(); newRot.setRotate(angle, newcenter.x, newcenter.y); if (tlist.numberOfItems) { tlist.insertItemBefore(newRot, 0); } else { tlist.appendItem(newRot); } } // We have special processing for tspans: Tspans are not transformable // but they can have x,y coordinates (sigh). Thus, if this was a translate, // on a text element, also translate any tspan children. if (selected.tagName == 'text') { var children = selected.childNodes; var c = children.length; while (c--) { var child = children.item(c); if (child.tagName == 'tspan') { var tspanChanges = { x: $(child).attr('x') || 0, y: $(child).attr('y') || 0 }; svgedit.coords.remapElement(child, tspanChanges, m); } } } } // [Rold][M][T][S][-T] became [Rold][M] // we want it to be [Rnew][M][Tr] where Tr is the // translation required to re-center it // Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M] else if (operation == 3 && angle) { var m = svgedit.math.transformListToTransform(tlist).matrix; var roldt = svgroot.createSVGTransform(); roldt.setRotate(angle, oldcenter.x, oldcenter.y); var rold = roldt.matrix; var rnew = svgroot.createSVGTransform(); rnew.setRotate(angle, newcenter.x, newcenter.y); var rnew_inv = rnew.matrix.inverse(); var m_inv = m.inverse(); var extrat = svgedit.math.matrixMultiply(m_inv, rnew_inv, rold, m); svgedit.coords.remapElement(selected, changes, extrat); if (angle) { if (tlist.numberOfItems) { tlist.insertItemBefore(rnew, 0); } else { tlist.appendItem(rnew); } } } } // a non-group // if the transform list has been emptied, remove it if (tlist.numberOfItems == 0) { selected.removeAttribute('transform'); } batchCmd.addSubCommand(new svgedit.history.ChangeElementCommand(selected, initial)); return batchCmd; }; })();