Source: editor/extensions/ext-opensave/ext-opensave.js

/* globals seConfirm */
/**
 * @file ext-opensave.js
 *
 * @license MIT
 *
 * @copyright 2020 OptimistikSAS
 *
 */

/**
   * @type {module:svgcanvas.EventHandler}
   * @param {external:Window} wind
   * @param {module:svgcanvas.SvgCanvas#event:saved} svg The SVG source
   * @listens module:svgcanvas.SvgCanvas#event:saved
   * @returns {void}
   */
import { fileOpen, fileSave } from 'browser-fs-access'

const name = 'opensave'
let handle = null

const loadExtensionTranslation = async function (svgEditor) {
  let translationModule
  const lang = svgEditor.configObj.pref('lang')
  try {
    translationModule = await import(`./locale/${lang}.js`)
  } catch (_error) {
    console.warn(`Missing translation (${lang}) for ${name} - using 'en'`)
    translationModule = await import('./locale/en.js')
  }
  svgEditor.i18next.addResourceBundle(lang, 'translation', translationModule.default, true, true)
}

export default {
  name,
  async init (_S) {
    const svgEditor = this
    const { svgCanvas } = svgEditor
    const { $id, $click } = svgCanvas
    await loadExtensionTranslation(svgEditor)
    /**
    * @param {Event} e
    * @returns {void}
    */
    const importImage = (e) => {
      $id('se-prompt-dialog').title = this.i18next.t('notification.loadingImage')
      $id('se-prompt-dialog').setAttribute('close', false)
      e.stopPropagation()
      e.preventDefault()
      const file = (e.type === 'drop') ? e.dataTransfer.files[0] : e.currentTarget.files[0]
      if (!file) {
        $id('se-prompt-dialog').setAttribute('close', true)
        return
      }

      if (!file.type.includes('image')) {
        return
      }
      // Detected an image
      // svg handling
      let reader
      if (file.type.includes('svg')) {
        reader = new FileReader()
        reader.onloadend = (ev) => {
          const newElement = this.svgCanvas.importSvgString(ev.target.result, true)
          this.svgCanvas.alignSelectedElements('m', 'page')
          this.svgCanvas.alignSelectedElements('c', 'page')
          // highlight imported element, otherwise we get strange empty selectbox
          this.svgCanvas.selectOnly([newElement])
          $id('se-prompt-dialog').setAttribute('close', true)
        }
        reader.readAsText(file)
      } else {
        // bitmap handling
        reader = new FileReader()
        reader.onloadend = function ({ target: { result } }) {
          /**
              * Insert the new image until we know its dimensions.
              * @param {Float} imageWidth
              * @param {Float} imageHeight
              * @returns {void}
              */
          const insertNewImage = (imageWidth, imageHeight) => {
            const newImage = this.svgCanvas.addSVGElementsFromJson({
              element: 'image',
              attr: {
                x: 0,
                y: 0,
                width: imageWidth,
                height: imageHeight,
                id: this.svgCanvas.getNextId(),
                style: 'pointer-events:inherit'
              }
            })
            this.svgCanvas.setHref(newImage, result)
            this.svgCanvas.selectOnly([newImage])
            this.svgCanvas.alignSelectedElements('m', 'page')
            this.svgCanvas.alignSelectedElements('c', 'page')
            this.topPanel.updateContextPanel()
            $id('se-prompt-dialog').setAttribute('close', true)
          }
          // create dummy img so we know the default dimensions
          let imgWidth = 100
          let imgHeight = 100
          const img = new Image()
          img.style.opacity = 0
          img.addEventListener('load', () => {
            imgWidth = img.offsetWidth || img.naturalWidth || img.width
            imgHeight = img.offsetHeight || img.naturalHeight || img.height
            insertNewImage(imgWidth, imgHeight)
          })
          img.src = result
        }
        reader.readAsDataURL(file)
      }
    }
    // create an input with type file to open the filesystem dialog
    const imgImport = document.createElement('input')
    imgImport.type = 'file'
    imgImport.addEventListener('change', importImage)
    // dropping a svg file will import it in the svg as well
    this.workarea.addEventListener('drop', importImage)

    const clickClear = async function () {
      const [x, y] = svgEditor.configObj.curConfig.dimensions
      const ok = await seConfirm(svgEditor.i18next.t('notification.QwantToClear'))
      if (ok === 'Cancel') {
        return
      }
      svgEditor.leftPanel.clickSelect()
      svgEditor.svgCanvas.clear()
      svgEditor.svgCanvas.setResolution(x, y)
      svgEditor.updateCanvas(true)
      svgEditor.zoomImage()
      svgEditor.layersPanel.populateLayers()
      svgEditor.topPanel.updateContextPanel()
      svgEditor.topPanel.updateTitle('untitled.svg')
    }

    /**
     * By default,  this.editor.svgCanvas.open() is a no-op. It is up to an extension
     *  mechanism (opera widget, etc.) to call `setCustomHandlers()` which
     *  will make it do something.
     * @returns {void}
     */
    const clickOpen = async function () {
      // ask user before clearing an unsaved SVG
      const response = await svgEditor.openPrep()
      if (response === 'Cancel') { return }
      svgCanvas.clear()
      try {
        const blob = await fileOpen({
          mimeTypes: ['image/*']
        })
        const svgContent = await blob.text()
        await svgEditor.loadSvgString(svgContent)
        svgEditor.updateCanvas()
        handle = blob.handle
        svgEditor.topPanel.updateTitle(blob.name)
        svgEditor.svgCanvas.runExtensions('onOpenedDocument', {
          name: blob.name,
          lastModified: blob.lastModified,
          size: blob.size,
          type: blob.type
        })
      } catch (err) {
        if (err.name !== 'AbortError') {
          return console.error(err)
        }
      }
    }
    const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => {
      const byteCharacters = atob(b64Data)
      const byteArrays = []
      for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
        const slice = byteCharacters.slice(offset, offset + sliceSize)
        const byteNumbers = new Array(slice.length)
        for (let i = 0; i < slice.length; i++) {
          byteNumbers[i] = slice.charCodeAt(i)
        }
        const byteArray = new Uint8Array(byteNumbers)
        byteArrays.push(byteArray)
      }
      const blob = new Blob(byteArrays, { type: contentType })
      return blob
    }

    /**
     *
     * @returns {void}
     */
    const clickSave = async function (type) {
      const $editorDialog = $id('se-svg-editor-dialog')
      const editingsource = $editorDialog.getAttribute('dialog') === 'open'
      if (editingsource) {
        svgEditor.saveSourceEditor()
      } else {
        // In the future, more options can be provided here
        const saveOpts = {
          images: svgEditor.configObj.pref('img_save'),
          round_digits: 2
        }
        // remove the selected outline before serializing
        svgCanvas.clearSelection()
        // Update save options if provided
        if (saveOpts) {
          const saveOptions = svgCanvas.mergeDeep(svgCanvas.getSvgOption(), saveOpts)
          for (const [key, value] of Object.entries(saveOptions)) {
            svgCanvas.setSvgOption(key, value)
          }
        }
        svgCanvas.setSvgOption('apply', true)

        // no need for doctype, see https://jwatt.org/svg/authoring/#doctype-declaration
        const svg = '<?xml version="1.0"?>\n' + svgCanvas.svgCanvasToString()
        const b64Data = svgCanvas.encode64(svg)
        const blob = b64toBlob(b64Data, 'image/svg+xml')
        try {
          if (type === 'save' && handle !== null) {
            const throwIfExistingHandleNotGood = false
            handle = await fileSave(blob, {
              fileName: 'untitled.svg',
              extensions: ['.svg']
            }, handle, throwIfExistingHandleNotGood)
          } else {
            handle = await fileSave(blob, {
              fileName: svgEditor.title,
              extensions: ['.svg']
            })
          }
          svgEditor.topPanel.updateTitle(handle.name)
          svgCanvas.runExtensions('onSavedDocument', {
            name: handle.name,
            kind: handle.kind
          })
        } catch (err) {
          if (err.name !== 'AbortError') {
            return console.error(err)
          }
        }
      }
    }

    return {
      name: svgEditor.i18next.t(`${name}:name`),
      // The callback should be used to load the DOM with the appropriate UI items
      callback () {
        const buttonTemplate = `
        <se-menu-item id="tool_clear" label="opensave.new_doc" shortcut="N" src="new.svg"></se-menu-item>`
        svgCanvas.insertChildAtIndex($id('main_button'), buttonTemplate, 0)
        const openButtonTemplate = '<se-menu-item id="tool_open" label="opensave.open_image_doc" src="open.svg"></se-menu-item>'
        svgCanvas.insertChildAtIndex($id('main_button'), openButtonTemplate, 1)
        const saveButtonTemplate = '<se-menu-item id="tool_save" label="opensave.save_doc" shortcut="S" src="saveImg.svg"></se-menu-item>'
        svgCanvas.insertChildAtIndex($id('main_button'), saveButtonTemplate, 2)
        const saveAsButtonTemplate = '<se-menu-item id="tool_save_as" label="opensave.save_as_doc" src="saveImg.svg"></se-menu-item>'
        svgCanvas.insertChildAtIndex($id('main_button'), saveAsButtonTemplate, 3)
        const importButtonTemplate = '<se-menu-item id="tool_import" label="tools.import_doc" src="importImg.svg"></se-menu-item>'
        svgCanvas.insertChildAtIndex($id('main_button'), importButtonTemplate, 4)

        // handler
        $click($id('tool_clear'), clickClear.bind(this))
        $click($id('tool_open'), clickOpen.bind(this))
        $click($id('tool_save'), clickSave.bind(this, 'save'))
        $click($id('tool_save_as'), clickSave.bind(this, 'saveas'))
        $click($id('tool_import'), () => imgImport.click())
      }
    }
  }
}