Implemented search.

This commit is contained in:
alekseybobkov 2015-03-17 22:20:04 -07:00
parent 03c46012dc
commit e2a9c25b65
14 changed files with 305 additions and 75 deletions

View File

@ -1146,10 +1146,10 @@ ul.tree-path li.root a{font-weight:600;color:#405261}
ul.tree-path li a{color:#95a5a6}
ul.tree-path li a:hover{text-decoration:none}
table.name-value-list{border-collapse:collapse;font-size:13px}
table.name-value-list th,table.name-value-list td{padding:4px 0 4px 0}
table.name-value-list th,table.name-value-list td{padding:4px 0 4px 0;vertical-align:top}
table.name-value-list tr:first-child th,table.name-value-list tr:first-child td{padding-top:0}
table.name-value-list th{font-weight:600;color:#95a5a6;padding-right:15px;text-transform:uppercase}
table.name-value-list td{color:#2b3e50}
table.name-value-list td{color:#2b3e50;word-wrap:break-word}
div.progress{height:9px;-webkit-box-shadow:none;box-shadow:none;background:#d9dee0}
.progress-bar{line-height:9px;-webkit-box-shadow:none;box-shadow:none;background-color:#2f99da}
.progress-bar.progress-bar-success{background-color:#31ac5f}

View File

@ -4,6 +4,7 @@ table.name-value-list {
th, td {
padding: 4px 0 4px 0;
vertical-align: top;
}
tr:first-child {
@ -21,5 +22,6 @@ table.name-value-list {
td {
color: #2b3e50;
word-wrap: break-word;
}
}

View File

@ -5,6 +5,7 @@ use SystemException;
use Config;
use Storage;
use Cache;
use Str;
/**
* Provides abstraction level for the Media Library operations.
@ -68,7 +69,7 @@ class MediaLibrary
* Returns a list of folders and files in a Library folder.
* @param string $folder Specifies the folder path relative the the Library root.
* @param string $sortBy Determines the sorting preference.
* Supported values are 'title', 'size', 'lastModified' (see SORT_BY_XXX class constants).
* Supported values are 'title', 'size', 'lastModified' (see SORT_BY_XXX class constants) and FALSE.
* @param string $filter Determines the document type filtering preference.
* Supported values are 'image', 'video', 'audio', 'document' (see FILE_TYPE_XXX constants of MediaLibraryItem class).
* @return array Returns an array of MediaLibraryItem objects.
@ -101,8 +102,11 @@ class MediaLibrary
* Sort the result and combine the file and folder lists
*/
$this->sortItemList($folderContents['files'], $sortBy);
$this->sortItemList($folderContents['folders'], $sortBy);
if ($sortBy !== false) {
$this->sortItemList($folderContents['files'], $sortBy);
$this->sortItemList($folderContents['folders'], $sortBy);
}
$this->filterItemList($folderContents['files'], $filter);
$folderContents = array_merge($folderContents['folders'], $folderContents['files']);
@ -110,6 +114,38 @@ class MediaLibrary
return $folderContents;
}
/**
* Finds files in the Library.
* @param string $searchTerm Specifies the search term.
* @param string $sortBy Determines the sorting preference.
* Supported values are 'title', 'size', 'lastModified' (see SORT_BY_XXX class constants).
* @param string $filter Determines the document type filtering preference.
* Supported values are 'image', 'video', 'audio', 'document' (see FILE_TYPE_XXX constants of MediaLibraryItem class).
* @return array Returns an array of MediaLibraryItem objects.
*/
public function findFiles($searchTerm, $sortBy = 'title', $filter = null)
{
$words = explode(' ', Str::lower($searchTerm));
$result = [];
$findInFolder = function($folder) use (&$findInFolder, $words, &$result, $sortBy, $filter) {
$folderContents = $this->listFolderContents($folder, $sortBy, $filter);
foreach ($folderContents as $item) {
if ($item->type == MediaLibraryItem::TYPE_FOLDER)
$findInFolder($item->path);
else
if ($this->pathMatchesSearch($item->path, $words))
$result[] = $item;
}
};
$findInFolder('/');
$this->sortItemList($result, $sortBy);
return $result;
}
/**
* Determines if a file with the specified path exists in the library.
* @param string $path Specifies the file path relative the the Library root.
@ -365,4 +401,26 @@ class MediaLibrary
return $this->storageDisk = Storage::disk(
Config::get('cms.storage.media.disk', 'local'));
}
/**
* Determines if file path contains all words form the search term.
* @param string $path Specifies a path to examine.
* @param array $words A list of words to check against.
* @return boolean
*/
protected function pathMatchesSearch($path, $words)
{
$path = Str::lower($path);
foreach ($words as $word) {
$word = trim($word);
if (!strlen($word))
continue;
if (!Str::contains($path, $word))
return false;
}
return true;
}
}

View File

@ -255,5 +255,8 @@ return [
'uploading_file_num' => 'Uploading :number file(s)...',
'uploading_complete' => 'Upload complete',
'order_by' => 'Order by',
'search' => 'Search',
'folder' => 'Folder',
'no_files_found' => 'No files found by your request.'
]
];

View File

@ -59,6 +59,14 @@ class MediaManager extends WidgetBase
public function onSearch()
{
$this->setSearchTerm(Input::get('search'));
$this->prepareVars();
return [
'#'.$this->getId('item-list') => $this->makePartial('item-list'),
'#'.$this->getId('folder-path') => $this->makePartial('folder-path')
];
}
public function onGoToFolder()
@ -68,6 +76,9 @@ class MediaManager extends WidgetBase
if (Input::get('clearCache'))
MediaLibrary::instance()->resetCache();
if (Input::get('resetSearch'))
$this->setSearchTerm(null);
$this->setCurrentFolder($path);
$this->prepareVars();
@ -189,8 +200,14 @@ class MediaManager extends WidgetBase
$viewMode = $this->getViewMode();
$filter = $this->getFilter();
$sortBy = $this->getSortBy();
$searchTerm = $this->getSearchTerm();
$searchMode = strlen($searchTerm) > 0;
if (!$searchMode)
$this->vars['items'] = $this->listFolderItems($folder, $filter, $sortBy);
else
$this->vars['items'] = $this->findFiles($searchTerm, $filter, $sortBy);
$this->vars['items'] = $this->listFolderItems($folder, $filter, $sortBy);
$this->vars['currentFolder'] = $folder;
$this->vars['isRootFolder'] = $folder == self::FOLDER_ROOT;
$this->vars['pathSegments'] = $this->splitPathToSegments($folder);
@ -198,6 +215,8 @@ class MediaManager extends WidgetBase
$this->vars['thumbnailParams'] = $this->getThumbnailParams($viewMode);
$this->vars['currentFilter'] = $filter;
$this->vars['sortBy'] = $sortBy;
$this->vars['searchMode'] = $searchMode;
$this->vars['searchTerm'] = $searchTerm;
}
protected function listFolderItems($folder, $filter, $sortBy)
@ -207,6 +226,20 @@ class MediaManager extends WidgetBase
return MediaLibrary::instance()->listFolderContents($folder, $sortBy, $filter);
}
protected function findFiles($searchTerm, $filter, $sortBy)
{
$filter = $filter !== self::FILTER_EVERYTHING ? $filter : null;
return MediaLibrary::instance()->findFiles($searchTerm, $sortBy, $filter);
}
protected function setCurrentFolder($path)
{
$path = MediaLibrary::validatePath($path);
$this->putSession('media_folder', $path);
}
protected function getCurrentFolder()
{
$folder = $this->getSession('media_folder', self::FOLDER_ROOT);
@ -214,11 +247,6 @@ class MediaManager extends WidgetBase
return $folder;
}
protected function getFilter()
{
return $this->getSession('media_filter', self::FILTER_EVERYTHING);
}
protected function setFilter($filter)
{
if (!in_array($filter, [
@ -232,9 +260,19 @@ class MediaManager extends WidgetBase
return $this->putSession('media_filter', $filter);
}
protected function getSortBy()
protected function getFilter()
{
return $this->getSession('media_sort_by', MediaLibrary::SORT_BY_TITLE);
return $this->getSession('media_filter', self::FILTER_EVERYTHING);
}
protected function setSearchTerm($searchTerm)
{
$this->putSession('media_search', trim($searchTerm));
}
protected function getSearchTerm()
{
return $this->getSession('media_search', null);
}
protected function setSortBy($sortBy)
@ -248,11 +286,9 @@ class MediaManager extends WidgetBase
return $this->putSession('media_sort_by', $sortBy);
}
protected function setCurrentFolder($path)
protected function getSortBy()
{
$path = MediaLibrary::validatePath($path);
$this->putSession('media_folder', $path);
return $this->getSession('media_sort_by', MediaLibrary::SORT_BY_TITLE);
}
protected function itemTypeToIconClass($item, $itemType)
@ -292,11 +328,6 @@ class MediaManager extends WidgetBase
$this->addJs('js/mediamanager.js', 'core');
}
protected function getViewMode()
{
return $this->getSession('view_mode', self::VIEW_MODE_GRID);
}
protected function setViewMode($viewMode)
{
if (!in_array($viewMode, [self::VIEW_MODE_GRID, self::VIEW_MODE_LIST, self::VIEW_MODE_TILES]))
@ -305,6 +336,11 @@ class MediaManager extends WidgetBase
return $this->putSession('view_mode', $viewMode);
}
protected function getViewMode()
{
return $this->getSession('view_mode', self::VIEW_MODE_GRID);
}
protected function getThumbnailParams($viewMode = null)
{
$result = [

View File

@ -254,6 +254,20 @@ div[data-control="media-manager"] .list-container {
position: relative;
z-index: 100;
}
div[data-control="media-manager"] .list-container .no-data {
font-size: 13px;
}
div[data-control="media-manager"] .list-container p.no-data {
padding: 0 20px 20px 20px;
}
div[data-control="media-manager"] .list-container li.no-data {
padding-top: 20px;
display: block!important;
width: 100%!important;
border: none!important;
background: transparent!important;
cursor: default!important;
}
div[data-control="media-manager"] [data-control="item-list"] {
position: relative;
display: table-cell;

View File

@ -23,6 +23,7 @@
this.listMouseUpHandler = this.onListMouseUp.bind(this)
this.listMouseMoveHandler = this.onListMouseMove.bind(this)
this.sortingChangedHandler = this.onSortingChanged.bind(this)
this.searchChangedHandler = this.onSearchChanged.bind(this)
// Instance-bound methods
this.updateSidebarPreviewBound = this.updateSidebarPreview.bind(this)
@ -36,6 +37,8 @@
this.uploadQueueCompleteBound = this.uploadQueueComplete.bind(this)
this.uploadSendingBound = this.uploadSending.bind(this)
this.uploadErrorBound = this.uploadError.bind(this)
this.updateSearchResultsBound = this.updateSearchResults.bind(this)
this.releaseNavigationAjaxBound = this.releaseNavigationAjax.bind(this)
// State properties
this.selectTimer = null
@ -46,6 +49,8 @@
this.sidebarThumbnailAjax = null
this.selectionMarker = null
this.dropzone = null
this.searchTrackInputTimer = null
this.navigationAjax = null
//
// Initialization
@ -58,6 +63,8 @@
this.unregisterHandlers()
this.clearSelectTimer()
this.disableUploader()
this.clearSearchTrackInputTimer()
this.releaseNavigationAjax()
this.$el = null
this.$form = null
@ -72,12 +79,15 @@
this.uploadQueueCompleteBound = null
this.uploadSendingBound = null
this.uploadErrorBound = null
this.updateSearchResultsBound = null
this.releaseNavigationAjaxBound = null
this.sidebarPreviewElement = null
this.itemListElement = null
this.sidebarThumbnailAjax = null
this.selectionMarker = null
this.thumbnailQueue = []
this.navigationAjax = null
}
// MEDIA MANAGER INTERNAL METHODS
@ -94,10 +104,11 @@
MediaManager.prototype.registerHandlers = function() {
this.$el.on('dblclick', this.navigateHandler)
this.$el.on('click.tree-path', 'ul.tree-path', this.navigateHandler)
this.$el.on('click.tree-path', 'ul.tree-path, [data-control="sidebar-labels"]', this.navigateHandler)
this.$el.on('click.command', '[data-command]', this.commandClickHandler)
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)
if (this.itemListElement)
this.itemListElement.addEventListener('mousedown', this.listMouseDownHandler)
@ -109,6 +120,7 @@
this.$el.off('click.command', this.commandClickHandler)
this.$el.off('click.item', this.itemClickHandler)
this.$el.off('change', '[data-control="sorting"]', this.sortingChangedHandler)
this.$el.off('keyup', '[data-control="search"]', this.searchChangedHandler)
if (this.itemListElement) {
this.itemListElement.removeEventListener('mousedown', this.listMouseDownHandler)
@ -123,36 +135,29 @@
this.listMouseUpHandler = null
this.listMouseMoveHandler = null
this.sortingChangedHandler = null
this.searchChangedHandler = null
}
MediaManager.prototype.changeView = function(view) {
$.oc.stripeLoadIndicator.show()
var data = {
view: view,
path: this.$el.find('[data-type="current-folder"]').val()
}
this.$form.request(this.options.alias+'::onChangeView', {
data: data
}).always(function() {
$.oc.stripeLoadIndicator.hide()
}).done(this.afterNavigateBound)
this.execNavigationRequest('onChangeView', data)
}
MediaManager.prototype.setFilter = function(filter) {
$.oc.stripeLoadIndicator.show()
var data = {
filter: filter,
path: this.$el.find('[data-type="current-folder"]').val()
}
this.$form.request(this.options.alias+'::onSetFilter', {
data: data
}).always(function() {
$.oc.stripeLoadIndicator.hide()
}).done(this.afterNavigateBound)
this.execNavigationRequest('onSetFilter', data)
}
MediaManager.prototype.isSearchMode = function() {
return this.$el.find('[data-type="search-mode"]').val() == 'true'
}
//
@ -160,7 +165,7 @@
//
MediaManager.prototype.clearSelectTimer = function() {
if (this.selectTimer == null)
if (this.selectTimer === null)
return
clearTimeout(this.selectTimer)
@ -198,20 +203,13 @@
// Navigation
//
MediaManager.prototype.gotoFolder = function(path, clearCache) {
MediaManager.prototype.gotoFolder = function(path, resetSearch) {
var data = {
path: path
path: path,
resetSearch: resetSearch !== undefined ? 1 : 0
}
if (clearCache)
data.clearCache = true
$.oc.stripeLoadIndicator.show()
this.$form.request(this.options.alias+'::onGoToFolder', {
data: data
}).always(function() {
$.oc.stripeLoadIndicator.hide()
}).done(this.afterNavigateBound)
this.execNavigationRequest('onGoToFolder', data)
}
MediaManager.prototype.afterNavigate = function() {
@ -220,10 +218,39 @@
}
MediaManager.prototype.refresh = function() {
this.gotoFolder(
this.$el.find('[data-type="current-folder"]').val(),
true
)
var data = {
path: this.$el.find('[data-type="current-folder"]').val(),
clearCache: true
}
this.execNavigationRequest('onGoToFolder', data)
}
MediaManager.prototype.execNavigationRequest = function(handler, data, element)
{
if (element === undefined)
element = this.$form
if (this.navigationAjax !== null) {
try {
this.navigationAjax.abort()
}
catch (e) {}
this.releaseNavigationAjax()
}
$.oc.stripeLoadIndicator.show()
this.navigationAjax = element.request(this.options.alias+'::' + handler, {
data: data
}).always(function() {
$.oc.stripeLoadIndicator.hide()
})
.done(this.afterNavigateBound)
.always(this.releaseNavigationAjaxBound)
}
MediaManager.prototype.releaseNavigationAjax = function() {
this.navigationAjax = null
}
//
@ -297,6 +324,14 @@
previewPanel.querySelector('[data-label="title"]').textContent = item.getAttribute('data-title')
previewPanel.querySelector('[data-label="last-modified"]').textContent = item.getAttribute('data-last-modified')
previewPanel.querySelector('[data-label="public-url"]').setAttribute('href', item.getAttribute('data-public-url'))
if (this.isSearchMode()) {
previewPanel.querySelector('[data-control="item-folder"]').setAttribute('class', '')
var folderNode = previewPanel.querySelector('[data-label="folder"]')
folderNode.textContent = item.getAttribute('data-folder')
folderNode.setAttribute('data-path', item.getAttribute('data-folder'))
} else
previewPanel.querySelector('[data-control="item-folder"]').setAttribute('class', 'hide')
}
else {
// Multiple items are selected
@ -610,6 +645,31 @@
})
}
//
// Search
//
MediaManager.prototype.clearSearchTrackInputTimer = function() {
if (this.searchTrackInputTimer === null)
return
clearTimeout(this.searchTrackInputTimer)
this.searchTrackInputTimer = null
}
MediaManager.prototype.updateSearchResults = function() {
var $searchField = this.$el.find('[data-control="search"]'),
data = {
search: $searchField.val()
}
this.execNavigationRequest('onSearch', data, $searchField)
}
MediaManager.prototype.resetSearch = function() {
this.$el.find('[data-control="search"]').val('')
}
// EVENT HANDLERS
// ============================
@ -619,8 +679,14 @@
if (!$item.length || !$item.data('path').length)
return
if ($item.data('item-type') == 'folder')
this.gotoFolder($item.data('path'))
if ($item.data('item-type') == 'folder') {
if (!$item.data('clear-search'))
this.gotoFolder($item.data('path'))
else {
this.resetSearch()
this.gotoFolder($item.data('path'), true)
}
}
return false
}
@ -749,18 +815,25 @@
}
MediaManager.prototype.onSortingChanged = function(ev) {
$.oc.stripeLoadIndicator.show()
var data = {
sortBy: $(ev.target).val(),
path: this.$el.find('[data-type="current-folder"]').val()
}
this.$form.request(this.options.alias+'::onSetSorting', {
data: data
}).always(function() {
$.oc.stripeLoadIndicator.hide()
}).done(this.afterNavigateBound)
this.execNavigationRequest('onSetSorting', data)
}
MediaManager.prototype.onSearchChanged = function(ev) {
var value = ev.currentTarget.value
if (this.lastSearchValue !== undefined && this.lastSearchValue == value)
return
this.lastSearchValue = value
this.clearSearchTrackInputTimer()
this.searchTrackInputTimer = window.setTimeout(this.updateSearchResultsBound, 300)
}
// MEDIA MANAGER PLUGIN DEFINITION

View File

@ -294,6 +294,23 @@ div[data-control="media-manager"] {
.list-container {
position: relative;
z-index: 100;
.no-data {
font-size: 13px;
}
p.no-data {
padding: 0 20px 20px 20px;
}
li.no-data {
padding-top: 20px;
display: block!important;
width: 100%!important;
border: none!important;
background: transparent!important;
cursor: default!important;
}
}
[data-control="item-list"] {

View File

@ -1,9 +1,13 @@
<ul class="tree-path">
<li class="root"><a href="#" data-type="media-item" data-item-type="folder" data-path="/"><?= e(trans('cms::lang.media.library')) ?></a></li>
<li class="root"><a href="#" data-type="media-item" data-item-type="folder" data-path="/" data-clear-search="true"><?= e(trans('cms::lang.media.library')) ?></a></li>
<?php foreach ($pathSegments as $segment): ?>
<?php if ($segment != '/'): ?>
<li><a href="#" data-type="media-item" data-item-type="folder" data-path="<?= e($segment) ?>"><?= basename($segment) ?></a></li>
<?php endif ?>
<?php endforeach?>
<?php if (!$searchMode): ?>
<?php foreach ($pathSegments as $segment): ?>
<?php if ($segment != '/'): ?>
<li><a href="#" data-type="media-item" data-item-type="folder" data-path="<?= e($segment) ?>"><?= basename($segment) ?></a></li>
<?php endif ?>
<?php endforeach?>
<?php else: ?>
<li><a href="#" data-type="media-item"><?= e(trans('cms::lang.media.search')) ?></a></li>
<?php endif ?>
</ul>

View File

@ -1,6 +1,6 @@
<ul class="media-list <?= $listClass ?>">
<?php if (count($items) > 0 || !$isRootFolder): ?>
<?php if (!$isRootFolder): ?>
<?php if (!$isRootFolder && !$searchMode): ?>
<li data-type="media-item" data-item-type="folder" data-root data-path="<?= e(dirname($currentFolder)) ?>">
<div class="icon-container folder">
<div class="icon-wrapper"><i class="icon-folder"></i></div>
@ -23,6 +23,7 @@
data-last-modified-ts="<?= $item->lastModified ?>"
data-public-url="<?= e($item->publicUrl) ?>"
data-document-type="<?= e($itemType) ?>"
data-folder="<?= e(dirname($item->path)) ?>"
>
<?= $this->makePartial('item-icon', ['itemType'=>$itemType, 'item'=>$item]) ?>
@ -33,4 +34,10 @@
</li>
<?php endforeach ?>
<?php endif ?>
<?php if (count($items) == 0 && $searchMode): ?>
<li class="no-data">
<?= e(trans('cms::lang.media.no_files_found')) ?>
</li>
<?php endif ?>
</ul>

View File

@ -1,5 +1,6 @@
<div class="panel no-padding padding-top">
<input type="hidden" data-type="current-folder" value="<?= e($currentFolder) ?>"/>
<input type="hidden" data-type="search-mode" value="<?= $searchMode ? 'true' : 'false' ?>"/>
<div class="list-container">
<?php if ($viewMode == Cms\Widgets\MediaManager::VIEW_MODE_GRID): ?>
<?= $this->makePartial('list-grid') ?>

View File

@ -1,7 +1,7 @@
<table class="table data">
<tbody class="icons clickable">
<?php if (count($items) > 0 || !$isRootFolder): ?>
<?php if (!$isRootFolder): ?>
<?php if (!$isRootFolder && !$searchMode): ?>
<tr data-type="media-item" data-item-type="folder" data-root data-path="<?= e(dirname($currentFolder)) ?>">
<td><i class="icon-folder"></i>..</td>
<td></td>
@ -21,12 +21,22 @@
data-last-modified-ts="<?= $item->lastModified ?>"
data-public-url="<?= e($item->publicUrl) ?>"
data-document-type="<?= e($itemType) ?>"
data-folder="<?= e(dirname($item->path)) ?>"
>
<td><i class="<?= $this->itemTypeToIconClass($item, $itemType) ?>"></i> <?= e(basename($item->path)) ?></td>
<td><?= e($item->sizeToString()) ?></td>
<td><?= e($item->lastModifiedAsString()) ?></td>
<?php if ($searchMode): ?>
<td><?= e(dirname($item->path)) ?></td>
<?php endif ?>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>
</table>
<?php if (count($items) == 0 && $searchMode): ?>
<p class="no-data">
<?= e(trans('cms::lang.media.no_files_found')) ?>
</p>
<?php endif ?>

View File

@ -17,5 +17,10 @@
<th><?= e(trans('cms::lang.media.last_modified')) ?></th>
<td data-label="last-modified"></td>
</tr>
<tr data-control="item-folder" class="hide">
<th><?= e(trans('cms::lang.media.folder')) ?></th>
<td><a href="#" data-type="media-item" data-item-type="folder" data-label="folder" data-clear-search="true"></a></td>
</tr>
</table>
</div>

View File

@ -16,12 +16,12 @@
</div>
<div class="layout-cell width-fix">
<div class="relative toolbar-item loading-indicator-container size-input-text last">
<input placeholder="<?= e(trans('cms::lang.media.search')) ?>" type="text" name="search" value=""
class="form-control icon search" autocomplete="off"
data-track-input
<input placeholder="<?= e(trans('cms::lang.media.search')) ?>" type="text" name="search" value="<?= e($searchTerm) ?>"
class="form-control icon search"
data-control="search"
autocomplete="off"
data-load-indicator
data-load-indicator-opaque
data-request="<?= $this->getEventHandler('onSearch') ?>"
/>
</div>
</div>