Editor.js

/* eslint-disable no-underscore-dangle */
import style from './iink.css'
import { editorLogger as logger } from './configuration/LoggerConfig'
import * as DefaultBehaviors from './configuration/DefaultBehaviors'
import * as DefaultConfiguration from './configuration/DefaultConfiguration'
import * as DefaultStyles from './configuration/DefaultPenStyle'
import * as DefaultTheme from './configuration/DefaultTheme'
import * as InkModel from './model/InkModel'
import * as UndoRedoContext from './model/UndoRedoContext'
import * as UndoRedoManager from './model/UndoRedoManager'
import * as ImageRenderer from './renderer/canvas/ImageRenderer'
import * as RecognizerContext from './model/RecognizerContext'
import * as SmartGuide from './smartguide/SmartGuide'
import Constants from './configuration/Constants'
import * as eastereggs from './eastereggs/InkImporter'
import {
  handleError,
  handleSuccess,
  emitEvents,
  manageRecognizedModel
} from './recognizer/RecognizerService'
import * as PromiseHelper from './util/PromiseHelper'

/**
 * Check if a clear is required, and does it if it is
 * @param {Editor} editor
 * @param {Model} model Current model
 * @return {Promise<*>}
 */
function manageResetState (editor, model) {
  // If strokes moved in the undo redo stack then a clear is mandatory before sending strokes.
  if (editor.recognizer.reset && !editor.isErasing && RecognizerContext.isResetRequired(editor.recognizerContext, model)) {
    return editor.recognizer.reset(editor.recognizerContext, model)
  }
  return null
}

/**
 * Check if the trigger in parameter is valid.
 * @param {Editor} editor
 * @param {String} type
 * @param {String} [trigger]
 * @return {Boolean}
 */
function isTriggerValid (editor, type, trigger = editor.configuration.triggers[type]) {
  if (editor.recognizer &&
    editor.recognizer.getInfo().availableTriggers[type].includes(trigger)) {
    return true
  }
  logger.error(`${trigger} is not a valid trigger for ${type}`)
  return false
}

/**
 * Launch the recognition with all editor relative configuration and state.
 * @param {Editor} editor
 * @param {Model} model
 * @param {String} [trigger]
 * @return {Promise}
 */
async function addStrokes (editor, model, trigger = editor.configuration.triggers.addStrokes) {
  if (editor.recognizer && editor.recognizer.addStrokes) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      // Firing addStrokes only if recognizer is configure to do it
      if (isTriggerValid(editor, 'addStrokes', trigger)) {
        const res = await manageResetState(editor, model)
        if (res) {
          return editor.recognizer.addStrokes(editor.recognizerContext, res)
        }
        return editor.recognizer.addStrokes(editor.recognizerContext, model)
      }
    }
  }
  return Promise.reject(new Error('Cannot addStrokes'))
}

/**
 * Launch ink import.
 * @param {Editor} editor
 * @param {Model} model
 * @param {PointerEvents} events
 * @return {Promise<*>}
 */
async function launchPointerEvents (editor, model, events) {
  if (editor.recognizer && editor.recognizer.pointerEvents) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.pointerEvents(editor.recognizerContext, model, events)
    }
  }
  return Promise.reject(new Error('Cannot launch pointerEvents'))
}

/**
 * Launch the recognition with all editor relative configuration and state.
 * @param {Editor} editor
 * @param {Model} model
 * @param {String} [requestedMimeTypes]
 * @param {String} [trigger]
 */
export async function launchExport (editor, model, requestedMimeTypes, trigger = editor.configuration.triggers.exportContent) {
  if (editor.recognizer && editor.recognizer.export_) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      if (isTriggerValid(editor, 'exportContent', trigger)) {
        const editorRef = editor
        window.clearTimeout(editor.exportTimer)
        const timeout = trigger === Constants.Trigger.QUIET_PERIOD ? editor.configuration.triggerDelay : 0
        const delayer = PromiseHelper.delay(timeout)
        editorRef.exportTimer = delayer.timer
        await delayer.promise
        const res = await manageResetState(editor, model)
        if (res) {
          return editor.recognizer.export_(editor.recognizerContext, res, requestedMimeTypes)
        }
        return editor.recognizer.export_(editor.recognizerContext, model, requestedMimeTypes)
      }
    }
  }
  return Promise.reject(new Error('Cannot launch export'))
}

/**
 * Launch the import.
 * @param {Editor} editor
 * @param {Model} model
 * @param {Blob} data
 * @return {Promise<*>}
 */
async function launchImport (editor, model, data) {
  if (editor.recognizer && editor.recognizer.import_) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.import_(editor.recognizerContext, model, data)
    }
  }
  return Promise.reject(new Error('Cannot launch import'))
}

/**
 * Get the supported mimetypes for import.
 * @param {Editor} editor
 * @param {Model} model
 * @return {Promise<*>}
 */
async function launchGetSupportedImportMimeTypes (editor, model) {
  if (editor.recognizer && editor.recognizer.getSupportedImportMimeTypes) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.getSupportedImportMimeTypes(editor.recognizerContext, model)
    }
  }
  return Promise.reject(new Error('Cannot launch getSupportedImportMimeTypes'))
}

/**
 * Launch the convert with all editor relative configuration and state.
 * @param {Editor} editor
 * @param {Model} model
 * @param {String} conversionState
 * @return {Promise<*>}
 */
async function launchConvert (editor, model, conversionState) {
  if (editor.recognizer && editor.recognizer.convert) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.convert(editor.recognizerContext, model, conversionState)
    }
  }
  return Promise.reject(new Error('Cannot launch convert'))
}

/**
 * Launch the configuration for the editor
 * @param {Editor} editor
 * @param {Model} model
 * @return {Promise<*>}
 */
async function launchConfig (editor, model) {
  if (editor.recognizer && editor.recognizer.sendConfiguration) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.sendConfiguration(editor.recognizerContext, model)
    }
  }
  return Promise.reject(new Error('Cannot launch config'))
}

/**
 * Launch the resize.
 * @param {Editor} editor
 * @param {Model} model
 */
async function launchResize (editor, model) {
  if (editor.recognizer && editor.recognizer.resize) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      const editorRef = editor
      window.clearTimeout(editor.resizeTimer)
      const delayer = PromiseHelper.delay(editor.configuration.resizeTriggerDelay)
      editorRef.resizeTimer = delayer.timer
      SmartGuide.resize(editor.smartGuide)
      await delayer.promise
      return editor.recognizer.resize(editor.recognizerContext, model, editor.domElement)
    }
  }
  return Promise.reject(new Error('Cannot launch resize'))
}

/**
 * Launch wait for idle
 * @param {Editor} editor
 * @param {Model} model
 * @return {Promise<*>}
 */
async function launchWaitForIdle (editor, model) {
  if (editor.recognizer && editor.recognizer.waitForIdle) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.waitForIdle(editor.recognizerContext, model)
    }
  }
  return Promise.reject(new Error('Cannot launch wait for idle'))
}

/**
 * Launch websocket close
 * @param {Editor} editor
 * @param {Model} model
 * @return {Promise<*>}
 */
async function launchClose (editor, model) {
  if (editor.smartGuide) {
    SmartGuide.reset(editor.smartGuide)
  }
  if (editor.recognizer && editor.recognizer.close) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      editor.loader.style.display = 'none'
      editor.error.innerText = Constants.Error.CLOSE
      editor.error.style.display = 'initial'
      return editor.recognizer.close(editor.recognizerContext, model)
    }
  }
  return Promise.reject(new Error('Cannot launch close'))
}

/**
 * Set pen style.
 * @param {Editor} editor
 * @param {Model} model
 * @return {Promise<*>}
 */
async function setPenStyle (editor, model) {
  if (editor.recognizer && editor.recognizer.setPenStyle) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.setPenStyle(editor.recognizerContext, model, editor.penStyle)
    }
    return Promise.reject(new Error('Cannot set pentStyle'))
  }
  return null
}

/**
 * Set pen style.
 * @param {Editor} editor
 * @param {Model} model
 * @return {Promise<*>}
 */
async function setPenStyleClasses (editor, model) {
  if (editor.recognizer && editor.recognizer.setPenStyleClasses) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.setPenStyleClasses(editor.recognizerContext, model, editor.penStyleClasses)
    }
    return Promise.reject(new Error('Cannot set penStyleClasses'))
  }
  return null
}

/**
 * Set theme.
 * @param {Editor} editor
 * @param {Model} model
 * @return {Promise<*>}
 */
async function setTheme (editor, model) {
  if (editor.recognizer && editor.recognizer.setTheme) {
    const init = await editor.recognizerContext.initPromise
    if (init) {
      return editor.recognizer.setTheme(editor.recognizerContext, model, editor.theme)
    }
    return Promise.reject(new Error('Cannot set theme'))
  }
  return null
}

/**
 * Editor
 */
export class Editor {
  /**
   * @param {Element} element DOM element to attach this editor
   * @param {Configuration} [configuration] Configuration to apply
   * @param {Theme} [theme] Custom theme to apply
   * @param {PenStyle} [penStyle] Custom style to apply
   * @param {Behaviors} [behaviors] Custom behaviors to apply
   */
  constructor (element, configuration, penStyle, theme, behaviors, globalClassCss) {
    globalClassCss = globalClassCss || 'ms-editor'

    const styleElement = document.createElement('style')
    styleElement.appendChild(document.createTextNode(''))
    element.appendChild(styleElement)

    const sheet = styleElement.sheet
    styleElement.textContent = style

    this.sheet = sheet
    /**
     * Inner reference to the DOM Element
     * @type {Element}
     */
    this.domElement = element
    this.domElement.classList.add(globalClassCss)

    // eslint-disable-next-line no-undef
    this.loader = document.createElement('div')
    this.loader.classList.add('loader')
    this.loader = this.domElement.appendChild(this.loader)

    // eslint-disable-next-line no-undef
    this.error = document.createElement('div')
    this.error.classList.add('error-msg')
    this.error = this.domElement.appendChild(this.error)

    /**
     * Launch export timer
     * @type {Number}
     */
    this.exportTimer = undefined

    /**
     * Launch resize timer
     * @type {Number}
     */
    this.resizeTimer = undefined

    /**
     * Notify delay timer
     * @type {Number}
     */
    this.notifyTimer = undefined

    /**
     * @private
     * @type {Behaviors}
     */
    this.innerBehaviors = DefaultBehaviors.overrideDefaultBehaviors(behaviors)
    this.configuration = configuration

    /**
     * Pen color used only for pending stroke
     * @type {string}
     */
    this.localTheme = ''

    this.theme = theme
    this._setThemeFontFamily()
    this.penStyle = penStyle
    this.penStyleClasses = ''

    // To override pointerType when ERASER
    this.isErasing = false

    this.domElement.editor = this
  }

  /**
   * Set the recognition parameters
   * WARNING : Need to fire a clear if user have already input some strokes.
   * @param {Configuration} configuration
   */
  set configuration (configuration) {
    this.loader.style.display = 'initial'
    this.error.style.display = 'none'
    /**
     * @private
     * @type {Configuration}
     */
    this.innerConfiguration = DefaultConfiguration.overrideDefaultConfiguration(configuration)
    this.behavior = this.behaviors.getBehaviorFromConfiguration(this.behaviors, this.innerConfiguration)
    if (this.smartGuide) {
      SmartGuide.reset(this.smartGuide)
    } else {
      this.smartGuide = SmartGuide.createSmartGuide(this)
    }
  }

  /**
   * Get the current recognition parameters
   * @return {Configuration}
   */
  get configuration () {
    return this.innerConfiguration
  }

  /**
   * Set the pen style
   * @param {PenStyle} penStyle
   */
  set penStyle (penStyle) {
    /**
     * @private
     * @type {PenStyle}
     */
    this.innerPenStyle = DefaultStyles.overrideDefaultPenStyle(penStyle)
    this.localPenStyle = this.innerPenStyle
    setPenStyle(this, this.model)
  }

  /**
   * Get the pen style
   * @return {PenStyle}
   */
  get penStyle () {
    return this.innerPenStyle
  }

  /**
   * Set the pen style
   * @param {String} penStyleClasses
   */
  set penStyleClasses (penStyleClasses) {
    /**
     * @private
     * @type {String}
     */
    this.innerPenStyleClasses = penStyleClasses
    this.localPenStyle = this.theme[`.${this.innerPenStyleClasses}`]
    setPenStyleClasses(this, this.model)
  }

  /**
   * Get the pen style
   * @return {String}
   */
  get penStyleClasses () {
    return this.innerPenStyleClasses
  }

  /**
   * Set the theme
   * @param {Theme} theme
   */
  set theme (theme) {
    /**
     * @private
     * @type {Theme}
     */
    this.innerTheme = DefaultTheme.overrideDefaultTheme(theme)
    setTheme(this, this.model)
  }

  /**
   * Get the theme
   * @return {Theme}
   */
  get theme () {
    return this.innerTheme
  }

  /**
   * Get behaviors
   * @return {Behaviors}
   */
  get behaviors () {
    return this.innerBehaviors
  }

  /**
   * @private
   * @param {Behavior} behavior
   */
  set behavior (behavior) {
    if (behavior) {
      if (this.grabber) { // Remove event handlers to avoid multiplication (detach grabber)
        this.grabber.detach(this.domElement, this.grabberContext)
      }
      /**
       * @private
       * @type {Behavior}
       */
      this.innerBehavior = behavior
      this.renderer = this.innerBehavior.renderer
      this.recognizer = this.innerBehavior.recognizer
      /**
       * Current grabber context
       * @type {GrabberContext}
       */
      this.grabberContext = this.grabber.attach(this.domElement, this)
    }
  }

  /**
   * Get current behavior
   * @return {Behavior}
   */
  get behavior () {
    return this.innerBehavior
  }

  /**
   * Set the current recognizer
   * @private
   * @param {Recognizer} recognizer
   */
  set recognizer (recognizer) {
    this.undoRedoContext = UndoRedoContext.createUndoRedoContext(this.configuration)
    this.undoRedoManager = UndoRedoManager

    const initialize = (model, shouldSendTheme) => {
      /**
       * @private
       * @type {Recognizer}
       */
      this.innerRecognizer = recognizer
      if (this.innerRecognizer) {
        /**
         * Current recognition context
         * @type {RecognizerContext}
         */
        this.recognizerContext = RecognizerContext.createEmptyRecognizerContext(this)
        // FIXME: merge undo/redo manager with default recognizer
        if (this.innerRecognizer.undo && this.innerRecognizer.redo && this.innerRecognizer.clear) {
          this.undoRedoContext = this.recognizerContext
          this.undoRedoManager = this.innerRecognizer
        }
        this.innerRecognizer.init(this.recognizerContext, model)
          .then((values) => {
            logger.info('Recognizer initialized !')
            if (shouldSendTheme) {
              setTheme(this, this.model)
              setPenStyle(this, this.model)
              setPenStyleClasses(this, this.model)
            }
            this.loader.style.display = 'none'
          })
          .catch(err => {
            handleError(this, err)
          })
      }
    }

    if (recognizer) {
      if (this.innerRecognizer) {
        this.innerRecognizer.close(this.recognizerContext, this.model)
          .then((model) => {
            logger.info('Recognizer closed')
            this._setThemeFontFamily()
            initialize(InkModel.clearModel(model), true)
            handleSuccess(this, model)
          })
          .catch(err => handleError(this, err))
      } else {
        /**
         * Current model
         * @type {Model}
         */
        this.model = InkModel.createModel(this.configuration)

        // INFO: Recognizer needs model to be initialized
        initialize(this.model, false)
      }
    }
  }

  /**
   * Get current recognizer
   * @return {Recognizer}
   */
  get recognizer () {
    return this.innerRecognizer
  }

  /**
   * Set the current renderer
   * @private
   * @param {Renderer} renderer
   */
  set renderer (renderer) {
    if (renderer) {
      if (this.innerRenderer) {
        this.innerRenderer.detach(this.domElement, this.rendererContext)
      }

      /**
       * @private
       * @type {Renderer}
       */
      this.innerRenderer = renderer
      if (this.innerRenderer) {
        /**
         * Current rendering context
         * @type {Object}
         */
        this.rendererContext = this.innerRenderer.attach(this.domElement, this.configuration.renderingParams.minHeight, this.configuration.renderingParams.minWidth)
      }
    }
  }

  /**
   * Get current renderer
   * @return {Renderer}
   */
  get renderer () {
    return this.innerRenderer
  }

  /**
   * Get current grabber
   * @return {Grabber}
   */
  get grabber () {
    return this.behavior ? this.behavior.grabber : undefined
  }

  /**
   * Get current stroker
   * @return {Stroker}
   */
  get stroker () {
    return this.behavior ? this.behavior.stroker : undefined
  }

  /**
   * Get current events
   * @return {Array}
   */
  get emit () {
    return this.behavior ? this.behavior.events : undefined
  }

  /**
   * Get a PNG image data url from the data model
   * @return {String}
   */
  get png () {
    return ImageRenderer.getImage(this.model, this.stroker)
  }

  /**
   * True if initialized, false otherwise
   * @return {Boolean}
   */
  get initialized () {
    return this.recognizerContext ? this.recognizerContext.initialized : false
  }

  enableEraser () {
    this.isErasing = true
    this.domElement.classList.add('erasing')
  }

  disableEraser () {
    document.body.style.cursor = 'initial'
    this.isErasing = false
    this.domElement.classList.remove('erasing')
  }

  /**
   * Handle a pointer down
   * @param {{x: Number, y: Number, t: Number}} point Captured point coordinates
   * @param {String} [pointerType=mouse] Current pointer type
   * @param {String} [pointerId] Current pointer id
   */
  pointerDown (point, pointerType = 'pen', pointerId) {
    logger.trace('Pointer down', point)
    window.clearTimeout(this.notifyTimer)
    window.clearTimeout(this.exportTimer)
    this.model = InkModel.initPendingStroke(this.model, point, Object.assign({ pointerType, pointerId }, this.theme.ink, this.localPenStyle))
    this.renderer.drawCurrentStroke(this.rendererContext, this.model, this.stroker)
    // Currently no recognition on pointer down
  }

  /**
   * Handle a pointer move
   * @param {{x: Number, y: Number, t: Number}} point Captured point coordinates
   */
  pointerMove (point) {
    logger.trace('Pointer move', point)
    this.model = InkModel.appendToPendingStroke(this.model, point)
    this.renderer.drawCurrentStroke(this.rendererContext, this.model, this.stroker)
    // Currently no recognition on pointer move
  }

  /**
   * Handle a pointer up
   * @param {{x: Number, y: Number, t: Number}} point Captured point coordinates
   */
  pointerUp (point) {
    logger.trace('Pointer up', point)
    this.model = InkModel.endPendingStroke(this.model, point, this.penStyle)
    this.renderer.drawModel(this.rendererContext, this.model, this.stroker)

    if (this.recognizer.addStrokes) {
      addStrokes(this, this.model)
    } else {
      // Push model in undo redo manager
      handleSuccess(this, this.model)
    }
  }

  removeStroke (stroke) {
    this.model.strokeGroups.forEach((group) => {
      const stringStrokes = group.strokes.map(strokeFromGroup => JSON.stringify(strokeFromGroup))
      const strokeIndex = stringStrokes.indexOf(JSON.stringify(stroke))
      if (strokeIndex !== -1) {
        group.strokes.splice(strokeIndex, 1)
      }
    })
    const stringRawStrokes = this.model.rawStrokes.map(strokes => JSON.stringify(strokes))
    const strokeIndex = stringRawStrokes.indexOf(JSON.stringify(stroke))
    if (strokeIndex !== -1) {
      this.model.rawStrokes.splice(strokeIndex, 1)
    }
    this.renderer.drawModel(this.rendererContext, this.model, this.stroker)
    handleSuccess(this, this.model)
    if (!(this.configuration.triggers.exportContent === 'DEMAND')) {
      launchExport(this, this.model)
    }
  }

  /**
   * @Deprecated
   * @param rawStrokes
   * @param strokeGroups
   */
  reDraw (rawStrokes, strokeGroups) {
    rawStrokes.forEach((stroke) => {
      InkModel.addStroke(this.model, stroke)
    })
    strokeGroups.forEach((group) => {
      group.strokes.forEach((strokeFromGroup) => {
        InkModel.addStrokeToGroup(this.model, strokeFromGroup, group.penStyle)
      })
    })
    this.renderer.drawModel(this.rendererContext, this.model, this.stroker)
    handleSuccess(this, this.model)
  }

  /**
   * True if idle state
   * @return {Boolean}
   */
  get idle () {
    return this.recognizerContext.idle
  }

  /**
   * Wait for idle state.
   * @return {Promise<*>}
   */
  waitForIdle () {
    emitEvents(this, undefined, Constants.EventType.IDLE)
    return launchWaitForIdle(this, this.model)
  }

  /**
   * True if can undo, false otherwise.
   * @return {Boolean}
   */
  get canUndo () {
    return this.undoRedoContext.canUndo
  }

  /**
   * Undo the last action.
   * @return {Promise<*>}
   */
  async undo () {
    logger.debug('Undo current model', this.model)
    emitEvents(this, undefined, Constants.EventType.UNDO)
    const { res, types } = await this.undoRedoManager.undo(this.undoRedoContext, this.model)
      .catch(err => handleError(this, err))
    manageRecognizedModel(this, res, ...types)
    return res
  }

  /**
   * True if can redo, false otherwise.
   * @return {Boolean}
   */
  get canRedo () {
    return this.undoRedoContext.canRedo
  }

  /**
   * Redo the last action.
   * @return {Promise<*>}
   */
  async redo () {
    logger.debug('Redo current model', this.model)
    emitEvents(this, undefined, Constants.EventType.REDO)
    const { res, types } = await this.undoRedoManager.redo(this.undoRedoContext, this.model)
      .catch(err => handleError(this, err))
    manageRecognizedModel(this, res, ...types)
    return res
  }

  /**
   * True if empty, false otherwise
   * @return {boolean}
   */
  get isEmpty () {
    return this.recognizerContext.isEmpty
  }

  /**
   * True if can clear, false otherwise.
   * @return {Boolean}
   */
  get canClear () {
    return !this.isEmpty
  }

  /**
   * Clear the output and the recognition result.
   * @return {Promise<*>}
   */
  async clear () {
    if (this.canClear) {
      logger.debug('Clear current model', this.model)
      emitEvents(this, undefined, Constants.EventType.CLEAR)
      const { res, events } = await this.recognizer.clear(this.recognizerContext, this.model)
        .catch(error => handleError(this, error))
      handleSuccess(this, res, ...events)
      return res
    }
    return Promise.reject(new Error('Cannot launch clear'))
  }

  /**
   * True if can convert, false otherwise.
   * @return {Boolean}
   */
  get canConvert () {
    return !!(this.canUndo && this.canClear && this.recognizer && this.recognizer.convert)
  }

  /**
   * Convert the current content
   * @param {string} conversionState
   * @return {Promise<*>}
   */
  convert (conversionState = 'DIGITAL_EDIT') {
    if (this.canConvert) {
      emitEvents(this, undefined, Constants.EventType.CONVERT)
      return launchConvert(this, this.model, conversionState)
    }
    return Promise.reject(new Error('Cannot launch convert'))
  }

  /**
   * Set the guides for text
   * @param {Boolean} [enable]
   * @return {Promise<*|null>}
   */
  setGuides (enable = true) {
    this.configuration.recognitionParams.iink.text.guides.enable = enable
    return launchConfig(this, this.model)
  }

  /**
   * Return the position of the cursor identifying the current state in the internal iink undo/redo stack.
   * @returns {Number}
   */
  get possibleUndoCount () {
    return this.recognizerContext.possibleUndoCount
  }

  /**
   * The number of operations that it is currently possible to undo.
   * @returns {Number}
   */
  get undoStackIndex () {
    return this.recognizerContext.undoStackIndex
  }

  /**
   * True if can export, false otherwise.
   * @return {Boolean}
   */
  get canExport () {
    return this.canUndo && this.canClear && this.recognizer && this.recognizer.getInfo().availableTriggers.exportContent.includes(Constants.Trigger.DEMAND)
  }

  /**
   * Explicitly ask to perform an export. You have to listen to events to get the content as this function is non blocking and does not have a return type.
   * @param {Array<String>} requestedMimeTypes Requested mime-types. Be sure to ask all the types required by the listeners of exported event.
   */
  export_ (requestedMimeTypes) {
    if (this.canExport) {
      emitEvents(this, undefined, Constants.EventType.EXPORT)
      return launchExport(this, this.model, requestedMimeTypes, Constants.Trigger.DEMAND)
    }
    return Promise.reject(new Error('Cannot launch export'))
  }

  /**
   * Import content.
   * @param {Blob|*} data Data to import
   * @param {String} [mimetype] Mimetype of the data, needed if data is not a Blob
   */
  import_ (data, mimetype) {
    emitEvents(this, undefined, Constants.EventType.IMPORT)
    return launchImport(this, this.model, !(data instanceof Blob) ? new Blob([data], { type: mimetype }) : data)
  }

  /**
   * Get supported import mime types
   * @return {Promise<*|null>}
   */
  getSupportedImportMimeTypes () {
    return launchGetSupportedImportMimeTypes(this, this.model)
  }

  /**
   * pointer events
   * @param {PointerEvents} events
   * @return {Promise<*|null>}
   */
  pointerEvents (events) {
    return launchPointerEvents(this, this.model, events)
  }

  /**
   * Get current state exports
   * @return {Object}
   */
  get exports () {
    return this.model ? this.model.exports : undefined
  }

  get supportedImportMimeTypes () {
    return this.recognizerContext.supportedImportMimeTypes
  }

  /**
   * Function to call when the dom element link to the current ink paper has been resize.
   */
  resize () {
    logger.debug('Resizing editor')
    this.renderer.resize(this.rendererContext, this.model, this.stroker, this.configuration.renderingParams.minHeight, this.configuration.renderingParams.minWidth)
    return launchResize(this, this.model)
  }

  /**
   * Detach event listeners from the DOM element created at editor creation.
   */
  unload () {
    if (this.grabber) { // Remove event handlers to avoid multiplication (detach grabber)
      this.grabber.detach(this.domElement, this.grabberContext)
    }
    if (this.innerRenderer) {
      this.innerRenderer.detach(this.domElement, this.rendererContext)
    }
  }

  /**
   * Close websocket connection
   * @return {Promise<*>}
   */
  close () {
    if (this.configuration.recognitionParams.protocol === Constants.Protocol.WEBSOCKET) {
      return launchClose(this, this.model)
    }
    return null
  }

  /**
   * Apply the font-family on theem corresponding to chosen language
   * @private
   */
  _setThemeFontFamily () {
    const defaultLang = !Object.keys(Constants.Languages).includes(this.configuration.recognitionParams.iink.lang)
    this.theme['.text']['font-family'] = defaultLang ? Constants.Languages.default : Constants.Languages[this.configuration.recognitionParams.iink.lang]
  }

  /**
   * Trigger the change callbacks (and by default send a change event).
   */
  forceChange () {
    emitEvents(this, undefined, Constants.EventType.CHANGED)
  }

  /* eslint-disable class-methods-use-this */
  /**
   * Get access to some easter egg features link ink injection. Use at your own risk (less tested and may be removed without notice).
   */
  get eastereggs () {
    return eastereggs
  }
  /* eslint-enable class-methods-use-this */
}