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

/* eslint-disable no-unsanitized/property */
/* globals seConfirm */
/**
 * @file ext-imagelib.js
 *
 * @license MIT
 *
 * @copyright 2010 Alexis Deveria
 *
 */

const name = "imagelib";

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

export default {
  name,
  async init({ decode64, dropXMLInternalSubset }) {
    const svgEditor = this;
    const { $id } = svgEditor.svgCanvas;
    const { $svgEditor } = svgEditor;
    const { imgPath } = svgEditor.configObj.curConfig;

    await loadExtensionTranslation(svgEditor);

    const { svgCanvas } = svgEditor;

    const imgLibs = [
      {
        name: svgEditor.i18next.t(`${name}:imgLibs_0_name`),
        url: 'extensions/ext-imagelib/index.html',
        description: svgEditor.i18next.t(`${name}:imgLibs_0_description`)
      },
      {
        name: svgEditor.i18next.t(`${name}:imgLibs_1_name`),
        url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
        description: svgEditor.i18next.t(`${name}:imgLibs_1_description`)
      }
    ];

    const allowedImageLibOrigins = imgLibs.map(({ url }) => {
      try {
        return new URL(url).origin;
      } catch (err) {
        return location.origin;
      }
    });

    /**
    *
    * @returns {void}
    */
    const closeBrowser = () => {
      $id("imgbrowse_holder").style.display = 'none';
      document.activeElement.blur(); // make sure focus is the body to correct issue #417
    };

    /**
    * @param {string} url
    * @returns {void}
    */
    const importImage = (url) => {
      const newImage = svgCanvas.addSVGElementFromJson({
        element: 'image',
        attr: {
          x: 0,
          y: 0,
          width: 0,
          height: 0,
          id: svgCanvas.getNextId(),
          style: 'pointer-events:inherit'
        }
      });
      svgCanvas.clearSelection();
      svgCanvas.addToSelection([ newImage ]);
      svgCanvas.setImageURL(url);
    };

    const pending = {};

    let mode = 's';
    let multiArr = [];
    let transferStopped = false;
    let preview; let submit;

    /**
     * Contains the SVG to insert.
     * @typedef {PlainObject} ImageLibMessage
     * @property {"imagelib"} namespace Required to distinguish from any other messages of app.
     * @property {string} href Set to same value as previous `ImageLibMetaMessage` `id`.
     * @property {string} data The response (as an SVG string or URL)
    */

    /**
     * Used for setting meta-data before images are retrieved.
     * @typedef {PlainObject} ImageLibMetaMessage
     * @property {"imagelib"} namespace Required to distinguish from any other messages of app.
     * @property {string} name If the subsequent response is an SVG string or if `preview_url`
     *   is present, will be used as the title for the preview image. When an
     *   SVG string is present, will default to the first `<title>`'s contents or
     *   "(SVG #<Length of response>)" if none is present. Otherwise, if `preview_url`
     *   is present, will default to the empty string. Though `name` may be falsy,
     *   it is always expected to be present for meta messages.
     * @property {string} id Identifier (the expected `href` for a subsequent response message);
     * used for ensuring the subsequent response can be tied to this `ImageLibMetaMessage` object.
     * @property {string} [preview_url] When import mode is multiple, used to set an image
     * source along with the name/title. If the subsequent response is an SVG string
     * and there is no `preview_url`, the default will just be to show the
     * name/title. If the response is not an SVG string, the default will be to
     * show that response (i.e., the URL).
     * @property {string} entry Set automatically with div holding retrieving
     * message (until ready to delete)
     * @todo Should use a separate Map instead of `entry`
    */

    /**
     * @param {PlainObject} cfg
     * @param {string} cfg.origin
     * @param {ImageLibMetaMessage|ImageLibMessage|string} cfg.data String is deprecated when parsed to JSON `ImageLibMessage`
     * @returns {void}
     */
    async function onMessage({ origin, data }) {
      let response = data;
      if (!response || ![ 'string', 'object' ].includes(typeof response)) {
        // Do nothing
        return;
      }
      let id;
      let type;
      try {
        // Todo: This block can be removed (and the above check changed to
        //   insist on an object) if embedAPI moves away from a string to
        //   an object (if IE9 support not needed)
        response = typeof response === 'object' ? response : JSON.parse(response);
        if (response.namespace !== 'imagelib') {
          return;
        }
        if (!allowedImageLibOrigins.includes('*') && !allowedImageLibOrigins.includes(origin)) {
          // Todo: Surface this error to user?
          console.error(`Origin ${origin} not whitelisted for posting to ${window.origin}`);
          return;
        }
        const hasName = 'name' in response;
        const hasHref = 'href' in response;

        if (!hasName && transferStopped) {
          transferStopped = false;
          return;
        }

        if (hasHref) {
          id = response.href;
          response = response.data;
        }

        // Hide possible transfer dialog box
        if (document.querySelector('se-elix-alert-dialog')) {
          document.querySelector('se-elix-alert-dialog').remove();
        }
        type = hasName
          ? 'meta'
          : response.charAt(0);
      } catch (e) {
        // This block is for backward compatibility (for IAN and Openclipart);
        //   should otherwise return
        if (typeof response === 'string') {
          const char1 = response.charAt(0);

          if (char1 !== '{' && transferStopped) {
            transferStopped = false;
            return;
          }

          if (char1 === '|') {
            const secondpos = response.indexOf('|', 1);
            id = response.substr(1, secondpos - 1);
            response = response.substr(secondpos + 1);
            type = response.charAt(0);
          }
        }
      }

      let entry; let curMeta; let svgStr; let imgStr;
      switch (type) {
      case 'meta': {
        // Metadata
        transferStopped = false;
        curMeta = response;

        // Should be safe to add dynamic property as passed metadata
        pending[curMeta.id] = curMeta; // lgtm [js/remote-property-injection]

        const name = (curMeta.name || 'file');

        const message = svgEditor.i18next.t('notification.retrieving').replace('%s', name);

        if (mode !== 'm') {
          await seConfirm(message);
          transferStopped = true;
        } else {
          entry = document.createElement('div');
          entry.textContent = message;
          entry.dataset.id = curMeta.id;
          preview.appendChild(entry);
          curMeta.entry = entry;
        }

        return;
      }
      case '<':
        svgStr = true;
        break;
      case 'd': {
        if (response.startsWith('data:image/svg+xml')) {
          const pre = 'data:image/svg+xml;base64,';
          const src = response.substring(pre.length);
          response = decode64(src);
          svgStr = true;
          break;
        } else if (response.startsWith('data:image/')) {
          imgStr = true;
          break;
        }
      }
      // Else fall through
      default:
        // TODO: See if there's a way to base64 encode the binary data stream
        // const str = 'data:;base64,' + svgedit.utilities.encode64(response, true);

        // Assume it's raw image data
        // importImage(str);

        // Don't give warning as postMessage may have been used by something else
        if (mode !== 'm') {
          closeBrowser();
        } else {
          pending[id].entry.remove();
        }
        // await alert('Unexpected data was returned: ' + response, function() {
        //   if (mode !== 'm') {
        //     closeBrowser();
        //   } else {
        //     pending[id].entry.remove();
        //   }
        // });
        return;
      }

      switch (mode) {
      case 's':
        // Import one
        if (svgStr) {
          svgEditor.svgCanvas.importSvgString(response);
        } else if (imgStr) {
          importImage(response);
        }
        closeBrowser();
        break;
      case 'm': {
        // Import multiple
        multiArr.push([ (svgStr ? 'svg' : 'img'), response ]);
        curMeta = pending[id];
        let title;
        if (svgStr) {
          if (curMeta && curMeta.name) {
            title = curMeta.name;
          } else {
            // Try to find a title
            // `dropXMLInternalSubset` is to help prevent the billion laughs attack
            const xml = new DOMParser().parseFromString(dropXMLInternalSubset(response), 'text/xml').documentElement; // lgtm [js/xml-bomb]
            title = xml.querySelector('title').textContent || '(SVG #' + response.length + ')';
          }
          if (curMeta) {
            Array.from(preview.children).forEach(function (element) {
              if (element.dataset.id === id) {
                if (curMeta.preview_url) {
                  const img = document.createElement("img");
                  img.src = curMeta.preview_url;
                  const span = document.createElement("span");
                  span.appendChild(img);
                  element.append(span);
                } else {
                  element.textContent = title;
                }
                submit.removeAttribute('disabled');
              }
            });
          } else {
            const div = document.createElement("div");
            div.textContent = title;
            preview.appendChild(div);
            submit.removeAttribute('disabled');
          }
        } else {
          if (curMeta && curMeta.preview_url) {
            title = curMeta.name || '';
            entry = document.createElement('span');
            const img = document.createElement("img");
            img.src = curMeta.preview_url;
            entry.appendChild(img);
            entry.appendChild(document.createTextNode(title));
          } else {
            entry = document.createElement("img");
            entry.src = response;
          }

          if (curMeta) {
            Array.from(preview.children).forEach(function (element) {
              if (element.dataset.id === id) {
                element.appendChild(entry);
                submit.removeAttribute('disabled');
              }
            });
          } else {
            const div = document.createElement("div");
            div.appendChild(entry);
            preview.appendChild(div);
            submit.removeAttribute('disabled');
          }
        }
        break;
      } case 'o': {
        // Open
        if (!svgStr) { break; }
        closeBrowser();
        const ok = await svgEditor.openPrep();
        if (!ok) { return; }
        svgCanvas.clear();
        svgCanvas.setSvgString(response);
        // updateCanvas();
        break;
      }
      }
    }

    // Receive `postMessage` data
    window.addEventListener('message', onMessage, true);

    const insertAfter = (referenceNode, newNode) => {
      referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
    };

    const toggleMultiLoop = () => {
      multiArr.forEach(function(item, i){
        const type = item[0];
        const data = item[1];
        if (type === 'svg') {
          svgCanvas.importSvgString(data);
        } else {
          importImage(data);
        }
        svgCanvas.moveSelectedElements(i * 20, i * 20, false);
      });
      while (preview.firstChild)
        preview.removeChild(preview.firstChild);
      multiArr = [];
      $id("imgbrowse_holder").style.display = 'none';
    };

    /**
    * @param {boolean} show
    * @returns {void}
    */
    const toggleMulti = (show) => {
      $id('lib_framewrap').style.right = (show ? 200 : 10);
      $id('imglib_opts').style.right = (show ? 200 : 10);
      if (!preview) {
        preview = document.createElement('div');
        preview.setAttribute('id', 'imglib_preview');
        // eslint-disable-next-line max-len
        preview.setAttribute('style', `position: absolute;top: 45px;right: 10px;width: 180px;bottom: 45px;background: #fff;overflow: auto;`);
        insertAfter($id('lib_framewrap'), preview);

        submit = document.createElement('button');
        submit.setAttribute('content', 'Import selected');
        submit.setAttribute('disabled', true);
        submit.textContent = 'Import selected';
        submit.setAttribute('style', `position: absolute;bottom: 10px;right: -10px;`);
        $id('imgbrowse').appendChild(submit);
        submit.addEventListener('click', toggleMultiLoop);
        submit.addEventListener('touchend', toggleMultiLoop);
      }
      submit.style.display = (show) ? 'block' : 'none';
      preview.style.display = (show) ? 'block' : 'none';

    };

    /**
    *
    * @returns {void}
    */
    const showBrowser = () => {
      let browser = $id('imgbrowse');
      if (!browser) {
        const div = document.createElement('div');
        div.id = 'imgbrowse_holder';
        div.innerHTML = '<div id=imgbrowse class=toolbar_button></div>';
        insertAfter($svgEditor, div);
        browser = $id('imgbrowse');

        const allLibs = svgEditor.i18next.t(`${name}:select_lib`);

        const divFrameWrap = document.createElement('div');
        divFrameWrap.id = 'lib_framewrap';

        const libOpts = document.createElement('ul');
        libOpts.id = 'imglib_opts';
        browser.append(libOpts);
        const frame = document.createElement('iframe');
        frame.src = "javascript:0";
        frame.style.display = 'none';
        divFrameWrap.append(frame);
        browser.prepend(divFrameWrap);

        const header = document.createElement('h1');
        browser.prepend(header);
        header.textContent = allLibs;
        header.setAttribute('style', `position: absolute;top: 0px;left: 0px;width: 100%;`);

        const button = document.createElement('button');
        // eslint-disable-next-line max-len
        button.innerHTML = svgEditor.i18next.t('common.cancel');
        browser.appendChild(button);
        button.addEventListener('click', function () {
          $id("imgbrowse_holder").style.display = 'none';
        });
        button.addEventListener('touchend', function () {
          $id("imgbrowse_holder").style.display = 'none';
        });
        button.setAttribute('style', `position: absolute;top: 5px;right: 10px;`);

        const leftBlock = document.createElement('span');
        leftBlock.setAttribute('style', `position: absolute;top: 5px;left: 10px;display: inline-flex;`);
        browser.appendChild(leftBlock);

        const back = document.createElement('button');
        back.style.visibility = "hidden";
        // eslint-disable-next-line max-len
        back.innerHTML = `<img class="svg_icon" src="${imgPath}/library.svg" alt="icon" width="16" height="16" />` + svgEditor.i18next.t(`${name}:show_list`);
        leftBlock.appendChild(back);
        back.addEventListener('click', function () {
          frame.setAttribute('src', 'about:blank');
          frame.style.display = 'none';
          libOpts.style.display = 'block';
          header.textContent = allLibs;
          back.style.display = 'none';
        });
        back.addEventListener('touchend', function () {
          frame.setAttribute('src', 'about:blank');
          frame.style.display = 'none';
          libOpts.style.display = 'block';
          header.textContent = allLibs;
          back.style.display = 'none';
        });
        back.setAttribute('style', `margin-right: 5px;`);
        back.style.display = 'none';

        const select = document.createElement('select');
        select.innerHTML = '<select><option value=s>' +
          svgEditor.i18next.t(`${name}:import_single`) + '</option><option value=m>' +
          svgEditor.i18next.t(`${name}:import_multi`) + '</option><option value=o>' +
          svgEditor.i18next.t(`${name}:open`) + '</option>';
        leftBlock.appendChild(select);
        select.addEventListener('change', function () {
          mode = this.value;
          switch (mode) {
          case 's':
          case 'o':
            toggleMulti(false);
            break;

          case 'm':
            // Import multiple
            toggleMulti(true);
            break;
          }
        });
        select.setAttribute('style', `margin-top: 10px;`);

        imgLibs.forEach(function ({ name, url, description }) {
          const li = document.createElement('li');
          libOpts.appendChild(li);
          li.textContent = name;
          li.addEventListener('click', function () {
            frame.setAttribute('src', url);
            frame.style.display = 'block';
            header.textContent = name;
            libOpts.style.display = 'none';
            back.style.display = 'block';
          });
          li.addEventListener('touchend', function () {
            frame.setAttribute('src', url);
            frame.style.display = 'block';
            header.textContent = name;
            libOpts.style.display = 'none';
            back.style.display = 'block';
          });
          const span = document.createElement("span");
          span.textContent = description;
          li.appendChild(span);
        });
      } else {
        $id("imgbrowse_holder").style.display = 'block';
      }
    };

    return {
      svgicons: 'ext-imagelib.xml',
      callback() {
        // Add the button and its handler(s)
        const buttonTemplate = document.createElement("template");
        const key = name + `:buttons.0.title`;
        buttonTemplate.innerHTML = `
        <se-menu-item id="tool_imagelib" label="${key}" src="library.svg"></se-menu-item>
        `;
        insertAfter($id('tool_export'), buttonTemplate.content.cloneNode(true));
        $id('tool_imagelib').addEventListener("click", () => {
          showBrowser();
        });

        const style = document.createElement('style');
        style.textContent = '#imgbrowse_holder {' +
          'position: absolute;' +
          'top: 0;' +
          'left: 0;' +
          'width: 100%;' +
          'height: 100%;' +
          'background-color: rgba(0, 0, 0, .5);' +
          'z-index: 5;' +
          '}' +
          '#imgbrowse {' +
          'position: absolute;' +
          'top: 25px;' +
          'left: 25px;' +
          'right: 25px;' +
          'bottom: 25px;' +
          'min-width: 300px;' +
          'min-height: 200px;' +
          'background: #5a6162;' +
          'border: 1px outset #777;' +
          '}' +
          '#imgbrowse h1 {' +
          'font-size: 20px;' +
          'margin: .4em;' +
          'text-align: center;' +
          '}' +
          '#lib_framewrap,' +
          '#imgbrowse > ul {' +
          'position: absolute;' +
          'top: 45px;' +
          'left: 10px;' +
          'right: 10px;' +
          'bottom: 10px;' +
          'background: white;' +
          'margin: 0;' +
          'padding: 0;' +
          '}' +
          '#imgbrowse > ul {' +
          'overflow: auto;' +
          '}' +
          '#imgbrowse > div {' +
          'border: 1px solid #666;' +
          '}' +
          '#imglib_preview > div {' +
          'padding: 5px;' +
          'font-size: 12px;' +
          '}' +
          '#imglib_preview img {' +
          'display: block;' +
          'margin: 0 auto;' +
          'max-height: 100px;' +
          '}' +
          '#imgbrowse li {' +
          'list-style: none;' +
          'padding: .5em;' +
          'background: #E8E8E8;' +
          'border-bottom: 1px solid #5a6162;' +
          'line-height: 1.2em;' +
          'font-style: sans-serif;' +
          '}' +
          '#imgbrowse li > span {' +
          'color: #666;' +
          'font-size: 15px;' +
          'display: block;' +
          '}' +
          '#imgbrowse li:hover {' +
          'background: #FFC;' +
          'cursor: pointer;' +
          '}' +
          '#imgbrowse iframe {' +
          'width: 100%;' +
          'height: 100%;' +
          'border: 0;' +
          '}';
        document.head.appendChild(style);
      }
    };
  }
};