mouseUpEvent = (evt) => {
  evt.preventDefault()
  moveSelectionThresholdReached = false
  if (evt.button === 2) { return }
  if (!svgCanvas.getStarted()) { return }

  svgCanvas.textActions.init()

  const selectedElements = svgCanvas.getSelectedElements()
  const zoom = svgCanvas.getZoom()

  const tempJustSelected = svgCanvas.getJustSelected()
  svgCanvas.setJustSelected(null)

  const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
  const mouseX = pt.x * zoom
  const mouseY = pt.y * zoom
  const x = mouseX / zoom
  const y = mouseY / zoom

  let element = getElement(svgCanvas.getId())
  let keep = false

  const realX = x
  const realY = y

  // TODO: Make true when in multi-unit mode
  const useUnit = false // (svgCanvas.getCurConfig().baseUnit !== 'px');
  svgCanvas.setStarted(false)
  let t
  switch (svgCanvas.getCurrentMode()) {
    // intentionally fall-through to select here
    case 'resize':
    case 'multiselect':
      if (svgCanvas.getRubberBox()) {
        svgCanvas.getRubberBox().setAttribute('display', 'none')
        svgCanvas.setCurBBoxes([])
      }
      svgCanvas.setCurrentMode('select')
    // Fallthrough
    case 'select':
      if (selectedElements[0]) {
        // if we only have one selected element
        if (!selectedElements[1]) {
          // set our current stroke/fill properties to the element's
          const selected = selectedElements[0]
          switch (selected.tagName) {
            case 'g':
            case 'use':
            case 'image':
            case 'foreignObject':
              break
            case 'text':
              svgCanvas.setCurText('font_size', selected.getAttribute('font-size'))
              svgCanvas.setCurText('font_family', selected.getAttribute('font-family'))
            // fallthrough
            default:
              svgCanvas.setCurProperties('fill', selected.getAttribute('fill'))
              svgCanvas.setCurProperties('fill_opacity', selected.getAttribute('fill-opacity'))
              svgCanvas.setCurProperties('stroke', selected.getAttribute('stroke'))
              svgCanvas.setCurProperties('stroke_opacity', selected.getAttribute('stroke-opacity'))
              svgCanvas.setCurProperties('stroke_width', selected.getAttribute('stroke-width'))
              svgCanvas.setCurProperties('stroke_dasharray', selected.getAttribute('stroke-dasharray'))
              svgCanvas.setCurProperties('stroke_linejoin', selected.getAttribute('stroke-linejoin'))
              svgCanvas.setCurProperties('stroke_linecap', selected.getAttribute('stroke-linecap'))
          }
          svgCanvas.selectorManager.requestSelector(selected).showGrips(true)
        }
        // always recalculate dimensions to strip off stray identity transforms
        svgCanvas.recalculateAllSelectedDimensions()
        // if it was being dragged/resized
        if (realX !== svgCanvas.getRStartX() || realY !== svgCanvas.getRStartY()) {
          const len = selectedElements.length
          for (let i = 0; i < len; ++i) {
            if (!selectedElements[i]) { break }
            svgCanvas.selectorManager.requestSelector(selectedElements[i]).resize()
          }
          // no change in position/size, so maybe we should move to pathedit
        } else {
          t = evt.target
          if (selectedElements[0].nodeName === 'path' && !selectedElements[1]) {
            svgCanvas.pathActions.select(selectedElements[0])
            // if it was a path
            // else, if it was selected and this is a shift-click, remove it from selection
          } else if (evt.shiftKey && tempJustSelected !== t) {
            svgCanvas.removeFromSelection([t])
          }
        } // no change in mouse position

        // Remove non-scaling stroke
        const elem = selectedElements[0]
        if (elem) {
          elem.removeAttribute('style')

          // we don't remove the style elements for contents of foreignObjects
          // because that is a valid way to style them
          if (elem.localName === 'foreignObject') {
            walkTree(elem, (el) => {
              el.style.removeProperty('pointer-events')
            })
          } else {
            walkTree(elem, (el) => {
              el.removeAttribute('style')
            })
          }
        }
      }
      return
    case 'zoom': {
      svgCanvas.getRubberBox()?.setAttribute('display', 'none')
      const factor = evt.shiftKey ? 0.5 : 2
      svgCanvas.call('zoomed', {
        x: Math.min(svgCanvas.getRStartX(), realX),
        y: Math.min(svgCanvas.getRStartY(), realY),
        width: Math.abs(realX - svgCanvas.getRStartX()),
        height: Math.abs(realY - svgCanvas.getRStartY()),
        factor
      })
      return
    } case 'fhpath': {
      // Check that the path contains at least 2 points; a degenerate one-point path
      // causes problems.
      // Webkit ignores how we set the points attribute with commas and uses space
      // to separate all coordinates, see https://bugs.webkit.org/show_bug.cgi?id=29870
      svgCanvas.setSumDistance(0)
      svgCanvas.setControllPoint2('x', 0)
      svgCanvas.setControllPoint2('y', 0)
      svgCanvas.setControllPoint1('x', 0)
      svgCanvas.setControllPoint1('y', 0)
      svgCanvas.setStart({ x: 0, y: 0 })
      svgCanvas.setEnd('x', 0)
      svgCanvas.setEnd('y', 0)
      const coords = element.getAttribute('points')
      const commaIndex = coords.indexOf(',')
      keep = commaIndex >= 0 ? coords.includes(',', commaIndex + 1) : coords.includes(' ', coords.indexOf(' ') + 1)
      if (keep) {
        element = svgCanvas.pathActions.smoothPolylineIntoPath(element)
      }
      break
    } case 'line': {
      const x1 = element.getAttribute('x1')
      const y1 = element.getAttribute('y1')
      const x2 = element.getAttribute('x2')
      const y2 = element.getAttribute('y2')
      keep = (x1 !== x2 || y1 !== y2)
    }
      break
    case 'foreignObject':
    case 'square':
    case 'rect':
    case 'image': {
      const width = element.getAttribute('width')
      const height = element.getAttribute('height')
      // Image should be kept regardless of size (use inherit dimensions later)
      const widthNum = Number(width)
      const heightNum = Number(height)
      keep = widthNum >= 1 || heightNum >= 1 || svgCanvas.getCurrentMode() === 'image'
    }
      break
    case 'circle':
      keep = (element.getAttribute('r') !== '0')
      break
    case 'ellipse': {
      const rx = Number(element.getAttribute('rx'))
      const ry = Number(element.getAttribute('ry'))
      keep = (rx || ry)
    }
      break
    case 'fhellipse':
      if ((svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) > 0 &&
        (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) > 0) {
        element = svgCanvas.addSVGElementsFromJson({
          element: 'ellipse',
          curStyles: true,
          attr: {
            cx: (svgCanvas.getFreehand('minx') + svgCanvas.getFreehand('maxx')) / 2,
            cy: (svgCanvas.getFreehand('miny') + svgCanvas.getFreehand('maxy')) / 2,
            rx: (svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) / 2,
            ry: (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) / 2,
            id: svgCanvas.getId()
          }
        })
        svgCanvas.call('changed', [element])
        keep = true
      }
      break
    case 'fhrect':
      if ((svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) > 0 &&
        (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) > 0) {
        element = svgCanvas.addSVGElementsFromJson({
          element: 'rect',
          curStyles: true,
          attr: {
            x: svgCanvas.getFreehand('minx'),
            y: svgCanvas.getFreehand('miny'),
            width: (svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')),
            height: (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')),
            id: svgCanvas.getId()
          }
        })
        svgCanvas.call('changed', [element])
        keep = true
      }
      break
    case 'text':
      keep = true
      svgCanvas.selectOnly([element])
      svgCanvas.textActions.start(element)
      break
    case 'path': {
      // set element to null here so that it is not removed nor finalized
      element = null
      // continue to be set to true so that mouseMove happens
      svgCanvas.setStarted(true)

      const res = svgCanvas.pathActions.mouseUp(evt, element, mouseX, mouseY);
      ({ element } = res);
      ({ keep } = res)
      break
    } case 'pathedit':
      keep = true
      element = null
      svgCanvas.pathActions.mouseUp(evt)
      break
    case 'textedit':
      keep = false
      element = null
      svgCanvas.textActions.mouseUp(evt, mouseX, mouseY)
      break
    case 'rotate': {
      keep = true
      element = null
      svgCanvas.setCurrentMode('select')
      const batchCmd = svgCanvas.undoMgr.finishUndoableChange()
      if (!batchCmd.isEmpty()) {
        svgCanvas.addCommandToHistory(batchCmd)
      }
      // perform recalculation to weed out any stray identity transforms that might get stuck
      svgCanvas.recalculateAllSelectedDimensions()
      svgCanvas.call('changed', selectedElements)
      break
    } default:
      // This could occur in an extension
      break
  }

  /**
* The main (left) mouse button is released (anywhere).
* @event module:svgcanvas.SvgCanvas#event:ext_mouseUp
* @type {PlainObject}
* @property {MouseEvent} event The event object
* @property {Float} mouse_x x coordinate on canvas
* @property {Float} mouse_y y coordinate on canvas
*/
  const extResult = svgCanvas.runExtensions('mouseUp', {
    event: evt,
    mouse_x: mouseX,
    mouse_y: mouseY
  }, true)

  extResult.forEach((r) => {
    if (r) {
      keep = r.keep || keep;
      ({ element } = r)
      svgCanvas.setStarted(r.started || svgCanvas.getStarted())
    }
  })

  if (!keep && element) {
    svgCanvas.getCurrentDrawing().releaseId(svgCanvas.getId())
    element.remove()
    element = null

    t = evt.target

    // if this element is in a group, go up until we reach the top-level group
    // just below the layer groups
    // TODO: once we implement links, we also would have to check for <a> elements
    while (t?.parentNode?.parentNode?.tagName === 'g') {
      t = t.parentNode
    }
    // if we are not in the middle of creating a path, and we've clicked on some shape,
    // then go to Select mode.
    // WebKit returns <div> when the canvas is clicked, Firefox/Opera return <svg>
    if ((svgCanvas.getCurrentMode() !== 'path' || !svgCanvas.getDrawnPath()) &&
      t &&
      t.parentNode?.id !== 'selectorParentGroup' &&
      t.id !== 'svgcanvas' && t.id !== 'svgroot'
    ) {
      // switch into "select" mode if we've clicked on an element
      svgCanvas.setMode('select')
      svgCanvas.selectOnly([t], true)
    }
  } else if (element) {
    /**
* @name module:svgcanvas.SvgCanvas#addedNew
* @type {boolean}
*/
    svgCanvas.addedNew = true

    if (useUnit) { convertAttrs(element) }

    let aniDur = 0.2
    let cAni
    const curShape = svgCanvas.getStyle()
    const opacAni = svgCanvas.getOpacAni()
    if (opacAni.beginElement && Number.parseFloat(element.getAttribute('opacity')) !== curShape.opacity) {
      cAni = opacAni.cloneNode(true)
      cAni.setAttribute('to', curShape.opacity)
      cAni.setAttribute('dur', aniDur)
      element.appendChild(cAni)
      try {
        // Fails in FF4 on foreignObject
        cAni.beginElement()
      } catch (e) { /* empty fn */ }
    } else {
      aniDur = 0
    }

    // Ideally this would be done on the endEvent of the animation,
    // but that doesn't seem to be supported in Webkit
    setTimeout(() => {
      if (cAni) { cAni.remove() }
      element.setAttribute('opacity', curShape.opacity)
      element.setAttribute('style', 'pointer-events:inherit')
      cleanupElement(element)
      if (svgCanvas.getCurrentMode() === 'path') {
        svgCanvas.pathActions.toEditMode(element)
      } else if (svgCanvas.getCurConfig().selectNew) {
        const modes = ['circle', 'ellipse', 'square', 'rect', 'fhpath', 'line', 'fhellipse', 'fhrect', 'star', 'polygon', 'shapelib']
        if (modes.indexOf(svgCanvas.getCurrentMode()) !== -1 && !evt.altKey) {
          svgCanvas.setMode('select')
        }
        svgCanvas.selectOnly([element], true)
      }
      // we create the insert command that is stored on the stack
      // undo means to call cmd.unapply(), redo means to call cmd.apply()
      svgCanvas.addCommandToHistory(new InsertElementCommand(element))
      svgCanvas.call('changed', [element])
    }, aniDur * 1000)
  }
  svgCanvas.setStartTransform(null)
}
@param {MouseEvent} evt
@fires module:svgcanvas.SvgCanvas#event:zoomed
@fires module:svgcanvas.SvgCanvas#event:changed
@fires module:svgcanvas.SvgCanvas#event:ext_mouseUp
@returns {void}