Source: svgcanvas/elem-get-set.js

/**
 * @module elem-get-set get and set methods.
 * @license MIT
 * @copyright 2011 Jeff Schiller
 */

import { jGraduate } from '../editor/components/jgraduate/jQuery.jGraduate.js'
import { NS } from './namespaces.js'
import {
  getVisibleElements, getStrokedBBoxDefaultVisible, findDefs,
  walkTree, getHref, setHref, getElement
} from './utilities.js'
import {
  convertToNum
} from '../common/units.js'
import { getParents } from '../editor/components/jgraduate/Util.js'

let svgCanvas = null

/**
* @function module:elem-get-set.init
* @param {module:elem-get-set.elemContext} elemContext
* @returns {void}
*/
export const init = (canvas) => {
  svgCanvas = canvas
  svgCanvas.getBold = getBoldMethod // Check whether selected element is bold or not.
  svgCanvas.setBold = setBoldMethod // Make the selected element bold or normal.
  svgCanvas.getItalic = getItalicMethod // Check whether selected element is in italics or not.
  svgCanvas.setItalic = setItalicMethod // Make the selected element italic or normal.
  svgCanvas.hasTextDecoration = hasTextDecorationMethod // Check whether the selected element has the given text decoration or not.
  svgCanvas.addTextDecoration = addTextDecorationMethod // Adds the given value to the text decoration
  svgCanvas.removeTextDecoration = removeTextDecorationMethod // Removes the given value from the text decoration
  svgCanvas.setTextAnchor = setTextAnchorMethod // Set the new text anchor.
  svgCanvas.setLetterSpacing = setLetterSpacingMethod // Set the new letter spacing.
  svgCanvas.setWordSpacing = setWordSpacingMethod // Set the new word spacing.
  svgCanvas.setTextLength = setTextLengthMethod // Set the new text length.
  svgCanvas.setLengthAdjust = setLengthAdjustMethod // Set the new length adjust.
  svgCanvas.getFontFamily = getFontFamilyMethod // The current font family
  svgCanvas.setFontFamily = setFontFamilyMethod // Set the new font family.
  svgCanvas.setFontColor = setFontColorMethod // Set the new font color.
  svgCanvas.getFontColor = getFontColorMethod // The current font color
  svgCanvas.getFontSize = getFontSizeMethod // The current font size
  svgCanvas.setFontSize = setFontSizeMethod // Applies the given font size to the selected element.
  svgCanvas.getText = getTextMethod // current text (`textContent`) of the selected element
  svgCanvas.setTextContent = setTextContentMethod // Updates the text element with the given string.
  svgCanvas.setImageURL = setImageURLMethod // Sets the new image URL for the selected image element
  svgCanvas.setLinkURL = setLinkURLMethod // Sets the new link URL for the selected anchor element.
  svgCanvas.setRectRadius = setRectRadiusMethod // Sets the `rx` and `ry` values to the selected `rect` element
  svgCanvas.makeHyperlink = makeHyperlinkMethod // Wraps the selected element(s) in an anchor element or converts group to one.
  svgCanvas.removeHyperlink = removeHyperlinkMethod
  svgCanvas.setSegType = setSegTypeMethod // Sets the new segment type to the selected segment(s).
  svgCanvas.setStrokeWidth = setStrokeWidthMethod // Sets the stroke width for the current selected elements.
  svgCanvas.getResolution = getResolutionMethod // The current dimensions and zoom level in an object
  svgCanvas.getTitle = getTitleMethod // the current group/SVG's title contents or `undefined` if no element
  svgCanvas.setGroupTitle = setGroupTitleMethod // Sets the group/SVG's title content.
  svgCanvas.setStrokeAttr = setStrokeAttrMethod // Set the given stroke-related attribute the given value for selected elements.
  svgCanvas.setBackground = setBackgroundMethod // Set the background of the editor (NOT the actual document).
  svgCanvas.setDocumentTitle = setDocumentTitleMethod // Adds/updates a title element for the document with the given name.
  svgCanvas.getEditorNS = getEditorNSMethod // Returns the editor's namespace URL, optionally adding it to the root element.
  svgCanvas.setResolution = setResolutionMethod // Changes the document's dimensions to the given size.
  svgCanvas.setBBoxZoom = setBBoxZoomMethod // Sets the zoom level on the canvas-side based on the given value.
  svgCanvas.setCurrentZoom = setZoomMethod // Sets the zoom to the given level.
  svgCanvas.setColor = setColorMethod // Change the current stroke/fill color/gradien
  svgCanvas.setGradient = setGradientMethod // Apply the current gradient to selected element's fill or stroke.
  svgCanvas.setPaint = setPaintMethod // Set a color/gradient to a fill/stroke.
}

/**
* @function module:elem-get-set.SvgCanvas#getResolution
* @returns {DimensionsAndZoom} The current dimensions and zoom level in an object
*/
const getResolutionMethod = () => {
  const zoom = svgCanvas.getZoom()
  const w = svgCanvas.getSvgContent().getAttribute('width') / zoom
  const h = svgCanvas.getSvgContent().getAttribute('height') / zoom

  return {
    w,
    h,
    zoom
  }
}

/**
* @function module:elem-get-set.SvgCanvas#getTitle
* @param {Element} [elem]
* @returns {string|void} the current group/SVG's title contents or
* `undefined` if no element is passed nd there are no selected elements.
*/
const getTitleMethod = (elem) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const dataStorage = svgCanvas.getDataStorage()
  elem = elem || selectedElements[0]
  if (!elem) { return undefined }
  if (dataStorage.has(elem, 'gsvg')) {
    elem = dataStorage.get(elem, 'gsvg')
  } else if (dataStorage.has(elem, 'symbol')) {
    elem = dataStorage.get(elem, 'symbol')
  }
  const childs = elem.childNodes
  for (const child of childs) {
    if (child.nodeName === 'title') {
      return child.textContent
    }
  }
  return ''
}

/**
* Sets the group/SVG's title content.
* @function module:elem-get-set.SvgCanvas#setGroupTitle
* @param {string} val
* @todo Combine this with `setDocumentTitle`
* @returns {void}
*/
const setGroupTitleMethod = (val) => {
  const {
    InsertElementCommand, RemoveElementCommand,
    ChangeElementCommand, BatchCommand
  } = svgCanvas.history
  const selectedElements = svgCanvas.getSelectedElements()
  const dataStorage = svgCanvas.getDataStorage()
  let elem = selectedElements[0]
  if (dataStorage.has(elem, 'gsvg')) {
    elem = dataStorage.get(elem, 'gsvg')
  }

  const ts = elem.querySelectorAll('title')

  const batchCmd = new BatchCommand('Set Label')

  let title
  if (val.length === 0) {
    // Remove title element
    const tsNextSibling = ts.nextSibling
    batchCmd.addSubCommand(new RemoveElementCommand(ts[0], tsNextSibling, elem))
    ts.remove()
  } else if (ts.length) {
    // Change title contents
    title = ts[0]
    batchCmd.addSubCommand(new ChangeElementCommand(title, { '#text': title.textContent }))
    title.textContent = val
  } else {
    // Add title element
    title = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'title')
    title.textContent = val
    elem.insertBefore(title, elem.firstChild)
    batchCmd.addSubCommand(new InsertElementCommand(title))
  }

  svgCanvas.addCommandToHistory(batchCmd)
}

/**
* Adds/updates a title element for the document with the given name.
* This is an undoable action.
* @function module:elem-get-set.SvgCanvas#setDocumentTitle
* @param {string} newTitle - String with the new title
* @returns {void}
*/
const setDocumentTitleMethod = (newTitle) => {
  const { ChangeElementCommand, BatchCommand } = svgCanvas.history
  const childs = svgCanvas.getSvgContent().childNodes
  let docTitle = false; let oldTitle = ''

  const batchCmd = new BatchCommand('Change Image Title')

  for (const child of childs) {
    if (child.nodeName === 'title') {
      docTitle = child
      oldTitle = docTitle.textContent
      break
    }
  }
  if (!docTitle) {
    docTitle = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'title')
    svgCanvas.getSvgContent().insertBefore(docTitle, svgCanvas.getSvgContent().firstChild)
    // svgContent.firstChild.before(docTitle); // Ok to replace above with this?
  }

  if (newTitle.length) {
    docTitle.textContent = newTitle
  } else {
    // No title given, so element is not necessary
    docTitle.remove()
  }
  batchCmd.addSubCommand(new ChangeElementCommand(docTitle, { '#text': oldTitle }))
  svgCanvas.addCommandToHistory(batchCmd)
}

/**
* Changes the document's dimensions to the given size.
* @function module:elem-get-set.SvgCanvas#setResolution
* @param {Float|"fit"} x - Number with the width of the new dimensions in user units.
* Can also be the string "fit" to indicate "fit to content".
* @param {Float} y - Number with the height of the new dimensions in user units.
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {boolean} Indicates if resolution change was successful.
* It will fail on "fit to content" option with no content to fit to.
*/
const setResolutionMethod = (x, y) => {
  const { ChangeElementCommand, BatchCommand } = svgCanvas.history
  const zoom = svgCanvas.getZoom()
  const res = svgCanvas.getResolution()
  const { w, h } = res
  let batchCmd

  if (x === 'fit') {
    // Get bounding box
    const bbox = getStrokedBBoxDefaultVisible()

    if (bbox) {
      batchCmd = new BatchCommand('Fit Canvas to Content')
      const visEls = getVisibleElements()
      svgCanvas.addToSelection(visEls)
      const dx = []; const dy = []
      visEls.forEach((_item, _i) => {
        dx.push(bbox.x * -1)
        dy.push(bbox.y * -1)
      })

      const cmd = svgCanvas.moveSelectedElements(dx, dy, true)
      batchCmd.addSubCommand(cmd)
      svgCanvas.clearSelection()

      x = Math.round(bbox.width)
      y = Math.round(bbox.height)
    } else {
      return false
    }
  }
  if (x !== w || y !== h) {
    if (!batchCmd) {
      batchCmd = new BatchCommand('Change Image Dimensions')
    }

    x = convertToNum('width', x)
    y = convertToNum('height', y)

    svgCanvas.getSvgContent().setAttribute('width', x)
    svgCanvas.getSvgContent().setAttribute('height', y)

    svgCanvas.contentW = x
    svgCanvas.contentH = y
    batchCmd.addSubCommand(new ChangeElementCommand(svgCanvas.getSvgContent(), { width: w, height: h }))

    svgCanvas.getSvgContent().setAttribute('viewBox', [0, 0, x / zoom, y / zoom].join(' '))
    batchCmd.addSubCommand(new ChangeElementCommand(svgCanvas.getSvgContent(), { viewBox: ['0 0', w, h].join(' ') }))

    svgCanvas.addCommandToHistory(batchCmd)
    svgCanvas.call('changed', [svgCanvas.getSvgContent()])
  }
  return true
}

/**
* Returns the editor's namespace URL, optionally adding it to the root element.
* @function module:elem-get-set.SvgCanvas#getEditorNS
* @param {boolean} [add] - Indicates whether or not to add the namespace value
* @returns {string} The editor's namespace URL
*/
const getEditorNSMethod = (add) => {
  if (add) {
    svgCanvas.getSvgContent().setAttribute('xmlns:se', NS.SE)
  }
  return NS.SE
}

/**
 * @typedef {PlainObject} module:elem-get-set.ZoomAndBBox
 * @property {Float} zoom
 * @property {module:utilities.BBoxObject} bbox
 */
/**
* Sets the zoom level on the canvas-side based on the given value.
* @function module:elem-get-set.SvgCanvas#setBBoxZoom
* @param {"selection"|"canvas"|"content"|"layer"|module:SVGEditor.BBoxObjectWithFactor} val - Bounding box object to zoom to or string indicating zoom option. Note: the object value type is defined in `svg-editor.js`
* @param {Integer} editorW - The editor's workarea box's width
* @param {Integer} editorH - The editor's workarea box's height
* @returns {module:elem-get-set.ZoomAndBBox|void}
*/
const setBBoxZoomMethod = (val, editorW, editorH) => {
  const zoom = svgCanvas.getZoom()
  const selectedElements = svgCanvas.getSelectedElements()
  let spacer = 0.85
  let bb
  const calcZoom = (bb) => {
    if (!bb) { return false }
    const wZoom = Math.round((editorW / bb.width) * 100 * spacer) / 100
    const hZoom = Math.round((editorH / bb.height) * 100 * spacer) / 100
    const zoom = Math.min(wZoom, hZoom)
    svgCanvas.setZoom(zoom)
    return { zoom, bbox: bb }
  }

  if (typeof val === 'object') {
    bb = val
    if (bb.width === 0 || bb.height === 0) {
      const newzoom = bb.zoom ? bb.zoom : zoom * bb.factor
      svgCanvas.setZoom(newzoom)
      return { zoom: zoom, bbox: bb }
    }
    return calcZoom(bb)
  }

  switch (val) {
    case 'selection': {
      if (!selectedElements[0]) { return undefined }
      const selectedElems = selectedElements.map((n, _) => {
        if (n) {
          return n
        }
        return undefined
      })
      bb = getStrokedBBoxDefaultVisible(selectedElems)
      break
    } case 'canvas': {
      const res = svgCanvas.getResolution()
      spacer = 0.95
      bb = { width: res.w, height: res.h, x: 0, y: 0 }
      break
    } case 'content':
      bb = getStrokedBBoxDefaultVisible()
      break
    case 'layer':
      bb = getStrokedBBoxDefaultVisible(getVisibleElements(svgCanvas.getCurrentDrawing().getCurrentLayer()))
      break
    default:
      return undefined
  }
  return calcZoom(bb)
}

/**
* Sets the zoom to the given level.
* @function module:elem-get-set.SvgCanvas#setZoom
* @param {Float} zoomLevel - Float indicating the zoom level to change to
* @fires module:elem-get-set.SvgCanvas#event:ext_zoomChanged
* @returns {void}
*/
const setZoomMethod = (zoomLevel) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const res = svgCanvas.getResolution()
  svgCanvas.getSvgContent().setAttribute('viewBox', '0 0 ' + res.w / zoomLevel + ' ' + res.h / zoomLevel)
  svgCanvas.setZoom(zoomLevel)
  selectedElements.forEach((elem) => {
    if (!elem) { return }
    svgCanvas.selectorManager.requestSelector(elem).resize()
  })
  svgCanvas.pathActions.zoomChange()
  svgCanvas.runExtensions('zoomChanged', zoomLevel)
}

/**
* Change the current stroke/fill color/gradient value.
* @function module:elem-get-set.SvgCanvas#setColor
* @param {string} type - String indicating fill or stroke
* @param {string} val - The value to set the stroke attribute to
* @param {boolean} preventUndo - Boolean indicating whether or not svgCanvas should be an undoable option
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
const setColorMethod = (type, val, preventUndo) => {
  const selectedElements = svgCanvas.getSelectedElements()
  svgCanvas.setCurShape(type, val)
  svgCanvas.setCurProperties(type + '_paint', { type: 'solidColor' })
  const elems = []
  /**
*
* @param {Element} e
* @returns {void}
*/
  const addNonG = (e) => {
    if (e.nodeName !== 'g') {
      elems.push(e)
    }
  }
  let i = selectedElements.length
  while (i--) {
    const elem = selectedElements[i]
    if (elem) {
      if (elem.tagName === 'g') {
        walkTree(elem, addNonG)
      } else if (type === 'fill') {
        if (elem.tagName !== 'polyline' && elem.tagName !== 'line') {
          elems.push(elem)
        }
      } else {
        elems.push(elem)
      }
    }
  }
  if (elems.length > 0) {
    if (!preventUndo) {
      svgCanvas.changeSelectedAttribute(type, val, elems)
      svgCanvas.call('changed', elems)
    } else {
      svgCanvas.changeSelectedAttributeNoUndo(type, val, elems)
    }
  }
}

/**
* Apply the current gradient to selected element's fill or stroke.
* @function module:elem-get-set.SvgCanvas#setGradient
* @param {"fill"|"stroke"} type - String indicating "fill" or "stroke" to apply to an element
* @returns {void}
*/
const setGradientMethod = (type) => {
  if (!svgCanvas.getCurProperties(type + '_paint') ||
    svgCanvas.getCurProperties(type + '_paint').type === 'solidColor') { return }
  const canvas = svgCanvas
  let grad = canvas[type + 'Grad']
  // find out if there is a duplicate gradient already in the defs
  const duplicateGrad = findDuplicateGradient(grad)
  const defs = findDefs()
  // no duplicate found, so import gradient into defs
  if (!duplicateGrad) {
    // const origGrad = grad;
    grad = svgCanvas.getDOMDocument().importNode(grad, true)
    defs.append(grad)
    // get next id and set it on the grad
    grad.id = svgCanvas.getNextId()
  } else { // use existing gradient
    grad = duplicateGrad
  }
  svgCanvas.setColor(type, 'url(#' + grad.id + ')')
}

/**
* Check if exact gradient already exists.
* @function module:svgcanvas~findDuplicateGradient
* @param {SVGGradientElement} grad - The gradient DOM element to compare to others
* @returns {SVGGradientElement} The existing gradient if found, `null` if not
*/
const findDuplicateGradient = (grad) => {
  const defs = findDefs()
  const existingGrads = defs.querySelectorAll('linearGradient, radialGradient')
  let i = existingGrads.length
  const radAttrs = ['r', 'cx', 'cy', 'fx', 'fy']
  while (i--) {
    const og = existingGrads[i]
    if (grad.tagName === 'linearGradient') {
      if (grad.getAttribute('x1') !== og.getAttribute('x1') ||
        grad.getAttribute('y1') !== og.getAttribute('y1') ||
        grad.getAttribute('x2') !== og.getAttribute('x2') ||
        grad.getAttribute('y2') !== og.getAttribute('y2')
      ) {
        continue
      }
    } else {
      const gradAttrs = {
        r: Number(grad.getAttribute('r')),
        cx: Number(grad.getAttribute('cx')),
        cy: Number(grad.getAttribute('cy')),
        fx: Number(grad.getAttribute('fx')),
        fy: Number(grad.getAttribute('fy'))
      }
      const ogAttrs = {
        r: Number(og.getAttribute('r')),
        cx: Number(og.getAttribute('cx')),
        cy: Number(og.getAttribute('cy')),
        fx: Number(og.getAttribute('fx')),
        fy: Number(og.getAttribute('fy'))
      }

      let diff = false
      radAttrs.forEach((attr) => {
        if (gradAttrs[attr] !== ogAttrs[attr]) { diff = true }
      })

      if (diff) { continue }
    }

    // else could be a duplicate, iterate through stops
    const stops = grad.getElementsByTagNameNS(NS.SVG, 'stop')
    const ostops = og.getElementsByTagNameNS(NS.SVG, 'stop')

    if (stops.length !== ostops.length) {
      continue
    }

    let j = stops.length
    while (j--) {
      const stop = stops[j]
      const ostop = ostops[j]

      if (stop.getAttribute('offset') !== ostop.getAttribute('offset') ||
        stop.getAttribute('stop-opacity') !== ostop.getAttribute('stop-opacity') ||
        stop.getAttribute('stop-color') !== ostop.getAttribute('stop-color')) {
        break
      }
    }

    if (j === -1) {
      return og
    }
  } // for each gradient in defs

  return null
}

/**
* Set a color/gradient to a fill/stroke.
* @function module:elem-get-set.SvgCanvas#setPaint
* @param {"fill"|"stroke"} type - String with "fill" or "stroke"
* @param {module:jGraduate.jGraduatePaintOptions} paint - The jGraduate paint object to apply
* @returns {void}
*/
const setPaintMethod = (type, paint) => {
  // make a copy
  const p = new jGraduate.Paint(paint)
  svgCanvas.setPaintOpacity(type, p.alpha / 100, true)

  // now set the current paint object
  svgCanvas.setCurProperties(type + '_paint', p)
  switch (p.type) {
    case 'solidColor':
      svgCanvas.setColor(type, p.solidColor !== 'none' ? '#' + p.solidColor : 'none')
      break
    case 'linearGradient':
    case 'radialGradient':
      svgCanvas.setCanvas(type + 'Grad', p[p.type])
      svgCanvas.setGradient(type)
      break
  }
}
/**
* Sets the stroke width for the current selected elements.
* When attempting to set a line's width to 0, this changes it to 1 instead.
* @function module:elem-get-set.SvgCanvas#setStrokeWidth
* @param {Float} val - A Float indicating the new stroke width value
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
const setStrokeWidthMethod = (val) => {
  const selectedElements = svgCanvas.getSelectedElements()
  if (val === 0 && ['line', 'path'].includes(svgCanvas.getMode())) {
    svgCanvas.setStrokeWidth(1)
    return
  }
  svgCanvas.setCurProperties('stroke_width', val)

  const elems = []
  /**
*
* @param {Element} e
* @returns {void}
*/
  const addNonG = (e) => {
    if (e.nodeName !== 'g') {
      elems.push(e)
    }
  }
  let i = selectedElements.length
  while (i--) {
    const elem = selectedElements[i]
    if (elem) {
      if (elem.tagName === 'g') {
        walkTree(elem, addNonG)
      } else {
        elems.push(elem)
      }
    }
  }
  if (elems.length > 0) {
    svgCanvas.changeSelectedAttribute('stroke-width', val, elems)
    svgCanvas.call('changed', selectedElements)
  }
}

/**
* Set the given stroke-related attribute the given value for selected elements.
* @function module:elem-get-set.SvgCanvas#setStrokeAttr
* @param {string} attr - String with the attribute name
* @param {string|Float} val - String or number with the attribute value
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
const setStrokeAttrMethod = (attr, val) => {
  const selectedElements = svgCanvas.getSelectedElements()
  svgCanvas.setCurShape(attr.replace('-', '_'), val)
  const elems = []

  let i = selectedElements.length
  while (i--) {
    const elem = selectedElements[i]
    if (elem) {
      if (elem.tagName === 'g') {
        walkTree(elem, (e) => { if (e.nodeName !== 'g') { elems.push(e) } })
      } else {
        elems.push(elem)
      }
    }
  }
  if (elems.length > 0) {
    svgCanvas.changeSelectedAttribute(attr, val, elems)
    svgCanvas.call('changed', selectedElements)
  }
}
/**
* Check whether selected element is bold or not.
* @function module:svgcanvas.SvgCanvas#getBold
* @returns {boolean} Indicates whether or not element is bold
*/
const getBoldMethod = () => {
  const selectedElements = svgCanvas.getSelectedElements()
  // should only have one element selected
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' &&
    !selectedElements[1]) {
    return (selected.getAttribute('font-weight') === 'bold')
  }
  return false
}

/**
* Make the selected element bold or normal.
* @function module:svgcanvas.SvgCanvas#setBold
* @param {boolean} b - Indicates bold (`true`) or normal (`false`)
* @returns {void}
*/
const setBoldMethod = (b) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' &&
    !selectedElements[1]) {
    svgCanvas.changeSelectedAttribute('font-weight', b ? 'bold' : 'normal')
  }
  if (!selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
 * Check whether selected element has the given text decoration value or not.
 * @returns {boolean} Indicates whether or not element has the text decoration value
 */
const hasTextDecorationMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]

  if (selected?.tagName === 'text' && !selectedElements[1]) {
    const attribute = selected.getAttribute('text-decoration') || ''
    return attribute.includes(value)
  }

  return false
}

/**
 * Adds the given text decoration value
 * @param value The text decoration value
 * @returns {void}
 */
const addTextDecorationMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    const oldValue = selected.getAttribute('text-decoration') || ''
    svgCanvas.changeSelectedAttribute('text-decoration', (oldValue + ' ' + value).trim())
  }
  if (selectedElements.length > 0 && !selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
 * Removes the given text decoration value
 * @param value The text decoration value
 * @returns {void}
 */
const removeTextDecorationMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    const actualValues = selected.getAttribute('text-decoration') || ''
    svgCanvas.changeSelectedAttribute('text-decoration', actualValues.replace(value, '').trim())
  }
  if (selectedElements.length > 0 && !selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
* Check whether selected element is in italics or not.
* @function module:svgcanvas.SvgCanvas#getItalic
* @returns {boolean} Indicates whether or not element is italic
*/
const getItalicMethod = () => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    return (selected.getAttribute('font-style') === 'italic')
  }
  return false
}

/**
* Make the selected element italic or normal.
* @function module:svgcanvas.SvgCanvas#setItalic
* @param {boolean} i - Indicates italic (`true`) or normal (`false`)
* @returns {void}
*/
const setItalicMethod = (i) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    svgCanvas.changeSelectedAttribute('font-style', i ? 'italic' : 'normal')
  }
  if (!selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
 * @function module:svgcanvas.SvgCanvas#setTextAnchorMethod Set the new text anchor
 * @param {string} value - The text anchor value (start, middle or end)
 * @returns {void}
 */
const setTextAnchorMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    svgCanvas.changeSelectedAttribute('text-anchor', value)
  }
  if (selectedElements.length > 0 && !selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
 * @function module:svgcanvas.SvgCanvas#setLetterSpacingMethod Set the new letter spacing
 * @param {string} value - The letter spacing value
 * @returns {void}
 */
const setLetterSpacingMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    svgCanvas.changeSelectedAttribute('letter-spacing', value)
  }
  if (selectedElements.length > 0 && !selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
 * @function module:svgcanvas.SvgCanvas#setWordSpacingMethod Set the new word spacing
 * @param {string} value - The word spacing value
 * @returns {void}
 */
const setWordSpacingMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    svgCanvas.changeSelectedAttribute('word-spacing', value)
  }
  if (selectedElements.length > 0 && !selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
 * @function module:svgcanvas.SvgCanvas#setTextLengthMethod Set the new text length
 * @param {string} value - The text length value
 * @returns {void}
 */
const setTextLengthMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    svgCanvas.changeSelectedAttribute('textLength', value)
  }
  if (selectedElements.length > 0 && !selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
 * @function module:svgcanvas.SvgCanvas#setLengthAdjustMethod Set the new length adjust
 * @param {string} value - The length adjust value
 * @returns {void}
 */
const setLengthAdjustMethod = (value) => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'text' && !selectedElements[1]) {
    svgCanvas.changeSelectedAttribute('lengthAdjust', value)
  }
  if (selectedElements.length > 0 && !selectedElements[0].textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
* @function module:svgcanvas.SvgCanvas#getFontFamily
* @returns {string} The current font family
*/
const getFontFamilyMethod = () => {
  return svgCanvas.getCurText('font_family')
}

/**
* Set the new font family.
* @function module:svgcanvas.SvgCanvas#setFontFamily
* @param {string} val - String with the new font family
* @returns {void}
*/
const setFontFamilyMethod = (val) => {
  const selectedElements = svgCanvas.getSelectedElements()
  svgCanvas.setCurText('font_family', val)
  svgCanvas.changeSelectedAttribute('font-family', val)
  if (!selectedElements[0]?.textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
* Set the new font color.
* @function module:svgcanvas.SvgCanvas#setFontColor
* @param {string} val - String with the new font color
* @returns {void}
*/
const setFontColorMethod = (val) => {
  svgCanvas.setCurText('fill', val)
  svgCanvas.changeSelectedAttribute('fill', val)
}

/**
* @function module:svgcanvas.SvgCanvas#getFontColor
* @returns {string} The current font color
*/
const getFontColorMethod = () => {
  return svgCanvas.getCurText('fill')
}

/**
* @function module:svgcanvas.SvgCanvas#getFontSize
* @returns {Float} The current font size
*/
const getFontSizeMethod = () => {
  return svgCanvas.getCurText('font_size')
}

/**
* Applies the given font size to the selected element.
* @function module:svgcanvas.SvgCanvas#setFontSize
* @param {Float} val - Float with the new font size
* @returns {void}
*/
const setFontSizeMethod = (val) => {
  const selectedElements = svgCanvas.getSelectedElements()
  svgCanvas.setCurText('font_size', val)
  svgCanvas.changeSelectedAttribute('font-size', val)
  if (!selectedElements[0]?.textContent) {
    svgCanvas.textActions.setCursor()
  }
}

/**
* @function module:svgcanvas.SvgCanvas#getText
* @returns {string} The current text (`textContent`) of the selected element
*/
const getTextMethod = () => {
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  return (selected) ? selected.textContent : ''
}

/**
* Updates the text element with the given string.
* @function module:svgcanvas.SvgCanvas#setTextContent
* @param {string} val - String with the new text
* @returns {void}
*/
const setTextContentMethod = (val) => {
  svgCanvas.changeSelectedAttribute('#text', val)
  svgCanvas.textActions.init(val)
  svgCanvas.textActions.setCursor()
}

/**
* Sets the new image URL for the selected image element. Updates its size if
* a new URL is given.
* @function module:svgcanvas.SvgCanvas#setImageURL
* @param {string} val - String with the image URL/path
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
const setImageURLMethod = (val) => {
  const { ChangeElementCommand, BatchCommand } = svgCanvas.history
  const selectedElements = svgCanvas.getSelectedElements()
  const elem = selectedElements[0]
  if (!elem) { return }

  const attrs = {
    width: elem.getAttribute('width'),
    height: elem.getAttribute('height')
  }
  const setsize = (!attrs.width || !attrs.height)

  const curHref = getHref(elem)

  // Do nothing if no URL change or size change
  if (curHref === val && !setsize) {
    return
  }

  const batchCmd = new BatchCommand('Change Image URL')

  setHref(elem, val)
  batchCmd.addSubCommand(new ChangeElementCommand(elem, {
    '#href': curHref
  }))
  const img = new Image()
  img.onload = function () {
    const changes = {
      width: elem.getAttribute('width'),
      height: elem.getAttribute('height')
    }
    elem.setAttribute('width', this.width)
    elem.setAttribute('height', this.height)

    svgCanvas.selectorManager.requestSelector(elem).resize()

    batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
    svgCanvas.addCommandToHistory(batchCmd)
    svgCanvas.call('changed', [elem])
  }
  img.src = val
}

/**
* Sets the new link URL for the selected anchor element.
* @function module:svgcanvas.SvgCanvas#setLinkURL
* @param {string} val - String with the link URL/path
* @returns {void}
*/
const setLinkURLMethod = (val) => {
  const { ChangeElementCommand, BatchCommand } = svgCanvas.history
  const selectedElements = svgCanvas.getSelectedElements()
  let elem = selectedElements[0]
  if (!elem) { return }
  if (elem.tagName !== 'a') {
    // See if parent is an anchor
    const parentsA = getParents(elem.parentNode, 'a')
    if (parentsA?.length) {
      elem = parentsA[0]
    } else {
      return
    }
  }

  const curHref = getHref(elem)

  if (curHref === val) { return }

  const batchCmd = new BatchCommand('Change Link URL')

  setHref(elem, val)
  batchCmd.addSubCommand(new ChangeElementCommand(elem, {
    '#href': curHref
  }))

  svgCanvas.addCommandToHistory(batchCmd)
}

/**
* Sets the `rx` and `ry` values to the selected `rect` element
* to change its corner radius.
* @function module:svgcanvas.SvgCanvas#setRectRadius
* @param {string|Float} val - The new radius
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
const setRectRadiusMethod = (val) => {
  const { ChangeElementCommand } = svgCanvas.history
  const selectedElements = svgCanvas.getSelectedElements()
  const selected = selectedElements[0]
  if (selected?.tagName === 'rect') {
    const r = Number(selected.getAttribute('rx'))
    if (r !== val) {
      selected.setAttribute('rx', val)
      selected.setAttribute('ry', val)
      svgCanvas.addCommandToHistory(new ChangeElementCommand(selected, { rx: r, ry: r }, 'Radius'))
      svgCanvas.call('changed', [selected])
    }
  }
}

/**
* Wraps the selected element(s) in an anchor element or converts group to one.
* @function module:svgcanvas.SvgCanvas#makeHyperlink
* @param {string} url
* @returns {void}
*/
const makeHyperlinkMethod = (url) => {
  svgCanvas.groupSelectedElements('a', url)
}

/**
* @function module:svgcanvas.SvgCanvas#removeHyperlink
* @returns {void}
*/
const removeHyperlinkMethod = () => {
  svgCanvas.ungroupSelectedElement()
}

/**
* Group: Element manipulation.
*/

/**
* Sets the new segment type to the selected segment(s).
* @function module:svgcanvas.SvgCanvas#setSegType
* @param {Integer} newType - New segment type. See {@link https://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg} for list
* @returns {void}
*/
const setSegTypeMethod = (newType) => {
  svgCanvas.pathActions.setSegType(newType)
}

/**
* Set the background of the editor (NOT the actual document).
* @function module:svgcanvas.SvgCanvas#setBackground
* @param {string} color - String with fill color to apply
* @param {string} url - URL or path to image to use
* @returns {void}
*/
const setBackgroundMethod = (color, url) => {
  const bg = getElement('canvasBackground')
  const border = bg.querySelector('rect')
  let bgImg = getElement('background_image')
  let bgPattern = getElement('background_pattern')
  border.setAttribute('fill', color === 'chessboard' ? '#fff' : color)
  if (color === 'chessboard') {
    if (!bgPattern) {
      bgPattern = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'foreignObject')
      svgCanvas.assignAttributes(bgPattern, {
        id: 'background_pattern',
        width: '100%',
        height: '100%',
        preserveAspectRatio: 'xMinYMin',
        style: 'pointer-events:none'
      })
      const div = document.createElement('div')
      svgCanvas.assignAttributes(div, {
        style: 'pointer-events:none;width:100%;height:100%;' +
          'background-image:url(data:image/gif;base64,' +
          'R0lGODlhEAAQAIAAAP///9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG+' +
          'gq4jM3IFLJgpswNly/XkcBpIiVaInlLJr9FZWAQA7);'
      })
      bgPattern.append(div)
      bg.append(bgPattern)
    }
  } else if (bgPattern) {
    bgPattern.remove()
  }
  if (url) {
    if (!bgImg) {
      bgImg = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'image')
      svgCanvas.assignAttributes(bgImg, {
        id: 'background_image',
        width: '100%',
        height: '100%',
        preserveAspectRatio: 'xMinYMin',
        style: 'pointer-events:none'
      })
    }
    setHref(bgImg, url)
    bg.append(bgImg)
  } else if (bgImg) {
    bgImg.remove()
  }
}