Merge branch '3.0' into master
authorJoshua Rüsweg <ruesweg@woltlab.com>
Fri, 12 Apr 2019 21:28:09 +0000 (23:28 +0200)
committerJoshua Rüsweg <ruesweg@woltlab.com>
Fri, 12 Apr 2019 21:28:09 +0000 (23:28 +0200)
1  2 
tmp_WCF.InlineEditor.js
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/InlineEditor.js
wcfsetup/install/files/lib/system/dialog/TestDialog.class.php

index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..1bf0b8e6af8025983f3153f9a8678977109a0dfa
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,342 @@@
++/**
++ * Default implementation for inline editors.
++ *
++ * @param        string                elementSelector
++ */
++WCF.InlineEditor = Class.extend({
++      /**
++       * list of registered callbacks
++       * @var        array<object>
++       */
++      _callbacks: [],
++      
++      /**
++       * list of dropdown selections
++       * @var        object
++       */
++      _dropdowns: {},
++      
++      /**
++       * list of container elements
++       * @var        object
++       */
++      _elements: {},
++      
++      /**
++       * notification object
++       * @var        WCF.System.Notification
++       */
++      _notification: null,
++      
++      /**
++       * list of known options
++       * @var        array<object>
++       */
++      _options: [],
++      
++      /**
++       * action proxy
++       * @var        WCF.Action.Proxy
++       */
++      _proxy: null,
++      
++      /**
++       * list of trigger elements by element id
++       * @var        object<object>
++       */
++      _triggerElements: {},
++      
++      /**
++       * list of data to update upon success
++       * @var        array<object>
++       */
++      _updateData: [],
++      
++      /**
++       * Initializes a new inline editor.
++       */
++      init: function (elementSelector) {
++              var $elements = $(elementSelector);
++              if (!$elements.length) {
++                      return;
++              }
++              
++              this._setOptions();
++              var $quickOption = '';
++              for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
++                      if (this._options[$i].isQuickOption) {
++                              $quickOption = this._options[$i].optionName;
++                              break;
++                      }
++              }
++              
++              var self = this;
++              $elements.each(function (index, element) {
++                      var $element = $(element);
++                      var $elementID = $element.wcfIdentify();
++                      
++                      // find trigger element
++                      var $trigger = self._getTriggerElement($element);
++                      if ($trigger === null || $trigger.length !== 1) {
++                              return;
++                      }
++                      
++                      $trigger.on(WCF_CLICK_EVENT, $.proxy(self._show, self)).data('elementID', $elementID);
++                      if ($quickOption) {
++                              // simulate click on target action
++                              $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self));
++                      }
++                      
++                      // store reference
++                      self._elements[$elementID] = $element;
++              });
++              
++              this._proxy = new WCF.Action.Proxy({
++                      success: $.proxy(this._success, this)
++              });
++              
++              WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this));
++              
++              this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
++      },
++      
++      /**
++       * Closes all inline editors.
++       */
++      _closeAll: function () {
++              for (var $elementID in this._elements) {
++                      this._hide($elementID);
++              }
++      },
++      
++      /**
++       * Sets options for this inline editor.
++       */
++      _setOptions: function () {
++              this._options = [];
++      },
++      
++      /**
++       * Register an option callback for validation and execution.
++       *
++       * @param        object                callback
++       */
++      registerCallback: function (callback) {
++              if ($.isFunction(callback)) {
++                      this._callbacks.push(callback);
++              }
++      },
++      
++      /**
++       * Returns the triggering element.
++       *
++       * @param        jQuery                element
++       * @return        jQuery
++       */
++      _getTriggerElement: function (element) {
++              return null;
++      },
++      
++      /**
++       * Shows a dropdown menu if options are available.
++       *
++       * @param        object                event
++       */
++      _show: function (event) {
++              event.preventDefault();
++              var $elementID = $(event.currentTarget).data('elementID');
++              
++              // build dropdown
++              var $trigger = null;
++              if (!this._dropdowns[$elementID]) {
++                      this._triggerElements[$elementID] = $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle');
++                      var parent = $trigger[0].parentNode;
++                      if (parent && parent.nodeName === 'LI' && parent.childElementCount === 1) {
++                              // do not add a wrapper element if the trigger is the only child
++                              parent.classList.add('dropdown');
++                      }
++                      else {
++                              $trigger.wrap('<span class="dropdown" />');
++                      }
++                      
++                      this._dropdowns[$elementID] = $('<ul class="dropdownMenu" />').insertAfter($trigger);
++              }
++              this._dropdowns[$elementID].empty();
++              
++              // validate options
++              var $hasOptions = false;
++              var $lastElementType = '';
++              for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
++                      var $option = this._options[$i];
++                      
++                      if ($option.optionName === 'divider') {
++                              if ($lastElementType !== '' && $lastElementType !== 'divider') {
++                                      $('<li class="dropdownDivider" />').appendTo(this._dropdowns[$elementID]);
++                                      $lastElementType = $option.optionName;
++                              }
++                      }
++                      else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) {
++                              var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]);
++                              $listItem.data('elementID', $elementID).data('optionName', $option.optionName).data('isQuickOption', ($option.isQuickOption ? true : false)).click($.proxy(this._click, this));
++                              
++                              $hasOptions = true;
++                              $lastElementType = $option.optionName;
++                      }
++              }
++              
++              if ($hasOptions) {
++                      // if last child is divider, remove it
++                      var $lastChild = this._dropdowns[$elementID].children().last();
++                      if ($lastChild.hasClass('dropdownDivider')) {
++                              $lastChild.remove();
++                      }
++                      
++                      // check if only element is a quick option
++                      var $quickOption = null;
++                      var $count = 0;
++                      this._dropdowns[$elementID].children().each(function (index, child) {
++                              var $child = $(child);
++                              if (!$child.hasClass('dropdownDivider')) {
++                                      if ($child.data('isQuickOption')) {
++                                              $quickOption = $child;
++                                      }
++                                      else {
++                                              $count++;
++                                      }
++                              }
++                      });
++                      
++                      if (!$count) {
++                              $quickOption.trigger('click');
++                              
++                              if (this._triggerElements[$elementID]) {
++                                      WCF.Dropdown.close(this._triggerElements[$elementID].parents('.dropdown').wcfIdentify());
++                              }
++                              
++                              return false;
++                      }
++              }
++              
++              if ($trigger !== null) {
++                      WCF.Dropdown.initDropdown($trigger, true);
++              }
++              
++              return false;
++      },
++      
++      /**
++       * Validates an option.
++       *
++       * @param        string                elementID
++       * @param        string                optionName
++       * @returns        boolean
++       */
++      _validate: function (elementID, optionName) {
++              return false;
++      },
++      
++      /**
++       * Validates an option provided by callbacks.
++       *
++       * @param        string                elementID
++       * @param        string                optionName
++       * @return        boolean
++       */
++      _validateCallbacks: function (elementID, optionName) {
++              var $length = this._callbacks.length;
++              if ($length) {
++                      for (var $i = 0; $i < $length; $i++) {
++                              if (this._callbacks[$i].validate(this._elements[elementID], optionName)) {
++                                      return true;
++                              }
++                      }
++              }
++              
++              return false;
++      },
++      
++      /**
++       * Handles AJAX responses.
++       *
++       * @param        object                data
++       * @param        string                textStatus
++       * @param        jQuery                jqXHR
++       */
++      _success: function (data, textStatus, jqXHR) {
++              var $length = this._updateData.length;
++              if (!$length) {
++                      return;
++              }
++              
++              this._updateState(data);
++              
++              this._updateData = [];
++      },
++      
++      /**
++       * Update element states based upon update data.
++       *
++       * @param        object                data
++       */
++      _updateState: function (data) {
++      },
++      
++      /**
++       * Handles clicks within dropdown.
++       *
++       * @param        object                event
++       */
++      _click: function (event) {
++              var $listItem = $(event.currentTarget);
++              var $elementID = $listItem.data('elementID');
++              var $optionName = $listItem.data('optionName');
++              
++              if (!this._execute($elementID, $optionName)) {
++                      this._executeCallback($elementID, $optionName);
++              }
++              
++              this._hide($elementID);
++      },
++      
++      /**
++       * Executes actions associated with an option.
++       *
++       * @param        string                elementID
++       * @param        string                optionName
++       * @return        boolean
++       */
++      _execute: function (elementID, optionName) {
++              return false;
++      },
++      
++      /**
++       * Executes actions associated with an option provided by callbacks.
++       *
++       * @param        string                elementID
++       * @param        string                optionName
++       * @return        boolean
++       */
++      _executeCallback: function (elementID, optionName) {
++              var $length = this._callbacks.length;
++              if ($length) {
++                      for (var $i = 0; $i < $length; $i++) {
++                              if (this._callbacks[$i].execute(this._elements[elementID], optionName)) {
++                                      return true;
++                              }
++                      }
++              }
++              
++              return false;
++      },
++      
++      /**
++       * Hides a dropdown menu.
++       *
++       * @param        string                elementID
++       */
++      _hide: function (elementID) {
++              if (this._dropdowns[elementID]) {
++                      this._dropdowns[elementID].empty().removeClass('dropdownOpen');
++              }
++      }
++});
index d022e2c0d9b0bca9d3903fcbe02d9a25074dd8a9,f49a57435e937dde5890f5b085a513fd336f7928..af23e8e9de52865aea482e90e5be07c732bbbe9d
@@@ -5321,740 -5217,787 +5321,765 @@@ WCF.System.Event = 
        }
  };
  
 -/**
 - * Worker support for frontend based upon DatabaseObjectActions.
 - * 
 - * @param     string          className
 - * @param     string          title
 - * @param     object          parameters
 - * @param     object          callback
 - */
 -WCF.System.Worker = Class.extend({
 -      /**
 -       * worker aborted
 -       * @var boolean
 -       */
 -      _aborted: false,
 -      
 -      /**
 -       * DBOAction method name
 -       * @var string
 -       */
 -      _actionName: '',
 -      
 -      /**
 -       * callback invoked after worker completed
 -       * @var object
 -       */
 -      _callback: null,
 -      
 -      /**
 -       * DBOAction class name
 -       * @var string
 -       */
 -      _className: '',
 -      
 -      /**
 -       * dialog object
 -       * @var jQuery
 -       */
 -      _dialog: null,
 -      
 -      /**
 -       * action proxy
 -       * @var WCF.Action.Proxy
 -       */
 -      _proxy: null,
 -      
 -      /**
 -       * dialog title
 -       * @var string
 -       */
 -      _title: '',
 -      
 -      /**
 -       * Initializes a new worker instance.
 -       * 
 -       * @param       string          actionName
 -       * @param       string          className
 -       * @param       string          title
 -       * @param       object          parameters
 -       * @param       object          callback
 -       * @param       object          confirmMessage
 -       */
 -      init: function(actionName, className, title, parameters, callback) {
 -              this._aborted = false;
 -              this._actionName = actionName;
 -              this._callback = callback || null;
 -              this._className = className;
 -              this._dialog = null;
 -              this._proxy = new WCF.Action.Proxy({
 -                      autoSend: true,
 -                      data: {
 -                              actionName: this._actionName,
 -                              className: this._className,
 -                              parameters: parameters || { }
 -                      },
 -                      showLoadingOverlay: false,
 -                      success: $.proxy(this._success, this)
 -              });
 -              this._title = title;
 -      },
 -      
 +if (COMPILER_TARGET_DEFAULT) {
        /**
 -       * Handles response from server.
 -       * 
 -       * @param       object          data
 +       * Worker support for frontend based upon DatabaseObjectActions.
 +       *
 +       * @param        string                className
 +       * @param        string                title
 +       * @param        object                parameters
 +       * @param        object                callback
         */
 -      _success: function(data) {
 -              // init binding
 -              if (this._dialog === null) {
 -                      this._dialog = $('<div />').hide().appendTo(document.body);
 -                      this._dialog.wcfDialog({
 -                              closeConfirmMessage: WCF.Language.get('wcf.worker.abort.confirmMessage'),
 -                              closeViaModal: false,
 -                              onClose: $.proxy(function() {
 -                                      this._aborted = true;
 -                                      this._proxy.abortPrevious();
 -                                      
 -                                      window.location.reload();
 -                              }, this),
 -                              title: this._title
 -                      });
 -              }
 +      WCF.System.Worker = Class.extend({
 +              /**
 +               * worker aborted
 +               * @var        boolean
 +               */
 +              _aborted: false,
                
 -              if (this._aborted) {
 -                      return;
 -              }
 +              /**
 +               * DBOAction method name
 +               * @var        string
 +               */
 +              _actionName: '',
                
 -              if (data.returnValues.template) {
 -                      this._dialog.html(data.returnValues.template);
 -              }
 +              /**
 +               * callback invoked after worker completed
 +               * @var        object
 +               */
 +              _callback: null,
                
 -              // update progress
 -              this._dialog.find('progress').attr('value', data.returnValues.progress).text(data.returnValues.progress + '%').next('span').text(data.returnValues.progress + '%');
 +              /**
 +               * DBOAction class name
 +               * @var        string
 +               */
 +              _className: '',
                
 -              // worker is still busy with its business, carry on
 -              if (data.returnValues.progress < 100) {
 -                      // send request for next loop
 -                      var $parameters = data.returnValues.parameters || { };
 -                      $parameters.loopCount = data.returnValues.loopCount;
 -                      
 -                      this._proxy.setOption('data', {
 -                              actionName: this._actionName,
 -                              className: this._className,
 -                              parameters: $parameters
 -                      });
 -                      this._proxy.sendRequest();
 -              }
 -              else if (this._callback !== null) {
 -                      this._callback(this, data);
 -              }
 -              else {
 -                      // exchange icon
 -                      this._dialog.find('.fa-spinner').removeClass('fa-spinner').addClass('fa-check green');
 -                      this._dialog.find('.contentHeader h1').text(WCF.Language.get('wcf.global.worker.completed'));
 -                      
 -                      // display continue button
 -                      var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
 -                      $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.button.next') + '</button>').appendTo($formSubmit).focus().click(function() {
 -                              if (data.returnValues.redirectURL) {
 -                                      window.location = data.returnValues.redirectURL;
 -                              }
 -                              else {
 -                                      window.location.reload();
 -                              }
 +              /**
 +               * dialog object
 +               * @var        jQuery
 +               */
 +              _dialog: null,
 +              
 +              /**
 +               * action proxy
 +               * @var        WCF.Action.Proxy
 +               */
 +              _proxy: null,
 +              
 +              /**
 +               * dialog title
 +               * @var        string
 +               */
 +              _title: '',
 +              
 +              /**
 +               * Initializes a new worker instance.
 +               *
 +               * @param        string                actionName
 +               * @param        string                className
 +               * @param        string                title
 +               * @param        object                parameters
 +               * @param        object                callback
 +               * @param        object                confirmMessage
 +               */
 +              init: function (actionName, className, title, parameters, callback) {
 +                      this._aborted = false;
 +                      this._actionName = actionName;
 +                      this._callback = callback || null;
 +                      this._className = className;
 +                      this._dialog = null;
 +                      this._proxy = new WCF.Action.Proxy({
 +                              autoSend: true,
 +                              data: {
 +                                      actionName: this._actionName,
 +                                      className: this._className,
 +                                      parameters: parameters || {}
 +                              },
 +                              showLoadingOverlay: false,
 +                              success: $.proxy(this._success, this)
                        });
 +                      this._title = title;
 +              },
 +              
 +              /**
 +               * Handles response from server.
 +               *
 +               * @param        object                data
 +               */
 +              _success: function (data) {
 +                      // init binding
 +                      if (this._dialog === null) {
 +                              this._dialog = $('<div />').hide().appendTo(document.body);
 +                              this._dialog.wcfDialog({
 +                                      closeConfirmMessage: WCF.Language.get('wcf.worker.abort.confirmMessage'),
 +                                      closeViaModal: false,
 +                                      onClose: $.proxy(function () {
 +                                              this._aborted = true;
 +                                              this._proxy.abortPrevious();
 +                                              
 +                                              window.location.reload();
 +                                      }, this),
 +                                      title: this._title
 +                              });
 +                      }
                        
 -                      this._dialog.wcfDialog('render');
 -              }
 -      }
 -});
 -
 -/**
 - * Default implementation for inline editors.
 - * 
 - * @param     string          elementSelector
 - */
 -WCF.InlineEditor = Class.extend({
 -      /**
 -       * list of registered callbacks
 -       * @var array<object>
 -       */
 -      _callbacks: [ ],
 -      
 -      /**
 -       * list of dropdown selections
 -       * @var object
 -       */
 -      _dropdowns: { },
 -      
 -      /**
 -       * list of container elements
 -       * @var object
 -       */
 -      _elements: { },
 -      
 -      /**
 -       * notification object
 -       * @var WCF.System.Notification
 -       */
 -      _notification: null,
 -      
 -      /**
 -       * list of known options
 -       * @var array<object>
 -       */
 -      _options: [ ],
 -      
 -      /**
 -       * action proxy
 -       * @var WCF.Action.Proxy
 -       */
 -      _proxy: null,
 -      
 -      /**
 -       * list of trigger elements by element id
 -       * @var object<object>
 -       */
 -      _triggerElements: { },
 -      
 -      /**
 -       * list of data to update upon success
 -       * @var array<object>
 -       */
 -      _updateData: [ ],
 -      
 -      /**
 -       * element selector
 -       * @var         string
 -       */
 -      _elementSelector: null,
 -      
 -      /**
 -       * quick option for the inline editor
 -       * @var         string
 -       */
 -      _quickOption: null,
 +                      if (this._aborted) {
 +                              return;
 +                      }
 +                      
 +                      if (data.returnValues.template) {
 +                              this._dialog.html(data.returnValues.template);
 +                      }
 +                      
 +                      // update progress
 +                      this._dialog.find('progress').attr('value', data.returnValues.progress).text(data.returnValues.progress + '%').next('span').text(data.returnValues.progress + '%');
 +                      
 +                      // worker is still busy with its business, carry on
 +                      if (data.returnValues.progress < 100) {
 +                              // send request for next loop
 +                              var $parameters = data.returnValues.parameters || {};
 +                              $parameters.loopCount = data.returnValues.loopCount;
 +                              
 +                              this._proxy.setOption('data', {
 +                                      actionName: this._actionName,
 +                                      className: this._className,
 +                                      parameters: $parameters
 +                              });
 +                              this._proxy.sendRequest();
 +                      }
 +                      else if (this._callback !== null) {
 +                              this._callback(this, data);
 +                      }
 +                      else {
 +                              // exchange icon
 +                              this._dialog.find('.fa-spinner').removeClass('fa-spinner').addClass('fa-check green');
 +                              this._dialog.find('.contentHeader h1').text(WCF.Language.get('wcf.global.worker.completed'));
 +                              
 +                              // display continue button
 +                              var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
 +                              $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.button.next') + '</button>').appendTo($formSubmit).focus().click(function () {
 +                                      if (data.returnValues.redirectURL) {
 +                                              window.location = data.returnValues.redirectURL;
 +                                      }
 +                                      else {
 +                                              window.location.reload();
 +                                      }
 +                              });
 +                              
 +                              this._dialog.wcfDialog('render');
 +                      }
 +              }
 +      });
        
        /**
 -       * Initializes a new inline editor.
 +       * Default implementation for inline editors.
 +       *
 +       * @param        string                elementSelector
         */
 -      init: function(elementSelector) {
 -              this._elementSelector = elementSelector;
 +      WCF.InlineEditor = Class.extend({
 +              /**
 +               * list of registered callbacks
 +               * @var        array<object>
 +               */
 +              _callbacks: [],
                
 -              var $elements = $(elementSelector);
 -              if (!$elements.length) {
 -                      return;
 -              }
 +              /**
 +               * list of dropdown selections
 +               * @var        object
 +               */
 +              _dropdowns: {},
                
 -              this._setOptions();
 -              for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
 -                      if (this._options[$i].isQuickOption) {
 -                              this._quickOption = this._options[$i].optionName;
 -                              break;
 -                      }
 -              }
 +              /**
 +               * list of container elements
 +               * @var        object
 +               */
 +              _elements: {},
                
 -              this.rebuild();
 +              /**
 +               * notification object
 +               * @var        WCF.System.Notification
 +               */
 +              _notification: null,
                
 -              WCF.DOMNodeInsertedHandler.addCallback('WCF.InlineEditor' + this._elementSelector.hashCode(), $.proxy(this.rebuild, this));
 +              /**
 +               * list of known options
 +               * @var        array<object>
 +               */
 +              _options: [],
                
 -              this._proxy = new WCF.Action.Proxy({
 -                      success: $.proxy(this._success, this)
 -              });
 +              /**
 +               * action proxy
 +               * @var        WCF.Action.Proxy
 +               */
 +              _proxy: null,
                
 -              WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this));
 +              /**
 +               * list of trigger elements by element id
 +               * @var        object<object>
 +               */
 +              _triggerElements: {},
                
 -              this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
 -      },
 -      
 -      /**
 -       * Identify new elements and adds the event listeners to them.
 -       */
 -      rebuild: function() {
 -              var $elements = $(this._elementSelector);
 -              var self = this;
 -              $elements.each(function (index, element) {
 -                      var $element = $(element);
 -                      var $elementID = $element.wcfIdentify();
 +              /**
 +               * list of data to update upon success
 +               * @var        array<object>
 +               */
 +              _updateData: [],
 +              
++              /**
++               * element selector
++               * @var         string
++               */
++              _elementSelector: null,
++              
++              /**
++               * quick option for the inline editor
++               * @var         string
++               */
++              _quickOption: null,
++              
 +              /**
 +               * Initializes a new inline editor.
 +               */
 +              init: function (elementSelector) {
++                      this._elementSelector = elementSelector;
+                       
 -                      if (self._elements[$elementID] === undefined) {
 -                              // find trigger element
 -                              var $trigger = self._getTriggerElement($element);
 -                              if ($trigger === null || $trigger.length !== 1) {
 -                                      return;
 +                      var $elements = $(elementSelector);
 +                      if (!$elements.length) {
 +                              return;
 +                      }
 +                      
 +                      this._setOptions();
-                       var $quickOption = '';
 +                      for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
 +                              if (this._options[$i].isQuickOption) {
-                                       $quickOption = this._options[$i].optionName;
++                                      this._quickOption = this._options[$i].optionName;
 +                                      break;
                                }
-                       var self = this;
-                       $elements.each(function (index, element) {
-                               var $element = $(element);
-                               var $elementID = $element.wcfIdentify();
-                               
-                               // find trigger element
-                               var $trigger = self._getTriggerElement($element);
-                               if ($trigger === null || $trigger.length !== 1) {
-                                       return;
-                               }
-                               
-                               $trigger.on(WCF_CLICK_EVENT, $.proxy(self._show, self)).data('elementID', $elementID);
-                               if ($quickOption) {
-                                       // simulate click on target action
-                                       $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self));
-                               }
-                               
-                               // store reference
-                               self._elements[$elementID] = $element;
-                       });
 +                      }
 +                      
 -                              $trigger.on(WCF_CLICK_EVENT, $.proxy(self._show, self)).data('elementID', $elementID);
 -                              if (this._quickOption) {
 -                                      // simulate click on target action
 -                                      $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self));
++                      this.rebuild();
++                      
++                      WCF.DOMNodeInsertedHandler.addCallback('WCF.InlineEditor' + this._elementSelector.hashCode(), $.proxy(this.rebuild, this));
 +                      
 +                      this._proxy = new WCF.Action.Proxy({
 +                              success: $.proxy(this._success, this)
 +                      });
 +                      
 +                      WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this));
 +                      
 +                      this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
 +              },
 +              
++              /**
++               * Identify new elements and adds the event listeners to them.
++               */
++              rebuild: function() {
++                      var $elements = $(this._elementSelector);
++                      var self = this;
++                      $elements.each(function (index, element) {
++                              var $element = $(element);
++                              var $elementID = $element.wcfIdentify();
+                               
 -                              
 -                              // store reference
 -                              self._elements[$elementID] = $element;
 -                      }
 -              });
 -      },
 -      
 -      /**
 -       * Closes all inline editors.
 -       */
 -      _closeAll: function() {
 -              for (var $elementID in this._elements) {
 -                      this._hide($elementID);
 -              }
 -      },
 -      
 -      /**
 -       * Sets options for this inline editor.
 -       */
 -      _setOptions: function() {
 -              this._options = [ ];
 -      },
 -      
 -      /**
 -       * Register an option callback for validation and execution.
 -       * 
 -       * @param       object          callback
 -       */
 -      registerCallback: function(callback) {
 -              if ($.isFunction(callback)) {
 -                      this._callbacks.push(callback);
 -              }
 -      },
 -      
 -      /**
 -       * Returns the triggering element.
 -       * 
 -       * @param       jQuery          element
 -       * @return      jQuery
 -       */
 -      _getTriggerElement: function(element) {
 -              return null;
 -      },
 -      
 -      /**
 -       * Shows a dropdown menu if options are available.
 -       * 
 -       * @param       object          event
 -       */
 -      _show: function(event) {
 -              event.preventDefault();
 -              var $elementID = $(event.currentTarget).data('elementID');
++                              if (self._elements[$elementID] === undefined) {
++                                      // find trigger element
++                                      var $trigger = self._getTriggerElement($element);
++                                      if ($trigger === null || $trigger.length !== 1) {
++                                              return;
++                                      }
++                                      
++                                      $trigger.on(WCF_CLICK_EVENT, $.proxy(self._show, self)).data('elementID', $elementID);
++                                      if (this._quickOption) {
++                                              // simulate click on target action
++                                              $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self));
++                                      }
++                                      
++                                      // store reference
++                                      self._elements[$elementID] = $element;
+                               }
 -              // build dropdown
 -              var $trigger = null;
 -              if (!this._dropdowns[$elementID]) {
 -                      this._triggerElements[$elementID] = $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle');
 -                      var parent = $trigger[0].parentNode;
 -                      if (parent && parent.nodeName === 'LI' && parent.childElementCount === 1) {
 -                              // do not add a wrapper element if the trigger is the only child
 -                              parent.classList.add('dropdown');
++                      });
++              },
+               
 +              /**
 +               * Closes all inline editors.
 +               */
 +              _closeAll: function () {
 +                      for (var $elementID in this._elements) {
 +                              this._hide($elementID);
                        }
 -                      else {
 -                              $trigger.wrap('<span class="dropdown" />');
 +              },
 +              
 +              /**
 +               * Sets options for this inline editor.
 +               */
 +              _setOptions: function () {
 +                      this._options = [];
 +              },
 +              
 +              /**
 +               * Register an option callback for validation and execution.
 +               *
 +               * @param        object                callback
 +               */
 +              registerCallback: function (callback) {
 +                      if ($.isFunction(callback)) {
 +                              this._callbacks.push(callback);
                        }
 -                      
 -                      this._dropdowns[$elementID] = $('<ul class="dropdownMenu" />').insertAfter($trigger);
 -              }
 -              this._dropdowns[$elementID].empty();
 +              },
 +              
 +              /**
 +               * Returns the triggering element.
 +               *
 +               * @param        jQuery                element
 +               * @return        jQuery
 +               */
 +              _getTriggerElement: function (element) {
 +                      return null;
 +              },
                
 -              // validate options
 -              var $hasOptions = false;
 -              var $lastElementType = '';
 -              for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
 -                      var $option = this._options[$i];
 +              /**
 +               * Shows a dropdown menu if options are available.
 +               *
 +               * @param        object                event
 +               */
 +              _show: function (event) {
 +                      event.preventDefault();
 +                      var $elementID = $(event.currentTarget).data('elementID');
                        
 -                      if ($option.optionName === 'divider') {
 -                              if ($lastElementType !== '' && $lastElementType !== 'divider') {
 -                                      $('<li class="dropdownDivider" />').appendTo(this._dropdowns[$elementID]);
 -                                      $lastElementType = $option.optionName;
 +                      // build dropdown
 +                      var $trigger = null;
 +                      if (!this._dropdowns[$elementID]) {
 +                              this._triggerElements[$elementID] = $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle');
 +                              var parent = $trigger[0].parentNode;
 +                              if (parent && parent.nodeName === 'LI' && parent.childElementCount === 1) {
 +                                      // do not add a wrapper element if the trigger is the only child
 +                                      parent.classList.add('dropdown');
 +                              }
 +                              else {
 +                                      $trigger.wrap('<span class="dropdown" />');
                                }
 -                      }
 -                      else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) {
 -                              var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]);
 -                              $listItem.data('elementID', $elementID).data('optionName', $option.optionName).data('isQuickOption', ($option.isQuickOption ? true : false)).click($.proxy(this._click, this));
                                
 -                              $hasOptions = true;
 -                              $lastElementType = $option.optionName;
 -                      }
 -              }
 -              
 -              if ($hasOptions) {
 -                      // if last child is divider, remove it
 -                      var $lastChild = this._dropdowns[$elementID].children().last();
 -                      if ($lastChild.hasClass('dropdownDivider')) {
 -                              $lastChild.remove();
 +                              this._dropdowns[$elementID] = $('<ul class="dropdownMenu" />').insertAfter($trigger);
                        }
 +                      this._dropdowns[$elementID].empty();
                        
 -                      // check if only element is a quick option
 -                      var $quickOption = null;
 -                      var $count = 0;
 -                      this._dropdowns[$elementID].children().each(function(index, child) {
 -                              var $child = $(child);
 -                              if (!$child.hasClass('dropdownDivider')) {
 -                                      if ($child.data('isQuickOption')) {
 -                                              $quickOption = $child;
 -                                      }
 -                                      else {
 -                                              $count++;
 +                      // validate options
 +                      var $hasOptions = false;
 +                      var $lastElementType = '';
 +                      for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
 +                              var $option = this._options[$i];
 +                              
 +                              if ($option.optionName === 'divider') {
 +                                      if ($lastElementType !== '' && $lastElementType !== 'divider') {
 +                                              $('<li class="dropdownDivider" />').appendTo(this._dropdowns[$elementID]);
 +                                              $lastElementType = $option.optionName;
                                        }
                                }
 -                      });
 +                              else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) {
 +                                      var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]);
 +                                      $listItem.data('elementID', $elementID).data('optionName', $option.optionName).data('isQuickOption', ($option.isQuickOption ? true : false)).click($.proxy(this._click, this));
 +                                      
 +                                      $hasOptions = true;
 +                                      $lastElementType = $option.optionName;
 +                              }
 +                      }
                        
 -                      if (!$count) {
 -                              $quickOption.trigger('click');
 -                              
 -                              if (this._triggerElements[$elementID]) {
 -                                      WCF.Dropdown.close(this._triggerElements[$elementID].parents('.dropdown').wcfIdentify());
 +                      if ($hasOptions) {
 +                              // if last child is divider, remove it
 +                              var $lastChild = this._dropdowns[$elementID].children().last();
 +                              if ($lastChild.hasClass('dropdownDivider')) {
 +                                      $lastChild.remove();
                                }
                                
 -                              return false;
 +                              // check if only element is a quick option
 +                              var $quickOption = null;
 +                              var $count = 0;
 +                              this._dropdowns[$elementID].children().each(function (index, child) {
 +                                      var $child = $(child);
 +                                      if (!$child.hasClass('dropdownDivider')) {
 +                                              if ($child.data('isQuickOption')) {
 +                                                      $quickOption = $child;
 +                                              }
 +                                              else {
 +                                                      $count++;
 +                                              }
 +                                      }
 +                              });
 +                              
 +                              if (!$count) {
 +                                      $quickOption.trigger('click');
 +                                      
 +                                      if (this._triggerElements[$elementID]) {
 +                                              WCF.Dropdown.close(this._triggerElements[$elementID].parents('.dropdown').wcfIdentify());
 +                                      }
 +                                      
 +                                      return false;
 +                              }
                        }
 -              }
 +                      
 +                      if ($trigger !== null) {
 +                              WCF.Dropdown.initDropdown($trigger, true);
 +                      }
 +                      
 +                      return false;
 +              },
                
 -              if ($trigger !== null) {
 -                      WCF.Dropdown.initDropdown($trigger, true);
 -              }
 +              /**
 +               * Validates an option.
 +               *
 +               * @param        string                elementID
 +               * @param        string                optionName
 +               * @returns        boolean
 +               */
 +              _validate: function (elementID, optionName) {
 +                      return false;
 +              },
                
 -              return false;
 -      },
 -      
 -      /**
 -       * Validates an option.
 -       * 
 -       * @param       string          elementID
 -       * @param       string          optionName
 -       * @returns     boolean
 -       */
 -      _validate: function(elementID, optionName) {
 -              return false;
 -      },
 -      
 -      /**
 -       * Validates an option provided by callbacks.
 -       * 
 -       * @param       string          elementID
 -       * @param       string          optionName
 -       * @return      boolean
 -       */
 -      _validateCallbacks: function(elementID, optionName) {
 -              var $length = this._callbacks.length;
 -              if ($length) {
 -                      for (var $i = 0; $i < $length; $i++) {
 -                              if (this._callbacks[$i].validate(this._elements[elementID], optionName)) {
 -                                      return true;
 +              /**
 +               * Validates an option provided by callbacks.
 +               *
 +               * @param        string                elementID
 +               * @param        string                optionName
 +               * @return        boolean
 +               */
 +              _validateCallbacks: function (elementID, optionName) {
 +                      var $length = this._callbacks.length;
 +                      if ($length) {
 +                              for (var $i = 0; $i < $length; $i++) {
 +                                      if (this._callbacks[$i].validate(this._elements[elementID], optionName)) {
 +                                              return true;
 +                                      }
                                }
                        }
 -              }
 +                      
 +                      return false;
 +              },
                
 -              return false;
 -      },
 -      
 -      /**
 -       * Handles AJAX responses.
 -       * 
 -       * @param       object          data
 -       * @param       string          textStatus
 -       * @param       jQuery          jqXHR
 -       */
 -      _success: function(data, textStatus, jqXHR) {
 -              var $length = this._updateData.length;
 -              if (!$length) {
 -                      return;
 -              }
 +              /**
 +               * Handles AJAX responses.
 +               *
 +               * @param        object                data
 +               * @param        string                textStatus
 +               * @param        jQuery                jqXHR
 +               */
 +              _success: function (data, textStatus, jqXHR) {
 +                      var $length = this._updateData.length;
 +                      if (!$length) {
 +                              return;
 +                      }
 +                      
 +                      this._updateState(data);
 +                      
 +                      this._updateData = [];
 +              },
                
 -              this._updateState(data);
 +              /**
 +               * Update element states based upon update data.
 +               *
 +               * @param        object                data
 +               */
 +              _updateState: function (data) {
 +              },
                
 -              this._updateData = [ ];
 -      },
 -      
 -      /**
 -       * Update element states based upon update data.
 -       * 
 -       * @param       object          data
 -       */
 -      _updateState: function(data) { },
 -      
 -      /**
 -       * Handles clicks within dropdown.
 -       * 
 -       * @param       object          event
 -       */
 -      _click: function(event) {
 -              var $listItem = $(event.currentTarget);
 -              var $elementID = $listItem.data('elementID');
 -              var $optionName = $listItem.data('optionName');
 +              /**
 +               * Handles clicks within dropdown.
 +               *
 +               * @param        object                event
 +               */
 +              _click: function (event) {
 +                      var $listItem = $(event.currentTarget);
 +                      var $elementID = $listItem.data('elementID');
 +                      var $optionName = $listItem.data('optionName');
 +                      
 +                      if (!this._execute($elementID, $optionName)) {
 +                              this._executeCallback($elementID, $optionName);
 +                      }
 +                      
 +                      this._hide($elementID);
 +              },
                
 -              if (!this._execute($elementID, $optionName)) {
 -                      this._executeCallback($elementID, $optionName);
 -              }
 +              /**
 +               * Executes actions associated with an option.
 +               *
 +               * @param        string                elementID
 +               * @param        string                optionName
 +               * @return        boolean
 +               */
 +              _execute: function (elementID, optionName) {
 +                      return false;
 +              },
                
 -              this._hide($elementID);
 -      },
 -      
 -      /**
 -       * Executes actions associated with an option.
 -       * 
 -       * @param       string          elementID
 -       * @param       string          optionName
 -       * @return      boolean
 -       */
 -      _execute: function(elementID, optionName) {
 -              return false;
 -      },
 -      
 -      /**
 -       * Executes actions associated with an option provided by callbacks.
 -       * 
 -       * @param       string          elementID
 -       * @param       string          optionName
 -       * @return      boolean
 -       */
 -      _executeCallback: function(elementID, optionName) {
 -              var $length = this._callbacks.length;
 -              if ($length) {
 -                      for (var $i = 0; $i < $length; $i++) {
 -                              if (this._callbacks[$i].execute(this._elements[elementID], optionName)) {
 -                                      return true;
 +              /**
 +               * Executes actions associated with an option provided by callbacks.
 +               *
 +               * @param        string                elementID
 +               * @param        string                optionName
 +               * @return        boolean
 +               */
 +              _executeCallback: function (elementID, optionName) {
 +                      var $length = this._callbacks.length;
 +                      if ($length) {
 +                              for (var $i = 0; $i < $length; $i++) {
 +                                      if (this._callbacks[$i].execute(this._elements[elementID], optionName)) {
 +                                              return true;
 +                                      }
                                }
                        }
 -              }
 +                      
 +                      return false;
 +              },
                
 -              return false;
 -      },
 -      
 -      /**
 -       * Hides a dropdown menu.
 -       * 
 -       * @param       string          elementID
 -       */
 -      _hide: function(elementID) {
 -              if (this._dropdowns[elementID]) {
 -                      this._dropdowns[elementID].empty().removeClass('dropdownOpen');
 +              /**
 +               * Hides a dropdown menu.
 +               *
 +               * @param        string                elementID
 +               */
 +              _hide: function (elementID) {
 +                      if (this._dropdowns[elementID]) {
 +                              this._dropdowns[elementID].empty().removeClass('dropdownOpen');
 +                      }
                }
 -      }
 -});
 -
 -/**
 - * Default implementation for ajax file uploads.
 - * 
 - * @deprecated        Use WoltLabSuite/Core/Upload
 - * 
 - * @param     jquery          buttonSelector
 - * @param     jquery          fileListSelector
 - * @param     string          className
 - * @param     jquery          options
 - */
 -WCF.Upload = Class.extend({
 -      /**
 -       * name of the upload field
 -       * @var string
 -       */
 -      _name: '__files[]',
 -      
 -      /**
 -       * button selector
 -       * @var jQuery
 -       */
 -      _buttonSelector: null,
 -      
 -      /**
 -       * file list selector
 -       * @var jQuery
 -       */
 -      _fileListSelector: null,
 -      
 -      /**
 -       * upload file
 -       * @var jQuery
 -       */
 -      _fileUpload: null,
 -      
 -      /**
 -       * class name
 -       * @var string
 -       */
 -      _className: '',
 -      
 -      /**
 -       * iframe for IE<10 fallback
 -       * @var jQuery
 -       */
 -      _iframe: null,
 -      
 -      /**
 -       * internal file id
 -       * @var integer
 -       */
 -      _internalFileID: 0,
 -      
 -      /**
 -       * additional options
 -       * @var jQuery
 -       */
 -      _options: {},
 -      
 -      /**
 -       * upload matrix
 -       * @var array
 -       */
 -      _uploadMatrix: [],
 -      
 -      /**
 -       * true, if the active user's browser supports ajax file uploads
 -       * @var boolean
 -       */
 -      _supportsAJAXUpload: true,
 -      
 -      /**
 -       * fallback overlay for stupid browsers
 -       * @var jquery
 -       */
 -      _overlay: null,
 +      });
        
        /**
 -       * Initializes a new upload handler.
 -       * 
 -       * @param       string          buttonSelector
 -       * @param       string          fileListSelector
 -       * @param       string          className
 -       * @param       object          options
 +       * Default implementation for ajax file uploads.
 +       *
 +       * @deprecated        Use WoltLabSuite/Core/Upload
 +       *
 +       * @param        jquery                buttonSelector
 +       * @param        jquery                fileListSelector
 +       * @param        string                className
 +       * @param        jquery                options
         */
 -      init: function(buttonSelector, fileListSelector, className, options) {
 -              this._buttonSelector = buttonSelector;
 -              this._fileListSelector = fileListSelector;
 -              this._className = className;
 -              this._internalFileID = 0;
 -              this._options = $.extend(true, {
 -                      action: 'upload',
 -                      multiple: false,
 -                      url: 'index.php?ajax-upload/&t=' + SECURITY_TOKEN
 -              }, options || { });
 +      WCF.Upload = Class.extend({
 +              /**
 +               * name of the upload field
 +               * @var        string
 +               */
 +              _name: '__files[]',
                
 -              this._options.url = WCF.convertLegacyURL(this._options.url);
 -              if (this._options.url.indexOf('index.php') === 0) {
 -                      this._options.url = WSC_API_URL + this._options.url;
 -              }
 +              /**
 +               * button selector
 +               * @var        jQuery
 +               */
 +              _buttonSelector: null,
 +              
 +              /**
 +               * file list selector
 +               * @var        jQuery
 +               */
 +              _fileListSelector: null,
                
 -              // check for ajax upload support
 -              var $xhr = new XMLHttpRequest();
 -              this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload));
 +              /**
 +               * upload file
 +               * @var        jQuery
 +               */
 +              _fileUpload: null,
                
 -              // create upload button
 -              this._createButton();
 -      },
 -      
 -      /**
 -       * Creates the upload button.
 -       */
 -      _createButton: function() {
 -              if (this._supportsAJAXUpload) {
 -                      this._fileUpload = $('<input type="file" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/>');
 -                      this._fileUpload.change($.proxy(this._upload, this));
 -                      var $button = $('<p class="button uploadButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
 -                      $button.prepend(this._fileUpload);
 -              }
 -              else {
 -                      var $button = $('<p class="button uploadFallbackButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
 -                      $button.click($.proxy(this._showOverlay, this));
 -              }
 +              /**
 +               * class name
 +               * @var        string
 +               */
 +              _className: '',
                
 -              this._insertButton($button);
 -      },
 -      
 -      /**
 -       * Inserts the upload button.
 -       * 
 -       * @param       jQuery          button
 -       */
 -      _insertButton: function(button) {
 -              this._buttonSelector.prepend(button);
 -      },
 -      
 -      /**
 -       * Removes the upload button.
 -       */
 -      _removeButton: function() {
 -              var $selector = '.uploadButton';
 -              if (!this._supportsAJAXUpload) {
 -                      $selector = '.uploadFallbackButton';
 -              }
 +              /**
 +               * iframe for IE<10 fallback
 +               * @var        jQuery
 +               */
 +              _iframe: null,
                
 -              this._buttonSelector.find($selector).remove();
 -      },
 -      
 -      /**
 -       * Callback for file uploads.
 -       * 
 -       * @param       object          event
 -       * @param       File            file
 -       * @param       Blob            blob
 -       * @return      integer
 -       */
 -      _upload: function(event, file, blob) {
 -              var $uploadID = null;
 -              var $files = [ ];
 -              if (file) {
 -                      $files.push(file);
 -              }
 -              else if (blob) {
 -                      var $ext = '';
 -                      switch (blob.type) {
 -                              case 'image/png':
 -                                      $ext = '.png';
 -                              break;
 -                              
 -                              case 'image/jpeg':
 -                                      $ext = '.jpg';
 -                              break;
 -                              
 -                              case 'image/gif':
 -                                      $ext = '.gif';
 -                              break;
 +              /**
 +               * internal file id
 +               * @var        integer
 +               */
 +              _internalFileID: 0,
 +              
 +              /**
 +               * additional options
 +               * @var        jQuery
 +               */
 +              _options: {},
 +              
 +              /**
 +               * upload matrix
 +               * @var        array
 +               */
 +              _uploadMatrix: [],
 +              
 +              /**
 +               * true, if the active user's browser supports ajax file uploads
 +               * @var        boolean
 +               */
 +              _supportsAJAXUpload: true,
 +              
 +              /**
 +               * fallback overlay for stupid browsers
 +               * @var        jquery
 +               */
 +              _overlay: null,
 +              
 +              /**
 +               * Initializes a new upload handler.
 +               *
 +               * @param        string                buttonSelector
 +               * @param        string                fileListSelector
 +               * @param        string                className
 +               * @param        object                options
 +               */
 +              init: function (buttonSelector, fileListSelector, className, options) {
 +                      this._buttonSelector = buttonSelector;
 +                      this._fileListSelector = fileListSelector;
 +                      this._className = className;
 +                      this._internalFileID = 0;
 +                      this._options = $.extend(true, {
 +                              action: 'upload',
 +                              multiple: false,
 +                              url: 'index.php?ajax-upload/&t=' + SECURITY_TOKEN
 +                      }, options || {});
 +                      
 +                      this._options.url = WCF.convertLegacyURL(this._options.url);
 +                      if (this._options.url.indexOf('index.php') === 0) {
 +                              this._options.url = WSC_API_URL + this._options.url;
                        }
                        
 -                      $files.push({
 -                              name: 'pasted-from-clipboard' + $ext
 -                      });
 -              }
 -              else {
 -                      $files = this._fileUpload.prop('files');
 -              }
 +                      // check for ajax upload support
 +                      var $xhr = new XMLHttpRequest();
 +                      this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload));
 +                      
 +                      // create upload button
 +                      this._createButton();
 +              },
                
 -              if ($files.length) {
 -                      var $fd = new FormData();
 -                      $uploadID = this._createUploadMatrix($files);
 +              /**
 +               * Creates the upload button.
 +               */
 +              _createButton: function () {
 +                      if (this._supportsAJAXUpload) {
 +                              this._fileUpload = $('<input type="file" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/>');
 +                              this._fileUpload.change($.proxy(this._upload, this));
 +                              var $button = $('<p class="button uploadButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
 +                              $button.prepend(this._fileUpload);
 +                      }
 +                      else {
 +                              var $button = $('<p class="button uploadFallbackButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
 +                              $button.click($.proxy(this._showOverlay, this));
 +                      }
                        
 -                      // no more files left, abort
 -                      if (!this._uploadMatrix[$uploadID].length) {
 -                              return null;
 +                      this._insertButton($button);
 +              },
 +              
 +              /**
 +               * Inserts the upload button.
 +               *
 +               * @param        jQuery                button
 +               */
 +              _insertButton: function (button) {
 +                      this._buttonSelector.prepend(button);
 +              },
 +              
 +              /**
 +               * Removes the upload button.
 +               */
 +              _removeButton: function () {
 +                      var $selector = '.uploadButton';
 +                      if (!this._supportsAJAXUpload) {
 +                              $selector = '.uploadFallbackButton';
                        }
                        
 -                      for (var $i = 0, $length = $files.length; $i < $length; $i++) {
 -                              if (this._uploadMatrix[$uploadID][$i]) {
 -                                      var $internalFileID = this._uploadMatrix[$uploadID][$i].data('internalFileID');
 +                      this._buttonSelector.find($selector).remove();
 +              },
 +              
 +              /**
 +               * Callback for file uploads.
 +               *
 +               * @param        object                event
 +               * @param        File                file
 +               * @param        Blob                blob
 +               * @return        integer
 +               */
 +              _upload: function (event, file, blob) {
 +                      var $uploadID = null;
 +                      var $files = [];
 +                      if (file) {
 +                              $files.push(file);
 +                      }
 +                      else if (blob) {
 +                              var $ext = '';
 +                              switch (blob.type) {
 +                                      case 'image/png':
 +                                              $ext = '.png';
 +                                              break;
                                        
 -                                      if (blob) {
 -                                              $fd.append('__files[' + $internalFileID + ']', blob, $files[$i].name);
 -                                      }
 -                                      else {
 -                                              $fd.append('__files[' + $internalFileID + ']', $files[$i]);
 -                                      }
 +                                      case 'image/jpeg':
 +                                              $ext = '.jpg';
 +                                              break;
 +                                      
 +                                      case 'image/gif':
 +                                              $ext = '.gif';
 +                                              break;
                                }
 +                              
 +                              $files.push({
 +                                      name: 'pasted-from-clipboard' + $ext
 +                              });
                        }
 -                      
 -                      $fd.append('actionName', this._options.action);
 -                      $fd.append('className', this._className);
 -                      var $additionalParameters = this._getParameters();
 -                      for (var $name in $additionalParameters) {
 -                              $fd.append('parameters[' + $name + ']', $additionalParameters[$name]);
 +                      else {
 +                              $files = this._fileUpload.prop('files');
                        }
                        
 -                      var self = this;
 -                      $.ajax({
 -                              type: 'POST',
 -                              url: this._options.url,
 -                              enctype: 'multipart/form-data',
 -                              data: $fd,
 -                              contentType: false,
 -                              processData: false,
 -                              success: function(data, textStatus, jqXHR) {
 -                                      self._success($uploadID, data);
 -                              },
 -                              error: $.proxy(this._error, this),
 -                              xhr: function() {
 -                                      var $xhr = $.ajaxSettings.xhr();
 -                                      if ($xhr) {
 -                                              $xhr.upload.addEventListener('progress', function(event) {
 -                                                      self._progress($uploadID, event);
 -                                              }, false);
 +                      if ($files.length) {
 +                              var $fd = new FormData();
 +                              $uploadID = this._createUploadMatrix($files);
 +                              
 +                              // no more files left, abort
 +                              if (!this._uploadMatrix[$uploadID].length) {
 +                                      return null;
 +                              }
 +                              
 +                              for (var $i = 0, $length = $files.length; $i < $length; $i++) {
 +                                      if (this._uploadMatrix[$uploadID][$i]) {
 +                                              var $internalFileID = this._uploadMatrix[$uploadID][$i].data('internalFileID');
 +                                              
 +                                              if (blob) {
 +                                                      $fd.append('__files[' + $internalFileID + ']', blob, $files[$i].name);
 +                                              }
 +                                              else {
 +                                                      $fd.append('__files[' + $internalFileID + ']', $files[$i]);
 +                                              }
                                        }
 -                                      return $xhr;
 -                              },
 -                              xhrFields: {
 -                                      withCredentials: true
                                }
 -                      });
 -              }
 -              
 -              return $uploadID;
 -      },
 -      
 -      /**
 -       * Creates upload matrix for provided files.
 -       * 
 -       * @param       array<object>           files
 -       * @return      integer
 -       */
 -      _createUploadMatrix: function(files) {
 -              if (files.length) {
 -                      var $uploadID = this._uploadMatrix.length;
 -                      this._uploadMatrix[$uploadID] = [ ];
 -                      
 -                      for (var $i = 0, $length = files.length; $i < $length; $i++) {
 -                              var $file = files[$i];
 -                              var $li = this._initFile($file);
                                
 -                              if (!$li.hasClass('uploadFailed')) {
 -                                      $li.data('filename', $file.name).data('internalFileID', this._internalFileID++);
 -                                      this._uploadMatrix[$uploadID][$i] = $li;
 +                              $fd.append('actionName', this._options.action);
 +                              $fd.append('className', this._className);
 +                              var $additionalParameters = this._getParameters();
 +                              for (var $name in $additionalParameters) {
 +                                      $fd.append('parameters[' + $name + ']', $additionalParameters[$name]);
                                }
 +                              
 +                              var self = this;
 +                              $.ajax({
 +                                      type: 'POST',
 +                                      url: this._options.url,
 +                                      enctype: 'multipart/form-data',
 +                                      data: $fd,
 +                                      contentType: false,
 +                                      processData: false,
 +                                      success: function (data, textStatus, jqXHR) {
 +                                              self._success($uploadID, data);
 +                                      },
 +                                      error: $.proxy(this._error, this),
 +                                      xhr: function () {
 +                                              var $xhr = $.ajaxSettings.xhr();
 +                                              if ($xhr) {
 +                                                      $xhr.upload.addEventListener('progress', function (event) {
 +                                                              self._progress($uploadID, event);
 +                                                      }, false);
 +                                              }
 +                                              return $xhr;
 +                                      },
 +                                      xhrFields: {
 +                                              withCredentials: true
 +                                      }
 +                              });
                        }
                        
                        return $uploadID;
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
new file mode 100644 (file)
--- /dev/null
--- /dev/null
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..9ceb64d80d25af4cf588c06013f31bbb2aff039a
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,167 @@@
++<?php
++namespace wcf\system\dialog;
++use wbb\data\board\BoardCache;
++use wbb\system\label\object\ThreadLabelObjectHandler;
++use wcf\data\AbstractDatabaseObjectAction;
++use wcf\system\form\builder\container\FormContainer;
++use wcf\system\form\builder\container\wysiwyg\WysiwygFormContainer;
++use wcf\system\form\builder\DialogFormDocument;
++use wcf\system\form\builder\field\acl\simple\SimpleAclFormField;
++use wcf\system\form\builder\field\BooleanFormField;
++use wcf\system\form\builder\field\CaptchaFormField;
++use wcf\system\form\builder\field\DateFormField;
++use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
++use wcf\system\form\builder\field\IconFormField;
++use wcf\system\form\builder\field\IntegerFormField;
++use wcf\system\form\builder\field\ItemListFormField;
++use wcf\system\form\builder\field\label\LabelFormField;
++use wcf\system\form\builder\field\MultilineTextFormField;
++use wcf\system\form\builder\field\MultipleSelectionFormField;
++use wcf\system\form\builder\field\RadioButtonFormField;
++use wcf\system\form\builder\field\RatingFormField;
++use wcf\system\form\builder\field\SingleSelectionFormField;
++use wcf\system\form\builder\field\tag\TagFormField;
++use wcf\system\form\builder\field\TextFormField;
++use wcf\system\form\builder\field\UploadFormField;
++use wcf\system\form\builder\field\user\UserFormField;
++use wcf\system\form\builder\field\user\UsernameFormField;
++use wcf\system\label\LabelHandler;
++
++class TestDialog extends AbstractDatabaseObjectAction {
++      /**
++       * @var DialogFormDocument
++       */
++      protected $form;
++      
++      public function __construct(array $objects, $action, array $parameters = []) {
++              $this->action = $action;
++              $this->parameters = $parameters;
++      }
++      
++      public function validateGetDialog() {
++              
++      }
++      
++      public function getDialog() {
++              $form = $this->getForm();
++              
++              return [
++                      'dialog' => $form->getHtml(),
++                      'formId' => $form->getId()
++              ];
++      }
++      
++      public function validateSaveDialog() {
++              $this->form = $this->getForm()->requestData($this->parameters['data']);
++              $this->form->readValues();
++      }
++      
++      public function saveDialog() {
++              $this->form->validate();
++              
++              if (!$this->form->hasValidationErrors()) {
++                      wcfDebug($_POST, $this->form->getData());
++              }
++              
++              return [
++                      'dialog' => $this->form->getHtml(),
++                      'formId' => $this->form->getId()
++              ];
++      }
++
++      /**
++       * @return DialogFormDocument
++       */
++      protected function getForm() {
++              $form = DialogFormDocument::create('test')
++                      ->ajax()
++                      ->prefix('test')
++                      ->appendChildren([
++                              // WysiwygFormContainer not supported because code like new WCF.Attachment.Upload executed before
++                              // WCF.Attachment.js loaded `$()` is probably executed immediately
++                              
++                              FormContainer::create('c')
++                                      ->appendChildren([
++                                              UploadFormField::create('upload')
++                                                      ->label('Upload'),
++                                              TextFormField::create('i18ntext')
++                                                      ->label('i18n text')
++                                                      ->i18n(),
++                                              
++                                              SimpleAclFormField::create('simpleAcl')
++                                                      ->label('Simple Acl'),
++                                              
++                                              // TODO: AclFormField currently not really possible because data is not accessible outside `aclPermissionJavaScript.tpl`
++                                              
++                                              // BBCodesAttributes will not be supported
++                                              // devtool fields will not be supported
++                                              
++                                              TagFormField::create(),
++                                              
++                                              UserFormField::create('user')
++                                                      ->label('User'),
++                                              
++                                              UsernameFormField::create(),
++                                              
++                                              BooleanFormField::create('boolean')
++                                                      ->label('Boolean'),
++                                              
++                                              //CaptchaFormField::create()
++                                              //      ->objectType(CAPTCHA_TYPE),
++                                              
++                                              DateFormField::create('date')
++                                                      ->label('Date'),
++                                              
++                                              IconFormField::create('icon')
++                                                      ->label('Icon'),
++                                              
++                                              IntegerFormField::create('integer')
++                                                      ->label('Integer'),
++                                              
++                                              ItemListFormField::create('itemList')
++                                                      ->label('Item List'),
++                                              
++                                              MultilineTextFormField::create('multilineText')
++                                                      ->label('Multiline Text'),
++
++                                              MultipleSelectionFormField::create('multipleSelection')
++                                                      ->label('Multiple Selection')
++                                                      ->options([
++                                                              1 => 1,
++                                                              2 => 2,
++                                                              3 => 3
++                                                      ]),
++
++                                              RadioButtonFormField::create('radioButton')
++                                                      ->label('Radio Button')
++                                                      ->options([
++                                                              1 => 1,
++                                                              2 => 2,
++                                                              3 => 3
++                                                      ])
++                                                      ->value(1),
++                                              
++                                              RatingFormField::create()
++                                                      ->label('Rating'),
++
++                                              SingleSelectionFormField::create('singleSelection')
++                                                      ->label('Single Selection')
++                                                      ->options([
++                                                              1 => 1,
++                                                              2 => 2,
++                                                              3 => 3
++                                                      ]),
++                                              
++                                              TextFormField::create('text')
++                                                      ->label('Text')
++                                      ])
++                      ]);
++              
++              $form->getNodeById('user')->addDependency(ValueFormFieldDependency::create('username')
++              ->field($form->getNodeById('username'))->values(['1234']));
++              
++              $form->build();
++              
++              return $form;
++      }
++}