/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* <p>Extends the standard (read-only) [property renderer]{@link module:alfresco/renderers/Property} to provide
* the ability to edit and save changes to the property. The edit view is rendered by a
* [DojoValidationTextBox widget]{@link module:alfresco/forms/controls/DojoValidationTextBox} and this module accepts the same
* [validationConfig]{@link module:alfresco/forms/controls/BaseFormControl#validationConfig} as it does.</p>
* <p>When an edit is completed and saved a publication will be made and that should be defined using the standard
* "publishTopic", "publishPayload" and related publication attributes. However, for convenience it is assumed that the typical
* use case will be for editing the properties of nodes and so if the "publishTopic" attribute is configured as null then
* the publication will automatically be set up to result in saving node properties (however, it will be necessary to make
* sure that the [CrudService]{@link module:alfresco/services/CrudService} is included on the page to service those requests).</p>
*
* @module alfresco/renderers/InlineEditProperty
* @extends module:alfresco/renderers/Property
* @mixes external:dojo/_OnDijitClickMixin
* @mixes module:alfresco/core/CoreWidgetProcessing
* @mixes module:alfresco/renderers/_PublishPayloadMixin
* @author Dave Draper
*/
define(["dojo/_base/declare",
"alfresco/renderers/Property",
"dijit/_OnDijitClickMixin",
"alfresco/core/CoreWidgetProcessing",
"alfresco/renderers/_PublishPayloadMixin",
"alfresco/lists/KeyboardNavigationSuppressionMixin",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/Deferred",
"dojo/dom-class",
"dojo/dom-attr",
"dojo/keys",
"dojo/_base/event",
"dojo/query",
"service/constants/Default",
"alfresco/forms/Form",
"alfresco/forms/controls/DojoValidationTextBox",
"alfresco/forms/controls/HiddenValue"],
function(declare, Property, _OnDijitClickMixin, CoreWidgetProcessing, _PublishPayloadMixin, KeyboardNavigationSuppressionMixin,
lang, array, Deferred, domClass, domAttr, keys, event, query) {
return declare([Property, _OnDijitClickMixin, CoreWidgetProcessing, _PublishPayloadMixin, KeyboardNavigationSuppressionMixin], {
/**
* The array of file(s) containing internationalised strings.
*
* @instance
* @type {object}
* @default [{i18nFile: "./i18n/InlineEditProperty.properties"}]
*/
i18nRequirements: [{i18nFile: "./i18n/InlineEditProperty.properties"}],
/**
* An array of the CSS files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{cssFile:"./css/InlineEditProperty.css"}]
*/
cssRequirements: [{cssFile:"./css/InlineEditProperty.css"}],
/**
* This is the message or message key that will be used for the cancel link text.
*
* @instance
* @type {string}
* @default
*/
cancelLabel: "inline-edit.cancel.label",
/**
* This is the message or message key that will be used for the alt text attribute on the edit icon
*
* @instance
* @type {string}
* @default
*/
editAltText: "inline-edit.edit.altText",
/**
* This is the message or message key that will be used for the label attribute on the edit label
*
* @instance
* @type {string}
* @default
*/
editLabel: "inline-edit.edit.label",
/**
* Whether the widget should be put into edit mode when rendered value is clicked.
*
* @type {boolean}
*/
editOnClickRenderedValue: true,
/**
* An optional array of topics to be subscribed to that can trigger editing. The typical use case is when
* another widget (a [PublishAcing]{@link module:alfresco/renderers/PublishAction} for example) is provided
* that when clicked will toggle editing of the property. The current caveat is that the payload published
* must be the [currentItem]{@link module:alfresco/core/CoreWidgetProcessing#currentItem} of this widget. This
* would be achieved by setting a
* [publishPayloadType]{@link module:alfresco/renderers/_PublishPayloadMixin#publishPayloadType} of
* "CURRENT_ITEM" and for both widgets (the publisher and the subscriber) rendering the same item.
*
* @instance
* @type {string[]}
* @default
* @since 1.0.72
*/
editSubscriptionsTopics: null,
/**
* References the widget used for editing. Created by calling the
* [getFormWidget]{@link module:alfresco/renderers/InlineEditProperty#getFormWidget}
* for the first time.
*
* @instance
* @type {object}
* @default
*/
formWidget: null,
/**
* <p>In certain circimstances it may be necessary to submit additional data along with that
* provided by the main edit control. This configuration property should take the form:</p>
* <p><pre>hiddenDataRules: [
* {
* name: "customProperties",
* rulePassValue: "hiddenData",
* ruleFailValue: "",
* is: ["includeHiddenData"]
* }
* ]</pre></p>
*
* @instance
* @type {array}
* @default
*/
hiddenDataRules: null,
/**
* The is the name of the parameter that will be used to persist changes to the property
* @instance
* @type {string}
* @default
*/
postParam: null,
/**
* The value configured will be used to look up a property for the item being rendered to
* determine whether or not to render the edit controls. If this is configured to be null
* then the edit controls will always be rendered.
*
* @instance
* @type {string}
* @default
* @since 1.0.31
*/
permissionProperty: null,
/**
* Indicates whether or not the currentItem should be updated following a successful
* save event.
*
* @instance
* @type {boolean}
* @default
*/
refreshCurrentItem: false,
/**
* This is the message or message key that will be used for save link text.
*
* @instance
* @type {string}
* @default
*/
saveLabel: "inline-edit.save.label",
/**
* If configured to be false then "Save" and "Cancel" actions will not be displayed when editing
* the property. It will still be possible to save changes by using the ENTER key.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.47
*/
showOkCancelActions: true,
/**
* The source file for the image to use to display when an item is being updated. This will
* typically only be displayed when an XHR request is made to retrieve the latest data for
* the item being edited.
*
* @instance
* @type {string}
* @default
* @since 1.0.83
*/
updateInProgressImgSrc: null,
/**
* The alt text label to use for the update in progress indicator.
*
* @instance
* @type {string}
* @default
* @since 1.0.83
*/
updateInProgressAltText: "inline-edit.update-in-progress.altText",
/**
* The alt text label to use for the update in progress indicator when the
* [updateInProgressItemLabelProperty]{@link module:alfresco/renderers/InlineEditProperty#updateInProgressItemLabelProperty}
* does not match a value in the [currentItem]{@link module:alfresco/core/CoreWidgetProcessing#currentItem}.
*
* @instance
* @type {string}
* @default
* @since 1.0.83
*/
updateInProgressNoLabelAltText: "inline-edit.update-in-progress.no-label.altText",
/**
* The property to to retrieve from the [currentItem]{@link module:alfresco/core/CoreWidgetProcessing#currentItem}
* to insert into the [updateInProgressAltText]{@link module:alfresco/renderers/InlineEditProperty#updateInProgressAltText}
* that identifies the overall item being updated (rather than just the individual property that is being changed).
*
* @instance
* @type {string}
* @default
* @since 1.0.83
*/
updateInProgressItemLabelProperty: "displayName",
/**
* Overrides [the inherited function]{@link module:aikau/core/BaseWidget#createWidgetDom}
* to construct the DOM for the widget using native browser capabilities.
*
* @instance
* @since 1.0.100
*/
createWidgetDom: function alfresco_renderers_InlineEditProperty__createWidgetDom() {
// jshint maxstatements:false
this.domNode = document.createElement("span");
this.renderedValueClassArray.forEach(function(className) {
this.domNode.classList.add(className);
}, this);
this.domNode.classList.add("alfresco-renderers-InlineEditProperty");
var labelSpan = document.createElement("span");
labelSpan.classList.add("label");
labelSpan.textContent = this.label;
this.domNode.appendChild(labelSpan);
this.renderedValueNode = document.createElement("span");
this.renderedValueNode.classList.add("inlineEditValue");
this.renderedValueClassArray.forEach(function(className) {
this.renderedValueNode.classList.add(className);
}, this);
this.renderedValueNode.setAttribute("tabindex", "0");
this.renderedValueNode.innerHTML = this.renderedValue;
this._attach(this.renderedValueNode, "onkeypress", lang.hitch(this, this.onKeyPress));
this._attach(this.renderedValueNode, "ondijitclick", lang.hitch(this, this.onClickRenderedValue));
this.domNode.appendChild(this.renderedValueNode);
this.editNode = document.createElement("span");
this.editNode.classList.add("editor");
this.editNode.classList.add("hidden");
this._attach(this.editNode, "onkeypress", lang.hitch(this, this.onValueEntryKeyPress));
this._attach(this.editNode, "onclick", lang.hitch(this, this.suppressFocusRequest));
this.formWidgetNode = document.createElement("span");
this.editNode.appendChild(this.formWidgetNode);
this.domNode.appendChild(this.editNode);
this.editIconNode = document.createElement("img");
this.editIconNode.classList.add("editIcon");
this.editIconNode.setAttribute("src", this.editIconImageSrc);
this.editIconNode.setAttribute("alt", this.editAltText);
this.editIconNode.setAttribute("title", this.editAltText);
this._attach(this.editIconNode, "ondijitclick", lang.hitch(this, this.onEditClick));
this.domNode.appendChild(this.editIconNode);
var progressNode = document.createElement("img");
progressNode.classList.add("alfresco-renderers-InlineEditProperty__progress");
progressNode.setAttribute("src", this.updateInProgressImgSrc);
progressNode.setAttribute("alt", this.updateInProgressAltText);
this.domNode.appendChild(progressNode);
},
/**
* The topic to publish when a property edit should be persisted. For convenience it is assumed that document
* or folder properties are being edited so this function is called whenever a 'publishTopic' attribute
* has not been set. The defaults are to publish on the "ALF_CRUD_CREATE" topic which will publish a payload
* to be processed by the [CrudService]{@link module:alfresco/services/CrudService} that should result in a
* POST a request being made to the Repository form processor.
*
* @instance
* @type {string}
*/
setDefaultPublicationData: function alfresco_renderers_InlineEditProperty__setDefaultPublicationData() {
this.publishTopic = "ALF_CRUD_CREATE";
this.publishPayloadType = "PROCESS";
this.publishPayloadModifiers = ["processCurrentItemTokens"];
this.publishPayloadItemMixin = false;
this.publishPayload = {
url: "api/node/{jsNode.nodeRef.uri}/formprocessor",
noRefresh: true
};
},
/**
* This extends the inherited function to set the [postParam]{@link module:alfresco/renderers/InlineEditProperty#postParam]
* attribute based on the [propertyToRender]{@link module:alfresco/renderers/InlineEditProperty#propertyToRender] if
* provided. It is expected that these will be different because the properties WebScript that this widget will use
* by default to persist changes takes just the name of the property but this is expected to be nested within the
* [currentItem]{@link module:alfresco/lists/views/layouts/_MultiItemRendererMixin#currentItem}.
*
* @instance
*/
postMixInProperties: function alfresco_renderers_InlineEditProperty__postMixInProperties() {
this.inherited(arguments);
// NOTE: We're just re-using the same progress indicator as used for forms validation here,
// although this could be updated in the future to be something else...
if (!this.updateInProgressImgSrc)
{
this.updateInProgressImgSrc = require.toUrl("alfresco/forms/controls/css/images/ajax_anim.gif");
}
var itemLabel = lang.getObject(this.updateInProgressItemLabelProperty, false, this.currentItem);
if (itemLabel)
{
this.updateInProgressAltText = this.message(this.updateInProgressAltText, {
0: itemLabel
});
}
else
{
this.updateInProgressAltText = this.message(this.updateInProgressNoLabelAltText);
}
// If no topic has been provided then assume the default behaviour of editing document/folder properties
if (!this.publishTopic)
{
this.setDefaultPublicationData();
}
if (this.propertyToRender && !this.postParam)
{
this.postParam = this.propertyToRender;
}
else
{
this.alfLog("warn", "Property to render attribute has not been set", this);
}
if (!this.editIconImageSrc)
{
this.editIconImageSrc = require.toUrl("alfresco/renderers/css/images/edit-16.png");
}
if (this.renderedValue)
{
this.editAltText = this.message(this.editAltText, {
0: this.renderedValue
});
}
else
{
this.editAltText = this.message("inline-edit.edit.altTextNoValue");
}
},
/**
* Extends the [inherited function]{@link module:alfresco/renderers/Property#postCreate} to
* check the [permissionProperty]{@link module:alfresco/renderers/InlineEditProperty#permissionProperty}
* to determine whether or not the current user actually has permission to edit the current item. If
* the user does not have permission then then edit controls will be hidden (and keyboard shortcuts suppressed).
*
* @instance
*/
postCreate: function alfresco_renderers_InlineEditProperty__postCreate() {
this.inherited(arguments);
if (!this.showOkCancelActions)
{
domClass.add(this.domNode, "alfresco-renderers-InlineEditProperty--hide-save-cancel-actions");
}
if (this.permissionProperty)
{
var hasEditPermission = lang.getObject(this.permissionProperty, false, this.currentItem);
if (!hasEditPermission)
{
domClass.add(this.editIconNode, "disabled");
this._disableEdit = true;
}
}
// See AKU-997...
if (this.editSubscriptionsTopics)
{
array.forEach(this.editSubscriptionsTopics, lang.hitch(this, function(topic) {
this.alfSubscribe(topic, lang.hitch(this, function(payload) {
if (payload === this.currentItem)
{
this.onEditClick();
}
}));
}));
}
},
/**
* Gets the form widget that will be rendered as the edit field. By default this will
* return a [textbox]{@link module:alfresco/forms/controls/TextBox}
* but can be overridden to return alternative form controls.
*
* @instance
*/
getPrimaryFormWidget: function alfresco_renderers_InlineEditProperty__getPrimaryFormWidget() {
return {
name: "alfresco/forms/controls/TextBox",
config: {
name: this.postParam,
validationConfig: this.validationConfig,
requirementConfig: this.requirementConfig,
additionalCssClasses: "hiddenlabel",
label: this.message(this.editLabel)
}
};
},
/**
* In certain circimstances it may be necessary to submit additional data along with that
* provided by the main edit control. This function processes configurable hidden data rules
* that generates an array of [hidden form controls]{@link module:alfresco/forms/controls/HiddenValue}
* that are configured with [autoSetConfig]{@link module:alfresco/forms/controls/BaseFormControl#autoSetConfig}
* that is derived from the [hiddenDataRules]{@link module:alfresco/renderers/InlineEditProperty#hiddenDataRules}.
*
* @instance
*/
processHiddenDataRules: function alfresco_renderers_InlineEditProperty__processHiddenDataRules() {
var additionalFormWidgets = [];
if (this.hiddenDataRules)
{
array.forEach(this.hiddenDataRules, lang.hitch(this, this.processHiddenDataRule, additionalFormWidgets));
}
return additionalFormWidgets;
},
/**
* Called for each entry in the [hiddenDataRules]{@link module:alfresco/renderers/InlineEditProperty#hiddenDataRules}
* configuration to add a new [hidden form control]{@link module:alfresco/forms/controls/HiddenValue} definition
* into the supplied array.
*
* @instance
* @param {array} additionalFormWidgets The array to add additional form widgets into
* @param {object} rule The current hidden data rule to process.
*/
processHiddenDataRule: function alfresco_renderers_InlineEditProperty__processHiddenDataRule(additionalFormWidgets, rule) {
additionalFormWidgets.push({
name: "alfresco/forms/controls/HiddenValue",
config: {
name: rule.name,
value: "",
autoSetConfig: [
{
rulePassValue: rule.rulePassValue,
ruleFailValue: rule.ruleFailValue,
rules: [{
targetId: "PRIMARY_FIELD",
is: rule.is,
isNot: rule.isNot
}]
}
]
}
});
},
/**
* Gets the edit widget (creating it the first time it is requested).
*
* @instance
* @returns {object} The widget for editing.
*/
getFormWidget: function alfresco_renderers_InlineEditProperty__getFormWidget() {
if (!this.formWidget)
{
var uuid = this.generateUuid();
var saveTopic = "_SAVE";
var cancelTopic = "_CANCEL";
this.alfSubscribe(uuid + saveTopic, lang.hitch(this, this.onSave), true);
this.alfSubscribe(uuid + cancelTopic, lang.hitch(this, this.onCancel), true);
var primaryFormWidget = this.getPrimaryFormWidget();
var autoSetFields = this.processHiddenDataRules();
lang.setObject("config.fieldId", "PRIMARY_FIELD", primaryFormWidget);
this.formWidget = this.createWidget({
name: "alfresco/forms/Form",
config: {
additionalCssClasses: "alfresco-forms-Form--single-line",
pubSubScope: uuid,
okButtonLabel: this.message(this.saveLabel),
cancelButtonLabel: this.message(this.cancelLabel),
cancelButtonPublishTopic: cancelTopic,
okButtonPublishTopic: saveTopic,
showOkButton: this.showOkCancelActions,
showCancelButton: this.showOkCancelActions,
widgets: [primaryFormWidget].concat(autoSetFields)
}
}, this.formWidgetNode);
// NOTE: This line is specifically required to support Firefox, for some reason the standard
// key handling is being suppressed, this was uncovered on the move from Dojo 1.9.0 to
// both 1.9.6 and then 1.10.4
query(".alfresco-forms-controls-BaseFormControl .control input", this.formWidget.domNode).on("keypress", lang.hitch(this, this.onValueEntryKeyPress));
}
return this.formWidget;
},
/**
* Since 1.0.62 this function is never called and performs no action. The action labels were removed
* and the buttons from the [Form]{@link module:alfresco/forms/Form} are now displayed instead and
* the [Form]{@link module:alfresco/forms/Form} automatically takes care of button enablement.
*
* @instance
* @param {object} payload The details of the updated form validity
* @deprecated Since 1.0.62 - This function no longer performs any action.
*/
onFormValidityChange: function alfresco_renderers_InlineEditProperty__onFormValidityChange() {
// No action.
},
/**
* This function is called whenever the user clicks on the rendered value. It checks an overridable
* instance variable (editOnClickRenderedValue), to see whether it should then launch into edit mode.
*
* @instance
* @param {object} evt Dojo-normalised event
*/
onClickRenderedValue: function alfresco_renderers_InlineEditProperty__onClickRenderedValue(evt) {
this.editOnClickRenderedValue && this.onEditClick(evt);
},
/**
* This function is called whenever the user clicks on the edit icon. It hides the display DOM node
* and shows the edit DOM nodes.
*
* @instance
*/
onEditClick: function alfresco_renderers_InlineEditProperty__onEditClick(evt) {
if (!this._disableEdit)
{
this.suppressContainerKeyboardNavigation(true);
var formWidget = this.getFormWidget();
var o = {};
var formValue = this.originalRenderedValue;
if (formValue !== null &&
typeof formValue !== "undefined" &&
typeof formValue.toString === "function")
{
formValue = this.decodeHTML(formValue.toString());
}
else
{
formValue = "";
}
lang.setObject(this.postParam, formValue, o);
formWidget.setValue(o);
domClass.toggle(this.renderedValueNode, "hidden");
domClass.toggle(this.editNode, "hidden");
formWidget.focus(); // Focus on the input node so typing can occur straight away
evt && event.stop(evt);
}
},
/**
* @instance
*/
onSave: function alfresco_renderers_InlineEditProperty__onSave(formPayload) {
/*jshint unused:false*/
domClass.add(this.domNode, "alfresco-renderers-InlineEditProperty--updating");
var responseTopic = this.generateUuid();
var payload = lang.clone(this.getGeneratedPayload(false, null));
payload.alfResponseTopic = responseTopic;
this._saveSuccessHandle = this.alfSubscribe(responseTopic + "_SUCCESS", lang.hitch(this, this.onSaveSuccess), true);
this._saveFailureHandle = this.alfSubscribe(responseTopic + "_FAILURE", lang.hitch(this, this.onSaveFailure), true);
this.updateSaveData(payload);
this.alfPublish(this.publishTopic, payload, true);
},
/**
* Updates the supplied payload with the current form value.
*
* @instance
* @param {object} payload The save payload to update.
*/
updateSaveData: function alfresco_renderers_InlineEditProperty__getSaveData(payload) {
lang.mixin(payload, this.getFormWidget().getValue());
},
/**
* Called following successful save attempts. This will update the read-only display using the requested save
* data.
*
* @instance
* @param {object} payload The success payload
*/
onSaveSuccess: function alfresco_renderers_InlineEditProperty__onSaveSuccess(payload) {
/*jshint unused:false*/
this.alfUnsubscribeSaveHandles([this._saveSuccessHandle, this._saveFailureHandle]);
this.alfLog("log", "Property '" + this.propertyToRender + "' successfully updated for node: ", this.currentItem);
var formValue = this.getFormWidget().getValue()[this.postParam];
if (formValue !== null &&
typeof formValue !== "undefined" &&
typeof formValue.toString === "function")
{
this.originalRenderedValue = this.encodeHTML(formValue.toString());
}
else
{
this.originalRenderedValue = "";
}
this.renderedValue = this.mapValueToDisplayValue(formValue);
// If requested, update the currentItem with the updated value. This is done in the
// case where the currentItem might be subsequently used elsewhere (e.g. in a
// form, etc)
if (this.refreshCurrentItem === true)
{
this.updateCurrentItem(payload).then(lang.hitch(this, this.reRenderProperty));
}
else
{
this.reRenderProperty();
}
},
/**
* This function is called from [onSaveSuccess]{@link module:alfresco/renderers/InlineEditProperty#onSaveSuccess}
* to re-render the property after an edit has successfully been saved.
*
* @instance
* @param {object} payload The success payload
* @since 1.0.83
*/
reRenderProperty: function alfresco_renderers_InlineEditProperty__reRenderProperty() {
this.renderedValue = this.generateRendering(this.renderedValue);
this.renderedValueNode.textContent = this.renderedValue;
domClass.remove(this.renderedValueNode, "hidden");
domClass.add(this.editNode, "hidden");
this.updateCssClasses();
this.renderedValueNode.focus();
domClass.remove(this.domNode, "alfresco-renderers-InlineEditProperty--updating");
},
/**
* This function is called from [onSaveSuccess]{@link module:alfresco/renderers/InlineEditProperty#onSaveSuccess}
* when [refreshCurrentItem]{@link module:alfresco/renderers/InlineEditProperty#refreshCurrentItem} is true
* and allows the [currentItem]{@link module:alfresco/core/CoreWidgetProcessing#currentItem} to be updated
* with the latest data following the update.
*
* @instance
* @param {object} payload The success payload
* @returns {object} A promise of the udpate that by default is immediately resolved.
* @since 1.0.83
* @overridable
*/
updateCurrentItem: function alfresco_renderers_InlineEditProperty__updateCurrentItem(/*jshint unused:false*/ payload) {
var d = new Deferred();
lang.setObject(this.propertyToRender, this.originalRenderedValue, this.currentItem);
d.resolve();
return d;
},
/**
* Called following a failed save attempt. Cancels the edit mode.
* TODO: Issues an error message
*
* @instance
* @param {object} payload The success payload
*/
onSaveFailure: function alfresco_renderers_InlineEditProperty__onSaveFailure(payload) {
/*jshint unused:false*/
this.alfUnsubscribeSaveHandles([this._saveSuccessHandle, this._saveFailureHandle]);
this.alfLog("warn", "Property '" + this.propertyToRender + "' was not updated for node: ", this.currentItem);
this.onCancel();
},
/**
* Called when a user cancels out of edit mode. Returns the read-only display to its original state
* before editing began.
*
* @instance
*/
onCancel: function alfresco_renderers_InlineEditProperty__onCancel() {
this.suppressContainerKeyboardNavigation(false);
domClass.remove(this.renderedValueNode, "hidden");
domClass.add(this.editNode, "hidden");
// Reset the input field...
this.getFormWidget().setValue(this.renderedValue);
this.renderedValueNode.focus();
}
});
});