Skip to content
8 changes: 8 additions & 0 deletions modules/backend/lang/en/lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@
'menu_label' => 'Media',
'upload' => 'Upload',
'move' => 'Move',
'duplicate' => 'Duplicate',
'delete' => 'Delete',
'add_folder' => 'Add folder',
'search' => 'Search',
Expand Down Expand Up @@ -631,10 +632,17 @@
'direction' => 'Direction',
'direction_asc' => 'Ascending',
'direction_desc' => 'Descending',
'file_exists_autorename' => 'The file already exists. Renamed to: :name',
'folder' => 'Folder',
'folder_exists_autorename' => 'The folder already exists. Renamed to: :name',
'no_files_found' => 'No files found by your request.',
'delete_empty' => 'Please select items to delete.',
'delete_confirm' => 'Delete the selected item(s)?',
'duplicate_empty' => 'Please select items to duplicate.',
'duplicate_multiple_confirm' => 'Multiple items selected. They will be duplicated with generated names. Are you sure?',
'duplicate_popup_title' => 'Duplicate file or folder',
'duplicate_new_name' => 'New name',
'duplicate_button' => 'Duplicate',
'error_renaming_file' => 'Error renaming the item.',
'new_folder_title' => 'New folder',
'folder_name' => 'Folder name',
Expand Down
8 changes: 8 additions & 0 deletions modules/backend/lang/fr/lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@
'menu_label' => 'Média',
'upload' => 'Déposer un fichier',
'move' => 'Déplacer',
'duplicate' => 'Dupliquer',
'delete' => 'Supprimer',
'add_folder' => 'Ajouter un répertoire',
'search' => 'Rechercher',
Expand Down Expand Up @@ -624,10 +625,17 @@
'direction' => 'Direction',
'direction_asc' => 'Ascendant',
'direction_desc' => 'Descendant',
'file_exists_autorename' => 'Le fichier existe déjà. Renommé en : :name',
'folder' => 'Répertoire',
'folder_exists_autorename' => 'Le dossier existe déjà. Renommé en : :name',
'no_files_found' => 'Aucun fichier trouvé.',
'delete_empty' => 'Veuillez sélectionner les éléments à supprimer.',
'delete_confirm' => 'Confirmer la suppression de ces éléments ?',
'duplicate_empty' => 'Veuillez sélectionner les éléments à dupliquer.',
'duplicate_multiple_confirm' => 'Plusieurs éléments sélectionnés. Ils seront clonés avec des noms générés. Êtes-vous sûr ?',
'duplicate_popup_title' => 'Dupliquer un fichier ou dossier',
'duplicate_new_name' => 'Nouveau nom',
'duplicate_button' => 'Dupliquer',
'error_renaming_file' => 'Erreur lors du renommage de l\'élément.',
'new_folder_title' => 'Nouveau répertoire',
'folder_name' => 'Nom du répertoire',
Expand Down
246 changes: 246 additions & 0 deletions modules/backend/widgets/MediaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use System\Classes\ImageResizer;
use System\Classes\MediaLibrary;
use System\Classes\MediaLibraryItem;
use Winter\Storm\Support\Facades\Flash;

/**
* Media Manager widget.
Expand Down Expand Up @@ -520,6 +521,224 @@ public function onCreateFolder(): array
];
}

/**
* Render the duplicate popup for the provided "path" from the request
*/
public function onLoadDuplicatePopup(): string
{
$this->abortIfReadOnly();

$path = Input::get('path');
$type = Input::get('type');
$path = MediaLibrary::validatePath($path);
$suggestedName = '';

$library = MediaLibrary::instance();

if ($type == MediaLibraryItem::TYPE_FILE) {
$suggestedName = $library->generateIncrementedFileName($path);
} else {
$suggestedName = $library->generateIncrementedFolderName($path);
}

$this->vars['originalPath'] = $path;
$this->vars['newName'] = $suggestedName;
$this->vars['type'] = $type;

return $this->makePartial('duplicate-form');
}

/**
* Duplicate the provided path from the request ("originalPath") to the new name ("name")
*
* @throws ApplicationException if the new name is invalid
*/
public function onDuplicateItem(): array
{
$this->abortIfReadOnly();

$newName = Input::get('newName');
if (!strlen($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty'));
}

if (!$this->validateFileName($newName)) {
throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name'));
}


$originalPath = Input::get('originalPath');
$originalPath = MediaLibrary::validatePath($originalPath);
$newPath = dirname($originalPath) . '/' . $newName;
$type = Input::get('type');

$newPath = $this->preventPathOverwrite($originalPath, $newPath, $type);

$library = MediaLibrary::instance();

if ($type == MediaLibraryItem::TYPE_FILE) {
/*
* Validate extension
*/
if (!$this->validateFileType($newName)) {
throw new ApplicationException(Lang::get('backend::lang.media.type_blocked'));
}

/*
* Duplicate single file
*/
$library->copyFile($originalPath, $newPath);

/**
* @event media.file.duplicate
* Called after a file is duplicated
*
* Example usage:
*
* Event::listen('media.file.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
* Or
*
* $mediaWidget->bindEvent('file.duplicate', function ((string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
*/
$this->fireSystemEvent('media.file.duplicate', [$originalPath, $newPath]);
} else {
/*
* Duplicate single folder
*/
$library->copyFolder($originalPath, $newPath);

/**
* @event media.folder.duplicate
* Called after a folder is duplicated
*
* Example usage:
*
* Event::listen('media.folder.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
* Or
*
* $mediaWidget->bindEvent('folder.duplicate', function ((string) $originalPath, (string) $newPath) {
* \Log::info($originalPath . " was duplicated to " . $path);
* });
*
*/
$this->fireSystemEvent('media.folder.duplicate', [$originalPath, $newPath]);
}

$library->resetCache();
$this->prepareVars();

return [
'#' . $this->getId('item-list') => $this->makePartial('item-list')
];
}

/**
* Duplicate the selected files or folders without prompting the user
* The new name will be generated in an incremented sequence
*
* @throws ApplicationException if the input data is invalid
*/
public function onDuplicateItems(): array
{
$this->abortIfReadOnly();

$paths = Input::get('paths');

if (!is_array($paths)) {
throw new ApplicationException('Invalid input data');
}

$library = MediaLibrary::instance();

$filesToDuplicate = [];
foreach ($paths as $pathInfo) {
$path = array_get($pathInfo, 'path');
$type = array_get($pathInfo, 'type');

if (!$path || !$type) {
throw new ApplicationException('Invalid input data');
}

if ($type === MediaLibraryItem::TYPE_FILE) {
/*
* Add to bulk collection
*/
$filesToDuplicate[] = $path;
} elseif ($type === MediaLibraryItem::TYPE_FOLDER) {
/*
* Duplicate single folder
*/
$library->duplicateFolder($path);

/**
* @event media.folder.duplicate
* Called after a folder is duplicated
*
* Example usage:
*
* Event::listen('media.folder.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path) {
* \Log::info($path . " was duplicated");
* });
*
* Or
*
* $mediaWidget->bindEvent('folder.duplicate', function ((string) $path) {
* \Log::info($path . " was duplicated");
* });
*
*/
$this->fireSystemEvent('media.folder.duplicate', [$path]);
}
}

if (count($filesToDuplicate) > 0) {
/*
* Duplicate collection of files
*/
$library->duplicateFiles($filesToDuplicate);

/*
* Extensibility
*/
foreach ($filesToDuplicate as $path) {
/**
* @event media.file.duplicate
* Called after a file is duplicated
*
* Example usage:
*
* Event::listen('media.file.duplicate', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) $path) {
* \Log::info($path . " was duplicated");
* });
*
* Or
*
* $mediaWidget->bindEvent('file.duplicate', function ((string) $path) {
* \Log::info($path . " was duplicated");
* });
*
*/
$this->fireSystemEvent('media.file.duplicate', [$path]);
}
}

$library->resetCache();
$this->prepareVars();

return [
'#' . $this->getId('item-list') => $this->makePartial('item-list')
];
}

/**
* Render the move popup with a list of folders to move the selected items to excluding the provided paths in the request ("exclude")
*
Expand Down Expand Up @@ -1376,4 +1595,31 @@ protected function getPreferenceKey()
// User preferences should persist across controller usages for the MediaManager
return "backend::widgets.media_manager." . strtolower($this->getId());
}

/**
* Check if file or folder already exists, then return an incremented name to prevent overwriting
*
* @param string $originalPath
* @param string $newPath
* @param string $type
*
* @todo Maybe the overwriting behavior can be config based
*/
protected function preventPathOverwrite($originalPath, $newPath, $type): string
{
$library = MediaLibrary::instance();

if ($library->exists($newPath)) {
if ($type == MediaLibraryItem::TYPE_FILE) {
$newName = $library->generateIncrementedFileName($originalPath);
} else {
$newName = $library->generateIncrementedFolderName($originalPath);
}
$newPath = dirname($originalPath) . '/' . $newName;

Flash::info(Lang::get('backend::lang.media.'. $type .'_exists_autorename', ['name' => $newName]));
}

return $newPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ this.$el.on('input','[data-control="search"]',this.proxy(this.onSearchChanged))
this.$el.on('mediarefresh',this.proxy(this.refresh))
this.$el.on('shown.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupShown))
this.$el.on('hidden.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupHidden))
this.$el.on('shown.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupShown))
this.$el.on('hidden.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupHidden))
this.$el.on('shown.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupShown))
this.$el.on('hidden.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupHidden))
this.$el.on('keydown',this.proxy(this.onKeyDown))
Expand All @@ -77,6 +79,8 @@ this.$el.off('change','[data-control="sorting"]',this.proxy(this.onSortingChange
this.$el.off('keyup','[data-control="search"]',this.proxy(this.onSearchChanged))
this.$el.off('shown.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupShown))
this.$el.off('hidden.oc.popup','[data-command="create-folder"]',this.proxy(this.onFolderPopupHidden))
this.$el.off('shown.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupShown))
this.$el.off('hidden.oc.popup','[data-command="duplicate"]',this.proxy(this.onDuplicatePopupHidden))
this.$el.off('shown.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupShown))
this.$el.off('hidden.oc.popup','[data-command="move"]',this.proxy(this.onMovePopupHidden))
this.$el.off('keydown',this.proxy(this.onKeyDown))
Expand All @@ -100,8 +104,9 @@ MediaManager.prototype.selectItem=function(node,expandSelection){if(!expandSelec
for(var i=0,len=items.length;i<len;i++){items[i].setAttribute('class','')}node.setAttribute('class','selected')}else{if(node.getAttribute('class')=='selected')node.setAttribute('class','')
else node.setAttribute('class','selected')}node.focus()
this.clearSelectTimer()
if(this.isPreviewSidebarVisible()){this.selectTimer=setTimeout(this.proxy(this.updateSidebarPreview),100)}if(node.hasAttribute('data-root')&&!expandSelection){this.toggleMoveAndDelete(true)}else{this.toggleMoveAndDelete(false)}if(expandSelection){this.unselectRoot()}}
MediaManager.prototype.toggleMoveAndDelete=function(value){$('[data-command=delete]',this.$el).prop('disabled',value)
if(this.isPreviewSidebarVisible()){this.selectTimer=setTimeout(this.proxy(this.updateSidebarPreview),100)}if(node.hasAttribute('data-root')&&!expandSelection){this.toggleMoveDuplicateDelete(true)}else{this.toggleMoveDuplicateDelete(false)}if(expandSelection){this.unselectRoot()}}
MediaManager.prototype.toggleMoveDuplicateDelete=function(value){$('[data-command=delete]',this.$el).prop('disabled',value)
$('[data-command=duplicate]',this.$el).prop('disabled',value)
$('[data-command=move]',this.$el).prop('disabled',value)}
MediaManager.prototype.unselectRoot=function(){var rootItem=this.$el.get(0).querySelector('[data-type="media-item"][data-root].selected')
if(rootItem)rootItem.setAttribute('class','')}
Expand Down Expand Up @@ -283,6 +288,24 @@ ev.preventDefault()
return false}
MediaManager.prototype.folderCreated=function(){this.$el.find('button[data-command="create-folder"]').popup('hide')
this.afterNavigate()}
MediaManager.prototype.duplicateItems=function(ev){var items=this.$el.get(0).querySelectorAll('[data-type="media-item"].selected')
if(!items.length){$.wn.alert(this.options.duplicateEmpty)
return}if(items.length>1){$.wn.confirm(this.options.duplicateMultipleConfirm,this.proxy(this.duplicateMultipleConfirmation))}else{var data={path:items[0].getAttribute('data-path'),type:items[0].getAttribute('data-item-type')}
$(ev.target).popup({handler:this.options.alias+'::onLoadDuplicatePopup',extraData:data,zIndex:1200})}}
MediaManager.prototype.duplicateMultipleConfirmation=function(confirmed){if(!confirmed)return
var items=this.$el.get(0).querySelectorAll('[data-type="media-item"].selected'),paths=[]
for(var i=0,len=items.length;i<len;i++){if(items[i].hasAttribute('data-root')){continue;}paths.push({'path':items[i].getAttribute('data-path'),'type':items[i].getAttribute('data-item-type')})}var data={paths:paths}
$.wn.stripeLoadIndicator.show()
this.$form.request(this.options.alias+'::onDuplicateItems',{data:data}).always(function(){$.wn.stripeLoadIndicator.hide()}).done(this.proxy(this.afterNavigate))}
MediaManager.prototype.onDuplicatePopupShown=function(ev,button,popup){$(popup).on('submit.media','form',this.proxy(this.onDuplicateItemSubmit))}
MediaManager.prototype.onDuplicateItemSubmit=function(ev){var item=this.$el.get(0).querySelector('[data-type="media-item"].selected'),data={newName:$(ev.target).find('input[name=newName]').val(),originalPath:$(ev.target).find('input[name=originalPath]').val(),type:$(ev.target).find('input[name=type]').val()}
$.wn.stripeLoadIndicator.show()
this.$form.request(this.options.alias+'::onDuplicateItem',{data:data}).always(function(){$.wn.stripeLoadIndicator.hide()}).done(this.proxy(this.itemDuplicated))
ev.preventDefault()
return false}
MediaManager.prototype.onDuplicatePopupHidden=function(ev,button,popup){$(popup).off('.media','form')}
MediaManager.prototype.itemDuplicated=function(){this.$el.find('button[data-command="duplicate"]').popup('hide')
this.afterNavigate()}
MediaManager.prototype.moveItems=function(ev){var items=this.$el.get(0).querySelectorAll('[data-type="media-item"].selected')
if(!items.length){$.wn.alert(this.options.moveEmpty)
return}var data={exclude:[],path:this.$el.find('[data-type="current-folder"]').val()}
Expand Down Expand Up @@ -310,6 +333,7 @@ break;case'close-uploader':this.hideUploadUi()
break;case'set-filter':this.setFilter($(ev.currentTarget).data('filter'))
break;case'delete':this.deleteItems()
break;case'create-folder':this.createFolder(ev)
break;case'duplicate':this.duplicateItems(ev)
break;case'move':this.moveItems(ev)
break;case'toggle-sidebar':this.toggleSidebar(ev)
break;case'popup-command':var popupCommand=$(ev.currentTarget).data('popup-command')
Expand Down Expand Up @@ -362,7 +386,7 @@ break;case'ArrowLeft':case'ArrowUp':this.selectRelative(false,ev.shiftKey)
eventHandled=true
break;}if(eventHandled){ev.preventDefault()
ev.stopPropagation()}}
MediaManager.DEFAULTS={url:window.location,uploadHandler:null,alias:'',deleteEmpty:'Please select files to delete.',deleteConfirm:'Delete the selected file(s)?',moveEmpty:'Please select files to move.',selectSingleImage:'Please select a single image.',selectionNotImage:'The selected item is not an image.',bottomToolbar:false,cropAndInsertButton:false}
MediaManager.DEFAULTS={url:window.location,uploadHandler:null,alias:'',duplicateEmpty:'Please select an item to duplicate.',duplicateMultipleConfirm:'Multiple items selected, they will be duplicated with generated names. Are you sure?',deleteEmpty:'Please select files to delete.',deleteConfirm:'Delete the selected file(s)?',moveEmpty:'Please select files to move.',selectSingleImage:'Please select a single image.',selectionNotImage:'The selected item is not an image.',bottomToolbar:false,cropAndInsertButton:false}
var old=$.fn.mediaManager
$.fn.mediaManager=function(option){var args=Array.prototype.slice.call(arguments,1),result=undefined
this.each(function(){var $this=$(this)
Expand Down Expand Up @@ -540,4 +564,4 @@ case'undo-resizing':this.undoResizing()
break}}
MediaManagerImageCropPopup.prototype.onSelectionChanged=function(c){this.updateSelectionSizeLabel(c.w,c.h)}
MediaManagerImageCropPopup.DEFAULTS={alias:undefined,onDone:undefined}
$.wn.mediaManager.imageCropPopup=MediaManagerImageCropPopup}(window.jQuery);
$.wn.mediaManager.imageCropPopup=MediaManagerImageCropPopup}(window.jQuery);
Loading