From 0fd4439c62ac4f476cbbf41b66cde4d354dbb64c Mon Sep 17 00:00:00 2001 From: Rajat Khurana Date: Thu, 19 Oct 2023 11:20:17 +0530 Subject: [PATCH] logic updated --- .../internal/form/FormConstants.java | 3 + .../models/v2/form/FileInputImplV2.java | 64 +++ .../components/models/form/FileInput.java | 2 + .../models/v2/form/FileInputImplV2Test.java | 396 ++++++++++++++ .../exporter-fileinput-customized.json | 42 ++ .../exporter-fileinput-datalayer.json | 39 ++ .../form/fileinputv2/exporter-fileinput.json | 30 ++ .../exporter-multiselect-fileinput.json | 41 ++ .../form/fileinputv2/test-content.json | 86 +++ .../schema/0.12.1/adaptive-form.schema.json | 1 + .../components/form/fileinput/.content.xml | 2 +- .../form/fileinput/_cq_template.xml | 1 + .../.content.xml | 2 +- .../components/form/fileinput/v2/.content.xml | 5 + .../form/fileinput/v2/fileinput/.content.xml | 8 + .../form/fileinput/v2/fileinput/README.md | 94 ++++ .../v2/fileinput/_cq_dialog/.content.xml | 190 +++++++ .../fileinput/v2/fileinput/_cq_template.xml | 9 + .../v2/fileinput/clientlibs/.content.xml | 3 + .../fileinput/clientlibs/editor/.content.xml | 5 + .../v2/fileinput/clientlibs/editor/js.txt | 18 + .../clientlibs/editor/js/editDialog.js | 65 +++ .../v2/fileinput/clientlibs/site/.content.xml | 5 + .../v2/fileinput/clientlibs/site/css.txt | 19 + .../v2/fileinput/clientlibs/site/js.txt | 20 + .../clientlibs/site/js/fileinputview.js | 136 +++++ .../clientlibs/site/js/fileinputwidget.js | 495 ++++++++++++++++++ .../fileinput/v2/fileinput/fileinput.html | 66 +++ .../form/fileinput/v2/fileinput/fileinput.js | 33 ++ .../fileinput/fileinput.authoring.spec.js | 1 + .../specs/fileinput/fileinput.runtime.spec.js | 30 ++ 31 files changed, 1909 insertions(+), 2 deletions(-) create mode 100644 bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2.java create mode 100644 bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2Test.java create mode 100644 bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-customized.json create mode 100644 bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-datalayer.json create mode 100644 bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput.json create mode 100644 bundles/af-core/src/test/resources/form/fileinputv2/exporter-multiselect-fileinput.json create mode 100644 bundles/af-core/src/test/resources/form/fileinputv2/test-content.json create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/README.md create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_dialog/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_template.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js.txt create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js/editDialog.js create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/css.txt create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js.txt create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputview.js create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputwidget.js create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.html create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.js diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java index 6ecdd327c3..faf5f62a4c 100644 --- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java @@ -62,6 +62,9 @@ private FormConstants() { /** The resource type for file input v1 */ public static final String RT_FD_FORM_FILE_INPUT_V1 = RT_FD_FORM_PREFIX + "fileinput/v1/fileinput"; + /** The resource type for file input v2 */ + public static final String RT_FD_FORM_FILE_INPUT_V2 = RT_FD_FORM_PREFIX + "fileinput/v2/fileinput"; + /** The resource type for check box group v1 */ public static final String RT_FD_FORM_CHECKBOX_GROUP_V1 = RT_FD_FORM_PREFIX + "checkboxgroup/v1/checkboxgroup"; diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2.java new file mode 100644 index 0000000000..ea24422942 --- /dev/null +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2.java @@ -0,0 +1,64 @@ +/* + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * ~ Copyright 2023 Adobe + * ~ + * ~ Licensed under the Apache License, Version 2.0 (the "License"); + * ~ you may not use this file except in compliance with the License. + * ~ You may obtain a copy of the License at + * ~ + * ~ http://www.apache.org/licenses/LICENSE-2.0 + * ~ + * ~ Unless required by applicable law or agreed to in writing, software + * ~ distributed under the License is distributed on an "AS IS" BASIS, + * ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * ~ See the License for the specific language governing permissions and + * ~ limitations under the License. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +package com.adobe.cq.forms.core.components.internal.models.v2.form; + +import java.util.Map; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.Default; +import org.apache.sling.models.annotations.Exporter; +import org.apache.sling.models.annotations.Model; +import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; +import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; +import org.jetbrains.annotations.NotNull; + +import com.adobe.cq.export.json.ComponentExporter; +import com.adobe.cq.export.json.ExporterConstants; +import com.adobe.cq.forms.core.components.internal.form.FormConstants; +import com.adobe.cq.forms.core.components.internal.models.v1.form.FileInputImpl; +import com.adobe.cq.forms.core.components.models.form.FileInput; +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Model( + adaptables = { SlingHttpServletRequest.class, Resource.class }, + adapters = { FileInput.class, + ComponentExporter.class }, + resourceType = { FormConstants.RT_FD_FORM_FILE_INPUT_V2 }) +@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION) +public class FileInputImplV2 extends FileInputImpl { + + private static final String FILE_INPUT_DRAG_DROP_TEXT = "fileInputDragDropText"; + + @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = "fileInputDragDropText") + @Default(values = FileInput.DEFAULT_FILE_DRAGDROP_TEXT) + protected String fileInputDragDropText; + + @JsonIgnore + public String getFileInputDragDropText() { + return fileInputDragDropText; + } + + @Override + public @NotNull Map getProperties() { + Map customProperties = super.getProperties(); + customProperties.put(FILE_INPUT_DRAG_DROP_TEXT, getFileInputDragDropText()); + return customProperties; + } +} diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/FileInput.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/FileInput.java index 63b4a53c92..4ecf2c8a93 100644 --- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/FileInput.java +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/FileInput.java @@ -28,6 +28,8 @@ public interface FileInput extends Field, FileConstraint, ContainerConstraint { String DEFAULT_BUTTON_TEXT = "Attach"; + String DEFAULT_FILE_DRAGDROP_TEXT = "Drag and Drop To Upload"; + /** * Returns {@code true} if multiple files can be selected, {@code false} otherwise * diff --git a/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2Test.java b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2Test.java new file mode 100644 index 0000000000..77fa709f91 --- /dev/null +++ b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FileInputImplV2Test.java @@ -0,0 +1,396 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.forms.core.components.internal.models.v2.form; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; + +import com.adobe.cq.forms.core.Utils; +import com.adobe.cq.forms.core.components.datalayer.FormComponentData; +import com.adobe.cq.forms.core.components.internal.form.FormConstants; +import com.adobe.cq.forms.core.components.models.form.Base; +import com.adobe.cq.forms.core.components.models.form.BaseConstraint; +import com.adobe.cq.forms.core.components.models.form.ConstraintType; +import com.adobe.cq.forms.core.components.models.form.FieldType; +import com.adobe.cq.forms.core.components.models.form.FileInput; +import com.adobe.cq.forms.core.components.models.form.Label; +import com.adobe.cq.forms.core.context.FormsCoreComponentTestContext; +import com.adobe.cq.wcm.style.ComponentStyleInfo; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +@ExtendWith(AemContextExtension.class) +public class FileInputImplV2Test { + private static final String BASE = "/form/fileinputv2"; + private static final String CONTENT_ROOT = "/content"; + private static final String PATH_FILEINPUT_CUSTOMIZED = CONTENT_ROOT + "/fileinput-customized"; + + private static final String PATH_FILEINPUT = CONTENT_ROOT + "/fileinput"; + private static final String PATH_FILEINPUT_DATALAYER = CONTENT_ROOT + "/fileinput-datalayer"; + private static final String PATH_MULTISELECT_FILEINPUT = CONTENT_ROOT + "/multiselect-fileinput"; + + private final AemContext context = FormsCoreComponentTestContext.newAemContext(); + + @BeforeEach + void setUp() { + context.load().json(BASE + FormsCoreComponentTestContext.TEST_CONTENT_JSON, CONTENT_ROOT); + } + + @Test + void testExportedType() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(FormConstants.RT_FD_FORM_FILE_INPUT_V2, fileInput.getExportedType()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getExportedType()).thenCallRealMethod(); + assertEquals("", fileInputMock.getExportedType()); + } + + @Test + void testFieldType() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(FieldType.FILE_INPUT.getValue(), fileInput.getFieldType()); + } + + @Test + void testGetLabel() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals("def", fileInput.getLabel().getValue()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getLabel()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getLabel()); + + Label labelMock = Mockito.mock(Label.class); + Mockito.when(labelMock.isRichText()).thenCallRealMethod(); + assertEquals(null, labelMock.isRichText()); + Mockito.when(labelMock.getValue()).thenCallRealMethod(); + assertEquals(null, labelMock.getValue()); + Mockito.when(labelMock.isVisible()).thenCallRealMethod(); + assertEquals(null, labelMock.isVisible()); + } + + @Test + void testGetName() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals("abc", fileInput.getName()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getName()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getName()); + } + + @Test + void testGetDataRef() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals("a.b", fileInput.getDataRef()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getDataRef()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getDataRef()); + } + + @Test + void testGetDescription() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals("dummy", fileInput.getDescription()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getDescription()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getDescription()); + } + + @Test + void testGetScreenReaderText() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals("'Custom screen reader text'", fileInput.getScreenReaderText()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getScreenReaderText()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getScreenReaderText()); + } + + @Test + void testIsVisible() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT, FileInput.class, context); + assertEquals(null, fileInput.isVisible()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isVisible()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isVisible()); + } + + @Test + void testIsVisibleForCustomized() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(false, fileInput.isVisible()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isVisible()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isVisible()); + } + + @Test + void testIsEnabled() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT, FileInput.class, context); + assertEquals(null, fileInput.isEnabled()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isEnabled()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isEnabled()); + } + + @Test + void testIsEnabledForCustomized() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(true, fileInput.isEnabled()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isEnabled()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isEnabled()); + } + + @Test + void testIsRequired() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT, FileInput.class, context); + assertEquals(null, fileInput.isRequired()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isRequired()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isRequired()); + } + + @Test + void testIsRequiredForCustomized() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(true, fileInput.isRequired()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isRequired()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isRequired()); + } + + @Test + void testIsReadOnly() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT, FileInput.class, context); + assertEquals(null, fileInput.isReadOnly()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isReadOnly()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isReadOnly()); + } + + @Test + void testIsReadOnlyForCustomized() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(false, fileInput.isReadOnly()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isReadOnly()).thenCallRealMethod(); + assertEquals(null, fileInputMock.isReadOnly()); + } + + @Test + void testGetPlaceHolder() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(null, fileInput.getPlaceHolder()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getPlaceHolder()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getPlaceHolder()); + } + + @Test + void testGetDisplayFormat() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(null, fileInput.getDisplayFormat()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getDisplayFormat()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getDisplayFormat()); + } + + @Test + void testGetEditFormat() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(null, fileInput.getEditFormat()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getEditFormat()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getEditFormat()); + } + + @Test + void testGetDataFormat() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(null, fileInput.getDataFormat()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getDataFormat()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getDataFormat()); + } + + @Test + void testGetMaxFileSize() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals("4MB", fileInput.getMaxFileSize()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getMaxFileSize()).thenCallRealMethod(); + assertEquals(FileInput.DEFAULT_MAX_FILE_SIZE, fileInputMock.getMaxFileSize()); + } + + @Test + void testGetAccept() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertThat(Arrays.asList("audio/*", "video/*", "image/*"), is(fileInput.getAccept())); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getAccept()).thenCallRealMethod(); + assertThat(FileInput.DEFAULT_ACCEPT, is(fileInputMock.getAccept())); + } + + @Test + void testGetTooltip() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals("test-short-description", fileInput.getTooltip()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getTooltip()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getTooltip()); + } + + @Test + void testGetConstraintMessages() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + Map constraintsMessages = fileInput.getConstraintMessages(); + assertEquals(constraintsMessages.get(ConstraintType.TYPE), "incorrect type"); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getConstraintMessages()).thenCallRealMethod(); + assertEquals(Collections.emptyMap(), fileInputMock.getConstraintMessages()); + } + + @Test + void testJSONExport() throws Exception { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT, FileInput.class, context); + Utils.testJSONExport(fileInput, Utils.getTestExporterJSONPath(BASE, PATH_FILEINPUT)); + } + + @Test + void testJSONExportForCustomized() throws Exception { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + Utils.testJSONExport(fileInput, Utils.getTestExporterJSONPath(BASE, PATH_FILEINPUT_CUSTOMIZED)); + } + + @Test + void testMultiSelectJSONExport() throws Exception { + FileInput fileInput = Utils.getComponentUnderTest(PATH_MULTISELECT_FILEINPUT, FileInput.class, context); + Utils.testJSONExport(fileInput, Utils.getTestExporterJSONPath(BASE, PATH_MULTISELECT_FILEINPUT)); + } + + @Test + void testGetProperties() throws Exception { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + Map properties = fileInput.getProperties(); + assertFalse(properties.isEmpty()); + // get custom properties of "afs:layout" + Map customProperties = (Map) properties.get(Base.CUSTOM_PROPERTY_WRAPPER); + assertFalse((boolean) customProperties.get("tooltipVisible")); + } + + @Test + void testGetProperties_should_return_empty_if_no_custom_properties() { + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getProperties()).thenCallRealMethod(); + assertTrue(fileInputMock.getProperties().isEmpty()); + } + + @Test + void testGetShortDescription() { + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getTooltip()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getTooltip()); + } + + @Test + void testIsShortDescriptionVisible() { + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.isTooltipVisible()).thenCallRealMethod(); + assertEquals(false, fileInputMock.isTooltipVisible()); + } + + @Test + void testGetType() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_CUSTOMIZED, FileInput.class, context); + assertEquals(BaseConstraint.Type.FILE, fileInput.getType()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getType()).thenCallRealMethod(); + assertEquals(BaseConstraint.Type.STRING, fileInputMock.getType()); + } + + @Test + void testGetMultiSelectType() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_MULTISELECT_FILEINPUT, FileInput.class, context); + assertEquals(BaseConstraint.Type.FILE_ARRAY, fileInput.getType()); + } + + @Test + void testGetMultiSelectMinItems() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_MULTISELECT_FILEINPUT, FileInput.class, context); + assertEquals(1, fileInput.getMinItems().intValue()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getMinItems()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getMinItems()); + } + + @Test + void testGetMultiSelectMaxItems() { + FileInput fileInput = Utils.getComponentUnderTest(PATH_MULTISELECT_FILEINPUT, FileInput.class, context); + assertEquals(2, fileInput.getMaxItems().intValue()); + FileInput fileInputMock = Mockito.mock(FileInput.class); + Mockito.when(fileInputMock.getMaxItems()).thenCallRealMethod(); + assertEquals(null, fileInputMock.getMaxItems()); + } + + @Test + void testStyleSystemClasses() { + ComponentStyleInfo componentStyleInfoMock = mock(ComponentStyleInfo.class); + Resource resource = spy(context.resourceResolver().getResource(PATH_FILEINPUT_CUSTOMIZED)); + Mockito.doReturn(componentStyleInfoMock).when(resource).adaptTo(ComponentStyleInfo.class); + MockSlingHttpServletRequest request = context.request(); + request.setResource(resource); + Mockito.doReturn("mystyle").when(componentStyleInfoMock).getAppliedCssClasses(); + FileInput fileInput = request.adaptTo(FileInput.class); + String appliedCssClasses = fileInput.getAppliedCssClasses(); + assertEquals("mystyle", appliedCssClasses); + } + + @Test + void testDataLayerProperties() throws IllegalAccessException { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_DATALAYER, FileInput.class, context); + FieldUtils.writeField(fileInput, "dataLayerEnabled", true, true); + FormComponentData dataObject = (FormComponentData) fileInput.getData(); + assert (dataObject != null); + assert (dataObject.getId()).equals("fileinput-90881b3d31"); + assert (dataObject.getType()).equals("core/fd/components/form/fileinput/v2/fileinput"); + assert (dataObject.getTitle()).equals("CV"); + assert (dataObject.getFieldType()).equals("file-input"); + assert (dataObject.getDescription()).equals("Upload your CV"); + } + + @Test + void testJSONExportDataLayer() throws Exception { + FileInput fileInput = Utils.getComponentUnderTest(PATH_FILEINPUT_DATALAYER, FileInput.class, context); + FieldUtils.writeField(fileInput, "dataLayerEnabled", true, true); + Utils.testJSONExport(fileInput, Utils.getTestExporterJSONPath(BASE, PATH_FILEINPUT_DATALAYER)); + } +} diff --git a/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-customized.json b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-customized.json new file mode 100644 index 0000000000..dc694c7698 --- /dev/null +++ b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-customized.json @@ -0,0 +1,42 @@ +{ + "id": "fileinput-87a9efb7f0", + "fieldType": "file-input", + "name": "abc", + "visible" : false, + "enabled": true, + "readOnly": false, + "required": true, + "label": { + "value": "def", + "visible": true + }, + "screenReaderText":"'Custom screen reader text'", + "fileInputDragDropText":"Drag and Drop To Upload", + "constraintMessages":{"type":"incorrect type"}, + "type": "file", + "description" : "dummy", + "maxFileSize" : "4MB", + "tooltip": "test-short-description", + "accept": [ + "audio/*", + "video/*", + "image/*" + ], + "dataRef":"a.b", + "events": { + "custom:setProperty": [ + "$event.payload" + ] + }, + "properties": { + "afs:layout": { + "tooltipVisible": false + }, + "fd:dor": { + "dorExclusion": false + }, + "fd:path": "/content/fileinput-customized", + "fileInputDragDropText":"Drag and Drop To Upload" + }, + ":type": "core/fd/components/form/fileinput/v2/fileinput" +} \ No newline at end of file diff --git a/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-datalayer.json b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-datalayer.json new file mode 100644 index 0000000000..f6c4ccf3b8 --- /dev/null +++ b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput-datalayer.json @@ -0,0 +1,39 @@ +{ + "id": "fileinput-90881b3d31", + "fieldType": "file-input", + "name": "CV", + "description": "Upload your CV", + "type": "file", + "fileInputDragDropText":"Drag and Drop To Upload", + "accept": [ + "audio/*", + " video/*", + " image/*", + " text/*", + " application/pdf" + ], + "label": { + "value": "CV" + }, + "events": { + "custom:setProperty": [ + "$event.payload" + ] + }, + "properties": { + "fd:dor": { + "dorExclusion": false + }, + "fd:path": "/content/fileinput-datalayer", + "fileInputDragDropText":"Drag and Drop To Upload" + }, + ":type": "core/fd/components/form/fileinput/v2/fileinput", + "dataLayer": { + "fileinput-90881b3d31": { + "dc:title": "CV", + "dc:description": "Upload your CV", + "@type": "core/fd/components/form/fileinput/v2/fileinput", + "fieldType": "file-input" + } + } +} \ No newline at end of file diff --git a/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput.json b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput.json new file mode 100644 index 0000000000..5839d425fb --- /dev/null +++ b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-fileinput.json @@ -0,0 +1,30 @@ +{ + "id": "fileinput-8857e83bbf", + "fieldType": "file-input", + "name": "abc", + "fileInputDragDropText":"Drag and Drop To Upload", + "label": { + "value": "def" + }, + "type": "file", + "accept": [ + "audio/*", + " video/*", + " image/*", + " text/*", + " application/pdf" + ], + "events": { + "custom:setProperty": [ + "$event.payload" + ] + }, + "properties": { + "fd:dor": { + "dorExclusion": false + }, + "fd:path": "/content/fileinput", + "fileInputDragDropText":"Drag and Drop To Upload" + }, + ":type": "core/fd/components/form/fileinput/v2/fileinput" +} \ No newline at end of file diff --git a/bundles/af-core/src/test/resources/form/fileinputv2/exporter-multiselect-fileinput.json b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-multiselect-fileinput.json new file mode 100644 index 0000000000..7540d33f48 --- /dev/null +++ b/bundles/af-core/src/test/resources/form/fileinputv2/exporter-multiselect-fileinput.json @@ -0,0 +1,41 @@ +{ + "id": "fileinput-4573ac70ac", + "dataRef": "a.b", + "fieldType": "file-input", + "name": "abc", + "visible": false, + "description": "dummy", + "fileInputDragDropText":"Drag and Drop To Upload", + "tooltip": "test-short-description", + "type": "file[]", + "minItems": 1, + "maxItems": 2, + "constraintMessages": { + "type": "incorrect type" + }, + "maxFileSize": "4GB", + "accept": [ + "text/*", + "application/pdf" + ], + "screenReaderText": "'Custom screen reader text'", + "label": { + "value": "def" + }, + "events": { + "custom:setProperty": [ + "$event.payload" + ] + }, + "properties": { + "afs:layout": { + "tooltipVisible": false + }, + "fd:dor": { + "dorExclusion": false + }, + "fd:path": "/content/multiselect-fileinput", + "fileInputDragDropText":"Drag and Drop To Upload" + }, + ":type": "core/fd/components/form/fileinput/v2/fileinput" +} \ No newline at end of file diff --git a/bundles/af-core/src/test/resources/form/fileinputv2/test-content.json b/bundles/af-core/src/test/resources/form/fileinputv2/test-content.json new file mode 100644 index 0000000000..caec05c60f --- /dev/null +++ b/bundles/af-core/src/test/resources/form/fileinputv2/test-content.json @@ -0,0 +1,86 @@ +{ + "fileinput" : { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType" : "core/fd/components/form/fileinput/v2/fileinput", + "name" : "abc", + "jcr:title" : "def", + "buttonText" : "Attach", + "fieldType": "file-input", + "accept" : ["audio/*", + " video/*", + " image/*", + " text/*", + " application/pdf"] + }, + "fileinput-customized" : { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType" : "core/fd/components/form/fileinput/v2/fileinput", + "name" : "abc", + "jcr:title" : "def", + "hideTitle" : false, + "description" : "dummy", + "visible" : false, + "enabled": true, + "readOnly": false, + "required": true, + "fieldType": "file-input", + "typeMessage" : "incorrect type", + "dataRef" : "a.b", + "assistPriority" : "custom", + "custom" : "Custom screen reader text", + "tooltip": "test-short-description", + "maxFileSize" : "4MB", + "accept" : ["audio/*", "video/*", "image/*"] + }, + "multiselect-fileinput" : { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType" : "core/fd/components/form/fileinput/v2/fileinput", + "fieldType": "file-input", + "name" : "abc", + "jcr:title" : "def", + "description" : "dummy", + "visible" : false, + "assistPriority" : "custom", + "dataRef" : "a.b", + "custom" : "Custom screen reader text", + "typeMessage" : "incorrect type", + "tooltip": "test-short-description", + "type" : "file[]", + "multiSelection" : true, + "minItems" : 1, + "maxItems" : 2, + "maxFileSize" : "4GB", + "accept" : ["text/*", "application/pdf"] + }, + "fileinput-datalayer": { + "id": "fileinput-90881b3d31", + "sling:resourceType": "core/fd/components/form/fileinput/v2/fileinput", + "fieldType": "file-input", + "description": "Upload your CV", + "jcr:title" : "CV", + "name": "CV", + "type": "file", + "accept": [ + "audio/*", + " video/*", + " image/*", + " text/*", + " application/pdf" + ], + "label": { + "value": "CV" + }, + "events": { + "custom:setProperty": [ + "$event.payload" + ] + }, + "properties": { + "fd:dor": { + "dorExclusion": false + }, + "fd:path": "/content/forms/af/af2/jcr:content/guideContainer/fileinput" + }, + ":type": "forms-components-examples/components/form/fileinput" + } +} \ No newline at end of file diff --git a/bundles/af-core/src/test/resources/schema/0.12.1/adaptive-form.schema.json b/bundles/af-core/src/test/resources/schema/0.12.1/adaptive-form.schema.json index 1bace02584..234678e6c7 100644 --- a/bundles/af-core/src/test/resources/schema/0.12.1/adaptive-form.schema.json +++ b/bundles/af-core/src/test/resources/schema/0.12.1/adaptive-form.schema.json @@ -732,6 +732,7 @@ "properties", "readOnly", "rules", + "fileInputDragDropText", "screenReaderText", "tooltip", "visible", diff --git a/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/.content.xml b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/.content.xml index 163dfb2aeb..3bd4aa80d4 100644 --- a/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/.content.xml +++ b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/.content.xml @@ -3,5 +3,5 @@ jcr:primaryType="cq:Component" jcr:title="Adaptive Form File Attachment" jcr:description="Add a button to upload one or more files as form attachment." - sling:resourceSuperType="core/fd/components/form/fileinput/v1/fileinput" + sling:resourceSuperType="core/fd/components/form/fileinput/v2/fileinput" componentGroup="Core Components Examples - Adaptive Form"/> diff --git a/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/_cq_template.xml b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/_cq_template.xml index 7bb0e9f0bd..8d9da5ed54 100644 --- a/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/_cq_template.xml +++ b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/fileinput/_cq_template.xml @@ -4,5 +4,6 @@ jcr:title="File Attachment" buttonText="Attach" fieldType="file-input" + fileInputDragDropText="Drag and drop to Upload" accept="[audio/*, video/*, image/*, text/*, application/pdf]"> \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml index 513006b23f..8786fcc5dd 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml @@ -5,4 +5,4 @@ cssProcessor="[default:none,min:none]" jsProcessor="[default:none,min:none]" categories="[core.forms.components.runtime.all]" - embed="[core.forms.components.runtime.base,core.forms.components.container.v2.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v1.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.termsandconditions.v1.runtime]"/> + embed="[core.forms.components.runtime.base,core.forms.components.container.v2.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v2.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.switch.v1.runtime,core.forms.components.termsandconditions.v1.runtime]"/> diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/.content.xml new file mode 100644 index 0000000000..f2cd6bb350 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/.content.xml @@ -0,0 +1,5 @@ + + + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/.content.xml new file mode 100644 index 0000000000..a7d773e604 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/.content.xml @@ -0,0 +1,8 @@ + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/README.md b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/README.md new file mode 100644 index 0000000000..0450684240 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/README.md @@ -0,0 +1,94 @@ + +Adaptive Form File Input (v2) +==== +Adaptive Form File input field component written in HTL. + +## Features + +* Drag and drop support +* Custom icon for drag and drop +* Provides the following type of input: + * file +* Custom constraint messages for the above types +* Styles + +### Use Object +The Form File component uses the `com.adobe.cq.forms.core.components.models.form.FileInput` Sling Model for its Use-object. + +### Edit Dialog Properties +The following properties are written to JCR for this Form File component and are expected to be available as `Resource` properties: + +1. `./jcr:title` - defines the label to use for this field +2. `./hideTitle` - if set to `true`, the label of this field will be hidden +3. `./name` - defines the name of the field, which will be submitted with the form data +4. `./default` - defines the default value of the field +5. `./description` - defines a help message that can be rendered in the field as a hint for the user +6. `./required` - if set to `true`, this field will be marked as required, not allowing the form to be submitted until the field has a value +7. `./requiredMessage` - defines the message displayed as tooltip when submitting the form if the value is left empty +8. `./readOnly` - if set to `true`, the filed will be read only +9. `./multiSelection` - if set to `true`, the filed will allow to add multiple files in single selection or multiple selections +10. `./minItems` - if value is selected/provided this will check for minimum number of files that can be attached +11. `./maxItems` - if value is selected/provided this will check for maximum number of files that can be attached +12. `./maxFileSize` - if value is selected/provided this will check for maximum file size allowed +13. `./accept` - defines the type of files accepted to upload +14. `./showComment` - if set to `true`, comments can be added to attachment +15. `./minItemsMessage` - defines the message displayed as tooltip when submitting the form if less than allowed minimum files uploaded +16. `./maxItemsMessage` - defines the message displayed as tooltip when submitting the form if more than allowed maximum files uploaded +17. `./maxFileSizeMessage` - defines the message displayed as tooltip when submitting the form if the uploaded file size is greater than allowed +18. `./acceptMessage` - defines the message displayed as tooltip when submitting the form if the uploaded file type is not allowed +19. `./fileInputDragDropText` - defines the file input drag and drop area title + +## Client Libraries +The component provides a `core.forms.components.fileinput.v2.runtime` client library category that contains the Javascript runtime for the component. +It should be added to a relevant site client library using the `embed` property. + +It also provides a `core.forms.components.fileinput.v2.editor` editor client library category that includes +JavaScript handling for dialog interaction. It is already included by its edit dialog. + +## BEM Description +``` +BLOCK cmp-adaptiveform-fileinput + ELEMENT cmp-adaptiveform-fileinput__label + ELEMENT cmp-adaptiveform-fileinput__label-container + ELEMENT cmp-adaptiveform-fileinput__widget + ELEMENT cmp-adaptiveform-fileinput__questionmark + ELEMENT cmp-adaptiveform-fileinput__shortdescription + ELEMENT cmp-adaptiveform-fileinput__longdescription + ELEMENT cmp-adaptiveform-fileinput__filelist + ELEMENT cmp-adaptiveform-fileinput__fileitem + ELEMENT cmp-adaptiveform-fileinput__filename + ELEMENT cmp-adaptiveform-fileinput__filedelete + ELEMENT cmp-adaptiveform-fileinput__widgetlabel +``` + +### Note +By placing the class names `cmp-adaptiveform-fileinput__label` and `cmp-adaptiveform-fileinput__questionmark` within the `cmp-adaptiveform-fileinput__label-container` class, you create a logical grouping of the label and question mark elements. This approach simplifies the process of maintaining a consistent styling for both elements. + +## JavaScript Data Attribute Bindings + + +The following attributes must be added for the initialization of the file-input component in the form view: + 1. `data-cmp-is="adaptiveFormFileInput"` + 2. `data-cmp-adaptiveformcontainer-path="${formstructparser.formContainerPath}"` + + +## Information +* **Vendor**: Adobe +* **Version**: v2 +* **Compatibility**: Cloud +* **Status**: production-ready + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_dialog/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_dialog/.content.xml new file mode 100644 index 0000000000..ef156aa4e6 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_dialog/.content.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_template.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_template.xml new file mode 100644 index 0000000000..8d9da5ed54 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/_cq_template.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/.content.xml new file mode 100644 index 0000000000..491392d539 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/.content.xml new file mode 100644 index 0000000000..c3849dc198 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/.content.xml @@ -0,0 +1,5 @@ + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js.txt new file mode 100644 index 0000000000..83b3a1eae4 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js.txt @@ -0,0 +1,18 @@ +############################################################################### +# Copyright 2023 Adobe +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################### + +#base=js +editDialog.js \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js/editDialog.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js/editDialog.js new file mode 100644 index 0000000000..7400af5ccf --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/editor/js/editDialog.js @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright 2023 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +(function($) { + "use strict"; + let EDIT_DIALOG = ".cmp-adaptiveform-fileinput__editdialog", + FILEINPUT_MULTISELECTION = EDIT_DIALOG + " .cmp-adaptiveform-fileinput__multiselection", + FILEINPUT_TYPE = EDIT_DIALOG + " .cmp-adaptiveform-fileinput__type", + FILEINPUT_MINITEMS = EDIT_DIALOG + " .cmp-adaptiveform-fileinput__minimumFiles", + FILEINPUT_MINITEMS_ERRMSG = EDIT_DIALOG + " .cmp-adaptiveform-fileinput__minimumFilesMessage", + FILEINPUT_MAXITEMS = EDIT_DIALOG + " .cmp-adaptiveform-fileinput__maximumFiles", + FILEINPUT_MAXITEMS_ERRMSG = EDIT_DIALOG + " .cmp-adaptiveform-fileinput__maximumFilesMessage", + Utils = window.CQ.FormsCoreComponents.Utils.v1; + + + function changeTypeProperty(component, fileinputType){ + if (component.checked){ + fileinputType.attributes.value.value="file[]"; + } + else { + fileinputType.attributes.value.value="file"; + } + } + + /** + * Toggles the addition of multi selection, value of type on the checked state of + * the multiSelection checkbox + * @param {HTMLElement} dialog The dialog on which the operation is to be performed. + */ + function handleMultiSelection(dialog) { + let component = dialog.find(FILEINPUT_MULTISELECTION)[0]; + let fileinputType=dialog.find(FILEINPUT_TYPE)[0]; + let fileinputMinItems=dialog.find(FILEINPUT_MINITEMS); + let fileinputMinItemsMessage = dialog.find(FILEINPUT_MINITEMS_ERRMSG); + let fileinputMaxItems=dialog.find(FILEINPUT_MAXITEMS); + let fileinputMaxItemsMessage = dialog.find(FILEINPUT_MAXITEMS_ERRMSG); + let listOfElements = [fileinputMinItems,fileinputMaxItems, fileinputMinItemsMessage, fileinputMaxItemsMessage]; + let isNotChecked = function() {return isChecked()}; + let isChecked = function() {return component.checked}; + let hideAndShowElements = function() { + // hide minItems elements + Utils.checkAndDisplay(listOfElements)(isNotChecked); + }; + hideAndShowElements(); + component.on("change", function() { + hideAndShowElements(); + changeTypeProperty(component, fileinputType); + }); + changeTypeProperty(component, fileinputType); + } + Utils.initializeEditDialog(EDIT_DIALOG)(handleMultiSelection); + +})(jQuery); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/.content.xml new file mode 100644 index 0000000000..5d56c91d54 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/.content.xml @@ -0,0 +1,5 @@ + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/css.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/css.txt new file mode 100644 index 0000000000..7d6d7ce183 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/css.txt @@ -0,0 +1,19 @@ +############################################################################### +# Copyright 2023 Adobe +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################### + +#base=css +fileinputview.css + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js.txt new file mode 100644 index 0000000000..3df402994a --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js.txt @@ -0,0 +1,20 @@ +############################################################################### +# Copyright 2023 Adobe +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################### + +#base=js +fileinputwidget.js +fileinputview.js + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputview.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputview.js new file mode 100644 index 0000000000..dd6daea60a --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputview.js @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright 2023 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +(function() { + + "use strict"; + class FileInputV2 extends FormView.FormFieldBase { + + static NS = FormView.Constants.NS; + /** + * Each FormField has a data attribute class that is prefixed along with the global namespace to + * distinguish between them. If a component wants to put a data-attribute X, the attribute in HTML would be + * data-{NS}-{IS}-x="" + * @type {string} + */ + static IS = "adaptiveFormFileInput"; + static bemBlock = 'cmp-adaptiveform-fileinput' + static selectors = { + self: "[data-" + this.NS + '-is="' + this.IS + '"]', + widget: `.${FileInputV2.bemBlock}__widget`, + label: `.${FileInputV2.bemBlock}__label`, + description: `.${FileInputV2.bemBlock}__longdescription`, + qm: `.${FileInputV2.bemBlock}__questionmark`, + errorDiv: `.${FileInputV2.bemBlock}__errormessage`, + tooltipDiv: `.${FileInputV2.bemBlock}__shortdescription`, + fileListDiv : `.${FileInputV2.bemBlock}__filelist`, + attachButtonLabel : `.${FileInputV2.bemBlock}__widgetlabel`, + dragArea: `.${FileInputV2.bemBlock}__dragarea` + }; + + constructor(params) { + super(params); + } + widgetFields = { + widget: this.getWidget(), + fileListDiv: this.getFileListDiv(), + model: () => this._model, + dragArea: this.getDragArea() + }; + + getWidget() { + return this.element.querySelector(FileInputV2.selectors.widget); + } + + getDragArea() { + return this.element.querySelector(FileInputV2.selectors.dragArea); + } + + getDescription() { + return this.element.querySelector(FileInputV2.selectors.description); + } + + getLabel() { + return this.element.querySelector(FileInputV2.selectors.label); + } + + getErrorDiv() { + return this.element.querySelector(FileInputV2.selectors.errorDiv); + } + + getTooltipDiv() { + return this.element.querySelector(FileInputV2.selectors.tooltipDiv); + } + + getQuestionMarkDiv() { + return this.element.querySelector(FileInputV2.selectors.qm); + } + + getFileListDiv() { + return this.element.querySelector(FileInputV2.selectors.fileListDiv); + } + + #getAttachButtonLabel() { + return this.element.querySelector(FileInputV2.selectors.attachButtonLabel); + } + + updateValue(value) { + if (this.widgetObject == null) { + this.widgetObject = new FileInputWidgetV2(this.widgetFields); + } + this.widgetObject.setValue(value); + super.updateEmptyStatus(); + } + + setModel(model) { + super.setModel(model); + if (this.widgetObject == null) { + this.widgetObject = new FileInputWidgetV2(this.widgetFields); + } + } + + #syncWidget() { + let widgetElement = this.getWidget ? this.getWidget() : null; + if (widgetElement) { + widgetElement.id = this.getId() + "__widget"; + this.#getAttachButtonLabel().setAttribute('for', this.getId() + "__widget"); + } + + } + + /* + We are overriding the syncLabel method of the FormFieldBase class because for all components, + we pass the widgetId in 'for' attribute. However, for the file input component, + we already have a widget, so we should not pass the widgetId twice + */ + #syncLabel() { + let labelElement = typeof this.getLabel === 'function' ? this.getLabel() : null; + if (labelElement) { + labelElement.setAttribute('for', this.getId()); + } + } + + syncMarkupWithModel() { + super.syncMarkupWithModel(); + this.#syncWidget(); + this.#syncLabel(); + } + } + + FormView.Utils.setupField(({element, formContainer}) => { + return new FileInputV2({element, formContainer}) + }, FileInputV2.selectors.self); + +})(); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputwidget.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputwidget.js new file mode 100644 index 0000000000..6345324b7d --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/clientlibs/site/js/fileinputwidget.js @@ -0,0 +1,495 @@ +/******************************************************************************* + * Copyright 2022 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +/** + * This class is responsible for interacting with the file input widget. It implements the file preview, + * file list, handling invalid file size, file name, file mime type functionality + */ + +if (typeof window.FileInputWidgetV2 === 'undefined') { + window.FileInputWidgetV2 = class { + #fileItemSelector='.cmp-adaptiveform-fileinput__fileitem' + #widget=null + #fileArr=[] + #fileList=null + #lang="en" + #model=null // passed by reference + #isFileUpdate=false // handle safari state + #options=null // initialize options + #regexMimeTypeList=[] // initialize + #values=[] // initialize + #invalidFeature={ + "SIZE":1, + "NAME":2, + "MIMETYPE":3 + } + #initialFileValueFileNameMap; + + constructor(widgetFields) { + // initialize the widget and model + this.#widget = widgetFields.widget; + this.#model = widgetFields.model(); + this.#fileList = widgetFields.fileListDiv; + // get the current lang + this.#lang = this.#model.form._jsonModel.lang; // todo: change this later, once API is added in af-core + // initialize options for backward compatibility + this.#options = Object.assign({}, { + "contextPath" : "" + }, this.#model._jsonModel); + this.#attachEventHandlers(widgetFields?.widget, widgetFields?.dragArea); + // initialize the regex initially + this.#regexMimeTypeList = this.#options.accept.map(function (value, i) { + try { + return new RegExp(value.trim()); + } catch (e) { + // failure during regex parsing, don't return anything specific to this value since the value contains + // incorrect regex string + if(window.console) { + console.log(e); + } + } + }); + } + + #attachEventHandlers(widget, dragArea, model) { + widget.addEventListener('change', (e)=> { + this.#handleChange(e?.target?.files); + }); + dragArea.addEventListener("dragover", (event)=>{ + event.preventDefault(); + }); + dragArea.addEventListener("drop", (event)=>{ + event.preventDefault(); + this.#handleChange(event?.dataTransfer?.files); + }); + dragArea.addEventListener("paste", (event)=>{ + event.preventDefault(); + this.#handleChange(event?.clipboardData?.files); + }); + } + + // checks if file name is valid or not to prevent security threats + static isValid(fname) { + let rg1=/^[^\\/:\*\;\$\%\?"<>\|]+$/; // forbidden characters \ / : * ? " < > | ; % $ + let rg2=/^\./; // cannot start with dot (.) + let rg3=/^(nul|prn|con|lpt[0-9]|com[0-9])(\.|$)/i; // forbidden file names + return rg1.test(fname) && !rg2.test(fname) && !rg3.test(fname); + } + + /*** + * Finds the value in the array, if the value is a url then it uses the filename in the url to search for the text + * This is done since our model stores the URL too in case of draft restore or clicking on save in guide + * @param text string representing the text of which the index is to be found + * @param elem reference to the jquery element. This is used if there are duplicate file names present in the file upload. + * @returns {number} + * @private + */ + #getIndexOfText (text, elem){ + let index = -1, + self = this, + isDuplicatePresent = false; + this.#values.find(function(value, iter){ + // if value is a url, then compare with last + let tempValue = value, + // can't use getOrElse here since value can have "." in URL and getOrElse splits based on period to find key inside object + fileName = (typeof self.#initialFileValueFileNameMap === "object" && typeof self.#initialFileValueFileNameMap[value] !== undefined) ? self._initialFileValueFileNameMap[value] : null; + if(tempValue.match(/\//g) && tempValue.match(/\//g).length > 1){ + tempValue = value.substring(value.lastIndexOf("/")+1); + } + // we pass file name explicity as options, if passed use that as fallback to find the URL + if(tempValue === text || fileName === text){ + index = iter; + isDuplicatePresent = self.#values.indexOf(value, index + 1) !== -1; + if(elem && isDuplicatePresent){ + // now check if duplicate present and get its correct index + // today all files are wrapped under .guide-fu-fileItem node + index = elem.closest(self.#fileItemSelector).index(); + } + // check if there is a duplicate + // this is to just break the loop + return value; + } + }); + return index; + } + + static previewFileUsingObjectUrl(file) { + if (file) { + if (window.navigator && window.navigator.msSaveOrOpenBlob) { // for IE + window.navigator.msSaveOrOpenBlob(file, file.name); + } else { + let url = window.URL.createObjectURL(file); + window.open(url, '', 'scrollbars=no,menubar=no,height=600,width=800,resizable=yes,toolbar=no,status=no'); + return url; + } + } + } + + static previewFile (event){ + let url = arguments[1].fileUrl; + let lastIndex = url.lastIndexOf('/'); + //to make sure url has a slash '/' + // added check for query param since sas url contains query params & does not have file name, encoding is not required in this case + if(lastIndex >= 0 && url.indexOf('?') === -1) { + //encode the filename after last slash to ensure the handling of special characters + url = url.substr(0, lastIndex) +'/'+ encodeURIComponent(url.substr(lastIndex + 1)); + } + // this would work for dataURl or normal URL + window.open(url, '', 'scrollbars=no,menubar=no,height=600,width=800,resizable=yes,toolbar=no,status=no'); + + } + // this function maintains a map for + #handleFilePreview (event){ + let elem = event.target, + text = elem.textContent, + index = this.#getIndexOfText(text, elem), + fileDom = null, + fileName = null, + fileUrl = null; + + // for draft usecase, if text contains "/" in it, it means the file is already uploaded + // text should contain the path, assuming that the fileUrl is stored in data element + if (index !== -1) { + // Store the url of file as data + if(typeof elem.dataset.key !== "undefined") + fileUrl = elem.dataset.key; + + if(fileUrl) { + //prepend context path if not already appended + if (!(fileUrl.lastIndexOf(this.#options.contextPath, 0) === 0)) { + fileUrl = this.#options.contextPath + fileUrl; + } + FileInputWidgetV2.previewFile.apply(this, [null, {"fileUrl" : fileUrl}]); + } else { + // todo: add support here + //let previewFileObjIdx = this._getFileObjIdx(index); + let previewFile = this.#fileArr[index]?.data; + let objectUrl = FileInputWidgetV2.previewFileUsingObjectUrl(previewFile); + if (objectUrl) { + elem.dataset.objectUrl = objectUrl; + } + } + } + } + + /** + * This event listener gets called on click of close button in file upload + * + * @param event + */ + #handleClick (event){ + let elem = event.target, + text = elem.parentElement.previousSibling.textContent, + index = this.#getIndexOfText(text, elem), + url = elem.parentElement.previousSibling.dataset.key, + objectUrl = elem.parentElement.previousSibling.dataset.objectUrl; + if (index !== -1) { + this.#values.splice(index, 1); + this.#fileArr.splice(index, 1); + // set the model with the new value + this.#model.value = this.#fileArr; + // value and fileArr contains items of both URL and file types, hence while removing from DOM + // get the correct index as per this.#widget.files + let domIndex = Array.from(this.#widget.files).findIndex(function(file) { + return file.name === text; + }); + this.#deleteFilesFromInputDom([domIndex]); + if (url != null) { + // remove the data so that others don't use this url + delete elem.parentElement.previousSibling.dataset.key; + } + if(objectUrl) { + // revoke the object URL to avoid memory leaks in browser + // since file is anyways getting deleted, remove the object URL's too + window.URL.revokeObjectURL(objectUrl); + } + } + // Remove the dom from view + //All bound events and jQuery data associated with the element are also removed + elem.parentElement.parentElement.remove(); + // Set the focus on file upload button after click of close + this.#widget.focus(); + + } + + #formatBytes(bytes, decimals = 0) { + if (!+bytes) return '0 Bytes' + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ['bytes', 'kb', 'mb', 'gb', 'tb', 'pb'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` + } + + #fileItem(fileName, fileSize, comment, fileUrl) { + let self = this; + let fileItem = document.createElement('li'); + fileItem.setAttribute("class", "cmp-adaptiveform-fileinput__fileitem"); + let fileNameDom = document.createElement('span'); + let fileSizeDom = document.createElement('span'); + + fileSizeDom.setAttribute('class', "cmp-adaptiveform-fileinput__filesize"); + fileSizeDom.textContent = this.#formatBytes(fileSize); + + fileNameDom.setAttribute('tabindex', '0'); + fileNameDom.setAttribute('class', "cmp-adaptiveform-fileinput__filename"); + fileNameDom.setAttribute('aria-label', fileName); + fileNameDom.textContent = fileName; + fileNameDom.addEventListener('keypress', function(e) { + if (e.keyCode === 13 || e.charCode === 32) { + e.target.click(); + } + }); + fileNameDom.addEventListener('click', function(e) { + self.#handleFilePreview(e); + }); + fileItem.appendChild(fileNameDom); + if(fileUrl != null){ + fileNameDom.dataset.key = fileUrl; + } + let fileEndContainer = document.createElement('span'); + let fileClose = document.createElement('span'); + fileEndContainer.setAttribute('class', "cmp-adaptiveform-fileinput__fileendcontainer"); + fileClose.setAttribute('tabindex', '0'); + fileClose.setAttribute('class', "cmp-adaptiveform-fileinput__filedelete"); + fileClose.setAttribute('aria-label', FormView.LanguageUtils.getTranslatedString(this.#lang, "FileCloseAccessText") + fileName); + fileClose.setAttribute('role', 'button'); + fileClose.textContent = "x"; + fileClose.addEventListener('keypress', function(e) { + if (e.keyCode === 13 || e.charCode === 32) { + e.target.click(); + } + }); + fileClose.addEventListener('click', function(e) { + self.#handleClick(e); + }); + fileEndContainer.appendChild(fileSizeDom); + fileEndContainer.appendChild(fileClose); + fileItem.appendChild(fileEndContainer); + return fileItem; + } + + #isMultiSelect() { + return this.#options.type === "file[]" || this.#options.type === "string[]"; + } + + #showFileList(fileName, fileSize, comment, fileUrl) { + if(!this.#isMultiSelect() || fileName === null || typeof fileName === "undefined") { + // if not multiselect, remove all the children of file list + while (this.#fileList.lastElementChild) { + this.#fileList.removeChild(this.#fileList.lastElementChild); + } + } + + // Add the file item + // On click of close, remove the element and update the model + // handle on click of preview button + if(fileName != null) { + this.#fileList.append(this.#fileItem(fileName, fileSize, comment, fileUrl)); + } + } + + #showInvalidMessage(fileName, invalidFeature){ + let that = this; + let IS_IPAD = navigator.userAgent.match(/iPad/i) !== null, + IS_IPHONE = (navigator.userAgent.match(/iPhone/i) !== null); + if(IS_IPAD || IS_IPHONE){ + setTimeout(function() { + that.#invalidMessage(that,fileName, invalidFeature); + }, 0); + } + else { + this.#invalidMessage(fileName, invalidFeature); + } + } + + #invalidMessage(fileName, invalidFeature){ + // todo: have add localization here + if(invalidFeature === this.#invalidFeature.SIZE) { + alert(FormView.LanguageUtils.getTranslatedString(this.#lang, "FileSizeGreater", [fileName, this.#options.maxFileSize])); + } else if (invalidFeature === this.#invalidFeature.NAME) { + alert(FormView.LanguageUtils.getTranslatedString(this.#lang, "FileNameInvalid", [fileName])); + } else if (invalidFeature === this.#invalidFeature.MIMETYPE) { + alert(FormView.LanguageUtils.getTranslatedString(this.#lang, "FileMimeTypeInvalid", [fileName])); + } + } + + /* + * This function returns FileList object of the passed file array + */ + #getFileListItem(files) { + try { + let dataContainer = new DataTransfer() || (new ClipboardEvent("")).clipboardData; + files.forEach(function (file) { + dataContainer.items.add(file); + }); + return dataContainer.files; + } catch(err) { + console.error(err); + throw err; + } + } + + #updateFilesInDom(files) { + // in safari, a change event is trigged if files property is changed dynamically + // hence adding this check to clear existing state only for safari browsers + this.#isFileUpdate = true; + this.#widget.files = this.#getFileListItem(files); + this.#isFileUpdate = false; + } + + /* + * This function deletes files at specified indexes from input dom elt + */ + #deleteFilesFromInputDom(deletedIndexes) { + let remainingFiles = []; + Array.from(this.#widget.files).forEach(function(file,idx){ + if(!deletedIndexes.includes(idx)){ + remainingFiles.push(file); + } + }); + try { + // in safari, a change event is trigged if files property is changed dynamically + // hence adding this check to clear existing state only for safari browsers + this.#updateFilesInDom(remainingFiles); + } catch(err){ + console.error("Deleting files is not supported in your browser"); + } + } + + setValue(value) { + let isValueSame = false; + if (value != null) { + // change to array since we store array internally + value = Array.isArray(value) ? value : [value]; + } + // check if current values and new values are same + if (this.#fileArr != null && value != null) { + isValueSame = JSON.stringify(FormView.FileAttachmentUtils.extractFileInfo(this.#fileArr)) === JSON.stringify(value); + } + // if new values, update the DOM + if (!isValueSame) { + let oldUrls = {}; + // Cache the url before deletion + this.#fileList.querySelectorAll(".cmp-adaptiveform-fileinput__filename").forEach(function (elem) { + let url = elem.dataset.key; + if (typeof url !== "undefined") { + let fileName = url.substring(url.lastIndexOf("/") + 1); + oldUrls[fileName] = url; + } + }); + // empty the file list + while (this.#fileList.lastElementChild) { + this.#fileList.removeChild(this.#fileList.lastElementChild); + } + // set the new value + if (value != null) { + let self = this; + // Update the value array with the file + this.#values = value.map(function (file, index) { + // Check if file Name is a path, if yes get the last part after "/" + let isFileObject= window.File ? file.data instanceof window.File : false, + fileName = typeof file === "string" ? file : file.name, + fileUrl = typeof file === "string" ? file : (isFileObject ? "" : file.data), + fileSize = file.size, + fileUploadUrl = fileUrl; + if (oldUrls[fileName]) { + fileUploadUrl = oldUrls[fileName]; + } + self.#showFileList(fileName, fileSize, null, fileUploadUrl); + return fileName; + }); + this.#fileArr = [...value]; + } + } + } + + #handleChange(filesUploaded) { + if (!this.#isFileUpdate) { + let currFileName = '', + inValidSizefileNames = '', + inValidNamefileNames = '', + inValidMimeTypefileNames = '', + files = filesUploaded; + // Initially set the invalid flag to false + let isInvalidSize = false, + isInvalidFileName = false, + isInvalidMimeType = false; + //this.resetIfNotMultiSelect(); + if (typeof files !== "undefined") { + let invalidFilesIndexes = []; + Array.from(files).forEach(function (file, fileIndex) { + let isCurrentInvalidFileSize = false, + isCurrentInvalidFileName = false, + isCurrentInvalidMimeType = false; + currFileName = file.name.split("\\").pop(); + // Now size is in MB + let size = file.size / 1024 / 1024; + // check if file size limit is within limits + if ((size > parseFloat(this.#options.maxFileSize))) { + isInvalidSize = isCurrentInvalidFileSize = true; + inValidSizefileNames = currFileName + "," + inValidSizefileNames; + } else if (!FileInputWidgetV2.isValid(currFileName)) { + // check if file names are valid (ie) there are no control characters in file names + isInvalidFileName = isCurrentInvalidFileName = true; + inValidNamefileNames = currFileName + "," + inValidNamefileNames; + } else if (file.type) { + let isMatch = this.#regexMimeTypeList.some(function (rx) { + return rx.test(file.type); + }); + if (!isMatch) { + isInvalidMimeType = isCurrentInvalidMimeType = true; + inValidMimeTypefileNames = currFileName + "," + inValidMimeTypefileNames; + } + } + + // if the file is not invalid, show it and push it to internal array + if (!isCurrentInvalidFileSize && !isCurrentInvalidFileName && !isCurrentInvalidMimeType) { + this.#showFileList(currFileName); + if(this.#isMultiSelect()) { + this.#values.push(currFileName); + this.#fileArr.push(file); + } else { + this.#values = [currFileName]; + this.#fileArr = [file]; + } + } else { + invalidFilesIndexes.push(fileIndex); + } + + + }, this); + + if (invalidFilesIndexes.length > 0 && this.#widget !== null) { + this.#deleteFilesFromInputDom(invalidFilesIndexes); + } + } + + // set the new model value + this.#model.value = this.#fileArr; + + if (isInvalidSize) { + this.#showInvalidMessage(inValidSizefileNames.substring(0, inValidSizefileNames.lastIndexOf(',')), this.#invalidFeature.SIZE); + } else if (isInvalidFileName) { + this.#showInvalidMessage(inValidNamefileNames.substring(0, inValidNamefileNames.lastIndexOf(',')), this.#invalidFeature.NAME); + } else if (isInvalidMimeType) { + this.#showInvalidMessage(inValidMimeTypefileNames.substring(0, inValidMimeTypefileNames.lastIndexOf(',')), this.#invalidFeature.MIMETYPE); + } + } + } + } +} \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.html b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.html new file mode 100644 index 0000000000..665fc971a5 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.html @@ -0,0 +1,66 @@ + + +
+
+
+
+
+
+
+
+
+
${file.fileInputDragDropText @context='html'}
+ + +
+
    +
    +
    +
    +
    +
    +
    + + + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.js new file mode 100644 index 0000000000..2d186985af --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v2/fileinput/fileinput.js @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright 2023 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +use(function () { + + var clientlibsArr = ['core.forms.components.base.v1.editor']; + var labelPath = 'core/fd/components/af-commons/v1/fieldTemplates/label.html'; + var shortDescriptionPath = "core/fd/components/af-commons/v1/fieldTemplates/shortDescription.html"; + var longDescriptionPath = "core/fd/components/af-commons/v1/fieldTemplates/longDescription.html"; + var questionMarkPath = "core/fd/components/af-commons/v1/fieldTemplates/questionMark.html" + var errorMessagePath = "core/fd/components/af-commons/v1/fieldTemplates/errorMessage.html"; + return { + labelPath: labelPath, + shortDescriptionPath: shortDescriptionPath, + longDescriptionPath: longDescriptionPath, + questionMarkPath: questionMarkPath, + errorMessagePath: errorMessagePath, + clientlibs: clientlibsArr + } +}); \ No newline at end of file diff --git a/ui.tests/test-module/specs/fileinput/fileinput.authoring.spec.js b/ui.tests/test-module/specs/fileinput/fileinput.authoring.spec.js index 2c5b2a7ae5..8a69d81fd0 100644 --- a/ui.tests/test-module/specs/fileinput/fileinput.authoring.spec.js +++ b/ui.tests/test-module/specs/fileinput/fileinput.authoring.spec.js @@ -53,6 +53,7 @@ describe('Page - Authoring', function () { .should("exist"); cy.get("[name='./accept']") .should("exist"); + cy.get("[name='./fileInputDragDropText']").should("exist"); // Checking some dynamic behaviours cy.get("[name='./multiSelection'][type=\"checkbox\"]").should("exist").check(); diff --git a/ui.tests/test-module/specs/fileinput/fileinput.runtime.spec.js b/ui.tests/test-module/specs/fileinput/fileinput.runtime.spec.js index 83b1194685..bf5a2ce698 100644 --- a/ui.tests/test-module/specs/fileinput/fileinput.runtime.spec.js +++ b/ui.tests/test-module/specs/fileinput/fileinput.runtime.spec.js @@ -237,6 +237,36 @@ describe("Form with File Input - Prefill & Submit tests", () => { submitTest(); }) + it.only(`${fileInput.type} - attach files using drag and drop, check model, view, preview attachment and submit the form`, () => { + cy.previewForm(pagePath, { + onBeforeLoad : (win) => { + cy.stub(win, 'open'); // creating a stub to check file preview + } + }); + + // attach the file + cy.get(fileInput.selector).attachFile(fileInput.fileNames, { subjectType: 'drag-n-drop', events: ['dragover', 'drop'] }); + if(fileInput.multiple) + cy.get(fileInput.selector).attachFile('sample2.txt'); + + // check for successful attachment of file in the view + checkFileNamesInFileAttachmentView(fileInput.selector, fileInput.fileNames); + if(fileInput.multiple) + checkFileNamesInFileAttachmentView(fileInput.selector, ['sample2.txt']); + + // check preview of the file + checkFilePreviewInFileAttachment(fileInput.selector); + + if(fileInput.multiple) + deleteSelectedFiles(fileInput.selector, ['sample2.txt']); + + // submit the form + cy.get(".cmp-adaptiveform-button__widget").click(); + + // check for successful submission + submitTest(); + }) + it(`${fileInput.type} - view prefill of submitted form, make changes to attachments and submit`, () => { cy.get("@prefillId").then(id => { cy.previewForm(pagePath, {