Take the community feedback survey now.
Take the community feedback survey now.
Hi!
It's possible to add a "Copy URL" to the context menu but it involves adding your own version of a built-in EPiServer component (epi-cms/component/MediaViewModel). Beware that this "hack" needs to be migrated if upgrading to EPiServer 8 because the MediaViewModel component looks slightly different. Below is some sample code for 7.5 (not tested though).
define("app/command/CopyMediaUrlToClipboard", [
// Dojo
"dojo/_base/declare",
// EPi Framework
"epi/shell/TypeDescriptorManager",
"epi/shell/command/_Command",
"epi/shell/command/_SelectionCommandMixin"
],
function (
// Dojo
declare,
// EPi Framework
TypeDescriptorManager,
_Command,
_SelectionCommandMixin
) {
return declare([_Command, _SelectionCommandMixin], {
label: "Copy URL",
tooltip: "Copy URL",
iconClass: "epi-iconCopy",
_copyTextToClipboard: function(text) {
var textArea = document.createElement("textarea");
textArea.style.position = 'fixed';
textArea.style.top = 0;
textArea.style.left = 0;
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = 0;
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
} catch (err) {
console && console.log("Unable to run document.execCommand('copy').");
}
document.body.removeChild(textArea);
},
_execute: function () {
var contentData = this._getContentData();
if (!contentData) {
return;
}
this._copyTextToClipboard(contentData.publicUrl);
},
_onModelChange: function () {
var contentData = this._getContentData(),
isPublicUrlAvailable = contentData && contentData.publicUrl,
typeIdentifier = contentData && contentData.typeIdentifier,
typeShouldActAsAsset = typeIdentifier && TypeDescriptorManager.getValue(typeIdentifier, "actAsAnAsset");
this.set("canExecute", !!(typeShouldActAsAsset && isPublicUrlAvailable));
},
_getContentData: function () {
// summary:
// Get current selected content data by selection
// tags:
// private
var selectionData = null;
if (this.selection && this.selection.data && this.selection.data instanceof Array && this.selection.data.length === 1) {
selectionData = this.selection.data[0].data;
}
return selectionData;
}
});
});
This is a copy of the built-in script from EPiServer with one modification: the _setupCommands method with the addition of the CopyUrlToClipboard command.
define("epi-cms/component/MediaViewModel", [
// dojo
"dojo/_base/array",
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/dom-class",
"dojo/when",
// epi
"epi",
"epi/shell/widget/dialog/Dialog",
"epi-cms/core/ContentReference",
"epi-cms/widget/ContextualContentForestStoreModel",
"epi-cms/widget/viewmodel/HierarchicalListViewModel",
"epi-cms/widget/viewmodel/MultipleFileUploadViewModel",
"epi-cms/widget/MultipleFileUpload",
"epi-cms/widget/UploadUtil",
"epi-cms/command/UploadContent",
"epi-cms/command/EditImage",
"epi-cms/command/DownloadMedia",
// our custom command
"app/command/CopyMediaUrlToClipboard",
// resource
"epi/i18n!epi/cms/nls/episerver.cms.components.media"
],
function (
// dojo
array,
declare,
lang,
domClass,
when,
// epi
epi,
Dialog,
ContentReference,
ContextualContentForestStoreModel,
HierarchicalListViewModel,
MultipleFileUploadViewModel,
MultipleFileUpload,
UploadUtil,
UploadContentCommand,
EditImageCommand,
DownloadCommand,
// our custom command
CopyMediaUrlToClipboardCommand,
// resources
resources
) {
return declare([HierarchicalListViewModel], {
// summary:
// Handles search and tree to list browsing widgets.
// tags:
// internal
// treeStoreModelClass: [const] Function
// Class to use as model for the tree.
treeStoreModelClass: ContextualContentForestStoreModel,
// Dialog widget for uploading new media
_dialog: null,
_getTypesToCreate: function () {
// No create commands for media since upload is used instead.
return [];
},
_setupCommands: function () {
// summary:
// Creates and registers the commands used.
// tags:
// protected
this.inherited(arguments);
var settings = {
selection: this.selection,
model: this
};
var customCommands = {
uploadDefault: {
command: new UploadContentCommand(lang.mixin({
iconClass: "epi-iconPlus",
label: resources.command.label,
resources: resources,
viewModel: this
}, settings))
},
upload: {
command: new UploadContentCommand(lang.mixin({
category: "context",
iconClass: "epi-iconUpload",
label: resources.linktocreateitem,
viewModel: this
}, settings)),
isAvailable: this.menuType.ROOT | this.menuType.TREE,
order: 2
},
editImage: {
command: new EditImageCommand(lang.mixin({
category: "context",
forceContextChange: true,
label: resources.command.openineditor
}, settings)),
isAvailable: this.menuType.LIST,
order: 3
},
download: {
command: new DownloadCommand(lang.mixin({
category: "context",
label: resources.command.download
}, settings)),
isAvailable: this.menuType.LIST,
order: 4
},
copyUrlToClipboard: {
command: new CopyMediaUrlToClipboardCommand(lang.mixin({
category: "context",
label: "Copy URL"
}, settings)),
isAvailable: this.menuType.LIST,
order: 5
}
};
this._commandRegistry = lang.mixin(this._commandRegistry, customCommands);
this.pseudoContextualCommands.push(this._commandRegistry.uploadDefault.command);
this.pseudoContextualCommands.push(this._commandRegistry.upload.command);
},
_updateTreeContextCommandModels: function (model) {
// summary:
// Update model of commands in case selected content is folder
// tags:
// private
this.inherited(arguments);
this._commandRegistry.uploadDefault.command.set("model", model);
this._commandRegistry.upload.command.set("model", model);
},
upload: function (/*Array*/fileList, /*String?*/targetId, /*Boolean?*/createAsLocalAsset) {
// summary:
// Upload multiple files.
// fileList: [Array]
// List files to upload.
// When null, only show upload form to select files for uploading.
// Otherwise, upload files in list.
// targetId: [String?]
// Parent content id
// createAsLocalAsset: [Boolean?]
// tags:
// protected
// only create diaglog if it is not available, otherwise, re-use it.
var uploader = new MultipleFileUpload({
model: new MultipleFileUploadViewModel({
store: this.get("store"),
query: this.get("listQuery")
})
});
uploader.on("beforeUploaderChange", lang.hitch(this, function () {
this._uploading = true;
}));
// close multiple files upload dialog when stop uploading
uploader.on("close", lang.hitch(this, function (uploading) {
this._dialog && (uploading ? this._dialog.hide() : this._dialog.destroy());
}));
// Reload current folder of tree, to reflect changes
uploader.on("uploadComplete", lang.hitch(this, function (/*Array*/uploadFiles) {
// Set current tree item again to reload items in list.
if (uploader.createAsLocalAsset) {
when(this.treeStoreModel && typeof this.treeStoreModel.refreshRoots === "function" && this.treeStoreModel.refreshRoots(this), lang.hitch(this, function () {
// Turn-off createAsLocalAsset
uploader.set("createAsLocalAsset", false);
// Update uploading directory after create a new real one local asset folder for the given content
uploader.set("uploadDirectory", this.get("currentTreeItem").id);
// Update content list query after create a new real one local asset folder for the given content
uploader.model.set("query", this.get("listQuery"));
}));
} else {
this.onListItemUpdated(uploadFiles);
this.set("currentTreeItem", this.get("currentTreeItem"));
}
if (this._dialog && !this._dialog.open) {
this._dialog.destroy();
}
this._uploading = false;
}));
this._dialog = new Dialog({
title: resources.linktocreateitem,
content: uploader,
autofocus: UploadUtil.supportNativeDndFiles(), // Only autofocus if not using flash.
defaultActionsVisible: false,
closeIconVisible: false
});
domClass.add(this._dialog.domNode, "epi-multiFileUploadDialog");
// only show close button for multiple files upload dialog
this._dialog.definitionConsumer.add({
name: "close",
label: epi.resources.action.close,
action: function () {
uploader.close();
}
});
this._dialog.resize({ w: 700 });
this._dialog.show();
var selectedContent = createAsLocalAsset ? this.selection.data[0].data : this.store.get(targetId);
when(selectedContent, lang.hitch(this, function (content) {
// Update breadcumb on upload dialog.
this._buildBreadcrumb(content, uploader);
// Set destination is current tree item.
uploader.set("uploadDirectory", targetId || this.get("currentTreeItem").id);
uploader.set("createAsLocalAsset", createAsLocalAsset);
uploader.upload(fileList);
}));
},
onListItemUpdated: function (updatedItems) {
// summary:
// Refresh the editing media if it have a new version
// updatedItems: [Array]
// Collection of the updated item. In this case, they are files.
// tags:
// public, extension
var store = this.store;
return when(this.getCurrentContext(), function (currentContext) {
var contentWithoutVersion = (new ContentReference(currentContext.id)).createVersionUnspecificReference().toString();
return when(store.get(contentWithoutVersion), function (currentContent) {
var editingMedia = array.filter(updatedItems, function (updatedItem) {
return currentContent.name.toLowerCase() === updatedItem.fileName.toLowerCase();
})[0];
return editingMedia ? currentContent : null;
});
});
},
_buildBreadcrumb: function (contentItem, uploader) {
// summary:
// Build breadcrumb for the provided content
// contentItem: Object
// The provided content
// uploader: Object
// The multiple file upload control
// tags:
// private
if (!uploader) {
return;
}
// Do not add more items when current content is sub root
if (this.treeStoreModel.isTypeOfRoot(contentItem)) {
uploader.set("breadcrumb", [contentItem]);
return;
}
this.treeStoreModel.getAncestors(contentItem.contentLink, lang.hitch(this, function (ancestors) {
var ancestor,
paths = [contentItem];
for (var i = ancestors.length - 1; i >= 0; i--) {
ancestor = ancestors[i];
paths.unshift(ancestor);
// Break after first sub root or context root
if (this.treeStoreModel.isTypeOfRoot(ancestor)) {
break;
}
}
uploader.set("breadcrumb", paths);
}));
},
getContextualParentLink: function ( /* Object */ contentItem, /* Object */ context) {
// summary:
// Retrieves the contextual parent link for given contentItem and context. If the found link is not in "DOM", then returns the last added item in the [roots].
// contentItem:
// The contentItem object.
// context:
// The currently loaded context.
// tags:
// protected
var link = this.inherited(arguments);
// sometimes we hide the immidiate parentLink (in case of media gadget) then we need to set the parent as last added item in roots.
var previousSelection = this.treeStoreModel.get("previousSelection");
if (previousSelection && previousSelection.selectedContent) {
// we need to query the Tree in order to find out if the item is hidden or not
var treeItem = previousSelection.selectedContent.tree.getNodesByItem(link)[0];
if (treeItem === undefined) {
link = this.roots[this.roots.length - 1];
}
}
return link;
}
});
});
You need to add a couple of things to your module.config (create it if it doesn't exist). For this sample it looks like this:
<?xml version="1.0" encoding="utf-8"?>
<module clientResourceRelativePath="" loadFromBin="false">
<dojo>
<paths>
<add name="app" path="ClientResources/Scripts" />
</paths>
</dojo>
<clientResources>
<!-- Inject custom MediaViewModel to replace the built-in -->
<add name="epi-cms.widgets.base" path="~/ClientResources/Scripts/component/MediaViewModel.js" resourceType="Script" />
</clientResources>
</module>
Awesome, thanks Mattias! Works perfectly. I had no idea that the dojo components even existed.
The only issue I have is a duplicate reference to ClientResources... my path to the Scripts folder is 'ClientResources/ClientResources/Scripts'. I'll try to track down where that is coming from.
Thanks!
I'm wondering if there's a user friendly way for editor level users to retrieve the live /globalassets/ url for a document in the Media library. We're currently on 7.5.
Currently, we are having the users drag the document into the WYSIWYG area, then selecting HTML view, and extracting the url from the href or src of the object. This is not an ideal workaround.
Ideally, I'd like to add a "Copy document URL" option to the Media Library contextual menu if there is no built-in easy solution... is this even possible?
Thanks!