/**
* 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>This module can be used to easily preview content from an Alfresco Repository. Typically this will
* be a child widget of an [AlfDocument]{@link module:alfresco/documentlibrary/AlfDocument} that can
* be used to retrieve all of the data required to generate the preview. In order to retrieve the data
* it will also be necesssary to include a [DocumentService]{@link module:alfresco/services/DocumentService}
* on the page.</p>
* <p>By default there are a number of "plugins" that handle specific previews (e.g. HTML5 video and audio, images,
* PDF.js renderings, etc) and it is possible to either override, remove, add or re-configure plugins using the
* [widgetsForPluginsOverrides]{@link module:alfresco/preview/AlfDocumentPreview#widgetsForPluginsOverrides}
* attribute.</p>
* <p>The dimensions of the previewer can generally be controlled using the [heightMode]
* {@link module:alfresco/preview/AlfDocumentPreview#heightMode} attribute. However, it is entirely dependant upon
* each plugin as to whether they honour this setting. For example the [Audio plugin]{@link module:alfresco/preview/Audio}
* will ignore it as the dimensions are not important.</p>
*
* @module alfresco/preview/AlfDocumentPreview
* @extends external:dijit/_WidgetBase
* @mixes external:dojo/_TemplatedMixin
* @mixes module:alfresco/core/Core
* @author Dave Draper
*/
define(["dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/_TemplatedMixin",
"dojo/text!./templates/AlfDocumentPreview.html",
"alfresco/core/Core",
"service/constants/Default",
"dijit/registry",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/dom-class",
"dojo/on",
"dojo/sniff",
"jquery"],
function(declare, _Widget, _Templated, template, Core, AlfConstants, registry, lang, array, domClass, on, sniff, $) {
return declare([_Widget, _Templated, Core], {
/**
* An array of the i18n files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{i18nFile: "./i18n/AlfDocumentPreview.properties"}]
*/
i18nRequirements: [{i18nFile: "./i18n/AlfDocumentPreview.properties"}],
/**
* An array of the CSS files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{cssFile:"./css/AlfDocumentPreview.css"}]
*/
cssRequirements: [{cssFile:"./css/AlfDocumentPreview.css"}],
/**
* The HTML template to use for the widget.
* @instance
* @type {string}
*/
templateString: template,
/**
* <p>This function attempts to set an appropriate height for the previewer. There are 3 different modes
* of controlling the height:
* <ul><li>"AUTO" will attempt to ensure that the previewer takes up the complete vertical space in the
* client from where it starts. This makes sense to use if you have a single previewer on a page.</li>
* <li>Any positive number (note: not as a string) will set an explicit height that won't change as the
* browser resizes</li>
* <li>Any negative number will result in that amount being deducted from the browser window height and this
* will change as the browser is resized. This can be useful for setting the previewer in a dialog.</li></ul>
* </p>
*
* @instance
* @default
*/
heightMode: "AUTO",
/**
*
*
* @instance
* @type {object}
* @default
*/
plugin: null,
/**
*
*
* @instance
* @type {object[]}
* @default
*/
thumbnailModification: null,
/**
* Noderef to the content to display
*
* @instance
* @type {string}
* @default
*/
nodeRef: "",
/**
* The size of the content
*
* @instance
* @type {string}
* @default
*/
size: "0",
/**
* The file name representing root container
*
* @instance
* @type {string}
* @default
*/
name: "",
/**
* The mimeType of the node to display, needed to decide what plugin that should be used.
*
* @instance
* @type {string}
* @default
*/
mimeType: "",
/**
* A list of previews available for this node, needed to decide which plugin that should be used.
*
* @instance
* @type {object[]}
* @default
*/
thumbnails: null,
/**
* The base to the rest api call for the node's content or thumbnails, used when using an
* [apiVersion]{@link module:alfresco/preview/AlfDocumentPreview#apiVersion} of 0.
*
* @isntance
* @type {string}
* @default
*/
api: "api",
/**
* The Alfresco repository features multiple rendition REST APIs. Up to and including version 5.1 of Alfresco
* supports the "version zero" REST API (also known as Share Services API) for Renditions. In addition post 5.1
* there is a "version one" Public Rendition API also. Set this value to 1 to use the new version one API, else
* the classic v0 API will be used and Share Services AMP must be applied to the repository.
*
* @instance
* @type {number}
* @default
* @since 1.0.60
*/
apiVersion: 0,
/**
* The proxy to use for the rest api call for the node's content or thumbnails.
* I.e. "alfresco" (or "alfresco-noauth" for public content & pages). Can be set as null or empty
* value for URLs that do not require a specific proxy id endpoint.
*
* @instance
* @type {string}
* @default
*/
proxy: "alfresco",
/**
* MNT-9235: This flag identifies whether content of current node modified.
* That means that the cached thumbnail is no more valid and it should be updated
*
* @instance
* @type {boolean}
* @default
*/
avoidCachedThumbnail: false,
/**
*
* @instance
*/
postCreate: function alfresco_preview_AlfDocumentPreview__postCreate() {
// TODO: Set a common resize policy for plugins such that the plugin viewer can only
// ever take up the available space in the view port *UNLESS* a specific height
// has been defined. If a height has been defined then the width should respond
// to that height accordingly. Set max-height as the current viewport height.
// TODO: Currently nothing will be publishing on this topic...
this.alfSubscribe("ALF_DOCUMENT_PREVIEW_UPDATE", lang.hitch(this, "onPreviewChanged"));
this.alfSubscribe("ALF_METADATA_REFRESH", lang.hitch(this, "doRefresh"));
if (this.currentItem)
{
this.nodeRef = this.currentItem.node.nodeRef;
this.name = this.currentItem.node.properties["cm:name"] || this.currentItem.node.properties["cm:title"];
this.mimeType = this.currentItem.node.mimetype;
this.size = this.currentItem.node.size;
this.thumbnails = this.currentItem.thumbnailDefinitions;
this.thumbnailModification = this.currentItem.node.properties["cm:lastThumbnailModification"];
}
// Initialise empty arrays as required...
if (!this.thumbnails)
{
this.thumbnails = [];
}
if (!this.thumbnailModification)
{
this.thumbnailModification = [];
}
this.plugins = {};
this.setupPlugins();
this.setupPreview(false);
},
/**
* This is the default set of plugins for the previewer. These can be overridden in their entirety
* or changes to a subset can be made through configuration of the
* [widgetsForPluginsOverrides]{@link module:alfresco/preview/AlfDocumentPreview#widgetsForPluginsOverrides}
* attribute.
*
* @instance
* @type {object[]}
*/
widgetsForPlugins: [
{
id: "PdfJs",
name: "alfresco/preview/PdfJs/PdfJs"
},
{
id: "Image",
name: "alfresco/preview/Image"
},
{
id: "Video",
name: "alfresco/preview/Video"
},
{
id: "Audio",
name: "alfresco/preview/Audio"
}
],
/**
* This configuration attribute can be set to specifically override individual plugins
* rather than replacing the entire default set. Each element in the array should contain an
* "id" attribute for the plugin and a name that maps to a plugin module. If the element contains
* the attribute "remove" set to true then any existing plugin with a matching ID will be removed,
* if the element contains an attribute "replace" set to true then the entire configuration of the
* existing plugin will be replaced. Otherwise the new configuration will be merged into the existing.
*
* @instance
* @type {array}
* @default []
*/
widgetsForPluginsOverrides: [],
/**
* This function is used to add, update or remove individual plugins in the main list.
*
* @instance
*/
updatePluginConfiguration: function alfresco_preview_AlfDocumentPreview__updatePluginConfiguration(pluginConfig) {
// Check to see if the plugin already exists...
var existingPlugin = null,
existingPluginIndex = null;
array.forEach(this.widgetsForPlugins, function(currentPlugin, index) {
if (currentPlugin.id === pluginConfig.id)
{
existingPlugin = currentPlugin;
existingPluginIndex = index;
}
});
// Update, remove or add as appropriate
if (existingPlugin)
{
if (pluginConfig.remove === true)
{
// Remove the plugin
this.widgetsForPlugins.splice(existingPluginIndex, 1);
}
else
{
// Update the name (if provided)...
if (pluginConfig.name)
{
existingPlugin.name = pluginConfig.name;
}
if (pluginConfig.replace === true || !existingPlugin.config)
{
// Completely replace the plugin configuration...
existingPlugin.config = pluginConfig.config;
}
else
{
// Merge the plugin...
$.extend(true, existingPlugin.config, pluginConfig.config);
}
}
}
else
{
// A plugin with the supplied "id" does not already exist so we can just add it into the array
// for processing...
this.widgetsForPlugins.push(pluginConfig);
}
},
/**
* This function is used to update the [pluginConditions]{@link module:alfresco/preview/AlfDocumentPreview#pluginConditions}
* with additional condition data from an entry defined in
* [pluginConditionsOverrides]{@link module:alfresco/preview/AlfDocumentPreview#pluginConditionsOverrides}. This function
* is called from [setupPlugins]{@link module:alfresco/preview/AlfDocumentPreview#setupPlugins} for each entry
* in the [pluginConditionsOverrides]{@link module:alfresco/preview/AlfDocumentPreview#pluginConditionsOverrides} array.
*
* @instance
* @param {object} condition The plugin condition to update
* @since 1.0.56
*/
updatePluginConditions: function alfresco_preview_AlfDocumentPreview__updatePluginConditions(condition) {
// Check to see if the plugin already exists...
var existingCondition = null,
existingConditionIndex = null;
// Get the MIME type and/or thumbnail of the condition to be updated (or added/removed)...
var mimeType = lang.getObject("attributes.mimeType", false, condition);
var thumbnail = lang.getObject("attributes.thumbnail", false, condition);
// Check to see if the condition already exists...
array.some(this.pluginConditions, function(currentCondition, index) {
var currMimeType = lang.getObject("attributes.mimeType", false, currentCondition);
var currThumbnail = lang.getObject("attributes.thumbnail", false, currentCondition);
if ((currMimeType && currMimeType === mimeType) || (currThumbnail && currThumbnail === thumbnail))
{
existingCondition = currentCondition;
existingConditionIndex = index;
return true;
}
return false;
});
if (existingCondition)
{
if (condition.remove)
{
// Remmove the existing condition...
this.pluginConditions.splice(existingConditionIndex, 1);
}
else if (condition.plugins)
{
if (condition.replace || !existingCondition.plugins)
{
// Completely replace the plugins for the condition...
existingCondition.plugins = condition.plugins;
}
else
{
// Add the plugins...
existingCondition.plugins = existingCondition.plugins.concat(condition.plugins);
}
}
}
else
{
// The condition provided does not already exist so add it now...
this.pluginConditions.unshift(condition);
}
},
/**
*
* @instance
*/
setupPlugins: function alfresco_preview_AlfDocumentPreview__setupPlugins() {
this.plugins = {};
if (this.widgetsForPluginsOverrides)
{
array.forEach(this.widgetsForPluginsOverrides, lang.hitch(this, this.updatePluginConfiguration));
}
if (this.pluginConditionsOverrides)
{
array.forEach(this.pluginConditionsOverrides, lang.hitch(this, this.updatePluginConditions));
}
array.forEach(this.widgetsForPlugins, function(plugin) {
if (plugin.id && plugin.name)
{
this.alfLog("log", "Creating plugin: ", plugin.id);
try
{
var pluginModule = [plugin.name];
require(pluginModule, lang.hitch(this, "createPlugin", plugin.id, plugin.config));
}
catch (e)
{
this.alfLog("error", "An error occurred creating a preview plugin", e);
}
}
else
{
this.alfLog("warn", "A preview plugin was incorrectly defined - it was either missing an 'id' or 'name' attribute", plugin, this);
}
}, this);
},
/**
*
* @instance
*/
createPlugin: function alfresco_preview_AlfDocumentPreview__createPlugin(pluginName, pluginConfig, PluginType) {
// Defensively coded around the possibility that two document previewers might be included
// on the same page, make sure that the ID is unique. This ID is actually mostly required for
// automated testing purposes but it is important to ensure it's available...
var i = 0;
var tmpName = pluginName;
while (registry.byId(tmpName))
{
i++;
tmpName = pluginName + "_" + i;
}
var config = {
id: tmpName,
pubSubScope: this.pubSubScope,
parentPubSubScope: this.parentPubSubScope,
currentItem: this.currentItem,
currentMetadata: this.currentMetadata,
groupMemberships: this.groupMemberships,
dataScope: this.dataScope
};
// Merge in any additional configuration...
$.extend(true, config, pluginConfig);
config.previewManager = this;
// TODO: Merge configuration...
var plugin = new PluginType(config);
this.plugins[pluginName] = plugin;
if (!this.widgetsToDestroy)
{
this.widgetsToDestroy = [];
}
this.widgetsToDestroy.push(plugin);
this.alfLog("log", "Created plugin: ", pluginName, plugin);
return plugin;
},
/**
* Space for preview "plugins" to register themselves in.
* To provide a 3rd party plugin:
*
* 1. Create a javascript file and make it define a javascript class that defines a "plugin class" in this namespace.
* 2. Override this component's .get.head.ftl file and make sure your javascript file (and its resources) are included.
* 3. Override this component's .get.config.xml and define for which mimeTypes or thumbnails it shall be used.
* 4. To make sure your plugin works in the browser, define a report() method that
* returns nothing if the browser is supported and otherwise a string with a message saying the reason the
* plugin can't be used in the browser.
* 5. Define a display() method that will display the browser plugin or simply return a string of markup that shall be inserted.
*
* @instance
* @type {object}
* @default
*/
plugins: null,
/**
* Will find a previewer and set it up if one existed
*
* @instance
*/
setupPreview: function alfresco_preview_AlfDocumentPreview__setupPreview() {
// jshint maxcomplexity:false,maxstatements:false
// Display the preparing previewer message
this.messageNode.innerHTML = this.message("label.preparingPreviewer");
// Parameter nodeRef is mandatory
if (this.nodeRef === undefined)
{
throw new Error("A nodeRef must be provided");
}
if (this.size === "0")
{
// Shrink the web previewers real estate and tell user that node has no content
this.previewerNode.innerHTML = "<div class=\"message\">" + this.message("label.noContent") + "</div>";
}
else
{
var condition, pluginDescriptor, plugin, messages = [];
for (var i = 0, il = this.pluginConditions.length; i <il ; i++)
{
// Test that all conditions are valid
condition = this.pluginConditions[i];
if (!this.conditionsMatch(condition))
{
this.alfLog("log", "Plugin condition does not match", condition);
continue;
}
// Conditions are valid, now create plugins and make sure they can run in this environment
if (condition.plugins)
{
for (var pi = 0, pil = condition.plugins.length; pi < pil; pi++)
{
pluginDescriptor = condition.plugins[pi];
this.alfLog("log", "Checking plugin:", pluginDescriptor);
// Check the plugin constructor actually exists, in case client-side dependencies
// have not been loaded (ALF-12798)
if (this.plugins[pluginDescriptor.name])
{
// Get plugin
plugin = this.plugins[pluginDescriptor.name];
plugin.setAttributes(pluginDescriptor.attributes);
// Make sure it may run in this browser...
var report = plugin.report();
if (report)
{
// ...the plugin can't be used in this browser, save report and try another plugin
messages.push(report);
}
else
{
// ...yes, the plugin can be used in this browser, lets store a reference to it.
this.plugin = plugin;
// Ask the plugin to display the node
var markup;
try
{
domClass.add(this.previewerNode, pluginDescriptor.name);
domClass.add(this.domNode, "alfresco-preview-AlfDocumentPreview--displayed");
markup = plugin.display();
if (markup)
{
// Insert markup if plugin provided it
this.previewerNode.innerHTML = markup;
plugin._setPreviewerElementHeight();
plugin.onMarkupAdded();
}
// Finally! We found a plugin that works and didn't crash
// TODO: Do we need to fire anything here? What's listening for it?
//YAHOO.Bubbling.fire('webPreviewSetupComplete');
this.alfLog("log", "Found a suitable plugin: ", pluginDescriptor.name);
return;
}
catch(e)
{
// Oops a plugin failure, log it and try the next one instead...
this.alfLog("error","Error:" + pluginDescriptor.name + " failed to display: " + e);
messages.push(this.message("label.error", pluginDescriptor.name, e.message));
}
}
}
else
{
// Plugin could not be instantiated, log it and try the next one instead...
this.alfLog("error","Error, Alfresco.WebPreview.Plugins." + pluginDescriptor.name + " does not exist");
messages.push(this.message("label.errorMissing", pluginDescriptor.name));
}
}
}
}
// Tell user that the content can't be displayed
var noPreviewLabel = "label.noPreview";
if (sniff("ios"))
{
noPreviewLabel = "label.noPreview.ios";
}
var message = this.message(noPreviewLabel, this.getContentUrl(true));
for (i = 0, il = messages.length; i < il; i++)
{
message += "<br/>" + messages[i];
}
this.previewerNode.innerHTML = "<div class=\"message\">" + message + "</div>";
}
},
/**
* MNT-9235: Handles all the 'onPreviewChangedEvent' events
*
* @instance
* @param {object} payload
*/
onPreviewChanged: function alfresco_preview_AlfDocumentPreview__onPreviewChanged(/*jshint unused:false*/ payload) {
this.avoidCachedThumbnail = true;
// No event to stop, so commented out pending deletion...
// YAHOO.util.Event.preventDefault(event);
// YAHOO.util.Event.stopPropagation(event)
},
/**
* Checks if the conditions are fulfilled.
*
* @instance
* @param {object} condition The condition to match gainst this components options
* @return {boolean} true if conditions are fulfilled for plugins to be used.
*/
conditionsMatch: function alfresco_preview_AlfDocumentPreview__conditionsMatch(condition) {
if (!condition.attributes.mimeType && !condition.attributes.thumbnail)
{
// Only continue with the test if either a thumbnail or mimeType condition has been provided.
return false;
}
if (condition.attributes.mimeType && condition.attributes.mimeType !== this.mimeType)
{
return false;
}
if (condition.attributes.thumbnail && (array.indexOf(this.thumbnails, condition.attributes.thumbnail) === -1))
{
return false;
}
return true;
},
/**
* Helper method for plugins to create url tp the node's content.
*
* @instance
* @param {Bbolean} download(Optional) Default false. Set to true if the url shall be constructed so it forces the
* browser to download the document, rather than displaying it inside the browser.
* @return {string} The "main" element holding the actual previewer.
*/
getContentUrl: function alfresco_preview_AlfDocumentPreview__getContentUrl(download) {
var proxy = window.location.protocol + "//" + window.location.host + AlfConstants.URL_CONTEXT + "proxy/" + (this.proxy ? this.proxy + "/" : ""),
attach;
switch (this.apiVersion)
{
case 0:
{
var nodeRefAsLink = this.nodeRef.replace(":/", ""),
noCache = "noCache=" + new Date().getTime();
attach = download ? "a=true" : "a=false";
return proxy + this.api + "/node/" + nodeRefAsLink + "/content/" + encodeURIComponent(this.name) + "?c=force&" + noCache + "&" + attach;
}
case 1:
{
var nodeRefAsId = this.nodeRef.replace(/.*:\/\/.*\//, "");
attach = download ? "attachment=true" : "attachment=false";
return proxy + "public/alfresco/versions/1/nodes/" + nodeRefAsId + "/content/" + encodeURIComponent(this.name) + "?" + attach;
}
default:
this.alfLog("error", "Unknown Rendition API version specified: " + this.apiVersion);
}
},
/**
* Helper method for plugins to create a url to the thumbnail's content.
*
* @instance
* @param thumbnail {String} The thumbnail definition name
* @param fileSuffix {String} (Optional) I.e. ".png" if shall be inserted in the url to make certain flash
* plugins understand the mimetype of the thumbnail.
* @return {String} The url to the thumbnail content.
*/
getThumbnailUrl: function alfresco_preview_AlfDocumentPreview__getThumbnailUrl(thumbnail, fileSuffix) {
var proxy = window.location.protocol + "//" + window.location.host + AlfConstants.URL_CONTEXT + "proxy/" + (this.proxy ? this.proxy + "/" : ""),
noCache = "noCache=" + new Date().getTime();
// Check to see if last modification data is available for the thumbnail...
for (var i = 0; i < this.thumbnailModification.length; i++)
{
if (this.thumbnailModification[i].indexOf(thumbnail) !== -1)
{
var timestampPostfix = noCache;
noCache = "lastModified=" + encodeURIComponent(this.thumbnailModification[i]);
// MNT-9235: Avoiding loading content of thumbnail from the cache
// if current node is updated without reloading of the page
if (this.avoidCachedThumbnail)
{
noCache += "&" + timestampPostfix;
// Resetting to 'false' since thumbnail will be eventually updated...
this.avoidCachedThumbnail = false;
}
break;
}
}
switch (this.apiVersion)
{
case 0:
{
var nodeRefAsLink = this.nodeRef.replace(":/", ""),
force = "c=force";
return proxy + this.api + "/node/" + nodeRefAsLink + "/content/thumbnails/" + thumbnail + (fileSuffix ? "/suffix" + fileSuffix : "") + "?" + force + "&" + noCache;
}
case 1:
{
var nodeRefAsId = this.nodeRef.replace(/.*:\/\/.*\//, "");
return proxy + "public/alfresco/versions/1/nodes/" + nodeRefAsId + "/renditions/" + thumbnail + "/content?attachment=false" + "&" + noCache;
}
default:
this.alfLog("error", "Unknown Rendition API version specified: " + this.apiVersion);
}
},
/**
* Makes it possible for plugins to get hold of the "previewer wrapper" HTMLElement.
*
* I.e. Useful for elements that use an "absolute" layout for their plugins (most likely flash), so they have
* an element in the Dom to position their own elements after.
*
* @instance
* @return {element} The "main" element holding the actual previewer.
*/
getPreviewerElement: function alfresco_preview_AlfDocumentPreview__getPreviewerElement() {
return this.previewerNode;
},
/**
* Refreshes component by metadataRefresh event
*
* @instance
*/
doRefresh: function alfresco_preview_AlfDocumentPreview__doRefresh() {
if (this.plugin)
{
this.plugin.display();
}
},
/**
*
* @instance
* @type {object[]}
* @default
* @since 1.0.56
*/
pluginConditionsOverrides: null,
/**
* A json representation of the .get.config.xml file.
* This is evaluated on the client side since we need the plugins to make sure it is supported
* the user's browser and browser plugins.
*
* @instance
* @type {object[]}
*/
pluginConditions: [
{
attributes:{
mimeType: "application/pdf"
},
plugins: [
{
name: "PdfJs",
attributes: {
src: ""
}
}
]
},
{
attributes: {
thumbnail: "pdf"
},
plugins: [
{
name: "PdfJs",
attributes: {
src: "pdf",
progressiveLoading: false
}
}
]
},
{
attributes:{
mimeType: "video/mp4"
},
plugins: [
{
name: "Video",
attributes: {}
}
]
},
{
attributes: {
mimeType: "audio/mpeg"
},
plugins: [
{
name: "Audio",
attributes: {}
}
]
},
{
attributes:{
mimeType: "audio/x-wav"
},
plugins: [
{
name: "Audio",
attributes: {}
}
]
},
{
attributes: {
thumbnail: "imgpreview"
},
plugins: [
{
name: "Image",
attributes: {
src: "imgpreview"
}
}
]
},
{
attributes: {
mimeType: "image/jpeg"
},
plugins: [
{
name: "Image",
attributes: {
srcMaxSize: "2000000"
}
}
]
},
{
attributes: {
mimeType: "image/png"
},
plugins: [
{
name: "Image",
attributes: {
srcMaxSize: "2000000"
}
}
]
},
{
attributes: {
mimeType: "image/gif"
},
plugins: [
{
name: "Image",
attributes: {
srcMaxSize: "2000000"
}
}
]
}
]
});
});