diff --git a/core/src/main/java/com/exadel/aem/toolkit/api/annotations/meta/ResourceTypes.java b/core/src/main/java/com/exadel/aem/toolkit/api/annotations/meta/ResourceTypes.java
index 6d187b418..6ef7e03e2 100644
--- a/core/src/main/java/com/exadel/aem/toolkit/api/annotations/meta/ResourceTypes.java
+++ b/core/src/main/java/com/exadel/aem/toolkit/api/annotations/meta/ResourceTypes.java
@@ -29,6 +29,7 @@ public class ResourceTypes {
public static final String BUTTON = "granite/ui/components/coral/foundation/button";
public static final String BUTTON_GROUP = "granite/ui/components/coral/foundation/form/buttongroup";
public static final String CHECKBOX = "granite/ui/components/coral/foundation/form/checkbox";
+ public static final String CODE_EDITOR = "etoolbox-authoring-kit/components/authoring/codeeditor";
public static final String COLORFIELD = "granite/ui/components/coral/foundation/form/colorfield";
public static final String CONTAINER = "granite/ui/components/coral/foundation/container";
public static final String CORAL_FILEUPLOAD = "granite/ui/components/coral/foundation/form/fileupload";
diff --git a/core/src/main/java/com/exadel/aem/toolkit/api/annotations/widgets/codeeditor/CodeEditor.java b/core/src/main/java/com/exadel/aem/toolkit/api/annotations/widgets/codeeditor/CodeEditor.java
new file mode 100644
index 000000000..0d3e3f352
--- /dev/null
+++ b/core/src/main/java/com/exadel/aem/toolkit/api/annotations/widgets/codeeditor/CodeEditor.java
@@ -0,0 +1,79 @@
+/*
+ * 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.exadel.aem.toolkit.api.annotations.widgets.codeeditor;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.exadel.aem.toolkit.api.annotations.meta.ResourceType;
+import com.exadel.aem.toolkit.api.annotations.meta.ResourceTypes;
+import com.exadel.aem.toolkit.api.annotations.meta.ValueRestriction;
+import com.exadel.aem.toolkit.api.annotations.meta.ValueRestrictions;
+
+/**
+ * Used to set up a syntax-highlighting code editor withing a Touch UI dialog or page. Default implementation is based
+ * on the open-source Ace Editor
+ */
+
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ResourceType(ResourceTypes.CODE_EDITOR)
+public @interface CodeEditor {
+
+ /**
+ * Specifies a particular link to the editor's {@code js} file in an external repository or CDN. Can be used to
+ * require a specific version or a custom build
+ * @return Optional string value
+ */
+ @ValueRestriction(ValueRestrictions.NOT_BLANK_OR_DEFAULT)
+ String source() default "";
+
+ /**
+ * Defines the code highlighting completion mode of the editor (the "language" or markup format it works with). Must
+ * match one of the built-in modes enumerated in the project's
+ * repository (unless a custom mode is supplied). By default, the {@code json} mode is used
+ * @return Optional string value
+ */
+ @ValueRestriction(ValueRestrictions.NOT_BLANK_OR_DEFAULT)
+ String mode() default "";
+
+ /**
+ * Defines the visual and code highlighting theme of the editor. Must match one of the built-in themes enumerated in
+ * the project's repository (unless a
+ * custom theme is supplied). By default, the {@code crimson_editor} theme is used
+ * @return Optional string value
+ */
+ @ValueRestriction(ValueRestrictions.NOT_BLANK_OR_DEFAULT)
+ String theme() default "";
+
+ /**
+ * Declares options that will be passed to the {@code CodeEditor} upon initialization. Every option is a key-value
+ * pair. The option keys originate from the editor's
+ * API
+ * @return Optional array of {@link CodeEditorOption} objects
+ */
+ CodeEditorOption[] options() default {};
+
+ /**
+ * When set, specifies the string marker prepended to the data stored in JCR for the current field. E.g., if the
+ * visible value of the field is {@code "Hello World"} and the {@code dataPrefix = "text:"} is specified, to the JCR
+ * goes {@code "text:Hello World"}. This can be used to distinguish between snippets written in different languages,
+ * or else to prevent AEM from falsely processing the field value as some Granite content (e.g. when storing JSON
+ * strings in a multifield)
+ * @return Optional string value
+ */
+ String dataPrefix() default "";
+}
diff --git a/core/src/main/java/com/exadel/aem/toolkit/api/annotations/widgets/codeeditor/CodeEditorOption.java b/core/src/main/java/com/exadel/aem/toolkit/api/annotations/widgets/codeeditor/CodeEditorOption.java
new file mode 100644
index 000000000..7ad4179ab
--- /dev/null
+++ b/core/src/main/java/com/exadel/aem/toolkit/api/annotations/widgets/codeeditor/CodeEditorOption.java
@@ -0,0 +1,39 @@
+/*
+ * 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.exadel.aem.toolkit.api.annotations.widgets.codeeditor;
+
+/**
+ * Represents a configuration option passed to a {@code CodeEditor} upon initialization
+ */
+public @interface CodeEditorOption {
+
+ /**
+ * Defines the name of the option
+ * @return String value, non-blank
+ */
+ String name();
+
+ /**
+ * Defines the value of the option
+ * @return String value
+ */
+ String value();
+
+ /**
+ * Defines the type of the option that will be used with the initialization script. Expected values are {@code
+ * String} (default), {@code Boolean}, and {@code Integer}
+ * @return Class reference
+ */
+ Class> type() default String.class;
+}
diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/CoreConstants.java b/core/src/main/java/com/exadel/aem/toolkit/core/CoreConstants.java
index 8ed367ac9..35bbb3d68 100644
--- a/core/src/main/java/com/exadel/aem/toolkit/core/CoreConstants.java
+++ b/core/src/main/java/com/exadel/aem/toolkit/core/CoreConstants.java
@@ -21,15 +21,18 @@ public class CoreConstants {
public static final String NN_LIST = "list";
public static final String PN_APPEND = "append";
+ public static final String PN_DISABLED = "disabled";
public static final String PN_ITEM_RESOURCE_TYPE = "itemResourceType";
public static final String PN_LIMIT = "limit";
public static final String PN_LIST_ITEM = "listItem";
public static final String PN_OFFSET = "offset";
public static final String PN_PATH = "path";
public static final String PN_PREPEND = "prepend";
+ public static final String PN_REQUIRED = "required";
public static final String PN_SELECTED = "selected";
public static final String PN_TEXT = "text";
public static final String PN_UPDATE_COMPONENT_LIST = "updatecomponentlist";
+ public static final String PN_VALIDATION = "validation";
public static final String PN_VALUE = "value";
public static final String PARAMETER_ID = "@id";
diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/BaseModel.java b/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/BaseModel.java
index afc9b658b..3119cf625 100644
--- a/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/BaseModel.java
+++ b/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/BaseModel.java
@@ -14,6 +14,7 @@
package com.exadel.aem.toolkit.core.authoring.models;
import java.util.Map;
+import javax.annotation.PostConstruct;
import javax.script.SimpleBindings;
import org.apache.commons.lang3.StringUtils;
@@ -25,11 +26,18 @@
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import com.adobe.granite.ui.components.AttrBuilder;
+import com.adobe.granite.ui.components.Config;
+import com.adobe.granite.ui.components.Value;
import com.adobe.granite.ui.components.htl.ComponentHelper;
import com.exadel.aem.toolkit.api.annotations.main.AemComponent;
import com.exadel.aem.toolkit.core.CoreConstants;
+/**
+ * Presents the basic logic for rendering Granite UI components with the use of Sling models and HTL markup. This class
+ * manages only the common component properties and is expected to be extended with component-specific Sling models for
+ * all cases but the most basic ones
+ */
@Model(adaptables = SlingHttpServletRequest.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@AemComponent(
path = "components/authoring/base",
@@ -38,6 +46,9 @@
)
public class BaseModel {
+ private static final String ATTRIBUTE_ARIA_REQUIRED = "aria-required";
+ private static final String ATTRIBUTE_FOUNDATION_VALIDATION = "data-foundation-validation";
+
@SlingObject
private SlingHttpServletRequest request;
@@ -46,14 +57,35 @@ public class BaseModel {
private LocalComponentHelper componentHelper;
+ /**
+ * Performs post-inject model initialization
+ */
+ @PostConstruct
+ private void init() {
+ componentHelper = new LocalComponentHelper(request);
+ }
+
+ /**
+ * Retrieves common attributes of a Granit UI component such as {@code required}, {@code disabled}, etc. These
+ * attributes are typically rendered to the main container of the component's HTML markup
+ * @return Map of string values
+ */
public Map getCommonAttributes() {
- return getComponentHelper().getCommonAttributes();
+ return componentHelper.getCommonAttributes();
}
+ /**
+ * Retrieves the value of the {@code name} attribute of the editable component
+ * @return String value; a non-blank string is expected
+ */
public String getName() {
return name;
}
+ /**
+ * Retrieves the value of the editable component
+ * @return A nullable string value
+ */
public Object getValue() {
if (request == null
|| request.getRequestPathInfo().getSuffixResource() == null
@@ -61,54 +93,90 @@ public Object getValue() {
return getDefaultValue();
}
- Resource suffixResource = request.getRequestPathInfo().getSuffixResource();
- String relativePath = name.contains(CoreConstants.SEPARATOR_SLASH)
- ? StringUtils.substringBeforeLast(name, CoreConstants.SEPARATOR_SLASH)
- : StringUtils.EMPTY;
+ Resource dataResource = getDataResource();
+ Config config = new Config(dataResource);
+ Value value = new Value(request, config);
+
String propertyName = name.contains(CoreConstants.SEPARATOR_SLASH)
? StringUtils.substringAfterLast(name, CoreConstants.SEPARATOR_SLASH)
: name;
-
- Resource endResource = StringUtils.isNotBlank(relativePath)
- ? request.getResourceResolver().getResource(suffixResource, relativePath)
- : suffixResource;
- if (endResource == null) {
- return getDefaultValue();
- }
-
- return endResource.getValueMap().get(propertyName);
+ return value.get(CoreConstants.RELATIVE_PATH_PREFIX + propertyName);
}
-
+ /**
+ * Retrieves the value which is rendered when no user-defined value is set for this editable component
+ * @return A nullable string value
+ */
public Object getDefaultValue() {
return null;
}
- private LocalComponentHelper getComponentHelper() {
- if (componentHelper == null) {
- componentHelper = new LocalComponentHelper(request);
- }
- return componentHelper;
+ /**
+ * Retrieves the {@link Resource} that contains data for the current component
+ * @return {@code Resource} object
+ */
+ private Resource getDataResource() {
+ Resource suffixResource = request.getRequestPathInfo().getSuffixResource();
+ String relativePath = name.contains(CoreConstants.SEPARATOR_SLASH)
+ ? StringUtils.substringBeforeLast(name, CoreConstants.SEPARATOR_SLASH)
+ : StringUtils.EMPTY;
+
+ return StringUtils.isNotBlank(relativePath)
+ ? request.getResourceResolver().getResource(suffixResource, relativePath)
+ : suffixResource;
}
+ /**
+ * Implements {@link ComponentHelper} to provide common HTML attributes for the Granite UI components rendered with
+ * Sling models and HTL
+ */
private static class LocalComponentHelper extends ComponentHelper {
+ private SlingHttpServletRequest request;
+ /**
+ * Creates a new {@link ComponentHelper} instance
+ * @param request {@code SlingHttpServletRequest} used to construct the current component
+ */
public LocalComponentHelper(SlingHttpServletRequest request) {
if (request == null) {
return;
}
+ this.request = request;
init(new SimpleBindings((SlingBindings) request.getAttribute(SlingBindings.class.getName())));
}
+ /**
+ * Retrieves common HTML attributes for the Granite UI components
+ * @return A non-null {@code Map}
+ */
public Map getCommonAttributes() {
AttrBuilder attrBuilder = getInheritedAttrs();
populateCommonAttrs(attrBuilder);
- return attrBuilder.getData();
+ Map result = attrBuilder.getData();
+ if (request == null) {
+ return result;
+ }
+ if (request.getResource().getValueMap().get(CoreConstants.PN_DISABLED, false)) {
+ result.put(CoreConstants.PN_DISABLED, Boolean.TRUE.toString());
+ }
+ if (request.getResource().getValueMap().get(CoreConstants.PN_REQUIRED, false)) {
+ result.put(ATTRIBUTE_ARIA_REQUIRED, Boolean.TRUE.toString());
+ }
+ String validation = StringUtils.join(
+ CoreConstants.SEPARATOR_COMMA,
+ request.getResource().getValueMap().get(CoreConstants.PN_VALIDATION, String[].class));
+ if (StringUtils.isNotBlank(validation)) {
+ result.put(ATTRIBUTE_FOUNDATION_VALIDATION, validation);
+ }
+ return result;
}
+ /**
+ * A required stub method according the {@code ComponentHelper} contract
+ */
@Override
protected void activate() {
- // Not implemented
+ // Not Implemented
}
}
}
diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/CodeEditor.java b/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/CodeEditor.java
new file mode 100644
index 000000000..e6fd2ce90
--- /dev/null
+++ b/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/CodeEditor.java
@@ -0,0 +1,114 @@
+/*
+ * 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.exadel.aem.toolkit.core.authoring.models;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.models.annotations.Default;
+import org.apache.sling.models.annotations.DefaultInjectionStrategy;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
+
+import com.exadel.aem.toolkit.api.annotations.main.AemComponent;
+
+/**
+ * Extends {@link BaseModel} to provide additional facilities for rendering {@link
+ * com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor}-defined widgets in Granite UI
+ */
+@Model(adaptables = SlingHttpServletRequest.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
+@AemComponent(
+ path = "components/authoring/codeeditor",
+ title = "CodeEditor",
+ resourceSuperType = "etoolbox-authoring-kit/components/authoring/base"
+)
+public class CodeEditor extends BaseModel {
+
+ private static final String DEFAULT_THEME = "crimson_editor";
+ private static final String DEFAULT_MODE = "json";
+
+ @ValueMapValue
+ private String source;
+
+ @ValueMapValue
+ @Default(values = DEFAULT_THEME)
+ private String theme;
+
+ @ValueMapValue
+ @Default(values = DEFAULT_MODE)
+ private String mode;
+
+ @ValueMapValue
+ private String options;
+
+ @ValueMapValue
+ private String dataPrefix;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object getValue() {
+ String result = ObjectUtils.defaultIfNull(super.getValue(), StringUtils.EMPTY).toString();
+ if (StringUtils.isNotBlank(dataPrefix) && StringUtils.startsWith(result, dataPrefix)) {
+ return result.substring(dataPrefix.length());
+ }
+ return result;
+ }
+
+ /**
+ * Retrieves the {@code source} value as defined by the user for the current component
+ * @return A nullable string value
+ * @see com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor#source()
+ */
+ public String getSource() {
+ return source;
+ }
+
+ /**
+ * Retrieves the {@code theme} value as defined by the user for the current component
+ * @return A nullable string value
+ * @see com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor#theme()
+ */
+ public String getTheme() {
+ return theme;
+ }
+
+ /**
+ * Retrieves the language {@code mode} value as defined by the user for the current component
+ * @return A nullable string value; can be empty
+ * @see com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor#mode()
+ */
+ public String getMode() {
+ return mode;
+ }
+
+ /**
+ * Retrieves initialization options for the current component
+ * @return A nullable string value
+ * @see com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor#options()
+ */
+ public String getOptions() {
+ return options;
+ }
+
+ /**
+ * Retrieves the {@code dataPrefix} value as defined by the user for the current component
+ * @return A nullable string value
+ * @see com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor#dataPrefix() ()
+ */
+ public String getDataPrefix() {
+ return dataPrefix;
+ }
+}
diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/IgnoreFreshnessToggler.java b/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/IgnoreFreshnessToggler.java
index 11be0475e..0925dfd11 100644
--- a/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/IgnoreFreshnessToggler.java
+++ b/core/src/main/java/com/exadel/aem/toolkit/core/authoring/models/IgnoreFreshnessToggler.java
@@ -39,7 +39,7 @@ public class IgnoreFreshnessToggler {
private SlingHttpServletRequest request;
/**
- * Sets or unsets the {@code forceIgnoreFreshness} flag to the Sling HTTP requests upon this Sling model initialization
+ * Performs post-inject model initialization
*/
@PostConstruct
private void init() {
diff --git a/docs/content/dev-tools/component-management/dialog-fields/widgets.md b/docs/content/dev-tools/component-management/dialog-fields/widgets.md
index 540b19aa8..2f05f93f9 100644
--- a/docs/content/dev-tools/component-management/dialog-fields/widgets.md
+++ b/docs/content/dev-tools/component-management/dialog-fields/widgets.md
@@ -226,6 +226,45 @@ public class NestedCheckboxListDialog {
}
```
+### CodeEditor
+* Resource type: /apps/etoolbox-authoring-kit/components/authoring/codeeditor"
+
+Used to render a customizable code editor with syntax highlighting, autocompletion, and similar facilities inside the Granite UI.
+
+Current implementation is based on the freeware open-source [Ace Editor](https://ace.c9.io/).
+
+You can set up the code editor in your dialog as follows:
+```java
+@Dialog
+public class CodeEditorDialog {
+ @DialogField
+ @CodeEditor(
+ source = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.10.0/ace.js",
+ mode = "js",
+ theme = "crimson_editor",
+ options = {
+ @CodeEditorOption(name = "wrap", value = "true", type = boolean.class),
+ @CodeEditorOption(name = "maxLines", value = "Infinity"),
+ }
+ )
+ private String code;
+}
+
+```
+_All_ of the properties of the `@CodeEditor` are optional and have meaningful defaults.
+
+You can specify _source_ to make sure that the code editor's library is fetched from the particular URL. This can be a specific version or a modified build of a library, e.g., packed with specific plugins. (Note: the source is selected once per a dialog; you cannot specify different sources for different instances of code editor within the same Touch UI experience).
+
+The _mode_ property is responsible for the "language" or markup the current editor supports. See [this link](https://github.com/ajaxorg/ace-builds/tree/master/src-noconflict) for the common variants. Default mode is `json`.
+
+The _theme_ property defines the graphic theme. See [link](https://github.com/ajaxorg/ace-builds/tree/master/src-noconflict) for available options. Default is the "_crimson_editor_" theme.
+
+_Options_ are essentially a set of key-value pairs used to initialize and profile an instance of the editor for different needs. You can specify keys and values that follow the [API reference](https://ajaxorg.github.io/ace-api-docs/index.html). Different instanced of the editor can have different modes, themes, and options even whet situated in the same dialog.
+
+There's also the possibility to set up _dataPrefix_. This is a rather rarely used setting that helps to differentiate code snippets as stored in the JCR and also prevents false handling of a value by the Granite engine. (For instance, if you would like to save a collection of JSON values inside a multifield, Granite could potentially "misunderstand" them for the JSON-encoded contents of the multifield itself, which would lead to a bad display. The use of prefixes helps to "escape" such values.)
+
+_Note:_ we do not modify, adapt, support, or redistribute the code of _Ace Editor_. We use it as-is by referencing a publicly available version of the code directly from the Internet. User experience can change with the release of a new version of _Ace Editor_ which we don't manage or control.
+
### ColorField
* [@ColorField](https://javadoc.io/doc/com.exadel.etoolbox/etoolbox-authoring-kit-core/latest/com/exadel/aem/toolkit/api/annotations/widgets/color/ColorField.html)
diff --git a/plugin/src/main/java/com/exadel/aem/toolkit/plugin/handlers/widgets/CodeEditorHandler.java b/plugin/src/main/java/com/exadel/aem/toolkit/plugin/handlers/widgets/CodeEditorHandler.java
new file mode 100644
index 000000000..2330e15e7
--- /dev/null
+++ b/plugin/src/main/java/com/exadel/aem/toolkit/plugin/handlers/widgets/CodeEditorHandler.java
@@ -0,0 +1,139 @@
+/*
+ * 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.exadel.aem.toolkit.plugin.handlers.widgets;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor;
+import com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditorOption;
+import com.exadel.aem.toolkit.api.handlers.Handler;
+import com.exadel.aem.toolkit.api.handlers.Handles;
+import com.exadel.aem.toolkit.api.handlers.Source;
+import com.exadel.aem.toolkit.api.handlers.Target;
+import com.exadel.aem.toolkit.plugin.maven.PluginRuntime;
+import com.exadel.aem.toolkit.plugin.utils.DialogConstants;
+import com.exadel.aem.toolkit.plugin.utils.StringUtil;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+/**
+ * Implements {@code BiConsumer} to populate a {@link Target} instance with properties originating from a {@link Source}
+ * object that define the {@code CodeEditor} widget look and behavior
+ */
+@Handles(CodeEditor.class)
+public class CodeEditorHandler implements Handler {
+
+ private static final String CLIENTLIB_CATEGORY_EDITOR = "eak.widgets.editor";
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
+ .registerModule(new SimpleModule()
+ .addSerializer(CodeEditorOption[].class, new CodeEditorOptionsSerializer()));
+
+ /**
+ * Processes data that can be extracted from the given {@code Source} and stores it into the provided {@code
+ * Target}
+ * @param source {@code Source} object used for data retrieval
+ * @param target Resulting {@code Target} object
+ */
+ @Override
+ public void accept(Source source, Target target) {
+ CodeEditor codeEditor = source.adaptTo(CodeEditor.class);
+ target.attributes(codeEditor);
+ populateClientLibrary(target);
+ if (ArrayUtils.isEmpty(codeEditor.options())) {
+ return;
+ }
+ try {
+ String options = OBJECT_MAPPER.writeValueAsString(codeEditor.options());
+ target.attribute(DialogConstants.NN_OPTIONS, escapeJson(options));
+ } catch (JsonProcessingException e) {
+ PluginRuntime.context().getExceptionHandler().handle(e);
+ }
+ }
+
+ /**
+ * Conditionally stores the reference to the {@code eak.widgets.editor} client library to the current dialog
+ * @param target Resulting {@code Target} object
+ */
+ private static void populateClientLibrary(Target target) {
+ Target dialogRoot = target.findParent(t -> DialogConstants.NN_ROOT.equals(t.getName()));
+ if (dialogRoot == null) {
+ return;
+ }
+ String extraClientlibs = dialogRoot.getAttribute(DialogConstants.PN_EXTRA_CLIENTLIBS);
+ if (StringUtils.isEmpty(extraClientlibs)) {
+ dialogRoot.attribute(DialogConstants.PN_EXTRA_CLIENTLIBS, new String[]{CLIENTLIB_CATEGORY_EDITOR});
+ } else {
+ Set extraClientlibSet = StringUtil.parseSet(extraClientlibs);
+ extraClientlibSet.add(CLIENTLIB_CATEGORY_EDITOR);
+ dialogRoot.attribute(DialogConstants.PN_EXTRA_CLIENTLIBS, StringUtil.format(extraClientlibSet, String.class));
+ }
+ }
+
+ /**
+ * Prepares the provided JSON string for storing as an XML attribute value
+ * @param value A non-null JSON string
+ * @return String value
+ */
+ private static String escapeJson(String value) {
+ return value.replace("{", "\\{").replace("}", "\\}");
+ }
+
+ /**
+ * Represents {@link JsonSerializer} for storing the configuration set up via {@link CodeEditor} in the content
+ * repository
+ */
+ private static class CodeEditorOptionsSerializer extends JsonSerializer {
+
+ /**
+ * Retrieves a JSON render of the provided {@code CodeEditor} annotation
+ * @param options An array of user-specified {@link CodeEditorOption} values
+ * @param jsonGenerator Managed {@code JsonGenerator} object
+ * @param serializerProvider Managed {@code SerializerProvider} object
+ * @throws IOException if the serialization fails
+ */
+ @Override
+ public void serialize(
+ CodeEditorOption[] options,
+ JsonGenerator jsonGenerator,
+ SerializerProvider serializerProvider) throws IOException {
+
+ jsonGenerator.writeStartObject();
+ for (CodeEditorOption option : options) {
+ if (option.type().equals(Boolean.class) || option.type().equals(boolean.class)) {
+ jsonGenerator.writeBooleanField(option.name(), Boolean.parseBoolean(option.value()));
+ } else if (
+ StringUtils.isNumeric(option.value())
+ && (ClassUtils.primitiveToWrapper(option.type()).equals(Integer.class)
+ || ClassUtils.primitiveToWrapper(option.type()).equals(Long.class))
+ ) {
+ jsonGenerator.writeNumberField(option.name(), Long.parseLong(option.value()));
+ } else {
+ jsonGenerator.writeStringField(option.name(), option.value());
+ }
+ }
+ jsonGenerator.writeEndObject();
+ }
+ }
+}
diff --git a/plugin/src/main/java/com/exadel/aem/toolkit/plugin/handlers/widgets/common/MultipleAnnotationHandler.java b/plugin/src/main/java/com/exadel/aem/toolkit/plugin/handlers/widgets/common/MultipleAnnotationHandler.java
index fe6bba4f5..73ad6520f 100644
--- a/plugin/src/main/java/com/exadel/aem/toolkit/plugin/handlers/widgets/common/MultipleAnnotationHandler.java
+++ b/plugin/src/main/java/com/exadel/aem/toolkit/plugin/handlers/widgets/common/MultipleAnnotationHandler.java
@@ -220,11 +220,11 @@ private static Map getTransferPolicies(Source so
.forEach(property -> transferPolicies.put(CoreConstants.SEPARATOR_AT + property.name(), PropertyTransferPolicy.LEAVE_IN_MULTIFIELD));
// However, we need to override policies for some properties that have been stored in a loop above
transferPolicies.put(CoreConstants.SEPARATOR_AT + DialogConstants.PN_NAME, PropertyTransferPolicy.MOVE_TO_NESTED_NODE);
- transferPolicies.put(CoreConstants.SEPARATOR_AT + DialogConstants.PN_REQUIRED, PropertyTransferPolicy.MOVE_TO_NESTED_NODE);
+ transferPolicies.put(CoreConstants.SEPARATOR_AT + CoreConstants.PN_REQUIRED, PropertyTransferPolicy.MOVE_TO_NESTED_NODE);
transferPolicies.put(CoreConstants.SEPARATOR_AT + DialogConstants.PN_WRAPPER_CLASS, PropertyTransferPolicy.MOVE_TO_NESTED_NODE);
// Some attribute values are expected to be moved or copied though have set to "skipped" above
transferPolicies.put(CoreConstants.SEPARATOR_AT + DialogConstants.PN_PRIMARY_TYPE, PropertyTransferPolicy.COPY_TO_NESTED_NODE);
- transferPolicies.put(CoreConstants.SEPARATOR_AT + DialogConstants.PN_DISABLED, PropertyTransferPolicy.COPY_TO_NESTED_NODE);
+ transferPolicies.put(CoreConstants.SEPARATOR_AT + CoreConstants.PN_DISABLED, PropertyTransferPolicy.COPY_TO_NESTED_NODE);
transferPolicies.put(CoreConstants.SEPARATOR_AT + DialogConstants.PN_RENDER_HIDDEN, PropertyTransferPolicy.COPY_TO_NESTED_NODE);
// Rest of element attributes will move to the inner node
transferPolicies.put(CoreConstants.SEPARATOR_AT + DialogConstants.WILDCARD, PropertyTransferPolicy.MOVE_TO_NESTED_NODE);
diff --git a/plugin/src/main/java/com/exadel/aem/toolkit/plugin/utils/DialogConstants.java b/plugin/src/main/java/com/exadel/aem/toolkit/plugin/utils/DialogConstants.java
index 861fa8c4e..9dde4ec36 100644
--- a/plugin/src/main/java/com/exadel/aem/toolkit/plugin/utils/DialogConstants.java
+++ b/plugin/src/main/java/com/exadel/aem/toolkit/plugin/utils/DialogConstants.java
@@ -109,11 +109,11 @@ public class DialogConstants {
public static final String PN_DEPENDS_ON_REF = "dependsOnRef";
public static final String PN_DEPENDS_ON_REFTYPE = "dependsOnRefType";
public static final String PN_DEPENDS_ON_REFLAZY = "dependsOnRefLazy";
- public static final String PN_DISABLED = "disabled";
public static final String PN_DISCONNECTED = "disconnected";
public static final String PN_EDIT_ELEMENT_QUERY = "editElementQuery";
public static final String PN_EDITOR_TYPE = "editorType";
public static final String PN_EXTERNAL_STYLESHEETS = "externalStyleSheets";
+ public static final String PN_EXTRA_CLIENTLIBS = "extraClientlibs";
public static final String PN_FALLBACK_BLOCK_TAG = "fallbackBlockTag";
public static final String PN_FALLBACK_PATH = "fallbackPath";
public static final String PN_FEATURES = "features";
@@ -133,7 +133,6 @@ public class DialogConstants {
public static final String PN_PROTOCOLS = "protocols";
public static final String PN_REF = "ref";
public static final String PN_RENDER_HIDDEN = "renderHidden";
- public static final String PN_REQUIRED = "required";
public static final String PN_RETYPE = "retype";
public static final String PN_SLING_RESOURCE_TYPE = "sling:resourceType";
public static final String PN_TAB_SIZE = "tabSize";
diff --git a/plugin/src/test/com/exadel/aem/toolkit/plugin/maven/WidgetsTest.java b/plugin/src/test/com/exadel/aem/toolkit/plugin/maven/WidgetsTest.java
index ef26b8ead..86224379d 100644
--- a/plugin/src/test/com/exadel/aem/toolkit/plugin/maven/WidgetsTest.java
+++ b/plugin/src/test/com/exadel/aem/toolkit/plugin/maven/WidgetsTest.java
@@ -23,6 +23,7 @@
import com.exadel.aem.toolkit.test.widget.AnchorButtonWidget;
import com.exadel.aem.toolkit.test.widget.ButtonGroupWidget;
import com.exadel.aem.toolkit.test.widget.ButtonWidget;
+import com.exadel.aem.toolkit.test.widget.CodeEditorWidget;
import com.exadel.aem.toolkit.test.widget.ColorFieldWidget;
import com.exadel.aem.toolkit.test.widget.DatePickerWidget;
import com.exadel.aem.toolkit.test.widget.FieldSetWidget;
@@ -82,6 +83,11 @@ public void testButtonGroup() {
test(ButtonGroupWidget.class);
}
+ @Test
+ public void testCodeEditor() {
+ test(CodeEditorWidget.class);
+ }
+
@Test
public void testColorFieldAndHtmlTag() {
test(ColorFieldWidget.class);
diff --git a/plugin/src/test/com/exadel/aem/toolkit/test/widget/CodeEditorWidget.java b/plugin/src/test/com/exadel/aem/toolkit/test/widget/CodeEditorWidget.java
new file mode 100644
index 000000000..4ea727979
--- /dev/null
+++ b/plugin/src/test/com/exadel/aem/toolkit/test/widget/CodeEditorWidget.java
@@ -0,0 +1,43 @@
+/*
+ * 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.exadel.aem.toolkit.test.widget;
+
+import com.exadel.aem.toolkit.api.annotations.main.AemComponent;
+import com.exadel.aem.toolkit.api.annotations.main.Dialog;
+import com.exadel.aem.toolkit.api.annotations.widgets.DialogField;
+import com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditor;
+import com.exadel.aem.toolkit.api.annotations.widgets.codeeditor.CodeEditorOption;
+import com.exadel.aem.toolkit.plugin.utils.TestConstants;
+
+@AemComponent(
+ path = TestConstants.DEFAULT_COMPONENT_NAME,
+ title = TestConstants.DEFAULT_COMPONENT_TITLE
+)
+@Dialog(extraClientlibs = {"eak.library.1", "eak.library.2"})
+@SuppressWarnings("unused")
+public class CodeEditorWidget {
+
+ @DialogField
+ @CodeEditor(
+ source = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.10.0/ace.js",
+ mode = "js",
+ theme = "crimson_editor",
+ options = {
+ @CodeEditorOption(name = "wrap", value = "true", type = boolean.class),
+ @CodeEditorOption(name = "maxLines", value = "Infinity"),
+ },
+ dataPrefix = "js:"
+ )
+ String code;
+}
diff --git a/plugin/src/test/resources/widget/codeEditor/.content.xml b/plugin/src/test/resources/widget/codeEditor/.content.xml
new file mode 100644
index 000000000..69644536e
--- /dev/null
+++ b/plugin/src/test/resources/widget/codeEditor/.content.xml
@@ -0,0 +1,4 @@
+
+
diff --git a/plugin/src/test/resources/widget/codeEditor/_cq_dialog.xml b/plugin/src/test/resources/widget/codeEditor/_cq_dialog.xml
new file mode 100644
index 000000000..695434d9e
--- /dev/null
+++ b/plugin/src/test/resources/widget/codeEditor/_cq_dialog.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/base/render.jsp b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/base/render.jsp
index 9fc8da89b..3287a3fb5 100644
--- a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/base/render.jsp
+++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/base/render.jsp
@@ -11,7 +11,6 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
--%>
-
<%@ include file="/libs/granite/ui/global.jsp" %>
<%@ page session="false" import="com.adobe.granite.ui.components.ComponentHelper.Options" %>
<%
diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/.content.xml b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/.content.xml
new file mode 100644
index 000000000..c796cf68a
--- /dev/null
+++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/.content.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/css.txt b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/css.txt
new file mode 100644
index 000000000..e8c7365cf
--- /dev/null
+++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/css.txt
@@ -0,0 +1 @@
+style.css
diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/js.txt b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/js.txt
new file mode 100644
index 000000000..06f56a3df
--- /dev/null
+++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/js.txt
@@ -0,0 +1 @@
+script.js
diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/script.js b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/script.js
new file mode 100644
index 000000000..abaadd9ec
--- /dev/null
+++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/script.js
@@ -0,0 +1,196 @@
+/*
+ * 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 strict';
+
+(function (window, document, $, Granite) {
+ const DEFAULT_SCRIPT_SOURCE = 'https://ajaxorg.github.io/ace-builds/src-min-noconflict/ace.js';
+ const EDITOR_SELECTOR = '.eak-editor';
+ const VALIDATION_MESSAGE = 'Error: Please fill out this field.';
+
+ $(document).on('foundation-contentloaded', init);
+
+ /**
+ * Registers the editor host for use with Granite UI and retrieves the Ace Editor script
+ */
+ function init() {
+ registerFieldAdapter();
+ registerValidator();
+ registerValueHook();
+
+ const scriptSrcAttribute = $(EDITOR_SELECTOR + '[data-source]').map((index, element) => $(element).data('source'));
+ const scriptSrc = scriptSrcAttribute && scriptSrcAttribute.length ? scriptSrcAttribute[0] : DEFAULT_SCRIPT_SOURCE;
+ $.getScript(scriptSrc)
+ .done(appendEditors)
+ .fail(function (jqxhr, settings, ex) {
+ console.error('Could not retrieve the editor script', ex);
+ });
+ }
+
+ /**
+ * Registers the editor host as a {@code foundation-field} entity in the global Granite UI registry
+ */
+ function registerFieldAdapter() {
+ $(window).adaptTo('foundation-registry').register('foundation.adapters', {
+ type: 'foundation-field',
+ selector: EDITOR_SELECTOR,
+ adapter: function (el) {
+ const input = $(el).find('input')[0];
+ return {
+ getName: function () {
+ return input.name;
+ },
+ setName: function (name) {
+ input.name = name;
+ },
+ isDisabled: function () {
+ return el.disabled;
+ },
+ setDisabled: function (disabled) {
+ el.disabled = disabled;
+ el.editor && el.editor.setReadOnly(disabled || this.isReadOnly());
+ },
+ isInvalid: function () {
+ return $(el).attr('aria-invalid') === 'true';
+ },
+ setInvalid: function (invalid) {
+ $(el).attr('aria-invalid', invalid ? 'true' : 'false');
+ $(el).toggleClass('is-invalid', invalid);
+ },
+ isReadOnly: function () {
+ return $(el).attr('readonly');
+ },
+ setReadOnly: function (readOnly) {
+ $(el).attr('readonly', readOnly);
+ el.editor && el.editor.setReadOnly(readOnly || this.isDisabled());
+ },
+ isRequired: function () {
+ return $(el).attr('aria-required') === 'true';
+ },
+ setRequired: function (required) {
+ $(el).attr('aria-required', required ? 'true' : 'false');
+ },
+ getValue: function () {
+ return el.editor && el.editor.getValue();
+ },
+ setValue: function (value) {
+ el.editor && el.editor.setValue(value);
+ },
+ getLabelledBy: function () {
+ return $(el).attr('aria-labelledby');
+ },
+ setLabelledBy: function (labelledBy) {
+ $(el).attr('aria-labelledby', labelledBy);
+ },
+ getValues: function () {
+ return [el.value];
+ },
+ setValues: function (values) {
+ el.value = values[0];
+ },
+ clear: function () {
+ el.editor && el.editor.setValue('');
+ }
+ };
+ }
+ });
+
+ $(document).on('change', EDITOR_SELECTOR, function () {
+ $(this).trigger('foundation-field-change');
+ });
+ }
+
+ /**
+ * Registers the editor host in the global Granite UI registry as subject to validation
+ */
+ function registerValidator() {
+ const registry = $(window).adaptTo('foundation-registry');
+ registry.register('foundation.validation.selector', {
+ submittable: EDITOR_SELECTOR,
+ candidate: EDITOR_SELECTOR + ':not([readonly]):not([disabled])',
+ exclusion: EDITOR_SELECTOR + ' *'
+ });
+ registry.register('foundation.validation.validator', {
+ selector: EDITOR_SELECTOR,
+ validate: function (el) {
+ const isRequired = $(el).attr('aria-required') === 'true';
+ if (!isRequired) {
+ return;
+ }
+ return !el.editor || el.editor.getValue().length > 0 ? undefined : Granite.I18n.get(VALIDATION_MESSAGE);
+ }
+ });
+ }
+
+ /**
+ * For all the editor hosts existing in the current dialog defines the {@code value} property. This one is used
+ * in property accessors, such as {@code DependsOn}'s
+ */
+ function registerValueHook() {
+ $(EDITOR_SELECTOR).each(function () {
+ const editorHost = this;
+ Object.defineProperty(this, 'value', {
+ get() {
+ if (editorHost.editor) {
+ return editorHost.editor.getValue();
+ } else {
+ return editorHost.textContent;
+ }
+ },
+ set(value) {
+ if (editorHost.editor) {
+ editorHost.editor.setValue(value);
+ } else {
+ editorHost.textContent = value;
+ }
+ },
+ configurable: true
+ });
+ });
+ }
+
+ /**
+ * For every editor hosts existing in the current dialog initializes an instance of Ace Editor and sets initial
+ * options
+ */
+ function appendEditors() {
+ $(EDITOR_SELECTOR).each(function () {
+ const $this = $(this);
+ const $input = $this.siblings('input');
+
+ const editor = window.ace.edit(this);
+ $this.data('theme') && editor.setTheme($this.data('theme'));
+ $this.data('mode') && editor.session.setMode($this.data('mode'));
+
+ const options = $this.data('options');
+ if (options) {
+ Object.keys(options).forEach(key => {
+ if (options[key] === 'Infinity') {
+ options[key] = Infinity;
+ }
+ });
+ editor.setOptions(options);
+ }
+
+ if ($(this).attr('disabled') || $(this).attr('readonly')) {
+ editor.setReadOnly(true);
+ }
+
+ editor.on('change', function () {
+ $input.val(($this.data('prefix') || '') + editor.getValue());
+ });
+
+ this.editor = editor;
+ });
+ }
+})(window, document, Granite.$, Granite);
diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/style.css b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/style.css
new file mode 100644
index 000000000..0029db79e
--- /dev/null
+++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/clientlibs/style.css
@@ -0,0 +1,14 @@
+.eak-editor {
+ width: 100%;
+ min-height: 100px;
+ font-size: 15px;
+ margin: 8px 0;
+}
+
+.eak-editor.is-invalid {
+ border: 1px solid #e14132;
+ }
+
+.eak-editor[disabled] {
+ background-color: #f5f5f5;
+}
diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/render.html b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/render.html
new file mode 100644
index 000000000..bcd4cb054
--- /dev/null
+++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-authoring-kit/components/authoring/codeeditor/render.html
@@ -0,0 +1,23 @@
+
+
+