├── .github └── workflows │ ├── deploy-snapshot.yml │ ├── release-and-deploy-release.yml │ └── verify.yml ├── .gitignore ├── LICENSE ├── README.md ├── ci ├── mvn-release.sh └── setup-git.sh ├── docs ├── component.png ├── item.png └── repo.png ├── pom.xml └── src └── main └── resources └── SLING-INF └── apps └── merkle ├── genericmultifield.json └── genericmultifield ├── clientlibs ├── css.json ├── css │ ├── css.txt │ └── genericmultifield.css ├── js.json └── js │ ├── CUI.GenericMultiField.js │ ├── GenericMultifieldDialogHandler.js │ ├── GenericMultifieldHelper.js │ ├── Namespace.js │ ├── js.txt │ └── validations.js ├── init.jsp ├── readonly └── readonly.jsp └── render.jsp /.github/workflows/deploy-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: deploy snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | deploy-snapshot: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Checkout source code 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | # Setup Java environment 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v2 20 | with: 21 | java-version: 11 22 | distribution: zulu 23 | # Run maven verify 24 | - name: Maven verify 25 | run: mvn verify --batch-mode 26 | # Publish 27 | - name: Release Maven package 28 | uses: samuelmeuli/action-maven-publish@v1 29 | with: 30 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 31 | gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} 32 | nexus_username: ${{ secrets.OSSRH_USER }} 33 | nexus_password: ${{ secrets.OSSRH_PASSWORD }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release-and-deploy-release.yml: -------------------------------------------------------------------------------- 1 | name: release and deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Checkout source code 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: '0' 19 | # Setup Java environment 20 | - name: Set up JDK 11 21 | uses: actions/setup-java@v2 22 | with: 23 | java-version: 11 24 | distribution: zulu 25 | # Install xmllint 26 | - name: Install dependencies 27 | run: sudo apt-get install libxml2-utils 28 | # Set git username and email 29 | - name: Set up Git 30 | run: | 31 | chmod +x ci/setup-git.sh 32 | ci/setup-git.sh 33 | # Release, set correct versions and create tag 34 | - name: Release (versioning/tag) 35 | run: | 36 | chmod +x ci/mvn-release.sh 37 | ci/mvn-release.sh 38 | 39 | deploy-release: 40 | 41 | needs: release 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | # Checkout source code 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | ref: 'master' 50 | # Setup Java environment 51 | - name: Set up JDK 11 52 | uses: actions/setup-java@v2 53 | with: 54 | java-version: 11 55 | distribution: zulu 56 | # Run maven verify 57 | - name: Maven verify 58 | run: mvn verify --batch-mode 59 | # Publish 60 | - name: Release Maven package 61 | uses: samuelmeuli/action-maven-publish@v1 62 | with: 63 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 64 | gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} 65 | nexus_username: ${{ secrets.OSSRH_USER }} 66 | nexus_password: ${{ secrets.OSSRH_PASSWORD }} 67 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: verify 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | - develop 8 | 9 | jobs: 10 | verify: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | # Checkout source code 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | # Setup Java environment 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: 11 23 | distribution: zulu 24 | # Run maven verify 25 | - name: Maven verify 26 | run: mvn verify --batch-mode 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specific ignore of README.md copy in site 2 | src/site/markdown/index.md 3 | 4 | # Compiled source # 5 | ################### 6 | *.class 7 | *.dll 8 | *.exe 9 | *.o 10 | *.so 11 | 12 | # Logs and databases # 13 | ###################### 14 | *.log 15 | *.sqlite 16 | 17 | # OS generated files # 18 | ###################### 19 | .DS_Store 20 | .DS_Store? 21 | ._* 22 | .Spotlight-V100 23 | .Trashes 24 | ehthumbs.db 25 | Thumbs.db 26 | 27 | # Maven # 28 | ######### 29 | target/ 30 | bin/ 31 | pom.xml.tag 32 | pom.xml.releaseBackup 33 | pom.xml.versionsBackup 34 | pom.xml.next 35 | release.properties 36 | dependency-reduced-pom.xml 37 | 38 | # Java # 39 | ######## 40 | # Mobile Tools for Java (J2ME) 41 | .mtj.tmp/ 42 | # Package Files # 43 | *.jar 44 | *.war 45 | *.ear 46 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 47 | hs_err_pid* 48 | 49 | # IntelliJ # 50 | ########### 51 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 52 | 53 | *.iml 54 | 55 | ## Directory-based project format: 56 | .idea/ 57 | .vscode/ 58 | .history/ 59 | .settings/ 60 | .classpath 61 | .project 62 | # if you remove the above rule, at least ignore the following: 63 | 64 | # User-specific stuff: 65 | # .idea/workspace.xml 66 | # .idea/tasks.xml 67 | # .idea/dictionaries 68 | 69 | # Sensitive or high-churn files: 70 | # .idea/dataSources.ids 71 | # .idea/dataSources.xml 72 | # .idea/sqlDataSources.xml 73 | # .idea/dynamic.xml 74 | # .idea/uiDesigner.xml 75 | 76 | # Gradle: 77 | # .idea/gradle.xml 78 | # .idea/libraries 79 | 80 | # Mongo Explorer plugin: 81 | # .idea/mongoSettings.xml 82 | 83 | ## File-based project format: 84 | *.ipr 85 | *.iws 86 | 87 | ## Plugin-specific files: 88 | 89 | # IntelliJ 90 | /out/ 91 | 92 | # mpeltonen/sbt-idea plugin 93 | .idea_modules/ 94 | 95 | # JIRA plugin 96 | atlassian-ide-plugin.xml 97 | 98 | # Crashlytics plugin (for Android Studio and IntelliJ) 99 | com_crashlytics_export_strings.xml 100 | crashlytics.properties 101 | crashlytics-build.properties 102 | 103 | 104 | secret/ 105 | local.* 106 | 107 | codesigning.asc 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Merkle Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generic Multifield for AEMaaCS 2 | 3 | With this project you can use a widget 4 | in [AEM as a Cloud Service](https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/release-notes/home.html) 5 | Touch UI which lets you create a generic multifield in a dialog. 6 | 7 | | System | Status | 8 | |------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 9 | | CI master | [![release and deploy](https://github.com/merkle-open/aem-generic-multifield/actions/workflows/release-and-deploy-release.yml/badge.svg?branch=master)](https://github.com/merkle-open/aem-generic-multifield/actions/workflows/release-and-deploy-release.yml) | 10 | | CI develop | [![deploy snapshot](https://github.com/merkle-open/aem-generic-multifield/actions/workflows/deploy-snapshot.yml/badge.svg?branch=develop)](https://github.com/merkle-open/aem-generic-multifield/actions/workflows/deploy-snapshot.yml) | 11 | | Dependency | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.namics.oss.aem/genericmultifield/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.namics.oss.aem/genericmultifield) | 12 | 13 | 14 | 15 | * [Generic Multifield for AEMaaCS](#generic-multifield-for-aemaacs) 16 | * [Requirements](#requirements) 17 | * [Maven Dependency](#maven-dependency) 18 | * [in AEM](#in-aem) 19 | * [Component Dialog](#component-dialog) 20 | * [Properties](#properties) 21 | * [Item-Dialog](#item-dialog) 22 | * [Repository](#repository) 23 | * [Development](#development) 24 | 25 | 26 | 27 | ## Requirements 28 | 29 | | System | Version | 30 | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 31 | | AEMaaCS | min version: [2023.12.0](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/release-notes/release-notes/2023/release-notes-2023-12-0) | 32 | 33 | ## Maven Dependency 34 | 35 | ``` 36 | 37 | com.namics.oss.aem 38 | genericmultifield 39 | 4.0.0 40 | 41 | ``` 42 | 43 | ## in AEM 44 | 45 | Since the Generic Multifield is built as an OSGi bundle, only the bundle has to be installed into your AEM instance. 46 | With the common AEM archetype it can be added within the embedded configuration of the `content-package-maven-plugin` 47 | plugin. 48 | 49 | ```xml 50 | 51 | com.day.jcr.vault 52 | content-package-maven-plugin 53 | true 54 | 55 | ... 56 | 57 | 58 | com.namics.oss.aem 59 | genericmultifield 60 | /apps/myProject/install 61 | 62 | 63 | 64 | 65 | ``` 66 | 67 | ### Component Dialog 68 | 69 | Example usage of the Generic Multifield in your component `_cq_dialog.xml` definition within AEM: 70 | 71 | ```xml 72 | 73 | 74 | 75 | ... 76 | 81 | <genericmultifield 82 | jcr:primaryType="nt:unstructured" 83 | sling:resourceType="merkle/genericmultifield" 84 | itemDialog="/your/project/path/component/item-dialog.xml" 85 | fieldLabel="Generic Multifield" 86 | fieldDescription="A list of generic multfield items" 87 | itemNameProperty="itemTitle" 88 | minElements="2" 89 | maxElements="5" 90 | required="{Boolean}true" 91 | itemStorageNode="./items"/> 92 | ... 93 | </jcr:root> 94 | ``` 95 | 96 | #### Properties 97 | 98 | | Property | Function | 99 | |----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| 100 | | **itemDialog** | Path reference to the dialog definition of a generic multifield item. | 101 | | **itemNameProperty** | Defines the value representation of a generic multifield entry within the component dialog. Must be a reference to an item dialog property. | 102 | | **minElements** | Defines the minimal amount of generic multifield entries. | 103 | | **maxElements** | Defines the maximal amount of generic multifield entries. | 104 | | **required** | If set to `{Boolean}true`, the main component dialog will not validate until at least one item hast been defined. | 105 | | **itemStorageNode** | Defines the parent node name created within the component node. Generic multifield items will be saved beneath this node <br/>(default: `items`). | 106 | 107 | ![main dialog](docs/component.png) 108 | 109 | ### Item-Dialog 110 | 111 | Example definition of the Generic Multifield item in your component's `item-dialog.xml` referenced 112 | within `<genericmultifield>` definition via property `itemDialog`: 113 | 114 | ```xml 115 | <?xml version="1.0" encoding="UTF-8"?> 116 | <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" 117 | xmlns:jcr="http://www.jcp.org/jcr/1.0" 118 | xmlns:nt="http://www.jcp.org/jcr/nt/1.0" 119 | jcr:primaryType="nt:unstructured" 120 | sling:resourceType="cq/gui/components/authoring/dialog" 121 | jcr:title="Generic Multifield Item"> 122 | <content 123 | jcr:primaryType="nt:unstructured" 124 | sling:resourceType="granite/ui/components/coral/foundation/tabs"> 125 | <items 126 | jcr:primaryType="nt:unstructured"> 127 | <tabOne 128 | jcr:primaryType="nt:unstructured" 129 | sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns" 130 | jcr:title="Tab 1" 131 | margin="{Boolean}true"> 132 | <items 133 | jcr:primaryType="nt:unstructured"> 134 | <column 135 | jcr:primaryType="nt:unstructured" 136 | sling:resourceType="granite/ui/components/coral/foundation/container"> 137 | <items 138 | jcr:primaryType="nt:unstructured"> 139 | <itemTitle 140 | jcr:primaryType="nt:unstructured" 141 | sling:resourceType="granite/ui/components/coral/foundation/form/textfield" 142 | fieldLabel="Item Title" 143 | fieldDescription="Item Title Description" 144 | required="{Boolean}true" 145 | name="./itemTitle"/> 146 | <itemText 147 | jcr:primaryType="nt:unstructured" 148 | sling:resourceType="granite/ui/components/coral/foundation/form/textarea" 149 | fieldLabel="Item Text" 150 | fieldDescription="Item Text Description" 151 | name="./itemText"/> 152 | <itemPath 153 | jcr:primaryType="nt:unstructured" 154 | sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser" 155 | fieldLabel="Item Path" 156 | fieldDescription="Item Path Description" 157 | name="./itemPath"/> 158 | </items> 159 | </column> 160 | </items> 161 | </tabOne> 162 | <tabTwo 163 | jcr:primaryType="nt:unstructured" 164 | sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns" 165 | jcr:title="Tab 2" 166 | margin="{Boolean}true"> 167 | <items 168 | jcr:primaryType="nt:unstructured"> 169 | <column 170 | jcr:primaryType="nt:unstructured" 171 | sling:resourceType="granite/ui/components/coral/foundation/container"> 172 | <items 173 | jcr:primaryType="nt:unstructured"> 174 | 175 | <!-- properties definition --> 176 | 177 | </items> 178 | </column> 179 | </items> 180 | </tabTwo> 181 | <tabThree 182 | jcr:primaryType="nt:unstructured" 183 | sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns" 184 | jcr:title="Tab 3" 185 | margin="{Boolean}true"> 186 | <items 187 | jcr:primaryType="nt:unstructured"> 188 | <column 189 | jcr:primaryType="nt:unstructured" 190 | sling:resourceType="granite/ui/components/coral/foundation/container"> 191 | <items 192 | jcr:primaryType="nt:unstructured"> 193 | 194 | <!-- properties definition --> 195 | 196 | </items> 197 | </column> 198 | </items> 199 | </tabThree> 200 | </items> 201 | </content> 202 | </jcr:root> 203 | ``` 204 | 205 | ![multifield dialog](docs/item.png) 206 | 207 | ### Repository 208 | 209 | In the repository the content is stored as follows: 210 | 211 | ![content](docs/repo.png) 212 | 213 | ## Development 214 | 215 | Build locally with Maven 216 | 217 | ``` 218 | mvn clean install -PautoInstallBundle 219 | ``` 220 | -------------------------------------------------------------------------------- /ci/mvn-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function getVersion() { 4 | xmllint --xpath '/*[local-name()="project"]/*[local-name()="version"]/text()' pom.xml 5 | } 6 | 7 | CURRENT_VERSION=$(getVersion) 8 | if [[ $CURRENT_VERSION == *-SNAPSHOT ]]; then 9 | echo "perform release" 10 | mvn versions:set -DremoveSnapshot versions:commit --no-transfer-progress 11 | NEW_VERSION=$(getVersion) 12 | 13 | echo "commit new release version" 14 | git commit -a -m "Release $NEW_VERSION: set master to new release version" 15 | 16 | echo "Update version in README.md" 17 | sed -i -e "s|<version>[0-9A-Za-z._-]\{1,\}</version>|<version>$NEW_VERSION</version>|g" README.md && rm -f README.md-e 18 | git commit -a -m "Release $NEW_VERSION: Update README.md" 19 | 20 | echo "create tag for new release" 21 | git tag -a $NEW_VERSION -m "Release $NEW_VERSION: tag release" 22 | 23 | echo "update develop version" 24 | git fetch --all 25 | git checkout develop 26 | mvn versions:set -DnextSnapshot versions:commit --no-transfer-progress 27 | NEXT_SNAPSHOT=$(getVersion) 28 | echo "commit new snapshot version" 29 | git commit -a -m "Release $NEW_VERSION: set develop to next development version $NEXT_SNAPSHOT" 30 | 31 | git push --all 32 | git push --tags 33 | fi 34 | -------------------------------------------------------------------------------- /ci/setup-git.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git config --global user.email "oss@namics.com" 4 | git config --global user.name "Namics OSS CI" 5 | -------------------------------------------------------------------------------- /docs/component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merkle-open/aem-generic-multifield/548ec8ccef4b528e84bb6b3e30abee031b82f469/docs/component.png -------------------------------------------------------------------------------- /docs/item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merkle-open/aem-generic-multifield/548ec8ccef4b528e84bb6b3e30abee031b82f469/docs/item.png -------------------------------------------------------------------------------- /docs/repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merkle-open/aem-generic-multifield/548ec8ccef4b528e84bb6b3e30abee031b82f469/docs/repo.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <project xmlns="http://maven.apache.org/POM/4.0.0" 3 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 5 | <modelVersion>4.0.0</modelVersion> 6 | <groupId>com.namics.oss.aem</groupId> 7 | <artifactId>genericmultifield</artifactId> 8 | <packaging>bundle</packaging> 9 | <version>4.0.0</version> 10 | <name>Generic Multi-field for AEMaaCS</name> 11 | <description>com.namics.oss.aem - Generic Multi-field for AEMaaCS</description> 12 | <url>https://github.com/merkle-open/aem-generic-multifield</url> 13 | 14 | <licenses> 15 | <license> 16 | <name>MIT License 2.0</name> 17 | <url>https://opensource.org/licenses/MIT</url> 18 | <distribution>repo</distribution> 19 | </license> 20 | </licenses> 21 | 22 | <developers> 23 | <developer> 24 | <name>Merkle Open Source</name> 25 | <email>opensource@merkle.com</email> 26 | <organization>Merkle DACH</organization> 27 | <organizationUrl>https://www.merkle.com/dach/de</organizationUrl> 28 | </developer> 29 | </developers> 30 | 31 | <properties> 32 | <!-- Maven Plugins --> 33 | <mvn.source.plugin.version>3.3.0</mvn.source.plugin.version> 34 | <mvn.javadoc.version>3.5.0</mvn.javadoc.version> 35 | <mvn.gpg.plugin.version>3.0.1</mvn.gpg.plugin.version> 36 | <mvn.nexus-staging.plugin.version>1.6.13</mvn.nexus-staging.plugin.version> 37 | 38 | <maven.build.timestamp.format>yyyy-MM-dd-z-HH-mm-ss</maven.build.timestamp.format> 39 | <git.build.time>${maven.build.timestamp}</git.build.time> 40 | <git.branch>unknown</git.branch> 41 | <git.commit.id>unknown</git.commit.id> 42 | <git.commit.time>unknown</git.commit.time> 43 | <java.version>11</java.version> 44 | <encoding>UTF-8</encoding> 45 | <project.build.sourceEncoding>${encoding}</project.build.sourceEncoding> 46 | <maven.deploy.skip>true</maven.deploy.skip> 47 | </properties> 48 | 49 | <repositories> 50 | <repository> 51 | <id>adobe-public-releases</id> 52 | <name>Adobe Public Repository</name> 53 | <url>https://repo.adobe.com/nexus/content/repositories/public/</url> 54 | <releases> 55 | <enabled>true</enabled> 56 | <updatePolicy>never</updatePolicy> 57 | </releases> 58 | <snapshots> 59 | <enabled>false</enabled> 60 | </snapshots> 61 | </repository> 62 | </repositories> 63 | 64 | <dependencies> 65 | <dependency> 66 | <groupId>com.adobe.aem</groupId> 67 | <artifactId>aem-sdk-api</artifactId> 68 | <version>2023.12.14697.20231215T125030Z-231200</version> 69 | <scope>provided</scope> 70 | </dependency> 71 | </dependencies> 72 | 73 | <build> 74 | <plugins> 75 | <plugin> 76 | <groupId>org.apache.felix</groupId> 77 | <artifactId>maven-bundle-plugin</artifactId> 78 | <extensions>true</extensions> 79 | <version>3.5.1</version> 80 | <configuration> 81 | <instructions> 82 | <Sling-Initial-Content> 83 | SLING-INF/apps/merkle;overwrite:=true;uninstall:=true;ignoreImportProviders:=xml;path:=/apps/merkle, 84 | </Sling-Initial-Content> 85 | </instructions> 86 | </configuration> 87 | </plugin> 88 | <plugin> 89 | <groupId>org.apache.sling</groupId> 90 | <artifactId>maven-sling-plugin</artifactId> 91 | <version>2.3.6</version> 92 | <configuration> 93 | <slingUrl>http://localhost:4502/system/console</slingUrl> 94 | <user>admin</user> 95 | <password>admin</password> 96 | </configuration> 97 | </plugin> 98 | </plugins> 99 | </build> 100 | 101 | <distributionManagement> 102 | <snapshotRepository> 103 | <id>ossrh</id> 104 | <url>https://oss.sonatype.org/content/repositories/snapshots</url> 105 | </snapshotRepository> 106 | <repository> 107 | <id>ossrh</id> 108 | <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> 109 | </repository> 110 | </distributionManagement> 111 | 112 | <scm> 113 | <url>https://github.com/merkle-open/aem-generic-multifield</url> 114 | <connection>scm:git:git@github.com:merkle-open/aem-generic-multifield</connection> 115 | <developerConnection>scm:git:git@github.com:merkle-open/aem-generic-multifield.git</developerConnection> 116 | </scm> 117 | 118 | <profiles> 119 | <profile> 120 | <id>autoInstallBundle</id> 121 | <build> 122 | <plugins> 123 | <plugin> 124 | <groupId>org.apache.sling</groupId> 125 | <artifactId>maven-sling-plugin</artifactId> 126 | <executions> 127 | <execution> 128 | <id>install-bundle</id> 129 | <goals> 130 | <goal>install</goal> 131 | </goals> 132 | </execution> 133 | </executions> 134 | </plugin> 135 | </plugins> 136 | </build> 137 | </profile> 138 | <profile> 139 | <id>deploy</id> 140 | <build> 141 | <plugins> 142 | <plugin> 143 | <groupId>org.apache.maven.plugins</groupId> 144 | <artifactId>maven-source-plugin</artifactId> 145 | <version>${mvn.source.plugin.version}</version> 146 | <executions> 147 | <execution> 148 | <id>attach-sources</id> 149 | <goals> 150 | <goal>jar-no-fork</goal> 151 | </goals> 152 | </execution> 153 | </executions> 154 | </plugin> 155 | <plugin> 156 | <groupId>org.apache.maven.plugins</groupId> 157 | <artifactId>maven-javadoc-plugin</artifactId> 158 | <version>${mvn.javadoc.version}</version> 159 | <configuration> 160 | <failOnError>false</failOnError> 161 | </configuration> 162 | <executions> 163 | <execution> 164 | <id>attach-javadocs</id> 165 | <goals> 166 | <goal>jar</goal> 167 | </goals> 168 | </execution> 169 | </executions> 170 | </plugin> 171 | <plugin> 172 | <groupId>org.apache.maven.plugins</groupId> 173 | <artifactId>maven-gpg-plugin</artifactId> 174 | <version>${mvn.gpg.plugin.version}</version> 175 | <executions> 176 | <execution> 177 | <id>sign-artifacts</id> 178 | <phase>verify</phase> 179 | <goals> 180 | <goal>sign</goal> 181 | </goals> 182 | <configuration> 183 | <!-- Prevent `gpg` from using pinentry programs --> 184 | <gpgArguments> 185 | <arg>--pinentry-mode</arg> 186 | <arg>loopback</arg> 187 | </gpgArguments> 188 | </configuration> 189 | </execution> 190 | </executions> 191 | </plugin> 192 | <plugin> 193 | <groupId>org.sonatype.plugins</groupId> 194 | <artifactId>nexus-staging-maven-plugin</artifactId> 195 | <version>${mvn.nexus-staging.plugin.version}</version> 196 | <extensions>true</extensions> 197 | <configuration> 198 | <serverId>ossrh</serverId> 199 | <nexusUrl>https://oss.sonatype.org/</nexusUrl> 200 | <autoReleaseAfterClose>true</autoReleaseAfterClose> 201 | </configuration> 202 | <executions> 203 | <execution> 204 | <id>deploy-to-sonatype</id> 205 | <phase>deploy</phase> 206 | <goals> 207 | <goal>deploy</goal> 208 | </goals> 209 | </execution> 210 | </executions> 211 | </plugin> 212 | </plugins> 213 | </build> 214 | </profile> 215 | </profiles> 216 | </project> 217 | -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield.json: -------------------------------------------------------------------------------- 1 | { 2 | "jcr:primaryType": "sling:Folder", 3 | "sling:resourceSuperType": "granite/ui/components/foundation/form/field" 4 | } -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/css.json: -------------------------------------------------------------------------------- 1 | { 2 | "jcr:primaryType": "cq:ClientLibraryFolder", 3 | "sling:resourceType": "widgets/clientlib", 4 | "categories": [ 5 | "cq.authoring.editor" 6 | ] 7 | } -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/css/css.txt: -------------------------------------------------------------------------------- 1 | genericmultifield.css -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/css/genericmultifield.css: -------------------------------------------------------------------------------- 1 | .coral-GenericMultiField { 2 | display: inline-block; 3 | -webkit-box-sizing: border-box; 4 | -moz-box-sizing: border-box; 5 | box-sizing: border-box; 6 | border: .0625rem solid #e9e9e9; 7 | padding: .5rem; 8 | vertical-align: top; 9 | } 10 | 11 | .coral-GenericMultiField[data-renderreadonly] { 12 | background: none; 13 | border: none; 14 | padding: 0; 15 | } 16 | 17 | .coral--dark .coral-GenericMultiField { 18 | border-color: #434343; 19 | background-color: #484848; 20 | } 21 | 22 | .coral-GenericMultiField-list { 23 | list-style: none; 24 | position: relative; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | .coral-SpectrumMultiField-edit, 30 | .coral-SpectrumMultiField-remove, 31 | .coral-SpectrumMultiField-move { 32 | width: 41px; 33 | height: 41px; 34 | } 35 | 36 | .coral-SpectrumMultiField-add { 37 | margin-top: 1.5rem; 38 | margin-left: 0.25rem; 39 | } 40 | 41 | .coral-GenericMultiField-label { 42 | position: relative; 43 | padding: .625rem 7rem .625rem .625rem; 44 | overflow: hidden; 45 | white-space: nowrap; 46 | text-overflow: ellipsis; 47 | } 48 | 49 | .coral-SpectrumMultiField-edit { 50 | position: absolute; 51 | top: 0; 52 | right: 2.1rem; 53 | } 54 | 55 | .coral-SpectrumMultiField-remove { 56 | position: absolute; 57 | top: 0; 58 | right: 4.2rem; 59 | } 60 | 61 | .coral-SpectrumMultiField-move { 62 | cursor: move; 63 | position: absolute; 64 | top: 0; 65 | right: 0; 66 | } 67 | 68 | .coral-GenericMultiField .coral-GenericMultiField-listEntry { 69 | border: .0625rem solid rgba(0, 0, 0, 0.15); 70 | position: relative; 71 | margin-top: .25rem; 72 | background-color: rgb(255, 255, 255); 73 | } 74 | 75 | .coral-GenericMultiField .coral-GenericMultiField-listEntry:only-child .coral-GenericMultiField-move { 76 | display: none; 77 | } 78 | 79 | .coral-GenericMultiField .coral-GenericMultiField-listEntry.is-dragging { 80 | opacity: 0.7; 81 | position: absolute; 82 | left: 0; 83 | top: 0; 84 | z-index: 10000; 85 | background-color: rgba(128, 128, 128, 0.5); 86 | } 87 | 88 | .coral-GenericMultiField .drag-after { 89 | position: relative; 90 | top: 2.625rem; 91 | } 92 | 93 | .coral-Form--vertical .coral-Form-field.coral-GenericMultiField { 94 | display: block; 95 | background-color: rgba(80, 80, 80, 0.02); 96 | } 97 | 98 | .coral-GenericMultiField-storageWarning { 99 | color: #969696; 100 | margin-top: 0.2em; 101 | margin-bottom: 0.2em; 102 | } 103 | 104 | .coral-GenericMultiField.is-invalid { 105 | border-color: #e14132; 106 | color: #e14132; 107 | border-width: 2px 108 | } 109 | 110 | .coral-GenericMultiField.is-invalid + coral-icon.coral-Form-fielderror._coral-Icon._coral-Icon--sizeS { 111 | height: 14px; 112 | width: 14px; 113 | } 114 | 115 | .coral-GenericMultiField.is-disabled { 116 | color: #bebebe; 117 | } 118 | 119 | .coral-GenericMultiField.is-disabled .coral-GenericMultiField-listEntry { 120 | background-color: #f0f0f0; 121 | } 122 | 123 | .cq-dialog-backdrop-GenericMultiField { 124 | z-index: 920; 125 | display: block; 126 | position: fixed; 127 | top: 0; 128 | right: 0; 129 | bottom: 0; 130 | left: 0; 131 | background-color: rgba(0, 0, 0, 0.3); 132 | -webkit-transition: opacity 0.35s; 133 | -moz-transition: opacity 0.35s; 134 | -o-transition: opacity 0.35s; 135 | -ms-transition: opacity 0.35s; 136 | transition: opacity 0.35s; 137 | opacity: 1; 138 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; 139 | filter: alpha(opacity=0); 140 | } 141 | 142 | /* hide the generic multifield backdrop if original backdrop is visible */ 143 | .cq-dialog-backdrop:not([style*='display:none']):not([style*='display: none']) ~ .cq-dialog-backdrop-GenericMultiField { 144 | display: none; 145 | } 146 | 147 | ._coral-Dialog-content { 148 | overflow-x: hidden !important; 149 | } -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js.json: -------------------------------------------------------------------------------- 1 | { 2 | "jcr:primaryType": "cq:ClientLibraryFolder", 3 | "sling:resourceType": "widgets/clientlib", 4 | "categories": [ 5 | "cq.authoring.editor" 6 | ] 7 | } -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/CUI.GenericMultiField.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The Merkle.GenericMultiField class represents an editable list 3 | * of form fields for editing multi value properties. 4 | */ 5 | (function ($) { 6 | "use strict"; 7 | 8 | var removeButton = "<button is=\"coral-button\" variant=\"minimal\" icon=\"delete\" size=\"S\" type=\"button\" class=\"js-coral-SpectrumMultiField-remove coral-SpectrumMultiField-remove\"></button>"; 9 | var editButton = "<button is=\"coral-button\" variant=\"minimal\" icon=\"edit\" size=\"S\" type=\"button\" class=\"js-coral-SpectrumMultiField-edit coral-SpectrumMultiField-edit\"></button>"; 10 | var moveButton = "<button is=\"coral-button\" variant=\"minimal\" icon=\"moveUpDown\" size=\"S\" type=\"button\" class=\"js-coral-SpectrumMultiField-move coral-SpectrumMultiField-move\"></button>"; 11 | 12 | /** 13 | * The Merkle.GenericMultiField class represents an editable list 14 | * of form fields for editing multi value properties. 15 | * 16 | * @extends CUI.Widget 17 | */ 18 | Merkle.GenericMultiField = new Class({ 19 | 20 | toString: 'GenericMultiField', 21 | 22 | extend: CUI.Widget, 23 | 24 | /** 25 | * Creates a new Merkle.GenericMultiField. 26 | * @constructor 27 | * @param options object containing config properties 28 | */ 29 | construct: function (options) { 30 | this.ui = $(window).adaptTo('foundation-ui'); 31 | this.ol = this.$element.children("ol"); 32 | 33 | // is needed for IE9 compatibility 34 | var opt = this.$element.get()[0]; 35 | 36 | if (!opt) { 37 | throw new Error('Controlled error thrown on purpose!'); 38 | } 39 | 40 | // get config properties 41 | this.itemDialog = (options.mergeroot || opt.getAttribute('data-mergeroot') || '/mnt/override') + (options.itemdialog || opt.getAttribute('data-itemdialog')); 42 | this.itemStorageNode = options.itemstoragenode || opt.getAttribute('data-itemstoragenode') || "items"; 43 | this.itemNameProperty = options.itemnameproperty || opt.getAttribute('data-itemnameproperty') || "jcr:title"; 44 | this.itemNameDisplayStrategy = options.itemnamedisplaystrategy || opt.getAttribute('data-itemnamedisplaystrategy'); 45 | this.minElements = options.minelements || opt.getAttribute('data-minelements'); 46 | this.maxElements = options.maxelements || opt.getAttribute('data-maxelements'); 47 | this.readOnly = options.renderreadonly || opt.getAttribute('data-renderreadonly'); 48 | 49 | // get the crx path of the current component from action attribute of the current form. 50 | this.crxPath = this.$element.parents("form").attr("action"); 51 | 52 | if (this.readOnly) { 53 | this.$element.addClass("is-disabled"); 54 | // add the "+" button for adding new items 55 | $(".coral-SpectrumMultiField-add", this.$element).attr("disabled", "disabled"); 56 | } else { 57 | this._checkAndReinitializeForSmallerScreens(); 58 | // add button listeners 59 | this._addListeners(); 60 | } 61 | // get list elements 62 | this._updateList(false); 63 | }, 64 | /** 65 | * Special handling for tablet and smaller viewports 66 | * 67 | * @private 68 | */ 69 | _checkAndReinitializeForSmallerScreens: function () { 70 | if (window.innerWidth < 1024) { 71 | $(document).one('foundation-contentloaded', function (e) { 72 | $(e.target).find('.coral-Form-field.coral-GenericMultiField').each(function () { 73 | new Merkle.GenericMultiField(); 74 | }); 75 | }); 76 | } 77 | }, 78 | /** 79 | * Performs an ajax call to the storage node and updates the list entries. 80 | * 81 | * @param {Boolean} triggerEvent if 'change' event should be triggered. 82 | * @private 83 | */ 84 | _updateList: function (triggerEvent) { 85 | var that = this; 86 | $.ajax({ 87 | type: "GET", 88 | dataType: "json", 89 | url: that.crxPath + "/" + that.itemStorageNode + ".-1.json" 90 | }).done(function (data) { 91 | that.ol.empty(); 92 | $.each(data, function (key) { 93 | if (typeof data[key] === 'object' && !Array.isArray(data[key]) && data[key] !== undefined && data[key]["jcr:primaryType"] !== undefined 94 | && data[key]["sling:resourceType"] !== "wcm/msm/components/ghost") { 95 | 96 | if (that.itemNameDisplayStrategy === "pageTitle") { 97 | //use the jcr:title from a page 98 | that._labelFromPage(key, data[key][that.itemNameProperty]); 99 | } else { 100 | var propertyValue; 101 | if (that.itemNameProperty.indexOf('/') > -1) { 102 | propertyValue = that.itemNameProperty.split('/'); 103 | var parent = data[key]; 104 | for (var i = 0; i < propertyValue.length - 1; i += 1) { 105 | parent = parent[propertyValue[i]]; 106 | } 107 | 108 | if (parent !== undefined) { 109 | propertyValue = parent[propertyValue[propertyValue.length - 1]]; 110 | } else { 111 | propertyValue = key; 112 | } 113 | 114 | } else { 115 | propertyValue = data[key][that.itemNameProperty]; 116 | } 117 | var li = that._createListEntry(key, propertyValue); 118 | li.appendTo(that.ol); 119 | } 120 | 121 | } 122 | }); 123 | // trigger change event on update of items 124 | if (triggerEvent === true) { 125 | that._triggerChangeEvent(); 126 | } 127 | }); 128 | }, 129 | 130 | /** 131 | * @private 132 | */ 133 | _labelFromPage: function (key, targetPath) { 134 | var that = this; 135 | $.ajax({ 136 | type: "GET", 137 | dataType: "json", 138 | async: false, 139 | url: targetPath + ".-1.json" 140 | }).done(function (data) { 141 | 142 | if (typeof data["jcr:content"] === 'object') { 143 | var li = that._createListEntry(key, data["jcr:content"]["jcr:title"]); 144 | li.appendTo(that.ol); 145 | } 146 | 147 | }); 148 | }, 149 | 150 | /** 151 | * Creates the markup for a single list entry. 152 | * 153 | * @param {String} key the name of the current item. 154 | * @param {String} label the label of the current item. 155 | * @private 156 | */ 157 | _createListEntry: function (key, label) { 158 | var escapedLabel = $("<div/>").text(label).html(); 159 | var labelWithKeyAsFallback = escapedLabel ? escapedLabel : key; 160 | var li = $('<li>', {id: key, title: labelWithKeyAsFallback, class: "coral-GenericMultiField-listEntry"}); 161 | var liInner = $('<div>', {text: labelWithKeyAsFallback, class: "coral-GenericMultiField-label"}); 162 | 163 | li.append(liInner); 164 | li.append($(removeButton)); 165 | li.append(editButton); 166 | li.append(moveButton); 167 | if (this.readOnly) { 168 | $(".coral-SpectrumMultiField-remove", li).attr("disabled", "disabled"); 169 | $(".coral-SpectrumMultiField-edit", li).attr("disabled", "disabled"); 170 | $(".coral-SpectrumMultiField-move", li).attr("disabled", "disabled"); 171 | } 172 | return li; 173 | }, 174 | 175 | /** 176 | * Initializes listeners. 177 | * @private 178 | */ 179 | _addListeners: function () { 180 | var that = this; 181 | 182 | this.$element.on("click", ".js-coral-SpectrumMultiField-add", function (e) { 183 | Merkle.Helper.addMarkup(Merkle.Helper.CONST.ADD_ITEM_WORKFLOW); 184 | e.preventDefault(); 185 | e.stopPropagation(); 186 | that._addNewItem(); 187 | }); 188 | 189 | this.$element.on("click", ".js-coral-SpectrumMultiField-remove", function (e) { 190 | var currentItem = $(this).closest("li"); 191 | that._removeItem(currentItem); 192 | }); 193 | 194 | this.$element.on("click", ".js-coral-SpectrumMultiField-edit", function (e) { 195 | var currentItem = $(this).closest("li"); 196 | that._editItem(currentItem); 197 | }); 198 | 199 | 200 | this.$element 201 | .fipo("taphold", "mousedown", ".js-coral-SpectrumMultiField-move", function (e) { 202 | e.preventDefault(); 203 | 204 | var item = $(this).closest("li"); 205 | item.prevAll().addClass("drag-before"); 206 | item.nextAll().addClass("drag-after"); 207 | 208 | // Fix height of list element to avoid flickering of page 209 | that.ol.css({height: that.ol.height() + $(e.item).height() + "px"}); 210 | new CUI.DragAction(e, that.$element, item, [that.ol], "vertical"); 211 | }) 212 | .on("dragenter", function (e) { 213 | that.ol.addClass("drag-over"); 214 | that._reorderPreview(e); 215 | }) 216 | .on("dragover", function (e) { 217 | that._reorderPreview(e); 218 | }) 219 | .on("dragleave", function (e) { 220 | that.ol.removeClass("drag-over").children().removeClass("drag-before drag-after"); 221 | }) 222 | .on("drop", function (e) { 223 | that._reorder($(e.item)); 224 | that.ol.children().removeClass("drag-before drag-after"); 225 | }) 226 | .on("dragend", function (e) { 227 | that.ol.css({height: ""}); 228 | }); 229 | 230 | document.addEventListener('keydown', function (event) { 231 | if (event.key === 'Escape') { 232 | if (Merkle.Helper.hasMarkup(Merkle.Helper.CONST.ADD_ITEM_WORKFLOW)) { 233 | var dialog = $('body.' + Merkle.Helper.CONST.ADD_ITEM_WORKFLOW); 234 | dialog.find('.cq-dialog-cancel').click(); 235 | } 236 | } 237 | }, true); 238 | 239 | }, 240 | 241 | /** 242 | * Opens the edit dialog for a given item id. 243 | * If the item id is not defined, a empty dialog for a new item is loaded. 244 | * 245 | * @param {String} itemPath of the current item 246 | * @param {Function} cancelCallback on abort. 247 | * @private 248 | */ 249 | _openEditDialog: function (itemPath, cancelCallback) { 250 | if (!itemPath) { 251 | throw new Error("Parameter 'itemPath' must be defined"); 252 | } 253 | 254 | var that = this, 255 | path = this.itemDialog + ".html" + itemPath; 256 | 257 | var dialog = { 258 | getConfig: function () { 259 | return { 260 | src: path, 261 | itemPath: itemPath, 262 | loadingMode: "auto", 263 | layout: "auto", 264 | isGenericMultifield: true 265 | }; 266 | }, 267 | getRequestData: function () { 268 | return {}; 269 | }, 270 | onSuccess: function () { 271 | that._updateList(true); 272 | return $.Deferred().promise(); 273 | }, 274 | onCancel: cancelCallback 275 | } 276 | try { 277 | Merkle.GenericMultifieldDialogHandler.openDialog(dialog); 278 | } catch (error) { 279 | console.error(error); 280 | if ($.isFunction(cancelCallback)) { 281 | cancelCallback(); 282 | } 283 | } 284 | }, 285 | 286 | /** 287 | * Edits an item by opening the item dialog. 288 | * 289 | * @param {Object} item List item to be edited. 290 | * @private 291 | */ 292 | _editItem: function (item) { 293 | var path = this.crxPath + "/" + this.itemStorageNode + "/" + item.attr("id"); 294 | this._openEditDialog(path); 295 | }, 296 | 297 | /** 298 | * Adds a new item by opening the empty item dialog if maxElements is not reached. 299 | * Otherwise, a warning dialog is displayed. 300 | * 301 | * @private 302 | */ 303 | _addNewItem: function () { 304 | var that = this; 305 | var currentElements = this.$element.find("li").length; 306 | 307 | if (!this.maxElements || (currentElements < this.maxElements)) { 308 | this._createNode(this.crxPath + "/" + this.itemStorageNode + "/*", function (path) { 309 | that._openEditDialog(path, function (event, dialog) { 310 | that._deleteNode(path, function () { 311 | // call update list after successful deletion of node 312 | that._updateList(true); 313 | }); 314 | }); 315 | }); 316 | } else { 317 | this.ui.alert(Granite.I18n.get("Maximum reached"), Granite.I18n.get("Maximum number of {0} item(s) reached, you cannot add any additional items.", this.maxElements), "warning"); 318 | } 319 | }, 320 | 321 | /** 322 | * Removes an item from the list. 323 | * Shows a warning dialog ('Cancel','Delete') before the delete action is executed. 324 | * 325 | * @param {Object} item the list item to be deleted 326 | * @private 327 | */ 328 | _removeItem: function (item) { 329 | var that = this, 330 | currentElements = this.$element.find("li").length; 331 | 332 | if (!this.minElements || (currentElements > this.minElements)) { 333 | this.ui.prompt(Granite.I18n.get("Remove Item"), Granite.I18n.get("Are you sure you want to delete this item?", this.minElements), "warning", 334 | [{text: Granite.I18n.get("Cancel")}, 335 | { 336 | text: Granite.I18n.get("Delete"), 337 | warning: true, 338 | handler: function () { 339 | if (currentElements === 1) { 340 | // delete whole itemStorageNode if last item is being removed 341 | that._deleteNode(that.crxPath + "/" + that.itemStorageNode, deleteItemCallback); 342 | } else { 343 | that._deleteNode(that.crxPath + "/" + that.itemStorageNode + "/" + item.attr("id"), deleteItemCallback); 344 | } 345 | } 346 | }]); 347 | } else { 348 | this.ui.alert(Granite.I18n.get("Minimum reached"), Granite.I18n.get("Minimum number of {0} item(s) reached, you cannot delete any additional items.", this.minElements), "warning"); 349 | } 350 | 351 | // remove item from DOM on successful callback 352 | function deleteItemCallback(path) { 353 | item.remove(); 354 | that._triggerChangeEvent(); 355 | } 356 | }, 357 | 358 | /** 359 | * Performs drag and drop reordering and 360 | * executes a sling reordering request on crx items. 361 | * 362 | * @param {Object} item the dragging item. 363 | * @private 364 | */ 365 | _reorder: function (item) { 366 | var before = this.ol.children(".drag-after").first(); 367 | var after = this.ol.children(".drag-before").last(); 368 | 369 | 370 | if (before.length > 0) { 371 | item.insertBefore(before); 372 | $.ajax({ 373 | type: "POST", 374 | data: ":order=before " + before.attr("id"), 375 | url: this.crxPath + "/" + this.itemStorageNode + "/" + item.attr("id") 376 | }); 377 | } else if (after.length > 0) { 378 | item.insertAfter(after); 379 | $.ajax({ 380 | type: "POST", 381 | data: ":order=after " + after.attr("id"), 382 | url: this.crxPath + "/" + this.itemStorageNode + "/" + item.attr("id") 383 | }); 384 | 385 | } 386 | }, 387 | 388 | /** 389 | * Creates a preview view on drag and drop reordering action. 390 | * 391 | * @param {Event} e the event object. 392 | * @private 393 | */ 394 | _reorderPreview: function (e) { 395 | var pos = this._pagePosition(e); 396 | this.ol.children(":not(.is-dragging)").each(function () { 397 | var el = $(this); 398 | var isAfter = pos.y < (el.offset().top + el.outerHeight() / 2); 399 | el.toggleClass("drag-after", isAfter); 400 | el.toggleClass("drag-before", !isAfter); 401 | }); 402 | }, 403 | 404 | /** 405 | * Gets the page position. 406 | * 407 | * @param {Event} e the event object. 408 | * @private 409 | */ 410 | _pagePosition: function (e) { 411 | var touch = {}; 412 | if (e.originalEvent) { 413 | var o = e.originalEvent; 414 | if (o.changedTouches && o.changedTouches.length > 0) { 415 | touch = o.changedTouches[0]; 416 | } 417 | if (o.touches && o.touches.length > 0) { 418 | touch = o.touches[0]; 419 | } 420 | } 421 | 422 | return { 423 | x: touch.pageX || e.pageX, 424 | y: touch.pageY || e.pageY 425 | }; 426 | }, 427 | 428 | /** 429 | * Creates a new node at given path. 430 | * 431 | * @param {String} path of node to be deleted. 432 | * @param {Function} callback node that has been created. 433 | * @private 434 | */ 435 | _createNode: function (path, callback) { 436 | $.ajax({ 437 | type: "POST", 438 | headers: { 439 | Accept: "application/json,**/**;q=0.9" 440 | }, 441 | url: path 442 | }).done(function (data) { 443 | if ($.isFunction(callback)) { 444 | if (data && data.path) { 445 | callback(data.path); 446 | } 447 | } 448 | }); 449 | }, 450 | 451 | /** 452 | * Deletes the node at given path. 453 | * 454 | * @param {String} path of node to be deleted. 455 | * @param {Function} callback node that has been created. 456 | * @private 457 | */ 458 | _deleteNode: function (path, callback) { 459 | $.ajax({ 460 | type: "POST", 461 | data: ":operation=delete", 462 | url: path 463 | }).done(function (data) { 464 | if ($.isFunction(callback)) { 465 | callback(path); 466 | } 467 | }); 468 | }, 469 | 470 | /** 471 | * Triggers the change event with the DOM element as the source. 472 | * 473 | * @private 474 | */ 475 | _triggerChangeEvent: function () { 476 | this.$element.trigger("change"); 477 | } 478 | }); 479 | 480 | // put Merkle.GenericMultiField on widget registry 481 | CUI.Widget.registry.register(" ", Merkle.GenericMultiField); 482 | 483 | // Data API 484 | if (CUI.options.dataAPI) { 485 | $(document).on("cui-contentloaded.data-api", function (e, data) { 486 | $(".coral-GenericMultiField[data-init~='genericmultifield']", e.target).genericMultiField(); 487 | if (data && data._foundationcontentloaded) { 488 | $(".coral-GenericMultiField[data-init~='genericmultifield']", e.target).trigger("change"); 489 | } 490 | }); 491 | } 492 | }(window.jQuery)); -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/GenericMultifieldDialogHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This part creates a new DialogFrame for the Generic Multi-field. 3 | */ 4 | ; 5 | (function ($, ns, channel, window, document, undefined) { 6 | "use strict"; 7 | 8 | /** 9 | * This dialog frame represents the Granite UI Dialog Frame in the Generic 10 | * MultiField (Merkle) context. It is basically a copy of the DialogFrame.js 11 | * with little extensions for the Generic MultiField. 12 | * 13 | * @namespace 14 | * @alias Merkle.DialogFrame 15 | */ 16 | ns.GenericMultifieldDialogHandler = (function () { 17 | var self = {}; 18 | var DIALOG_SELECTOR = "coral-dialog"; 19 | var DIALOG_CONTENT_SELECTOR = "coral-dialog-content"; 20 | var DIALOG_MODE = { 21 | COMPONENT: "COMPONENT", 22 | PAGE: "PAGE" 23 | }; 24 | 25 | /** 26 | * Array of parent dialogs. 27 | * 28 | * Save parent dialogs as a stack. Whenever a dialog gets closed, the parent 29 | * gets opened (if existing). 30 | */ 31 | self.parentDialogs = []; 32 | /** 33 | * Array of form data from parent dialogs. 34 | * 35 | * Save form data of parent dialogs as a stack. Whenever a dialog gets 36 | * closed, the parent gets opened (if existing) and the data gets restored. 37 | */ 38 | self.parentDialogsData = []; 39 | /** 40 | * Mode of dialog. 41 | * 42 | * Specifies if dialog was loaded by a component's dialog or by a page 43 | * properties dialog. 44 | */ 45 | self.dialogMode; 46 | 47 | /** 48 | * Opens a new dialog. 49 | * Closes the current dialog and opens the new one. 50 | * 51 | * @param {Object} dialog dialog to be opened. 52 | */ 53 | self.openDialog = function (dialog) { 54 | var currentDialog = Granite.author.DialogFrame.currentDialog; 55 | if (currentDialog) { 56 | self.dialogMode = DIALOG_MODE.COMPONENT; 57 | 58 | if (self.parentDialogs.length === 0) { 59 | currentDialog = _extendOriginalDialog(currentDialog); 60 | } 61 | 62 | // push old dialog to parent 63 | self.parentDialogs.push(currentDialog); 64 | // save data of parent dialog 65 | _saveDialogData(currentDialog); 66 | 67 | // close current dialog 68 | Granite.author.DialogFrame.closeDialog(); 69 | } else { 70 | self.dialogMode = DIALOG_MODE.PAGE; 71 | } 72 | 73 | // create custom backdrop 74 | ns.Helper.createCustomBackdrop(); 75 | 76 | // open new dialog 77 | Granite.author.DialogFrame.openDialog(_extendGenericMultiFieldDialog(dialog)); 78 | } 79 | 80 | /** 81 | * Extend original dialog. 82 | * Extends the dialog object with necessary callback functions. 83 | * 84 | * @param {Object} originalDialog dialog to be extended. 85 | * @returns {Object} extended dialog. 86 | * @private 87 | */ 88 | function _extendOriginalDialog(originalDialog) { 89 | // save original onClose callback 90 | var _onCloseOrig = originalDialog.onClose, _onReadyOrig = originalDialog.onReady; 91 | 92 | // overwrite onClose function of dialog 93 | originalDialog.onClose = function () { 94 | // if original onClose callback was set, execute it first 95 | if ($.isFunction(_onCloseOrig)) { 96 | _onCloseOrig(); 97 | } 98 | 99 | ns.Helper.removeCustomBackdrop(); 100 | } 101 | 102 | // overwrite onReady function of dialog if "onCancel" callback has been 103 | // configured 104 | originalDialog.onReady = function () { 105 | ns.Helper.createCustomBackdrop(); 106 | 107 | // if original onReady callback was set, execute it first 108 | if ($.isFunction(_onReadyOrig)) { 109 | _onReadyOrig(); 110 | } 111 | } 112 | 113 | return originalDialog; 114 | } 115 | 116 | /** 117 | * Extend dialogs created by generic multi-field. 118 | * Extends the dialog object with necessary callback functions. 119 | * 120 | * @param {Object} dialog dialog to be extended. 121 | * @returns {Object} extended dialog. 122 | * @private 123 | */ 124 | function _extendGenericMultiFieldDialog(dialog) { 125 | // save original onClose callback 126 | var _onCloseOrig = dialog.onClose, _onReadyOrig = dialog.onReady; 127 | 128 | // overwrite onClose function of dialog 129 | dialog.onClose = function () { 130 | ns.Helper.removeMarkup(ns.Helper.CONST.ADD_ITEM_WORKFLOW); 131 | 132 | // if original onClose callback was set, execute it first 133 | if ($.isFunction(_onCloseOrig)) { 134 | _onCloseOrig(); 135 | } 136 | 137 | Granite.author.DialogFrame.closeDialog(); 138 | 139 | // execute function after fading effect has finished 140 | setTimeout(function waitToClose() { 141 | // make sure that currentDialog has been cleared 142 | if (Granite.author.DialogFrame.isOpened()) { 143 | setTimeout(waitToClose, 50); 144 | } 145 | 146 | // perform closing of dialog 147 | _performCloseDialog(); 148 | }, 50); 149 | } 150 | 151 | // overwrite onReady function of dialog if "onCancel" callback has been 152 | // configured 153 | dialog.onReady = function () { 154 | // if original onReady callback was set, execute it first 155 | if ($.isFunction(_onReadyOrig)) { 156 | _onReadyOrig(); 157 | } 158 | 159 | // register callback function to dialog cancelled event 160 | if ($.isFunction(dialog.onCancel)) { 161 | var cqDialogForm = ns.Helper.findDialog(dialog.getConfig().itemPath, ".cq-dialog-cancel"); 162 | $(cqDialogForm, DIALOG_SELECTOR).click(dialog.onCancel); 163 | } 164 | } 165 | 166 | return dialog; 167 | } 168 | 169 | /** 170 | * Performs closing of current dialog. 171 | * Closes the current dialog and opens its parent. 172 | */ 173 | function _performCloseDialog() { 174 | // get parent dialog 175 | var parentDialog = self.parentDialogs.pop(); 176 | // open parent dialog if it exists 177 | if (parentDialog) { 178 | // register handler to restore data after the content of the dialog has been loaded 179 | $(document).on("foundation-contentloaded", function restoreDataHandler(e, data) { 180 | if (!data.restored) { 181 | // restore data 182 | _restoreDialogData(parentDialog); 183 | } 184 | 185 | // unregister handler 186 | $(document).off("foundation-contentloaded", restoreDataHandler); 187 | }); 188 | 189 | Granite.author.DialogFrame.openDialog(parentDialog); 190 | } 191 | 192 | // remove custom backdrop on the last dialog after fading effect has finished 193 | if (self.dialogMode === DIALOG_MODE.PAGE && self.parentDialogs.length === 0) { 194 | ns.Helper.removeCustomBackdrop(); 195 | } 196 | } 197 | 198 | /** 199 | * Saves the dialog and it's data 200 | * 201 | * @param {Object} dialog from which to retrieve the data from. 202 | * @private 203 | */ 204 | function _saveDialogData(dialog) { 205 | var dialogContainer = _getDomElementForDialog(dialog); 206 | if (dialogContainer) { 207 | // push content of current dialog 208 | self.parentDialogsData.push($(DIALOG_CONTENT_SELECTOR, dialogContainer)); 209 | } 210 | } 211 | 212 | /** 213 | * Restores the dialog and it's data. 214 | * 215 | * @param {Object} dialog to be restored. 216 | * @private 217 | */ 218 | function _restoreDialogData(dialog) { 219 | var dialogContainer = _getDomElementForDialog(dialog); 220 | if (dialogContainer) { 221 | // replace content with previous 222 | $(DIALOG_CONTENT_SELECTOR, dialogContainer).replaceWith(self.parentDialogsData.pop()); 223 | dialogContainer.trigger("foundation-contentloaded", {restored: true}); 224 | } 225 | } 226 | 227 | /** 228 | * Returns DOM element for dialog 229 | * 230 | * @param {Object} dialog to retrieve. 231 | * @returns {Object} self jQuery object. 232 | * @private 233 | */ 234 | function _getDomElementForDialog(dialog) { 235 | var cqDialogForm; 236 | if (dialog.getConfig().itemPath) { 237 | cqDialogForm = ns.Helper.findDialog(dialog.getConfig().itemPath); 238 | } else { 239 | cqDialogForm = ns.Helper.findDialog(dialog.editable.path); 240 | } 241 | return $(cqDialogForm, DIALOG_SELECTOR).closest(DIALOG_SELECTOR); 242 | } 243 | 244 | return self; 245 | }()); 246 | 247 | }(jQuery, Merkle, jQuery(document), this, document)); 248 | -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/GenericMultifieldHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helpers for the Generic Multi-field. 3 | */ 4 | (function ($, ns, channel, window, document, undefined) { 5 | "use strict"; 6 | 7 | /** 8 | * Helpers for the Generic Multi-field in the ns namespace. 9 | */ 10 | ns.Helper = { 11 | 12 | CONST: { 13 | ADD_ITEM_WORKFLOW: 'add-item', 14 | CUSTOM_BACKDROP_CLASS: 'q-dialog-backdrop-GenericMultiField', 15 | CUSTOM_BACKDROP_SELECTOR: '.cq-dialog-backdrop-GenericMultiField', 16 | CORAL_GENERIC_MULTIFIELD_SELECTOR: '.coral-GenericMultiField', 17 | ERROR_MESSAGE_REQUIRED: 'Error: Please fill out this field.', 18 | ERROR_MESSAGE_MIN: 'Error: At least {0} items must be created.', 19 | ERROR_MESSAGE_MAX: 'Error: At most {0} items can be created.' 20 | }, 21 | 22 | /** 23 | * Displays the dialog backdrop over the content. 24 | */ 25 | createCustomBackdrop: function () { 26 | var $customBackdrop = $(ns.Helper.CONST.CUSTOM_BACKDROP_SELECTOR), 27 | $originalBackdrop = $(".cq-dialog-backdrop"); 28 | 29 | // don't create backdrop if it already exists 30 | if ($customBackdrop.length) { 31 | return; 32 | } 33 | 34 | // create backdrop 35 | $customBackdrop = $('<div class="' + ns.Helper.CONST.CUSTOM_BACKDROP_CLASS + '"></div>'); 36 | if ($originalBackdrop.length) { 37 | $customBackdrop.insertAfter($originalBackdrop); 38 | } else { 39 | $("body").append($customBackdrop); 40 | } 41 | 42 | // backdrop has CSS transition to fade in 43 | $customBackdrop.css("opacity", "1"); 44 | }, 45 | 46 | /** 47 | * Retrieves dialog object. 48 | * 49 | * @param path of dialog to fetch. 50 | * @param optionalSelector to specific dialog selection. 51 | * @returns {Object} found dialog. 52 | */ 53 | findDialog: function (path, optionalSelector = "") { 54 | var cqDialogForm = $("form.cq-dialog[action='" + path + "'] " + optionalSelector); 55 | if (cqDialogForm === undefined || !cqDialogForm.length) { 56 | cqDialogForm = $("form.cq-dialog[action='" + this._manglePath(path) + "'] " + optionalSelector); 57 | } 58 | return cqDialogForm; 59 | }, 60 | 61 | /** 62 | * Mangle string value. 63 | * 64 | * @param path to mangle. 65 | * @returns {String} adjusted path value. 66 | * @private 67 | */ 68 | _manglePath: function (path) { 69 | if (!path) { 70 | return; 71 | } 72 | return path.replace(/\/(\w+):(\w+)/g, "/_$1_$2"); 73 | }, 74 | 75 | /** 76 | * Hides the dialog backdrop over the content. 77 | */ 78 | removeCustomBackdrop: function () { 79 | var $customBackdrop = $(ns.Helper.CONST.CUSTOM_BACKDROP_SELECTOR); 80 | $customBackdrop.one("transitionend", function () { 81 | $customBackdrop.remove(); 82 | }); 83 | $customBackdrop.css("opacity", "0"); 84 | 85 | // remove backdrop after a maximum of 1s if no transition event was fired 86 | setTimeout(function waitToClose() { 87 | // remove backdrop 88 | if ($customBackdrop.length) { 89 | $customBackdrop.remove(); 90 | } 91 | }, 1000); 92 | }, 93 | 94 | /** 95 | * Adds a CSS markup class to the body element. 96 | * @param {String} markup CSS class name to add. 97 | */ 98 | addMarkup: function (markup) { 99 | document.body.classList.add(markup); 100 | }, 101 | 102 | /** 103 | * Removes a CSS markup class from the body element. 104 | * @param {String} markup CSS class name to remove. 105 | */ 106 | removeMarkup: function (markup) { 107 | document.body.classList.remove(markup); 108 | }, 109 | 110 | /** 111 | * Checks if the body element has a specific markup class. 112 | * @param {String} markup CSS class name to check for. 113 | * @returns {boolean} true if the class exists, false otherwise. 114 | */ 115 | hasMarkup: function (markup) { 116 | return document.body.classList.contains(markup); 117 | } 118 | 119 | } 120 | 121 | }(jQuery, Merkle, jQuery(document), this, document)); 122 | -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/Namespace.js: -------------------------------------------------------------------------------- 1 | // Create the namespace 2 | var Merkle = Merkle || {}; 3 | -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/js.txt: -------------------------------------------------------------------------------- 1 | Namespace.js 2 | GenericMultifieldHelper.js 3 | GenericMultifieldDialogHandler.js 4 | CUI.GenericMultiField.js 5 | validations.js -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/validations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Validates the generic multi-field's minimum and maximum number of elements 3 | * restriction. 4 | */ 5 | 6 | (function (window, $, CUI) { 7 | "use strict"; 8 | 9 | /** 10 | * Performs the validation of the generic multi-field. 11 | * 12 | * @param {Object} multiField to perform validation on. 13 | * @private 14 | */ 15 | function _performValidation(multiField) { 16 | var api = multiField.adaptTo("foundation-validation"); 17 | if (api) { 18 | api.checkValidity(); 19 | api.updateUI(); 20 | } 21 | } 22 | 23 | // get global foundation registry 24 | var registry = $(window).adaptTo("foundation-registry"); 25 | 26 | // register adapter for generic multi-field 27 | registry.register("foundation.adapters", { 28 | type: "foundation-field", 29 | selector: Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR, 30 | adapter: function (el) { 31 | var $el = $(el); 32 | return { 33 | getName: function () { 34 | return $el.data("name"); 35 | }, 36 | setName: function (name) { 37 | $el.data("name", name); 38 | }, 39 | isDisabled: function () { 40 | return !!$el.attr("disabled"); 41 | }, 42 | setDisabled: function (disabled) { 43 | if (disabled === true) { 44 | $el.attr("disabled", "disabled"); 45 | } 46 | }, 47 | isInvalid: function () { 48 | return $el.attr("aria-invalid") === "true"; 49 | }, 50 | setInvalid: function (invalid) { 51 | $el.attr("aria-invalid", !!invalid ? "true" : "false").toggleClass("is-invalid", invalid); 52 | }, 53 | isRequired: function () { 54 | return $el.attr("aria-required") === "true"; 55 | }, 56 | setRequired: function (required) { 57 | $el.attr("aria-required", !!required ? "true" : "false"); 58 | } 59 | }; 60 | } 61 | }); 62 | 63 | // register selector for generic multi-field 64 | registry.register("foundation.validation.selector", { 65 | submittable: Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR, 66 | candidate: ".coral-GenericMultiField:not([disabled]):not([data-renderreadonly=true])", 67 | exclusion: ".coral-GenericMultiField *" 68 | }); 69 | 70 | // register validator for generic multi-field 71 | registry.register("foundation.validation.validator", { 72 | selector: Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR, 73 | validate: function (el) { 74 | var $field = $(el).closest(".coral-Form-field"), items = $field.find(".coral-GenericMultiField-list li"), 75 | minElements = $field.data("minelements"), maxElements = $field.data("maxelements"); 76 | 77 | // validate required attribute 78 | if ($field.adaptTo("foundation-field").isRequired() && items.length === 0) { 79 | return Granite.I18n.get(Merkle.Helper.CONST.ERROR_MESSAGE_REQUIRED); 80 | 81 | } 82 | 83 | // validate min and max elements (only if field is required) 84 | if ($field.adaptTo("foundation-field").isRequired()) { 85 | // validate if minElements restriction is met 86 | if (items && !isNaN(minElements) && items.length < minElements) { 87 | return Granite.I18n.get(Merkle.Helper.CONST.ERROR_MESSAGE_MIN, minElements); 88 | } 89 | // validate if maxElements restriction is met 90 | if (items && !isNaN(maxElements) && items.length > maxElements) { 91 | return Granite.I18n.get(Merkle.Helper.CONST.ERROR_MESSAGE_MAX, maxElements); 92 | 93 | } 94 | } 95 | 96 | return null; 97 | }, 98 | show: function (el, message, ctx) { 99 | var $field = $(el).closest(".coral-Form-field"); 100 | $field.adaptTo("foundation-field").setInvalid(true); 101 | 102 | setTimeout(function() { 103 | $field.siblings(".coral-Form-errorlabel").each(function (index, element) { 104 | if (index > 0) { 105 | element.remove() 106 | } 107 | }); 108 | }, 200); 109 | 110 | ctx.next(); 111 | }, 112 | clear: function (el, ctx) { 113 | var $field = $(el).closest(".coral-Form-field"); 114 | $field.adaptTo("foundation-field").setInvalid(false); 115 | $field.siblings(".coral-Form-fielderror").remove(); 116 | $field.siblings(".coral-Form-errorlabel").remove(); 117 | ctx.next(); 118 | } 119 | }); 120 | 121 | // perform validation every time generic multifield changed 122 | $(document).on("change", Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR, function () { 123 | _performValidation($(this)); 124 | }); 125 | 126 | })(window, Granite.$, CUI); 127 | -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/init.jsp: -------------------------------------------------------------------------------- 1 | <% 2 | %> 3 | <%@include file="/libs/granite/ui/global.jsp" %> 4 | <% 5 | %> 6 | <%@page session="false" 7 | import="com.adobe.granite.ui.components.Field, 8 | org.apache.sling.api.resource.ValueMap, 9 | org.apache.sling.api.wrappers.ValueMapDecorator, 10 | java.util.HashMap" %> 11 | <% 12 | final ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>()); 13 | 14 | // set non-empty string, otherwise the read only rendering will not work 15 | vm.put("value", "-"); 16 | 17 | request.setAttribute(Field.class.getName(), vm); 18 | %> -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/readonly/readonly.jsp: -------------------------------------------------------------------------------- 1 | <% 2 | %> 3 | <%@include file="/libs/granite/ui/global.jsp" %> 4 | <% 5 | %> 6 | <%@page session="false" 7 | import="com.adobe.granite.ui.components.AttrBuilder, 8 | com.adobe.granite.ui.components.Config, 9 | com.adobe.granite.ui.components.Tag" %> 10 | <% 11 | Config cfg = cmp.getConfig(); 12 | 13 | Tag tag = cmp.consumeTag(); 14 | AttrBuilder attrs = tag.getAttrs(); 15 | 16 | attrs.addClass("coral-GenericMultiField"); 17 | attrs.add("data-init", "genericmultifield"); 18 | attrs.add("id", cfg.get("id", String.class)); 19 | attrs.addClass(cfg.get("class", String.class)); 20 | attrs.addRel(cfg.get("rel", String.class)); 21 | attrs.add("title", i18n.getVar(cfg.get("title", String.class))); 22 | 23 | attrs.addOthers(cfg.getProperties(), "id", "rel", "class", "title", "fieldLabel", "fieldDescription"); 24 | 25 | String fieldLabel = cfg.get("fieldLabel", ""); 26 | %> 27 | 28 | <div <%= attrs.build() %>> 29 | <label class="coral-Form-fieldlabel"><%= outVar(xssAPI, i18n, fieldLabel) %></label> 30 | <ol class="coral-GenericMultiField-list js-coral-GenericMultiField-list coral-List--minimal"></ol> 31 | </div> -------------------------------------------------------------------------------- /src/main/resources/SLING-INF/apps/merkle/genericmultifield/render.jsp: -------------------------------------------------------------------------------- 1 | <% 2 | %> 3 | <%@include file="/libs/granite/ui/global.jsp" %> 4 | <% 5 | %> 6 | <%@page session="false" 7 | import="com.adobe.granite.ui.components.AttrBuilder, 8 | com.adobe.granite.ui.components.Config, 9 | com.adobe.granite.ui.components.Tag, 10 | org.osgi.service.cm.Configuration, 11 | org.osgi.service.cm.ConfigurationAdmin" %> 12 | <% 13 | final ConfigurationAdmin cfgAdmin = sling.getService(org.osgi.service.cm.ConfigurationAdmin.class); 14 | final Configuration mergePickerConfig = cfgAdmin.getConfiguration("org.apache.sling.resourcemerger.picker.overriding", null); 15 | final String mergeRoot = (String) mergePickerConfig.getProperties().get(org.apache.sling.resourcemerger.spi.MergedResourcePicker2.MERGE_ROOT); 16 | 17 | final Config cfg = cmp.getConfig(); 18 | final Tag tag = cmp.consumeTag(); 19 | final AttrBuilder attrs = tag.getAttrs(); 20 | 21 | attrs.addClass("coral-GenericMultiField"); 22 | attrs.add("data-init", "genericmultifield"); 23 | attrs.add("id", cfg.get("id", String.class)); 24 | attrs.addClass(cfg.get("class", String.class)); 25 | attrs.addRel(cfg.get("rel", String.class)); 26 | attrs.add("title", i18n.getVar(cfg.get("title", String.class))); 27 | attrs.add("data-mergeroot", mergeRoot); 28 | if (cfg.get("required", false)) { 29 | attrs.add("aria-required", true); 30 | } 31 | 32 | attrs.addOthers(cfg.getProperties(), "id", "rel", "class", "title", "fieldLabel", "fieldDescription", "storageWarningText", "renderReadOnly", "required"); 33 | %> 34 | 35 | <div <%= attrs.build() %>> 36 | <ol class="coral-GenericMultiField-list js-coral-GenericMultiField-list"></ol> 37 | <button is="coral-button" icon="add" size="M" class="js-coral-SpectrumMultiField-add coral-SpectrumMultiField-add"></button> 38 | </div> 39 | 40 | <ui:includeClientLib categories="cq.authoring.editor.hook"/> --------------------------------------------------------------------------------