diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js index a559b224749..23b4e6515ba 100644 --- a/src/Autocomplete/assets/dist/controller.js +++ b/src/Autocomplete/assets/dist/controller.js @@ -1,25 +1,25 @@ import { Controller } from '@hotwired/stimulus'; import TomSelect from 'tom-select'; -/****************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ - -function __classPrivateFieldGet(receiver, state, kind, f) { - if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); - if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); - return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __classPrivateFieldGet(receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); } var _instances, _getCommonConfig, _createAutocomplete, _createAutocompleteWithHtmlContents, _createAutocompleteWithRemoteData, _stripTags, _mergeObjects, _createTomSelect, _dispatchEvent; diff --git a/src/Collection/.gitattributes b/src/Collection/.gitattributes new file mode 100644 index 00000000000..3868d83c803 --- /dev/null +++ b/src/Collection/.gitattributes @@ -0,0 +1,8 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.symfony.bundle.yaml export-ignore +/phpunit.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/Resources/assets/test export-ignore +/Resources/assets/jest.config.js export-ignore +/Tests export-ignore diff --git a/src/Collection/.gitignore b/src/Collection/.gitignore new file mode 100644 index 00000000000..859b1aa2473 --- /dev/null +++ b/src/Collection/.gitignore @@ -0,0 +1,12 @@ +/.php_cs.cache +/.php_cs +/.phpunit.result.cache +/composer.phar +/composer.lock +/phpunit.xml +/vendor/ +/Tests/app/var +/Tests/app/public/build/ +node_modules/ +package-lock.json +yarn.lock diff --git a/src/Collection/.symfony.bundle.yaml b/src/Collection/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/Collection/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/Collection/CONTRIBUTING.md b/src/Collection/CONTRIBUTING.md new file mode 100644 index 00000000000..fd849829c72 --- /dev/null +++ b/src/Collection/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +Install the test app: + + $ composer install + $ cd Tests/app + $ yarn install + $ yarn build + +Start the test app: + + $ symfony serve + +## Run tests + + $ php vendor/bin/simple-phpunit diff --git a/src/Collection/CollectionBundle.php b/src/Collection/CollectionBundle.php new file mode 100644 index 00000000000..a8bee206970 --- /dev/null +++ b/src/Collection/CollectionBundle.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Collection; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * @author Kévin Dunglas + */ +final class CollectionBundle extends Bundle +{ +} diff --git a/src/Collection/LICENSE b/src/Collection/LICENSE new file mode 100644 index 00000000000..5dcd9af2f97 --- /dev/null +++ b/src/Collection/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Kévin Dunglas + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +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. diff --git a/src/Collection/README.md b/src/Collection/README.md new file mode 100644 index 00000000000..35efa7c85f9 --- /dev/null +++ b/src/Collection/README.md @@ -0,0 +1,11 @@ +# Symfony UX Collection + +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. + +## Resources + +- [Documentation](https://symfony.com/bundles/ux-collection/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) diff --git a/src/Collection/Resources/assets/dist/controller.js b/src/Collection/Resources/assets/dist/controller.js new file mode 100644 index 00000000000..eecb6252531 --- /dev/null +++ b/src/Collection/Resources/assets/dist/controller.js @@ -0,0 +1,87 @@ +import { Controller } from '@hotwired/stimulus'; + +const DEFAULT_ITEMS_SELECTOR = ':scope > :is(div, fieldset)'; +var ButtonType; +(function (ButtonType) { + ButtonType[ButtonType["Add"] = 0] = "Add"; + ButtonType[ButtonType["Delete"] = 1] = "Delete"; +})(ButtonType || (ButtonType = {})); +class default_1 extends Controller { + connect() { + this.connectCollection(this.element); + } + connectCollection(parent) { + parent.querySelectorAll('[data-prototype]').forEach((el) => { + const collectionEl = el; + const items = this.getItems(collectionEl); + collectionEl.dataset.currentIndex = items.length.toString(); + this.addAddButton(collectionEl); + this.getItems(collectionEl).forEach((itemEl) => this.addDeleteButton(collectionEl, itemEl)); + }); + } + getItems(collectionElement) { + return collectionElement.querySelectorAll(collectionElement.dataset.itemsSelector || DEFAULT_ITEMS_SELECTOR); + } + createButton(collectionEl, buttonType) { + var _a; + const attributeName = `${ButtonType[buttonType].toLowerCase()}Button`; + const button = (_a = collectionEl.dataset[attributeName]) !== null && _a !== void 0 ? _a : this.element.dataset[attributeName]; + console.log(button); + if ('' === button) + return null; + if (undefined === button || !('content' in document.createElement('template'))) { + const button = document.createElement('button'); + button.type = 'button'; + button.textContent = ButtonType[buttonType]; + return button; + } + const buttonTemplate = document.getElementById(button); + if (!buttonTemplate) + throw new Error(`template with ID "${buttonTemplate}" not found`); + const fragment = buttonTemplate.content.cloneNode(true); + if (1 !== fragment.children.length) + throw new Error('template with ID "${buttonTemplateID}" must have exactly one child'); + return fragment.firstElementChild; + } + addItem(collectionEl) { + const currentIndex = collectionEl.dataset.currentIndex; + collectionEl.dataset.currentIndex++; + const collectionNamePattern = collectionEl.id.replace(/_/g, '(?:_|\\[|]\\[)'); + const prototype = collectionEl.dataset.prototype + .replace('__name__label__', currentIndex) + .replace(new RegExp(`(${collectionNamePattern}(?:_|]\\[))__name__`, 'g'), `$1${currentIndex}`); + const fakeEl = document.createElement('div'); + fakeEl.innerHTML = prototype; + const itemEl = fakeEl.firstElementChild; + this.connectCollection(itemEl); + this.addDeleteButton(collectionEl, itemEl); + const items = this.getItems(collectionEl); + items.length ? items[items.length - 1].insertAdjacentElement('afterend', itemEl) : collectionEl.prepend(itemEl); + } + addAddButton(collectionEl) { + const addButton = this.createButton(collectionEl, ButtonType.Add); + if (!addButton) + return; + addButton.onclick = (e) => { + e.preventDefault(); + this.addItem(collectionEl); + }; + collectionEl.appendChild(addButton); + } + addDeleteButton(collectionEl, itemEl) { + const deleteButton = this.createButton(collectionEl, ButtonType.Delete); + if (!deleteButton) + return; + deleteButton.onclick = (e) => { + e.preventDefault(); + itemEl.remove(); + }; + itemEl.appendChild(deleteButton); + } +} +default_1.values = { + addButton: '', + deleteButton: '', +}; + +export { default_1 as default }; diff --git a/src/Collection/Resources/assets/jest.config.js b/src/Collection/Resources/assets/jest.config.js new file mode 100644 index 00000000000..6358ddf2772 --- /dev/null +++ b/src/Collection/Resources/assets/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../../../jest.config.js'); diff --git a/src/Collection/Resources/assets/package.json b/src/Collection/Resources/assets/package.json new file mode 100644 index 00000000000..d777a69ba74 --- /dev/null +++ b/src/Collection/Resources/assets/package.json @@ -0,0 +1,24 @@ +{ + "name": "@symfony/ux-collection", + "description": "Support for collection embedding with Symfony Form", + "license": "MIT", + "main": "dist/controller.js", + "module": "dist/controller.js", + "version": "1.0.0", + "symfony": { + "controllers": { + "collection": { + "main": "dist/controller.js", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + } + } + }, + "peerDependencies": { + "@hotwired/stimulus": "^3.0.0" + }, + "devDependencies": { + "@hotwired/stimulus": "^3.0.0" + } +} diff --git a/src/Collection/Resources/assets/src/controller.ts b/src/Collection/Resources/assets/src/controller.ts new file mode 100644 index 00000000000..b5c3ea8d981 --- /dev/null +++ b/src/Collection/Resources/assets/src/controller.ts @@ -0,0 +1,115 @@ +import { Controller } from '@hotwired/stimulus'; + +const DEFAULT_ITEMS_SELECTOR = ':scope > :is(div, fieldset)'; + +interface CollectionDataset extends DOMStringMap { + prototype: string; + currentIndex: string; + itemsSelector?: string; + addButton?: string; + deleteButton?: string; +} + +enum ButtonType { + Add, + Delete, +} + +export default class extends Controller { + static values = { + addButton: '', + deleteButton: '', + }; + + connect() { + this.connectCollection(this.element as HTMLElement); + } + + connectCollection(parent: HTMLElement) { + parent.querySelectorAll('[data-prototype]').forEach((el) => { + const collectionEl = el as HTMLElement; + const items = this.getItems(collectionEl); + collectionEl.dataset.currentIndex = items.length.toString(); + + this.addAddButton(collectionEl); + + this.getItems(collectionEl).forEach((itemEl) => this.addDeleteButton(collectionEl, itemEl as HTMLElement)); + }); + } + + getItems(collectionElement: HTMLElement) { + return collectionElement.querySelectorAll(collectionElement.dataset.itemsSelector || DEFAULT_ITEMS_SELECTOR); + } + + createButton(collectionEl: HTMLElement, buttonType: ButtonType): HTMLElement | null { + const attributeName = `${ButtonType[buttonType].toLowerCase()}Button`; + const button = collectionEl.dataset[attributeName] ?? (this.element as HTMLElement).dataset[attributeName]; + console.log(button); + + // Button explicitly disabled through data attribute + if ('' === button) return null; + + // No data attribute provided or