From 8ec67d1ef0d6d0481c84eb7c0ca53cb070bd8d16 Mon Sep 17 00:00:00 2001 From: Ehyiah Date: Fri, 19 Apr 2024 18:52:06 +0200 Subject: [PATCH] update QuillJs to version 2.0, add new fields, update readme && clean code --- src/QuillJs/.github/PULL_REQUEST_TEMPLATE.md | 8 + .../.github/workflows/check-subtree-split.yml | 33 +++ src/QuillJs/LICENSE | 12 +- src/QuillJs/README.md | 280 +++++++++++++++++- src/QuillJs/assets/dist/blots/image.js | 33 +++ src/QuillJs/assets/dist/controller.js | 217 ++++++-------- src/QuillJs/assets/dist/imageUploader.js | 138 +++++++++ src/QuillJs/assets/package.json | 37 ++- src/QuillJs/assets/src/blots/image.ts | 30 ++ src/QuillJs/assets/src/controller.ts | 63 ++-- src/QuillJs/assets/src/imageUploader.ts | 199 +++++++++++++ src/QuillJs/composer.json | 63 ++-- src/QuillJs/phpstan.neon.dist | 8 + src/QuillJs/phpunit.xml.dist | 27 ++ .../src/DTO/Fields/BlockField/AlignField.php | 15 +- .../BlockField/BackgroundColorField.php | 29 ++ .../src/DTO/Fields/BlockField/ColorField.php | 15 +- .../DTO/Fields/BlockField/DirectionField.php | 12 +- .../src/DTO/Fields/BlockField/FontField.php | 33 +++ .../src/DTO/Fields/BlockField/HeaderField.php | 12 +- .../Fields/BlockField/HeaderGroupField.php | 15 +- .../src/DTO/Fields/BlockField/IndentField.php | 12 +- .../src/DTO/Fields/BlockField/ListField.php | 23 +- .../src/DTO/Fields/BlockField/ScriptField.php | 20 +- .../src/DTO/Fields/BlockField/SizeField.php | 17 +- .../Fields/InlineField/BlockQuoteField.php | 13 + .../src/DTO/Fields/InlineField/BoldField.php | 13 + .../src/DTO/Fields/InlineField/CleanField.php | 13 + .../DTO/Fields/InlineField/CodeBlockField.php | 13 + .../src/DTO/Fields/InlineField/CodeField.php | 13 + .../src/DTO/Fields/InlineField/EmojiField.php | 9 - .../DTO/Fields/InlineField/FormulaField.php | 13 + .../src/DTO/Fields/InlineField/ImageField.php | 13 + .../DTO/Fields/InlineField/ItalicField.php | 13 + .../src/DTO/Fields/InlineField/LinkField.php | 13 + .../DTO/Fields/InlineField/StrikeField.php | 13 + .../DTO/Fields/InlineField/UnderlineField.php | 13 + .../src/DTO/Fields/InlineField/VideoField.php | 13 + .../Interfaces/QuillBlockFieldInterface.php | 12 +- .../Fields/Interfaces/QuillGroupInterface.php | 12 +- .../Interfaces/QuillInlineFieldInterface.php | 9 - src/QuillJs/src/DTO/Options/DebugOption.php | 9 - src/QuillJs/src/DTO/Options/ThemeOption.php | 9 - src/QuillJs/src/DTO/QuillGroup.php | 84 +++++- .../DependencyInjection/QuillJsExtension.php | 17 +- src/QuillJs/src/Form/QuillAdminField.php | 21 +- src/QuillJs/src/Form/QuillType.php | 13 +- src/QuillJs/src/QuillJsBundle.php | 17 +- src/QuillJs/src/templates/form.html.twig | 39 +++ .../tests/DTO/Fields/Block/AlignFieldTest.php | 50 ++++ .../Fields/Block/BackgroundColorFieldTest.php | 32 ++ .../tests/DTO/Fields/Block/ColorFieldTest.php | 32 ++ .../DTO/Fields/Block/DirectionFieldTest.php | 24 ++ .../tests/DTO/Fields/Block/FontFieldTest.php | 45 +++ .../DTO/Fields/Block/FormulaFieldTest.php | 26 ++ .../DTO/Fields/Block/HeaderFieldTest.php | 30 ++ .../DTO/Fields/Block/HeaderGroupFieldTest.php | 30 ++ .../DTO/Fields/Block/IndentFieldTest.php | 30 ++ .../tests/DTO/Fields/Block/ListFieldTest.php | 36 +++ .../DTO/Fields/Block/ScriptFieldTest.php | 30 ++ .../tests/DTO/Fields/Block/SizeFieldTest.php | 40 +++ .../Inline/BlockQuoteInlineFieldTest.php | 23 ++ .../DTO/Fields/Inline/BoldInlineFieldTest.php | 24 ++ .../Fields/Inline/CleanInlineFieldTest.php | 24 ++ .../Fields/Inline/ItalicInlineFieldTest.php | 24 ++ src/QuillJs/tests/DTO/QuillGroupTest.php | 38 +++ src/QuillJs/tests/Form/QuillTypeTest.php | 89 ++++++ src/QuillJs/tests/Hooks/ByPassFinalHook.php | 14 + 68 files changed, 1973 insertions(+), 426 deletions(-) create mode 100644 src/QuillJs/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 src/QuillJs/.github/workflows/check-subtree-split.yml create mode 100644 src/QuillJs/assets/dist/blots/image.js create mode 100644 src/QuillJs/assets/dist/imageUploader.js create mode 100644 src/QuillJs/assets/src/blots/image.ts create mode 100644 src/QuillJs/assets/src/imageUploader.ts create mode 100644 src/QuillJs/phpstan.neon.dist create mode 100644 src/QuillJs/phpunit.xml.dist create mode 100644 src/QuillJs/src/DTO/Fields/BlockField/BackgroundColorField.php create mode 100644 src/QuillJs/src/DTO/Fields/BlockField/FontField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/BlockQuoteField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/BoldField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/CleanField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/CodeBlockField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/CodeField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/FormulaField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/ImageField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/ItalicField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/LinkField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/StrikeField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/UnderlineField.php create mode 100644 src/QuillJs/src/DTO/Fields/InlineField/VideoField.php create mode 100644 src/QuillJs/src/templates/form.html.twig create mode 100644 src/QuillJs/tests/DTO/Fields/Block/AlignFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/BackgroundColorFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/ColorFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/DirectionFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/FontFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/FormulaFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/HeaderFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/HeaderGroupFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/IndentFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/ListFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/ScriptFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Block/SizeFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Inline/BlockQuoteInlineFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Inline/BoldInlineFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Inline/CleanInlineFieldTest.php create mode 100644 src/QuillJs/tests/DTO/Fields/Inline/ItalicInlineFieldTest.php create mode 100644 src/QuillJs/tests/DTO/QuillGroupTest.php create mode 100644 src/QuillJs/tests/Form/QuillTypeTest.php create mode 100644 src/QuillJs/tests/Hooks/ByPassFinalHook.php diff --git a/src/QuillJs/.github/PULL_REQUEST_TEMPLATE.md b/src/QuillJs/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..d7b215e0321 --- /dev/null +++ b/src/QuillJs/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/ux + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/src/QuillJs/.github/workflows/check-subtree-split.yml b/src/QuillJs/.github/workflows/check-subtree-split.yml new file mode 100644 index 00000000000..9218f0d4d53 --- /dev/null +++ b/src/QuillJs/.github/workflows/check-subtree-split.yml @@ -0,0 +1,33 @@ +name: Check subtree split +on: + pull_request_target: +jobs: + close-pull-request: + runs-on: ubuntu-latest + steps: + - name: Close pull request + uses: actions/github-script@v6 + with: + script: | + if (context.repo.owner === "symfony") { + github.rest.issues.createComment({ + owner: "symfony", + repo: context.repo.repo, + issue_number: context.issue.number, + body: ` + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/ux + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! + ` + }); + github.rest.pulls.update({ + owner: "symfony", + repo: context.repo.repo, + pull_number: context.issue.number, + state: "closed" + }); + } diff --git a/src/QuillJs/LICENSE b/src/QuillJs/LICENSE index 2315af08a55..e374a5c8339 100644 --- a/src/QuillJs/LICENSE +++ b/src/QuillJs/LICENSE @@ -1,13 +1,11 @@ -MIT License - -Copyright (c) 2023 Matthieu Gostiaux +Copyright (c) 2024-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. @@ -17,5 +15,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/QuillJs/README.md b/src/QuillJs/README.md index c2420d7dc7d..f09c4ac18ee 100644 --- a/src/QuillJs/README.md +++ b/src/QuillJs/README.md @@ -1,13 +1,275 @@ -# Symfony UX QuillJs +# QuillJs Bundle for Symfony using Symfony UX -Symfony UX QuillJs integrates [QuillJs](https://quilljs.com/) into Symfony applications. +Symfony UX Bundle implementing the Quill JS Wysiwyg https://quilljs.com/ -**This repository is a READ-ONLY sub-tree split**. See -https://github.com/symfony/ux to create issues or submit pull requests. +If you need a easy to use WYSIWYG (with no complex configuration) into a symfony project this is what you need. -## Resources +* [Installation](#installation) -- [Documentation](https://symfony.com/bundles/ux-quill/current/index.html) -- [Report issues](https://github.com/symfony/ux/issues) and - [send Pull Requests](https://github.com/symfony/ux/pulls) - in the [main Symfony UX repository](https://github.com/symfony/ux) + +* [Basic Usage](#basic-usage) +* [Display Result](#display-result) + + +* [Customize quillJS with options and extra_options](#customize-options) +* [Handle images uploads](#image-upload-handling) + + +* [EasyAdmin Integration](#easyadmin-integration) +* [EasyAdmin Usage](#usage) + +## Installation +### Step 1 Require bundle +```sh + composer require symfony/ux-quill +``` +If you are using the AssetMapper Component you're done ! + +### step 2 next run (If you are using webpack encore, not needed with AssetMapper) +``` sh + yarn install --force + yarn watch +``` +OR +``` sh + npm install --force + npm run watch +``` +It's done, you can use the QuillType to build a QuillJs WYSIWYG + +You can add as many WYSIWYG fields inside same page like any normal fields. + +# Basic Usage +In a form, use QuillType. It works like a classic Type except it has more options : e.g: +```php + use Symfony\UX\QuillJs\Form\QuillType; + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('myField', QuillType::class) + ; + } +``` + +# Display result +in a twig template simply : +``` +
{{ myField|raw }}
+``` +you can of course sanitize HTML if you need to for security reason, but don't forget to configure it +to your needs as many html balise and style elements will be removed by default. +Same goes in your Form configuration +``` + 'sanitize_html' => false, + 'sanitizer' => 'my_awesome_sanitizer_config +``` + + +For the most basic this is only what you have to do. +# Customize Options +```php + use Symfony\UX\QuillJs\Form\QuillType; + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('myField', QuillType::class, [ + 'quill_extra_options' => [ + 'height' => '780px', + 'theme' => 'snow', + 'placeholder' => 'Hello Quill WYSIWYG', + ], + 'quill_options' => [ + // this is where you customize the WYSIWYG by creating one or many Groups + // you can also build your groups using a classic array but many classes are covering every Quill available Fields (see below for detailed list) + QuillGroup::build( + new BoldInlineField(), + new ItalicInlineField(), + // and many more + ), + QuillGroup::build( + new HeaderField(HeaderField::HEADER_OPTION_1), + new HeaderField(HeaderField::HEADER_OPTION_2), + // and many more + ), + // Or add all available fields at once + QuillGroup::buildWithAllFields() + ] + ]) + ; + } +``` + + +## quill_options : +This is where you will choose what elements you want to display in your WYSIWYG. +You can build an array like you would do following the QuillJs official documentation +Or use a more convenient with Autocomplete using the many Fields Object in this bundle. +``` + QuillGroup::build( + new HeaderField(HeaderField::HEADER_OPTION_1), + new HeaderField(HeaderField::HEADER_OPTION_2), + ) +``` +This example will display a h1 and h2 header options side by side +``` + QuillGroup::build( + new HeaderField(HeaderField::HEADER_OPTION_1), + new HeaderField(HeaderField::HEADER_OPTION_2), + ) + QuillGroup::build( + new BoldInlineField(), + new ItalicInlineField(), + ) +``` +This example will display a h1 and h2 header options side by side and another Group containing a Bold and an Italic fields + +You can add as many Groups as you like or just One if you don't need the WYSIWYG options to have spaces between them. + +### Fields +- Below is the list of available fields from QuillJS (https://v2.quilljs.com/docs/formats) + +| Field | Description | Available options (options are available as class constants in each Field Class) | Default option | QuillJS field name | +|:--------------------:|:------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------:|:--------------:|:------------------:| +| BoldField | mark text as bold | - | | bold | +| ColorField | Change color of the font | array of colors (default is empty array to get quillJS default value | | color | +| BackGroundColorField | change background color of the selected text | array of colors (default is empty array to get quillJS default value | | background | +| AlignField | Choose text alignment | false (left), center, right, justify | all | align | +| DirectionField | Choose text direction | rtl (right to left, this is the only option available this widget is more like a toggle) | rtl | direction | +| FontField | Choose a font | ''(sans serif) ,serif, monospace | all | font | +| HeaderGroupField | Display a list of header levels | 1, 2, 3, 4, 5, 6, false (will only display normal) | all | header | +| HeaderField | Add a H1 or H2 widget only | 1, 2 | 1 | header | +| IndentField | Add or Remove indent | +1, -1 | +1 | indent | +| ListField | Add a list | ordered, bullet, check | ordered | list | +| ScriptField | | sub, super | sub | script | +| SizeField | Change text size | small, false (normal), large, huge | all | size | +| BlockQuoteField | Quote a text | - | | blockquote | +| CleanField | Clean text styling | - | | clean | +| CodeBlockField | Add a code-block | - | | code-block | +| CodeField | Add some code | - | | code | +| FormulaField | add a formula (with [Katex](https://katex.org/)) | - | | formula | +| ImageField | Add an image (see [quill_extra_options](#image-upload-handling) for uploads options) | - | | image | +| ItalicField | mark text as italic | - | | italic | +| LinkField | Add a link to a text | - | | link | +| StrikeField | mark a text as striked | - | | strike | +| UnderlineField | mark text as underlined | - | | underline | +| VideoField | add an embed video | - | | video | + + +- Below is a list of fields not available in QuillJS but taken from community + +| Field | Description | Available options (options are available as class constants in each Field Class) | Default option | +|:----------:|:------------:|:----------------------------------------------------------------------------------:|:--------------:| +| EmojiField | Add an emoji | - | | + + + +## quill_extra_options: + +- **debug**: type:string, values: 'error', 'warn', 'log', 'info' (you can use DebugOption class to build it) +- **height**: type string, examples: 200px, 200em, default: '200px' +- **theme**: type: string, values: 'snow', 'bubble', default: 'snow' (you can use ThemeOption class to build it) +- **placeholder**: type: string +- **upload_handler**: type: array (explained below) + +### Image upload Handling +in ***ImageInlineField*** : QuillJS transform images in base64 encoded file by default to save your files. +However, you can specify a custom endpoint to handle image uploading and pass in response the entire public URL to display the image. +- currently handling : +- data sending in base64 inside a json +- OR +- in a multipart/form-data +``` + 'quill_extra_options' => [ + /// + 'upload_handler' => [ + 'type' => 'json', + // or 'type' => 'form', + 'path' => '/my-custom-endpoint/upload', + ] + ], +``` +- your endpoint must return the complete URL of the file example : +``` + https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/JavaScript-logo.png/480px-JavaScript-logo.png +``` +- in json mode data will look like this by calling $request->getContent() and ```application/json``` in content-type headers +``` + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAJYCAQAAAAUb1BXAAAABGdBTUEAALGPC/xhBQAAACyygyyioiBqFCUIKC64x..." +``` +- in form mode you will find a ```multipart/form-data``` in content-type headers and file will be present in $request->files named ```file``` +- then you can handle it like you would do with a FileType + + + +# Easyadmin Integration +- First create a quill-admin.js inside assets directory +``` + // start the Stimulus application + import './bootstrap'; +``` +## When using AssetMapper +create a new entry in importmap.php +(the key must be quill-admin as it is the name used in the built-in QuillAdminField) +``` + 'quill-admin' => [ + 'path' => './assets/quill-admin.js', + 'entrypoint' => true, + ], +``` +and i should be done. but read below + +WARNING => at the moment there seems to have an issue with easyadmin with the ->addAssetMapperEntries() function +as I can not get it work as it should be. +a quick fix is to add in your crudControllers +``` + public function configureAssets(Assets $assets): Assets + { + $assets->addAssetMapperEntry('quill-admin'); + return parent::configureAssets($assets); // TODO: Change the autogenerated stub + } +``` + +OR + +in your dashboard +``` + public function configureAssets(): Assets + { + $assets = Assets::new(); + $assets->addAssetMapperEntry('quill-admin'); + + return $assets; + } +``` + +## When using webpack +- Next create in webpack.config a new entry +(the entry name must be quill-admin as it is the name used in the built-in QuillAdminField) +``` + .addEntry('quill-admin', './assets/quill-admin.js') +``` +don't forget to recompile assets (yarn build/watch or npm equivalent). + +## EasyAdmin +Then you can use the QuillAdminField like this : +``` + QuillAdminField::new('quill') +``` + +Or add custom options like you would do with the normal type +``` + QuillAdminField::new('quill') + ->setFormTypeOptions([ + 'quill_options' => + QuillGroup::build( + new BoldInlineField(), + new ItalicInlineField(), + new HeaderField(HeaderField::HEADER_OPTION_1), + new HeaderField(HeaderField::HEADER_OPTION_2), + ) + ]) +``` diff --git a/src/QuillJs/assets/dist/blots/image.js b/src/QuillJs/assets/dist/blots/image.js new file mode 100644 index 00000000000..27ba4307692 --- /dev/null +++ b/src/QuillJs/assets/dist/blots/image.js @@ -0,0 +1,33 @@ +import Quill from 'quill'; +const InlineBlot = Quill.import('blots/block'); +class LoadingImage extends InlineBlot { + static create(src) { + const node = super.create(src); + if (src === true) return node; + const image = document.createElement('img'); + image.setAttribute('src', src); + node.appendChild(image); + return node; + } + deleteAt(index, length) { + super.deleteAt(index, length); + this.cache = {}; + } + static value(domNode) { + const { + src, + custom + } = domNode.dataset; + return { + src, + custom + }; + } +} +LoadingImage.blotName = 'imageBlot'; +LoadingImage.className = 'image-uploading'; +LoadingImage.tagName = 'span'; +Quill.register({ + 'formats/imageBlot': LoadingImage +}); +export default LoadingImage; diff --git a/src/QuillJs/assets/dist/controller.js b/src/QuillJs/assets/dist/controller.js index a3204cc368d..7a1a9965075 100644 --- a/src/QuillJs/assets/dist/controller.js +++ b/src/QuillJs/assets/dist/controller.js @@ -1,133 +1,102 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.default = void 0; -var _stimulus = require("@hotwired/stimulus"); -var _quill = _interopRequireDefault(require("quill")); -var _quillImageUploader = _interopRequireDefault(require("quill-image-uploader")); -var _axios = _interopRequireDefault(require("axios")); -require("quill-image-uploader/dist/quill.imageUploader.min.css"); -require("quill-emoji/dist/quill-emoji.css"); -var Emoji = _interopRequireWildcard(require("quill-emoji")); -var _quillBlotFormatter = _interopRequireDefault(require("quill-blot-formatter")); -var _customImage = _interopRequireDefault(require("./customImage")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } -function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } -function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } -function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } -function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } -function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } -function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -_quill.default.register('modules/imageUploader', _quillImageUploader.default); -_quill.default.register("modules/emoji", Emoji); -_quill.default.register('modules/blotFormatter', _quillBlotFormatter.default); -_quill.default.register(_customImage.default, true); -var _default = /*#__PURE__*/function (_Controller) { - _inherits(_default, _Controller); - var _super = _createSuper(_default); - function _default() { - _classCallCheck(this, _default); - return _super.apply(this, arguments); +import { Controller } from '@hotwired/stimulus'; +import Quill from 'quill'; +import axios from 'axios'; +import ImageUploader from './imageUploader.js'; +Quill.register('modules/imageUploader', ImageUploader); +import * as Emoji from 'quill2-emoji'; +import 'quill2-emoji/dist/style.css'; +Quill.register('modules/emoji', Emoji); +import QuillResizeImage from 'quill-resize-image'; +Quill.register('modules/resize', QuillResizeImage); +// allow image resize and position to be reloaded after persist +const Image = Quill.import('formats/image'); +const oldFormats = Image.formats; +Image.formats = function (domNode) { + const formats = oldFormats(domNode); + if (domNode.hasAttribute('style')) { + formats.style = domNode.getAttribute('style'); } - _createClass(_default, [{ - key: "connect", - value: function connect() { - var _this = this; - var toolbarOptionsValue = this.toolbarOptionsValue; - var options = { - debug: this.extraOptionsValue.debug, - modules: { - toolbar: toolbarOptionsValue, - "emoji-toolbar": true, - "emoji-shortname": true, - blotFormatter: { - overlay: { - style: { - border: '2px solid red' - } - } - } - }, - placeholder: this.extraOptionsValue.placeholder, - theme: this.extraOptionsValue.theme - }; - if (this.extraOptionsValue.upload_handler.path !== null && this.extraOptionsValue.upload_handler.type === 'form') { - Object.assign(options.modules, { - imageUploader: { - upload: function upload(file) { - return new Promise(function (resolve, reject) { - var formData = new FormData(); - formData.append('file', file); - _axios.default.post(_this.extraOptionsValue.upload_handler.path, formData).then(function (response) { - resolve(response.data); - }).catch(function (err) { - reject('Upload failed'); - console.log(err); - }); + return formats; +}; +Image.prototype.format = function (name, value) { + if (value) { + this.domNode.setAttribute(name, value); + } else { + this.domNode.removeAttribute(name); + } +}; +export default class _Class extends Controller { + connect() { + const toolbarOptionsValue = this.toolbarOptionsValue; + const options = { + debug: this.extraOptionsValue.debug, + modules: { + toolbar: toolbarOptionsValue, + 'emoji-toolbar': true, + resize: {} + }, + placeholder: this.extraOptionsValue.placeholder, + theme: this.extraOptionsValue.theme + }; + if (this.extraOptionsValue.upload_handler.path !== null && this.extraOptionsValue.upload_handler.type === 'form') { + Object.assign(options.modules, { + imageUploader: { + upload: file => { + return new Promise((resolve, reject) => { + const formData = new FormData(); + formData.append('file', file); + axios.post(this.extraOptionsValue.upload_handler.path, formData).then(response => { + resolve(response.data); + }).catch(err => { + reject('Upload failed'); + console.log(err); }); - } + }); } - }); - } - if (this.extraOptionsValue.upload_handler.path !== null && this.extraOptionsValue.upload_handler.type === 'json') { - Object.assign(options.modules, { - imageUploader: { - upload: function upload(file) { - return new Promise(function (resolve, reject) { - var reader = function reader(file) { - return new Promise(function (resolve) { - var fileReader = new FileReader(); - fileReader.onload = function () { - return resolve(fileReader.result); - }; - fileReader.readAsDataURL(file); - }); - }; - reader(file).then(function (result) { - return _axios.default.post(_this.extraOptionsValue.upload_handler.path, result, { - headers: { - 'Content-Type': 'application/json' - } - }).then(function (response) { - resolve(response.data); - }).catch(function (err) { - reject('Upload failed'); - console.log(err); - }); + } + }); + } + if (this.extraOptionsValue.upload_handler.path !== null && this.extraOptionsValue.upload_handler.type === 'json') { + Object.assign(options.modules, { + imageUploader: { + upload: file => { + return new Promise((resolve, reject) => { + const reader = file => { + return new Promise(resolve => { + const fileReader = new FileReader(); + fileReader.onload = () => resolve(fileReader.result); + fileReader.readAsDataURL(file); }); - }); - } + }; + reader(file).then(result => axios.post(this.extraOptionsValue.upload_handler.path, result, { + headers: { + 'Content-Type': 'application/json' + } + }).then(response => { + resolve(response.data); + }).catch(err => { + reject('Upload failed'); + console.log(err); + })); + }); } - }); - } - if (typeof this.extraOptionsValue.height === "string") { - this.editorContainerTarget.style.height = this.extraOptionsValue.height; - } - var quill = new _quill.default(this.editorContainerTarget, options); - quill.on('text-change', function () { - var quillContent = quill.root.innerHTML; - var inputContent = _this.inputTarget; - inputContent.value = quillContent; + } }); } - }]); - return _default; -}(_stimulus.Controller); -exports.default = _default; -_defineProperty(_default, "targets", ['input', 'editorContainer']); -_defineProperty(_default, "values", { + const heightDefined = this.extraOptionsValue.height; + if (null !== heightDefined) { + this.editorContainerTarget.style.height = heightDefined; + } + const quill = new Quill(this.editorContainerTarget, options); + quill.on('text-change', () => { + const quillContent = quill.root.innerHTML; + const inputContent = this.inputTarget; + inputContent.value = quillContent; + }); + } +} +_Class.targets = ['input', 'editorContainer']; +_Class.values = { toolbarOptions: { type: Array, default: [] @@ -136,4 +105,4 @@ _defineProperty(_default, "values", { type: Object, default: {} } -}); \ No newline at end of file +}; diff --git a/src/QuillJs/assets/dist/imageUploader.js b/src/QuillJs/assets/dist/imageUploader.js new file mode 100644 index 00000000000..5adce2aa2aa --- /dev/null +++ b/src/QuillJs/assets/dist/imageUploader.js @@ -0,0 +1,138 @@ +import LoadingImage from './blots/image.js'; +class ImageUploader { + constructor(quill, options) { + this.quill = quill; + this.options = options; + this.range = null; + this.placeholderDelta = null; + if (typeof this.options.upload !== 'function') console.warn('[Missing config] upload function that returns a promise is required'); + const toolbar = this.quill.getModule('toolbar'); + if (toolbar) { + toolbar.addHandler('image', this.selectLocalImage.bind(this)); + } + this.handleDrop = this.handleDrop.bind(this); + this.handlePaste = this.handlePaste.bind(this); + this.quill.root.addEventListener('drop', this.handleDrop, false); + this.quill.root.addEventListener('paste', this.handlePaste, false); + } + selectLocalImage() { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.fileHolder = document.createElement('input'); + this.fileHolder.setAttribute('type', 'file'); + this.fileHolder.setAttribute('accept', 'image/*'); + this.fileHolder.setAttribute('style', 'visibility:hidden'); + this.fileHolder.onchange = this.fileChanged.bind(this); + document.body.appendChild(this.fileHolder); + this.fileHolder.click(); + window.requestAnimationFrame(() => { + document.body.removeChild(this.fileHolder); + }); + } + handleDrop(evt) { + if (evt.dataTransfer && evt.dataTransfer.files && evt.dataTransfer.files.length) { + evt.stopPropagation(); + evt.preventDefault(); + if (document.caretRangeFromPoint) { + const selection = document.getSelection(); + const range = document.caretRangeFromPoint(evt.clientX, evt.clientY); + if (selection && range) { + selection.setBaseAndExtent(range.startContainer, range.startOffset, range.startContainer, range.startOffset); + } + } else { + const selection = document.getSelection(); + const range = document.caretPositionFromPoint(evt.clientX, evt.clientY); + if (selection && range) { + selection.setBaseAndExtent(range.offsetNode, range.offset, range.offsetNode, range.offset); + } + } + this.quill.focus(); + this.range = this.quill.getSelection(); + const file = evt.dataTransfer.files[0]; + setTimeout(() => { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.readAndUploadFile(file); + }, 0); + } + } + handlePaste(evt) { + const clipboard = evt.clipboardData || window.clipboardData; + + // IE 11 is .files other browsers are .items + if (clipboard && (clipboard.items || clipboard.files)) { + const items = clipboard.items || clipboard.files; + const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png|svg|webp)$/i; + for (let i = 0; i < items.length; i++) { + if (IMAGE_MIME_REGEX.test(items[i].type)) { + const file = items[i].getAsFile ? items[i].getAsFile() : items[i]; + if (file) { + this.quill.focus(); + this.range = this.quill.getSelection(); + evt.preventDefault(); + setTimeout(() => { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.readAndUploadFile(file); + }, 0); + } + } + } + } + } + readAndUploadFile(file) { + let isUploadReject = false; + const fileReader = new FileReader(); + fileReader.addEventListener('load', () => { + if (!isUploadReject) { + const base64ImageSrc = fileReader.result; + this.insertBase64Image(base64ImageSrc); + } + }, false); + if (file) { + fileReader.readAsDataURL(file); + } + this.options.upload(file).then(imageUrl => { + this.insertToEditor(imageUrl); + }, error => { + isUploadReject = true; + this.removeBase64Image(); + console.warn(error); + }); + } + fileChanged() { + const file = this.fileHolder.files[0]; + this.readAndUploadFile(file); + } + insertBase64Image(url) { + const range = this.range; + this.placeholderDelta = this.quill.insertEmbed(range.index, LoadingImage.blotName, "" + url, 'user'); + } + insertToEditor(url) { + const range = this.range; + const lengthToDelete = this.calculatePlaceholderInsertLength(); + + // Delete the placeholder image + this.quill.deleteText(range.index, lengthToDelete, 'user'); + // Insert the server saved image + this.quill.insertEmbed(range.index, 'image', "" + url, 'user'); + range.index++; + this.quill.setSelection(range, 'user'); + } + + // The length of the insert delta from insertBase64Image can vary depending on what part of the line the insert occurs + calculatePlaceholderInsertLength() { + return this.placeholderDelta.ops.reduce((accumulator, deltaOperation) => { + const hasBarProperty = Object.prototype.hasOwnProperty.call(deltaOperation, 'insert'); + if (hasBarProperty) accumulator++; + return accumulator; + }, 0); + } + removeBase64Image() { + const range = this.range; + const lengthToDelete = this.calculatePlaceholderInsertLength(); + this.quill.deleteText(range.index, lengthToDelete, 'user'); + } +} +window.ImageUploader = ImageUploader; +export default ImageUploader; diff --git a/src/QuillJs/assets/package.json b/src/QuillJs/assets/package.json index 5abba8af465..bb6c3482fed 100644 --- a/src/QuillJs/assets/package.json +++ b/src/QuillJs/assets/package.json @@ -1,7 +1,7 @@ { "name": "@symfony/ux-quill", "description": "Symfony bundle to use Quill JS text editor", - "version": "0.0.5", + "version": "2.0.0", "license": "MIT", "main": "dist/controller.js", "symfony": { @@ -16,6 +16,17 @@ "quill/dist/quill.bubble.css": false } } + }, + "importmap": { + "@hotwired/stimulus": "^3.0.0", + "file-loader": "^6.2.0", + "quill": "2.0.0", + "quill/dist/quill.snow.css": "2.0.0", + "quill/dist/quill.bubble.css": "2.0.0", + "axios": "^1.4.0", + "quill2-emoji": "^0.1.2", + "quill2-emoji/dist/style.css": "^0.1.2", + "quill-resize-image": "^1.0.4" } }, "scripts": { @@ -23,22 +34,28 @@ "dev": "encore dev", "watch": "babel src --out-dir dist --source-maps --watch --extensions '.ts,.js'", "build": "babel src --extensions .ts -d dist", - "lint": "yarn run eslint src" + "lint": "yarn run eslint src", + "lint-fix": "yarn run eslint src --fix", + "build:types": "tsc --emitDeclarationOnly" }, "devDependencies": { "@babel/cli": "^7.20.7", "@babel/core": "^7.20.12", "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", "@babel/preset-env": "^7.20.2", "@babel/preset-typescript": "^7.18.6", "@hotwired/stimulus": "^3.2.1", "@symfony/webpack-encore": "^4.0.0", + "@testing-library/dom": "^9.3.0", + "@types/jest": "^29.5.2", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "core-js": "^3.30.2", "eslint": "^8.1.0", "eslint-config-prettier": "^8.0.0", "eslint-plugin-jest": "^25.2.2", + "quill2-emoji": "^0.1.2", "ts-loader": "^9.0.0", "typescript": "^4.9.5", "webpack": "^5.74.0", @@ -47,17 +64,19 @@ }, "peerDependencies": { "@hotwired/stimulus": "^3.0.0", - "slugify": "^1.6.5" + "axios": "^1.4.0", + "file-loader": "^6.2.0", + "quill": "2.0.0" }, "dependencies": { - "@testing-library/dom": "^9.3.0", - "@types/jest": "^29.5.2", "axios": "^1.4.0", + "eventemitter3": "^5.0.1", "file-loader": "^6.2.0", - "quill": "^1.3.7", - "quill-blot-formatter": "^1.0.5", - "quill-emoji": "^0.2.0", - "quill-image-uploader": "^1.3.0" + "lodash-es": "^4.17.21", + "quill": "2.0.0", + "quill-delta": "^5.1.0", + "quill-resize-image": "^1.0.4", + "quill2-emoji": "^0.1.2" }, "eslintConfig": { "root": true, diff --git a/src/QuillJs/assets/src/blots/image.ts b/src/QuillJs/assets/src/blots/image.ts new file mode 100644 index 00000000000..278641d7719 --- /dev/null +++ b/src/QuillJs/assets/src/blots/image.ts @@ -0,0 +1,30 @@ +import Quill from 'quill'; + +const InlineBlot = Quill.import('blots/block'); + +class LoadingImage extends InlineBlot { + static create(src) { + const node = super.create(src); + if (src === true) return node; + + const image = document.createElement('img'); + image.setAttribute('src', src); + node.appendChild(image); + return node; + } + deleteAt(index, length) { + super.deleteAt(index, length); + this.cache = {}; + } + static value(domNode) { + const { src, custom } = domNode.dataset; + return { src, custom }; + } +} + +LoadingImage.blotName = 'imageBlot'; +LoadingImage.className = 'image-uploading'; +LoadingImage.tagName = 'span'; +Quill.register({ 'formats/imageBlot': LoadingImage }); + +export default LoadingImage; diff --git a/src/QuillJs/assets/src/controller.ts b/src/QuillJs/assets/src/controller.ts index 756ad3e6ff7..95ce481e101 100644 --- a/src/QuillJs/assets/src/controller.ts +++ b/src/QuillJs/assets/src/controller.ts @@ -1,19 +1,36 @@ import { Controller } from '@hotwired/stimulus'; import Quill from 'quill'; +import * as Options from 'quill/core/quill'; -import ImageUploader from 'quill-image-uploader'; import axios from 'axios'; -import 'quill-image-uploader/dist/quill.imageUploader.min.css'; + +import ImageUploader from './imageUploader.js' Quill.register('modules/imageUploader', ImageUploader); -import "quill-emoji/dist/quill-emoji.css"; -import * as Emoji from "quill-emoji"; -Quill.register("modules/emoji", Emoji); +import * as Emoji from 'quill2-emoji'; +import 'quill2-emoji/dist/style.css'; +Quill.register('modules/emoji', Emoji); -import BlotFormatter from 'quill-blot-formatter' -Quill.register('modules/blotFormatter', BlotFormatter); -import ImageFormat from "./customImage"; -Quill.register(ImageFormat, true); +import QuillResizeImage from 'quill-resize-image'; +Quill.register('modules/resize', QuillResizeImage); +// allow image resize and position to be reloaded after persist +const Image = Quill.import('formats/image'); +const oldFormats = Image.formats; +Image.formats = function (domNode) { + const formats = oldFormats(domNode); + if (domNode.hasAttribute('style')) { + formats.style = domNode.getAttribute('style'); + } + return formats; +} + +Image.prototype.format = function (name, value) { + if (value) { + this.domNode.setAttribute(name, value); + } else { + this.domNode.removeAttribute(name); + } +} type ExtraOptions = { theme: string; @@ -24,16 +41,16 @@ type ExtraOptions = { } type uploadOptions = { type: string; - path; string + path: string; } export default class extends Controller { - readonly inputTarget: HTMLInputElement; - readonly editorContainerTarget: HTMLDivElement; + declare readonly inputTarget: HTMLInputElement; + declare readonly editorContainerTarget: HTMLDivElement; static targets = ['input', 'editorContainer']; - readonly extraOptionsValue: ExtraOptions; - readonly toolbarOptionsValue: HTMLDivElement; + declare readonly extraOptionsValue: ExtraOptions; + declare readonly toolbarOptionsValue: HTMLDivElement; static values = { toolbarOptions: { type: Array, @@ -48,19 +65,12 @@ export default class extends Controller { connect() { const toolbarOptionsValue = this.toolbarOptionsValue; - const options = { + const options: Options = { debug: this.extraOptionsValue.debug, modules: { toolbar: toolbarOptionsValue, - "emoji-toolbar": true, - "emoji-shortname": true, - blotFormatter: { - overlay: { - style: { - border: '2px solid red', - } - } - } + 'emoji-toolbar': true, + resize: {}, }, placeholder: this.extraOptionsValue.placeholder, theme: this.extraOptionsValue.theme, @@ -123,8 +133,9 @@ export default class extends Controller { ) } - if (typeof this.extraOptionsValue.height === "string") { - this.editorContainerTarget.style.height = this.extraOptionsValue.height + const heightDefined = this.extraOptionsValue.height; + if (null !== heightDefined) { + this.editorContainerTarget.style.height = heightDefined } const quill = new Quill(this.editorContainerTarget, options); diff --git a/src/QuillJs/assets/src/imageUploader.ts b/src/QuillJs/assets/src/imageUploader.ts new file mode 100644 index 00000000000..c7dac85e61a --- /dev/null +++ b/src/QuillJs/assets/src/imageUploader.ts @@ -0,0 +1,199 @@ +import LoadingImage from './blots/image.js'; + +class ImageUploader { + constructor(quill, options) { + this.quill = quill; + this.options = options; + this.range = null; + this.placeholderDelta = null; + + if (typeof this.options.upload !== 'function') + console.warn( + '[Missing config] upload function that returns a promise is required' + ); + + const toolbar = this.quill.getModule('toolbar'); + if (toolbar) { + toolbar.addHandler('image', this.selectLocalImage.bind(this)); + } + + this.handleDrop = this.handleDrop.bind(this); + this.handlePaste = this.handlePaste.bind(this); + + this.quill.root.addEventListener('drop', this.handleDrop, false); + this.quill.root.addEventListener('paste', this.handlePaste, false); + } + + selectLocalImage() { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.fileHolder = document.createElement('input'); + this.fileHolder.setAttribute('type', 'file'); + this.fileHolder.setAttribute('accept', 'image/*'); + this.fileHolder.setAttribute('style', 'visibility:hidden'); + + this.fileHolder.onchange = this.fileChanged.bind(this); + + document.body.appendChild(this.fileHolder); + + this.fileHolder.click(); + + window.requestAnimationFrame(() => { + document.body.removeChild(this.fileHolder); + }); + } + + handleDrop(evt) { + if ( + evt.dataTransfer && + evt.dataTransfer.files && + evt.dataTransfer.files.length + ) { + evt.stopPropagation(); + evt.preventDefault(); + if (document.caretRangeFromPoint) { + const selection = document.getSelection(); + const range = document.caretRangeFromPoint(evt.clientX, evt.clientY); + if (selection && range) { + selection.setBaseAndExtent( + range.startContainer, + range.startOffset, + range.startContainer, + range.startOffset + ); + } + } else { + const selection = document.getSelection(); + const range = document.caretPositionFromPoint(evt.clientX, evt.clientY); + if (selection && range) { + selection.setBaseAndExtent( + range.offsetNode, + range.offset, + range.offsetNode, + range.offset + ); + } + } + + this.quill.focus(); + this.range = this.quill.getSelection(); + const file = evt.dataTransfer.files[0]; + + setTimeout(() => { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.readAndUploadFile(file); + }, 0); + } + } + + handlePaste(evt) { + const clipboard = evt.clipboardData || window.clipboardData; + + // IE 11 is .files other browsers are .items + if (clipboard && (clipboard.items || clipboard.files)) { + const items = clipboard.items || clipboard.files; + const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png|svg|webp)$/i; + + for (let i = 0; i < items.length; i++) { + if (IMAGE_MIME_REGEX.test(items[i].type)) { + const file = items[i].getAsFile ? items[i].getAsFile() : items[i]; + + if (file) { + this.quill.focus(); + this.range = this.quill.getSelection(); + evt.preventDefault(); + setTimeout(() => { + this.quill.focus(); + this.range = this.quill.getSelection(); + this.readAndUploadFile(file); + }, 0); + } + } + } + } + } + + readAndUploadFile(file) { + let isUploadReject = false; + + const fileReader = new FileReader(); + + fileReader.addEventListener( + 'load', + () => { + if (!isUploadReject) { + const base64ImageSrc = fileReader.result; + this.insertBase64Image(base64ImageSrc); + } + }, + false + ); + + if (file) { + fileReader.readAsDataURL(file); + } + + this.options.upload(file).then( + (imageUrl) => { + this.insertToEditor(imageUrl); + }, + (error) => { + isUploadReject = true; + this.removeBase64Image(); + console.warn(error); + } + ); + } + + fileChanged() { + const file = this.fileHolder.files[0]; + this.readAndUploadFile(file); + } + + insertBase64Image(url) { + const range = this.range; + + this.placeholderDelta = this.quill.insertEmbed( + range.index, + LoadingImage.blotName, + `${url}`, + 'user' + ); + } + + insertToEditor(url) { + const range = this.range; + + const lengthToDelete = this.calculatePlaceholderInsertLength(); + + // Delete the placeholder image + this.quill.deleteText(range.index, lengthToDelete, 'user'); + // Insert the server saved image + this.quill.insertEmbed(range.index, 'image', `${url}`, 'user'); + + range.index++; + this.quill.setSelection(range, 'user'); + } + + // The length of the insert delta from insertBase64Image can vary depending on what part of the line the insert occurs + calculatePlaceholderInsertLength() { + return this.placeholderDelta.ops.reduce((accumulator, deltaOperation) => { + const hasBarProperty = Object.prototype.hasOwnProperty.call(deltaOperation, 'insert'); + if (hasBarProperty) + accumulator++; + + return accumulator; + }, 0); + } + + removeBase64Image() { + const range = this.range; + const lengthToDelete = this.calculatePlaceholderInsertLength(); + + this.quill.deleteText(range.index, lengthToDelete, 'user'); + } +} + +window.ImageUploader = ImageUploader; +export default ImageUploader; diff --git a/src/QuillJs/composer.json b/src/QuillJs/composer.json index d8e4f896587..a7d5903559a 100644 --- a/src/QuillJs/composer.json +++ b/src/QuillJs/composer.json @@ -1,41 +1,41 @@ { - "name": "symfony/ux-quill", "type": "symfony-bundle", - "description": "Integration of Quill JS text editor", + "name": "symfony/ux-quill", + "description": "Quill JS wysiwyg integration for Symfony", "keywords": [ "symfony-ux" ], "homepage": "https://symfony.com", "license": "MIT", "minimum-stability": "dev", + "prefer-stable": true, "authors": [ { "name": "Matthieu Gostiaux", + "role": "Author", "email": "rei_eva@hotmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" } ], "require": { - "php": ">=8.1", + "php": ">=8.1.0", "symfony/stimulus-bundle": "^2.9.1", "twig/extra-bundle": "^2.12|^3.0", - "symfony/twig-bundle": "^5.4|^6.0", - "symfony/form": "^5.4|^6.0", - "symfony/html-sanitizer": "6.3.*" + "symfony/twig-bundle": "^6.1|^7.0", + "symfony/form": "^6.1|^7.0", + "symfony/html-sanitizer": "^6.1|^7.0" }, "require-dev": { - "symfony/console": "6.2.*", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5", "friendsofphp/php-cs-fixer": "^3.1", - "symfony/framework-bundle": "^5.4|^6.0", - "symfony/asset-mapper": "^6.3", + "symfony/browser-kit": "^6.1|^7.0", + "symfony/framework-bundle": "^6.1|^7.0", + "symfony/asset-mapper": "^6.3|^7.0", "easycorp/easyadmin-bundle": "^4.7", "phpstan/phpstan-symfony": "^1.3", - "phpstan/extension-installer": "^1.3" + "phpstan/extension-installer": "^1.3", + "friendsoftwig/twigcs": "^6.4", + "dg/bypass-finals": "^1.6" }, "autoload": { "psr-4": { @@ -47,40 +47,15 @@ "Symfony\\UX\\QuillJs\\Tests\\": "tests/" } }, - "scripts": { - "phpcsfixer": "./vendor/bin/php-cs-fixer fix", - "phpcsfixer-lint": "./vendor/bin/php-cs-fixer fix --dry-run --diff", - "twig-cs-lint": "./vendor/bin/twigcs ./templates/", - "phpstan": "./vendor/bin/phpstan --memory-limit=1G analyse", - "statham" : "cat build/statham", - "lint-twig": "./bin/console lint:twig ./templates", - "lint-config": "./bin/console lint:yaml ./config", - "lint-container": "./bin/console lint:container", - "rector": "./vendor/bin/rector process --dry-run", - "rector-nocache": "./vendor/bin/rector process --dry-run --clear-cache", - "rector-no-dry": "./vendor/bin/rector process", - "ci": [ - "@phpcsfixer-lint", - "@phpstan", - "@lint-twig", - "@twig-cs-lint", - "@lint-config", - "@lint-container", - "@statham" - ] - }, - "config": { - "allow-plugins": { - "phpstan/extension-installer": true - } - }, - "conflict": { - "symfony/flex": "<1.13" - }, "extra": { "thanks": { "name": "symfony/ux", "url": "https://github.com/symfony/ux" } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/src/QuillJs/phpstan.neon.dist b/src/QuillJs/phpstan.neon.dist new file mode 100644 index 00000000000..55b5c5503b5 --- /dev/null +++ b/src/QuillJs/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + inferPrivatePropertyTypeFromConstructor: true + checkGenericClassInNonGenericObjectType: false + level: 7 + paths: + - src + excludePaths: + - src/Kernel.php diff --git a/src/QuillJs/phpunit.xml.dist b/src/QuillJs/phpunit.xml.dist new file mode 100644 index 00000000000..3dec59980a0 --- /dev/null +++ b/src/QuillJs/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + + tests + + + + + + + diff --git a/src/QuillJs/src/DTO/Fields/BlockField/AlignField.php b/src/QuillJs/src/DTO/Fields/BlockField/AlignField.php index 2677c6964c2..7f532eee7c0 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/AlignField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/AlignField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -20,13 +11,17 @@ final class AlignField implements QuillBlockFieldInterface public const ALIGN_FIELD_OPTION_RIGHT = 'right'; public const ALIGN_FIELD_OPTION_JUSTIFY = 'justify'; + /** @var bool[]|string[] */ private array $options; - public function __construct(string|bool ...$options) + public function __construct(bool|string ...$options) { $this->options = $options; } + /** + * @return array> + */ public function getOption(): array { $array = []; diff --git a/src/QuillJs/src/DTO/Fields/BlockField/BackgroundColorField.php b/src/QuillJs/src/DTO/Fields/BlockField/BackgroundColorField.php new file mode 100644 index 00000000000..648c0181c98 --- /dev/null +++ b/src/QuillJs/src/DTO/Fields/BlockField/BackgroundColorField.php @@ -0,0 +1,29 @@ +options = $options; + } + + /** + * @return array> + */ + public function getOption(): array + { + $array = []; + $array['background'] = $this->options; + + return $array; + } +} diff --git a/src/QuillJs/src/DTO/Fields/BlockField/ColorField.php b/src/QuillJs/src/DTO/Fields/BlockField/ColorField.php index a06309781ab..ea5aac4fd12 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/ColorField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/ColorField.php @@ -1,20 +1,14 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; final class ColorField implements QuillBlockFieldInterface { + /** + * @var string[] + */ private array $options = []; public function __construct(string ...$options) @@ -22,6 +16,9 @@ public function __construct(string ...$options) $this->options = $options; } + /** + * @return array> + */ public function getOption(): array { $array = []; diff --git a/src/QuillJs/src/DTO/Fields/BlockField/DirectionField.php b/src/QuillJs/src/DTO/Fields/BlockField/DirectionField.php index 16f53e236a7..e336a667974 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/DirectionField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/DirectionField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -24,6 +15,9 @@ public function __construct(?string $option = self::DIRECTION_FIELD_OPTION_RTL) $this->options = $option; } + /** + * @return string[] + */ public function getOption(): array { $array = []; diff --git a/src/QuillJs/src/DTO/Fields/BlockField/FontField.php b/src/QuillJs/src/DTO/Fields/BlockField/FontField.php new file mode 100644 index 00000000000..5ec7e1038cc --- /dev/null +++ b/src/QuillJs/src/DTO/Fields/BlockField/FontField.php @@ -0,0 +1,33 @@ +options = $options; + } + + /** + * @return array> + */ + public function getOption(): array + { + $array = []; + $array['font'] = $this->options; + + return $array; + } +} diff --git a/src/QuillJs/src/DTO/Fields/BlockField/HeaderField.php b/src/QuillJs/src/DTO/Fields/BlockField/HeaderField.php index b1b5ae58260..1c2555bd3f5 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/HeaderField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/HeaderField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -25,6 +16,9 @@ public function __construct(?int $options = self::HEADER_OPTION_1) $this->options = $options; } + /** + * @return int[] + */ public function getOption(): array { $array = []; diff --git a/src/QuillJs/src/DTO/Fields/BlockField/HeaderGroupField.php b/src/QuillJs/src/DTO/Fields/BlockField/HeaderGroupField.php index 7e4f1f79970..8ec3ddfbb52 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/HeaderGroupField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/HeaderGroupField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -23,6 +14,9 @@ final class HeaderGroupField implements QuillBlockFieldInterface public const HEADER_OPTION_6 = 6; public const HEADER_OPTION_NORMAL = false; + /** + * @var int[] + */ private array $options; public function __construct(int ...$options) @@ -30,6 +24,9 @@ public function __construct(int ...$options) $this->options = $options; } + /** + * @return array> + */ public function getOption(): array { $array = []; diff --git a/src/QuillJs/src/DTO/Fields/BlockField/IndentField.php b/src/QuillJs/src/DTO/Fields/BlockField/IndentField.php index d478799e4d2..8c7dee57374 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/IndentField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/IndentField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -25,6 +16,9 @@ public function __construct(string $option = self::INDENT_FIELD_OPTION_PLUS) $this->option = $option; } + /** + * @return string[] + */ public function getOption(): array { $array = []; diff --git a/src/QuillJs/src/DTO/Fields/BlockField/ListField.php b/src/QuillJs/src/DTO/Fields/BlockField/ListField.php index 19b82a0700b..9ac4c076ccb 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/ListField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/ListField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -17,20 +8,22 @@ final class ListField implements QuillBlockFieldInterface { public const LIST_FIELD_OPTION_ORDERED = 'ordered'; public const LIST_FIELD_OPTION_BULLET = 'bullet'; + public const LIST_FIELD_OPTION_CHECK = 'check'; - private array $options; + private string $option; - public function __construct(string ...$options) + public function __construct(string $option = self::LIST_FIELD_OPTION_ORDERED) { - $this->options = $options; + $this->option = $option; } + /** + * @return string[] + */ public function getOption(): array { $array = []; - foreach ($this->options as $option) { - $array[] = ['list' => $option]; - } + $array['list'] = $this->option; return $array; } diff --git a/src/QuillJs/src/DTO/Fields/BlockField/ScriptField.php b/src/QuillJs/src/DTO/Fields/BlockField/ScriptField.php index a43ab14f30d..003e24aebb0 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/ScriptField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/ScriptField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -18,17 +9,20 @@ final class ScriptField implements QuillBlockFieldInterface public const SCRIPT_FIELD_OPTION_SUB = 'sub'; public const SCRIPT_FIELD_OPTION_SUPER = 'super'; - private array $options = []; + private string $option; - public function __construct(string ...$options) + public function __construct(string $option = self::SCRIPT_FIELD_OPTION_SUB) { - $this->options = $options; + $this->option = $option; } + /** + * @return array|mixed[] + */ public function getOption(): array { $array = []; - $array['script'] = $this->options; + $array['script'] = $this->option; return $array; } diff --git a/src/QuillJs/src/DTO/Fields/BlockField/SizeField.php b/src/QuillJs/src/DTO/Fields/BlockField/SizeField.php index 9dbb3ae6048..00fbf98b62f 100644 --- a/src/QuillJs/src/DTO/Fields/BlockField/SizeField.php +++ b/src/QuillJs/src/DTO/Fields/BlockField/SizeField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\BlockField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; @@ -20,13 +11,19 @@ final class SizeField implements QuillBlockFieldInterface public const SIZE_FIELD_OPTION_LARGE = 'large'; public const SIZE_FIELD_OPTION_HUGE = 'huge'; + /** + * @var bool[]|string[] + */ private array $options; - public function __construct(string|bool ...$options) + public function __construct(bool|string ...$options) { $this->options = $options; } + /** + * @return array|mixed[] + */ public function getOption(): array { $array = []; diff --git a/src/QuillJs/src/DTO/Fields/InlineField/BlockQuoteField.php b/src/QuillJs/src/DTO/Fields/InlineField/BlockQuoteField.php new file mode 100644 index 00000000000..922c4810bb2 --- /dev/null +++ b/src/QuillJs/src/DTO/Fields/InlineField/BlockQuoteField.php @@ -0,0 +1,13 @@ + - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\InlineField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillInlineFieldInterface; diff --git a/src/QuillJs/src/DTO/Fields/InlineField/FormulaField.php b/src/QuillJs/src/DTO/Fields/InlineField/FormulaField.php new file mode 100644 index 00000000000..21058399b3e --- /dev/null +++ b/src/QuillJs/src/DTO/Fields/InlineField/FormulaField.php @@ -0,0 +1,13 @@ + - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\Interfaces; interface QuillBlockFieldInterface { + /** + * @return array + */ public function getOption(): array; } diff --git a/src/QuillJs/src/DTO/Fields/Interfaces/QuillGroupInterface.php b/src/QuillJs/src/DTO/Fields/Interfaces/QuillGroupInterface.php index 9e8ed17f05d..e89230e7721 100644 --- a/src/QuillJs/src/DTO/Fields/Interfaces/QuillGroupInterface.php +++ b/src/QuillJs/src/DTO/Fields/Interfaces/QuillGroupInterface.php @@ -1,17 +1,11 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\Interfaces; interface QuillGroupInterface { + /** + * @return array + */ public static function build(QuillInlineFieldInterface ...$fields): array; } diff --git a/src/QuillJs/src/DTO/Fields/Interfaces/QuillInlineFieldInterface.php b/src/QuillJs/src/DTO/Fields/Interfaces/QuillInlineFieldInterface.php index 87491f01ee0..4b200f93474 100644 --- a/src/QuillJs/src/DTO/Fields/Interfaces/QuillInlineFieldInterface.php +++ b/src/QuillJs/src/DTO/Fields/Interfaces/QuillInlineFieldInterface.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Fields\Interfaces; interface QuillInlineFieldInterface diff --git a/src/QuillJs/src/DTO/Options/DebugOption.php b/src/QuillJs/src/DTO/Options/DebugOption.php index dd300f608b6..f604448cc6f 100644 --- a/src/QuillJs/src/DTO/Options/DebugOption.php +++ b/src/QuillJs/src/DTO/Options/DebugOption.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Options; class DebugOption diff --git a/src/QuillJs/src/DTO/Options/ThemeOption.php b/src/QuillJs/src/DTO/Options/ThemeOption.php index c6e0fb178b8..445b9dceae8 100644 --- a/src/QuillJs/src/DTO/Options/ThemeOption.php +++ b/src/QuillJs/src/DTO/Options/ThemeOption.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO\Options; final class ThemeOption diff --git a/src/QuillJs/src/DTO/QuillGroup.php b/src/QuillJs/src/DTO/QuillGroup.php index 53f1b4a096a..efb432450d3 100644 --- a/src/QuillJs/src/DTO/QuillGroup.php +++ b/src/QuillJs/src/DTO/QuillGroup.php @@ -1,23 +1,41 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DTO; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\AlignField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\BackgroundColorField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\ColorField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\DirectionField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\FontField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\HeaderField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\HeaderGroupField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\IndentField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\ListField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\ScriptField; +use Symfony\UX\QuillJs\DTO\Fields\BlockField\SizeField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\BlockQuoteField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\BoldField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\CleanField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\CodeBlockField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\CodeField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\EmojiField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\FormulaField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\ImageField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\ItalicField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\LinkField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\StrikeField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\UnderlineField; +use Symfony\UX\QuillJs\DTO\Fields\InlineField\VideoField; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillBlockFieldInterface; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillGroupInterface; use Symfony\UX\QuillJs\DTO\Fields\Interfaces\QuillInlineFieldInterface; final class QuillGroup implements QuillGroupInterface { - public static function build(QuillInlineFieldInterface|QuillBlockFieldInterface ...$fields): array + /** + * @return array, array|string> + */ + public static function build(QuillBlockFieldInterface|QuillInlineFieldInterface ...$fields): array { $array = []; foreach ($fields as $field) { @@ -33,4 +51,50 @@ public static function build(QuillInlineFieldInterface|QuillBlockFieldInterface return $array; } + + /** + * @return array, array|string> + */ + public static function buildWithAllFields(): array + { + $stylingFields = [ + new BoldField(), + new ItalicField(), + new UnderlineField(), + new StrikeField(), + new BlockQuoteField(), + new LinkField(), + new SizeField(), + new HeaderField(), + new HeaderGroupField(), + new ColorField(), + new IndentField(), + ]; + + $orgaFields = [ + new AlignField(), + new BackgroundColorField(), + new ListField(), + new ListField(ListField::LIST_FIELD_OPTION_BULLET), + new ListField(ListField::LIST_FIELD_OPTION_CHECK), + new FontField(), + new DirectionField(), + new CodeField(), + new CodeBlockField(), + new ScriptField(), + new ScriptField(ScriptField::SCRIPT_FIELD_OPTION_SUPER), + new FormulaField(), + ]; + + $otherFields = [ + new ImageField(), + new VideoField(), + new EmojiField(), + new CleanField(), + ]; + + $fields = array_merge($stylingFields, $orgaFields, $otherFields); + + return self::build(...$fields); + } } diff --git a/src/QuillJs/src/DependencyInjection/QuillJsExtension.php b/src/QuillJs/src/DependencyInjection/QuillJsExtension.php index aa328554480..bd4998b68ee 100644 --- a/src/QuillJs/src/DependencyInjection/QuillJsExtension.php +++ b/src/QuillJs/src/DependencyInjection/QuillJsExtension.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\DependencyInjection; use Symfony\Component\AssetMapper\AssetMapperInterface; @@ -21,7 +12,7 @@ class QuillJsExtension extends Extension implements PrependExtensionInterface { - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { // Register the QuillJS form theme if TwigBundle is available $bundles = $container->getParameter('kernel.bundles'); @@ -41,10 +32,10 @@ public function prepend(ContainerBuilder $container) } } - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $container - ->setDefinition('form.ux-quill', new Definition(QuillType::class)) + ->setDefinition('form.ux-quill-js', new Definition(QuillType::class)) ->addTag('form.type') ->setPublic(false) ; @@ -53,7 +44,7 @@ public function load(array $configs, ContainerBuilder $container) if (isset($bundles['EasyAdminBundle'])) { $container - ->setDefinition('form.ux-quill', new Definition(QuillAdminField::class)) + ->setDefinition('form.ux-quill-js', new Definition(QuillAdminField::class)) ->addTag('form.type_admin') ->setPublic(false) ; diff --git a/src/QuillJs/src/Form/QuillAdminField.php b/src/QuillJs/src/Form/QuillAdminField.php index b6ef56efd0d..fb162d8bf1f 100644 --- a/src/QuillJs/src/Form/QuillAdminField.php +++ b/src/QuillJs/src/Form/QuillAdminField.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\Form; use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface; @@ -23,12 +14,22 @@ class QuillAdminField implements FieldInterface */ public static function new(string $propertyName, $label = null): self { + if (!class_exists('Symfony\\Component\\AssetMapper\\AssetMapper')) { + return (new self()) + ->addFormTheme('@QuillJs/form.html.twig', '@EasyAdmin/crud/form_theme.html.twig') + ->setProperty($propertyName) + ->setLabel($label) + ->setFormType(QuillType::class) + ->addWebpackEncoreEntries('quill-admin') + ; + } + return (new self()) ->addFormTheme('@QuillJs/form.html.twig', '@EasyAdmin/crud/form_theme.html.twig') ->setProperty($propertyName) ->setLabel($label) ->setFormType(QuillType::class) - ->addWebpackEncoreEntries('quill') + ->addAssetMapperEntries('quill-admin') ; } } diff --git a/src/QuillJs/src/Form/QuillType.php b/src/QuillJs/src/Form/QuillType.php index b2d061b621c..bfcf17b4d51 100644 --- a/src/QuillJs/src/Form/QuillType.php +++ b/src/QuillJs/src/Form/QuillType.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs\Form; use Symfony\Component\Form\AbstractType; @@ -21,14 +12,14 @@ class QuillType extends AbstractType { - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['attr']['quill_options'] = json_encode($options['quill_options']); $view->vars['attr']['quill_extra_options'] = json_encode($options['quill_extra_options']); $view->vars['attr']['sanitizer'] = $options['quill_extra_options']['sanitizer']; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'sanitize_html' => false, diff --git a/src/QuillJs/src/QuillJsBundle.php b/src/QuillJs/src/QuillJsBundle.php index 8e3a1f6cbc2..c951f192f2e 100644 --- a/src/QuillJs/src/QuillJsBundle.php +++ b/src/QuillJs/src/QuillJsBundle.php @@ -1,25 +1,10 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Symfony\UX\QuillJs; use Symfony\Component\HttpKernel\Bundle\Bundle; -/** - * @final - */ +// More details on https://symfony.com/doc/current/bundles/configuration.html#using-the-abstractbundle-class class QuillJsBundle extends Bundle { - public function getPath(): string - { - return \dirname(__DIR__); - } } diff --git a/src/QuillJs/src/templates/form.html.twig b/src/QuillJs/src/templates/form.html.twig new file mode 100644 index 00000000000..2977939ad71 --- /dev/null +++ b/src/QuillJs/src/templates/form.html.twig @@ -0,0 +1,39 @@ +{% block quill_widget %} + {%- set controller_name = "symfony--ux-quill--quill" -%} + {% set data_set = "data-symfony--ux-quill--quill" %} + {% set html_sanitizer = attr['sanitizer'] %} + +
+
+ +
+ +
+
+ {% if html_sanitizer is not null %} + {{ value|raw|sanitize_html(html_sanitizer) }} + {% else %} + {{ value|raw }} + {% endif %} +
+
+
+ + {# include katex only if a formula field is present #} + {% set json_string = attr['quill_options']|json_encode %} + {% set formula_exists = 'formula' in json_string %} + {% if formula_exists %} + + + {% endif %} +{% endblock %} diff --git a/src/QuillJs/tests/DTO/Fields/Block/AlignFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/AlignFieldTest.php new file mode 100644 index 00000000000..42ecd0e6fa5 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/AlignFieldTest.php @@ -0,0 +1,50 @@ +getOption(); + + $expectedResult = [ + 'align' => [ + AlignField::ALIGN_FIELD_OPTION_LEFT, + AlignField::ALIGN_FIELD_OPTION_CENTER, + ], + ]; + + $this->assertEquals($expectedResult, $result); + } + + /** + * @covers ::getOption + */ + public function testGetOptionWithFalseValue(): void + { + $alignField = new AlignField(false); + + $result = $alignField->getOption(); + + $expectedResult = [ + 'align' => [false], + ]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/BackgroundColorFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/BackgroundColorFieldTest.php new file mode 100644 index 00000000000..d41ccc7867c --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/BackgroundColorFieldTest.php @@ -0,0 +1,32 @@ +getOption(); + + $expectedResult = [ + 'background' => [ + 'green', 'red', 'yellow', + ], + ]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/ColorFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/ColorFieldTest.php new file mode 100644 index 00000000000..1bd79dd2b61 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/ColorFieldTest.php @@ -0,0 +1,32 @@ +getOption(); + + $expectedResult = [ + 'color' => [ + 'green', 'red', 'yellow', + ], + ]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/DirectionFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/DirectionFieldTest.php new file mode 100644 index 00000000000..2a8000e5ff5 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/DirectionFieldTest.php @@ -0,0 +1,24 @@ +getOption(); + $expectedResult = ['direction' => 'rtl']; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/FontFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/FontFieldTest.php new file mode 100644 index 00000000000..686afed0cae --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/FontFieldTest.php @@ -0,0 +1,45 @@ +getOption(); + + $expectedResult = [ + 'font' => [ + ], + ]; + + $this->assertEquals($expectedResult, $result); + } + + /** + * @covers ::getOption + */ + public function testGetOptionWithOneValue(): void + { + $field = new FontField(FontField::FONT_OPTION_SERIF); + + $result = $field->getOption(); + + $expectedResult = [ + 'font' => ['serif'], + ]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/FormulaFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/FormulaFieldTest.php new file mode 100644 index 00000000000..d920a7706eb --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/FormulaFieldTest.php @@ -0,0 +1,26 @@ +getOption(); + + $expectedResult = 'formula'; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/HeaderFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/HeaderFieldTest.php new file mode 100644 index 00000000000..e1b460476b3 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/HeaderFieldTest.php @@ -0,0 +1,30 @@ +getOption(); + $expectedResult = ['header' => 1]; + + $this->assertEquals($expectedResult, $result); + + $field = new HeaderField(HeaderField::HEADER_OPTION_2); + $result = $field->getOption(); + $expectedResult = ['header' => 2]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/HeaderGroupFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/HeaderGroupFieldTest.php new file mode 100644 index 00000000000..676e8d23af0 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/HeaderGroupFieldTest.php @@ -0,0 +1,30 @@ +getOption(); + $expectedResult = ['header' => []]; + + $this->assertEquals($expectedResult, $result); + + $field = new HeaderGroupField(HeaderGroupField::HEADER_OPTION_1, HeaderGroupField::HEADER_OPTION_3); + $result = $field->getOption(); + $expectedResult = ['header' => [1, 3]]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/IndentFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/IndentFieldTest.php new file mode 100644 index 00000000000..e43b382d93b --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/IndentFieldTest.php @@ -0,0 +1,30 @@ +getOption(); + $expectedResult = ['indent' => +1]; + + $this->assertEquals($expectedResult, $result); + + $field = new IndentField(IndentField::INDENT_FIELD_OPTION_MINUS); + $result = $field->getOption(); + $expectedResult = ['indent' => -1]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/ListFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/ListFieldTest.php new file mode 100644 index 00000000000..7ead152065d --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/ListFieldTest.php @@ -0,0 +1,36 @@ +getOption(); + $expectedResult = ['list' => 'ordered']; + + $this->assertEquals($expectedResult, $result); + + $field = new ListField(ListField::LIST_FIELD_OPTION_BULLET); + $result = $field->getOption(); + $expectedResult = ['list' => 'bullet']; + + $this->assertEquals($expectedResult, $result); + + $field = new ListField(ListField::LIST_FIELD_OPTION_CHECK); + $result = $field->getOption(); + $expectedResult = ['list' => 'check']; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/ScriptFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/ScriptFieldTest.php new file mode 100644 index 00000000000..c0cf3095479 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/ScriptFieldTest.php @@ -0,0 +1,30 @@ +getOption(); + $expectedResult = ['script' => 'sub']; + + $this->assertEquals($expectedResult, $result); + + $field = new ScriptField(ScriptField::SCRIPT_FIELD_OPTION_SUPER); + $result = $field->getOption(); + $expectedResult = ['script' => 'super']; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Block/SizeFieldTest.php b/src/QuillJs/tests/DTO/Fields/Block/SizeFieldTest.php new file mode 100644 index 00000000000..ea4a3a216c9 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Block/SizeFieldTest.php @@ -0,0 +1,40 @@ +getOption(); + $expectedResult = ['size' => ['small']]; + + $this->assertEquals($expectedResult, $result); + + $field = new SizeField( + SizeField::SIZE_FIELD_OPTION_SMALL, + SizeField::SIZE_FIELD_OPTION_LARGE, + SizeField::SIZE_FIELD_OPTION_HUGE, + ); + $result = $field->getOption(); + $expectedResult = ['size' => ['small', 'large', 'huge']]; + + $this->assertEquals($expectedResult, $result); + + $field = new SizeField(SizeField::SIZE_FIELD_OPTION_NORMAL); + $result = $field->getOption(); + $expectedResult = ['size' => [false]]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Inline/BlockQuoteInlineFieldTest.php b/src/QuillJs/tests/DTO/Fields/Inline/BlockQuoteInlineFieldTest.php new file mode 100644 index 00000000000..7fcbdeb9c3f --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Inline/BlockQuoteInlineFieldTest.php @@ -0,0 +1,23 @@ +getOption(); + + $this->assertEquals('blockquote', $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Inline/BoldInlineFieldTest.php b/src/QuillJs/tests/DTO/Fields/Inline/BoldInlineFieldTest.php new file mode 100644 index 00000000000..f866129a6f0 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Inline/BoldInlineFieldTest.php @@ -0,0 +1,24 @@ +getOption(); + + $this->assertEquals('bold', $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Inline/CleanInlineFieldTest.php b/src/QuillJs/tests/DTO/Fields/Inline/CleanInlineFieldTest.php new file mode 100644 index 00000000000..9116bb85c4d --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Inline/CleanInlineFieldTest.php @@ -0,0 +1,24 @@ +getOption(); + + $this->assertEquals('clean', $result); + } +} diff --git a/src/QuillJs/tests/DTO/Fields/Inline/ItalicInlineFieldTest.php b/src/QuillJs/tests/DTO/Fields/Inline/ItalicInlineFieldTest.php new file mode 100644 index 00000000000..e1db760da99 --- /dev/null +++ b/src/QuillJs/tests/DTO/Fields/Inline/ItalicInlineFieldTest.php @@ -0,0 +1,24 @@ +getOption(); + + $this->assertEquals('italic', $result); + } +} diff --git a/src/QuillJs/tests/DTO/QuillGroupTest.php b/src/QuillJs/tests/DTO/QuillGroupTest.php new file mode 100644 index 00000000000..c6b1012d9ba --- /dev/null +++ b/src/QuillJs/tests/DTO/QuillGroupTest.php @@ -0,0 +1,38 @@ + ['green']], + ['header' => [1, 3]], + ]; + + $this->assertEquals($expectedResult, $result); + } +} diff --git a/src/QuillJs/tests/Form/QuillTypeTest.php b/src/QuillJs/tests/Form/QuillTypeTest.php new file mode 100644 index 00000000000..633e451b223 --- /dev/null +++ b/src/QuillJs/tests/Form/QuillTypeTest.php @@ -0,0 +1,89 @@ +quillType = new QuillType(); + $this->form = $this->createMock(FormInterface::class); + $this->formView = new FormView(); + } + + /** + * @covers ::buildView + */ + public function testBuildView(): void + { + $options = [ + 'quill_options' => ['bold', 'italic'], + 'quill_extra_options' => [ + 'sanitizer' => 'some_sanitizer', + ], + ]; + + $this->quillType->buildView($this->formView, $this->form, $options); + + $this->assertArrayHasKey('attr', $this->formView->vars); + $this->assertArrayHasKey('quill_options', $this->formView->vars['attr']); + $this->assertArrayHasKey('quill_extra_options', $this->formView->vars['attr']); + $this->assertArrayHasKey('sanitizer', $this->formView->vars['attr']); + + $this->assertEquals(json_encode($options['quill_options']), $this->formView->vars['attr']['quill_options']); + $this->assertEquals(json_encode($options['quill_extra_options']), $this->formView->vars['attr']['quill_extra_options']); + $this->assertEquals($options['quill_extra_options']['sanitizer'], $this->formView->vars['attr']['sanitizer']); + } + + /** + * @covers ::configureOptions + */ + public function testConfigureOptions(): void + { + $quillType = new QuillType(); + + $resolver = new OptionsResolver(); + $quillType->configureOptions($resolver); + + $this->assertTrue($resolver->hasDefault('sanitize_html')); + $this->assertTrue($resolver->hasDefault('error_bubbling')); + $this->assertTrue($resolver->hasDefault('quill_options')); + $this->assertTrue($resolver->hasDefault('quill_extra_options')); + } + + /** + * @covers ::getBlockPrefix + */ + public function testGetBlockPrefix(): void + { + $quillType = new QuillType(); + + $this->assertEquals('quill', $quillType->getBlockPrefix()); + } + + /** + * @covers ::getParent + */ + public function testGetParent(): void + { + $quillType = new QuillType(); + + $this->assertEquals(TextareaType::class, $quillType->getParent()); + } +} diff --git a/src/QuillJs/tests/Hooks/ByPassFinalHook.php b/src/QuillJs/tests/Hooks/ByPassFinalHook.php new file mode 100644 index 00000000000..87d40daa1e6 --- /dev/null +++ b/src/QuillJs/tests/Hooks/ByPassFinalHook.php @@ -0,0 +1,14 @@ +