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