', {text: labelWithKeyAsFallback, class: "coral-GenericMultiField-label"});
162 |
163 | li.append(liInner);
164 | li.append($(removeButton));
165 | li.append(editButton);
166 | li.append(moveButton);
167 | if (this.readOnly) {
168 | $(".coral-SpectrumMultiField-remove", li).attr("disabled", "disabled");
169 | $(".coral-SpectrumMultiField-edit", li).attr("disabled", "disabled");
170 | $(".coral-SpectrumMultiField-move", li).attr("disabled", "disabled");
171 | }
172 | return li;
173 | },
174 |
175 | /**
176 | * Initializes listeners.
177 | * @private
178 | */
179 | _addListeners: function () {
180 | var that = this;
181 |
182 | this.$element.on("click", ".js-coral-SpectrumMultiField-add", function (e) {
183 | Merkle.Helper.addMarkup(Merkle.Helper.CONST.ADD_ITEM_WORKFLOW);
184 | e.preventDefault();
185 | e.stopPropagation();
186 | that._addNewItem();
187 | });
188 |
189 | this.$element.on("click", ".js-coral-SpectrumMultiField-remove", function (e) {
190 | var currentItem = $(this).closest("li");
191 | that._removeItem(currentItem);
192 | });
193 |
194 | this.$element.on("click", ".js-coral-SpectrumMultiField-edit", function (e) {
195 | var currentItem = $(this).closest("li");
196 | that._editItem(currentItem);
197 | });
198 |
199 |
200 | this.$element
201 | .fipo("taphold", "mousedown", ".js-coral-SpectrumMultiField-move", function (e) {
202 | e.preventDefault();
203 |
204 | var item = $(this).closest("li");
205 | item.prevAll().addClass("drag-before");
206 | item.nextAll().addClass("drag-after");
207 |
208 | // Fix height of list element to avoid flickering of page
209 | that.ol.css({height: that.ol.height() + $(e.item).height() + "px"});
210 | new CUI.DragAction(e, that.$element, item, [that.ol], "vertical");
211 | })
212 | .on("dragenter", function (e) {
213 | that.ol.addClass("drag-over");
214 | that._reorderPreview(e);
215 | })
216 | .on("dragover", function (e) {
217 | that._reorderPreview(e);
218 | })
219 | .on("dragleave", function (e) {
220 | that.ol.removeClass("drag-over").children().removeClass("drag-before drag-after");
221 | })
222 | .on("drop", function (e) {
223 | that._reorder($(e.item));
224 | that.ol.children().removeClass("drag-before drag-after");
225 | })
226 | .on("dragend", function (e) {
227 | that.ol.css({height: ""});
228 | });
229 |
230 | document.addEventListener('keydown', function (event) {
231 | if (event.key === 'Escape') {
232 | if (Merkle.Helper.hasMarkup(Merkle.Helper.CONST.ADD_ITEM_WORKFLOW)) {
233 | var dialog = $('body.' + Merkle.Helper.CONST.ADD_ITEM_WORKFLOW);
234 | dialog.find('.cq-dialog-cancel').click();
235 | }
236 | }
237 | }, true);
238 |
239 | },
240 |
241 | /**
242 | * Opens the edit dialog for a given item id.
243 | * If the item id is not defined, a empty dialog for a new item is loaded.
244 | *
245 | * @param {String} itemPath of the current item
246 | * @param {Function} cancelCallback on abort.
247 | * @private
248 | */
249 | _openEditDialog: function (itemPath, cancelCallback) {
250 | if (!itemPath) {
251 | throw new Error("Parameter 'itemPath' must be defined");
252 | }
253 |
254 | var that = this,
255 | path = this.itemDialog + ".html" + itemPath;
256 |
257 | var dialog = {
258 | getConfig: function () {
259 | return {
260 | src: path,
261 | itemPath: itemPath,
262 | loadingMode: "auto",
263 | layout: "auto",
264 | isGenericMultifield: true
265 | };
266 | },
267 | getRequestData: function () {
268 | return {};
269 | },
270 | onSuccess: function () {
271 | that._updateList(true);
272 | return $.Deferred().promise();
273 | },
274 | onCancel: cancelCallback
275 | }
276 | try {
277 | Merkle.GenericMultifieldDialogHandler.openDialog(dialog);
278 | } catch (error) {
279 | console.error(error);
280 | if ($.isFunction(cancelCallback)) {
281 | cancelCallback();
282 | }
283 | }
284 | },
285 |
286 | /**
287 | * Edits an item by opening the item dialog.
288 | *
289 | * @param {Object} item List item to be edited.
290 | * @private
291 | */
292 | _editItem: function (item) {
293 | var path = this.crxPath + "/" + this.itemStorageNode + "/" + item.attr("id");
294 | this._openEditDialog(path);
295 | },
296 |
297 | /**
298 | * Adds a new item by opening the empty item dialog if maxElements is not reached.
299 | * Otherwise, a warning dialog is displayed.
300 | *
301 | * @private
302 | */
303 | _addNewItem: function () {
304 | var that = this;
305 | var currentElements = this.$element.find("li").length;
306 |
307 | if (!this.maxElements || (currentElements < this.maxElements)) {
308 | this._createNode(this.crxPath + "/" + this.itemStorageNode + "/*", function (path) {
309 | that._openEditDialog(path, function (event, dialog) {
310 | that._deleteNode(path, function () {
311 | // call update list after successful deletion of node
312 | that._updateList(true);
313 | });
314 | });
315 | });
316 | } else {
317 | this.ui.alert(Granite.I18n.get("Maximum reached"), Granite.I18n.get("Maximum number of {0} item(s) reached, you cannot add any additional items.", this.maxElements), "warning");
318 | }
319 | },
320 |
321 | /**
322 | * Removes an item from the list.
323 | * Shows a warning dialog ('Cancel','Delete') before the delete action is executed.
324 | *
325 | * @param {Object} item the list item to be deleted
326 | * @private
327 | */
328 | _removeItem: function (item) {
329 | var that = this,
330 | currentElements = this.$element.find("li").length;
331 |
332 | if (!this.minElements || (currentElements > this.minElements)) {
333 | this.ui.prompt(Granite.I18n.get("Remove Item"), Granite.I18n.get("Are you sure you want to delete this item?", this.minElements), "warning",
334 | [{text: Granite.I18n.get("Cancel")},
335 | {
336 | text: Granite.I18n.get("Delete"),
337 | warning: true,
338 | handler: function () {
339 | if (currentElements === 1) {
340 | // delete whole itemStorageNode if last item is being removed
341 | that._deleteNode(that.crxPath + "/" + that.itemStorageNode, deleteItemCallback);
342 | } else {
343 | that._deleteNode(that.crxPath + "/" + that.itemStorageNode + "/" + item.attr("id"), deleteItemCallback);
344 | }
345 | }
346 | }]);
347 | } else {
348 | this.ui.alert(Granite.I18n.get("Minimum reached"), Granite.I18n.get("Minimum number of {0} item(s) reached, you cannot delete any additional items.", this.minElements), "warning");
349 | }
350 |
351 | // remove item from DOM on successful callback
352 | function deleteItemCallback(path) {
353 | item.remove();
354 | that._triggerChangeEvent();
355 | }
356 | },
357 |
358 | /**
359 | * Performs drag and drop reordering and
360 | * executes a sling reordering request on crx items.
361 | *
362 | * @param {Object} item the dragging item.
363 | * @private
364 | */
365 | _reorder: function (item) {
366 | var before = this.ol.children(".drag-after").first();
367 | var after = this.ol.children(".drag-before").last();
368 |
369 |
370 | if (before.length > 0) {
371 | item.insertBefore(before);
372 | $.ajax({
373 | type: "POST",
374 | data: ":order=before " + before.attr("id"),
375 | url: this.crxPath + "/" + this.itemStorageNode + "/" + item.attr("id")
376 | });
377 | } else if (after.length > 0) {
378 | item.insertAfter(after);
379 | $.ajax({
380 | type: "POST",
381 | data: ":order=after " + after.attr("id"),
382 | url: this.crxPath + "/" + this.itemStorageNode + "/" + item.attr("id")
383 | });
384 |
385 | }
386 | },
387 |
388 | /**
389 | * Creates a preview view on drag and drop reordering action.
390 | *
391 | * @param {Event} e the event object.
392 | * @private
393 | */
394 | _reorderPreview: function (e) {
395 | var pos = this._pagePosition(e);
396 | this.ol.children(":not(.is-dragging)").each(function () {
397 | var el = $(this);
398 | var isAfter = pos.y < (el.offset().top + el.outerHeight() / 2);
399 | el.toggleClass("drag-after", isAfter);
400 | el.toggleClass("drag-before", !isAfter);
401 | });
402 | },
403 |
404 | /**
405 | * Gets the page position.
406 | *
407 | * @param {Event} e the event object.
408 | * @private
409 | */
410 | _pagePosition: function (e) {
411 | var touch = {};
412 | if (e.originalEvent) {
413 | var o = e.originalEvent;
414 | if (o.changedTouches && o.changedTouches.length > 0) {
415 | touch = o.changedTouches[0];
416 | }
417 | if (o.touches && o.touches.length > 0) {
418 | touch = o.touches[0];
419 | }
420 | }
421 |
422 | return {
423 | x: touch.pageX || e.pageX,
424 | y: touch.pageY || e.pageY
425 | };
426 | },
427 |
428 | /**
429 | * Creates a new node at given path.
430 | *
431 | * @param {String} path of node to be deleted.
432 | * @param {Function} callback node that has been created.
433 | * @private
434 | */
435 | _createNode: function (path, callback) {
436 | $.ajax({
437 | type: "POST",
438 | headers: {
439 | Accept: "application/json,**/**;q=0.9"
440 | },
441 | url: path
442 | }).done(function (data) {
443 | if ($.isFunction(callback)) {
444 | if (data && data.path) {
445 | callback(data.path);
446 | }
447 | }
448 | });
449 | },
450 |
451 | /**
452 | * Deletes the node at given path.
453 | *
454 | * @param {String} path of node to be deleted.
455 | * @param {Function} callback node that has been created.
456 | * @private
457 | */
458 | _deleteNode: function (path, callback) {
459 | $.ajax({
460 | type: "POST",
461 | data: ":operation=delete",
462 | url: path
463 | }).done(function (data) {
464 | if ($.isFunction(callback)) {
465 | callback(path);
466 | }
467 | });
468 | },
469 |
470 | /**
471 | * Triggers the change event with the DOM element as the source.
472 | *
473 | * @private
474 | */
475 | _triggerChangeEvent: function () {
476 | this.$element.trigger("change");
477 | }
478 | });
479 |
480 | // put Merkle.GenericMultiField on widget registry
481 | CUI.Widget.registry.register(" ", Merkle.GenericMultiField);
482 |
483 | // Data API
484 | if (CUI.options.dataAPI) {
485 | $(document).on("cui-contentloaded.data-api", function (e, data) {
486 | $(".coral-GenericMultiField[data-init~='genericmultifield']", e.target).genericMultiField();
487 | if (data && data._foundationcontentloaded) {
488 | $(".coral-GenericMultiField[data-init~='genericmultifield']", e.target).trigger("change");
489 | }
490 | });
491 | }
492 | }(window.jQuery));
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/GenericMultifieldDialogHandler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This part creates a new DialogFrame for the Generic Multi-field.
3 | */
4 | ;
5 | (function ($, ns, channel, window, document, undefined) {
6 | "use strict";
7 |
8 | /**
9 | * This dialog frame represents the Granite UI Dialog Frame in the Generic
10 | * MultiField (Merkle) context. It is basically a copy of the DialogFrame.js
11 | * with little extensions for the Generic MultiField.
12 | *
13 | * @namespace
14 | * @alias Merkle.DialogFrame
15 | */
16 | ns.GenericMultifieldDialogHandler = (function () {
17 | var self = {};
18 | var DIALOG_SELECTOR = "coral-dialog";
19 | var DIALOG_CONTENT_SELECTOR = "coral-dialog-content";
20 | var DIALOG_MODE = {
21 | COMPONENT: "COMPONENT",
22 | PAGE: "PAGE"
23 | };
24 |
25 | /**
26 | * Array of parent dialogs.
27 | *
28 | * Save parent dialogs as a stack. Whenever a dialog gets closed, the parent
29 | * gets opened (if existing).
30 | */
31 | self.parentDialogs = [];
32 | /**
33 | * Array of form data from parent dialogs.
34 | *
35 | * Save form data of parent dialogs as a stack. Whenever a dialog gets
36 | * closed, the parent gets opened (if existing) and the data gets restored.
37 | */
38 | self.parentDialogsData = [];
39 | /**
40 | * Mode of dialog.
41 | *
42 | * Specifies if dialog was loaded by a component's dialog or by a page
43 | * properties dialog.
44 | */
45 | self.dialogMode;
46 |
47 | /**
48 | * Opens a new dialog.
49 | * Closes the current dialog and opens the new one.
50 | *
51 | * @param {Object} dialog dialog to be opened.
52 | */
53 | self.openDialog = function (dialog) {
54 | var currentDialog = Granite.author.DialogFrame.currentDialog;
55 | if (currentDialog) {
56 | self.dialogMode = DIALOG_MODE.COMPONENT;
57 |
58 | if (self.parentDialogs.length === 0) {
59 | currentDialog = _extendOriginalDialog(currentDialog);
60 | }
61 |
62 | // push old dialog to parent
63 | self.parentDialogs.push(currentDialog);
64 | // save data of parent dialog
65 | _saveDialogData(currentDialog);
66 |
67 | // close current dialog
68 | Granite.author.DialogFrame.closeDialog();
69 | } else {
70 | self.dialogMode = DIALOG_MODE.PAGE;
71 | }
72 |
73 | // create custom backdrop
74 | ns.Helper.createCustomBackdrop();
75 |
76 | // open new dialog
77 | Granite.author.DialogFrame.openDialog(_extendGenericMultiFieldDialog(dialog));
78 | }
79 |
80 | /**
81 | * Extend original dialog.
82 | * Extends the dialog object with necessary callback functions.
83 | *
84 | * @param {Object} originalDialog dialog to be extended.
85 | * @returns {Object} extended dialog.
86 | * @private
87 | */
88 | function _extendOriginalDialog(originalDialog) {
89 | // save original onClose callback
90 | var _onCloseOrig = originalDialog.onClose, _onReadyOrig = originalDialog.onReady;
91 |
92 | // overwrite onClose function of dialog
93 | originalDialog.onClose = function () {
94 | // if original onClose callback was set, execute it first
95 | if ($.isFunction(_onCloseOrig)) {
96 | _onCloseOrig();
97 | }
98 |
99 | ns.Helper.removeCustomBackdrop();
100 | }
101 |
102 | // overwrite onReady function of dialog if "onCancel" callback has been
103 | // configured
104 | originalDialog.onReady = function () {
105 | ns.Helper.createCustomBackdrop();
106 |
107 | // if original onReady callback was set, execute it first
108 | if ($.isFunction(_onReadyOrig)) {
109 | _onReadyOrig();
110 | }
111 | }
112 |
113 | return originalDialog;
114 | }
115 |
116 | /**
117 | * Extend dialogs created by generic multi-field.
118 | * Extends the dialog object with necessary callback functions.
119 | *
120 | * @param {Object} dialog dialog to be extended.
121 | * @returns {Object} extended dialog.
122 | * @private
123 | */
124 | function _extendGenericMultiFieldDialog(dialog) {
125 | // save original onClose callback
126 | var _onCloseOrig = dialog.onClose, _onReadyOrig = dialog.onReady;
127 |
128 | // overwrite onClose function of dialog
129 | dialog.onClose = function () {
130 | ns.Helper.removeMarkup(ns.Helper.CONST.ADD_ITEM_WORKFLOW);
131 |
132 | // if original onClose callback was set, execute it first
133 | if ($.isFunction(_onCloseOrig)) {
134 | _onCloseOrig();
135 | }
136 |
137 | Granite.author.DialogFrame.closeDialog();
138 |
139 | // execute function after fading effect has finished
140 | setTimeout(function waitToClose() {
141 | // make sure that currentDialog has been cleared
142 | if (Granite.author.DialogFrame.isOpened()) {
143 | setTimeout(waitToClose, 50);
144 | }
145 |
146 | // perform closing of dialog
147 | _performCloseDialog();
148 | }, 50);
149 | }
150 |
151 | // overwrite onReady function of dialog if "onCancel" callback has been
152 | // configured
153 | dialog.onReady = function () {
154 | // if original onReady callback was set, execute it first
155 | if ($.isFunction(_onReadyOrig)) {
156 | _onReadyOrig();
157 | }
158 |
159 | // register callback function to dialog cancelled event
160 | if ($.isFunction(dialog.onCancel)) {
161 | var cqDialogForm = ns.Helper.findDialog(dialog.getConfig().itemPath, ".cq-dialog-cancel");
162 | $(cqDialogForm, DIALOG_SELECTOR).click(dialog.onCancel);
163 | }
164 | }
165 |
166 | return dialog;
167 | }
168 |
169 | /**
170 | * Performs closing of current dialog.
171 | * Closes the current dialog and opens its parent.
172 | */
173 | function _performCloseDialog() {
174 | // get parent dialog
175 | var parentDialog = self.parentDialogs.pop();
176 | // open parent dialog if it exists
177 | if (parentDialog) {
178 | // register handler to restore data after the content of the dialog has been loaded
179 | $(document).on("foundation-contentloaded", function restoreDataHandler(e, data) {
180 | if (!data.restored) {
181 | // restore data
182 | _restoreDialogData(parentDialog);
183 | }
184 |
185 | // unregister handler
186 | $(document).off("foundation-contentloaded", restoreDataHandler);
187 | });
188 |
189 | Granite.author.DialogFrame.openDialog(parentDialog);
190 | }
191 |
192 | // remove custom backdrop on the last dialog after fading effect has finished
193 | if (self.dialogMode === DIALOG_MODE.PAGE && self.parentDialogs.length === 0) {
194 | ns.Helper.removeCustomBackdrop();
195 | }
196 | }
197 |
198 | /**
199 | * Saves the dialog and it's data
200 | *
201 | * @param {Object} dialog from which to retrieve the data from.
202 | * @private
203 | */
204 | function _saveDialogData(dialog) {
205 | var dialogContainer = _getDomElementForDialog(dialog);
206 | if (dialogContainer) {
207 | // push content of current dialog
208 | self.parentDialogsData.push($(DIALOG_CONTENT_SELECTOR, dialogContainer));
209 | }
210 | }
211 |
212 | /**
213 | * Restores the dialog and it's data.
214 | *
215 | * @param {Object} dialog to be restored.
216 | * @private
217 | */
218 | function _restoreDialogData(dialog) {
219 | var dialogContainer = _getDomElementForDialog(dialog);
220 | if (dialogContainer) {
221 | // replace content with previous
222 | $(DIALOG_CONTENT_SELECTOR, dialogContainer).replaceWith(self.parentDialogsData.pop());
223 | dialogContainer.trigger("foundation-contentloaded", {restored: true});
224 | }
225 | }
226 |
227 | /**
228 | * Returns DOM element for dialog
229 | *
230 | * @param {Object} dialog to retrieve.
231 | * @returns {Object} self jQuery object.
232 | * @private
233 | */
234 | function _getDomElementForDialog(dialog) {
235 | var cqDialogForm;
236 | if (dialog.getConfig().itemPath) {
237 | cqDialogForm = ns.Helper.findDialog(dialog.getConfig().itemPath);
238 | } else {
239 | cqDialogForm = ns.Helper.findDialog(dialog.editable.path);
240 | }
241 | return $(cqDialogForm, DIALOG_SELECTOR).closest(DIALOG_SELECTOR);
242 | }
243 |
244 | return self;
245 | }());
246 |
247 | }(jQuery, Merkle, jQuery(document), this, document));
248 |
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/GenericMultifieldHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helpers for the Generic Multi-field.
3 | */
4 | (function ($, ns, channel, window, document, undefined) {
5 | "use strict";
6 |
7 | /**
8 | * Helpers for the Generic Multi-field in the ns namespace.
9 | */
10 | ns.Helper = {
11 |
12 | CONST: {
13 | ADD_ITEM_WORKFLOW: 'add-item',
14 | CUSTOM_BACKDROP_CLASS: 'q-dialog-backdrop-GenericMultiField',
15 | CUSTOM_BACKDROP_SELECTOR: '.cq-dialog-backdrop-GenericMultiField',
16 | CORAL_GENERIC_MULTIFIELD_SELECTOR: '.coral-GenericMultiField',
17 | ERROR_MESSAGE_REQUIRED: 'Error: Please fill out this field.',
18 | ERROR_MESSAGE_MIN: 'Error: At least {0} items must be created.',
19 | ERROR_MESSAGE_MAX: 'Error: At most {0} items can be created.'
20 | },
21 |
22 | /**
23 | * Displays the dialog backdrop over the content.
24 | */
25 | createCustomBackdrop: function () {
26 | var $customBackdrop = $(ns.Helper.CONST.CUSTOM_BACKDROP_SELECTOR),
27 | $originalBackdrop = $(".cq-dialog-backdrop");
28 |
29 | // don't create backdrop if it already exists
30 | if ($customBackdrop.length) {
31 | return;
32 | }
33 |
34 | // create backdrop
35 | $customBackdrop = $('
');
36 | if ($originalBackdrop.length) {
37 | $customBackdrop.insertAfter($originalBackdrop);
38 | } else {
39 | $("body").append($customBackdrop);
40 | }
41 |
42 | // backdrop has CSS transition to fade in
43 | $customBackdrop.css("opacity", "1");
44 | },
45 |
46 | /**
47 | * Retrieves dialog object.
48 | *
49 | * @param path of dialog to fetch.
50 | * @param optionalSelector to specific dialog selection.
51 | * @returns {Object} found dialog.
52 | */
53 | findDialog: function (path, optionalSelector = "") {
54 | var cqDialogForm = $("form.cq-dialog[action='" + path + "'] " + optionalSelector);
55 | if (cqDialogForm === undefined || !cqDialogForm.length) {
56 | cqDialogForm = $("form.cq-dialog[action='" + this._manglePath(path) + "'] " + optionalSelector);
57 | }
58 | return cqDialogForm;
59 | },
60 |
61 | /**
62 | * Mangle string value.
63 | *
64 | * @param path to mangle.
65 | * @returns {String} adjusted path value.
66 | * @private
67 | */
68 | _manglePath: function (path) {
69 | if (!path) {
70 | return;
71 | }
72 | return path.replace(/\/(\w+):(\w+)/g, "/_$1_$2");
73 | },
74 |
75 | /**
76 | * Hides the dialog backdrop over the content.
77 | */
78 | removeCustomBackdrop: function () {
79 | var $customBackdrop = $(ns.Helper.CONST.CUSTOM_BACKDROP_SELECTOR);
80 | $customBackdrop.one("transitionend", function () {
81 | $customBackdrop.remove();
82 | });
83 | $customBackdrop.css("opacity", "0");
84 |
85 | // remove backdrop after a maximum of 1s if no transition event was fired
86 | setTimeout(function waitToClose() {
87 | // remove backdrop
88 | if ($customBackdrop.length) {
89 | $customBackdrop.remove();
90 | }
91 | }, 1000);
92 | },
93 |
94 | /**
95 | * Adds a CSS markup class to the body element.
96 | * @param {String} markup CSS class name to add.
97 | */
98 | addMarkup: function (markup) {
99 | document.body.classList.add(markup);
100 | },
101 |
102 | /**
103 | * Removes a CSS markup class from the body element.
104 | * @param {String} markup CSS class name to remove.
105 | */
106 | removeMarkup: function (markup) {
107 | document.body.classList.remove(markup);
108 | },
109 |
110 | /**
111 | * Checks if the body element has a specific markup class.
112 | * @param {String} markup CSS class name to check for.
113 | * @returns {boolean} true if the class exists, false otherwise.
114 | */
115 | hasMarkup: function (markup) {
116 | return document.body.classList.contains(markup);
117 | }
118 |
119 | }
120 |
121 | }(jQuery, Merkle, jQuery(document), this, document));
122 |
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/Namespace.js:
--------------------------------------------------------------------------------
1 | // Create the namespace
2 | var Merkle = Merkle || {};
3 |
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/js.txt:
--------------------------------------------------------------------------------
1 | Namespace.js
2 | GenericMultifieldHelper.js
3 | GenericMultifieldDialogHandler.js
4 | CUI.GenericMultiField.js
5 | validations.js
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/clientlibs/js/validations.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Validates the generic multi-field's minimum and maximum number of elements
3 | * restriction.
4 | */
5 |
6 | (function (window, $, CUI) {
7 | "use strict";
8 |
9 | /**
10 | * Performs the validation of the generic multi-field.
11 | *
12 | * @param {Object} multiField to perform validation on.
13 | * @private
14 | */
15 | function _performValidation(multiField) {
16 | var api = multiField.adaptTo("foundation-validation");
17 | if (api) {
18 | api.checkValidity();
19 | api.updateUI();
20 | }
21 | }
22 |
23 | // get global foundation registry
24 | var registry = $(window).adaptTo("foundation-registry");
25 |
26 | // register adapter for generic multi-field
27 | registry.register("foundation.adapters", {
28 | type: "foundation-field",
29 | selector: Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR,
30 | adapter: function (el) {
31 | var $el = $(el);
32 | return {
33 | getName: function () {
34 | return $el.data("name");
35 | },
36 | setName: function (name) {
37 | $el.data("name", name);
38 | },
39 | isDisabled: function () {
40 | return !!$el.attr("disabled");
41 | },
42 | setDisabled: function (disabled) {
43 | if (disabled === true) {
44 | $el.attr("disabled", "disabled");
45 | }
46 | },
47 | isInvalid: function () {
48 | return $el.attr("aria-invalid") === "true";
49 | },
50 | setInvalid: function (invalid) {
51 | $el.attr("aria-invalid", !!invalid ? "true" : "false").toggleClass("is-invalid", invalid);
52 | },
53 | isRequired: function () {
54 | return $el.attr("aria-required") === "true";
55 | },
56 | setRequired: function (required) {
57 | $el.attr("aria-required", !!required ? "true" : "false");
58 | }
59 | };
60 | }
61 | });
62 |
63 | // register selector for generic multi-field
64 | registry.register("foundation.validation.selector", {
65 | submittable: Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR,
66 | candidate: ".coral-GenericMultiField:not([disabled]):not([data-renderreadonly=true])",
67 | exclusion: ".coral-GenericMultiField *"
68 | });
69 |
70 | // register validator for generic multi-field
71 | registry.register("foundation.validation.validator", {
72 | selector: Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR,
73 | validate: function (el) {
74 | var $field = $(el).closest(".coral-Form-field"), items = $field.find(".coral-GenericMultiField-list li"),
75 | minElements = $field.data("minelements"), maxElements = $field.data("maxelements");
76 |
77 | // validate required attribute
78 | if ($field.adaptTo("foundation-field").isRequired() && items.length === 0) {
79 | return Granite.I18n.get(Merkle.Helper.CONST.ERROR_MESSAGE_REQUIRED);
80 |
81 | }
82 |
83 | // validate min and max elements (only if field is required)
84 | if ($field.adaptTo("foundation-field").isRequired()) {
85 | // validate if minElements restriction is met
86 | if (items && !isNaN(minElements) && items.length < minElements) {
87 | return Granite.I18n.get(Merkle.Helper.CONST.ERROR_MESSAGE_MIN, minElements);
88 | }
89 | // validate if maxElements restriction is met
90 | if (items && !isNaN(maxElements) && items.length > maxElements) {
91 | return Granite.I18n.get(Merkle.Helper.CONST.ERROR_MESSAGE_MAX, maxElements);
92 |
93 | }
94 | }
95 |
96 | return null;
97 | },
98 | show: function (el, message, ctx) {
99 | var $field = $(el).closest(".coral-Form-field");
100 | $field.adaptTo("foundation-field").setInvalid(true);
101 |
102 | setTimeout(function() {
103 | $field.siblings(".coral-Form-errorlabel").each(function (index, element) {
104 | if (index > 0) {
105 | element.remove()
106 | }
107 | });
108 | }, 200);
109 |
110 | ctx.next();
111 | },
112 | clear: function (el, ctx) {
113 | var $field = $(el).closest(".coral-Form-field");
114 | $field.adaptTo("foundation-field").setInvalid(false);
115 | $field.siblings(".coral-Form-fielderror").remove();
116 | $field.siblings(".coral-Form-errorlabel").remove();
117 | ctx.next();
118 | }
119 | });
120 |
121 | // perform validation every time generic multifield changed
122 | $(document).on("change", Merkle.Helper.CONST.CORAL_GENERIC_MULTIFIELD_SELECTOR, function () {
123 | _performValidation($(this));
124 | });
125 |
126 | })(window, Granite.$, CUI);
127 |
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/init.jsp:
--------------------------------------------------------------------------------
1 | <%
2 | %>
3 | <%@include file="/libs/granite/ui/global.jsp" %>
4 | <%
5 | %>
6 | <%@page session="false"
7 | import="com.adobe.granite.ui.components.Field,
8 | org.apache.sling.api.resource.ValueMap,
9 | org.apache.sling.api.wrappers.ValueMapDecorator,
10 | java.util.HashMap" %>
11 | <%
12 | final ValueMap vm = new ValueMapDecorator(new HashMap
());
13 |
14 | // set non-empty string, otherwise the read only rendering will not work
15 | vm.put("value", "-");
16 |
17 | request.setAttribute(Field.class.getName(), vm);
18 | %>
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/readonly/readonly.jsp:
--------------------------------------------------------------------------------
1 | <%
2 | %>
3 | <%@include file="/libs/granite/ui/global.jsp" %>
4 | <%
5 | %>
6 | <%@page session="false"
7 | import="com.adobe.granite.ui.components.AttrBuilder,
8 | com.adobe.granite.ui.components.Config,
9 | com.adobe.granite.ui.components.Tag" %>
10 | <%
11 | Config cfg = cmp.getConfig();
12 |
13 | Tag tag = cmp.consumeTag();
14 | AttrBuilder attrs = tag.getAttrs();
15 |
16 | attrs.addClass("coral-GenericMultiField");
17 | attrs.add("data-init", "genericmultifield");
18 | attrs.add("id", cfg.get("id", String.class));
19 | attrs.addClass(cfg.get("class", String.class));
20 | attrs.addRel(cfg.get("rel", String.class));
21 | attrs.add("title", i18n.getVar(cfg.get("title", String.class)));
22 |
23 | attrs.addOthers(cfg.getProperties(), "id", "rel", "class", "title", "fieldLabel", "fieldDescription");
24 |
25 | String fieldLabel = cfg.get("fieldLabel", "");
26 | %>
27 |
28 | >
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/main/resources/SLING-INF/apps/merkle/genericmultifield/render.jsp:
--------------------------------------------------------------------------------
1 | <%
2 | %>
3 | <%@include file="/libs/granite/ui/global.jsp" %>
4 | <%
5 | %>
6 | <%@page session="false"
7 | import="com.adobe.granite.ui.components.AttrBuilder,
8 | com.adobe.granite.ui.components.Config,
9 | com.adobe.granite.ui.components.Tag,
10 | org.osgi.service.cm.Configuration,
11 | org.osgi.service.cm.ConfigurationAdmin" %>
12 | <%
13 | final ConfigurationAdmin cfgAdmin = sling.getService(org.osgi.service.cm.ConfigurationAdmin.class);
14 | final Configuration mergePickerConfig = cfgAdmin.getConfiguration("org.apache.sling.resourcemerger.picker.overriding", null);
15 | final String mergeRoot = (String) mergePickerConfig.getProperties().get(org.apache.sling.resourcemerger.spi.MergedResourcePicker2.MERGE_ROOT);
16 |
17 | final Config cfg = cmp.getConfig();
18 | final Tag tag = cmp.consumeTag();
19 | final AttrBuilder attrs = tag.getAttrs();
20 |
21 | attrs.addClass("coral-GenericMultiField");
22 | attrs.add("data-init", "genericmultifield");
23 | attrs.add("id", cfg.get("id", String.class));
24 | attrs.addClass(cfg.get("class", String.class));
25 | attrs.addRel(cfg.get("rel", String.class));
26 | attrs.add("title", i18n.getVar(cfg.get("title", String.class)));
27 | attrs.add("data-mergeroot", mergeRoot);
28 | if (cfg.get("required", false)) {
29 | attrs.add("aria-required", true);
30 | }
31 |
32 | attrs.addOthers(cfg.getProperties(), "id", "rel", "class", "title", "fieldLabel", "fieldDescription", "storageWarningText", "renderReadOnly", "required");
33 | %>
34 |
35 |
39 |
40 |
--------------------------------------------------------------------------------