diff --git a/modules/cms/classes/MediaLibrary.php b/modules/cms/classes/MediaLibrary.php index f5c380823..d034a94b9 100644 --- a/modules/cms/classes/MediaLibrary.php +++ b/modules/cms/classes/MediaLibrary.php @@ -211,6 +211,79 @@ class MediaLibrary return $this->getStorageDisk()->put($fullPath, $contents); } + /** + * Moves a file to another location. + * @param string $oldPath Specifies the original path of the file. + * @param string $newPath Specifies the new path of the file. + * @return boolean + */ + public function moveFile($oldPath, $newPath, $isRename = false) + { + $oldPath = self::validatePath($oldPath); + $fullOldPath = $this->getMediaPath($oldPath); + + $newPath = self::validatePath($newPath); + $fullNewPath = $this->getMediaPath($newPath); + + return $this->getStorageDisk()->move($fullOldPath, $fullNewPath); + } + + /** + * Copies a folder. + * @param string $originalPath Specifies the original path of the folder. + * @param string $newPath Specifies the new path of the folder. + * @return boolean + */ + public function copyFolder($originalPath, $newPath) + { + $disk = $this->getStorageDisk(); + + $copyDirectory = function($srcPath, $destPath) use (&$copyDirectory, $disk) { + $srcPath = self::validatePath($srcPath); + $fullSrcPath = $this->getMediaPath($srcPath); + + $destPath = self::validatePath($destPath); + $fullDestPath = $this->getMediaPath($destPath); + + if (!$disk->makeDirectory($fullDestPath)) + return false; + + $folderContents = $this->scanFolderContents($fullSrcPath); + + foreach ($folderContents['folders'] as $dirInfo) { + if (!$copyDirectory($dirInfo->path, $destPath.'/'.basename($dirInfo->path))) + return false; + } + + foreach ($folderContents['files'] as $fileInfo) { + $fullFileSrcPath = $this->getMediaPath($fileInfo->path); + + if (!$disk->copy($fullFileSrcPath, $fullDestPath.'/'.basename($fileInfo->path))) + return false; + } + + return true; + }; + + return $copyDirectory($originalPath, $newPath); + } + + /** + * Moves a folder. + * @param string $originalPath Specifies the original path of the folder. + * @param string $newPath Specifies the new path of the folder. + * @return boolean + */ + public function moveFolder($originalPath, $newPath) + { + if (!$this->copyFolder($originalPath, $newPath)) + return false; + + $this->deleteFolder($originalPath); + + return true; + } + /** * Resets the Library cache. * diff --git a/modules/cms/lang/en/lang.php b/modules/cms/lang/en/lang.php index c5aba71f7..f0003b24d 100644 --- a/modules/cms/lang/en/lang.php +++ b/modules/cms/lang/en/lang.php @@ -262,5 +262,6 @@ return [ 'no_files_found' => 'No files found by your request.', 'delete_empty' => 'Please select files to delete.', 'delete_confirm' => 'Do you really want to delete the selected file(s)?', + 'error_renaming_file' => 'Error renaming file.' ] ]; diff --git a/modules/cms/widgets/MediaManager.php b/modules/cms/widgets/MediaManager.php index 4f46cb7dc..a14a33458 100644 --- a/modules/cms/widgets/MediaManager.php +++ b/modules/cms/widgets/MediaManager.php @@ -211,7 +211,7 @@ class MediaManager extends WidgetBase if (count($filesToDelete) > 0) $library->deleteFiles($filesToDelete); - MediaLibrary::instance()->resetCache(); + $library->resetCache(); $this->prepareVars(); return [ @@ -219,6 +219,42 @@ class MediaManager extends WidgetBase ]; } + public function onLoadRenamePopup() + { + $path = Input::get('path'); + $path = MediaLibrary::validatePath($path); + + $this->vars['originalPath'] = $path; + $this->vars['name'] = basename($path); + $this->vars['listId'] = Input::get('listId'); + $this->vars['type'] = Input::get('type'); + return $this->makePartial('rename_form'); + } + + public function onApplyName() + { + $newName = trim(Input::get('name')); + 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'); + + if ($type == MediaLibraryItem::TYPE_FILE) + MediaLibrary::instance()->moveFile($originalPath, $newPath); + else + MediaLibrary::instance()->moveFolder($originalPath, $newPath); + + MediaLibrary::instance()->resetCache(); + } + // // Methods for th internal use // @@ -338,14 +374,18 @@ class MediaManager extends WidgetBase protected function splitPathToSegments($path) { $path = MediaLibrary::validatePath($path, true); + $path = explode('/', ltrim($path, '/')); - $path = ltrim($path, '/'); + $result = []; + while (count($path) > 0) { + $folder = array_pop($path); - $result = explode('/', $path); - if (count($result) == 1 && $result[0] == '') - $result = []; + $result[$folder] = implode('/', $path).'/'.$folder; + if (substr($result[$folder], 0, 1) != '/') + $result[$folder] = '/'.$result[$folder]; + } - return $result; + return array_reverse($result); } /** @@ -618,4 +658,15 @@ class MediaManager extends WidgetBase die(); } } + + protected function validateFileName($name) + { + if (!preg_match('/^[0-9a-z\.\s_\-]+$/i', $name)) + return false; + + if (strpos($name, '..') !== false) + return false; + + return true; + } } \ No newline at end of file diff --git a/modules/cms/widgets/mediamanager/assets/css/mediamanager.css b/modules/cms/widgets/mediamanager/assets/css/mediamanager.css index f76b66ec5..98ca84d06 100644 --- a/modules/cms/widgets/mediamanager/assets/css/mediamanager.css +++ b/modules/cms/widgets/mediamanager/assets/css/mediamanager.css @@ -60,9 +60,27 @@ div[data-control="media-manager"] .media-list li h4 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - padding-right: 15px; line-height: 150%; margin: 15px 0 5px 0; + padding-right: 0; + -webkit-transition: padding 0.1s; + transition: padding 0.1s; + position: relative; +} +div[data-control="media-manager"] .media-list li h4 a { + position: absolute; + right: 0; + top: 0; + font-size: 15px; + color: #2b3e50; + display: none; +} +div[data-control="media-manager"] .media-list li h4 a:hover { + color: #0181b9; + text-decoration: none; +} +div[data-control="media-manager"] .media-list li:hover h4 a { + display: block; } div[data-control="media-manager"] .media-list li p.size { font-size: 12px; @@ -147,6 +165,15 @@ div[data-control="media-manager"] .media-list.list li.selected h4 { div[data-control="media-manager"] .media-list.list li.selected .icon-container { border-right-color: #4da7e8 !important; } +div[data-control="media-manager"] .media-list.list h4 { + padding-right: 15px; +} +div[data-control="media-manager"] .media-list.list h4 a { + right: 15px; +} +div[data-control="media-manager"] .media-list.list li:hover h4 { + padding-right: 35px; +} div[data-control="media-manager"] .media-list.tiles li { width: 167px; margin-bottom: 25px; @@ -200,12 +227,18 @@ div[data-control="media-manager"] .media-list.tiles li.selected .icon-container div[data-control="media-manager"] .media-list.tiles li.selected h4 { color: #2581b8; } +div[data-control="media-manager"] .media-list.tiles li:hover h4 { + padding-right: 20px; +} div[data-control="media-manager"] .media-list.tiles i.icon-chain-broken { margin-top: 47px; } div[data-control="media-manager"] .media-list.tiles p.size { margin-bottom: 0; } +div[data-control="media-manager"] [data-control="sidebar-labels"] { + word-wrap: break-word; +} div[data-control="media-manager"] .sidebar-image-placeholder-container { display: table; width: 100%; @@ -272,6 +305,32 @@ div[data-control="media-manager"] [data-control="item-list"] { position: relative; display: table-cell; } +div[data-control="media-manager"] table.table { + table-layout: fixed; + white-space: nowrap; +} +div[data-control="media-manager"] table.table div.no-wrap-text { + overflow: hidden; + text-overflow: ellipsis; +} +div[data-control="media-manager"] table.table div.item-title { + position: relative; + padding-right: 0; + -webkit-transition: padding 0.1s; + transition: padding 0.1s; +} +div[data-control="media-manager"] table.table div.item-title a { + position: absolute; + right: 0; + top: 0; + display: none; +} +div[data-control="media-manager"] table.table tr:hover div.item-title { + padding-right: 25px; +} +div[data-control="media-manager"] table.table tr:hover div.item-title a { + display: block; +} div[data-control="media-manager"] div[data-control="selection-marker"] { position: absolute; z-index: 50; diff --git a/modules/cms/widgets/mediamanager/assets/js/mediamanager.js b/modules/cms/widgets/mediamanager/assets/js/mediamanager.js index a6c3022fb..aa96522e0 100644 --- a/modules/cms/widgets/mediamanager/assets/js/mediamanager.js +++ b/modules/cms/widgets/mediamanager/assets/js/mediamanager.js @@ -40,6 +40,7 @@ this.updateSearchResultsBound = this.updateSearchResults.bind(this) this.releaseNavigationAjaxBound = this.releaseNavigationAjax.bind(this) this.deleteConfirmationBound = this.deleteConfirmation.bind(this) + this.refreshBound = this.refresh.bind(this) // State properties this.selectTimer = null @@ -83,6 +84,7 @@ this.updateSearchResultsBound = null this.releaseNavigationAjaxBound = null this.deleteConfirmationBound = null + this.refreshBound = null this.sidebarPreviewElement = null this.itemListElement = null @@ -111,6 +113,7 @@ this.$el.on('click.item', '[data-type="media-item"]', this.itemClickHandler) this.$el.on('change', '[data-control="sorting"]', this.sortingChangedHandler) this.$el.on('keyup', '[data-control="search"]', this.searchChangedHandler) + this.$el.on('mediarefresh', this.refreshBound) if (this.itemListElement) this.itemListElement.addEventListener('mousedown', this.listMouseDownHandler) @@ -760,6 +763,7 @@ break; case 'set-filter': this.setFilter($(ev.currentTarget).data('filter')) + break; case 'delete': this.deleteFiles() break; @@ -769,7 +773,8 @@ } MediaManager.prototype.onItemClick = function(ev) { - if (ev.currentTarget.hasAttribute('data-root')) + // Don't select "Go up" folders and don't select items when the rename icon is clicked + if (ev.currentTarget.hasAttribute('data-root') || ev.target.tagName == 'I') return this.selectItem(ev.currentTarget, ev.shiftKey) diff --git a/modules/cms/widgets/mediamanager/assets/less/mediamanager.less b/modules/cms/widgets/mediamanager/assets/less/mediamanager.less index 8c1dc5a0e..1cc662dec 100644 --- a/modules/cms/widgets/mediamanager/assets/less/mediamanager.less +++ b/modules/cms/widgets/mediamanager/assets/less/mediamanager.less @@ -119,9 +119,30 @@ div[data-control="media-manager"] { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - padding-right: 15px; line-height: 150%; margin: 15px 0 5px 0; + padding-right: 0; + .transition(padding 0.1s); + + position: relative; + + a { + position: absolute; + right: 0; + top: 0; + font-size: 15px; + color: #2b3e50; + display: none; + + &:hover { + color: @color-link; + text-decoration: none; + } + } + } + + &:hover h4 a { + display: block; } p.size { @@ -205,6 +226,18 @@ div[data-control="media-manager"] { li.selected { .media-selected-list(); } + + h4 { + padding-right: 15px; + + a { + right: 15px; + } + } + + li:hover h4 { + padding-right: 35px; + } } &.tiles { @@ -247,6 +280,10 @@ div[data-control="media-manager"] { .media-selected-tiles(); } + li:hover h4 { + padding-right: 20px; + } + i.icon-chain-broken { margin-top: 47px; } @@ -257,6 +294,10 @@ div[data-control="media-manager"] { } } + [data-control="sidebar-labels"] { + word-wrap: break-word; + } + .sidebar-image-placeholder-container { display: table; width: 100%; @@ -318,6 +359,37 @@ div[data-control="media-manager"] { display: table-cell; } + table.table { + table-layout: fixed; + white-space: nowrap; + + div.no-wrap-text { + overflow: hidden; + text-overflow: ellipsis; + } + + div.item-title { + position: relative; + padding-right: 0; + .transition(padding 0.1s); + + a { + position: absolute; + right: 0; + top: 0; + display: none; + } + } + + tr:hover div.item-title{ + padding-right: 25px; + + a { + display: block; + } + } + } + div[data-control="selection-marker"] { position: absolute; z-index: 50; diff --git a/modules/cms/widgets/mediamanager/partials/_folder-path.htm b/modules/cms/widgets/mediamanager/partials/_folder-path.htm index 742cd2806..7a3485254 100644 --- a/modules/cms/widgets/mediamanager/partials/_folder-path.htm +++ b/modules/cms/widgets/mediamanager/partials/_folder-path.htm @@ -2,9 +2,9 @@
= e($item->sizeToString()) ?>
| = e(basename($item->path)) ?> | ++ + | = e($item->sizeToString()) ?> | = e($item->lastModifiedAsString()) ?> | -= e(dirname($item->path)) ?> | +
+ = e(dirname($item->path)) ?>
+ |
diff --git a/modules/cms/widgets/mediamanager/partials/_rename_form.htm b/modules/cms/widgets/mediamanager/partials/_rename_form.htm
new file mode 100644
index 000000000..a4d44635e
--- /dev/null
+++ b/modules/cms/widgets/mediamanager/partials/_rename_form.htm
@@ -0,0 +1,50 @@
+= Form::ajax($this->getEventHandler('onApplyName'), [
+ 'success' => "\$el.trigger('close.oc.popup'); \$('#".$listId."').trigger('mediarefresh');",
+ 'data-stripe-load-indicator' => 1,
+ 'id' => 'media-rename-popup-form'
+]) ?>
+