renderer/svg/SVGRenderer.js

import * as d3 from 'd3-selection'
import { rendererLogger as logger } from '../../configuration/LoggerConfig'
import { drawStroke } from './symbols/StrokeSymbolSVGRenderer'
import * as InkModel from '../../model/InkModel'

/**
 * Get info
 * @return {RendererInfo} Information about this renderer
 */
export function getInfo () {
  return {
    type: 'svg'
  }
}

/**
 * Populate the dom element
 * @param {Element} element DOM element to attach the rendering elements
 * @return {Object} The renderer context to give as parameter when a draw model will be call
 */
export function attach (element) {
  const elementRef = element
  logger.debug('populate root element', elementRef)
  elementRef.style.fontSize = '10px'
  return d3.select(elementRef)
}

/**
 * Detach the renderer from the DOM element
 * @param {Element} element DOM element to attach the rendering elements
 * @param {Object} context Current rendering context
 */
export function detach (element, context) {
  logger.debug('detach renderer', element)
  context.selectAll('svg').remove()
}

/**
 * Update the rendering context size
 * @param {Object} context Current rendering context
 * @param {Model} model Current model
 * @param {Stroker} stroker Current stroker
 * @param {Number} minHeight Minimal height for resize
 * @param {Number} minWidth Minimal Width for resize
 * @return {Model}
 */
export function resize (context, model, stroker, minHeight, minWidth) {
  const rect = context.node().getBoundingClientRect()
  const svg = context.selectAll('svg')
  const width = rect.width < minWidth ? minWidth : rect.width
  const height = rect.height < minHeight ? minHeight : rect.height
  svg.attr('viewBox', `0 0 ${width}, ${height}`)
  svg.attr('width', `${width}px`)
  svg.attr('height', `${height}px`)
  logger.debug('svg viewBox changed', svg)
  return model
}

/**
 * Draw the current stroke from the model
 * @param {Object} context Current rendering context
 * @param {Model} model Current model
 * @param {Stroker} stroker Current stroker
 * @return {Model}
 */
export function drawCurrentStroke (context, model, stroker) {
  const modelRef = model
  // Add a pending id for pending strokes rendering
  modelRef.currentStroke.id = `pendingStroke-${model.rawStrokes.length}`
  // Render the current stroke
  logger.trace('drawing current stroke ', model.currentStroke)
  context.select(`#pendingStrokes #${modelRef.currentStroke.id}`).remove()
  drawStroke(context.select('#pendingStrokes').append('path').attr('id', model.currentStroke.id), model.currentStroke, stroker)
  return modelRef
}

function insertAdjacentSVG (element, position, html) {
  const container = element.ownerDocument.createElementNS('http://www.w3.org/2000/svg', '_')
  container.innerHTML = html

  switch (position.toLowerCase()) {
    case 'beforebegin':
      element.parentNode.insertBefore(container.firstChild, element)
      break
    case 'afterbegin':
      element.insertBefore(container.lastChild, element.firstChild)
      break
    case 'beforeend':
      element.appendChild(container.firstChild)
      break
    case 'afterend':
      element.parentNode.insertBefore(container.lastChild, element.nextSibling)
      break
    default:
      logger.warn('Invalid insertAdjacentHTML position')
      break
  }
}

/**
 * Draw all symbols contained into the model
 * @param {Object} context Current rendering context
 * @param {Model} model Current model
 * @param {Stroker} stroker Current stroker
 * @return {Model}
 */
export function drawModel (context, model, stroker) {
  const drawSymbol = (symbol, symbolContext) => {
    logger.trace(`attempting to draw ${symbol.type} symbol`)
    if (symbol.type === 'stroke' && !symbolContext.select('id', symbol.id)) {
      drawStroke(symbolContext.append('path').attr('id', symbol.id), symbol, stroker)
    } else {
      logger.warn(`impossible to draw ${symbol.type} symbol`)
    }
  }

  const updateView = (patchUpdate) => {
    // We only add in the stack patch with updates
    patchUpdate.updates.forEach((update) => {
      try {
        const svgElementSelector = 'svg[data-layer="' + patchUpdate.layer + '"]'
        switch (update.type) {
          case 'REPLACE_ALL': {
            context.select(svgElementSelector).remove()
            const parent = context.node()
            if (parent.insertAdjacentHTML) {
              parent.insertAdjacentHTML('beforeEnd', update.svg)
            } else {
              insertAdjacentSVG(parent, 'beforeEnd', update.svg)
            }
            if (patchUpdate.layer === 'MODEL') {
              context.select(svgElementSelector).append('g').attr('id', 'pendingStrokes')
            }
          }
            break
          case 'REMOVE_ELEMENT': {
            if (update.id.includes('s') || update.id.includes('MODEL')) {
              context.select(`#${update.id}`).remove()
            } else {
              context.select(`#${update.id}`).attr('class', 'removed-stroke')
              context.select(`#${update.id}`).remove()
            }
            break
          }
          case 'REPLACE_ELEMENT': {
            const parent = context.select(`#${update.id}`).node().parentNode
            context.select(`#${update.id}`).remove()
            if (parent.insertAdjacentHTML) {
              parent.insertAdjacentHTML('beforeEnd', update.svg)
            } else {
              insertAdjacentSVG(parent, 'beforeEnd', update.svg)
              context.node().insertAdjacentHTML('beforeEnd', context.select(svgElementSelector).remove().node().outerHTML)
            }
          }
            break
          case 'REMOVE_CHILD':
            context.select(`#${update.parentId} > *:nth-child(${update.index + 1})`).remove()
            break
          case 'APPEND_CHILD': {
            const parent = context.select(update.parentId ? `#${update.parentId}` : svgElementSelector).node()
            if (parent.insertAdjacentHTML) {
              parent.insertAdjacentHTML('beforeEnd', update.svg)
            } else {
              insertAdjacentSVG(parent, 'beforeEnd', update.svg)
              context.node().insertAdjacentHTML('beforeEnd', context.select(svgElementSelector).remove().node().outerHTML)
            }
          }
            break
          case 'INSERT_BEFORE': {
            const parent = context.select(`#${update.refId}`).node()
            if (parent.insertAdjacentHTML) {
              parent.insertAdjacentHTML('beforeBegin', update.svg)
            } else {
              insertAdjacentSVG(parent, 'beforeBegin', update.svg)
              context.node().insertAdjacentHTML('beforeEnd', context.select(svgElementSelector).remove().node().outerHTML)
            }
          }
            break
          case 'REMOVE_ATTRIBUTE':
            context.selectAll(update.id ? `#${update.id}` : 'svg').attr(update.name, null)
            break
          case 'SET_ATTRIBUTE': {
            // We ignore setAttributes on the svg element because we handle the resize elsewhere to prevent a blink effect
            // that occurs if we are waiting for the server to resize.
            if (update.id) {
              context.selectAll(`#${update.id}`).attr(update.name, update.value)
            }
            break
          }
          default:
            logger.debug(`unknown update ${update.type} action`)
            break
        }
      } catch (e) {
        logger.error(`Invalid update ${update.type}`, update)
        logger.error('Error on svg patch', e)
      }
    })
  }

  const removeErasingStrokes = () => {
    context.select('.erasing-stroke').remove()
  }

  const pendingRecognizedSymbols = InkModel.extractPendingRecognizedSymbols(model)
  if (pendingRecognizedSymbols) {
    pendingRecognizedSymbols.forEach(patch => updateView(patch))
    InkModel.updateModelRenderedPosition(model)
  }

  const pendingStrokes = InkModel.extractPendingStrokes(model)
  if (pendingStrokes) {
    pendingStrokes.forEach(stroke => drawSymbol(stroke, context.select('#pendingStrokes')))
  }

  removeErasingStrokes()

  return model
}