Overhauled clipboard API (both PHP and JS)
authorAlexander Ebert <ebert@woltlab.com>
Sat, 30 May 2015 19:21:11 +0000 (21:21 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 30 May 2015 19:21:11 +0000 (21:21 +0200)
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/js/WoltLab/WCF/Controller/Clipboard.js [new file with mode: 0644]
wcfsetup/install/files/lib/action/ClipboardAction.class.php [deleted file]
wcfsetup/install/files/lib/action/ClipboardLoadMarkedItemsAction.class.php [deleted file]
wcfsetup/install/files/lib/action/ClipboardProxyAction.class.php [deleted file]
wcfsetup/install/files/lib/data/AbstractDatabaseObjectAction.class.php
wcfsetup/install/files/lib/data/clipboard/item/ClipboardItemAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/clipboard/ClipboardHandler.class.php
wcfsetup/install/files/lib/system/clipboard/action/AbstractClipboardAction.class.php
wcfsetup/install/files/lib/system/clipboard/action/IClipboardAction.class.php

index 57cd0022b48532c1808640b380ec38afc1108f36..68687d7b4f242a1ce656b6c94a51dd9785cbb814 100755 (executable)
@@ -1400,68 +1400,10 @@ WCF.Dropdown.Interactive.Instance = Class.extend({
 
 /**
  * Clipboard API
+ * 
+ * @deprecated 2.2 - please use `WoltLab/WCF/Controller/Clipboard` instead
  */
 WCF.Clipboard = {
-       /**
-        * action proxy object
-        * @var WCF.Action.Proxy
-        */
-       _actionProxy: null,
-       
-       /**
-        * action objects
-        * @var object
-        */
-       _actionObjects: {},
-       
-       /**
-        * list of clipboard containers
-        * @var jQuery
-        */
-       _containers: null,
-       
-       /**
-        * container meta data
-        * @var object
-        */
-       _containerData: { },
-       
-       /**
-        * user has marked items
-        * @var boolean
-        */
-       _hasMarkedItems: false,
-       
-       /**
-        * list of ids of marked objects grouped by object type
-        * @var object
-        */
-       _markedObjectIDs: { },
-       
-       /**
-        * current page
-        * @var string
-        */
-       _page: '',
-       
-       /**
-        * current page's object id
-        * @var integer
-        */
-       _pageObjectID: 0,
-       
-       /**
-        * proxy object
-        * @var WCF.Action.Proxy
-        */
-       _proxy: null,
-       
-       /**
-        * list of elements already tracked for clipboard actions
-        * @var object
-        */
-       _trackedElements: { },
-       
        /**
         * Initializes the clipboard API.
         * 
@@ -1471,52 +1413,12 @@ WCF.Clipboard = {
         * @param       integer         pageObjectID
         */
        init: function(page, hasMarkedItems, actionObjects, pageObjectID) {
-               this._page = page;
-               this._actionObjects = actionObjects || { };
-               this._hasMarkedItems = (hasMarkedItems > 0);
-               this._pageObjectID = parseInt(pageObjectID) || 0;
-               
-               this._actionProxy = new WCF.Action.Proxy({
-                       success: $.proxy(this._actionSuccess, this),
-                       url: 'index.php/ClipboardProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND
-               });
-               
-               this._proxy = new WCF.Action.Proxy({
-                       success: $.proxy(this._success, this),
-                       url: 'index.php/Clipboard/?t=' + SECURITY_TOKEN + SID_ARG_2ND
-               });
-               
-               // init containers first
-               this._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) {
-                       this._initContainer(container);
-               }, this));
-               
-               // loads marked items
-               if (this._hasMarkedItems && this._containers.length) {
-                       this._loadMarkedItems();
-               }
-               
-               var self = this;
-               WCF.DOMNodeInsertedHandler.addCallback('WCF.Clipboard', function() {
-                       self._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) {
-                               self._initContainer(container);
-                       }, self));
-               });
-       },
-       
-       /**
-        * Loads marked items on init.
-        */
-       _loadMarkedItems: function() {
-               new WCF.Action.Proxy({
-                       autoSend: true,
-                       data: {
-                               containerData: this._containerData,
-                               pageClassName: this._page,
-                               pageObjectID: this._pageObjectID
-                       },
-                       success: $.proxy(this._loadMarkedItemsSuccess, this),
-                       url: 'index.php/ClipboardLoadMarkedItems/?t=' + SECURITY_TOKEN + SID_ARG_2ND
+               require(['WoltLab/WCF/Controller/Clipboard'], function(ControllerClipboard) {
+                       ControllerClipboard.setup({
+                               hasMarkedItems: (hasMarkedItems > 0),
+                               pageClassName: page,
+                               pageObjectId: pageObjectID
+                       });
                });
        },
        
@@ -1524,472 +1426,9 @@ WCF.Clipboard = {
         * Reloads the list of marked items.
         */
        reload: function() {
-               if (this._containers === null) {
-                       return;
-               }
-               
-               this._loadMarkedItems();
-       },
-       
-       /**
-        * Marks all returned items as marked
-        * 
-        * @param       object          data
-        * @param       string          textStatus
-        * @param       jQuery          jqXHR
-        */
-       _loadMarkedItemsSuccess: function(data, textStatus, jqXHR) {
-               this._resetMarkings();
-               
-               for (var $typeName in data.markedItems) {
-                       if (!this._markedObjectIDs[$typeName]) {
-                               this._markedObjectIDs[$typeName] = [ ];
-                       }
-                       
-                       var $objectData = data.markedItems[$typeName];
-                       for (var $i in $objectData) {
-                               this._markedObjectIDs[$typeName].push($objectData[$i]);
-                       }
-                       
-                       // loop through all containers
-                       this._containers.each($.proxy(function(index, container) {
-                               var $container = $(container);
-                               
-                               // typeName does not match, continue
-                               if ($container.data('type') != $typeName) {
-                                       return true;
-                               }
-                               
-                               // mark items as marked
-                               $container.find('input.jsClipboardItem').each($.proxy(function(innerIndex, item) {
-                                       var $item = $(item);
-                                       if (WCF.inArray($item.data('objectID'), this._markedObjectIDs[$typeName])) {
-                                               $item.prop('checked', true);
-                                               
-                                               // add marked class for element container
-                                               $item.parents('.jsClipboardObject').addClass('jsMarked');
-                                       }
-                               }, this));
-                               
-                               // check if there is a markAll-checkbox
-                               $container.find('input.jsClipboardMarkAll').each(function(innerIndex, markAll) {
-                                       var $allItemsMarked = true;
-                                       
-                                       $container.find('input.jsClipboardItem').each(function(itemIndex, item) {
-                                               var $item = $(item);
-                                               if (!$item.prop('checked')) {
-                                                       $allItemsMarked = false;
-                                               }
-                                       });
-                                       
-                                       if ($allItemsMarked) {
-                                               $(markAll).prop('checked', true);
-                                       }
-                               });
-                       }, this));
-               }
-               
-               // call success method to build item list editors
-               this._success(data, textStatus, jqXHR);
-       },
-       
-       /**
-        * Resets all checkboxes.
-        */
-       _resetMarkings: function() {
-               this._containers.each($.proxy(function(index, container) {
-                       var $container = $(container);
-                       
-                       this._markedObjectIDs[$container.data('type')] = [ ];
-                       $container.find('input.jsClipboardItem, input.jsClipboardMarkAll').prop('checked', false);
-                       $container.find('.jsClipboardObject').removeClass('jsMarked');
-               }, this));
-       },
-       
-       /**
-        * Initializes a clipboard container.
-        * 
-        * @param       object          container
-        */
-       _initContainer: function(container) {
-               var $container = $(container);
-               var $containerID = $container.wcfIdentify();
-               
-               if (!this._trackedElements[$containerID]) {
-                       $container.find('.jsClipboardMarkAll').data('hasContainer', $containerID).click($.proxy(this._markAll, this));
-                       
-                       this._markedObjectIDs[$container.data('type')] = [ ];
-                       this._containerData[$container.data('type')] = {};
-                       $.each($container.data(), $.proxy(function(index, element) {
-                               if (index.match(/^type(.+)/)) {
-                                       this._containerData[$container.data('type')][WCF.String.lcfirst(index.replace(/^type/, ''))] = element;
-                               }
-                       }, this));
-                       
-                       this._trackedElements[$containerID] = [ ];
-               }
-               
-               // track individual checkboxes
-               $container.find('input.jsClipboardItem').each($.proxy(function(index, input) {
-                       var $input = $(input);
-                       var $inputID = $input.wcfIdentify();
-                       
-                       if (!WCF.inArray($inputID, this._trackedElements[$containerID])) {
-                               this._trackedElements[$containerID].push($inputID);
-                               
-                               $input.data('hasContainer', $containerID).click($.proxy(this._click, this));
-                       }
-               }, this));
-       },
-       
-       /**
-        * Processes change checkbox state.
-        * 
-        * @param       object          event
-        */
-       _click: function(event) {
-               var $item = $(event.target);
-               var $objectID = $item.data('objectID');
-               var $isMarked = ($item.prop('checked')) ? true : false;
-               var $objectIDs = [ $objectID ];
-               
-               if ($item.data('hasContainer')) {
-                       var $container = $('#' + $item.data('hasContainer'));
-                       var $type = $container.data('type');
-               }
-               else {
-                       var $type = $item.data('type');
-               }
-               
-               if ($isMarked) {
-                       this._markedObjectIDs[$type].push($objectID);
-                       $item.parents('.jsClipboardObject').addClass('jsMarked');
-               }
-               else {
-                       this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID);
-                       $item.parents('.jsClipboardObject').removeClass('jsMarked');
-               }
-               
-               // item is part of a container
-               if ($item.data('hasContainer')) {
-                       // check if all items are marked
-                       var $markedAll = true;
-                       $container.find('input.jsClipboardItem').each(function(index, containerItem) {
-                               var $containerItem = $(containerItem);
-                               if (!$containerItem.prop('checked')) {
-                                       $markedAll = false;
-                               }
-                       });
-                       
-                       // simulate a ticked 'markAll' checkbox
-                       $container.find('.jsClipboardMarkAll').each(function(index, markAll) {
-                               if ($markedAll) {
-                                       $(markAll).prop('checked', true);
-                               }
-                               else {
-                                       $(markAll).prop('checked', false);
-                               }
-                       });
-               }
-               
-               this._saveState($type, $objectIDs, $isMarked);
-       },
-       
-       /**
-        * Marks all associated clipboard items as checked.
-        * 
-        * @param       object          event
-        */
-       _markAll: function(event) {
-               var $item = $(event.target);
-               var $objectIDs = [ ];
-               var $isMarked = true;
-               
-               // if markAll object is a checkbox, allow toggling
-               if ($item.is('input')) {
-                       $isMarked = $item.prop('checked');
-               }
-               
-               if ($item.data('hasContainer')) {
-                       var $container = $('#' + $item.data('hasContainer'));
-                       var $type = $container.data('type');
-               }
-               else {
-                       var $type = $item.data('type');
-               }
-               
-               // handle item containers
-               if ($item.data('hasContainer')) {
-                       // toggle state for all associated items
-                       $container.find('input.jsClipboardItem').each($.proxy(function(index, containerItem) {
-                               var $containerItem = $(containerItem);
-                               var $objectID = $containerItem.data('objectID');
-                               if ($isMarked) {
-                                       if (!$containerItem.prop('checked')) {
-                                               $containerItem.prop('checked', true);
-                                               this._markedObjectIDs[$type].push($objectID);
-                                               $objectIDs.push($objectID);
-                                       }
-                               }
-                               else {
-                                       if ($containerItem.prop('checked')) {
-                                               $containerItem.prop('checked', false);
-                                               this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID);
-                                               $objectIDs.push($objectID);
-                                       }
-                               }
-                       }, this));
-                       
-                       if ($isMarked) {
-                               $container.find('.jsClipboardObject').addClass('jsMarked');
-                       }
-                       else {
-                               $container.find('.jsClipboardObject').removeClass('jsMarked');
-                       }
-               }
-               
-               // save new status
-               this._saveState($type, $objectIDs, $isMarked);
-       },
-       
-       /**
-        * Saves clipboard item state.
-        * 
-        * @param       string          type
-        * @param       array           objectIDs
-        * @param       boolean         isMarked
-        */
-       _saveState: function(type, objectIDs, isMarked) {
-               this._proxy.setOption('data', {
-                       action: (isMarked) ? 'mark' : 'unmark',
-                       containerData: this._containerData,
-                       objectIDs: objectIDs,
-                       pageClassName: this._page,
-                       pageObjectID: this._pageObjectID,
-                       type: type
-               });
-               this._proxy.sendRequest();
-       },
-       
-       /**
-        * Updates editor options.
-        * 
-        * @param       object          data
-        * @param       string          textStatus
-        * @param       jQuery          jqXHR
-        */
-       _success: function(data, textStatus, jqXHR) {
-               // clear all editors first
-               var $containers = {};
-               $('.jsClipboardEditor').each(function(index, container) {
-                       var $container = $(container);
-                       var $types = eval($container.data('types'));
-                       for (var $i = 0, $length = $types.length; $i < $length; $i++) {
-                               var $typeName = $types[$i];
-                               $containers[$typeName] = $container;
-                       }
-                       
-                       var $containerID = $container.wcfIdentify();
-                       WCF.CloseOverlayHandler.removeCallback($containerID);
-                       
-                       $container.empty();
-               });
-               
-               // do not build new editors
-               if (!data.items) return;
-               
-               // rebuild editors
-               for (var $typeName in data.items) {
-                       if (!$containers[$typeName]) {
-                               continue;
-                       }
-                       
-                       // create container
-                       var $container = $containers[$typeName];
-                       var $list = $container.children('ul');
-                       if ($list.length == 0) {
-                               $list = $('<ul />').appendTo($container);
-                       }
-                       
-                       var $editor = data.items[$typeName];
-                       var $label = $('<li class="dropdown"><span class="dropdownToggle button">' + $editor.label + '</span></li>').appendTo($list);
-                       var $itemList = $('<ol class="dropdownMenu"></ol>').appendTo($label);
-                       
-                       // create editor items
-                       for (var $itemIndex in $editor.items) {
-                               var $item = $editor.items[$itemIndex];
-                               
-                               var $listItem = $('<li><span>' + $item.label + '</span></li>').appendTo($itemList);
-                               $listItem.data('container', $container);
-                               $listItem.data('objectType', $typeName);
-                               $listItem.data('actionName', $item.actionName).data('parameters', $item.parameters);
-                               $listItem.data('internalData', $item.internalData).data('url', $item.url).data('type', $typeName);
-                               
-                               // bind event
-                               $listItem.click($.proxy(this._executeAction, this));
-                       }
-                       
-                       // add 'unmark all'
-                       $('<li class="dropdownDivider" />').appendTo($itemList);
-                       var $foo = $typeName;
-                       $('<li><span>' + WCF.Language.get('wcf.clipboard.item.unmarkAll') + '</span></li>').data('typeName', $typeName).appendTo($itemList).click($.proxy(function(event) {
-                               var $typeName = $(event.currentTarget).data('typeName');
-                               
-                               this._proxy.setOption('data', {
-                                       action: 'unmarkAll',
-                                       type: $typeName
-                               });
-                               this._proxy.setOption('success', $.proxy(function(data, textStatus, jqXHR) {
-                                       this._containers.each($.proxy(function(index, container) {
-                                               var $container = $(container);
-                                               if ($container.data('type') == $typeName) {
-                                                       $container.find('.jsClipboardMarkAll, .jsClipboardItem').prop('checked', false);
-                                                       $container.find('.jsClipboardObject').removeClass('jsMarked');
-                                                       
-                                                       return false;
-                                               }
-                                       }, this));
-                                       
-                                       // call and restore success method
-                                       this._success(data, textStatus, jqXHR);
-                                       this._proxy.setOption('success', $.proxy(this._success, this));
-                                       this._loadMarkedItems();
-                               }, this));
-                               this._proxy.sendRequest();
-                       }, this));
-                       
-                       WCF.Dropdown.initDropdown($label.children('.dropdownToggle'), false);
-               }
-       },
-       
-       /**
-        * Closes the clipboard editor item list.
-        */
-       _closeLists: function() {
-               $('.jsClipboardEditor ul').removeClass('dropdownOpen');
-       },
-       
-       /**
-        * Executes a clipboard editor item action.
-        * 
-        * @param       object          event
-        */
-       _executeAction: function(event) {
-               var $listItem = $(event.currentTarget);
-               var $url = $listItem.data('url');
-               if ($url) {
-                       window.location.href = $url;
-               }
-               
-               var $fireEvent = true;
-               if ($listItem.data('parameters').className && $listItem.data('parameters').actionName) {
-                       if ($listItem.data('parameters').actionName === 'unmarkAll' || $listItem.data('parameters').objectIDs) {
-                               var $confirmMessage = $listItem.data('internalData')['confirmMessage'];
-                               if ($confirmMessage) {
-                                       var $template = $listItem.data('internalData')['template'];
-                                       if ($template) $template = $($template);
-                                       
-                                       WCF.System.Confirmation.show($confirmMessage, $.proxy(function(action) {
-                                               if (action === 'confirm') {
-                                                       var $data = { };
-                                                       
-                                                       if ($template && $template.length) {
-                                                               $('#wcfSystemConfirmationContent').find('input, select, textarea').each(function(index, item) {
-                                                                       var $item = $(item);
-                                                                       $data[$item.prop('name')] = $item.val();
-                                                               });
-                                                       }
-                                                       
-                                                       this._executeAJAXActions($listItem, $data);
-                                               }
-                                       }, this), '', $template);
-                               }
-                               else {
-                                       this._executeAJAXActions($listItem, { });
-                               }
-                       }
-               }
-               else {
-                       var $confirmMessage = $listItem.data('internalData')['confirmMessage'];
-                       if ($confirmMessage) {
-                               $fireEvent = false;
-                               
-                               WCF.System.Confirmation.show($confirmMessage, function(action) {
-                                       if (action === 'confirm') {
-                                               // fire event
-                                               $listItem.data('container').trigger('clipboardAction', [ $listItem.data('type'), $listItem.data('actionName'), $listItem.data('parameters') ]);
-                                       }
-                               });
-                       }
-               }
-               
-               if ($fireEvent) {
-                       // fire event
-                       $listItem.data('container').trigger('clipboardAction', [ $listItem.data('type'), $listItem.data('actionName'), $listItem.data('parameters') ]);
-               }
-       },
-       
-       /**
-        * Executes the AJAX actions for the given editor list item.
-        * 
-        * @param       jQuery          listItem
-        * @param       object          data
-        */
-       _executeAJAXActions: function(listItem, data) {
-               data = data || { };
-               var $objectIDs = [];
-               if (listItem.data('parameters').actionName !== 'unmarkAll') {
-                       $.each(listItem.data('parameters').objectIDs, function(index, objectID) {
-                               $objectIDs.push(parseInt(objectID));
-                       });
-               }
-               
-               var $parameters = {
-                       data: data,
-                       containerData: this._containerData[listItem.data('type')]
-               };
-               var $__parameters = listItem.data('internalData')['parameters'];
-               if ($__parameters !== undefined) {
-                       for (var $key in $__parameters) {
-                               $parameters[$key] = $__parameters[$key];
-                       }
-               }
-               
-               new WCF.Action.Proxy({
-                       autoSend: true,
-                       data: {
-                               actionName: listItem.data('parameters').actionName,
-                               className: listItem.data('parameters').className,
-                               objectIDs: $objectIDs,
-                               parameters: $parameters
-                       },
-                       success: $.proxy(function(data) {
-                               if (listItem.data('parameters').actionName !== 'unmarkAll') {
-                                       listItem.data('container').trigger('clipboardActionResponse', [ data, listItem.data('type'), listItem.data('actionName'), listItem.data('parameters') ]);
-                               }
-                               
-                               this._loadMarkedItems();
-                       }, this)
-               });
-               
-               if (this._actionObjects[listItem.data('objectType')] && this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName]) {
-                       this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName].triggerEffect($objectIDs);
-               }
-       },
-       
-       /**
-        * Sends a clipboard proxy request.
-        * 
-        * @param       object          item
-        */
-       sendRequest: function(item) {
-               var $item = $(item);
-               
-               this._actionProxy.setOption('data', {
-                       parameters: $item.data('parameters'),
-                       typeName: $item.data('type')
+               require(['WoltLab/WCF/Controller/Clipboard'], function(ControllerClipboard) {
+                       ControllerClipboard.reload();
                });
-               this._actionProxy.sendRequest();
        }
 };
 
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Clipboard.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Clipboard.js
new file mode 100644 (file)
index 0000000..d1545aa
--- /dev/null
@@ -0,0 +1,558 @@
+/**
+ * Clipboard API Handler.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Controller/Clipboard
+ */
+define(
+       [
+               'Ajax',         'Core',     'Dictionary',      'EventHandler',
+               'Language',     'List',     'ObjectMap',       'DOM/ChangeListener',
+               'DOM/Traverse', 'DOM/Util', 'UI/Confirmation', 'UI/SimpleDropdown'
+       ],
+       function(
+               Ajax,            Core,       Dictionary,        EventHandler,
+               Language,        List,       ObjectMap,         DOMChangeListener,
+               DOMTraverse,     DOMUtil,    UIConfirmation,    UISimpleDropdown
+       )
+{
+       "use strict";
+       
+       var _containers = new Dictionary();
+       var _editors = new Dictionary();
+       var _elements = document.getElementsByClassName('jsClipboardContainer');
+       var _itemData = new ObjectMap();
+       var _knownCheckboxes = new List();
+       var _options = {};
+       
+       var _callbackCheckbox = null;
+       var _callbackItem = null;
+       var _callbackUnmarkAll = null;
+       
+       /**
+        * Clipboard API
+        * 
+        * @exports     WoltLab/WCF/Controller/Clipboard
+        */
+       var ControllerClipboard = {
+               /**
+                * Initializes the clipboard API handler.
+                * 
+                * @param       {object<string, *>}     options         initialization options
+                */
+               setup: function(options) {
+                       _callbackCheckbox = this._mark.bind(this);
+                       _callbackItem = this._executeAction.bind(this);
+                       _callbackUnmarkAll = this._unmarkAll.bind(this);
+                       _options = Core.extend({
+                               hasMarkedItems: false,
+                               pageClassName: '',
+                               pageObjectId: 0
+                       }, options);
+                       
+                       if (!_options.pageClassName) {
+                               throw new Error("Expected a non-empty string for parameter 'pageClassName'.");
+                       }
+                       
+                       this._initContainers();
+                       this._initEditors();
+                       
+                       if (_options.hasMarkedItems && _elements.length) {
+                               this._loadMarkedItems();
+                       }
+                       
+                       DOMChangeListener.add('WoltLab/WCF/Controller/Clipboard', this._initContainers.bind(this));
+               },
+               
+               /**
+                * Reloads the clipboard data.
+                */
+               reload: function() {
+                       if (_containers.size) {
+                               this._loadMarkedItems();
+                       }
+               },
+               
+               /**
+                * Initializes clipboard containers.
+                */
+               _initContainers: function() {
+                       for (var i = 0, length = _elements.length; i < length; i++) {
+                               var container = _elements[i];
+                               var containerId = DOMUtil.identify(container);
+                               var containerData = _containers.get(containerId);
+                               
+                               if (containerData === undefined) {
+                                       var markAll = container.querySelector('.jsClipboardMarkAll');
+                                       markAll.setAttribute('data-container-id', containerId);
+                                       markAll.addEventListener('click', this._markAll.bind(this));
+                                       
+                                       containerData = {
+                                               checkboxes: container.getElementsByClassName('jsClipboardItem'),
+                                               element: container,
+                                               markAll: markAll,
+                                               markedObjectIds: new List()
+                                       };
+                                       _containers.set(containerId, containerData);
+                               }
+                               
+                               for (var j = 0, innerLength = containerData.checkboxes.length; j < innerLength; j++) {
+                                       var checkbox = containerData.checkboxes[j];
+                                       
+                                       if (!_knownCheckboxes.has(checkbox)) {
+                                               checkbox.setAttribute('data-container-id', containerId);
+                                               checkbox.addEventListener('click', _callbackCheckbox);
+                                               
+                                               _knownCheckboxes.add(checkbox);
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Initializes the clipboard editor dropdowns.
+                */
+               _initEditors: function() {
+                       var getTypes = function(editor) {
+                               var tmp = null;
+                               
+                               try {
+                                       var types = editor.getAttribute('data-types');
+                                       if (typeof types === 'string') {
+                                               tmp = JSON.parse('{ "types": ' + types.replace(/'/g, '"') + '}');
+                                       }
+                               }
+                               catch (e) {
+                                       throw new Error("Expected a valid 'data-type' attribute for element '" + DOMUtil.identify(editor) + "'.");
+                               }
+                               
+                               return tmp.types;
+                               if (types !== null) {
+                                       types = types.types;
+                               }
+                               
+                               return types;
+                       };
+                       
+                       var editors = document.getElementsByClassName('jsClipboardEditor');
+                       for (var i = 0, length = editors.length; i < length; i++) {
+                               var editor = editors[i];
+                               var types = getTypes(editor);
+                               
+                               for (var j = 0, innerLength = types.length; j < innerLength; j++) {
+                                       _editors.set(types[j], editor);
+                               }
+                       }
+               },
+               
+               /**
+                * Loads marked items from clipboard.
+                */
+               _loadMarkedItems: function() {
+                       Ajax.api(this, {
+                               actionName: 'getMarkedItems',
+                               parameters: {
+                                       pageClassName: _options.pageClassName,
+                                       pageObjectID: _options.pageObjectId
+                               }
+                       });
+               },
+               
+               /**
+                * Marks or unmarks all visible items at once.
+                * 
+                * @param       {object}        event   event object
+                */
+               _markAll: function(event) {
+                       var checkbox = event.currentTarget;
+                       var isMarked = (checkbox.nodeName !== 'INPUT' || checkbox.checked);
+                       var objectIds = [];
+                       
+                       var containerId = checkbox.getAttribute('data-container-id');
+                       var data = _containers.get(containerId);
+                       var type = data.element.getAttribute('data-type');
+                       
+                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
+                               var item = data.checkboxes[i];
+                               var objectId = ~~item.getAttribute('data-object-id');
+                               
+                               if (isMarked) {
+                                       if (!item.checked) {
+                                               item.checked = true;
+                                               
+                                               data.markedObjectIds.add(objectId);
+                                               objectIds.push(objectId);
+                                       }
+                               }
+                               else {
+                                       if (item.checked) {
+                                               item.checked = false;
+                                               
+                                               data.markedObjectIds['delete'](objectId);
+                                               objectIds.push(objectId);
+                                       }
+                               }
+                               
+                               var clipboardObject = DOMTraverse.parentByClass(checkbox, 'jsClipboardObject');
+                               if (clipboardObject !== null) {
+                                       clipboardObject.classList[(isMarked ? 'addClass' : 'removeClass')]('jsMarked');
+                               }
+                       }
+                       
+                       this._saveState(type, objectIds, isMarked);
+               },
+               
+               /**
+                * Marks or unmarks an individual item.
+                * 
+                * @param       {object}        event           event object
+                */
+               _mark: function(event) {
+                       var checkbox = event.currentTarget;
+                       var objectId = ~~checkbox.getAttribute('data-object-id');
+                       var isMarked = checkbox.checked;
+                       var containerId = checkbox.getAttribute('data-container-id');
+                       var data = _containers.get(containerId);
+                       var type = data.element.getAttribute('data-type');
+                       
+                       var clipboardObject = DOMTraverse.parentByClass(checkbox, 'jsClipboardObject');
+                       data.markedObjectIds[(isMarked ? 'add' : 'delete')](objectId);
+                       clipboardObject.classList[(isMarked) ? 'add' : 'remove']('jsMarked');
+                       
+                       var markedAll = true;
+                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
+                               if (!data.checkboxes[i].checked) {
+                                       markedAll = false;
+                                       
+                                       break;
+                               }
+                       }
+                       
+                       data.markAll.checked = markedAll;
+                       
+                       this._saveState(type, [ objectId ], isMarked);
+               },
+               
+               /**
+                * Saves the state for given item object ids.
+                * 
+                * @param       {string}                type            object type
+                * @param       {array<integer>}        objectIds       item object ids
+                * @param       {boolean]               isMarked        true if marked
+                */
+               _saveState: function(type, objectIds, isMarked) {
+                       Ajax.api(this, {
+                               actionName: (isMarked ? 'mark' : 'unmark'),
+                               parameters: {
+                                       pageClassName: _options.pageClassName,
+                                       pageObjectID: _options.pageObjectId,
+                                       objectIDs: objectIds,
+                                       objectType: type
+                               }
+                       });
+               },
+               
+               /**
+                * Executes an editor action.
+                * 
+                * @param       {object}        event           event object
+                */
+               _executeAction: function(event) {
+                       var listItem = event.currentTarget;
+                       var data = _itemData.get(listItem);
+                       
+                       if (data.url) {
+                               window.location.href = data.url;
+                               return;
+                       }
+                       
+                       var triggerEvent = function() {
+                               var type = listItem.getAttribute('data-type');
+                               
+                               EventHandler.fire('com.woltlab.wcf.clipboard', type, {
+                                       data: data,
+                                       listItem: listItem,
+                                       responseData: null
+                               });
+                               
+                               if (typeof window.jQuery === 'function') {
+                                       window.jQuery(_editors.get(type)).trigger('clipboardAction', [ type, data.actionName, data.parameters ]);
+                               }
+                       };
+                       
+                       var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : '';
+                       var fireEvent = true;
+                       
+                       if (typeof data.parameters === 'object' && data.parameters.actionName && data.parameters.className) {
+                               if (data.parameters.actionName === 'unmarkAll' || Array.isArray(data.parameters.objectIDs)) {
+                                       if (confirmMessage.length) {
+                                               var template = (typeof data.internalData.template === 'string') ? data.internalData.template : '';
+                                               
+                                               UIConfirmation.show({
+                                                       confirm: (function() {
+                                                               var formData = {};
+                                                               
+                                                               if (template.length) {
+                                                                       var items = UIConfirmation.getContentElement().querySelectorAll('input, select, textarea');
+                                                                       for (var i = 0, length = items.length; i < length; i++) {
+                                                                               var item = items[i];
+                                                                               var name = item.getAttribute('name');
+                                                                               
+                                                                               switch (item.nodeName) {
+                                                                                       case 'INPUT':
+                                                                                               if (item.checked) {
+                                                                                                       formData[name] = item.getAttribute('value');
+                                                                                               }
+                                                                                               break;
+                                                                                       
+                                                                                       case 'SELECT':
+                                                                                               formData[name] = item.value;
+                                                                                               break;
+                                                                                       
+                                                                                       case 'TEXTAREA':
+                                                                                               formData[name] = item.value.trim();
+                                                                                               break;
+                                                                               }
+                                                                       }
+                                                               }
+                                                               
+                                                               this._executeProxyAction(listItem, data, formData);
+                                                       }).bind(this),
+                                                       message: confirmMessage,
+                                                       template: template
+                                               });
+                                       }
+                                       else {
+                                               this._executeProxyAction(listItem, data);
+                                       }
+                               }
+                       }
+                       else if (confirmMessage.length) {
+                               fireEvent = false;
+                               
+                               UIConfirmation.show({
+                                       confirm: triggerEvent,
+                                       message: confirmMessage
+                               });
+                       }
+                       
+                       if (fireEvent) {
+                               triggerEvent();
+                       }
+               },
+               
+               /**
+                * Forwards clipboard actions to an individual handler.
+                * 
+                * @param       {Element}               listItem        dropdown item element
+                * @param       {object<string, *>}     data            action data
+                * @param       {object<string, *>=}    formData        form data
+                */
+               _executeProxyAction: function(listItem, data, formData) {
+                       formData = formData || {};
+                       
+                       var objectIds = (data.parameters.actionName !== 'unmarkAll') ? data.parameters.objectIDs : [];
+                       var parameters = { data: formData };
+                       
+                       if (typeof data.internalData.parameters === 'object') {
+                               for (var key in data.internalData.parameters) {
+                                       if (data.internalData.parameters.hasOwnProperty(key)) {
+                                               parameters[key] = data.internalData.parameters[key];
+                                       }
+                               }
+                       }
+                       
+                       Ajax.api(this, {
+                               actionName: data.parameters.actionName,
+                               className: data.parameters.className,
+                               objectIDs: objectIds,
+                               parameters: parameters
+                       }, (function(responseData) {
+                               if (data.actionName !== 'unmarkAll') {
+                                       var type = listItem.getAttribute('data-type');
+                                       
+                                       EventHandler.fire('com.woltlab.wcf.clipboard', type, {
+                                               data: data,
+                                               listItem: listItem,
+                                               responseData: responseData
+                                       });
+                                       
+                                       if (typeof window.jQuery === 'function') {
+                                               window.jQuery(_editors.get(type)).trigger('clipboardActionResponse', [ responseData, type, data.actionName, data.parameters ]);
+                                       }
+                               }
+                               
+                               this._loadMarkedItems();
+                       }).bind(this));
+               },
+               
+               /**
+                * Unmarks all clipboard items for an object type.
+                * 
+                * @param       {object}        event           event object
+                */
+               _unmarkAll: function(event) {
+                       var type = event.currentTarget.getAttribute('data-type');
+                       
+                       Ajax.api(this, {
+                               actionName: 'unmarkAll',
+                               parameters: {
+                                       objectType: type
+                               }
+                       });
+               },
+               
+               /**
+                * Sets up ajax request object.
+                * 
+                * @return      {object}        request options
+                */
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       className: 'wcf\\data\\clipboard\\item\\ClipboardItemAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                * 
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       if (data.actionName === 'unmarkAll') {
+                               _containers.forEach((function(containerData) {
+                                       if (containerData.element.getAttribute('data-type') === data.returnValues.objectType) {
+                                               var clipboardObjects = containerData.element.getElementsByClassName('jsMarked');
+                                               while (clipboardObjects.length) {
+                                                       clipboardObjects[0].classList.remove('jsMarked');
+                                               }
+                                               
+                                               containerData.markAll.checked = false;
+                                               for (var i = 0, length = containerData.checkboxes.length; i < length; i++) {
+                                                       containerData.checkboxes[i].checked = false;
+                                               }
+                                               
+                                               _editors.get(data.returnValues.objectType).innerHTML = '';
+                                       }
+                               }).bind(this));
+                               
+                               return;
+                       }
+                       
+                       // clear editors
+                       _editors.forEach(function(editor) {
+                               editor.innerHTML = '';
+                       });
+                       _itemData = new ObjectMap();
+                       
+                       // rebuild markings
+                       _containers.forEach((function(containerData) {
+                               var typeName = containerData.element.getAttribute('data-type');
+                               
+                               var objectIds = (data.returnValues.markedItems.hasOwnProperty(typeName)) ? data.returnValues.markedItems[typeName] : [];
+                               this._rebuildMarkings(containerData, objectIds);
+                       }).bind(this));
+                       
+                       // no marked items
+                       if (!data.returnValues || !data.returnValues.items) {
+                               return;
+                       }
+                       
+                       // rebuild editors
+                       var fragment = document.createDocumentFragment();
+                       for (var typeName in data.returnValues.items) {
+                               if (!data.returnValues.items.hasOwnProperty(typeName) || !_editors.has(typeName)) {
+                                       continue;
+                               }
+                               
+                               var typeData = data.returnValues.items[typeName];
+                               
+                               var editor = _editors.get(typeName);
+                               var lists = DOMTraverse.childrenByTag('UL');
+                               var list = lists[0] || null;
+                               if (list === null) {
+                                       list = document.createElement('ul');
+                               }
+                               
+                               fragment.appendChild(list);
+                               
+                               var listItem = document.createElement('li');
+                               listItem.classList.add('dropdown');
+                               list.appendChild(listItem);
+                               
+                               var toggleButton = document.createElement('span');
+                               toggleButton.className = 'dropdownToggle button';
+                               toggleButton.textContent = typeData.label;
+                               listItem.appendChild(toggleButton);
+                               
+                               var itemList = document.createElement('ol');
+                               itemList.classList.add('dropdownMenu');
+                               
+                               // create editor items
+                               for (var itemIndex in typeData.items) {
+                                       if (!typeData.items.hasOwnProperty(itemIndex)) continue;
+                                       
+                                       var itemData = typeData.items[itemIndex];
+                                       
+                                       var item = document.createElement('li');
+                                       var label = document.createElement('span');
+                                       label.textContent = itemData.label;
+                                       item.appendChild(label);
+                                       itemList.appendChild(item);
+                                       
+                                       item.setAttribute('data-type', typeName);
+                                       item.addEventListener('click', _callbackItem);
+                                       
+                                       _itemData.set(item, itemData);
+                               }
+                               
+                               var divider = document.createElement('li');
+                               divider.classList.add('dropdownDivider');
+                               itemList.appendChild(divider);
+                               
+                               // add 'unmark all'
+                               var unmarkAll = document.createElement('li');
+                               unmarkAll.setAttribute('data-type', typeName);
+                               var label = document.createElement('span');
+                               label.textContent = Language.get('wcf.clipboard.item.unmarkAll');
+                               unmarkAll.appendChild(label);
+                               itemList.appendChild(unmarkAll);
+                               listItem.appendChild(itemList);
+                               
+                               unmarkAll.addEventListener('click', _callbackUnmarkAll);
+                               editor.appendChild(fragment);
+                               
+                               UISimpleDropdown.init(toggleButton, false);
+                       }
+               },
+               
+               /**
+                * Rebuilds the mark state for each item.
+                * 
+                * @param       {object<string, *>}     data            container data
+                * @param       {array<integer>}        objectIds       item object ids
+                */
+               _rebuildMarkings: function(data, objectIds) {
+                       var markAll = true;
+                       
+                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
+                               var checkbox = data.checkboxes[i];
+                               var clipboardObject = DOMTraverse.parentByClass(checkbox, 'jsClipboardObject');
+                               
+                               var isMarked = (objectIds.indexOf(~~checkbox.getAttribute('data-object-id')) !== -1);
+                               if (!isMarked) markAll = false;
+                               
+                               checkbox.checked = isMarked;
+                               clipboardObject.classList[(isMarked ? 'add' : 'remove')]('jsMarked');
+                       }
+                       
+                       data.markAll.checked = markAll;
+               }
+       };
+       
+       return ControllerClipboard;
+});
diff --git a/wcfsetup/install/files/lib/action/ClipboardAction.class.php b/wcfsetup/install/files/lib/action/ClipboardAction.class.php
deleted file mode 100644 (file)
index 14e27a7..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-<?php
-namespace wcf\action;
-use wcf\system\clipboard\ClipboardHandler;
-use wcf\system\exception\UserInputException;
-use wcf\system\WCF;
-use wcf\util\ArrayUtil;
-use wcf\util\JSON;
-use wcf\util\StringUtil;
-
-/**
- * Handles clipboard items.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    com.woltlab.wcf
- * @subpackage action
- * @category   Community Framework
- */
-class ClipboardAction extends AJAXInvokeAction {
-       /**
-        * clipboard action
-        * @var string
-        */
-       protected $action = '';
-       
-       /**
-        * list of allowed action methods
-        * @var array<string>
-        */
-       protected $allowedActions = array('mark', 'unmark', 'unmarkAll');
-       
-       /**
-        * container data
-        * @var array
-        */
-       protected $containerData = array();
-       
-       /**
-        * list of object ids
-        * @var array<integer>
-        */
-       protected $objectIDs = array();
-       
-       /**
-        * clipboard page class name
-        * @var string
-        */
-       protected $pageClassName = '';
-       
-       /**
-        * page object id
-        * @var integer
-        */
-       protected $pageObjectID = 0;
-       
-       /**
-        * object type
-        * @var string
-        */
-       protected $type = '';
-       
-       /**
-        * object type id
-        * @var integer
-        */
-       protected $objectTypeID = 0;
-       
-       /**
-        * @see \wcf\action\Action::readParameters()
-        */
-       public function readParameters() {
-               AbstractSecureAction::readParameters();
-               
-               if (isset($_POST['action'])) $this->action = StringUtil::trim($_POST['action']);
-               if (isset($_POST['containerData']) && is_array($_POST['containerData'])) $this->containerData = $_POST['containerData'];
-               if (isset($_POST['objectIDs']) && is_array($_POST['objectIDs'])) $this->objectIDs = ArrayUtil::toIntegerArray($_POST['objectIDs']);
-               if (isset($_POST['pageClassName'])) $this->pageClassName = StringUtil::trim($_POST['pageClassName']);
-               if (isset($_POST['pageObjectID'])) $this->pageObjectID = intval($_POST['pageObjectID']);
-               if (isset($_POST['type'])) $this->type = StringUtil::trim($_POST['type']);
-       }
-       
-       /**
-        * @see \wcf\action\Action::execute()
-        */
-       public function execute() {
-               AbstractSecureAction::execute();
-               
-               // execute clipboard action
-               $this->executeAction();
-               
-               // get editor items
-               $returnValues = $this->getEditorItems();
-               // send JSON response
-               header('Content-type: application/json');
-               echo JSON::encode($returnValues);
-               exit;
-       }
-       
-       /**
-        * Executes clipboard action.
-        */
-       protected function executeAction() {
-               // validate parameters
-               $this->validate();
-               
-               // execute action
-               if ($this->action == 'unmarkAll') {
-                       ClipboardHandler::getInstance()->unmarkAll($this->objectTypeID);
-               }
-               else {
-                       ClipboardHandler::getInstance()->{$this->action}($this->objectIDs, $this->objectTypeID);
-               }
-       }
-       
-       /**
-        * Returns a list of clipboard editor items grouped by type name.
-        * 
-        * @return      array<array>
-        */
-       protected function getEditorItems() {
-               $data = ClipboardHandler::getInstance()->getEditorItems($this->pageClassName, $this->pageObjectID, $this->containerData);
-               
-               if ($data === null) {
-                       return array();
-               }
-               
-               $editorItems = array();
-               foreach ($data as $typeName => $itemData) {
-                       $items = array(
-                               'label' => $itemData['label'],
-                               'items' => array()
-                       );
-                       
-                       foreach ($itemData['items'] as $showOrder => $item) {
-                               $items['items'][$showOrder] = array(
-                                       'actionName' => $item->getName(),
-                                       'internalData' => $item->getInternalData(),
-                                       'parameters' => $item->getParameters(),
-                                       'label' => WCF::getLanguage()->getDynamicVariable('wcf.clipboard.item.' . $item->getName(), array('count' => $item->getCount())),
-                                       'url' => $item->getURL()
-                               );
-                       }
-                       
-                       $editorItems[$typeName] = $items;
-               }
-               
-               return array(
-                       'action' => $this->action,
-                       'items' => $editorItems
-               );
-       }
-       
-       /**
-        * Validates parameters.
-        */
-       protected function validate() {
-               if (!in_array($this->action, $this->allowedActions)) {
-                       throw new UserInputException('action');
-               }
-               
-               if ($this->action != 'unmarkAll') {
-                       if (empty($this->objectIDs)) {
-                               throw new UserInputException('objectIDs');
-                       }
-                       
-                       if (empty($this->pageClassName)) {
-                               throw new UserInputException('pageClassName');
-                       }
-               }
-               
-               $this->objectTypeID = (!empty($this->type)) ? ClipboardHandler::getInstance()->getObjectTypeID($this->type) : null;
-               if ($this->objectTypeID === null) {
-                       throw new UserInputException('type');
-               }
-       }
-}
diff --git a/wcfsetup/install/files/lib/action/ClipboardLoadMarkedItemsAction.class.php b/wcfsetup/install/files/lib/action/ClipboardLoadMarkedItemsAction.class.php
deleted file mode 100644 (file)
index 3f90aab..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-namespace wcf\action;
-use wcf\system\clipboard\ClipboardHandler;
-
-/**
- * Handles marked clipboard items once DOM is loaded.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    com.woltlab.wcf
- * @subpackage action
- * @category   Community Framework
- */
-class ClipboardLoadMarkedItemsAction extends ClipboardAction {
-       /**
-        * @see \wcf\action\ClipboardAction::executeAction()
-        */
-       protected function executeAction() { }
-       
-       /**
-        * @see \wcf\action\ClipboardAction::getEditorItems()
-        */
-       protected function getEditorItems() {
-               $returnValues = parent::getEditorItems();
-               $returnValues['markedItems'] = array();
-               
-               // break if no items are available (status was cached by browser)
-               if (empty($returnValues['items'])) {
-                       return $returnValues;
-               }
-               
-               // load marked items from runtime cache
-               $data = ClipboardHandler::getInstance()->getMarkedItems();
-               
-               // insert object ids for each type of marked items
-               $returnValues['markedItems'] = array();
-               foreach ($data as $typeName => $itemData) {
-                       $returnValues['markedItems'][$typeName] = array_keys($itemData);
-               }
-               
-               return $returnValues;
-       }
-}
diff --git a/wcfsetup/install/files/lib/action/ClipboardProxyAction.class.php b/wcfsetup/install/files/lib/action/ClipboardProxyAction.class.php
deleted file mode 100644 (file)
index f77b414..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-<?php
-namespace wcf\action;
-use wcf\system\clipboard\ClipboardHandler;
-use wcf\system\exception\AJAXException;
-use wcf\system\exception\ValidateActionException;
-use wcf\util\ClassUtil;
-use wcf\util\JSON;
-use wcf\util\StringUtil;
-
-/**
- * Clipboard proxy implementation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    com.woltlab.wcf
- * @subpackage action
- * @category   Community Framework
- */
-class ClipboardProxyAction extends AbstractSecureAction {
-       /**
-        * IDatabaseObjectAction object
-        * @var \wcf\data\IDatabaseObjectAction
-        */
-       protected $objectAction = null;
-       
-       /**
-        * list of parameters
-        * @var array
-        */
-       protected $parameters = array();
-       
-       /**
-        * type name identifier
-        * @var string
-        */
-       protected $typeName = '';
-       
-       /**
-        * @see \wcf\action\IAction::__run()
-        */
-       public function __run() {
-               try {
-                       parent::__run();
-               }
-               catch (\Exception $e) {
-                       if ($e instanceof AJAXException) {
-                               throw $e;
-                       }
-                       else {
-                               throw new AJAXException($e->getMessage());
-                       }
-               }
-       }
-       
-       /**
-        * @see \wcf\action\IAction::readParameters()
-        */
-       public function readParameters() {
-               parent::readParameters();
-               
-               if (isset($_POST['parameters']) && is_array($_POST['parameters'])) $this->parameters = $_POST['parameters'];
-               if (isset($_POST['typeName'])) $this->typeName = StringUtil::trim($_POST['typeName']);
-       }
-       
-       /**
-        * Validates parameters.
-        */
-       protected function validate() {
-               // validate required parameters
-               if (!isset($this->parameters['className']) || empty($this->parameters['className'])) {
-                       throw new AJAXException("missing class name");
-               }
-               if (!isset($this->parameters['actionName']) || empty($this->parameters['actionName'])) {
-                       throw new AJAXException("missing action name");
-               }
-               if (empty($this->typeName)) {
-                       throw new AJAXException("type name cannot be empty");
-               }
-               
-               // validate class name
-               if (!class_exists($this->parameters['className'])) {
-                       throw new AJAXException("unknown class '".$this->parameters['className']."'");
-               }
-               if (!ClassUtil::isInstanceOf($this->parameters['className'], 'wcf\data\IDatabaseObjectAction')) {
-                       throw new AJAXException("'".$this->parameters['className']."' should implement wcf\system\IDatabaseObjectAction");
-               }
-       }
-       
-       /**
-        * Loads object ids from clipboard.
-        * 
-        * @return      array<integer>
-        */
-       protected function getObjectIDs() {
-               $typeID = ClipboardHandler::getInstance()->getObjectTypeID($this->typeName);
-               if ($typeID === null) {
-                       throw new AJAXException("clipboard item type '".$this->typeName."' is unknown");
-               }
-               
-               $objects = ClipboardHandler::getInstance()->getMarkedItems($typeID);
-               if (empty($objects) || !isset($objects[$this->typeName]) || empty($objects[$this->typeName])) {
-                       return null;
-               }
-               
-               return array_keys($objects[$this->typeName]);
-       }
-       
-       /**
-        * @see \wcf\action\IAction::execute()
-        */
-       public function execute() {
-               parent::execute();
-               
-               // get object ids
-               $objectIDs = $this->getObjectIDs();
-               
-               // create object action instance
-               $this->objectAction = new $this->parameters['className']($objectIDs, $this->parameters['actionName']);
-               
-               // validate action
-               try {
-                       $this->objectAction->validateAction();
-               }
-               catch (ValidateActionException $e) {
-                       throw new AJAXException("validation failed: ".$e->getMessage());
-               }
-               
-               // execute action
-               try {
-                       $this->response = $this->objectAction->executeAction();
-               }
-               catch (\Exception $e) {
-                       throw new AJAXException('unknown exception caught: '.$e->getMessage());
-               }
-               $this->executed();
-               
-               // send JSON-encoded response
-               header('Content-type: application/json');
-               echo JSON::encode($this->response);
-               exit;
-       }
-}
index ddf78a03dcdaa56f374bcf0ff26bef317e640384..7ba3107252853dfc61a79640f4f5e0fbb289087e 100644 (file)
@@ -6,10 +6,10 @@ use wcf\system\exception\SystemException;
 use wcf\system\exception\UserInputException;
 use wcf\system\request\RequestHandler;
 use wcf\system\WCF;
+use wcf\util\ArrayUtil;
 use wcf\util\ClassUtil;
 use wcf\util\JSON;
 use wcf\util\StringUtil;
-use wcf\util\ArrayUtil;
 
 /**
  * Default implementation for DatabaseObject-related actions.
diff --git a/wcfsetup/install/files/lib/data/clipboard/item/ClipboardItemAction.class.php b/wcfsetup/install/files/lib/data/clipboard/item/ClipboardItemAction.class.php
new file mode 100644 (file)
index 0000000..29a15c7
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+namespace wcf\data\clipboard\item;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\clipboard\ClipboardHandler;
+use wcf\system\event\EventHandler;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+
+/**
+ * Clipboard API handler.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.clipboard.item
+ * @category   Community Framework
+ */
+class ClipboardItemAction extends AbstractDatabaseObjectAction {
+       /**
+        * object type id
+        * @var integer
+        */
+       public $objectTypeID = 0;
+       
+       /**
+        * This is a heavily modified constructor which behaves differently from other DBOActions,
+        * primarily because this class just masquerades as a regular DBOAction.
+        * 
+        * @see \wcf\data\AbstractDatabaseObjectAction
+        */
+       public function __construct(array $objects, $action, array $parameters = array()) {
+               $this->action = $action;
+               $this->parameters = $parameters;
+               
+               // fire event action
+               EventHandler::getInstance()->fireAction($this, 'initializeAction');
+       }
+       
+       /**
+        * Validates parameters to set an item as marked.
+        */
+       public function validateMark() {
+               $this->validateDefaultParameters();
+               
+               $this->readIntegerArray('objectIDs');
+               
+               $this->readObjectType();
+       }
+       
+       /**
+        * Sets an item as marked.
+        * 
+        * @return      array<array>
+        */
+       public function mark() {
+               ClipboardHandler::getInstance()->mark($this->parameters['objectIDs'], $this->objectTypeID);
+               
+               return $this->getEditorItems();
+       }
+       
+       /**
+        * Validates parameters to unset an item as marked.
+        */
+       public function validateUnmark() {
+               $this->validateMark();
+       }
+       
+       /**
+        * Unsets an item as marked.
+        * 
+        * @return      array<array>
+        */
+       public function unmark() {
+               ClipboardHandler::getInstance()->unmark($this->parameters['objectIDs'], $this->objectTypeID);
+               
+               return $this->getEditorItems();
+       }
+       
+       /**
+        * Validates parameters to fetch the list of marked items.
+        */
+       public function validateGetMarkedItems() {
+               $this->validateDefaultParameters();
+       }
+       
+       /**
+        * Returns the list of marked items.
+        * 
+        * @return      array<array>
+        */
+       public function getMarkedItems() {
+               return $this->getEditorItems();
+       }
+       
+       /**
+        * Validates parameters to unmark all items of a type.
+        */
+       public function validateUnmarkAll() {
+               $this->readObjectType();
+       }
+       
+       /**
+        * Unmarks all items of a type.
+        * 
+        * @return      array<string>
+        */
+       public function unmarkAll() {
+               ClipboardHandler::getInstance()->unmarkAll($this->objectTypeID);
+               
+               return [ 'objectType' => $this->parameters['objectType'] ];
+       }
+       
+       /**
+        * Validates generic parameters used for most clipboard actions.
+        */
+       protected function validateDefaultParameters() {
+               $this->readString('pageClassName');
+               $this->readInteger('pageObjectID', true);
+       }
+       
+       /**
+        * Reads the object type and sets the internal object type id.
+        */
+       protected function readObjectType() {
+               $this->readString('objectType', false);
+               
+               if (!empty($this->parameters['objectType'])) {
+                       $this->objectTypeID = ClipboardHandler::getInstance()->getObjectTypeID($this->parameters['objectType']);
+                       if ($this->objectTypeID === null) {
+                               throw new UserInputException('objectType');
+                       }
+               }
+       }
+       
+       /**
+        * Returns a list of clipboard editor items grouped by type name.
+        *
+        * @return      array<array>
+        */
+       protected function getEditorItems() {
+               $data = ClipboardHandler::getInstance()->getEditorItems($this->parameters['pageClassName'], $this->parameters['pageObjectID']);
+               
+               if ($data === null) {
+                       return [];
+               }
+               
+               $editorItems = [];
+               foreach ($data as $typeName => $itemData) {
+                       $items = [
+                               'label' => $itemData['label'],
+                               'items' => []
+                       ];
+                       
+                       foreach ($itemData['items'] as $showOrder => $item) {
+                               $items['items'][$showOrder] = [
+                                       'actionName' => $item->getName(),
+                                       'internalData' => $item->getInternalData(),
+                                       'parameters' => $item->getParameters(),
+                                       'label' => WCF::getLanguage()->getDynamicVariable('wcf.clipboard.item.' . $item->getName(), [ 'count' => $item->getCount() ]),
+                                       'url' => $item->getURL()
+                               ];
+                       }
+                       
+                       $editorItems[$typeName] = $items;
+               }
+               
+               $returnValues = [
+                       'action' => $this->action,
+                       'items' => $editorItems,
+                       'markedItems' => []
+               ];
+               
+               // break if no items are available (status was cached by browser)
+               if (empty($returnValues['items'])) {
+                       return $returnValues;
+               }
+               
+               // load marked items from runtime cache
+               $data = ClipboardHandler::getInstance()->getMarkedItems();
+               
+               // insert object ids for each type of marked items
+               $returnValues['markedItems'] = [];
+               foreach ($data as $typeName => $itemData) {
+                       $returnValues['markedItems'][$typeName] = array_keys($itemData);
+               }
+               
+               return $returnValues;
+       }
+}
index 2ed188977b8aab4113d8297f9db4cd92170fd6ab..6d3882f1b60d7123664902b428bd86a3db481269 100644 (file)
@@ -289,10 +289,9 @@ class ClipboardHandler extends SingletonFactory {
         * 
         * @param       string          $page
         * @param       integer         $pageObjectID
-        * @param       array           $containerData
         * @return      array<array>
         */
-       public function getEditorItems($page, $pageObjectID, $containerData) {
+       public function getEditorItems($page, $pageObjectID) {
                $this->pageObjectID = 0;
                
                // ignore unknown pages
@@ -341,24 +340,15 @@ class ClipboardHandler extends SingletonFactory {
                        $typeName = $actionData['object']->getTypeName();
                        if (!isset($this->markedItems[$typeName]) || empty($this->markedItems[$typeName])) continue;
                        
-                       $typeData = array();
-                       if (isset($containerData[$typeName])) {
-                               $typeData = $containerData[$typeName];
-                       }
-                       
-                       // filter objects by type data
-                       $objects = $actionData['object']->filterObjects($this->markedItems[$typeName], $typeData);
-                       if (empty($objects)) continue;
-                       
                        if (!isset($editorData[$typeName])) {
                                $editorData[$typeName] = array(
-                                       'label' => $actionData['object']->getEditorLabel($objects),
+                                       'label' => $actionData['object']->getEditorLabel($this->markedItems[$typeName]),
                                        'items' => array()
                                );
                        }
                        
                        foreach ($actionData['actions'] as $actionObject) {
-                               $data = $actionData['object']->execute($objects, $actionObject);
+                               $data = $actionData['object']->execute($this->markedItems[$typeName], $actionObject);
                                if ($data === null) {
                                        continue;
                                }
index 198b7f79843fabe67a1afccdf7eda9c932180601..378346456e8cb3069d785febe6aadef79e723df4 100644 (file)
@@ -67,13 +67,6 @@ abstract class AbstractClipboardAction implements IClipboardAction {
                return $item;
        }
        
-       /**
-        * @see \wcf\system\clipboard\action\IClipboardAction::filterObjects()
-        */
-       public function filterObjects(array $objects, array $typeData) {
-               return $objects;
-       }
-       
        /**
         * @see \wcf\system\clipboard\action\IClipboardAction::getEditorLabel()
         */
index 48f1f05374b5ac1c04ff700e63b1570affa22ca3..0a035faea0a6807d23dd952f6ea12aab73c2b7e7 100644 (file)
@@ -30,16 +30,6 @@ interface IClipboardAction {
         */
        public function execute(array $objects, ClipboardAction $action);
        
-       /**
-        * Filters the given objects by the given type data and returns the filtered
-        * list.
-        * 
-        * @param       array           $objects
-        * @param       array           $typeData
-        * @return      array
-        */
-       public function filterObjects(array $objects, array $typeData);
-       
        /**
         * Returns action class name.
         *