Source: svgcanvas/svgcanvas.js

/**
 * Numerous tools for working with the editor's "canvas".
 * @module svgcanvas
 *
 * @license MIT
 *
 * @copyright 2010 Alexis Deveria, 2010 Pavol Rusnak, 2010 Jeff Schiller, 2021 OptimistikSAS
 *
 */
import 'pathseg' // SVGPathSeg Polyfill (see https://github.com/progers/pathseg)

import * as pathModule from './path.js'
import * as history from './history.js'
import * as draw from './draw.js'
import { init as pasteInit, pasteElementsMethod } from './paste-elem.js'
import { init as touchInit } from './touch.js'
import { svgRootElement } from './svgroot.js'
import {
  init as undoInit,
  changeSelectedAttributeNoUndoMethod,
  changeSelectedAttributeMethod
} from './undo.js'
import { init as selectionInit } from './selection.js'
import { init as textActionsInit, textActionsMethod } from './text-actions.js'
import { init as eventInit } from './event.js'
import {
  init as jsonInit,
  getJsonFromSvgElements,
  addSVGElementsFromJson
} from './json.js'
import * as elemGetSet from './elem-get-set.js'
import { init as selectedElemInit } from './selected-elem.js'
import {
  init as blurInit,
  setBlurNoUndo,
  setBlurOffsets,
  setBlur
} from './blur-event.js'
import { sanitizeSvg } from './sanitize.js'
import { getReverseNS, NS } from './namespaces.js'
import {
  assignAttributes,
  cleanupElement,
  getElement,
  getUrlFromAttr,
  findDefs,
  getHref,
  setHref,
  getRefElem,
  getRotationAngle,
  getBBoxOfElementAsPath,
  convertToPath,
  encode64,
  decode64,
  getVisibleElements,
  init as utilsInit,
  getBBox as utilsGetBBox,
  getStrokedBBoxDefaultVisible,
  blankPageObjectURL,
  $id,
  $qa,
  $qq,
  $click,
  getFeGaussianBlur,
  stringToHTML,
  insertChildAtIndex
} from './utilities.js'
import {
  matrixMultiply,
  hasMatrixTransform,
  transformListToTransform
} from './math.js'
import { convertToNum, init as unitsInit, getTypeMap } from '../common/units.js'
import { init as svgInit } from './svg-exec.js'
import { remapElement, init as coordsInit } from './coords.js'
import {
  recalculateDimensions,
  init as recalculateInit
} from './recalculate.js'
import { getSelectorManager, Selector, init as selectInit } from './select.js'
import { clearSvgContentElementInit, init as clearInit } from './clear.js'
import {
  getClosest,
  getParents,
  mergeDeep
} from '../editor/components/jgraduate/Util.js'

import dataStorage from './dataStorage.js'

const visElems =
  'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'
const refAttrs = [
  'clip-path',
  'fill',
  'filter',
  'marker-end',
  'marker-mid',
  'marker-start',
  'mask',
  'stroke'
]

const THRESHOLD_DIST = 0.8
const STEP_COUNT = 10
const CLIPBOARD_ID = 'svgedit_clipboard'

/**
 * The main SvgCanvas class that manages all SVG-related functions.
 * @memberof module:svgcanvas
 *
 */
class SvgCanvas {
  /**
   * @param {HTMLElement} container - The container HTML element that should hold the SVG root element
   * @param {module:SVGeditor.configObj.curConfig} config - An object that contains configuration data
   */
  constructor (container, config) {
    // imported function made available as methods
    this.initializeSvgCanvasMethods()
    const { pathActions } = pathModule

    // initialize class variables
    this.saveOptions = { round_digits: 2 } // Object with save options
    this.importIds = {} // Object with IDs for imported files, to see if one was already added
    this.extensions = {} // Object to contain all included extensions
    this.removedElements = {} // Map of deleted reference elements
    this.started = false // Boolean indicating whether or not a draw action has been this.started
    this.startTransform = null // String with an element's initial transform attribute value
    this.currentMode = 'select' // String indicating the current editor mode
    this.currentResizeMode = 'none' // String with the current direction in which an element is being resized
    this.justSelected = null // The DOM element that was just selected
    this.rubberBox = null // DOM element for selection rectangle drawn by the user
    this.curBBoxes = [] // Array of current BBoxes, used in getIntersectionList().
    this.lastClickPoint = null // Canvas point for the most recent right click
    this.events = {} // Object to contain editor event names and callback functions
    this.rootSctm = null // Root Current Transformation Matrix in user units
    this.drawnPath = null
    this.freehand = {
      // Mouse events
      minx: null,
      miny: null,
      maxx: null,
      maxy: null
    }
    this.dAttr = null
    this.startX = null
    this.startY = null
    this.rStartX = null
    this.rStartY = null
    this.initBbox = {}
    this.sumDistance = 0
    this.controllPoint2 = { x: 0, y: 0 }
    this.controllPoint1 = { x: 0, y: 0 }
    this.start = { x: 0, y: 0 }
    this.end = { x: 0, y: 0 }
    this.bSpline = { x: 0, y: 0 }
    this.nextPos = { x: 0, y: 0 }
    this.idprefix = 'svg_' // Prefix string for element IDs
    this.encodableImages = {}

    this.curConfig = {
      // Default configuration options
      show_outside_canvas: true,
      selectNew: true,
      dimensions: [640, 480]
    }
    // Update config with new one if given
    if (config) {
      this.curConfig = SvgCanvas.mergeDeep(this.curConfig, config)
    }
    this.lastGoodImgUrl = `${this.curConfig.imgPath}/logo.svg` // String with image URL of last loadable image
    const { dimensions } = this.curConfig // Array with width/height of canvas

    // "document" element associated with the container (same as window.document using default svg-editor.js)
    // NOTE: This is not actually a SVG document, but an HTML document.
    this.svgdoc = window.document
    this.container = container
    // This is a container for the document being edited, not the document itself.
    this.svgroot = svgRootElement(this.svgdoc, dimensions)
    container.append(this.svgroot)
    // The actual element that represents the final output SVG element.
    this.svgContent = this.svgdoc.createElementNS(NS.SVG, 'svg')
    touchInit(this)
    clearInit(this)
    this.clearSvgContentElement()
    // Current `draw.Drawing` object.
    this.current_drawing_ = new draw.Drawing(this.svgContent, this.idprefix)
    // Float displaying the current zoom level (1 = 100%, .5 = 50%, etc.).
    this.zoom = 1

    // pointer to current group (for in-group editing)
    this.currentGroup = null

    // Object containing data for the currently selected styles
    const allProperties = {
      shape: {
        fill:
          (this.curConfig.initFill.color === 'none' ? '' : '#') +
          this.curConfig.initFill.color,
        fill_paint: null,
        fill_opacity: this.curConfig.initFill.opacity,
        stroke: '#' + this.curConfig.initStroke.color,
        stroke_paint: null,
        stroke_opacity: this.curConfig.initStroke.opacity,
        stroke_width: this.curConfig.initStroke.width,
        stroke_dasharray: 'none',
        stroke_linejoin: 'miter',
        stroke_linecap: 'butt',
        opacity: this.curConfig.initOpacity
      }
    }
    allProperties.text = SvgCanvas.mergeDeep({}, allProperties.shape)
    allProperties.text = SvgCanvas.mergeDeep(allProperties.text, {
      fill: '#000000',
      stroke_width: this.curConfig.text?.stroke_width,
      font_size: this.curConfig.text?.font_size,
      font_family: this.curConfig.text?.font_family
    })
    this.curText = allProperties.text // Current text style properties

    // Current shape style properties
    this.curShape = allProperties.shape
    this.curProperties = this.curShape // Current general properties

    // Array with all the currently selected elements
    // default size of 1 until it needs to grow bigger
    this.selectedElements = []

    jsonInit(this)
    unitsInit(this)
    utilsInit(this)
    coordsInit(this)
    recalculateInit(this)
    selectInit(this)
    undoInit(this)
    selectionInit(this)

    this.nsMap = getReverseNS()
    this.selectorManager = getSelectorManager()

    this.pathActions = pathActions
    pathModule.init(this)
    // Interface strings, usually for title elements
    this.uiStrings = {}

    // Animation element to change the opacity of any newly created element
    this.opacAni = document.createElementNS(NS.SVG, 'animate')
    this.opacAni.setAttribute('attributeName', 'opacity')
    this.opacAni.setAttribute('begin', 'indefinite')
    this.opacAni.setAttribute('dur', 1)
    this.opacAni.setAttribute('fill', 'freeze')
    this.svgroot.appendChild(this.opacAni)

    eventInit(this)
    textActionsInit(this)
    svgInit(this)
    draw.init(this)
    elemGetSet.init(this)

    // prevent links from being followed in the canvas
    const handleLinkInCanvas = e => {
      e.preventDefault()
      return false
    }
    container.addEventListener('mousedown', this.mouseDownEvent)
    container.addEventListener('mousemove', this.mouseMoveEvent)
    $click(container, handleLinkInCanvas)
    container.addEventListener('dblclick', this.dblClickEvent)
    container.addEventListener('mouseup', this.mouseUpEvent)
    container.addEventListener('mouseleave', this.mouseOutEvent)
    container.addEventListener('mousewheel', this.DOMMouseScrollEvent)
    container.addEventListener('DOMMouseScroll', this.DOMMouseScrollEvent)

    // Alias function
    this.linkControlPoints = pathActions.linkControlPoints
    this.curCommand = null
    this.filter = null
    this.filterHidden = false

    blurInit(this)
    selectedElemInit(this)

    /**
     * Transfers sessionStorage from one tab to another.
     * @param {!Event} ev Storage event.
     * @returns {void}
     */
    const storageChange = ev => {
      if (!ev.newValue) return // This is a call from removeItem.
      if (ev.key === CLIPBOARD_ID + '_startup') {
        // Another tab asked for our sessionStorage.
        localStorage.removeItem(CLIPBOARD_ID + '_startup')
        this.flashStorage()
      } else if (ev.key === CLIPBOARD_ID) {
        // Another tab sent data.
        sessionStorage.setItem(CLIPBOARD_ID, ev.newValue)
      }
    }

    // Listen for changes to localStorage.
    window.addEventListener('storage', storageChange, false)
    // Ask other tabs for sessionStorage (this is ONLY to trigger event).
    localStorage.setItem(CLIPBOARD_ID + '_startup', Math.random())

    pasteInit(this)

    this.contentW = this.getResolution().w
    this.contentH = this.getResolution().h
    this.clear()
  } // End constructor

  getSvgOption () {
    return this.saveOptions
  }

  setSvgOption (key, value) {
    this.saveOptions[key] = value
  }

  getSelectedElements () {
    return this.selectedElements
  }

  setSelectedElements (key, value) {
    this.selectedElements[key] = value
  }

  setEmptySelectedElements () {
    this.selectedElements = []
  }

  getSvgRoot () {
    return this.svgroot
  }

  getDOMDocument () {
    return this.svgdoc
  }

  getDOMContainer () {
    return this.container
  }

  getCurConfig () {
    return this.curConfig
  }

  setIdPrefix (p) {
    this.idprefix = p
  }

  getCurrentDrawing () {
    return this.current_drawing_
  }

  getCurShape () {
    return this.curShape
  }

  getCurrentGroup () {
    return this.currentGroup
  }

  getBaseUnit () {
    return this.curConfig.baseUnit
  }

  getHeight () {
    return this.svgContent.getAttribute('height') / this.zoom
  }

  getWidth () {
    return this.svgContent.getAttribute('width') / this.zoom
  }

  getRoundDigits () {
    return this.saveOptions.round_digits
  }

  getSnappingStep () {
    return this.curConfig.snappingStep
  }

  getGridSnapping () {
    return this.curConfig.gridSnapping
  }

  getStartTransform () {
    return this.startTransform
  }

  setStartTransform (transform) {
    this.startTransform = transform
  }

  getZoom () {
    return this.zoom
  }

  round (val) {
    return Number.parseInt(val * this.zoom) / this.zoom
  }

  createSVGElement (jsonMap) {
    return this.addSVGElementsFromJson(jsonMap)
  }

  getContainer () {
    return this.container
  }

  setStarted (s) {
    this.started = s
  }

  getRubberBox () {
    return this.rubberBox
  }

  setRubberBox (rb) {
    this.rubberBox = rb
    return this.rubberBox
  }

  addPtsToSelection ({ closedSubpath, grips }) {
    // TODO: Correct this:
    this.pathActions.canDeleteNodes = true
    this.pathActions.closed_subpath = closedSubpath
    this.call('pointsAdded', { closedSubpath, grips })
    this.call('selected', grips)
  }

  /**
   * @param {PlainObject} changes
   * @param {ChangeElementCommand} changes.cmd
   * @param {SVGPathElement} changes.elem
   * @fires module:svgcanvas.SvgCanvas#event:changed
   * @returns {void}
   */
  endChanges ({ cmd, elem }) {
    this.addCommandToHistory(cmd)
    this.call('changed', [elem])
  }

  getCurrentMode () {
    return this.currentMode
  }

  setCurrentMode (cm) {
    this.currentMode = cm
    return this.currentMode
  }

  getDrawnPath () {
    return this.drawnPath
  }

  setDrawnPath (dp) {
    this.drawnPath = dp
    return this.drawnPath
  }

  setCurrentGroup (cg) {
    this.currentGroup = cg
  }

  changeSvgContent () {
    this.call('changed', [this.svgContent])
  }

  getStarted () {
    return this.started
  }

  getCanvas () {
    return this
  }

  getrootSctm () {
    return this.rootSctm
  }

  getStartX () {
    return this.startX
  }

  setStartX (value) {
    this.startX = value
  }

  getStartY () {
    return this.startY
  }

  setStartY (value) {
    this.startY = value
  }

  getRStartX () {
    return this.rStartX
  }

  getRStartY () {
    return this.rStartY
  }

  getInitBbox () {
    return this.initBbox
  }

  getCurrentResizeMode () {
    return this.currentResizeMode
  }

  getJustSelected () {
    return this.justSelected
  }

  getOpacAni () {
    return this.opacAni
  }

  getParameter () {
    return this.parameter
  }

  getNextParameter () {
    return this.nextParameter
  }

  getStepCount () {
    return STEP_COUNT
  }

  getThreSholdDist () {
    return THRESHOLD_DIST
  }

  getSumDistance () {
    return this.sumDistance
  }

  getStart (key) {
    return this.start[key]
  }

  getEnd (key) {
    return this.end[key]
  }

  getbSpline (key) {
    return this.bSpline[key]
  }

  getNextPos (key) {
    return this.nextPos[key]
  }

  getControllPoint1 (key) {
    return this.controllPoint1[key]
  }

  getControllPoint2 (key) {
    return this.controllPoint2[key]
  }

  getFreehand (key) {
    return this.freehand[key]
  }

  getDrawing () {
    return this.getCurrentDrawing()
  }

  getDAttr () {
    return this.dAttr
  }

  getLastGoodImgUrl () {
    return this.lastGoodImgUrl
  }

  getCurText (key) {
    return this.curText[key]
  }

  setDAttr (value) {
    this.dAttr = value
  }

  setEnd (key, value) {
    this.end[key] = value
  }

  setControllPoint1 (key, value) {
    this.controllPoint1[key] = value
  }

  setControllPoint2 (key, value) {
    this.controllPoint2[key] = value
  }

  setJustSelected (value) {
    this.justSelected = value
  }

  setParameter (value) {
    this.parameter = value
  }

  setStart (value) {
    this.start = value
  }

  setRStartX (value) {
    this.rStartX = value
  }

  setRStartY (value) {
    this.rStartY = value
  }

  setSumDistance (value) {
    this.sumDistance = value
  }

  setbSpline (value) {
    this.bSpline = value
  }

  setNextPos (value) {
    this.nextPos = value
  }

  setNextParameter (value) {
    this.nextParameter = value
  }

  setCurText (key, value) {
    this.curText[key] = value
  }

  setFreehand (key, value) {
    this.freehand[key] = value
  }

  setCurBBoxes (value) {
    this.curBBoxes = value
  }

  getCurBBoxes () {
    return this.curBBoxes
  }

  setInitBbox (value) {
    this.initBbox = value
  }

  setRootSctm (value) {
    this.rootSctm = value
  }

  setCurrentResizeMode (value) {
    this.currentResizeMode = value
  }

  getLastClickPoint (key) {
    return this.lastClickPoint[key]
  }

  setLastClickPoint (value) {
    this.lastClickPoint = value
  }

  getId () {
    return this.getCurrentDrawing().getId()
  }

  getUIStrings () {
    return this.uiStrings
  }

  getNsMap () {
    return this.nsMap
  }

  getSvgOptionApply () {
    return this.saveOptions.apply
  }

  getSvgOptionImages () {
    return this.saveOptions.images
  }

  getEncodableImages (key) {
    return this.encodableImages[key]
  }

  setEncodableImages (key, value) {
    this.encodableImages[key] = value
  }

  getVisElems () {
    return visElems
  }

  getIdPrefix () {
    return this.idprefix
  }

  getDataStorage () {
    return dataStorage
  }

  setZoom (value) {
    this.zoom = value
  }

  getImportIds (key) {
    return this.importIds[key]
  }

  setImportIds (key, value) {
    this.importIds[key] = value
  }

  setRemovedElements (key, value) {
    this.removedElements[key] = value
  }

  setSvgContent (value) {
    this.svgContent = value
  }

  getrefAttrs () {
    return refAttrs
  }

  setCanvas (key, value) {
    this[key] = value
  }

  setCurProperties (key, value) {
    this.curProperties[key] = value
  }

  getCurProperties (key) {
    return this.curProperties[key]
  }

  setCurShape (key, value) {
    this.curShape[key] = value
  }

  gettingSelectorManager () {
    return this.selectorManager
  }

  getContentW () {
    return this.contentW
  }

  getContentH () {
    return this.contentH
  }

  getClipboardID () {
    return CLIPBOARD_ID
  }

  getSvgContent () {
    return this.svgContent
  }

  getExtensions () {
    return this.extensions
  }

  getSelector () {
    return Selector
  }

  getMode () {
    return this.currentMode
  } // The current editor mode string

  getNextId () {
    return this.getCurrentDrawing().getNextId()
  }

  getCurCommand () {
    return this.curCommand
  }

  setCurCommand (value) {
    this.curCommand = value
  }

  getFilter () {
    return this.filter
  }

  setFilter (value) {
    this.filter = value
  }

  getFilterHidden () {
    return this.filterHidden
  }

  setFilterHidden (value) {
    this.filterHidden = value
  }

  /**
   * Sets the editor's mode to the given string.
   * @function module:svgcanvas.SvgCanvas#setMode
   * @param {string} name - String with the new mode to change to
   * @returns {void}
   */
  setMode (name) {
    this.pathActions.clear(true)
    this.textActions.clear()
    this.curProperties =
      this.selectedElements[0]?.nodeName === 'text'
        ? this.curText
        : this.curShape
    this.currentMode = name
  }

  /**
   * Clears the current document. This is not an undoable action.
   * @function module:svgcanvas.SvgCanvas#clear
   * @fires module:svgcanvas.SvgCanvas#event:beforeClear|afterClear
   * @returns {void}
   */
  clear () {
    this.call('beforeClear')
    this.pathActions.clear()
    this.clearSelection()
    // clear the svgcontent node
    this.clearSvgContentElement()
    // create new document
    this.current_drawing_ = new draw.Drawing(this.svgContent)
    // create empty first layer
    this.createLayer('Layer 1')
    // clear the undo stack
    this.undoMgr.resetUndoStack()
    // reset the selector manager
    this.selectorManager.initGroup()
    // reset the rubber band box
    this.rubberBox = this.selectorManager.getRubberBandBox()
    this.call('afterClear')
  }

  async addExtension (name, extInitFunc, { importLocale }) {
    if (typeof extInitFunc !== 'function') {
      throw new TypeError(
        'Function argument expected for `svgcanvas.addExtension`'
      )
    }
    if (name in this.extensions) {
      throw new Error(
        'Cannot add extension "' +
          name +
          '", an extension by that name already exists.'
      )
    }
    const argObj = {
      importLocale,
      svgroot: this.svgroot,
      svgContent: this.svgContent,
      nonce: this.getCurrentDrawing().getNonce(),
      selectorManager: this.selectorManager
    }
    const extObj = await extInitFunc(argObj)
    if (extObj) {
      extObj.name = name
    }
    this.extensions[name] = extObj
    return this.call('extension_added', extObj)
  }

  addCommandToHistory (cmd) {
    this.undoMgr.addCommandToHistory(cmd)
  }

  restoreRefElements (elem) {
    // Look for missing reference elements, restore any found
    const attrs = {}
    refAttrs.forEach((item, _) => {
      attrs[item] = elem.getAttribute(item)
    })
    Object.values(attrs).forEach(val => {
      if (val?.startsWith('url(')) {
        const id = getUrlFromAttr(val).substr(1)
        const ref = getElement(id)
        if (!ref) {
          findDefs().append(this.removedElements[id])
          delete this.removedElements[id]
        }
      }
    })
    const childs = elem.getElementsByTagName('*')

    if (childs.length) {
      for (let i = 0, l = childs.length; i < l; i++) {
        this.restoreRefElements(childs[i])
      }
    }
  }

  call (ev, arg) {
    if (this.events[ev]) {
      return this.events[ev](window, arg)
    }
    return undefined
  }

  /**
   * Attaches a callback function to an event.
   * @function module:svgcanvas.SvgCanvas#bind
   * @param  {string} ev - String indicating the name of the event
   * @param {module:svgcanvas.EventHandler} f - The callback function to bind to the event
   * @returns {module:svgcanvas.EventHandler} The previous event
   */
  bind (ev, f) {
    const old = this.events[ev]
    this.events[ev] = f
    return old
  }

  /**
   * Flash the clipboard data momentarily on localStorage so all tabs can see.
   * @returns {void}
   */
  flashStorage () {
    const data = sessionStorage.getItem(CLIPBOARD_ID)
    localStorage.setItem(CLIPBOARD_ID, data)
    setTimeout(() => {
      localStorage.removeItem(CLIPBOARD_ID)
    }, 1)
  }

  /**
   * Selects only the given elements, shortcut for `clearSelection(); addToSelection()`.
   * @function module:svgcanvas.SvgCanvas#selectOnly
   * @param {Element[]} elems - an array of DOM elements to be selected
   * @param {boolean} showGrips - Indicates whether the resize grips should be shown
   * @returns {void}
   */
  selectOnly (elems, showGrips) {
    this.clearSelection(true)
    this.addToSelection(elems, showGrips)
  }

  /**
   * Removes elements from the selection.
   * @function module:svgcanvas.SvgCanvas#removeFromSelection
   * @param {Element[]} elemsToRemove - An array of elements to remove from selection
   * @returns {void}
   */
  removeFromSelection (elemsToRemove) {
    if (!this.selectedElements[0]) {
      return
    }
    if (!elemsToRemove.length) {
      return
    }

    // find every element and remove it from our array copy
    const newSelectedItems = []
    const len = this.selectedElements.length
    for (let i = 0; i < len; ++i) {
      const elem = this.selectedElements[i]
      if (elem) {
        // keep the item
        if (!elemsToRemove.includes(elem)) {
          newSelectedItems.push(elem)
        } else {
          // remove the item and its selector
          this.selectorManager.releaseSelector(elem)
        }
      }
    }
    // the copy becomes the master now
    this.selectedElements = newSelectedItems
  }

  /**
   * Clears the selection, then adds all elements in the current layer to the selection.
   * @function module:svgcanvas.SvgCanvas#selectAllInCurrentLayer
   * @returns {void}
   */
  selectAllInCurrentLayer () {
    const currentLayer = this.getCurrentDrawing().getCurrentLayer()
    if (currentLayer) {
      this.currentMode = 'select'
      if (this.currentGroup) {
        this.selectOnly(this.currentGroup.children)
      } else {
        this.selectOnly(currentLayer.children)
      }
    }
  }

  getOpacity () {
    return this.curShape.opacity
  }

  /**
   * @function module:svgcanvas.SvgCanvas#getSnapToGrid
   * @returns {boolean} The current snap to grid setting
   */
  getSnapToGrid () {
    return this.curConfig.gridSnapping
  }

  /**
   * @function module:svgcanvas.SvgCanvas#getVersion
   * @returns {string} A string which describes the revision number of SvgCanvas.
   */
  getVersion () {
    return 'svgcanvas.js ($Rev$)'
  }

  /**
   * Update interface strings with given values.
   * @function module:svgcanvas.SvgCanvas#setUiStrings
   * @param {module:path.uiStrings} strs - Object with strings (see the [locales API]{@link module:locale.LocaleStrings} and the [tutorial]{@tutorial LocaleDocs})
   * @returns {void}
   */
  setUiStrings (strs) {
    Object.assign(this.uiStrings, strs.notification)
    pathModule.setUiStrings(strs)
  }

  /**
   * Update configuration options with given values.
   * @function module:svgcanvas.SvgCanvas#setConfig
   * @param {module:SVGEditor.Config} opts - Object with options
   * @returns {void}
   */
  setConfig (opts) {
    Object.assign(this.curConfig, opts)
  }

  /**
   * @function module:svgcanvas.SvgCanvas#getDocumentTitle
   * @returns {string|void} The current document title or an empty string if not found
   */
  getDocumentTitle () {
    return this.getTitle(this.svgContent)
  }

  getOffset () {
    return {
      x: Number(this.svgContent.getAttribute('x')),
      y: Number(this.svgContent.getAttribute('y'))
    }
  }

  getColor (type) {
    return this.curProperties[type]
  }

  setStrokePaint (paint) {
    this.setPaint('stroke', paint)
  }

  /**
   * @function module:svgcanvas.SvgCanvas#setFillPaint
   * @param {module:jGraduate~Paint} paint
   * @returns {void}
   */
  setFillPaint (paint) {
    this.setPaint('fill', paint)
  }

  /**
   * @function module:svgcanvas.SvgCanvas#getStrokeWidth
   * @returns {Float|string} The current stroke-width value
   */
  getStrokeWidth () {
    return this.curProperties.stroke_width
  }

  /**
   * @function module:svgcanvas.SvgCanvas#getStyle
   * @returns {module:svgcanvas.StyleOptions} current style options
   */
  getStyle () {
    return this.curShape
  }

  /**
   * Sets the given opacity on the current selected elements.
   * @function module:svgcanvas.SvgCanvas#setOpacity
   * @param {string} val
   * @returns {void}
   */
  setOpacity (val) {
    this.curShape.opacity = val
    this.changeSelectedAttribute('opacity', val)
  }

  /**
   * @function module:svgcanvas.SvgCanvas#getFillOpacity
   * @returns {Float} the current fill opacity
   */
  getFillOpacity () {
    return this.curShape.fill_opacity
  }

  /**
   * @function module:svgcanvas.SvgCanvas#getStrokeOpacity
   * @returns {string} the current stroke opacity
   */
  getStrokeOpacity () {
    return this.curShape.stroke_opacity
  }

  /**
   * Sets the current fill/stroke opacity.
   * @function module:svgcanvas.SvgCanvas#setPaintOpacity
   * @param {string} type - String with "fill" or "stroke"
   * @param {Float} val - Float with the new opacity value
   * @param {boolean} preventUndo - Indicates whether or not this should be an undoable action
   * @returns {void}
   */
  setPaintOpacity (type, val, preventUndo) {
    this.curShape[type + '_opacity'] = val
    if (!preventUndo) {
      this.changeSelectedAttribute(type + '-opacity', val)
    } else {
      this.changeSelectedAttributeNoUndo(type + '-opacity', val)
    }
  }

  /**
   * Gets the current fill/stroke opacity.
   * @function module:svgcanvas.SvgCanvas#getPaintOpacity
   * @param {"fill"|"stroke"} type - String with "fill" or "stroke"
   * @returns {Float} Fill/stroke opacity
   */
  getPaintOpacity (type) {
    return type === 'fill' ? this.getFillOpacity() : this.getStrokeOpacity()
  }

  /**
   * Gets the `stdDeviation` blur value of the given element.
   * @function module:svgcanvas.SvgCanvas#getBlur
   * @param {Element} elem - The element to check the blur value for
   * @returns {string} stdDeviation blur attribute value
   */
  getBlur (elem) {
    let val = 0
    if (elem) {
      const filterUrl = elem.getAttribute('filter')
      if (filterUrl) {
        const blur = getElement(elem.id + '_blur')
        if (blur) {
          val = blur.firstChild.getAttribute('stdDeviation')
        } else {
          const filterElem = getRefElem(filterUrl)
          const blurElem = getFeGaussianBlur(filterElem)
          if (blurElem !== null) {
            val = blurElem.getAttribute('stdDeviation')
          }
        }
      }
    }
    return val
  }

  /**
   * Sets a given URL to be a "last good image" URL.
   * @function module:svgcanvas.SvgCanvas#setGoodImage
   * @param {string} val
   * @returns {void}
   */
  setGoodImage (val) {
    this.lastGoodImgUrl = val
  }

  /**
   * Returns the current drawing as raw SVG XML text.
   * @function module:svgcanvas.SvgCanvas#getSvgString
   * @returns {string} The current drawing as raw SVG XML text.
   */
  getSvgString () {
    this.saveOptions.apply = false
    return this.svgCanvasToString()
  }

  /**
   * This function determines whether to use a nonce in the prefix, when
   * generating IDs for future documents in SVG-Edit.
   * If you're controlling SVG-Edit externally, and want randomized IDs, call
   * this BEFORE calling `svgCanvas.setSvgString`.
   * @function module:svgcanvas.SvgCanvas#randomizeIds
   * @param {boolean} [enableRandomization] If true, adds a nonce to the prefix. Thus
   * `svgCanvas.randomizeIds() <==> svgCanvas.randomizeIds(true)`
   * @returns {void}
   */
  randomizeIds (enableRandomization) {
    if (arguments.length > 0 && enableRandomization === false) {
      draw.randomizeIds(false, this.getCurrentDrawing())
    } else {
      draw.randomizeIds(true, this.getCurrentDrawing())
    }
  }

  /**
   * Convert selected element to a path, or get the BBox of an element-as-path.
   * @function module:svgcanvas.SvgCanvas#convertToPath
   * @todo (codedread): Remove the getBBox argument and split this function into two.
   * @param {Element} elem - The DOM element to be converted
   * @param {boolean} getBBox - Boolean on whether or not to only return the path's BBox
   * @returns {void|DOMRect|false|SVGPathElement|null} If the getBBox flag is true, the resulting path's bounding box object.
   * Otherwise the resulting path element is returned.
   */
  convertToPath (elem, getBBox) {
    // if elems not given, recursively call convertPath for all selected elements.
    if (!elem) {
      const elems = this.selectedElements
      elems.forEach(el => {
        if (el) {
          this.convertToPath(el)
        }
      })
      return undefined
    }
    if (getBBox) {
      return getBBoxOfElementAsPath(
        elem,
        this.addSVGElementsFromJson,
        this.pathActions
      )
    }
    // TODO: Why is this applying attributes from this.curShape, then inside utilities.convertToPath it's pulling addition attributes from elem?
    // TODO: If convertToPath is called with one elem, this.curShape and elem are probably the same; but calling with multiple is a bug or cool feature.
    const attrs = {
      fill: this.curShape.fill,
      'fill-opacity': this.curShape.fill_opacity,
      stroke: this.curShape.stroke,
      'stroke-width': this.curShape.stroke_width,
      'stroke-dasharray': this.curShape.stroke_dasharray,
      'stroke-linejoin': this.curShape.stroke_linejoin,
      'stroke-linecap': this.curShape.stroke_linecap,
      'stroke-opacity': this.curShape.stroke_opacity,
      opacity: this.curShape.opacity,
      visibility: 'hidden'
    }
    return convertToPath(elem, attrs, this) // call convertToPath from utilities.js
  }

  /**
   * Removes all selected elements from the DOM and adds the change to the
   * history stack. Remembers removed elements on the clipboard.
   * @function module:svgcanvas.SvgCanvas#cutSelectedElements
   * @returns {void}
   */
  cutSelectedElements () {
    this.copySelectedElements()
    this.deleteSelectedElements()
  }

  initializeSvgCanvasMethods () {
    this.getJsonFromSvgElements = getJsonFromSvgElements
    this.addSVGElementsFromJson = addSVGElementsFromJson
    this.clearSvgContentElement = clearSvgContentElementInit
    this.textActions = textActionsMethod
    this.getStrokedBBox = getStrokedBBoxDefaultVisible
    this.getVisibleElements = getVisibleElements
    this.stringToHTML = stringToHTML
    this.insertChildAtIndex = insertChildAtIndex
    this.getClosest = getClosest
    this.getParents = getParents
    this.isLayer = draw.Layer.isLayer
    this.matrixMultiply = matrixMultiply
    this.hasMatrixTransform = hasMatrixTransform
    this.transformListToTransform = transformListToTransform
    this.convertToNum = convertToNum
    this.findDefs = findDefs
    this.getUrlFromAttr = getUrlFromAttr
    this.getHref = getHref
    this.setHref = setHref
    this.getBBox = utilsGetBBox
    this.getRotationAngle = getRotationAngle
    this.getElement = getElement
    this.getRefElem = getRefElem
    this.assignAttributes = assignAttributes
    this.cleanupElement = cleanupElement
    this.remapElement = remapElement
    this.recalculateDimensions = recalculateDimensions
    this.sanitizeSvg = sanitizeSvg
    this.pasteElements = pasteElementsMethod // Remembers the current selected elements on the clipboard.
    this.identifyLayers = draw.identifyLayers
    this.createLayer = draw.createLayer
    this.cloneLayer = draw.cloneLayer
    this.deleteCurrentLayer = draw.deleteCurrentLayer
    this.setCurrentLayer = draw.setCurrentLayer
    this.renameCurrentLayer = draw.renameCurrentLayer
    this.setCurrentLayerPosition = draw.setCurrentLayerPosition
    this.indexCurrentLayer = draw.indexCurrentLayer
    this.setLayerVisibility = draw.setLayerVisibility
    this.moveSelectedToLayer = draw.moveSelectedToLayer
    this.mergeLayer = draw.mergeLayer
    this.mergeAllLayers = draw.mergeAllLayers
    this.leaveContext = draw.leaveContext
    this.setContext = draw.setContext
    this.changeSelectedAttributeNoUndo = changeSelectedAttributeNoUndoMethod // This function makes the changes to the elements. It does not add the change to the history stack.
    this.changeSelectedAttribute = changeSelectedAttributeMethod // Change the given/selected element and add the original value to the history stack.
    this.setBlurNoUndo = setBlurNoUndo // Sets the `stdDeviation` blur value on the selected element without being undoable.
    this.setBlurOffsets = setBlurOffsets // Sets the `x`, `y`, `width`, `height` values of the filter element in order to make the blur not be clipped. Removes them if not neeeded.
    this.setBlur = setBlur // Adds/updates the blur filter to the selected element.
    this.smoothControlPoints = pathModule.smoothControlPoints
    this.getTypeMap = getTypeMap
    this.history = history // object with all histor methods
    this.NS = NS
    this.$id = $id
    this.$qq = $qq
    this.$qa = $qa
    this.$click = $click
    this.encode64 = encode64
    this.decode64 = decode64
    this.mergeDeep = mergeDeep
  }
} // End class

// attach utilities function to the class that are used by SvgEdit so
// we can avoid using the whole utilities.js file in svgEdit.js
SvgCanvas.$id = $id
SvgCanvas.$qq = $qq
SvgCanvas.$qa = $qa
SvgCanvas.$click = $click
SvgCanvas.encode64 = encode64
SvgCanvas.decode64 = decode64
SvgCanvas.mergeDeep = mergeDeep
SvgCanvas.getClosest = getClosest
SvgCanvas.getParents = getParents
SvgCanvas.blankPageObjectURL = blankPageObjectURL

export default SvgCanvas