Source: svgcanvas/selected-elem.js

/**
 * Tools for SVG selected element operation.
 * @module selected-elem
 * @license MIT
 *
 * @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
 */

import { NS } from './namespaces.js'
import * as hstry from './history.js'
import * as pathModule from './path.js'
import {
  isNullish, getStrokedBBoxDefaultVisible, setHref, getElement, getHref, getVisibleElements,
  findDefs, getRotationAngle, getRefElem, getBBox as utilsGetBBox, walkTreePost, assignAttributes, getFeGaussianBlur
} from './utilities.js'
import {
  transformPoint, matrixMultiply, transformListToTransform
} from './math.js'
import {
  recalculateDimensions
} from './recalculate.js'
import {
  isGecko
} from '../common/browser.js' // , supportsEditableText
import { getParents } from '../editor/components/jgraduate/Util.js'

const {
  MoveElementCommand, BatchCommand, InsertElementCommand, RemoveElementCommand, ChangeElementCommand
} = hstry

let svgCanvas = null

/**
* @function module:selected-elem.init
* @param {module:selected-elem.elementContext} elementContext
* @returns {void}
*/
export const init = (canvas) => {
  svgCanvas = canvas
}

/**
* Repositions the selected element to the bottom in the DOM to appear on top of
* other elements.
* @function module:selected-elem.SvgCanvas#moveToTopSelectedElem
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const moveToTopSelectedElem = function () {
  const [selected] = svgCanvas.getSelectedElements()
  if (!isNullish(selected)) {
    const t = selected
    const oldParent = t.parentNode
    const oldNextSibling = t.nextSibling
    t.parentNode.append(t)
    // If the element actually moved position, add the command and fire the changed
    // event handler.
    if (oldNextSibling !== t.nextSibling) {
      svgCanvas.addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'top'))
      svgCanvas.call('changed', [t])
    }
  }
}

/**
* Repositions the selected element to the top in the DOM to appear under
* other elements.
* @function module:selected-elem.SvgCanvas#moveToBottomSelectedElement
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const moveToBottomSelectedElem = function () {
  const [selected] = svgCanvas.getSelectedElements()
  if (!isNullish(selected)) {
    let t = selected
    const oldParent = t.parentNode
    const oldNextSibling = t.nextSibling
    let { firstChild } = t.parentNode
    if (firstChild.tagName === 'title') {
      firstChild = firstChild.nextSibling
    }
    // This can probably be removed, as the defs should not ever apppear
    // inside a layer group
    if (firstChild.tagName === 'defs') {
      firstChild = firstChild.nextSibling
    }
    t = t.parentNode.insertBefore(t, firstChild)
    // If the element actually moved position, add the command and fire the changed
    // event handler.
    if (oldNextSibling !== t.nextSibling) {
      svgCanvas.addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'bottom'))
      svgCanvas.call('changed', [t])
    }
  }
}

/**
* Moves the select element up or down the stack, based on the visibly
* intersecting elements.
* @function module:selected-elem.SvgCanvas#moveUpDownSelected
* @param {"Up"|"Down"} dir - String that's either 'Up' or 'Down'
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const moveUpDownSelected = function (dir) {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (!selected) { return }

  svgCanvas.setCurBBoxes([])
  let closest; let foundCur
  // jQuery sorts this list
  const list = svgCanvas.getIntersectionList(getStrokedBBoxDefaultVisible([selected]))
  if (dir === 'Down') { list.reverse() }

  Array.prototype.forEach.call(list, function (el) {
    if (!foundCur) {
      if (el === selected) {
        foundCur = true
      }
      return true
    }
    closest = el
    return false
  })
  if (!closest) { return }

  const t = selected
  const oldParent = t.parentNode
  const oldNextSibling = t.nextSibling
  if (dir === 'Down') {
    closest.insertAdjacentElement('beforebegin', t)
  } else {
    closest.insertAdjacentElement('afterend', t)
  }
  // If the element actually moved position, add the command and fire the changed
  // event handler.
  if (oldNextSibling !== t.nextSibling) {
    svgCanvas.addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir))
    svgCanvas.call('changed', [t])
  }
}

/**
* Moves selected elements on the X/Y axis.
* @function module:selected-elem.SvgCanvas#moveSelectedElements
* @param {Float} dx - Float with the distance to move on the x-axis
* @param {Float} dy - Float with the distance to move on the y-axis
* @param {boolean} undoable - Boolean indicating whether or not the action should be undoable
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {BatchCommand|void} Batch command for the move
*/

export const moveSelectedElements = function (dx, dy, undoable = true) {
  const selectedElements = svgCanvas.getSelectedElements()
  const zoom = svgCanvas.getZoom()
  // if undoable is not sent, default to true
  // if single values, scale them to the zoom
  if (!Array.isArray(dx)) {
    dx /= zoom
    dy /= zoom
  }

  const batchCmd = new BatchCommand('position')
  selectedElements.forEach((selected, i) => {
    if (selected) {
      const xform = svgCanvas.getSvgRoot().createSVGTransform()
      const tlist = selected.transform?.baseVal

      // dx and dy could be arrays
      if (Array.isArray(dx)) {
        xform.setTranslate(dx[i], dy[i])
      } else {
        xform.setTranslate(dx, dy)
      }

      if (tlist.numberOfItems) {
        tlist.insertItemBefore(xform, 0)
      } else {
        tlist.appendItem(xform)
      }

      const cmd = recalculateDimensions(selected)
      if (cmd) {
        batchCmd.addSubCommand(cmd)
      }

      svgCanvas.gettingSelectorManager().requestSelector(selected).resize()
    }
  })
  if (!batchCmd.isEmpty()) {
    if (undoable) {
      svgCanvas.addCommandToHistory(batchCmd)
    }
    svgCanvas.call('changed', selectedElements)
    return batchCmd
  }
  return undefined
}

/**
* Create deep DOM copies (clones) of all selected elements and move them slightly
* from their originals.
* @function module:selected-elem.SvgCanvas#cloneSelectedElements
* @param {Float} x Float with the distance to move on the x-axis
* @param {Float} y Float with the distance to move on the y-axis
* @returns {void}
*/
export const cloneSelectedElements = function (x, y) {
  const selectedElements = svgCanvas.getSelectedElements()
  const currentGroup = svgCanvas.getCurrentGroup()
  let i; let elem
  const batchCmd = new BatchCommand('Clone Elements')
  // find all the elements selected (stop at first null)
  const len = selectedElements.length

  function index (el) {
    if (!el) return -1
    let i = 0
    do {
      i++
    } while (el === el.previousElementSibling)
    return i
  }

  /**
* Sorts an array numerically and ascending.
* @param {Element} a
* @param {Element} b
* @returns {Integer}
*/
  function sortfunction (a, b) {
    return (index(b) - index(a))
  }
  selectedElements.sort(sortfunction)
  for (i = 0; i < len; ++i) {
    elem = selectedElements[i]
    if (isNullish(elem)) { break }
  }
  // use slice to quickly get the subset of elements we need
  const copiedElements = selectedElements.slice(0, i)
  svgCanvas.clearSelection(true)
  // note that we loop in the reverse way because of the way elements are added
  // to the selectedElements array (top-first)
  const drawing = svgCanvas.getDrawing()
  i = copiedElements.length
  while (i--) {
    // clone each element and replace it within copiedElements
    elem = copiedElements[i] = drawing.copyElem(copiedElements[i]);
    (currentGroup || drawing.getCurrentLayer()).append(elem)
    batchCmd.addSubCommand(new InsertElementCommand(elem))
  }

  if (!batchCmd.isEmpty()) {
    svgCanvas.addToSelection(copiedElements.reverse()) // Need to reverse for correct selection-adding
    moveSelectedElements(x, y, false)
    svgCanvas.addCommandToHistory(batchCmd)
  }
}
/**
* Aligns selected elements.
* @function module:selected-elem.SvgCanvas#alignSelectedElements
* @param {string} type - String with single character indicating the alignment type
* @param {"selected"|"largest"|"smallest"|"page"} relativeTo
* @returns {void}
*/
export const alignSelectedElements = function (type, relativeTo) {
  const selectedElements = svgCanvas.getSelectedElements()
  const bboxes = [] // angles = [];
  const len = selectedElements.length
  if (!len) { return }
  let minx = Number.MAX_VALUE; let maxx = Number.MIN_VALUE
  let miny = Number.MAX_VALUE; let maxy = Number.MIN_VALUE
  let curwidth = Number.MIN_VALUE; let curheight = Number.MIN_VALUE
  for (let i = 0; i < len; ++i) {
    if (isNullish(selectedElements[i])) { break }
    const elem = selectedElements[i]
    bboxes[i] = getStrokedBBoxDefaultVisible([elem])

    // now bbox is axis-aligned and handles rotation
    switch (relativeTo) {
      case 'smallest':
        if (((type === 'l' || type === 'c' || type === 'r' || type === 'left' || type === 'center' || type === 'right') &&
          (curwidth === Number.MIN_VALUE || curwidth > bboxes[i].width)) ||
          ((type === 't' || type === 'm' || type === 'b' || type === 'top' || type === 'middle' || type === 'bottom') &&
            (curheight === Number.MIN_VALUE || curheight > bboxes[i].height))
        ) {
          minx = bboxes[i].x
          miny = bboxes[i].y
          maxx = bboxes[i].x + bboxes[i].width
          maxy = bboxes[i].y + bboxes[i].height
          curwidth = bboxes[i].width
          curheight = bboxes[i].height
        }
        break
      case 'largest':
        if (((type === 'l' || type === 'c' || type === 'r' || type === 'left' || type === 'center' || type === 'right') &&
          (curwidth === Number.MIN_VALUE || curwidth < bboxes[i].width)) ||
          ((type === 't' || type === 'm' || type === 'b' || type === 'top' || type === 'middle' || type === 'bottom') &&
            (curheight === Number.MIN_VALUE || curheight < bboxes[i].height))
        ) {
          minx = bboxes[i].x
          miny = bboxes[i].y
          maxx = bboxes[i].x + bboxes[i].width
          maxy = bboxes[i].y + bboxes[i].height
          curwidth = bboxes[i].width
          curheight = bboxes[i].height
        }
        break
      default: // 'selected'
        if (bboxes[i].x < minx) { minx = bboxes[i].x }
        if (bboxes[i].y < miny) { miny = bboxes[i].y }
        if (bboxes[i].x + bboxes[i].width > maxx) { maxx = bboxes[i].x + bboxes[i].width }
        if (bboxes[i].y + bboxes[i].height > maxy) { maxy = bboxes[i].y + bboxes[i].height }
        break
    }
  } // loop for each element to find the bbox and adjust min/max

  if (relativeTo === 'page') {
    minx = 0
    miny = 0
    maxx = svgCanvas.getContentW()
    maxy = svgCanvas.getContentH()
  }

  const dx = new Array(len)
  const dy = new Array(len)
  for (let i = 0; i < len; ++i) {
    if (isNullish(selectedElements[i])) { break }
    // const elem = selectedElements[i];
    const bbox = bboxes[i]
    dx[i] = 0
    dy[i] = 0
    switch (type) {
      case 'l': // left (horizontal)
      case 'left': // left (horizontal)
        dx[i] = minx - bbox.x
        break
      case 'c': // center (horizontal)
      case 'center': // center (horizontal)
        dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2)
        break
      case 'r': // right (horizontal)
      case 'right': // right (horizontal)
        dx[i] = maxx - (bbox.x + bbox.width)
        break
      case 't': // top (vertical)
      case 'top': // top (vertical)
        dy[i] = miny - bbox.y
        break
      case 'm': // middle (vertical)
      case 'middle': // middle (vertical)
        dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2)
        break
      case 'b': // bottom (vertical)
      case 'bottom': // bottom (vertical)
        dy[i] = maxy - (bbox.y + bbox.height)
        break
    }
  }
  moveSelectedElements(dx, dy)
}

/**
* Removes all selected elements from the DOM and adds the change to the
* history stack.
* @function module:selected-elem.SvgCanvas#deleteSelectedElements
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const deleteSelectedElements = function () {
  const selectedElements = svgCanvas.getSelectedElements()
  const batchCmd = new BatchCommand('Delete Elements')
  const len = selectedElements.length
  const selectedCopy = [] // selectedElements is being deleted

  for (let i = 0; i < len; ++i) {
    const selected = selectedElements[i]
    if (isNullish(selected)) { break }

    let parent = selected.parentNode
    let t = selected

    // this will unselect the element and remove the selectedOutline
    svgCanvas.gettingSelectorManager().releaseSelector(t)

    // Remove the path if present.
    pathModule.removePath_(t.id)

    // Get the parent if it's a single-child anchor
    if (parent.tagName === 'a' && parent.childNodes.length === 1) {
      t = parent
      parent = parent.parentNode
    }

    const { nextSibling } = t
    t.remove()
    const elem = t
    selectedCopy.push(selected) // for the copy
    batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent))
  }
  svgCanvas.setEmptySelectedElements()

  if (!batchCmd.isEmpty()) { svgCanvas.addCommandToHistory(batchCmd) }
  svgCanvas.call('changed', selectedCopy)
  svgCanvas.clearSelection()
}

/**
* Remembers the current selected elements on the clipboard.
* @function module:selected-elem.SvgCanvas#copySelectedElements
* @returns {void}
*/
export const copySelectedElements = function () {
  const selectedElements = svgCanvas.getSelectedElements()
  const data =
    JSON.stringify(selectedElements.map((x) => svgCanvas.getJsonFromSvgElements(x)))
  // Use sessionStorage for the clipboard data.
  sessionStorage.setItem(svgCanvas.getClipboardID(), data)
  svgCanvas.flashStorage()

  // Context menu might not exist (it is provided by editor.js).
  const canvMenu = document.getElementById('se-cmenu_canvas')
  canvMenu.setAttribute('enablemenuitems', '#paste,#paste_in_place')
}

/**
* Wraps all the selected elements in a group (`g`) element.
* @function module:selected-elem.SvgCanvas#groupSelectedElements
* @param {"a"|"g"} [type="g"] - type of element to group into, defaults to `<g>`
* @param {string} [urlArg]
* @returns {void}
*/
export const groupSelectedElements = function (type, urlArg) {
  const selectedElements = svgCanvas.getSelectedElements()
  if (!type) { type = 'g' }
  let cmdStr = ''
  let url

  switch (type) {
    case 'a': {
      cmdStr = 'Make hyperlink'
      url = urlArg || ''
      break
    } default: {
      type = 'g'
      cmdStr = 'Group Elements'
      break
    }
  }

  const batchCmd = new BatchCommand(cmdStr)

  // create and insert the group element
  const g = svgCanvas.addSVGElementsFromJson({
    element: type,
    attr: {
      id: svgCanvas.getNextId()
    }
  })
  if (type === 'a') {
    setHref(g, url)
  }
  batchCmd.addSubCommand(new InsertElementCommand(g))

  // now move all children into the group
  let i = selectedElements.length
  while (i--) {
    let elem = selectedElements[i]
    if (isNullish(elem)) { continue }

    if (elem.parentNode.tagName === 'a' && elem.parentNode.childNodes.length === 1) {
      elem = elem.parentNode
    }

    const oldNextSibling = elem.nextSibling
    const oldParent = elem.parentNode
    g.append(elem)
    batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent))
  }
  if (!batchCmd.isEmpty()) { svgCanvas.addCommandToHistory(batchCmd) }

  // update selection
  svgCanvas.selectOnly([g], true)
}

/**
* Pushes all appropriate parent group properties down to its children, then
* removes them from the group.
* @function module:selected-elem.SvgCanvas#pushGroupProperty
* @param {SVGAElement|SVGGElement} g
* @param {boolean} undoable
* @returns {BatchCommand|void}
*/
export const pushGroupProperty = function (g, undoable) {
  const children = g.childNodes
  const len = children.length
  const xform = g.getAttribute('transform')

  const glist = g.transform.baseVal
  const m = transformListToTransform(glist).matrix

  const batchCmd = new BatchCommand('Push group properties')

  // TODO: get all fill/stroke properties from the group that we are about to destroy
  // "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
  // "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
  // "stroke-width"
  // and then for each child, if they do not have the attribute (or the value is 'inherit')
  // then set the child's attribute

  const gangle = getRotationAngle(g)

  const gattrs = {
    filter: g.getAttribute('filter'),
    opacity: g.getAttribute('opacity')
  }
  let gfilter; let gblur; let changes
  const drawing = svgCanvas.getDrawing()

  for (let i = 0; i < len; i++) {
    const elem = children[i]

    if (elem.nodeType !== 1) { continue }

    if (gattrs.opacity !== null && gattrs.opacity !== 1) {
      // const c_opac = elem.getAttribute('opacity') || 1;
      const newOpac = Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100) / 100
      svgCanvas.changeSelectedAttribute('opacity', newOpac, [elem])
    }

    if (gattrs.filter) {
      let cblur = svgCanvas.getBlur(elem)
      const origCblur = cblur
      if (!gblur) { gblur = svgCanvas.getBlur(g) }
      if (cblur) {
        // Is this formula correct?
        cblur = Number(gblur) + Number(cblur)
      } else if (cblur === 0) {
        cblur = gblur
      }

      // If child has no current filter, get group's filter or clone it.
      if (!origCblur) {
        // Set group's filter to use first child's ID
        if (!gfilter) {
          gfilter = getRefElem(gattrs.filter)
        } else {
          // Clone the group's filter
          gfilter = drawing.copyElem(gfilter)
          findDefs().append(gfilter)

          // const filterElem = getRefElem(gfilter);
          const blurElem = getFeGaussianBlur(gfilter)
          // Change this in future for different filters
          const suffix = (blurElem?.tagName === 'feGaussianBlur') ? 'blur' : 'filter'
          gfilter.id = elem.id + '_' + suffix
          svgCanvas.changeSelectedAttribute('filter', 'url(#' + gfilter.id + ')', [elem])
        }
      } else {
        gfilter = getRefElem(elem.getAttribute('filter'))
      }
      // const filterElem = getRefElem(gfilter);
      const blurElem = getFeGaussianBlur(gfilter)

      // Update blur value
      if (cblur) {
        svgCanvas.changeSelectedAttribute('stdDeviation', cblur, [blurElem])
        svgCanvas.setBlurOffsets(gfilter, cblur)
      }
    }

    let chtlist = elem.transform?.baseVal

    // Don't process gradient transforms
    if (elem.tagName.includes('Gradient')) { chtlist = null }

    // Hopefully not a problem to add this. Necessary for elements like <desc/>
    if (!chtlist) { continue }

    // Apparently <defs> can get get a transformlist, but we don't want it to have one!
    if (elem.tagName === 'defs') { continue }

    if (glist.numberOfItems) {
      // TODO: if the group's transform is just a rotate, we can always transfer the
      // rotate() down to the children (collapsing consecutive rotates and factoring
      // out any translates)
      if (gangle && glist.numberOfItems === 1) {
        // [Rg] [Rc] [Mc]
        // we want [Tr] [Rc2] [Mc] where:
        //  - [Rc2] is at the child's current center but has the
        // sum of the group and child's rotation angles
        //  - [Tr] is the equivalent translation that this child
        // undergoes if the group wasn't there

        // [Tr] = [Rg] [Rc] [Rc2_inv]

        // get group's rotation matrix (Rg)
        const rgm = glist.getItem(0).matrix

        // get child's rotation matrix (Rc)
        let rcm = svgCanvas.getSvgRoot().createSVGMatrix()
        const cangle = getRotationAngle(elem)
        if (cangle) {
          rcm = chtlist.getItem(0).matrix
        }

        // get child's old center of rotation
        const cbox = utilsGetBBox(elem)
        const ceqm = transformListToTransform(chtlist).matrix
        const coldc = transformPoint(cbox.x + cbox.width / 2, cbox.y + cbox.height / 2, ceqm)

        // sum group and child's angles
        const sangle = gangle + cangle

        // get child's rotation at the old center (Rc2_inv)
        const r2 = svgCanvas.getSvgRoot().createSVGTransform()
        r2.setRotate(sangle, coldc.x, coldc.y)

        // calculate equivalent translate
        const trm = matrixMultiply(rgm, rcm, r2.matrix.inverse())

        // set up tlist
        if (cangle) {
          chtlist.removeItem(0)
        }

        if (sangle) {
          if (chtlist.numberOfItems) {
            chtlist.insertItemBefore(r2, 0)
          } else {
            chtlist.appendItem(r2)
          }
        }

        if (trm.e || trm.f) {
          const tr = svgCanvas.getSvgRoot().createSVGTransform()
          tr.setTranslate(trm.e, trm.f)
          if (chtlist.numberOfItems) {
            chtlist.insertItemBefore(tr, 0)
          } else {
            chtlist.appendItem(tr)
          }
        }
      } else { // more complicated than just a rotate
        // transfer the group's transform down to each child and then
        // call recalculateDimensions()
        const oldxform = elem.getAttribute('transform')
        changes = {}
        changes.transform = oldxform || ''

        const newxform = svgCanvas.getSvgRoot().createSVGTransform()

        // [ gm ] [ chm ] = [ chm ] [ gm' ]
        // [ gm' ] = [ chmInv ] [ gm ] [ chm ]
        const chm = transformListToTransform(chtlist).matrix
        const chmInv = chm.inverse()
        const gm = matrixMultiply(chmInv, m, chm)
        newxform.setMatrix(gm)
        chtlist.appendItem(newxform)
      }
      const cmd = recalculateDimensions(elem)
      if (cmd) { batchCmd.addSubCommand(cmd) }
    }
  }

  // remove transform and make it undo-able
  if (xform) {
    changes = {}
    changes.transform = xform
    g.setAttribute('transform', '')
    g.removeAttribute('transform')
    batchCmd.addSubCommand(new ChangeElementCommand(g, changes))
  }

  if (undoable && !batchCmd.isEmpty()) {
    return batchCmd
  }
  return undefined
}

/**
* Converts selected/given `<use>` or child SVG element to a group.
* @function module:selected-elem.SvgCanvas#convertToGroup
* @param {Element} elem
* @fires module:selected-elem.SvgCanvas#event:selected
* @returns {void}
*/
export const convertToGroup = function (elem) {
  const selectedElements = svgCanvas.getSelectedElements()
  if (!elem) {
    elem = selectedElements[0]
  }
  const $elem = elem
  const batchCmd = new BatchCommand()
  let ts
  const dataStorage = svgCanvas.getDataStorage()
  if (dataStorage.has($elem, 'gsvg')) {
    // Use the gsvg as the new group
    const svg = elem.firstChild
    const pt = {
      x: Number(svg.getAttribute('x')),
      y: Number(svg.getAttribute('y'))
    }

    // $(elem.firstChild.firstChild).unwrap();
    const firstChild = elem.firstChild.firstChild
    if (firstChild) {
      firstChild.outerHTML = firstChild.innerHTML
    }
    dataStorage.remove(elem, 'gsvg')

    const tlist = elem.transform.baseVal
    const xform = svgCanvas.getSvgRoot().createSVGTransform()
    xform.setTranslate(pt.x, pt.y)
    tlist.appendItem(xform)
    recalculateDimensions(elem)
    svgCanvas.call('selected', [elem])
  } else if (dataStorage.has($elem, 'symbol')) {
    elem = dataStorage.get($elem, 'symbol')

    ts = $elem.getAttribute('transform')
    const pos = {
      x: Number($elem.getAttribute('x')),
      y: Number($elem.getAttribute('y'))
    }

    const vb = elem.getAttribute('viewBox')

    if (vb) {
      const nums = vb.split(' ')
      pos.x -= Number(nums[0])
      pos.y -= Number(nums[1])
    }

    // Not ideal, but works
    ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'

    const prev = $elem.previousElementSibling

    // Remove <use> element
    batchCmd.addSubCommand(new RemoveElementCommand($elem, $elem.nextElementSibling, $elem.parentNode))
    $elem.remove()

    // See if other elements reference this symbol
    const svgContent = svgCanvas.getSvgContent()
    // const hasMore = svgContent.querySelectorAll('use:data(symbol)').length;
    // @todo review this logic
    const hasMore = svgContent.querySelectorAll('use').length

    const g = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'g')
    const childs = elem.childNodes

    let i
    for (i = 0; i < childs.length; i++) {
      g.append(childs[i].cloneNode(true))
    }

    // Duplicate the gradients for Gecko, since they weren't included in the <symbol>
    if (isGecko()) {
      const svgElement = findDefs()
      const gradients = svgElement.querySelectorAll('linearGradient,radialGradient,pattern')
      for (let i = 0, im = gradients.length; im > i; i++) {
        g.appendChild(gradients[i].cloneNode(true))
      }
    }

    if (ts) {
      g.setAttribute('transform', ts)
    }

    const parent = elem.parentNode

    svgCanvas.uniquifyElems(g)

    // Put the dupe gradients back into <defs> (after uniquifying them)
    if (isGecko()) {
      const svgElement = findDefs()
      const elements = g.querySelectorAll('linearGradient,radialGradient,pattern')
      for (let i = 0, im = elements.length; im > i; i++) {
        svgElement.appendChild(elements[i])
      }
    }

    // now give the g itself a new id
    g.id = svgCanvas.getNextId()

    prev.after(g)

    if (parent) {
      if (!hasMore) {
        // remove symbol/svg element
        const { nextSibling } = elem
        elem.remove()
        batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent))
      }
      batchCmd.addSubCommand(new InsertElementCommand(g))
    }

    svgCanvas.setUseData(g)

    if (isGecko()) {
      svgCanvas.convertGradients(findDefs())
    } else {
      svgCanvas.convertGradients(g)
    }

    // recalculate dimensions on the top-level children so that unnecessary transforms
    // are removed
    walkTreePost(g, function (n) {
      try {
        recalculateDimensions(n)
      } catch (e) {
        console.error(e)
      }
    })

    // Give ID for any visible element missing one
    const visElems = g.querySelectorAll(svgCanvas.getVisElems())
    Array.prototype.forEach.call(visElems, function (el) {
      if (!el.id) { el.id = svgCanvas.getNextId() }
    })

    svgCanvas.selectOnly([g])

    const cm = pushGroupProperty(g, true)
    if (cm) {
      batchCmd.addSubCommand(cm)
    }

    svgCanvas.addCommandToHistory(batchCmd)
  } else {
    console.warn('Unexpected element to ungroup:', elem)
  }
}

/**
* Unwraps all the elements in a selected group (`g`) element. This requires
* significant recalculations to apply group's transforms, etc. to its children.
* @function module:selected-elem.SvgCanvas#ungroupSelectedElement
* @returns {void}
*/
export const ungroupSelectedElement = function () {
  const selectedElements = svgCanvas.getSelectedElements()
  const dataStorage = svgCanvas.getDataStorage()
  let g = selectedElements[0]
  if (!g) {
    return
  }
  if (dataStorage.has(g, 'gsvg') || dataStorage.has(g, 'symbol')) {
    // Is svg, so actually convert to group
    convertToGroup(g)
    return
  }
  if (g.tagName === 'use') {
    // Somehow doesn't have data set, so retrieve
    const symbol = getElement(getHref(g).substr(1))
    dataStorage.put(g, 'symbol', symbol)
    dataStorage.put(g, 'ref', symbol)
    convertToGroup(g)
    return
  }
  const parentsA = getParents(g.parentNode, 'a')
  if (parentsA?.length) {
    g = parentsA[0]
  }

  // Look for parent "a"
  if (g.tagName === 'g' || g.tagName === 'a') {
    const batchCmd = new BatchCommand('Ungroup Elements')
    const cmd = pushGroupProperty(g, true)
    if (cmd) { batchCmd.addSubCommand(cmd) }

    const parent = g.parentNode
    const anchor = g.nextSibling
    const children = new Array(g.childNodes.length)

    let i = 0
    while (g.firstChild) {
      const elem = g.firstChild
      const oldNextSibling = elem.nextSibling
      const oldParent = elem.parentNode

      // Remove child title elements
      if (elem.tagName === 'title') {
        const { nextSibling } = elem
        batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, oldParent))
        elem.remove()
        continue
      }

      children[i++] = parent.insertBefore(elem, anchor)
      batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent))
    }

    // remove the group from the selection
    svgCanvas.clearSelection()

    // delete the group element (but make undo-able)
    const gNextSibling = g.nextSibling
    g.remove()
    batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent))

    if (!batchCmd.isEmpty()) { svgCanvas.addCommandToHistory(batchCmd) }

    // update selection
    svgCanvas.addToSelection(children)
  }
}
/**
* Updates the editor canvas width/height/position after a zoom has occurred.
* @function module:svgcanvas.SvgCanvas#updateCanvas
* @param {Float} w - Float with the new width
* @param {Float} h - Float with the new height
* @fires module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
* @returns {module:svgcanvas.CanvasInfo}
*/
export const updateCanvas = function (w, h) {
  svgCanvas.getSvgRoot().setAttribute('width', w)
  svgCanvas.getSvgRoot().setAttribute('height', h)
  const zoom = svgCanvas.getZoom()
  const bg = document.getElementById('canvasBackground')
  const oldX = Number(svgCanvas.getSvgContent().getAttribute('x'))
  const oldY = Number(svgCanvas.getSvgContent().getAttribute('y'))
  const x = ((w - this.contentW * zoom) / 2)
  const y = ((h - this.contentH * zoom) / 2)

  assignAttributes(svgCanvas.getSvgContent(), {
    width: this.contentW * zoom,
    height: this.contentH * zoom,
    x,
    y,
    viewBox: '0 0 ' + this.contentW + ' ' + this.contentH
  })

  assignAttributes(bg, {
    width: svgCanvas.getSvgContent().getAttribute('width'),
    height: svgCanvas.getSvgContent().getAttribute('height'),
    x,
    y
  })

  const bgImg = getElement('background_image')
  if (bgImg) {
    assignAttributes(bgImg, {
      width: '100%',
      height: '100%'
    })
  }

  svgCanvas.selectorManager.selectorParentGroup.setAttribute('transform', 'translate(' + x + ',' + y + ')')

  /**
* Invoked upon updates to the canvas.
* @event module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
* @type {PlainObject}
* @property {Integer} new_x
* @property {Integer} new_y
* @property {string} old_x (Of Integer)
* @property {string} old_y (Of Integer)
* @property {Integer} d_x
* @property {Integer} d_y
*/
  svgCanvas.runExtensions(
    'canvasUpdated',
    /**
 * @type {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated}
 */
    { new_x: x, new_y: y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY }
  )
  return { x, y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY }
}
/**
* Select the next/previous element within the current layer.
* @function module:svgcanvas.SvgCanvas#cycleElement
* @param {boolean} next - true = next and false = previous element
* @fires module:svgcanvas.SvgCanvas#event:selected
* @returns {void}
*/
export const cycleElement = function (next) {
  const selectedElements = svgCanvas.getSelectedElements()
  const currentGroup = svgCanvas.getCurrentGroup()
  let num
  const curElem = selectedElements[0]
  let elem = false
  const allElems = getVisibleElements(currentGroup || svgCanvas.getCurrentDrawing().getCurrentLayer())
  if (!allElems.length) { return }
  if (isNullish(curElem)) {
    num = next ? allElems.length - 1 : 0
    elem = allElems[num]
  } else {
    let i = allElems.length
    while (i--) {
      if (allElems[i] === curElem) {
        num = next ? i - 1 : i + 1
        if (num >= allElems.length) {
          num = 0
        } else if (num < 0) {
          num = allElems.length - 1
        }
        elem = allElems[num]
        break
      }
    }
  }
  svgCanvas.selectOnly([elem], true)
  svgCanvas.call('selected', selectedElements)
}