From e2a9c25b65bd27b414eb7bd3a441bf1a7f379394 Mon Sep 17 00:00:00 2001 From: alekseybobkov Date: Tue, 17 Mar 2015 22:20:04 -0700 Subject: [PATCH] Implemented search. --- modules/backend/assets/css/controls.css | 4 +- .../assets/less/controls/namevaluelist.less | 2 + modules/cms/classes/MediaLibrary.php | 64 +++++++- modules/cms/lang/en/lang.php | 3 + modules/cms/widgets/MediaManager.php | 70 ++++++-- .../mediamanager/assets/css/mediamanager.css | 14 ++ .../mediamanager/assets/js/mediamanager.js | 153 +++++++++++++----- .../assets/less/mediamanager.less | 17 ++ .../mediamanager/partials/_folder-path.htm | 16 +- .../mediamanager/partials/_generic-list.htm | 9 +- .../mediamanager/partials/_item-list.htm | 1 + .../mediamanager/partials/_list-grid.htm | 14 +- .../mediamanager/partials/_right-sidebar.htm | 5 + .../mediamanager/partials/_toolbar.htm | 8 +- 14 files changed, 305 insertions(+), 75 deletions(-) diff --git a/modules/backend/assets/css/controls.css b/modules/backend/assets/css/controls.css index 2a4eac444..d7583a17e 100644 --- a/modules/backend/assets/css/controls.css +++ b/modules/backend/assets/css/controls.css @@ -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} diff --git a/modules/backend/assets/less/controls/namevaluelist.less b/modules/backend/assets/less/controls/namevaluelist.less index cb67f58e0..afd6c1e6e 100644 --- a/modules/backend/assets/less/controls/namevaluelist.less +++ b/modules/backend/assets/less/controls/namevaluelist.less @@ -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; } } \ No newline at end of file diff --git a/modules/cms/classes/MediaLibrary.php b/modules/cms/classes/MediaLibrary.php index cc2f621b1..21dfb99ae 100644 --- a/modules/cms/classes/MediaLibrary.php +++ b/modules/cms/classes/MediaLibrary.php @@ -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; + } } \ No newline at end of file diff --git a/modules/cms/lang/en/lang.php b/modules/cms/lang/en/lang.php index 3fe002fe2..388beabd8 100644 --- a/modules/cms/lang/en/lang.php +++ b/modules/cms/lang/en/lang.php @@ -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.' ] ]; diff --git a/modules/cms/widgets/MediaManager.php b/modules/cms/widgets/MediaManager.php index 0d5c945c7..14d433f66 100644 --- a/modules/cms/widgets/MediaManager.php +++ b/modules/cms/widgets/MediaManager.php @@ -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 = [ diff --git a/modules/cms/widgets/mediamanager/assets/css/mediamanager.css b/modules/cms/widgets/mediamanager/assets/css/mediamanager.css index 848a831b7..f76b66ec5 100644 --- a/modules/cms/widgets/mediamanager/assets/css/mediamanager.css +++ b/modules/cms/widgets/mediamanager/assets/css/mediamanager.css @@ -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; diff --git a/modules/cms/widgets/mediamanager/assets/js/mediamanager.js b/modules/cms/widgets/mediamanager/assets/js/mediamanager.js index 1d80cf6af..f6b558c86 100644 --- a/modules/cms/widgets/mediamanager/assets/js/mediamanager.js +++ b/modules/cms/widgets/mediamanager/assets/js/mediamanager.js @@ -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 diff --git a/modules/cms/widgets/mediamanager/assets/less/mediamanager.less b/modules/cms/widgets/mediamanager/assets/less/mediamanager.less index 46a1ffe00..8c1dc5a0e 100644 --- a/modules/cms/widgets/mediamanager/assets/less/mediamanager.less +++ b/modules/cms/widgets/mediamanager/assets/less/mediamanager.less @@ -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"] { diff --git a/modules/cms/widgets/mediamanager/partials/_folder-path.htm b/modules/cms/widgets/mediamanager/partials/_folder-path.htm index d1fa2862c..742cd2806 100644 --- a/modules/cms/widgets/mediamanager/partials/_folder-path.htm +++ b/modules/cms/widgets/mediamanager/partials/_folder-path.htm @@ -1,9 +1,13 @@ \ No newline at end of file diff --git a/modules/cms/widgets/mediamanager/partials/_generic-list.htm b/modules/cms/widgets/mediamanager/partials/_generic-list.htm index abe639198..a1016dc1c 100644 --- a/modules/cms/widgets/mediamanager/partials/_generic-list.htm +++ b/modules/cms/widgets/mediamanager/partials/_generic-list.htm @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/modules/cms/widgets/mediamanager/partials/_item-list.htm b/modules/cms/widgets/mediamanager/partials/_item-list.htm index 7013d99ef..3c9b587ba 100644 --- a/modules/cms/widgets/mediamanager/partials/_item-list.htm +++ b/modules/cms/widgets/mediamanager/partials/_item-list.htm @@ -1,5 +1,6 @@
+
makePartial('list-grid') ?> diff --git a/modules/cms/widgets/mediamanager/partials/_list-grid.htm b/modules/cms/widgets/mediamanager/partials/_list-grid.htm index 97f14cbc4..caaa83b49 100644 --- a/modules/cms/widgets/mediamanager/partials/_list-grid.htm +++ b/modules/cms/widgets/mediamanager/partials/_list-grid.htm @@ -1,7 +1,7 @@ 0 || !$isRootFolder): ?> - + @@ -21,12 +21,22 @@ data-last-modified-ts="lastModified ?>" data-public-url="publicUrl) ?>" data-document-type="" + data-folder="path)) ?>" > + + + -
.. path)) ?> sizeToString()) ?> lastModifiedAsString()) ?>path)) ?>
\ No newline at end of file + + + +

+ +

+ diff --git a/modules/cms/widgets/mediamanager/partials/_right-sidebar.htm b/modules/cms/widgets/mediamanager/partials/_right-sidebar.htm index 52ded02db..8f495ef07 100644 --- a/modules/cms/widgets/mediamanager/partials/_right-sidebar.htm +++ b/modules/cms/widgets/mediamanager/partials/_right-sidebar.htm @@ -17,5 +17,10 @@ + + + + +
\ No newline at end of file diff --git a/modules/cms/widgets/mediamanager/partials/_toolbar.htm b/modules/cms/widgets/mediamanager/partials/_toolbar.htm index 026f5d3af..66584c100 100644 --- a/modules/cms/widgets/mediamanager/partials/_toolbar.htm +++ b/modules/cms/widgets/mediamanager/partials/_toolbar.htm @@ -16,12 +16,12 @@
-