Add category support for media
authorMatthias Schmidt <gravatronics@live.com>
Thu, 9 Mar 2017 18:04:46 +0000 (19:04 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Thu, 9 Mar 2017 18:04:46 +0000 (19:04 +0100)
Closes #2191

24 files changed:
com.woltlab.wcf/acpMenu.xml
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/templates/mediaEditor.tpl
com.woltlab.wcf/templates/mediaManager.tpl
wcfsetup/install/files/acp/templates/mediaEditor.tpl
wcfsetup/install/files/acp/templates/mediaList.tpl
wcfsetup/install/files/acp/templates/mediaManager.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Media/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Search.js
wcfsetup/install/files/lib/acp/form/MediaCategoryAddForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/MediaCategoryEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/MediaCategoryListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/MediaListPage.class.php
wcfsetup/install/files/lib/data/media/Media.class.php
wcfsetup/install/files/lib/data/media/MediaAction.class.php
wcfsetup/install/files/lib/data/media/MediaList.class.php
wcfsetup/install/files/lib/system/category/MediaCategoryType.class.php [new file with mode: 0644]
wcfsetup/install/files/style/layout/form.scss
wcfsetup/install/files/style/ui/media.scss
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 2bf0b7b8735d2f2f3235c075224429d0a7624bba..0e017691b699d9a18926e76159b76ebeeb139f92 100644 (file)
                                <permissions>admin.content.cms.canManageBox</permissions>
                                <icon>fa-plus</icon>
                        </acpmenuitem>
+                       <!-- /cms -->
                        
-                       <acpmenuitem name="wcf.acp.menu.link.cms.media.list">
+                       <!-- media -->
+                       <acpmenuitem name="wcf.acp.menu.link.media">
+                               <parent>wcf.acp.menu.link.content</parent>
+                               <showorder>2</showorder>
+                       </acpmenuitem>
+                       <acpmenuitem name="wcf.acp.menu.link.media.list">
                                <controller>wcf\acp\page\MediaListPage</controller>
-                               <parent>wcf.acp.menu.link.cms</parent>
+                               <parent>wcf.acp.menu.link.media</parent>
                                <permissions>admin.content.cms.canManageMedia</permissions>
                        </acpmenuitem>
-                       <!-- /cms -->
-               
+                       <acpmenuitem name="wcf.acp.menu.link.media.category.list">
+                               <controller>wcf\acp\page\MediaCategoryListPage</controller>
+                               <parent>wcf.acp.menu.link.media</parent>
+                               <permissions>admin.content.cms.canManageMedia</permissions>
+                       </acpmenuitem>
+                       <acpmenuitem name="wcf.acp.menu.link.media.category.add">
+                               <controller>wcf\acp\form\MediaCategoryAddForm</controller>
+                               <parent>wcf.acp.menu.link.media.category.list</parent>
+                               <permissions>admin.content.cms.canManageMedia</permissions>
+                               <icon>fa-plus</icon>
+                       </acpmenuitem>
+                       <!-- /media -->
+                       
                        <!-- article -->
                        <acpmenuitem name="wcf.acp.menu.link.article">
                                <parent>wcf.acp.menu.link.content</parent>
                                <options>module_article</options>
-                               <showorder>2</showorder>
+                               <showorder>3</showorder>
                        </acpmenuitem>
                        
                        <acpmenuitem name="wcf.acp.menu.link.article.list">
                        <!-- label -->
                        <acpmenuitem name="wcf.acp.menu.link.label">
                                <parent>wcf.acp.menu.link.content</parent>
-                               <showorder>3</showorder>
+                               <showorder>4</showorder>
                        </acpmenuitem>
                        <acpmenuitem name="wcf.acp.menu.link.label.list">
                                <controller>wcf\acp\page\LabelListPage</controller>
                        <!-- bbcode -->
                        <acpmenuitem name="wcf.acp.menu.link.bbcode">
                                <parent>wcf.acp.menu.link.content</parent>
-                               <showorder>4</showorder>
+                               <showorder>5</showorder>
                        </acpmenuitem>
                        <acpmenuitem name="wcf.acp.menu.link.bbcode.list">
                                <controller>wcf\acp\page\BBCodeListPage</controller>
                        <acpmenuitem name="wcf.acp.menu.link.tag">
                                <parent>wcf.acp.menu.link.content</parent>
                                <options>module_tagging</options>
-                               <showorder>5</showorder>
+                               <showorder>6</showorder>
                        </acpmenuitem>
                        
                        <acpmenuitem name="wcf.acp.menu.link.tag.list">
                        <!-- attachment -->
                        <acpmenuitem name="wcf.acp.menu.link.attachment">
                                <parent>wcf.acp.menu.link.content</parent>
-                               <showorder>6</showorder>
+                               <showorder>7</showorder>
                        </acpmenuitem>
                        
                        <acpmenuitem name="wcf.acp.menu.link.attachment.list">
index 861cebef4cb354b86b0e69c492bdd550e0742c9e..3994c310a3fd4341acfee518c4749f4590c59b3d 100644 (file)
                        <name>com.woltlab.wcf.box</name>
                        <definitionname>com.woltlab.wcf.acl.simple</definitionname>
                </type>
-               <type>
-                       <name>com.woltlab.wcf.media</name>
-                       <definitionname>com.woltlab.wcf.acl.simple</definitionname>
-               </type>
                <!-- /simple acl -->
                
                <!-- article list box condition -->
                </type>
                <!-- /recent activity box condition -->
                
+               <!-- media -->
+               <type>
+                       <name>com.woltlab.wcf.media</name>
+                       <definitionname>com.woltlab.wcf.acl.simple</definitionname>
+               </type>
+               <type>
+                       <name>com.woltlab.wcf.media.category</name>
+                       <definitionname>com.woltlab.wcf.category</definitionname>
+                       <classname>wcf\system\category\MediaCategoryType</classname>
+               </type>
+               <!-- /media -->
+               
                <!-- deprecated -->
                <type>
                        <name>com.woltlab.wcf.page.controller</name>
index 973da485c230b88101c934176d283815431c47d8..c192460011a8fffc03fe58dcc7d7c1c98e3ada0e 100644 (file)
 <section class="section">
        <h2 class="sectionTitle">{lang}wcf.global.form.data{/lang}</h2>
        
+       {hascontent}
+               <dl>
+                       <dt><label for="categoryID_{@$media->mediaID}">{lang}wcf.media.categoryID{/lang}</label></dt>
+                       <dd>
+                               <select id="categoryID_{@$media->mediaID}" name="categoryID">
+                                       <option value="0">{lang}wcf.global.noSelection{/lang}</option>
+                                       
+                                       {content}
+                                               {foreach from=$categoryList item=categoryItem}
+                                                       <option value="{$categoryItem->categoryID}">{$categoryItem->getTitle()}</option>
+                                                       
+                                                       {if $categoryItem->hasChildren()}
+                                                               {foreach from=$categoryItem item=subCategoryItem}
+                                                                       <option value="{$subCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;{$subCategoryItem->getTitle()}</option>
+                                                                       
+                                                                       {if $subCategoryItem->hasChildren()}
+                                                                               {foreach from=$subCategoryItem item=subSubCategoryItem}
+                                                                                       <option value="{$subSubCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{$subSubCategoryItem->getTitle()}</option>
+                                                                               {/foreach}
+                                                                       {/if}
+                                                               {/foreach}
+                                                       {/if}
+                                               {/foreach}
+                                       {/content}
+                               </select>
+                       </dd>
+               </dl>
+       {/hascontent}
+       
        {if $availableLanguages|count > 1}
                <dl>
                        <dt></dt>
index ea78c3cc760a9f303a6c6e502a39fe2e5845c284..4850e5571d460c97b91dd1d432b5ccfc7f881fce 100644 (file)
@@ -1,3 +1,29 @@
+{hascontent}
+       <div class="mediaManagerCategoryList">
+               <select name="categoryID" class="fullWidth">
+                       <option value="0">{lang}wcf.media.category.choose{/lang}</option>
+                       
+                       {content}
+                               {foreach from=$categoryList item=categoryItem}
+                                       <option value="{$categoryItem->categoryID}">{$categoryItem->getTitle()}</option>
+                                       
+                                       {if $categoryItem->hasChildren()}
+                                               {foreach from=$categoryItem item=subCategoryItem}
+                                                       <option value="{$subCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;{$subCategoryItem->getTitle()}</option>
+                                                       
+                                                       {if $subCategoryItem->hasChildren()}
+                                                               {foreach from=$subCategoryItem item=subSubCategoryItem}
+                                                                       <option value="{$subSubCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{$subSubCategoryItem->getTitle()}</option>
+                                                               {/foreach}
+                                                       {/if}
+                                               {/foreach}
+                                       {/if}
+                               {/foreach}
+                       {/content}
+               </select>
+       </div>
+{/hascontent}
+
 <div class="inputAddon mediaManagerSearch">
        <input type="text" class="mediaManagerSearchField" placeholder="{lang}wcf.media.search.placeholder{/lang}">
        <span class="inputSuffix">
index 42bf8376a02ee1b1aa764f9e6520fed07482fecd..6a783ae063d625509fcfe8e7e74e4ad7fd7000cd 100644 (file)
 
 <section class="section">
        <h2 class="sectionTitle">{lang}wcf.global.form.data{/lang}</h2>
-
+       
+       {hascontent}
+               <dl>
+                       <dt><label for="categoryID_{@$media->mediaID}">{lang}wcf.media.categoryID{/lang}</label></dt>
+                       <dd>
+                               <select id="categoryID_{@$media->mediaID}" name="categoryID">
+                                       <option value="0">{lang}wcf.global.noSelection{/lang}</option>
+                                       
+                                       {content}
+                                               {foreach from=$categoryList item=categoryItem}
+                                                       <option value="{$categoryItem->categoryID}">{$categoryItem->getTitle()}</option>
+                                                       
+                                                       {if $categoryItem->hasChildren()}
+                                                               {foreach from=$categoryItem item=subCategoryItem}
+                                                                       <option value="{$subCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;{$subCategoryItem->getTitle()}</option>
+                                                                       
+                                                                       {if $subCategoryItem->hasChildren()}
+                                                                               {foreach from=$subCategoryItem item=subSubCategoryItem}
+                                                                                       <option value="{$subSubCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{$subSubCategoryItem->getTitle()}</option>
+                                                                               {/foreach}
+                                                                       {/if}
+                                                               {/foreach}
+                                                       {/if}
+                                               {/foreach}
+                                       {/content}
+                               </select>
+                       </dd>
+               </dl>
+       {/hascontent}
+       
        {if $availableLanguages|count > 1}
                <dl>
                        <dt></dt>
index 381d8f8480663820ec66deecb0cda8c391c7db83..c7f296fb6f40815f92af18dc6b1f7a7d12da6e01 100644 (file)
                <h2 class="sectionTitle">{lang}wcf.global.filter{/lang}</h2>
                
                <div class="row rowColGap formGrid">
+                       {hascontent}
+                               <dl class="col-xs-12 col-md-4">
+                                       <dt></dt>
+                                       <dd>
+                                               <select id="categoryID" name="categoryID">
+                                                       <option value="0">{lang}wcf.media.category.choose{/lang}</option>
+                                                       
+                                                       {content}
+                                                               {foreach from=$categoryList item=categoryItem}
+                                                                       <option value="{$categoryItem->categoryID}"{if $categoryItem->categoryID == $categoryID} selected="selected"{/if}>{$categoryItem->getTitle()}</option>
+                                                                       
+                                                                       {if $categoryItem->hasChildren()}
+                                                                               {foreach from=$categoryItem item=subCategoryItem}
+                                                                                       <option value="{$subCategoryItem->categoryID}"{if $subCategoryItem->categoryID == $categoryID} selected="selected"{/if}>&nbsp;&nbsp;&nbsp;&nbsp;{$subCategoryItem->getTitle()}</option>
+                                                                                       
+                                                                                       {if $subCategoryItem->hasChildren()}
+                                                                                               {foreach from=$subCategoryItem item=subSubCategoryItem}
+                                                                                                       <option value="{$subSubCategoryItem->categoryID}"{if $subSubCategoryItem->categoryID == $categoryID} selected="selected"{/if}>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{$subSubCategoryItem->getTitle()}</option>
+                                                                                               {/foreach}
+                                                                                       {/if}
+                                                                               {/foreach}
+                                                                       {/if}
+                                                               {/foreach}
+                                                       {/content}
+                                               </select>
+                                       </dd>
+                               </dl>
+                       {/hascontent}
+                       
                        <dl class="col-xs-12 col-md-4">
                                <dt></dt>
                                <dd>
index ea78c3cc760a9f303a6c6e502a39fe2e5845c284..4850e5571d460c97b91dd1d432b5ccfc7f881fce 100644 (file)
@@ -1,3 +1,29 @@
+{hascontent}
+       <div class="mediaManagerCategoryList">
+               <select name="categoryID" class="fullWidth">
+                       <option value="0">{lang}wcf.media.category.choose{/lang}</option>
+                       
+                       {content}
+                               {foreach from=$categoryList item=categoryItem}
+                                       <option value="{$categoryItem->categoryID}">{$categoryItem->getTitle()}</option>
+                                       
+                                       {if $categoryItem->hasChildren()}
+                                               {foreach from=$categoryItem item=subCategoryItem}
+                                                       <option value="{$subCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;{$subCategoryItem->getTitle()}</option>
+                                                       
+                                                       {if $subCategoryItem->hasChildren()}
+                                                               {foreach from=$subCategoryItem item=subSubCategoryItem}
+                                                                       <option value="{$subSubCategoryItem->categoryID}">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{$subSubCategoryItem->getTitle()}</option>
+                                                               {/foreach}
+                                                       {/if}
+                                               {/foreach}
+                                       {/if}
+                               {/foreach}
+                       {/content}
+               </select>
+       </div>
+{/hascontent}
+
 <div class="inputAddon mediaManagerSearch">
        <input type="text" class="mediaManagerSearchField" placeholder="{lang}wcf.media.search.placeholder{/lang}">
        <span class="inputSuffix">
index 6f87a8c5c0729bdd37a228e3979712f34dd2bf10..279485a31b9876a3fbcb55a5883788cd55717796 100644 (file)
@@ -29,7 +29,15 @@ define(['EventHandler', 'WoltLabSuite/Core/Controller/Clipboard', 'WoltLabSuite/
                        var deleteAction = new WCF.Action.Delete('wcf\\data\\media\\MediaAction', '.jsMediaRow');
                        deleteAction.setCallback(Clipboard.reload.bind(Clipboard));
                        
-                       _mediaEditor = new MediaEditor();
+                       _mediaEditor = new MediaEditor({
+                               _editorSuccess: function(media, oldCategoryId) {
+                                       if (media.categoryID != oldCategoryId) {
+                                               window.setTimeout(function() {
+                                                       window.location.reload();
+                                               }, 500);
+                                       }
+                               }
+                       });
                        
                        var editButtons = elByClass('jsMediaEditButton');
                        for (var i = 0, length = editButtons.length; i < length; i++) {
index 5f048352ae191921148060926d6f513a047ee1e0..87ecce7042c46204690de3106b51a3b3ac700142 100644 (file)
@@ -35,6 +35,8 @@ define(
                
                this._media = null;
                this._availableLanguageCount = 1;
+               this._categoryIds = [];
+               this._oldCategoryId = 0;
                
                this._dialogs = new Dictionary();
        }
@@ -62,7 +64,8 @@ define(
                        UiNotification.show();
                        
                        if (this._callbackObject._editorSuccess) {
-                               this._callbackObject._editorSuccess(this._media);
+                               this._callbackObject._editorSuccess(this._media, this._oldCategoryId);
+                               this._oldCategoryId = 0;
                        }
                        
                        UiDialog.close('mediaEditor_' + this._media.mediaID);
@@ -100,6 +103,7 @@ define(
                _saveData: function() {
                        var content = UiDialog.getDialog('mediaEditor_' + this._media.mediaID).content;
                        
+                       var categoryId = elBySel('select[name=categoryID]', content);
                        var altText = elBySel('input[name=altText]', content);
                        var caption = elBySel('textarea[name=caption]', content);
                        var title = elBySel('input[name=title]', content);
@@ -109,6 +113,18 @@ define(
                        var captionError = (caption ? DomTraverse.childByClass(caption.parentNode.parentNode, 'innerError') : false);
                        var titleError = DomTraverse.childByClass(title.parentNode.parentNode, 'innerError');
                        
+                       // category
+                       this._oldCategoryId = this._media.categoryID;
+                       if (this._categoryIds.length) {
+                               this._media.categoryID = ~~categoryId.value;
+                               
+                               // if the selected category id not valid (manipulated DOM), ignore
+                               if (this._categoryIds.indexOf(this._media.categoryID) === -1) {
+                                       this._media.categoryID = 0;
+                               }
+                       }
+                       
+                       // language and multilingualism
                        if (this._availableLanguageCount > 1) {
                                this._media.isMultilingual = ~~elBySel('input[name=isMultilingual]', content).checked;
                                this._media.languageID = this._media.isMultilingual ? null : LanguageChooser.getLanguageId('languageID');
@@ -117,6 +133,7 @@ define(
                                this._media.languageID = LANGUAGE_ID;
                        }
                        
+                       // altText, caption and title
                        this._media.altText = {};
                        this._media.caption = {};
                        this._media.title = {};
@@ -188,6 +205,7 @@ define(
                                                altText: this._media.altText,
                                                caption: this._media.caption,
                                                data: {
+                                                       categoryID: this._media.categoryID,
                                                        isMultilingual: this._media.isMultilingual,
                                                        languageID: this._media.languageID
                                                },
@@ -253,6 +271,9 @@ define(
                                                        source: {
                                                                after: (function(content, data) {
                                                                        this._availableLanguageCount = ~~data.returnValues.availableLanguageCount;
+                                                                       this._categoryIds = data.returnValues.categoryIDs.map(function(number) {
+                                                                               return ~~number;
+                                                                       });
                                                                        
                                                                        var didLoadMediaData = false;
                                                                        if (data.returnValues.mediaData) {
@@ -267,6 +288,10 @@ define(
                                                                                        LanguageChooser.setLanguageId('languageID', this._media.languageID || LANGUAGE_ID);
                                                                                }
                                                                                
+                                                                               if (this._categoryIds.length) {
+                                                                                       elBySel('select[name=categoryID]', content).value = ~~this._media.categoryID;
+                                                                               }
+                                                                               
                                                                                var title = elBySel('input[name=title]', content);
                                                                                var altText = elBySel('input[name=altText]', content);
                                                                                var caption = elBySel('textarea[name=caption]', content);
index 519ef8966a298b9a59d7f0000e4e377db7927c71..623a0ebd0b653524c5d723ee7548a54c2617b97e 100644 (file)
@@ -70,6 +70,13 @@ define(
                        }
                },
                
+               /**
+                * Is called when a new category is selected.
+                */
+               _categoryChange: function() {
+                       this._search.search();
+               },
+               
                /**
                 * Handles clicks on the media manager button.
                 * 
@@ -158,7 +165,14 @@ define(
                 */
                _dialogShow: function() {
                        if (!this._mediaManagerMediaList) {
-                               this._mediaManagerMediaList = elByClass('mediaManagerMediaList', UiDialog.getDialog(this).dialog)[0];
+                               var dialog = this.getDialog();
+                               
+                               this._mediaManagerMediaList = elByClass('mediaManagerMediaList', dialog)[0];
+                               
+                               this._mediaCategorySelect = elBySel('.mediaManagerCategoryList > select', dialog);
+                               if (this._mediaCategorySelect) {
+                                       this._mediaCategorySelect.addEventListener('change', this._categoryChange.bind(this));
+                               }
                                
                                // store list items locally
                                var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
@@ -232,8 +246,23 @@ define(
                 * successfully editing a media file.
                 * 
                 * @param       {object}        media           updated media file data
+                * @param       {integer}       oldCategoryId   old category id
                 */
-               _editorSuccess: function(media) {
+               _editorSuccess: function(media, oldCategoryId) {
+                       // if the category changed of media changed and category
+                       // is selected, check if media list needs to be refreshed
+                       if (this._mediaCategorySelect) {
+                               var selectedCategoryId = ~~this._mediaCategorySelect.value;
+                               
+                               if (selectedCategoryId) {
+                                       var newCategoryId = ~~media.categoryID;
+                                       
+                                       if (oldCategoryId != newCategoryId && (oldCategoryId == selectedCategoryId || newCategoryId == selectedCategoryId)) {
+                                               this._search.search();
+                                       }
+                               }
+                       }
+                       
                        UiDialog.open(this);
                        
                        this._media.set(~~media.mediaID, media);
@@ -328,6 +357,19 @@ define(
                        }
                },
                
+               /**
+                * Returns the id of the currently selected category or `0` if no category is selected.
+                * 
+                * @return      {integer}
+                */
+               getCategoryId: function() {
+                       if (this._mediaCategorySelect) {
+                               return this._mediaCategorySelect.value;
+                       }
+                       
+                       return 0;
+               },
+               
                /**
                 * Returns the media manager dialog element.
                 * 
index 9ad00b9e26d27cf692f1f53eebc4bc58a937ebbe..94bfd6351138c764882204b38348faef82ac18ed 100644 (file)
@@ -60,6 +60,16 @@ define(['Ajax', 'Core', 'Dom/Traverse', 'Dom/Util', 'EventKey', 'Language', 'Ui/
                        }
                },
                
+               /**
+                * Hides the search string treshold error.
+                */
+               _hideStringThresholdError: function() {
+                       var innerInfo = DomTraverse.childByClass(this._input.parentNode.parentNode, 'innerInfo');
+                       if (innerInfo) {
+                               elHide(innerInfo);
+                       }
+               },
+               
                /**
                 * Handles the `[ENTER]` key to submit the form.
                 *
@@ -69,43 +79,32 @@ define(['Ajax', 'Core', 'Dom/Traverse', 'Dom/Util', 'EventKey', 'Language', 'Ui/
                        if (EventKey.Enter(event)) {
                                event.preventDefault();
                                
-                               var innerInfo = DomTraverse.childByClass(this._input.parentNode.parentNode, 'innerInfo');
-                               
                                if (this._input.value.length >= this._mediaManager.getOption('minSearchLength')) {
-                                       if (innerInfo) {
-                                               elHide(innerInfo);
-                                       }
+                                       this._hideStringThresholdError();
                                        
-                                       this._search();
+                                       this.search();
                                }
                                else {
-                                       if (innerInfo) {
-                                               elShow(innerInfo);
-                                       }
-                                       else {
-                                               innerInfo = elCreate('p');
-                                               innerInfo.className = 'innerInfo';
-                                               innerInfo.textContent = Language.get('wcf.media.search.info.searchStringThreshold');
-                                               
-                                               DomUtil.insertAfter(innerInfo, this._input.parentNode);
-                                       }
+                                       this._showStringThresholdError();
                                }
                        }
                },
                
                /**
-                * Sends an AJAX request to fetch search results.
+                * Shows the search string treshold error.
                 */
-               _search: function() {
-                       this._searchMode = true;
-                       
-                       Ajax.api(this, {
-                               parameters: {
-                                       imagesOnly: this._mediaManager.getOption('imagesOnly'),
-                                       mode: this._mediaManager.getMode(),
-                                       searchString: this._input.value
-                               }
-                       });
+               _showStringThresholdError: function() {
+                       var innerInfo = DomTraverse.childByClass(this._input.parentNode.parentNode, 'innerInfo');
+                       if (innerInfo) {
+                               elShow(innerInfo);
+                       }
+                       else {
+                               innerInfo = elCreate('p');
+                               innerInfo.className = 'innerInfo';
+                               innerInfo.textContent = Language.get('wcf.media.search.info.searchStringThreshold');
+                               
+                               DomUtil.insertAfter(innerInfo, this._input.parentNode);
+                       }
                },
                
                /**
@@ -127,7 +126,33 @@ define(['Ajax', 'Core', 'Dom/Traverse', 'Dom/Util', 'EventKey', 'Language', 'Ui/
                 */
                showSearch: function() {
                        elShow(this._searchContainer);
-               }
+               },
+               
+               /**
+                * Sends an AJAX request to fetch search results.
+                */
+               search: function() {
+                       var searchString = this._input.value;
+                       if (searchString && this._input.value.length < this._mediaManager.getOption('minSearchLength')) {
+                               this._showStringThresholdError();
+                               
+                               searchString = '';
+                       }
+                       else {
+                               this._hideStringThresholdError();
+                       }
+                       
+                       this._searchMode = true;
+                       
+                       Ajax.api(this, {
+                               parameters: {
+                                       categoryID: this._mediaManager.getCategoryId(),
+                                       imagesOnly: this._mediaManager.getOption('imagesOnly'),
+                                       mode: this._mediaManager.getMode(),
+                                       searchString: searchString
+                               }
+                       });
+               },
        };
        
        return MediaManagerSearch;
diff --git a/wcfsetup/install/files/lib/acp/form/MediaCategoryAddForm.class.php b/wcfsetup/install/files/lib/acp/form/MediaCategoryAddForm.class.php
new file mode 100644 (file)
index 0000000..0800bfb
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+namespace wcf\acp\form;
+
+/**
+ * Shows the media category add form.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ * @since      3.1
+ */
+class MediaCategoryAddForm extends AbstractCategoryAddForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.media.category.add';
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectTypeName = 'com.woltlab.wcf.media.category';
+}
diff --git a/wcfsetup/install/files/lib/acp/form/MediaCategoryEditForm.class.php b/wcfsetup/install/files/lib/acp/form/MediaCategoryEditForm.class.php
new file mode 100644 (file)
index 0000000..710b647
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+namespace wcf\acp\form;
+
+/**
+ * Shows the media category edit form.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ * @since      3.1
+ */
+class MediaCategoryEditForm extends AbstractCategoryEditForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.media.category.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectTypeName = 'com.woltlab.wcf.media.category';
+}
diff --git a/wcfsetup/install/files/lib/acp/page/MediaCategoryListPage.class.php b/wcfsetup/install/files/lib/acp/page/MediaCategoryListPage.class.php
new file mode 100644 (file)
index 0000000..dfce532
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+namespace wcf\acp\page;
+
+/**
+ * Shows the list media categories.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Page
+ * @since      3.1
+ */
+class MediaCategoryListPage extends AbstractCategoryListPage {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.media.category.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectTypeName = 'com.woltlab.wcf.media.category';
+}
index 75e29d8d2a5dfff25daf593561a3ab01638e4d70..a5bdb0104ca6b110dadc8a31f37eb7865f6f63a1 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\acp\page;
+use wcf\data\category\CategoryNodeTree;
 use wcf\data\media\ViewableMediaList;
 use wcf\page\SortablePage;
 use wcf\system\clipboard\ClipboardHandler;
@@ -22,7 +23,19 @@ class MediaListPage extends SortablePage {
        /**
         * @inheritDoc
         */
-       public $activeMenuItem = 'wcf.acp.menu.link.cms.media.list';
+       public $activeMenuItem = 'wcf.acp.menu.link.media.list';
+       
+       /**
+        * id of the selected media category
+        * @var integer
+        */
+       public $categoryID = 0;
+       
+       /**
+        * node tree with all available media categories
+        * @var \RecursiveIteratorIterator
+        */
+       public $categoryList;
        
        /**
         * @inheritDoc
@@ -79,6 +92,8 @@ class MediaListPage extends SortablePage {
                parent::assignVariables();
                
                WCF::getTPL()->assign([
+                       'categoryID' => $this->categoryID,
+                       'categoryList' => $this->categoryList,
                        'q' => $this->query,
                        'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media')),
                        'username' => $this->username
@@ -91,6 +106,9 @@ class MediaListPage extends SortablePage {
        protected function initObjectList() {
                parent::initObjectList();
                
+               if ($this->categoryID) {
+                       $this->objectList->getConditionBuilder()->add('media.categoryID = ?', [$this->categoryID]);
+               }
                if ($this->query) {
                        $this->objectList->addSearchConditions($this->query);
                }
@@ -99,12 +117,23 @@ class MediaListPage extends SortablePage {
                }
        }
        
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               $this->categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
+               $this->categoryList->setMaxDepth(0);
+       }
+       
        /**
         * @inheritDoc
         */
        public function readParameters() {
                parent::readParameters();
                
+               if (isset($_REQUEST['categoryID'])) $this->categoryID = intval($_REQUEST['categoryID']);
                if (isset($_REQUEST['q'])) $this->query = StringUtil::trim($_REQUEST['q']);
                if (isset($_REQUEST['username'])) $this->username = StringUtil::trim($_REQUEST['username']);
                
index 238cdcb2f31feca889852fa0617f7e4ebdf4bd24..6c9af61684aa84b2c3eedb645ab911a1475e200d 100644 (file)
@@ -18,6 +18,7 @@ use wcf\system\WCF;
  * @since      3.0
  * 
  * @property-read      integer         $mediaID                unique id of the media file
+ * @property-read      integer         $categoryID             id of the category the media file belongs to or `null` if it belongs to no category
  * @property-read      string          $filename               name of the physical media file
  * @property-read      integer         $filesize               size of the physical media file
  * @property-read      string          $fileType               type of the physical media file
index 57b37f6dc9c8110e42d35308c15ce1f2f4b001d2..8e9472ac04003a191c79359197292d67ec432eac 100644 (file)
@@ -1,9 +1,11 @@
 <?php
 namespace wcf\data\media;
 use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\category\CategoryNodeTree;
 use wcf\data\ISearchAction;
 use wcf\data\IUploadAction;
 use wcf\system\acl\simple\SimpleAclHandler;
+use wcf\system\category\CategoryHandler;
 use wcf\system\clipboard\ClipboardHandler;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\exception\PermissionDeniedException;
@@ -109,6 +111,7 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction,
                return [
                        'altText' => $media instanceof ViewableMedia ? $media->altText : [],
                        'caption' => $media instanceof ViewableMedia ? $media->caption : [],
+                       'categoryID' => $media->categoryID,
                        'fileHash' => $media->fileHash,
                        'filename' => $media->filename,
                        'filesize' => $media->filesize,
@@ -178,10 +181,14 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction,
                $mediaList->sqlLimit = 50;
                $mediaList->readObjects();
                
+               $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
+               $categoryList->setMaxDepth(0);
+               
                return [
                        'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media')),
                        'media' => $this->getI18nMediaData($mediaList),
                        'template' => WCF::getTPL()->fetch('mediaManager', 'wcf', [
+                               'categoryList' => $categoryList,
                                'mediaList' => $mediaList,
                                'mode' => $this->parameters['mode']
                        ])
@@ -256,14 +263,19 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction,
                I18nHandler::getInstance()->register('altText_' . $media->mediaID);
                I18nHandler::getInstance()->assignVariables();
                
+               $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
+               $categoryList->setMaxDepth(0);
+               
                return [
                        'availableLanguageCount' => count(LanguageFactory::getInstance()->getLanguages()),
+                       'categoryIDs' => array_keys(CategoryHandler::getInstance()->getCategories('com.woltlab.wcf.media.category')),
                        'mediaData' => $this->getI18nMediaData($mediaList)[$this->getSingleObject()->mediaID],
                        'template' => WCF::getTPL()->fetch('mediaEditor', 'wcf', [
                                '__aclSimplePrefix' => 'mediaEditor_' . $media->mediaID . '_',
                                '__languageChooserPrefix' => 'mediaEditor_' . $media->mediaID . '_',
                                'aclValues' => SimpleAclHandler::getInstance()->getValues('com.woltlab.wcf.media', $media->mediaID),
                                'availableLanguages' => LanguageFactory::getInstance()->getLanguages(),
+                               'categoryList' => $categoryList,
                                'languageID' => WCF::getUser()->languageID,
                                'languages' => LanguageFactory::getInstance()->getLanguages(),
                                'media' => $media
@@ -285,6 +297,7 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction,
                        }
                }
                
+               $this->readInteger('categoryID', true, 'data');
                $this->readInteger('languageID', true, 'data');
                $this->readBoolean('isMultilingual', true, 'data');
                
@@ -309,12 +322,24 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction,
                if ($this->parameters['data']['languageID'] && !LanguageFactory::getInstance()->getLanguage($this->parameters['data']['languageID'])) {
                        throw new UserInputException('languageID');
                }
+               
+               // check category id
+               if ($this->parameters['data']['categoryID']) {
+                       $category = CategoryHandler::getInstance()->getCategory($this->parameters['data']['categoryID']);
+                       if ($category === null || $category->getObjectType()->objectType !== 'com.woltlab.wcf.media.category') {
+                               throw new UserInputException('categoryID');
+                       }
+               }
        }
        
        /**
         * @inheritDoc
         */
        public function update() {
+               if (isset($this->parameters['data']['categoryID']) && $this->parameters['data']['categoryID'] === 0) {
+                       $this->parameters['data']['categoryID'] = null;
+               }
+               
                if (empty($this->objects)) {
                        $this->readObjects();
                }
@@ -396,11 +421,7 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction,
                }
                
                $this->readString('searchString', true);
-               $this->readString('fileType', true);
-               
-               if (!$this->parameters['searchString'] && !$this->parameters['fileType']) {
-                       throw new UserInputException('searchString');
-               }
+               $this->readInteger('categoryID', true);
                
                $this->readBoolean('imagesOnly', true);
                
@@ -419,6 +440,9 @@ class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction,
                if ($this->parameters['imagesOnly']) {
                        $mediaList->getConditionBuilder()->add('media.isImage = ?', [1]);
                }
+               if ($this->parameters['categoryID']) {
+                       $mediaList->getConditionBuilder()->add('media.categoryID = ?', [$this->parameters['categoryID']]);
+               }
                $mediaList->sqlOrderBy = 'media.uploadTime DESC';
                $mediaList->sqlLimit = 50;
                $mediaList->readObjectIDs();
index 5af616b41101a96927f378410faa31c06e837dde..3793ea1cf3c056aac3e4058621a78fcbb21bfd9e 100644 (file)
@@ -29,6 +29,8 @@ class MediaList extends DatabaseObjectList {
         * @param       string          $searchString
         */
        public function addSearchConditions($searchString) {
+               if ($searchString === '') return;
+               
                $searchString = '%'.addcslashes($searchString, '_%').'%';
                
                $this->sqlConditionJoins .= ' LEFT JOIN wcf'.WCF_N.'_media_content media_content ON (media_content.mediaID = media.mediaID)';
diff --git a/wcfsetup/install/files/lib/system/category/MediaCategoryType.class.php b/wcfsetup/install/files/lib/system/category/MediaCategoryType.class.php
new file mode 100644 (file)
index 0000000..fc54b68
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+namespace wcf\system\category;
+use wcf\system\WCF;
+
+/**
+ * Category implementation for media files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Category
+ * @since      3.1
+ */
+class MediaCategoryType extends AbstractCategoryType {
+       /**
+        * @inheritDoc
+        */
+       protected $langVarPrefix = 'wcf.media.category';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $hasDescription = false;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $maximumNestingLevel = 2;
+       
+       /**
+        * @inheritDoc
+        */
+       public function canAddCategory() {
+               return WCF::getSession()->getPermission('admin.content.cms.canManageMedia');
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function canDeleteCategory() {
+               return WCF::getSession()->getPermission('admin.content.cms.canManageMedia');
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function canEditCategory() {
+               return WCF::getSession()->getPermission('admin.content.cms.canManageMedia');
+       }
+}
\ No newline at end of file
index 9e0c82f8853fbcdd48b7099961241c5bc4388d43..af36ea9e9328dcb99535ee7a8089107e0de3f9d5 100644 (file)
@@ -102,6 +102,10 @@ select {
        // scrollbar instead. Setting a `max-width` will cause the browser to respect the page
        // boundaries and nicely wrap the displayed value instead.
        max-width: 100%;
+       
+       &.fullWidth {
+               width: 100%;
+       }
 }
 
 .formSubmit {
index 434856cb8f0d88f5027d9c04fa5dc0db424e15e1..6aa7799fd35edcd55889bcba79303133f1a1b6c0 100644 (file)
                }
        }
 }
+
+.mediaManagerCategoryList {
+       margin-bottom: 5px;
+}
index c8e4c8826c467414f6a5b9d26287115f314bc223..1f73a0bb98a16b9a622a356cee39f7c4a71b41c2 100644 (file)
                <item name="wcf.acp.menu.link.cms.menu.add"><![CDATA[Menü hinzufügen]]></item>
                <item name="wcf.acp.menu.link.cms.box.list"><![CDATA[Boxen]]></item>
                <item name="wcf.acp.menu.link.cms.box.add"><![CDATA[Box hinzufügen]]></item>
-               <item name="wcf.acp.menu.link.cms.media.list"><![CDATA[Medien]]></item>
+               <item name="wcf.acp.menu.link.media"><![CDATA[Medien]]></item>
+               <item name="wcf.acp.menu.link.media.list"><![CDATA[Medien]]></item>
+               <item name="wcf.acp.menu.link.media.category.list"><![CDATA[Kategorien]]></item>
+               <item name="wcf.acp.menu.link.media.category.add"><![CDATA[Kategorie hinzufügen]]></item>
                <item name="wcf.acp.menu.link.article"><![CDATA[Artikel]]></item>
                <item name="wcf.acp.menu.link.article.list"><![CDATA[Artikel]]></item>
                <item name="wcf.acp.menu.link.article.add"><![CDATA[Artikel hinzufügen]]></item>
@@ -2656,6 +2659,8 @@ Fehler sind beispielsweise:
                <item name="wcf.media.button.insert"><![CDATA[Einfügen]]></item>
                <item name="wcf.media.button.select"><![CDATA[Auswählen]]></item>
                <item name="wcf.media.caption"><![CDATA[Bildunterschrift]]></item>
+               <item name="wcf.media.category.choose"><![CDATA[Kategorien]]></item>
+               <item name="wcf.media.categoryID"><![CDATA[Kategorie]]></item>
                <item name="wcf.media.chooseImage"><![CDATA[Bild auswählen]]></item>
                <item name="wcf.media.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Möchtest du{else}Möchten Sie{/if} die Datei <span class="confirmationObject">{$title}</span> wirklich löschen?]]></item>
                <item name="wcf.media.edit"><![CDATA[Datei bearbeiten]]></item>
index 598a732bf0c152b3525eebeea6bc8ab1f7bf54e6..946fe7af436f1d6363afc2893746562d29df6905 100644 (file)
@@ -778,7 +778,10 @@ Examples for medium ID detection:
                <item name="wcf.acp.menu.link.cms.menu.add"><![CDATA[Add Menu]]></item>
                <item name="wcf.acp.menu.link.cms.box.list"><![CDATA[Boxes]]></item>
                <item name="wcf.acp.menu.link.cms.box.add"><![CDATA[Add Box]]></item>
-               <item name="wcf.acp.menu.link.cms.media.list"><![CDATA[Media]]></item>
+               <item name="wcf.acp.menu.link.media"><![CDATA[Media]]></item>
+               <item name="wcf.acp.menu.link.media.list"><![CDATA[Media]]></item>
+               <item name="wcf.acp.menu.link.media.category.list"><![CDATA[Categories]]></item>
+               <item name="wcf.acp.menu.link.media.category.add"><![CDATA[Add Category]]></item>
                <item name="wcf.acp.menu.link.article"><![CDATA[Articles]]></item>
                <item name="wcf.acp.menu.link.article.list"><![CDATA[Articles]]></item>
                <item name="wcf.acp.menu.link.article.add"><![CDATA[Add Article]]></item>
@@ -2611,6 +2614,8 @@ Errors are:
                <item name="wcf.media.button.insert"><![CDATA[Insert]]></item>
                <item name="wcf.media.button.select"><![CDATA[Select]]></item>
                <item name="wcf.media.caption"><![CDATA[Caption]]></item>
+               <item name="wcf.media.category.choose"><![CDATA[Categories]]></item>
+               <item name="wcf.media.categoryID"><![CDATA[Category]]></item>
                <item name="wcf.media.chooseImage"><![CDATA[Select Image]]></item>
                <item name="wcf.media.delete.confirmMessage"><![CDATA[Do you really want to delete the media file <span class="confirmationObject">{$title}</span>?]]></item>
                <item name="wcf.media.edit"><![CDATA[Edit Media File]]></item>
index 500ebd39b733e4751fd607d84e04b18dc9344f72..12ce932d2ce7ff2fb52896c817d1883a42bacc8c 100644 (file)
@@ -592,6 +592,7 @@ CREATE TABLE wcf1_like_object (
 DROP TABLE IF EXISTS wcf1_media;
 CREATE TABLE wcf1_media (
        mediaID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       categoryID INT(10),
        
        filename VARCHAR(255) NOT NULL DEFAULT '',
        filesize INT(10) NOT NULL DEFAULT 0,
@@ -1745,6 +1746,7 @@ ALTER TABLE wcf1_language_item ADD FOREIGN KEY (languageID) REFERENCES wcf1_lang
 ALTER TABLE wcf1_language_item ADD FOREIGN KEY (languageCategoryID) REFERENCES wcf1_language_category (languageCategoryID) ON DELETE CASCADE;
 ALTER TABLE wcf1_language_item ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
 
+ALTER TABLE wcf1_media ADD FOREIGN KEY (categoryID) REFERENCES wcf1_category (categoryID) ON DELETE SET NULL;
 ALTER TABLE wcf1_media ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
 ALTER TABLE wcf1_media ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE SET NULL;