From c4fd1e580ae84b5e83fce02ea3cbe27e04bbce03 Mon Sep 17 00:00:00 2001
From: "Eirik S. Morland" <eirik@morland.no>
Date: Wed, 14 Mar 2018 13:54:33 +0100
Subject: [PATCH] First working version

---
 image_canvas_editor_api.info.yml              |   6 +
 image_canvas_editor_api.libraries.yml         |   5 +
 image_canvas_editor_api.permissions.yml       |   5 +
 image_canvas_editor_api.routing.yml           |  15 ++
 image_canvas_editor_api.services.yml          |   4 +
 js/editor.js                                  |  42 ++++++
 src/Annotation/ImageCanvasEditor.php          |  27 ++++
 src/Controller/EditorController.php           | 127 ++++++++++++++++
 src/Plugin/EditorInterface.php                |  13 ++
 src/Plugin/EditorPluginManager.php            |  19 +++
 .../FieldWidget/ImageCanvasEditorWidget.php   | 139 ++++++++++++++++++
 11 files changed, 402 insertions(+)
 create mode 100644 image_canvas_editor_api.info.yml
 create mode 100644 image_canvas_editor_api.libraries.yml
 create mode 100644 image_canvas_editor_api.permissions.yml
 create mode 100644 image_canvas_editor_api.routing.yml
 create mode 100644 image_canvas_editor_api.services.yml
 create mode 100644 js/editor.js
 create mode 100644 src/Annotation/ImageCanvasEditor.php
 create mode 100644 src/Controller/EditorController.php
 create mode 100644 src/Plugin/EditorInterface.php
 create mode 100644 src/Plugin/EditorPluginManager.php
 create mode 100644 src/Plugin/Field/FieldWidget/ImageCanvasEditorWidget.php

diff --git a/image_canvas_editor_api.info.yml b/image_canvas_editor_api.info.yml
new file mode 100644
index 0000000..e64aa11
--- /dev/null
+++ b/image_canvas_editor_api.info.yml
@@ -0,0 +1,6 @@
+name: Image canvas editor API
+description: Exposes an API for having canvas editors for images.
+core: 8.x
+type: module
+dependencies:
+  - image
diff --git a/image_canvas_editor_api.libraries.yml b/image_canvas_editor_api.libraries.yml
new file mode 100644
index 0000000..a87adb0
--- /dev/null
+++ b/image_canvas_editor_api.libraries.yml
@@ -0,0 +1,5 @@
+editor:
+  js:
+    js/editor.js: {}
+  dependencies:
+    - core/drupalSettings
diff --git a/image_canvas_editor_api.permissions.yml b/image_canvas_editor_api.permissions.yml
new file mode 100644
index 0000000..770844c
--- /dev/null
+++ b/image_canvas_editor_api.permissions.yml
@@ -0,0 +1,5 @@
+administer image_canvas_editor_api configuration:
+  title: 'Administer image_canvas_editor_api configuration'
+
+use image canvas editors:
+  title: 'Use image canvas editors'
diff --git a/image_canvas_editor_api.routing.yml b/image_canvas_editor_api.routing.yml
new file mode 100644
index 0000000..445e8a6
--- /dev/null
+++ b/image_canvas_editor_api.routing.yml
@@ -0,0 +1,15 @@
+image_canvas_editor_api.editor:
+  path: '/image-canvas-editor/edit/{entity_type}/{bundle}/{form_mode}/{field_name}/{fid}'
+  defaults:
+    _title: 'Edit image'
+    _controller: '\Drupal\image_canvas_editor_api\Controller\EditorController::build'
+  requirements:
+    _permission: 'use image canvas editors'
+
+image_canvas_editor_api.save_image:
+  path: '/image-canvas-editor/save/{fid}'
+  defaults:
+    _title: 'Save image'
+    _controller: '\Drupal\image_canvas_editor_api\Controller\EditorController::saveImage'
+  requirements:
+    _permission: 'use image canvas editors'
diff --git a/image_canvas_editor_api.services.yml b/image_canvas_editor_api.services.yml
new file mode 100644
index 0000000..ba87696
--- /dev/null
+++ b/image_canvas_editor_api.services.yml
@@ -0,0 +1,4 @@
+services:
+  plugin.manager.image_editor_plugin:
+    class: Drupal\image_canvas_editor_api\Plugin\EditorPluginManager
+    parent: default_plugin_manager
diff --git a/js/editor.js b/js/editor.js
new file mode 100644
index 0000000..2cc6311
--- /dev/null
+++ b/js/editor.js
@@ -0,0 +1,42 @@
+;(function ($) {
+  'use strict';
+  var BUTTON_CLASS = 'image-canvas-editor-save';
+  var BUTTON_SELECTOR = '.' + BUTTON_CLASS;
+  var ATTACHED_CLASS = 'image-canvas-attached';
+  var el;
+  Drupal.behaviors.imageCanvas = {
+    setImage: function (element) {
+      el = element;
+    },
+    attach: function (context) {
+      if (!$(context).find(BUTTON_SELECTOR).length) {
+        return;
+      }
+      var $btn = $(context).find(BUTTON_SELECTOR);
+      if ($btn.hasClass(ATTACHED_CLASS)) {
+        return;
+      }
+      $btn.addClass(ATTACHED_CLASS);
+      $btn.click(function (e) {
+        console.log(e)
+        // Get image data.
+        var imgData = el.toDataURL();
+        $.ajax({
+          url: '/image-canvas-editor/save/' + drupalSettings.imageCanvasEditorApi.fid,
+          type: 'POST',
+          data: {
+            image: imgData
+          },
+          success: function (data) {
+            // todo: Seems a bit fragile.
+            $('.ui-dialog-titlebar-close').click()
+            // Also fake invalidation of the thumbnail. By setting it directly
+            // to the new full image. Which would also invalidate the cache for
+            // editing for the second time.
+            $('img[data-fid="' + data.fid + '"]').attr('src', data.url + '?cache_bust=' + Date.now());
+          }
+        });
+      })
+    }
+  }
+})(jQuery);
diff --git a/src/Annotation/ImageCanvasEditor.php b/src/Annotation/ImageCanvasEditor.php
new file mode 100644
index 0000000..ff40ce1
--- /dev/null
+++ b/src/Annotation/ImageCanvasEditor.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\image_canvas_editor_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Class ImageCanvasEditor
+ *
+ * @see \Drupal\image_canvas_editor_api\Plugin\EditorPluginManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class ImageCanvasEditor extends Plugin {
+
+  /**
+   * The plugin id.
+   */
+  public $id;
+
+  /**
+   * The label.
+   */
+  public $label;
+
+}
diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php
new file mode 100644
index 0000000..dd3dc83
--- /dev/null
+++ b/src/Controller/EditorController.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\image_canvas_editor_api\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Url;
+use Drupal\image_canvas_editor_api\Plugin\EditorPluginManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Returns responses for Image canvas editor API routes.
+ */
+class EditorController extends ControllerBase {
+
+  /**
+   * Editor plugin manager service.
+   *
+   * @var \Drupal\image_canvas_editor_api\Plugin\EditorPluginManager
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs the controller object.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_manager, EditorPluginManager $manager) {
+    $this->entityTypeManager = $entity_manager;
+    $this->pluginManager = $manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('plugin.manager.image_editor_plugin')
+    );
+  }
+
+  /**
+   * Builds the response.
+   */
+  public function build($field_name, $entity_type, $bundle, $form_mode, $fid) {
+    $form_display = $this->entityTypeManager
+      ->getStorage('entity_form_display')
+      ->load($entity_type . '.' . $bundle . '.' . $form_mode);
+    if (!$widget = $form_display->getComponent($field_name)) {
+      throw new NotFoundHttpException();
+    }
+    $editors = $this->pluginManager->getDefinitions();
+    if (empty($editors)) {
+      throw new \Exception('No image editors found');
+    }
+    // See which one to use.
+    $editor_ids = array_keys($editors);
+    $editor_id = reset($editor_ids);
+    if (!empty($widget['settings']['editor'])) {
+      $editor_id = $widget['settings']['editor'];
+    }
+    /** @var \Drupal\image_canvas_editor_api\Plugin\EditorInterface $instance */
+    $instance = $this->pluginManager->createInstance($editor_id);
+    if (!$file = $this->entityTypeManager->getStorage('file')->load($fid)) {
+      throw new NotFoundHttpException();
+    }
+    /** @var \Drupal\file\Entity\File $file */
+    $image_url = file_create_url($file->getFileUri());
+
+    $build['editor'] = $instance->renderEditor($image_url);
+    $build['save'] = [
+      '#type' => 'inline_template',
+      '#template' => '<button class="btn button image-canvas-editor-save">{{ save }}</button>',
+      '#context' => [
+        'save' => $this->t('Save'),
+      ],
+      '#attached' => [
+        'library' => [
+          'image_canvas_editor_api/editor',
+        ],
+      ],
+    ];
+    $build['#attached'] = [
+      'drupalSettings' => [
+        'imageCanvasEditorApi' => [
+          'fid' => $fid,
+        ],
+      ],
+    ];
+
+    return $build;
+  }
+
+  /**
+   * Saves an image.
+   */
+  public function saveImage($fid, Request $request) {
+    if (!$file = $this->entityTypeManager->getStorage('file')->load($fid)) {
+      throw new NotFoundHttpException();
+    }
+    /* @var \Drupal\file\Entity\File $file*/
+    if (!$image_data = $request->get('image')) {
+      throw new BadRequestHttpException('No image data specified');
+    }
+    $image_data = str_replace('data:image/png;base64,', '', $image_data);
+    $image_data = str_replace(' ', '+', $image_data);
+    $data = base64_decode($image_data);
+    // Then brute-force it into place.
+    file_put_contents($file->getFileUri(), $data);
+    // Re-save the file, so it invalidates some caches.
+    $file->save();
+    image_path_flush($file->getFileUri());
+    return new JsonResponse([
+      'fid' => $fid,
+      'url' => file_create_url($file->getFileUri()),
+    ]);
+  }
+
+}
diff --git a/src/Plugin/EditorInterface.php b/src/Plugin/EditorInterface.php
new file mode 100644
index 0000000..054cf45
--- /dev/null
+++ b/src/Plugin/EditorInterface.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Drupal\image_canvas_editor_api\Plugin;
+
+interface EditorInterface {
+
+  /**
+   * Should dictate how the editor should be rendered.
+   *
+   * @return array
+   */
+  public function renderEditor($image_url);
+}
diff --git a/src/Plugin/EditorPluginManager.php b/src/Plugin/EditorPluginManager.php
new file mode 100644
index 0000000..8031759
--- /dev/null
+++ b/src/Plugin/EditorPluginManager.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\image_canvas_editor_api\Plugin;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+class EditorPluginManager extends DefaultPluginManager {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/ImageCanvasEditor', $namespaces, $module_handler, 'Drupal\image_canvas_editor_api\Plugin\EditorInterface', 'Drupal\image_canvas_editor_api\Annotation\ImageCanvasEditor');
+    $this->alterInfo('image_canvas_editor_api_plugin_info');
+  }
+
+}
diff --git a/src/Plugin/Field/FieldWidget/ImageCanvasEditorWidget.php b/src/Plugin/Field/FieldWidget/ImageCanvasEditorWidget.php
new file mode 100644
index 0000000..72d6dd3
--- /dev/null
+++ b/src/Plugin/Field/FieldWidget/ImageCanvasEditorWidget.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\image_canvas_editor_api\Plugin\Field\FieldWidget;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Url;
+use Drupal\image\Plugin\Field\FieldWidget\ImageWidget;
+use Drupal\image_canvas_editor_api\Plugin\EditorPluginManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines the 'image_canvas_editor' field widget.
+ *
+ * @FieldWidget(
+ *   id = "image_canvas_editor",
+ *   label = @Translation("Image canvas editor"),
+ *   field_types = {"image"},
+ * )
+ */
+class ImageCanvasEditorWidget extends ImageWidget {
+
+  /**
+   * ImageCanvasEditorWidget constructor.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info, EditorPluginManager $plugin_manager) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info);
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['third_party_settings'],
+      $container->get('element_info'),
+      $container->get('plugin.manager.image_editor_plugin')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $form = parent::settingsForm($form, $form_state);
+    $editors = $this->pluginManager->getDefinitions();
+    $opts = [];
+    foreach ($editors as $editor) {
+      $opts[$editor['id']] = $editor['label'];
+    }
+    $form['editor'] = [
+      '#title' => $this->t('Editor'),
+      '#type' => 'select',
+      '#options' => $opts,
+      '#default_setting' => $this->getSetting('editor'),
+      '#required' => TRUE,
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+    // Add the part about the editor.
+    $editor_setting = $this->getSetting('editor');
+    $editors = $this->pluginManager->getDefinitions();
+    if (isset($editor_setting)) {
+      if (!empty($editors[$editor_setting])) {
+        $editor_label = $editors[$editor_setting]['label'];
+      }
+      else {
+        $editor_label = t('Broken/Missing.');
+      }
+    }
+    else {
+      // Use the first one.
+      $editor = reset($editors);
+      $editor_label = $editor['label'];
+    }
+    $summary[] = t('Editor: @editor', [
+      '@editor' => $editor_label,
+    ]);
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function process($element, FormStateInterface $form_state, $form) {
+    $element = parent::process($element, $form_state, $form);
+    if (!empty($element['#files']) && $element['#preview_image_style']) {
+      $file = reset($element['#files']);
+      $element['preview']['#attributes'] = [
+        'data-fid' => $file->id(),
+      ];
+      $field_name = $element['#field_name'];
+      // Find the form mode.
+      // @todo: This seems a bit hacky.
+      $form_mode = 'default';
+      if (in_array('inline_entity_form', $element['#array_parents'])) {
+        $inline_form = NestedArray::getValue($form, array_slice($element['#array_parents'], 0, 4));
+        $form_mode = $inline_form['#form_mode'];
+      }
+      $link = [
+        '#attached' => ['library' => ['core/drupal.ajax']],
+        '#type' => 'link',
+        '#url' => Url::fromRoute('image_canvas_editor_api.editor', [
+          'bundle' => $element['#bundle'],
+          'field_name' => $field_name,
+          'form_mode' => $form_mode,
+          'entity_type' => $element['#entity_type'],
+          'fid' => $file->id(),
+        ]),
+        '#title' => t('Edit image'),
+        '#attributes' => array(
+          'class' => array('use-ajax'),
+          'data-dialog-type' => 'dialog',
+          'data-dialog-options' => Json::encode(array(
+            'width' => 1048,
+            'height' => 1048,
+          )),
+        ),
+      ];
+      $element['edit'] = $link;
+    }
+    return $element;
+  }
+
+}
-- 
GitLab