AEM Authoring Toolkit is the set of tools for creating comprehensive TouchUI dialogs for AEM components with use of existing and/or specially designed Java classes. The plugin aimed at providing the fastest and most intuitive way to supplement a Sling model class or a POJO with a TouchUI dialog compliant with the newest facilities of AEM 6.4+ / Coral UI v.3+ and support for Coral UI v.2. In the course of plugin development, a thorough comparative investigation of Coral v.2 and Coral v.3 has been carried out, their features and drawbacks tested, so that backward compatibility is preserved to a highest degree possible.
The toolkit frees an AEM developer of the necessity to compose and/or edit XML markup by hand. For typical use cases it provides set of reasonable defaults and feature templates. For more specific ones, it is powerful enough to create or edit arbitrary XML nodes and attributes at any level of XML tree, even if not directly supported by any predefined component. The usage of plugin helps reduce the risk of compilation extensions and misbehavior in production due to XML design errors and typos. In fact, there's virtually no necessity to edit TouchUI dialogs via XML or crx/de interface anymore.
It works at the package phase of Maven-powered project building process. Dialog markup is autogenerated based on the values of specific Java annotations added by a developer to fields of Sling Model / POJO use classes. Then the markup is injected into content package before deployment to JCR.
The Toolkit
- Generates rich and versatile AEM components dialogs and in-place editing facilities based on annotations added to backend Java classes and/or their fields.
- Relieves from writing down the complex XML markup for the authoring interface, preventing difficult-to-trace errors.
- Allows “inheriting” certain features and markup specificity across components, reducing the amount of Java code to store and maintain.
- Supports reusing and fine-tuning standard available authoring widgets and redefining their attributes.
- Ships with the frontend DependsOn package for providing best interactive authoring experience.
- Highly configurable via both annotation values and Maven configuration.
- Works with AEM 6.4 and newer on JDK 1.8+.
The main parts of the Toolkit are:
- Java API module aem-authoring-toolkit-api - comes as the OSGI-compatible Maven artifact,
- Package processing module aem-authoring-toolkit-plugin - comes as the Maven plugin,
- Fronted module aem-authoring-toolkit-assets - comes as the deployable AEM package.
AEM Authoring Toolkit has been developed in the course of Exadel™ Digital Marketing Practice. It is an evolving project that has yet to reach its maturity. Many new features are planned to be implemented, and more testing for the present features is required. The authors of the Toolkit heartily welcome the creative input from the AEM community worldwide to bring the best of programming techniques and design for creating best authoring and user experience.
Feel free to clone the project sources and run mvn clean install
from the project's root folder. The plugin and the API artifacts will be installed in local .m2 repository. The compiled aem-authoring-toolkit-assets package will be found under /distrib folder of the project. You may then deploy it to your AEM authoring instance as usual.
Add links to the repository to "repositories" and "pluginRepositories" sections of you Maven settings file (<user_profile_folder>/.m2/settings.xml):
<repositories>
<!-- AEM-Authroring Toolkit -->
<repository>
<id><!-- repository id goes here --></id>
<url><!-- repository URL goes here --></url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<!-- AEM-Authoring-Toolkit -->
<pluginRepository>
<id><!-- plugin repository id goes here --></id>
<url><!-- plugin repository URL goes here --></url>
<releases>
<enabled>true</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
- Insert dependency to the API module in the <dependencies> section of the POM file of your bundle:
<dependency>
<groupId>com.exadel.aem</groupId>
<artifactId>aem-authoring-toolkit-api</artifactId>
<version>1.0.1-SNAPSHOT</version> <!-- prefer latest stable version whenever possible -->
</dependency>
- Insert plugin's config in the <plugins> section of the POM file of your package:
<plugin>
<groupId>com.exadel.aem</groupId>
<artifactId>aem-dialog-maven-plugin</artifactId>
<version>1.0.1-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>aem-dialog</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- Place here the path to the node under which your component nodes are stored -->
<componentsPathBase>jcr_root/apps/projectName/components</componentsPathBase>
<!-- OPTIONAL: specify root package for component classes -->
<componentsReferenceBase>com.acme.project.samples</componentsReferenceBase>
<!-- OPTIONAL: specify list of exceptions, comma-separated, that would cause this plugin to terminate
the build process. 'ALL' and 'NONE' may be specified as well.
Default is java.io.IOException -->
<terminateOn>ALL</terminateOn>
</configuration>
</plugin>
###Installing Assets
For some of the Toolkit's features to work properly, namely the DependsOn
set of instructions, you need to deploy the aem-authoring-toolkit-assets-[version].zip package to your AEM author instance, The file can be found under the /distrib folder of current repository.
In order to create a dialog you need create a Java class and mark it with @Dialog
annotation. All required root attributes and namespace fields for the XML markup of cq:dialog will be added.
Besides, @Dialog
possesses properties that are translated into common attributes of AEM component itself, according to the Adobe specification, thus covering most of the use-cases. See the code snippet below:
@Dialog(
name = "myComponent",
title = "My AEM Component",
description = "The most awesome AEM component ever",
componentGroup = "my-brand-new-components",
templatePath = "/some/absolute/jcr/path",
resourceSuperType = "/path/to/resource",
cellName = "some-cell-name",
helpPath = "https://www.google.com/search?q=my+aem+component",
isContainer = true,
width = 800,
height = 600,
layout = DialogLayout.TABS
)
@Properties({
@Property(name = "customString", value = "custom"),
@Property(name = "customLongValue", value = "{Long}21")
})
public class MyComponentDialog { /* ... */}
There are several ways to create tabbed dialogs. First, you may need to mark a nested class of your @Dialog-annotated class with @Tab annotation. The title property of @Tab will be used as the tab's node name, non-alphanumeric characters skipped (for example, @Tab(title="First tab title!")
will produce <firstTabTitle> tag)
@Dialog(layout = Layout.TABS)
public class Dialog {
@Tab(title = "First tab")
class Tab1 {
@DialogField(label="Field on the first tab")
@TextField
String field1;
}
@Tab(title = "Second tab")
class Tab2 {
@DialogField(label="Field on the first tab")
@TextField
String field2;
}
}
(Note the layout = DialogLayout.TABS
assignment. This is to specify that the dialog must display fields encapsulated in nested classes per corresponding tabs. If this property is skipped, or set to its default FIXED_COLUMNS
value, tabs will not show and only "immediate" fields of the basic class will be displayed).
The other way of laying out tabs is to define array of @Tab
within @Dialog
annotation. Then, to settle a field to a certain tab you will need to add @PlaceOnTab
annotation to this particular field. The values of @PlaceOnTab
must correspond to the title value of the desired tab. This is a somewhat more flexible technique which avoids creating nested classes and allows freely moving fields. You only need to ensure that tab title is specified everywhere in the very same format, no extra spaces, etc.
@Dialog(
name = "test-component",
title = "test-component-dialog",
tabs = {
@Tab(title = "First tab"),
@Tab(title = "Second tab"),
@Tab(title = "Third tab")
}
// layout = Layout.TABS is implied by default here because of "tabs" property set
)
public class TestTabs {
@DialogField(label = "Field on the first tab")
@TextField
@PlaceOnTab("First tab")
String field1;
@DialogField(label = "Field on the second tab")
@TextField
@PlaceOnTab("Second tab")
String field2;
@DialogField(description = "Field on the third tab")
@TextField
@PlaceOnTab("Third tab")
String field3;
}
The plugin makes use of @DialogField
annotation and the set of specific annotations, such as @TextField
, @Checkbox
, @DatePicker
, etc., discussed further. The latter are referred as field-specific annotations.
Used for defining common properties of a field, such as the name attribute (specifies under which name the value will be persisted, equals to the class' field name if not specified), and also label, description, required, disabled, wrapperClass, renderHidden. In addition, @DialogField
provides the possibility to order fields inside the dialog container by specifying ranking value. Generally in reflects the task and capabilities of Adobe's Granite UI Field component.
Typically @DialogField
is used in pair with one of field-specific annotations e.g. @TextField
.
@Dialog
public class Dialog {
@DialogField(
label = "Field 1",
description = "This is the first field",
wrapperClass = "my-class",
renderHidden = true,
ranking = 5
)
@TextField
String field1;
}
Please note that if @DialogField
is specified but a field-specific annotation is not, such field will not be rendered (@DialogField
exposes only most common information about a field and does not hint on which HTML component to use).
The other way around, you can indeed specify a field-specific annotation and omit @DialogField
. Such field will be rendered (however without label and description, etc.), but its value will not be persisted.
In case when the dialog class extends another class that has some fields marked with field-specific annotations, relevant fields from both ancestral and child class are rendered. All fields from ancestral and child class (even those sharing same name) are considered different and rendered separately. Still namesake fields may interfere if rendered within same container (dialog or tab), so please avoid using same names. Still if you wish to engage some deliberate "field overriding", refer to the chapter on usage of @Extends
below.
The fields are sorted in order of their ranking. If several fields have the same (or default) ranking, they are rendered in the order as they appear in the source code. Fields collected from ancestral classes have precedence over fields from child classes.
Used to render autocomplete widgets in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on Autocomplete. Options becoming available as user enters text depend on the value of "namespaces" property of @AutocompleteDataSource
. If unset, all tags under the content/cq:Tags JCR directory will be available. Otherwise you specify one or more particular cq:Tag nodes as in the snippet below:
public class AutocompleteDialog{
@DialogField
@Autocomplete(multiple = true, datasource = @AutocompleteDatasource(namespaces = {"workflow", "we-retail"}))
String field;
}
Used to produce checkbox inputs in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on Checkbox.
Sometimes there is a need to supply a list of sub-level checkboxes to a parent checkbox whose displayed state will be affected by the states of child inputs. You can achieve this by specifying sublist property of @Checkbox
with a reference to a nested class encapsulating all the sub-level options. This is actually a full-feature rendition of Granite UI NestedCheckboxList.
@Dialog
public class NestedCheckboxListDialog {
@Checkbox(text = "Level 1 Checkbox", sublist = Sublist.class)
boolean option1L1;
class Sublist {
@Checkbox(text = "Level 2 Checkbox 1")
boolean option2L1;
@Checkbox(text = "Level 2 Checkbox 2", sublist = Sublist2.class)
boolean option2L2;
}
class Sublist2 {
@Checkbox(text = "Level 3 Checkbox 1")
boolean option3L1;
@Checkbox(text = "Level 3 Checkbox 2")
boolean option3L2;
}
}
Used to render inputs for storing color values in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on ColorField.
Used to render date/time pickers in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on DatePicker. You can set the type of DatePicker (whether it stores only date, only time, or both). Also you can display format (see Java documentation on possible formats),
minimal and maximal date/time to select (may also specify timezone). To make formatter effective, set typeHint = TypeHint.STRING
to store date/time to JCR as merely string and not a numeric value.
public class DatePickerDialog {
@DialogField
@DatePicker(
type = DatePickerType.DATETIME,
displayedFormat = "DD.MM.YYYY HH:mm",
valueFormat = "DD.MM.YYYY HH:mm",
minDate = @DateTimeValue(day = 1, month = 1, year = 2019),
maxDate = @DateTimeValue(day = 30, month = 4, year = 2020, hour = 12, minute = 10, timezone = "UTC+3"),
typeHint = TypeHint.STRING
)
String currentDate;
}
Used to render the FileUpload components in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on FileUpload. You can specify MIME types of files acceptable, graphic styles of the created component. It is required to specify uploadUrl to an actual and accessible JCR path (may also specify a sub-node of an existing node that will be created as needed). Sling shortcut ${suffix.path} for component-relative JCR path is also supported.
public class FileUploadDialog {
@DialogField
@FileUpload(
uploadUrl = "/content/dam/my-project",
autoStart = true,
async = true,
mimeTypes = {
"image/png",
"image/jpg"
},
buttonSize = ButtonSize.LARGE,
buttonVariant = ButtonVariant.ACTION_BAR,
icon = "dataUpload",
iconSize = IconSize.SMALL
)
String currentDate;
}
Designed as a companion to @FileUpload, mimics the features of FileUpload component that was there before Coral 3 was introduced, and the build-it upload component situated at cq/gui/components/authoring/dialog/fileupload in your AEM installation. Technically, this is but another rendition of FileUpload logic aimed at mainly uploading images via drag-and-drop
public class ImageFieldDialog {
@DialogField
@ImageUpload(
mimeTypes="image",
title="Upload Image Asset",
sizeLimit = 100000
)
String file;
}
@Hidden
Used to render hidden inputs in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on Hidden.
Used to render inputs for storing numbers in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on NumberField.
Used to render password inputs in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on Password. If you wish to engage "confirm password" box in your dialog's layout, create two @Password
-annotated fields in your Java class, then feed the name of the second field to the retype property for the first one. If the values of the two fields do not match, validation error is produced.
public class PasswordDialog {
@DialogField
@Password(retype = "confirmPass")
String pass;
@DialogField
@Password
String confirmPass;
}
Used to produce path selectors in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on PathField.
Used to render groups of RadioButtons in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on RadioGroup. The usage is as follows:
public class RadioGroupDialog {
@DialogField
@RadioGroup(buttons = {
@RadioButton(text = "Button 1", value = "1", checked=true),
@RadioButton(text = "Button 2", value = "2"),
@RadioButton(text = "Button 3", value = "3", disabled=true)
})
String field8;
}
Used to render select inputs in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on Select. @Select
comprises set of @Option
items. Each of them must be initialized with mandatory value and several optional parameters, such as text (represents option label), boolean flags selected and disabled, and also String values responsible for visual presentation of an option: icon, statusIcon, statusText and statusVariant.
Here is a little code snippet on @Select
usage:
public class MyDialogWithDropdown {
@DialogField(label = "Rating")
@Select(options = {
@Option(
text = "1 star",
value = "1",
selected = true,
statusIcon = "/content/dam/samples/icons/1-star-rating.png",
statusText = "This is to set 1-star rating",
statusVariant = StatusVariantConstants.SUCCESS
),
@Option(text = "2 stars", value = "2"),
@Option(text = "3 stars", value = "3"),
@Option(text = "4 stars", value = "4", disabled=true),
@Option(text = "5 stars", value = "5", disabled=true)
},
emptyText = "Select rating")
String dropdown;
}
Used to render on-off toggle switches in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on Switch.
Used to render textarea HTML inputs in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on TextArea.
Used to produce text inputs in TouchUI dialogs. Exposes properties as described in Adobe's Granite UI manual on TextField.
Used to logically group a number of different fields as described in Adobe's Granite UI manual on FieldSet. This goal is achieved by a nested class that encapsulates grouping fields. Then a <NestedClass>-typed field is declared, and @FieldSet
annotation is added.
Hierarchy of nested classes is honored (so that a FieldSet-producing class may extend another class from same or even foreign scope. Proper field order within a fieldset can be guaranteed by use of ranking values (see chapter on @DialogField
above).
Names of fields added to a FieldSet may share common prefix. This is specified in namePrefix property.
public class DialogWithFieldSet {
@FieldSet(title = "Field set example", namePrefix="fs-")
private FieldSetExample fieldSet;
class FieldSetExample extends ParentFieldSetExample {
//Constructors are omitted
@DialogField(ranking = 1)
@TextField
String field6;
@DialogField(ranking = 2)
@TextField
String field7;
@DialogField(ranking = 3)
@TextField
String field8;
}
class ParentFieldSetExample {
//Constructors are omitted for simplicity
@DialogField(ranking = 4)
@TextField
String field6;
}
}
#####@MultiField
Used to facilitate multiple (repeating) instances of same fields or same groups if fields as described in Adobe's Granite UI manual on MultiField. The logic of the component relies on the presence of a nested class encapsulating one or more fields to be repeated. Reference to that class is passed to @MultiField
's field property. See below how it works for a single field repetition, and for a subset of fields multiplied.
######Simple multi field
public class SimpleMultiFieldDialog {
@DialogField(label="Multi")
@MultiField(field = MultiFieldContainer.class)
String multiField;
class MultiFieldContainer {
@DialogField
@TextField
String dialogItem;
}
}
######Composite multi field
public class CompositeMultiFieldDialog {
@DialogField
@MultiField(field = MultiCompositeField.class)
String multiComposite;
class MultiCompositeField {
@DialogField
@TextField(description = "Multi Text")
String multiText;
@DialogField(description = "Multi Checkbox")
@Checkbox(text = "Multi CheckBox")
String checkboxMulti;
}
}
#####Fields common attributes Components TouchUI dialogs honor the concept of global HTML attributes added to rendered HTML tags. To set them via AEM-Dialog-Plugin, you use the @Attribute annotation.
public class DialogWithHtmlAttributes {
@DialogField
@TextField
@Attribute(
id = "field1-id",
clas = "field1-attribute-class",
data = {
@Data(name = "field1-data1", value = "value-data1"),
@Data(name = "field1-data2", value = "value-data2")
})
String field1;
}
####Implementing RichTextEditor RichTextEditor (RTE) is somehow special yet vastly demanded Coral dialog component that provides possibility of editing strings and texts with WYSIWYG experience. the functionality of the component is based upon set of plugins, either built-in or custom. Most plugins expose sets of "features" reflected by UI elements (buttons, or dropdown lists, or button panels, or floating panels - so called "popovers").
Traditionally, to add a feature to RichTextEditor a user needs to include a string representing a button in toolbar
attributes of one or more XML nodes, include another node representing a plugin to a corresponding plugin tree and/or populate features
attribute of that node and then possibly set plugin's custom features, each in one's own specific format. It the feature is to sit in a floating panel, the <popovers>
node and its sub-nodes must be additionally taken care of.
The AEM Authoring Toolkit streamlines that process a lot.
######RTE features and popovers
Using AEM Authoring Toolkit, to initialize a RichTextEditor component with certain plugins/features, a user needs to apply @RichTextEditor
annotation to a class field and then set the annotation's features property. This property accepts an array of strings in plugin#feature format. To specify a popover, one adds to the array a square-bracketed string like [plugin#feature1, plugin#feature2,...plugin#featureN] or [plugin:feature1:feature2:...featureN] depending on plugin's specification.
The built-in plugin#feature pairs are stored as constants of RteFeatures class for convenience. Feature sets of various built-in plugins grouped by plugin (so that to show them in separate popovers) are stored within RteFeatures.Popovers class. Definitions of specific panels are in RteFeatures.Panels class (for now only one specific panel, "table", is supported).
Thus the nearly maximal set of built-in features for a RichTextEditor component can be exposed in the following manner:
@RichTextEditor(
features = {
RteFeatures.Popovers.CONTROL_ALL,
RteFeatures.UNDO_UNDO,
RteFeatures.UNDO_REDO,
RteFeatures.SEPARATOR,
RteFeatures.Popovers.EDIT_ALL,
RteFeatures.Popovers.FINDREPLACE_ALL,
RteFeatures.SEPARATOR,
RteFeatures.Popovers.FORMAT_ALL,
RteFeatures.Popovers.SUBSUPERSCRIPT_ALL,
RteFeatures.Popovers.STYLES,
RteFeatures.Popovers.PARAFORMAT,
RteFeatures.Popovers.JUSTIFY_ALL,
RteFeatures.Popovers.LISTS_ALL,
RteFeatures.Popovers.LINKS_MODIFY_DELETE,
RteFeatures.SEPARATOR,
RteFeatures.Panels.TABLE,
RteFeatures.SPELLCHECK_CHECKTEXT,
RteFeatures.Popovers.MISCTOOLS_ALL,
RteFeatures.FULLSCREEN_TOGGLE,
}
)
private String text;
Apart from built-in features, you can append any features provided by a custom RTE plugin using the same string format. Technically, the plugin searches for plugin#feature strings and converts each into a toolbar button. Then it searches for [plugin#feature1,plugin#feature2]
patterns and converts each into a popover. First plugin#feature entry becomes the button to bring on the popover. This one and all the rest entries are shown as the popover content.
Thus, you alter any of the predefined popovers or compose a different popover (from either built-in, or custom features, or both). See the following snippet that indicates appending a custom feature, then two custom popovers to a feature set:
@RichTextEditor ( /* ... */
features = {
"some#feature",
RteFeatures.BEGIN_POPOVER +
"myPlugin#feature1" + RteFeatures.FEATURE_SEPARATOR +
"myPlugin#feature2" +
RteFeatures.END_POPOVER,
RteFeatures.BEGIN_POPOVER +
RteFeatures.FORMAT_BOLD + RteFeatures.FEATURE_SEPARATOR +
"myAnotherPlugin#feature3" + RteFeatures.FEATURE_SEPARATOR +
RteFeatures.LINKS_ANCHOR +
RteFeatures.END_POPOVER
}
)
######RTE view modes RichTextEditor configuration allows specifying features for three different editor modes. These are:
- inline (for an ordinary dialog window),
- dialogFullScreen (for a "maximized" dialog window), and
- fullscreen (for a dialog window which shows after "ToggleFullscreen" button pressed and for a "maximized" in-place editor).
You can separately specify set of features for inline and dialogFullScreen/fullscreen modes by populating "features" and fullscreenFeatures properties, accordingly. These two properties accept values in the same format. Or you can use one set of features for either, by populating only "features".
Generally it is recommended that a narrower set of features be used for the inline, e.g. "windowed" mode, and popover elements avoided in this mode due to unwanted visual effects, and a wider set of features with popovers unrestricted be used for any of the "fullscreen" modes.
If neither features nor fullscreenFeatures are populated, a default set of buttons will be generated for each of the three editor modes.
######Toolbar icons A user can override existing or add new icon definitions for toolbar buttons via icons property. Several icon definitions may be missing from Coral installation. To provide complete user experience with the mentioned full feature set, you may use the following snippet:
@RichTextEditor ( /* ... */
icons = {
@IconMapping(command = "#edit", icon = "copy"),
@IconMapping(command = "#findreplace", icon = "search"),
@IconMapping(command = "#links", icon = "link"),
@IconMapping(command = "#table", icon = "table"),
@IconMapping(command = "#subsuperscript", icon = "textSuperscript"),
@IconMapping(command = "#control", icon = "check"),
@IconMapping(command = "#misctools", icon = "fileCode"),
}
)
######Settings for pasting text One substantial concern for a RichTextEditor component maintainer is the rules for processing the input from clipboard. A user may specify defaultPasteMode and htmlPasteRules for dealing with non-plaintext clipboard content as in the below snippet:
@RichTextEditor ( /* ... */
defaultPasteMode = PasteMode.WORDHTML,
htmlPasteRules = @HtmlPasteRules(
allowBold = false,
allowItalic = true,
allowImages = false,
allowLists = AllowElement.ALLOW,
allowTables = AllowElement.REPLACE_WITH_PARAGRAPHS,
allowedBlockTags = {"p", "sub"},
fallbackBlockTag = "p"
)
)
Setting the htmlLinkRules property allows to additionally control the way internal and external links in pasted text are processed. See the following snippet:
@RichTextEditor ( /* ... */
htmlLinkRules = @HtmlLinkRules(
cssInternal = "my-internal-link-style",
cssExternal = "my-external-link-style",
targetInternal = LinkTarget.MANUAL,
targetExternal = LinkTarget.BLANK,
protocols = {Protocol.HTTP, Protocol.HTTPS},
defaultProtocol = Protocol.HTTPS,
)
)
######Inserting special characters
Among the commonly user RTE assets is the misctools#specialchars
feature that represents an "Insert symbol"-like dialog. The set of Unicode characters to offer may be defined in specialCharacters property. This is an array that stores either a single HTML entity definition or a Unicode range (decimal values) as in the following snippet:
@RichTextEditor ( /* ... */
specialCharacters = {
@Characters(name = "Copyright", entity = "©"),
@Characters(name = "Euro sign", entity = "€"),
@Characters(name = "Registered", entity = "®"),
@Characters(name = "Trademark", entity = "™"),
@Characters(rangeStart = 512, rangeEnd = 514),
@Characters(rangeStart = 998, rangeEnd = 1020)
}
)
######Paragraph tagging and text styles Set of formatting tags for a "paraformat" button may be defined in formats property as in the snippet:
@RichTextEditor ( /* ... */
formats = {
@ParagraphFormat(tag = "h1", description = "My custom header"),
@ParagraphFormat(tag = "h2", description = "My custom subheader")
}
)
RichTextEditor allows to define text visual features by CSS rules. Property externalStyleSheets is for specifying array of strings representing paths to JCR-stored CSS files that will be applied to the RTE content. After externalStyleSheets are set, one may populate the styles property with the CSS classes that will be offered to a use in styles dropdown, as in the below snippet:
@RichTextEditor ( /* ... */
externalStyleSheets = {
"/etc/clientlibs/myLib/style1.css",
"/etc/clientlibs/myLib/style2.css"
},
styles = {
@Style(cssName = "my-style", text = "My custom style 1"),
@Style(cssName = "my-another-style", text = "My custom style 2")
}
)
(Unlike formats above, these are not HTML tag definitions but rather <span style='...'>...</span> blocks that will be added to selected text.) ######Miscellaneous tweaks Additionally, a user can specify amount of edit operations stored for undo/redo logic (via maxUndoSteps property), the width of tabulation (in spaces, via tabSize property) and the indentation margin of lists (in spaces, via indentMargin property).
####@Extends-ing fields annotations Several dialog fields, such as RichTextEditor field, may require vast and sophisticated annotation code. If there are multiple such fields in your Java files, they may become overgrown and difficult to maintain. Moreover, you will probably face the need to copy the lengthy annotation listings between fields, e.g. if you plan to use several RTE boxes with virtually the same set of toolbar buttons, plugins, etc.
One of the powerful features of AEM Authoring Toolkit is its extension/inheritance technique that helps to cope with that issue.
Suppose that you have marked private String sampleText; in your HelloWorld.java class with several AEM Authoring Toolkit annotations and wish to use the same set of annotations for private String anotherField; in this very or other class.
To achieve this, add to the anotherText field the @Extends
annotation pointing to sampleText. Whatever field-specific annotation you defined for the sampleText field will now be "inherited" by anotherText. You still can add another @TextField
to anotherText with properties that were not specified in sampleText field or have different values there. Thereby "inheritance with overriding" is achieved. See the following snippet:
public class CustomPropetiesDialog {
@DialogField(label = "My text field")
@Extends(value = HelloWorld.class, field = "sampleText")
@TextField(emptyText = "Enter your text here")
private String anotherText;
/* ... */
}
The plugin will first look for the sampleText field in HelloWorld class, and if found, will use that field's @DialogField
and @DatePicker
annotations to prepare XML markup for the current field. For such properties as label or emptyText that have local "overrides", the local values will be used, rest will be taken from the anotherText field.
Note that it is possible that the "parent" field in its own turn @Extends
-es some third "grandparent" field, so rendering starts from "grandparent" (same as it is with inheriting class members in object-oriented programming).
Yet make sure that all the fields involved have the same component annotation. A field marked with, say, @DatePicker
will not extend some @Checkbox
field, and so on.
Also mind that when you extend a field and add another field-specific annotation to override some properties (like in the sample above), property values are either replaced or appended (like adding values from an array-typed property of "child" to the array-typed property of "parent"), but not erased. You cannot replace a non-empty value of a "parent" with a blank, or empty, value of a "child". So take care to design you "inheritance tree" starting from fields with more abstract, less populated component annotations, and then shifting to more specific ones.
If you wish to engage such TouchUI dialog features as listeners or in-place editing (those living in <cq:editConfig> node and, accordingly, _cq_editConfig.xml file), add @EditConfig
annotation to your Java class.
It facilitates setting of the following properties and features:
- Actions
- Empty text
- Inherit
- Dialog layout
- Drop targets
- Form parameters
- In-place editing
- Listeners
To specify in-place editing configurations for your component, populate the inplaceEditing property of @EditConfig
annotation like follows.
@Dialog(name = "componentName")
@EditConfig(
inplaceEditing = @InplaceEditingConfig(
type = EditorType.TEXT,
editElementQuery = ".editable-header",
name = "header",
propertyName = "header"
)
)
public class CustomPropertiesDialog {
@DialogField
@TextField
String field1;
}
Note that if you use type = EditorType.PLAINTEXT
, there is an additional required textPropertyName value. If you do not specify a value for that, same string as for propertyName will be used.
There is the possibility to create multiple in-place editors like in the following snippet:
@Dialog(name = "componentName")
@EditConfig(
inplaceEditing = {
@InplaceEditingConfig(
type = EditorType.PLAINTEXT,
editElementQuery = ".editable-headline",
name = "headline",
propertyName = "headline"
),
@InplaceEditingConfig(
type = "CustomType",
editElementQuery = ".editable-description",
name = "description",
propertyName = "description"
)
}
)
public class CustomPropertiesDialog {
@DialogField
@TextField
String field1;
}
With an in-place configuration of type = EditorType.TEXT
, a richTextConfig may be specified with syntax equivalent to that of @RichTextEditor
component annotation.
Here is a very basic example of "richTextConfig" for an in-place editor
@InplaceEditingConfig (
type = EditorType.TEXT, ...
richTextConfig = @RichTextEditor(
features = {
RteFeatures.UNDO_UNDO,
RteFeatures.UNDO_REDO,
RteFeatures.Popovers.MISCTOOLS,
RteFeatures.Panels.TABLE,
RteFeatures.FULLSCREEN_TOGGLE
},
icons = @IconMapping(command = "#misctools", icon = "fileCode"),
htmlPasteRules = @HtmlPasteRules(
allowBold = false,
allowImages = false,
allowLists = AllowElement.REPLACE_WITH_PARAGRAPHS,
allowTables = AllowElement.REPLACE_WITH_PARAGRAPHS
)
)
)
class DialogSample { /* ... */ }
Ever simpler, you can specify the richText field to "extend" RTE configuration specified for a Touch-UI dialog elsewhere in your project:
@InplaceEditingConfig (
type = EditorType.TEXT,
richText = @Extends(value = HelloWorld.class, field = "myRteAnnotatedField"),
richTextConfig = @RichTextEditor(/* ... */)
)
From the above snippet you can see that richText and richTextConfig work together fine. Configuration inherited via richText can be altered by whatever properties specified in richTextConfig. If you use both in the same @InplaceEditingConfig
, plain values, such as strings and numbers, specified for the @Extends
-ed field are overwritten by their correlates from richTextConfig. But array-typed values (such as features, specialCharacters, formats, etc.) are actually merged. So you can design a fairly basic set of features, styles, formats to store in a field somewhere in your project and then implement several richTextConfig-s with more comprehensive and different feature sets.
Value restrictions can be imposed on some of the annotations' fields. For instance, if you set a negative integer to a field that requires a positive one (say, tabIndent or undoSteps field of @RichTextEditor
), or you set some string that is not a complete JCR path to a field requiring such (e.g uploadUrl field), a warning will be issued and the field's default value will be rendered instead. Or, if the default is omittable, nothing will be rendered.
You may change this behavior by specifying validationPolicy in plugin's section in POM file. Possible values are:
- rewrite - as described above,
- idle - no warnings and values rendered as they are,
- report - warning logged but values stored as they are,
- terminate - exception thrown, build process terminates.
###Customization
The AEM Authoring Toolkit allows to flexibly customize the structure of TouchUI dialog markup using the following approaches
####Custom annotations and handlers
You can create your custom annotations to change existing node structure of a particular field. One requirement for a custom annotation is to be in turn annotated with @DialogAnnotation
or @DialogWidgetAnnotation
. Its required source property is needed to pick up appropriate custom handler (see below).
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@DialogWidgetAnnotation(source = "helloworld")
public @interface HelloWorld {
String greeting() default "Hello World!";
}
Basically, declaring @interface HelloWorld
as above and then using it to annotate a field in a Sling POJO/model Java class is enough to render an XML node in TouchUI dialog markup that will have greeting attribute with "Hello World!" value. Combined with @DialogField annotation, it would produce a nearly complete TouchUI dialog component (you may add resourceType property with a default value to your @HelloWorld interface and anything else necessary to mimic a "regular" dialog component).
Still there might be a necessity to implement special rendering for your custom annotation. To achieve this, you may create a handler class implementing either DialogHandler
or DialogWidgetHandler
interfaces.
DialogHandler
interface is for custom processing of the whole Dialog XML structure. It has 'getName()' method to be overridden in your implementation. This represents the name we need it to bind a custom annotation that has equivalent source value, with the handler. Since DialogHandler
extends BiCounsumer<Element, Class>
, you will then need to override the .accept()
method.
The Element instance represents root element of the corresponding XML file, and the Class<?>
parameter points to the current AEM component Java class.
Same way, if you want to apply the handler's logic to only particular field of class, you can add @DialogWidgetComponent
to an own-written annotation, and then implement DialogWidgetHandler
interface that extends BiCounsumer<Element, Field>
.
Here is how a custom DialogAnnotation and a custom DialogHandler may look like:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@DialogAnnotation(source = "testSource")
public @interface CustomDialog {
String greeting() default "Hello World!";
}
public class CustomDialogHandler implements DialogHandler {
@Override
public String getName() {
return "testSource";
}
@Override
public void accept(Element element, Class<?> dialogClass) {
element.setAttribute("test", dialogClass.getSimpleName());
}
}
This way, added to a Sling model / POJO use class like
@Dialog(name = "componentName")
@CustomDialog
public class CustomStructureDialog {
@DialogField(label = "Field 1")
@TextField
String field1;
}
...the @CustomDialog
annotation will result in test attribute being added to the <cq:dialog> node of TouchUI markup.
####Runtime methods for custom handlers
If you define in your handler class a field of type RuntimeContext
, the link to the global RuntimeContext
object will be injected by the Maven plugin. It allows to engage a number of utility methods and techniques, such as those of the XmlUtility
interface. Of special interest are the methods .createNodeElement()
with overloads for creating nodes with specific jcr:primaryType, sling:resourceType and other attributes, .appendChild()
with overloads for appending or merging a newcomer node to a set of existing child nodes of a local root, and .setAttribute()
with overloads for populating previously created node with generic-typed annotation values, optionally validated and then optionally fallen back to defaults.
Developer can (and is encouraged to) also call .getExceptionHandler()
method whenever his or her logic is ought to throw or manage an exception. This way, all the exceptions from either built-in or custom routines are managed uniformly.
####Restricting custom annotations' values
You can modify rendering of your custom-developed annotations by adding built-in "meta"-annotations, such as @ValueRestriction
or @IgnoredValue
.
@ValueRestriction
accepts simple name of a RestrictionTester class as an argument. Predefined names are in ValueRestrictions class.
To avoid rendering attribute with a value implied by Coral engine and thus redundant, use @IgnoredValue
annotation with argument set to String representation of unneeded value.
####Custom Properties
#####Custom properties for fields
If you need some attributes with plain values added to a dialog field, this can be achieved without creating a custom annotation and handler. Just add @Properties
annotation to a field in your Java class and populate it with properties you need.
@Dialog(name = "componentName")
public class CustomPropertiesDialog {
@DialogField(label = "Field 1")
@TextField
@Propeties({
// this will produce the String-typed JCR attribute "stringAttr" with value "Hello World"
@Property(name = "stringAttr", value = "Hello World"),
// this way you create a Long-typed JCR attribute. Attributes of other JCR-supported types
// are stored similarly
@Property(name = "numericAttr", value = "{Long}42")
})
String field1;
}
#####Custom properties for in-place editing configurations
Arbitrary attributes can be set to in-place editing configurations. For that, set a value for the config field of an @InplaceEditingConfig
annotation.
@InplaceEditingConfig(
type = "CustomType",
editElementQuery = ".editable-description",
name = "description", propertyName = "description",
config = {
@Property(name="stringAttr", value = "Hello World"),
@Property(name="booleanAttr", value = "{Boolean}true")
}
)
public class CustomPropertiesDialog { /* ... */ }
#####Dialog-wide properties Yet another mechanism available is to specify custom properties at Java class level. This may be used:
- for setting entire component's attributes (those exposed in .content.xml file);
- for setting attributes of <cq:dialog> root node (_cq_dialog.xml file);
- for setting attributes of <cq:editConfig> node (_cq_editConfig.xml file).
For these goals, @CommonProperties
annotation is designed. It accepts similar arguments to those of @Properties
annotation. Yet you can also specify the XML scope for each @CommonProperty
(it exactly means - in which of the XML trees, or files, the attribute will be stored, default is .content.xml) and a relative path to the root node. See the code snippet:
@CommonProperties({
@CommonProperty(name = "stringAttr", value = "Hello World"), // goes to .content.xml by default
@CommonProperty(scope = XmlScope.CQ_DIALOG, name = "numericAttr", value = "{Long}-1000"),
@CommonProperty(scope = XmlScope.CQ_EDIT_CONFIG, name = "arrayAttr", value = "[any,many,minny,moe]"),
@CommonProperty(
scope = XmlScope.CQ_EDIT_CONFIG,
path = "/root/inplaceEditing/config/rtePlugins/edit/htmlPasteRules/table",
name = "allow",
value = "{Boolean}true"
),
@CommonProperty(
scope = XmlScope.CQ_DIALOG,
path = "//*[@size='L']",
name = "size",
value = "S"
),
})
public class CustomPropertiesDialog { /* ... */ }
Pay attention to the third and forth @CommonProperty
-s. Specifying the path value gives the ability to traverse to any child node of the prepared XML with use of an XPath-formatted string.
@CommonProperties
are rendered after the XML tree is completed. Thus, setting them provides a kind of "last-chance" alternation of your TouchUI logic (may be used for debugging also). For instance, the last @CommonProperty
in the sample uses the power of XPath to change size attribute of every single node where size has been set to "L". Only make sure that the path points to at least one truly existing XML node.
Note that XPath parser is namespace-agnostic. That is why you need to use /root/inplaceEditing... instead of /jcr:root/cq:inplaceEditing... in the sample above.
You can debug your custom logic while building your app. In order to do it run your build in debug mode e.g.:
mvnDebug clean install -PautoInstallPackage
Afterwards you can set breakpoints in your IDE, start a debugging session and connect to the build process. Default port is 8000.
##Frontend assets
(see more in DependsOn Readme)
DependsOn asset is a client library that triggers pre-defined actions over a dependent TouchUI dialog widget or tab upon a change of other (referenced) widget/field in the authoring interface on the AEM installation frontend. Typical use-case for DependsOn is changing widget's visibility or turning it enabled/disabled because upon triggering some switch, and also storing conditional data to a widget's input field.
DependsOn uses data attributes for fetching expected configuration. To define data attribute from JCR use granite:data sub-node under the widget node. AEM Authoring Toolkit provides a set of annotations to use DependsOn from Java code.
DependsOn
is based on the following data attributes.
For dependent field:
- dependsOn (
data-dependson
) - to provide query with condition or expression for the action. - dependsOnAction (
data-dependsonaction
) - (optional) to define action that should be executed. - dependsOnSkipInitial (
data-dependsonskipinitial
) - (optional) marker to disable initial execution.
For referenced field:
- dependsOnRef (
data-dependsonref
) - to mark a field, that is referenced from the query. - dependsOnRefType (
data-dependsonreftype
) - (optional) to define expected type of reference value.
@DependsOn
- to define single DependsOn Action with the Query. Multiple annotations per element can be used.@DependsOnRef
- to define referenced element name and type. Only a single annotation is allowed.@DependsOnTab
- to define DependsOn query withtab-visibility
action for tab.
The following snippet discloses the @DependsOn
usage in brief:
public class DependsOnSample {
@DialogField(
label = "The switch",
description = "Turn the fieldset visibility on/off"
)
@Switch
@DependsOnRef(name = "first")
private boolean firstDialogEnabled;
@DialogField
@FieldSet(
title = "Conditional fieldset",
description = "This will be shown or hidden depending on the switch"
)
@DependsOn(query = "@first")
@PlaceOnTab(TAB_ADDITIONAL_TOPICS)
private SomeFieldsetDefinitionClass fieldsetDefinitionClass;
}
Built-in plugin actions are:
visibility
- hide the element if the query result is 'falsy'tab-visibility
- hide the tab or element's parent tab if the query result is 'falsy'set
- set the query result as field's valueset-if-blank
- set the query result as field's value only if the current value is blankrequired
- set the required marker of the field from the query result.validate
- set the validation state of the field from the query result.
If the action is not specified then visibility
is used by default.
Query is a plain JavaScript condition or expression. Any global and native JavaScript object can be used inside of Query. We can also use dynamic references to access other fields' values. To define a reference we should specify referenced field name in dependsOnRef attribute on it. Then it's accessible in the query by this name via @ symbol.
Area to find referenced field can be narrowed down by providing the Scope. Scope is a CSS Selector of the closest container element. Scope is defined in parentheses after reference name.
Examples:
@enableCta (coral-panel)
- will reference the value of the field marked bydependsOnRef=enableCta
in bounds of the closest parent Panel element.@enableCta (.my-fieldset)
- will reference the value of the field marked bydependsOnRef=enableCta
in bounds of the closest parent container element with "my-fieldset" class.
"Back-forward" CSS selectors are available in the Scope syntax, i.e. we can define CSS selector to determinate parent element and then provide selector to search the target element for scope in bounds of found parent. Back and forward selectors are separated by '|>' combination.
For example:
@enableCta (section |> .fieldset-1)
- will reference the value of the field marked bydependsOnRef=enableCta
in bounds of element withfieldset-1
class placed in the closest parent section element.