/*
* Copyright 2014 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This tool is a tool for viewing or editing a content
* @private
*/
Ext.define('Ametys.plugins.cms.content.tool.ContentTool', {
extend: "Ametys.tool.Tool",
statics: {
/**
* @private
* @readonly
* @property {RegExp} __URL_REGEXP The regexp to match url
*/
__URL_REGEXP: new RegExp("^(https?:\/\/[^\/]+)(\/.*)?")
},
/**
* @property {String} _contentId The unique identifier of the content.
* @private
*/
/**
* @property {Object} _widgetInfo Informations transmitted to the widget
* @private
*/
/**
* @cfg {String} [mode="view"] The content view mode : 'view' for viewing content, 'edit' for editing content
*/
/**
* @property {String} _mode The view mode. See {@link #cfg-mode}
* @private
*/
/**
* @cfg {String} [view-name="main"] The name of view of content to use for edition.
*/
/**
* @property {String} _viewName The view for edition. See #cfg-view-name.
*/
/**
* @cfg {String} [fallback-view-name="main"] The fallback view for view or edition. Used (for legacy purpose) when #cfg-view-name is not found.
*/
/**
* @property {String} _fallbackViewName The fallback view for view or edition. See #cfg-fallback-view-name.
*/
/**
* @cfg {Number} [content-width=0] The width of content to fix width of rich text fields
*/
/**
* @property {String} _contentWidth The fixed width of rich text fields. See #cfg-content-width.
*/
/**
* @cfg {String} [jcr-workspace-name] The workspace name
*/
/**
* @property {String} _workspaceName The JCR workspace name. See #cfg-jcr-workspace-name.
*/
/**
* @cfg {Boolean/String} [ignore-workflow=false] Set to true or 'true' to ignore workflow
*/
/**
* @property {Boolean} _ignoreWorkflow True to ignore workflow. See #cfg-ignore-workflow.
*/
/**
* @cfg {Number} [edit-workflow-action=2] The workflow action to execute for editing content
*/
/**
* @property {Number} _workflowAction The workflow action for edition. See #cfg-workflow-action
*/
/**
* @cfg {String} [content-message-type="content"] The message type to send. Defaults to 'content'.
* A MessageTargetFactory handling the message type and accepting an 'ids' array parameter must exist.
*/
/**
* @property {String} _contentMessageType The message type to send. See #cfg-content-message-type
*/
/**
* @private
* @property {Ext.ux.IFrame} _iframe The iframe object
*/
/**
* @private
* @property {String} _formId The unique id for the form edition
*/
/**
* @private
* @property {Ext.Template} _tooltipDescriptionTpl The template used for tooltip description
*/
/**
* @private
* @property {Ametys.relation.dd.AmetysDropZone} _dropZone The drop zone (initializated on render)
*/
/**
* @private
* @property {Boolean} _paramsHasChanged If set params was called but not applied. Will be applied on next focus.
*/
/**
* @private
* @property {String} _baseUrl The url that the iframe should stay on
*/
/**
* @property {Number} _autoSaveFrequency When auto-save is enabled, represents the save frequency in minutes. `0` if disabled.
* @private
*/
_autoSaveFrequency: 0,
/**
* @private
* @property {Boolean} _openedAtStartup Was the tool opened during the UI startup
*/
/**
* @cfg {Boolean/String} [close-after-edition=false] Set to true or 'true' to close the tool when leaving the edition mode
*/
/**
* @property {Number} _closeAfterEdition true to close the tool when leaving the edition mode. See #cfg-close-after-edition
*/
constructor: function(config)
{
this._widgetInfo = {};
this.callParent(arguments);
this._tooltipDescriptionTpl = new Ext.Template(
"<u>{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_TOOLTIP_TEXT_LASTCONTRIBUTOR}}</u> : ",
"<b>{author}</b><br/>",
"<u>{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_TOOLTIP_TEXT_LASTMODIFIED}}</u> : ",
"<b>{lastModified}</b>"
);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onEdited, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onDeleted, this);
Ametys.message.MessageBus.on(Ametys.message.Message.ARCHIVED, this._onDeleted, this);
Ametys.message.MessageBus.on(Ametys.message.Message.UNARCHIVED, this._onDeleted, this);
Ametys.message.MessageBus.on('*', this._onAnyMessageBus, this);
},
createPanel: function ()
{
this.form = this._createFormEditionPanel();
this.view = this._createViewPanel();
return new Ext.Panel ({
layout: {
type: 'card',
deferredRender: false
},
cls: 'contenttool',
items: [
this.view,
this.form
]
});
},
/**
* @inheritdoc
* @param {Object} params The new parameter. Depends on the tool implementation.
* @param {String} [params.mode="view"] The content view mode : 'view' for viewing content, 'edit' for editing content
* @param {Boolean} [params.forceMode=false] True to bypass verification on mode when opening tool
* @param {String} [params.view-name="main"] The name of view of content to use for edition.
* @param {Boolean} [params.ignore-workflow=false] True to ignore workflow
* @param {Number} [params.edit-workflow-action=2] The workflow action to execute for editing content
* @param {String} [params.content-message-type="content"] The message type to send.
* @param {String} [params.jcr-workspace-name] The workspace name
* @param {String} [params.content-width] the width of content to fix the width of rich text fields in 'edit' mode.
*/
setParams: function (params)
{
this._openedAtStartup = !Ametys.tool.ToolsManager.isInitialized();
var requiredViewName = params['view-name'] || 'main';
if (!params.mode && this._mode == 'edit' && this._viewName == requiredViewName)
{
// No explicit mode is required, stay in edition mode without refreshing
return;
}
var requiredMode = params['mode'] || "view";
if (this._mode == requiredMode && this._viewName == requiredViewName)
{
// The tool is already opened in required mode
return;
}
// Clear the auto save timeout, if it exists.
this.clearAutosaveTimeout();
this.callParent(arguments);
this._paramsHasChanged = true;
// Was not already opened ?
if (!this._mode)
{
// First render will be done by onActivate... for the moment just get minimal informations to have a tab title and tooltip
this._setParams2();
this._updateInfos();
}
else
{
this._setParams();
}
},
/**
* @private
* This method effectively change params IF params was changed since last call.
*/
_setParams: function()
{
if (!this._paramsHasChanged)
{
return;
}
this._paramsHasChanged = false;
var params = this.getParams();
var requiredMode = params['mode'] || "view";
var requiredViewName = params['view-name'] || 'main';
if (!params.forceMode && this._mode != requiredMode && this._mode == 'edit')
{
// Quit the edition mode ?
if (this.isDirty())
{
var me = this;
Ametys.Msg.confirm("{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_LABEL}}",
"{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_CONFIRM}}",
function (answer) {
if (answer == 'yes')
{
me._setParamsCB();
me.setDirty(false);
this.sendCurrentSelection();
}
},
this
);
}
else
{
this._setParamsCB();
}
return;
}
if (!params.forceMode && this._mode == requiredMode && this._mode == 'edit' && this._viewName != requiredViewName)
{
if (this.isDirty())
{
var me = this;
Ametys.Msg.confirm("{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_LABEL}}",
"{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_CONFIRM}}",
function (answer) {
if (answer == 'yes')
{
me._destroyEditionComponents();
me._setParamsCB();
me.setDirty(false);
this.sendCurrentSelection();
}
},
this
);
}
else
{
this._destroyEditionComponents();
this._setParamsCB();
}
return;
}
this._setParamsCB();
},
/**
* Internal set parameters and refresh the tool
* @private
*/
_setParamsCB: function ()
{
this._setParams2();
Ametys.cms.content.ContentDAO.getContent(this._contentId, Ext.bind(this._getContentCb, this), this._workspaceName, false);
},
/**
* Internal set parameters
* @private
*/
_setParams2: function ()
{
var params = this.getParams();
this._contentId = params['contentId'] || params['id'];
this._widgetInfo.contentId = this._contentId;
this._mode = params['mode'] || 'view';
this._viewName = params['view-name'] || 'main';
this._fallbackViewName = params['fallback-view-name'] || 'main';
this._workflowAction = params['edit-workflow-action'] || 2;
this._ignoreWorkflow = Ext.isBoolean(params["ignore-workflow"]) ? params["ignore-workflow"] : params["ignore-workflow"] == "true";
this._workspaceName = params['jcr-workspace-name'];
this._contentMessageType = params['content-message-type'] || Ametys.message.MessageTarget.CONTENT;
this._contentWidth = params['content-width'] || 0;
this._closeAfterEdition = Ext.isBoolean(params["close-after-edition"]) ? params["close-after-edition"] : params["close-after-edition"] == "true";
},
/**
* @private
* Callback of #_setParams for the Ametys.cms.content.ContentDAO#getContent call
* @param {Ametys.cms.content.Content} content The content retrieved
*/
_getContentCb: function (content)
{
this._content = content;
this.refresh();
},
getMBSelectionInteraction: function()
{
return Ametys.tool.Tool.MB_TYPE_ACTIVE;
},
getType: function()
{
return Ametys.tool.Tool.TYPE_CONTENT;
},
/**
* Function called when leaving edition mode
*/
quitEditionMode: function ()
{
this.clearAutosaveTimeout();
if (this._closeAfterEdition)
{
this.close();
}
else
{
Ametys.tool.ToolsManager.openTool("uitool-content", {id: this._contentId, mode: 'view', forceMode: true});
if (this.isDirty())
{
this.setDirty(false);
}
this.sendCurrentSelection();
}
},
/**
* @private Get the panel used for view content
* @return {Ext.Panel} The panel used for view
*/
_createViewPanel: function ()
{
this._iframe = Ext.create("Ext.ux.IFrame", {});
this._iframe.on ('render', this._onIframeRender, this);
this._iframe.on ('load', this._onIframeLoad, this);
return this._iframe;
},
/**
* @private Get the panel used for edit content
* @return {Ext.Panel} The form panel
*/
_createFormEditionPanel: function ()
{
return Ext.create('Ametys.form.ConfigurableFormPanel', {
cls: 'content-form-inner',
listeners: {
'fieldchange': Ext.bind(this.setDirty, this, [true], false),
'inputfocus': Ext.bind(this.sendCurrentSelection, this),
'htmlnodeselected': Ext.bind(this.sendCurrentSelection, this),
'testresultschange': Ext.bind(this.sendCurrentSelection, this),
'formready': Ext.bind(this.onFormReady, this)
},
additionalWidgetsConf: {
contentInfo: this._widgetInfo
},
additionalWidgetsConfFromParams: {
contentType: 'contentType', // some widgets require the contentType configuration
editableSource: 'editableSource' // for richtext widgets we want to check the right on content
},
fieldNamePrefix: 'content.input.',
displayGroupsDescriptions: false
});
},
/**
* @private
* Focuses the form panel
*/
_focusForm: function()
{
this.form.focus();
},
refresh: function()
{
this.showRefreshing();
if (this.getMode() == 'view')
{
this._destroyEditionComponents();
this.getContentPanel().getLayout().setActiveItem(0);
var wrappedUrl = this.getWrappedContentUrl ();
this._iframe.load(Ametys.CONTEXT_PATH + wrappedUrl)
this._baseUrl = Ametys.CONTEXT_PATH + wrappedUrl;
this.showRefreshed();
}
else
{
this.getContentPanel().getLayout().setActiveItem(1);
this._content.lock(Ext.bind(this._drawEdition, this), false);
}
this._updateInfos();
},
/**
* @private
* Listener called when the iframe is rendered.
* Protects the iframe by handling links of the internal frame by setting a onclick on every one
* @param {Ext.ux.IFrame} iframe The iframe
*/
_onIframeRender: function (iframe)
{
this._dropZone = new Ametys.relation.dd.AmetysDropZone(iframe.getEl(), {setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)});
},
/**
* @private
* This event is thrown before the beforeDrop event and create the target of the drop operation relation.
* @param {Object} target The target records of the drop operation.
* @param {Object} item The default drag data that will be transmitted. You have to add a 'target' item in it:
* @param {Object} item.target The target (in the relation way) of the drop operation. A Ametys.relation.RelationPoint config.
*/
getDropInfo: function(target, item)
{
item.target = {
relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.REFERENCE, Ametys.relation.Relation.COPY],
targets: {
id: this._contentMessageType,
parameters: { ids: [this._contentId] }
}
};
},
/**
* @private
* Listener called when the iframe is loaded.
* Protects the iframe by handling links of the internal frame by setting a onclick on every one
* @param {Ext.ux.IFrame} iframe The iframe
*/
_onIframeLoad: function (iframe)
{
if (window.event && window.event.srcElement.readyState && !/loaded|complete/.test(window.event.srcElement.readyState))
{
return;
}
Ametys.plugins.cms.content.tool.ContentTool.__URL_REGEXP.test(window.location.href);
var win = this._iframe.getWin();
var outside = false;
try
{
win.location.href
}
catch (e)
{
outside = true;
}
if (outside || (win.location.href != 'about:blank' && win.location.href.indexOf(RegExp.$1 + this._baseUrl) != 0))
{
var outsideUrl = win.location.href;
// Back to iframe base url
win.location.href = this._baseUrl;
// Open ouside url in a new window
window.open(outsideUrl);
}
else
{
var links = win.document.getElementsByTagName("a");
for (var a = 0; a < links.length; a++)
{
if (links[a].getAttribute("internal") == null && !links[a].onclick)
{
links[a].onclick = Ext.bind(this._handleLink, this, [links[a]], false);
}
}
this._iframe.getWin().onfocus = Ext.bind(this._onIframeFocus, this);
}
},
/**
* @private
* Listener when iframe takes the focus.<br/>
* Force the focus on tool.
*/
_onIframeFocus: function ()
{
// Force focus on tool
this.focus();
},
/**
* @private
* Lazy handling of links. Each times a link is clicked, check if we should open it in a window, open another tool...
* @param {HTMLElement} link The clicked link
*/
_handleLink: function(link)
{
var currentURI = this._iframe.getWin().location.href;
var absHref = link.href;
var relHref = null;
if (/^(https?:\/\/[^\/]+)(\/.*)?/.test(absHref) && absHref.indexOf(RegExp.$1 + Ametys.CONTEXT_PATH) == 0)
{
relHref = absHref.substring(absHref.indexOf(RegExp.$1 + Ametys.CONTEXT_PATH) + (RegExp.$1 + Ametys.CONTEXT_PATH).length);
}
// Internal link
if (absHref.indexOf(currentURI) == 0) return true;
// JS Link
if (absHref.indexOf("javascript:") == 0) return true;
// Download link
if (relHref != null && relHref.indexOf("/plugins/explorer/download/") == 0) return true;
// Unknown link : open in a new window
window.open(absHref);
return false;
},
/**
* Update tool information
* @private
*/
_updateInfos: function()
{
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.repository.ContentDAO",
methodName: 'getContentDescription',
parameters: [ this.getContentId(), this._workspaceName],
waitMessage: false,
callback: {
handler: this._getContentDescriptionCb,
scope: this,
ignoreOnError: false
}
});
},
/**
* Callback function called after #_updateInfos is processed
* @param {Object} data The server response
* @param {String} data.title The title
* @param {Object} data.lastContributor The last contributor object
* @param {String} data.lastContributor.fullname The fullname of the last contributor
* @param {String} data.lastModified The last modified date at 'd/m/Y, H:i' format
* @param {String} data.iconGlyph A css class to set a glyph
* @param {String} data.iconDecorator A css class to set a glyph decorator
* @param {String} data.smallIcon The path to the small (16x16) icon. iconGlyph win if specified
* @param {String} data.mediumIcon The path to the medium (32x32) icon. iconGlyph win if specified
* @param {String} data.largeIcon The path to the large (48x48) icon. iconGlyph win if specified
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_getContentDescriptionCb: function (data, args)
{
if (!this.isNotDestroyed())
{
return;
}
if (!data)
{
// Alert user then close tool
var details = '';
var params = this.getParams();
for (var name in params)
{
details += name + " : " + params[name] + '\n';
}
if (!this._openedAtStartup)
{
Ametys.log.ErrorDialog.display({
title: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_TITLE}}",
text: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_CLOSE_MSG}}",
details: details,
category: this.self.getName()
});
}
else
{
Ametys.notify({
title: "{{i18n PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_TITLE}}",
description: "{{i18n PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_CLOSE_MSG}}",
type: "warn"
});
this.getLogger().error("Cannot open unexisting content");
}
this.close();
return;
}
this._openedAtStartup = false;
this.setTitle(data.title);
var values = {author: data.lastContributor.fullname, lastModified: Ext.util.Format.date(data.lastModified, 'd/m/Y, H:i')};
var description = this._tooltipDescriptionTpl.applyTemplate (values);
this.setDescription(description);
if (data.iconGlyph)
{
this.setGlyphIcon(data.iconGlyph);
this.setIconDecorator(data.iconDecorator);
}
else
{
this.setGlyphIcon(null);
this.setSmallIcon(data.smallIcon);
this.setMediumIcon(data.mediumIcon);
this.setLargeIcon(data.largeIcon);
}
// Register the accessed content in navigation history
this._saveToNavhistory();
},
/**
* Get the mode of the tool
* @returns {String} The mode of the tool. Cannot be null.
*/
getMode : function()
{
return this._mode;
},
/**
* Get the unique identifier of the content.
* @returns {String} The identifier of the content
*/
getContentId: function()
{
return this._contentId;
},
/**
* Get the name of the view for edition.
* @return {String} The name of the view
*/
getViewName: function()
{
return this._viewName;
},
/**
* Get the name of the fallback view for edition.
* @return {String} The name of the fallback view
*/
getFallbackViewName: function()
{
return this._fallbackViewName;
},
/**
* Get the wrapped url for content
* @returns {String} The wrapped url
*/
getWrappedContentUrl: function ()
{
var additionParams = '';
// FIXME CMS-3057
if (this._ignoreWorkflow)
{
additionParams += '&ignore-workflow=true';
}
additionParams += '&viewName=' + this._viewName;
additionParams += '&lang=' + Ametys.cms.language.LanguageDAO.getCurrentLanguage(); // default rendering language for multilingual content
// FIXME CMS-2316
var appParameters = Ametys.getAppParameters();
Ext.Object.each(appParameters, function(key, value) {
additionParams += '&' + key + '=' + encodeURIComponent(value);
});
return (this._workspaceName ? '/' + this._workspaceName : '') + '/_wrapped-content.html?contentId=' + this.getContentId() + additionParams;
},
/**
* Draw edition form
* @private
*/
_drawEdition: function (success)
{
if (!success)
{
Ametys.plugins.cms.content.actions.SaveAction.backToContent(this._contentId);
return;
}
// Reset automatic backup.
this._backupDate = null;
this._backupCreator = null;
this._backupData = null;
this._hasIndexingReferences = false;
Ametys.data.ServerComm.suspend();
// 1 Ask server for view definition
Ametys.data.ServerComm.callMethod({
role: 'org.ametys.cms.content.ContentHelper',
methodName: 'getContentViewAsJSON',
parameters: [this._contentId, this._viewName, this._fallbackViewName, true],
callback: {
scope: this,
handler: this._drawEditionForm,
ignoreOnError: false
},
waitMessage: false, // The tool is refreshing
cancelCode: this.getId() + "$1"
});
// 2 Ask server if an automatic backup exists for the content.
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
methodName: "getContentBackup",
parameters: [this._contentId],
callback: {
scope: this,
handler: this._handleAutoBackup
},
waitMessage: false, // The tool is refreshing
errorMessage: {
msg: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORMDEFINITION_ERROR}} '" + this._contentId + "'",
category: Ext.getClassName(this)
},
cancelCode: this.getId() + "$2"
});
// 3 Ask server for view values
this._getData(this._fillEditionForm);
// 4 Test if the content has "indexing references".
Ametys.data.ServerComm.callMethod({
role: 'org.ametys.cms.content.ContentHelper',
methodName: 'getContentEditionInformation',
parameters: [this._contentId],
callback: {
scope: this,
handler: this._getContentInformation
},
waitMessage: false, // The tool is refreshing
errorMessage: {
msg: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORMDEFINITION_ERROR}} '" + this._contentId + "'",
category: Ext.getClassName(this)
},
cancelCode: this.getId() + "$4"
});
Ametys.data.ServerComm.restart();
},
_extractDataRegExp: /(<[^>\/\s]*\s)[^>]*(typeId="content"\s)[^>\/]*>(.*?)(<\/[^>\/\s]*>)/gm,
_extractDataRegExp2: /"id":"([^"]*)"/gm,
/**
* @private
*/
_extractDataFromResponse(response)
{
let me = this;
if (me.isNotDestroyed() && me._editionFormReady && !Ametys.data.ServerComm.isBadResponse(response))
{
let rawData = Ext.dom.Query.selectNode("> content > metadata", response).innerHTML;
// remove linked content titles and other meta that are not part of the current content
rawData = rawData.replace(me._extractDataRegExp, function(match, g1, g2, g3, g4, index, fulltext) {
let ids = "";
g3.replace(me._extractDataRegExp2, function(match, id) {
ids += (ids ? ", ":"") + id;
});
return g1 + "\"id\"=\"" + ids + "\"/>";
});
return {
version: Ext.dom.Query.selectValue("> content > @version", response),
data: rawData
};
}
else
{
return null;
}
},
/**
* @private
* Download the content data
*/
_getData: function(callback)
{
Ametys.data.ServerComm.send({
url: '_content.xml',
parameters: {contentId: this._contentId, isEdition: "true", viewName: this._viewName, fallbackViewName: this._fallbackViewName},
priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
callback: {
handler: callback,
scope: this
},
cancelCode: this.getId() + "$3"
});
},
/**
* Callback function called after retrieving view from the content.
* This draws the form for content edition
* @param {Object} response The XML response provided by the {@link Ametys.data.ServerComm}
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_drawEditionForm: function (response, args)
{
this._destroyEditionComponents(); // Ensure form has no components
if (response === undefined)
{
Ametys.notify({
title: Ext.String.format("{{i18n PLUGINS_CMS_TOOL_CONTENT_FORM_VIEW_ERROR_TITLE}}", this._content.getTitle()),
description: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORM_VIEW_ERROR_MESSAGE}}",
type: "warn"
});
this.getLogger().error("Cannot modify content '" + this._contentId + "' because the view is not compatible with edition");
this.close();
}
else
{
if (this._contentWidth > 0)
{
this.form.setAdditionalWidgetsConf ('wysiwygWidth', this._contentWidth);
}
// Add the hidden field that store the content version to avoid mid-air collisions at save time
response.view.elements['_version'] = {
"widget": "edition.hidden",
"type": "string",
"multiple": false,
"label": "Version",
"description": "Version",
"help": null
};
this.form.configure(response.view.elements);
this._editionFormReady = true;
this.sendCurrentSelection();
}
},
/**
* Handle automatic backup: store if automatic save is enabled, the frequency, and retrieve the backup data if it exists.
* @param {Object} result The server response as JSON object
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_handleAutoBackup: function(result, args)
{
var freq = result.frequency;
if (this.isNotDestroyed() && result.enabled && freq > 0)
{
// Store the frequency.
this._autoSaveFrequency = freq;
// Test if an auto backup exists.
var autoBackup = result['auto-backup'];
if (autoBackup)
{
// There is an auto save: store all its data for later use.
this._backupDate = autoBackup.date;
this._backupCreator = autoBackup.creator;
this._backupData = this._getBackupData(autoBackup.data);
}
}
},
/**
* Extract backup data from the server response.
* @param {Object} data the XML data node.
* @return {Object} The backup data object.
* @private
*/
_getBackupData: function(data)
{
var backupData = {
values: {},
invalid: {},
repeaters: []
};
// Get the repeater data.
var repeaters = data.repeaters || [];
for (var i = 0; i < repeaters.length; i++)
{
var name = repeaters[i].name;
var prefix = repeaters[i].prefix;
var count = parseInt(repeaters[i].count);
backupData.repeaters.push({
name: name,
prefix: prefix,
count: count
});
}
// Get the valid attributes values.
var attributes = data.attributes || [];
for (var i = 0; i < attributes.length; i++)
{
var name = attributes[i].name;
var value = attributes[i].value;
if (attributes[i].json)
{
value = Ext.decode(value);
}
backupData.values[name] = value;
}
// Get the invalid attributes values.
var invalidAttributes = data['invalid-attributes'] || [];
for (var i = 0; i < invalidAttributes.length; i++)
{
var name = invalidAttributes[i].name;
var value = invalidAttributes[i].value;
backupData.invalid[name] = value;
}
return backupData;
},
/**
* Callback function called after retrieving metadata values from the
* content. This sets the the form field values
*
* @param {Object} response The XML response provided by the {@link Ametys.data.ServerComm}
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_fillEditionForm: function (response, args)
{
if (!this.isNotDestroyed())
{
return;
}
if (Ametys.data.ServerComm.handleBadResponse("{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORMDEFINITION_ERROR}} '" + this._contentId + "'", response, Ext.getClassName(this)))
{
this.showRefreshed();
return;
}
if (!this._editionFormReady)
{
if (this.getLogger().isWarnEnabled())
{
this.getLogger().warn("#_fillEditionForm has been called but the edition form is not drawn");
}
this.showRefreshed();
}
this._baseData = this._extractDataFromResponse(response);
// Test if there is an automatic backup for the content.
if (this._backupDate != null && this._backupData != null)
{
var lastModification = Ext.dom.Query.selectValue('> content > @lastModifiedAt', response);
var backupDate = Ext.Date.parse(this._backupDate, 'c');
var lastModifDate = Ext.Date.parse(lastModification, 'c');
if (backupDate > lastModifDate)
{
var contentTitle = Ext.DomQuery.selectValue('content/@title', response);
// The backup is more recent: ask the user if he wants to use this data instead of the regular content.
Ametys.Msg.confirm(
"{{i18n plugin.cms:CONTENT_EDITION_USE_BACKUP_LABEL}}",
"{{i18n plugin.cms:CONTENT_EDITION_USE_BACKUP_MESSAGE_1}}" + '"' + contentTitle + '"' + "{{i18n plugin.cms:CONTENT_EDITION_USE_BACKUP_MESSAGE_2}}",
function (answer) {
if (answer == 'yes')
{
// Initialize the form from the backup.
this.form.setValues(this._backupData);
this.form.getForm().clearInvalid();
this.showRefreshed();
}
else
{
// Delete the automatic save.
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
methodName: "deleteContentBackup",
parameters: [this._contentId],
waitMessage: false,
callback: {
scope: this,
handler: this._deleteAutoBackupCb
},
errorMessage: false
});
// Initialize the form from the regular content.
this.form.setValues(response, "metadata");
this.showRefreshed();
}
},
this
);
return;
}
}
this.form.setValues(response, "metadata");
this.showRefreshed();
this.form.on({
afterlayout: {fn: this._focusForm, scope: this, single: true}
});
},
/**
* Get additional edition information on the content.
* @param {Object} result The server response as JSON object
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_getContentInformation: function(result, args)
{
this._hasIndexingReferences = result.hasIndexingReferences;
},
/**
* Destroy the all form items
* @private
*/
_destroyEditionComponents: function()
{
this.form.destroyComponents();
},
/**
* Launch an automatic save of the current form.
* @private
*/
_autoSaveContent: function()
{
var me = this;
function removePrefix(rawValues)
{
var values = {};
Ext.Object.each(rawValues, function(key, value) {
if (Ext.String.startsWith(key, me.form.getFieldNamePrefix()))
{
key = key.substring(me.form.getFieldNamePrefix().length);
}
else if (Ext.String.startsWith(key, "_" + me.form.getFieldNamePrefix()))
{
key = "_" + key.substring(me.form.getFieldNamePrefix().length + 1);
}
values[key] = value;
});
return values;
}
if (this.getMode() == 'edit')
{
// Schedule the next automatic save.
this.armAutosaveTimeout();
// Get form values from the form and backup them.
var values = removePrefix(this.form.getJsonValues());
var invalidValues = removePrefix(this.form.getInvalidFieldValues());
var repeaters = [];
var fRepeaters = this.form.getRepeaters();
for (var i = 0; i < fRepeaters.length; i++)
{
repeaters.push({
name: fRepeaters[i].name,
prefix: fRepeaters[i].prefix.substring(this.form.getFieldNamePrefix().length),
count: fRepeaters[i].getItemCount().toString()
});
}
// Store the values
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
methodName: "setContentBackup",
parameters: [this._contentId, values, invalidValues, repeaters],
callback: {
scope: this,
handler: this._autoSaveContentCb
},
errorMessage: false
});
}
},
/**
* Callback invoked when the automatic save is finished.
* @param {Object} response The XML response provided by the {@link Ametys.data.ServerComm}
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_autoSaveContentCb: function(response, args)
{
// Ignore.
},
/**
* Test if the content currently being edited has indexing references.
*/
hasIndexingReferences: function()
{
return this._hasIndexingReferences;
},
/**
* Disarm the autosave function timer.
*/
clearAutosaveTimeout: function()
{
// Clear the auto save timeout, if it exists.
if (this._autoSaveTimeoutId != null)
{
window.clearTimeout(this._autoSaveTimeoutId);
this._autoSaveTimeoutId = null;
}
},
/**
* Arm the autosave function timer.
*/
armAutosaveTimeout: function()
{
// Launch the auto save timer.
if (this._autoSaveFrequency > 0)
{
var millis = this._autoSaveFrequency * 60 * 1000;
this._autoSaveTimeoutId = Ext.defer(this._autoSaveContent, millis, this);
}
},
/**
* @private
* When closing edition
*/
_closeEdit: function(manual)
{
// Delete the automatic save.
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.content.autosave.ContentBackupClientInteraction",
methodName: "deleteContentBackup",
parameters: [this._contentId],
callback: {
scope: this,
handler: this._deleteAutoBackupCb
},
errorMessage: false
});
// Unlock the content
Ametys.cms.content.ContentDAO.unlockContent(this._contentId, this.messageTarget);
Ametys.plugins.cms.content.tool.ContentTool.superclass.close.call(this, [manual]);
},
close: function (manual)
{
if (manual && this.getMode() == 'edit')
{
if (this.isDirty())
{
Ametys.Msg.confirm("{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_LABEL}}",
"{{i18n plugin.cms:CONTENT_EDITION_UNSAVE_CONFIRM}}",
function (answer) {
if (answer == 'yes')
{
this.setDirty(false);
this._closeEdit(manual);
}
},
this
);
}
else
{
this._closeEdit(manual);
}
return;
}
if (this._dropZone)
{
this._dropZone.destroy();
}
this.callParent (arguments);
},
/**
* Callback invoked when an automatic save is deleted.
* @param {Object} response The XML response provided by the {@link Ametys.data.ServerComm}
* @param {Object} params The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_deleteAutoBackupCb: function(response, params)
{
// Ignore the callback.
},
onClose: function ()
{
// Clear the auto-save timeout.
this.clearAutosaveTimeout();
this.callParent(arguments);
},
sendCurrentSelection: function()
{
if (this._contentMessageType == Ametys.message.MessageTarget.CONTENT)
{
// For traditional 'content' messages, the content object is stored in the _content variable.
if (this._content == null)
{
Ametys.cms.content.ContentDAO.getContent(this.getContentId(), Ext.bind(this._createContentTarget, this, [], 1), this._workspaceName, this._openedAtStartup ? false : undefined);
}
else
{
this._createContentTarget(this._content);
}
}
else
{
// 'Custom' content message.
this._createSimpleSelectionEvent();
}
},
/**
* Listener called when field receives the focus.
* Fires a event of selection on message bus.
* @param {Ext.form.Panel} form The form that is ready
*/
onFormReady: function(form)
{
this.armAutosaveTimeout();
},
/**
* Create and fire a selection event on the message bus.
* @private
*/
_createSimpleSelectionEvent: function()
{
Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: {
id: this._contentMessageType,
parameters: { ids: [this.getContentId()] }
}
});
},
/**
* Creates and fires a event of selection on the message bus
* @param {Ametys.cms.content.Content} content The concerned content
* @private
*/
_createContentTarget: function (content)
{
this._content = content;
if (content)
{
var msgParams = {};
// Targets need to be prepared for that SELECTION_CHANGING says the message is ready
function prep(targets)
{
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.SELECTION_CHANGED,
parameters: msgParams,
targets: targets
});
}
Ametys.message.MessageTargetFactory.createTargets({
id: this._contentMessageType,
parameters: { contents: [this._content] },
subtargets: this._mode == 'edit' ? this.form.getMessageTargetConf() : null
}, prep);
}
},
/**
* Listener on {@link Ametys.message.Message#MODIFIED} message. If the current content is concerned, the tool will be out-of-date.
* @param {Ametys.message.Message} message The edited message.
* @protected
*/
_onEdited: function (message)
{
var me = this;
var contentTargets = message.getTargets(function (target) {return me._contentMessageType == target.getId();});
for (var i=0; i < contentTargets.length; i++)
{
if (this._contentId == contentTargets[i].getParameters().id && this._mode != 'edit')
{
this.showOutOfDate();
}
}
},
/**
* Listener on {@link Ametys.message.Message#DELETED} message. If the current content is concerned, the tool will be closed.
* @param {Ametys.message.Message} message The deleted message.
* @protected
*/
_onDeleted: function (message)
{
var me = this;
var contentTargets = message.getTargets(function (target) {return me._contentMessageType == target.getId();});
for (var i=0; i < contentTargets.length; i++)
{
if (this._contentId == contentTargets[i].getParameters().id)
{
this.close();
}
}
},
onActivate: function()
{
this.callParent(arguments);
this._setParams();
},
/**
* Saves the accessed content in navigation history
* @private
*/
_saveToNavhistory: function ()
{
var i18nDesc = "{{i18n PLUGINS_CMS_TOOL_CONTENT_HISTORY_ENTRY_VIEW}}";
if (this._mode == 'edit')
{
i18nDesc = "{{i18n PLUGINS_CMS_TOOL_CONTENT_HISTORY_ENTRY_EDIT}}";
}
var toolId = this.getFactory().getId();
var currentToolParams = Ext.clone(this.getParams());
delete currentToolParams.mode; // Force mode to view
Ametys.navhistory.HistoryDAO.addEntry({
id: this.getId(),
objectId: this._contentId,
label: this.getTitle(),
description: Ext.String.format(i18nDesc, Ext.String.escapeHtml(this.getTitle())),
iconGlyph: this._mode == 'edit' ? 'ametysicon-edit5' : this.getGlyphIcon(),
iconSmall: this.getSmallIcon(),
iconMedium: this.getMediumIcon(),
iconLarge: this.getLargeIcon(),
type: Ametys.navhistory.HistoryDAO.CONTENT_TYPE,
action: Ext.bind(Ametys.tool.ToolsManager.openTool, Ametys.tool.ToolsManager, [toolId, currentToolParams], false)
});
},
/**
* Fire message before saving content
*/
fireMessageBeforeSaving: function ()
{
// Nothing
},
/**
* Fire messages on bus event when the content edition succeeded.
*/
fireMessagesOnSuccessEdition: function ()
{
// Nothing
},
/**
* Fire message on bus event when the content edition failed
*/
fireMessageOnFailEdition: function ()
{
// Nothing
},
/**
* Listener on any message. If the current content is concerned, the tool will update it's internal reference.
* @param {Ametys.message.Message} message The message.
* @protected
*/
_onAnyMessageBus: function(message)
{
var me = this;
var contentTargets = message.getTargets(function (target) {return me._contentMessageType == target.getId();});
for (var i=0; i < contentTargets.length; i++)
{
if (this._contentId == contentTargets[i].getParameters().id && contentTargets[i].getParameters().content)
{
//this.close();
this._content = contentTargets[i].getParameters().content;
}
}
}
});