-/**\r
- * Class and function collection for WCF\r
- * \r
- * @author Alexander Ebert\r
- * @copyright 2001-2011 WoltLab GmbH\r
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>\r
- */\r
-\r
-/**\r
- * Initialize WCF namespace\r
- */\r
-var WCF = {};\r
-\r
-/**\r
- * Extends jQuery with additional methods.\r
- */\r
-$.extend(true, {\r
- /**\r
- * Escapes an ID to work with jQuery selectors.\r
- *\r
- * @see http://docs.jquery.com/Frequently_Asked_Questions#How_do_I_select_an_element_by_an_ID_that_has_characters_used_in_CSS_notation.3F\r
- * @param string id\r
- * @return string\r
- */\r
- wcfEscapeID: function(id) {\r
- return id.replace(/(:|\.)/g, '\\$1');\r
- },\r
- \r
- /**\r
- * Returns true if given ID exists within DOM.\r
- * \r
- * @param string id\r
- * @return boolean\r
- */\r
- wcfIsset: function(id) {\r
- return !!$('#' + $.wcfEscapeID(id)).length;\r
- }\r
-});\r
-\r
-/**\r
- * Extends jQuery's chainable methods.\r
- */\r
-$.fn.extend({\r
- /**\r
- * Returns tag name of current jQuery element.\r
- * \r
- * @returns string\r
- */\r
- getTagName: function() {\r
- return this.get(0).tagName.toLowerCase();\r
- },\r
- \r
- /**\r
- * Returns the dimensions for current element.\r
- * \r
- * @see http://api.jquery.com/hidden-selector/\r
- * @param string type\r
- * @return object\r
- */\r
- getDimensions: function(type) {\r
- var dimensions = css = {};\r
- var wasHidden = false;\r
- \r
- // show element to retrieve dimensions and restore them later\r
- if (this.is(':hidden')) {\r
- css = {\r
- display: this.css('display'),\r
- visibility: this.css('visibility')\r
- };\r
- \r
- wasHidden = true;\r
- \r
- this.css({\r
- display: 'block',\r
- visibility: 'hidden'\r
- });\r
- }\r
- \r
- switch (type) {\r
- case 'inner':\r
- dimensions = {\r
- height: this.innerHeight(),\r
- width: this.innerWidth()\r
- };\r
- break;\r
- \r
- case 'outer':\r
- dimensions = {\r
- height: this.outerHeight(),\r
- width: this.innerWidth()\r
- };\r
- break;\r
- \r
- default:\r
- dimensions = {\r
- height: this.height(),\r
- width: this.width()\r
- };\r
- break;\r
- }\r
- \r
- // restore previous settings\r
- if (wasHidden) {\r
- this.css(css);\r
- }\r
- \r
- return dimensions;\r
- },\r
- \r
- /**\r
- * Returns the offsets for current element, defaults to position\r
- * relative to document.\r
- * \r
- * @see http://api.jquery.com/hidden-selector/\r
- * @param string type\r
- * @return object\r
- */\r
- getOffsets: function(type) {\r
- var offsets = css = {};\r
- var wasHidden = false;\r
- \r
- // show element to retrieve dimensions and restore them later\r
- if (this.is(':hidden')) {\r
- css = {\r
- display: this.css('display'),\r
- visibility: this.css('visibility')\r
- };\r
- \r
- wasHidden = true;\r
- \r
- this.css({\r
- display: 'block',\r
- visibility: 'hidden'\r
- });\r
- }\r
- \r
- switch (type) {\r
- case 'offset':\r
- offsets = this.offset();\r
- break;\r
- \r
- case 'position':\r
- default:\r
- offsets = this.position();\r
- break;\r
- }\r
- \r
- // restore previous settings\r
- if (wasHidden) {\r
- this.css(css);\r
- }\r
- \r
- return offsets;\r
- },\r
- \r
- /**\r
- * Changes element's position to 'absolute' or 'fixed' while maintaining it's\r
- * current position relative to viewport. Optionally removes element from\r
- * current DOM-node and moving it into body-element (useful for drag & drop)\r
- * \r
- * @param boolean rebase\r
- * @return object\r
- */\r
- makePositioned: function(position, rebase) {\r
- if (position != 'absolute' && position != 'fixed') {\r
- position = 'absolute';\r
- }\r
- \r
- var $currentPosition = this.getOffsets('position');\r
- this.css({\r
- position: position,\r
- left: $currentPosition.left,\r
- margin: 0,\r
- top: $currentPosition.top\r
- });\r
- \r
- if (rebase) {\r
- this.remove().appentTo('body');\r
- }\r
- \r
- return this;\r
- },\r
- \r
- /**\r
- * Disables a form element.\r
- * \r
- * @return jQuery\r
- */\r
- disable: function() {\r
- return this.attr('disabled', 'disabled');\r
- },\r
- \r
- /**\r
- * Enables a form element.\r
- * \r
- * @return jQuery\r
- */\r
- enable: function() {\r
- return this.removeAttr('disabled');\r
- },\r
- \r
- /**\r
- * Applies a grow-effect by resizing element while moving the\r
- * element appropriately\r
- * \r
- * @param object data\r
- * @param object options\r
- * @return jQuery\r
- */\r
- wcfGrow: function(data, options) {\r
- // create temporarily element to determine dimensions\r
- var $tempElementID = WCF.getRandomID();\r
- $('body').append('<div id="' + $tempElementID + '" class="wcfDimensions">' + data.content + '</div>');\r
- var $tempElement = $('#' + $tempElementID);\r
- \r
- // get content dimensions\r
- var $dimensions = $tempElement.getDimensions();\r
- \r
- // remove temporarily element\r
- $tempElement.empty().remove();\r
- \r
- // move parent element, used if applying effect on dialogs\r
- if (!data.parent) {\r
- data.parent = this;\r
- }\r
- \r
- // calculate values for grow-effect\r
- var $borderHeight = parseInt(data.parent.css('borderTopWidth')) + parseInt(data.parent.css('borderBottomWidth'));\r
- var $borderWidth = parseInt(data.parent.css('borderLeftWidth')) + parseInt(data.parent.css('borderRightWidth'));\r
- \r
- var $windowDimensions = $(window).getDimensions();\r
- var $leftOffset = Math.round(($windowDimensions.width - ($dimensions.width + $borderWidth)) / 2);\r
- var $topOffset = Math.round(($windowDimensions.height - ($dimensions.height + $borderHeight)) / 2);\r
- \r
- data.parent.makePositioned('fixed', false);\r
- data.parent.animate({\r
- left: $leftOffset + 'px',\r
- top: $topOffset + 'px'\r
- }, options);\r
- \r
- return this.animate({\r
- height: $dimensions.height,\r
- width: $dimensions.width\r
- }, options);\r
- },\r
- \r
- /**\r
- * Shows an element by sliding and fading it into viewport.\r
- * \r
- * @param string direction\r
- * @param object callback\r
- * @returns jQuery\r
- */\r
- wcfDropIn: function(direction, callback) {\r
- if (!direction) direction = 'up';\r
- \r
- return this.show(WCF.getEffect(this.getTagName(), 'drop'), { direction: direction }, 600, callback);\r
- },\r
- \r
- /**\r
- * Hides an element by sliding and fading it out the viewport.\r
- * \r
- * @param string direction\r
- * @param object callback\r
- * @returns jQuery\r
- */\r
- wcfDropOut: function(direction, callback) {\r
- if (!direction) direction = 'down';\r
- \r
- return this.hide(WCF.getEffect(this.getTagName(), 'drop'), { direction: direction }, 600, callback);\r
- },\r
- \r
- /**\r
- * Shows an element by blinding it up.\r
- * \r
- * @param string direction\r
- * @param object callback\r
- * @returns jQuery\r
- */\r
- wcfBlindIn: function(direction, callback) {\r
- if (!direction) direction = 'vertical';\r
- \r
- return this.show(WCF.getEffect(this.getTagName(), 'blind'), { direction: direction }, 200, callback);\r
- },\r
- \r
- /**\r
- * Hides an element by blinding it down.\r
- * \r
- * @param string direction\r
- * @param object callback\r
- * @returns jQuery\r
- */\r
- wcfBlindOut: function(direction, callback) {\r
- if (!direction) direction = 'vertical';\r
- \r
- return this.hide(WCF.getEffect(this.getTagName(), 'blind'), { direction: direction }, 200, callback);\r
- },\r
- \r
- /**\r
- * Highlights an element.\r
- * \r
- * @param object options\r
- * @param object callback\r
- * @returns jQuery\r
- */\r
- wcfHighlight: function(options, callback) {\r
- return this.effect('highlight', options, 600, callback);\r
- }\r
-});\r
-\r
-/**\r
- * WoltLab Community Framework core methods\r
- */\r
-$.extend(WCF, {\r
- /**\r
- * Counter for dynamic element id's\r
- *\r
- * @var integer\r
- */\r
- _idCounter: 0,\r
- \r
- /**\r
- * Shows a modal dialog with a built-in AJAX-loader.\r
- * \r
- * @param string dialogID\r
- * @param boolean resetDialog\r
- */\r
- showAJAXDialog: function(dialogID, resetDialog) {\r
- if (!dialogID) {\r
- dialogID = this.getRandomID();\r
- }\r
- \r
- if (!$.wcfIsset(dialogID)) {\r
- $('body').append($('<div id="' + dialogID + '"></div>'));\r
- }\r
- \r
- var dialog = $('#' + $.wcfEscapeID(dialogID));\r
- \r
- if (resetDialog) {\r
- dialog.empty();\r
- }\r
- \r
- dialog.addClass('overlayLoading');\r
- \r
- var dialogOptions = arguments[2] || {};\r
- dialog.wcfAJAXDialog(dialogOptions);\r
- },\r
- \r
- /**\r
- * Shows a modal dialog.\r
- * @param string dialogID\r
- */\r
- showDialog: function(dialogID) {\r
- // we cannot work with a non-existant dialog, if you wish to\r
- // load content via AJAX, see showAJAXDialog() instead\r
- if (!$.wcfIsset(dialogID)) return;\r
- \r
- var $dialog = $('#' + $.wcfEscapeID(dialogID));\r
- \r
- var dialogOptions = arguments[2] || {};\r
- $dialog.wcfDialog(dialogOptions);\r
- },\r
- \r
- /**\r
- * Returns a dynamically created id.\r
- * \r
- * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/dom/dom.js#L1789\r
- * @return string\r
- */\r
- getRandomID: function() {\r
- var $elementID = '';\r
- \r
- do {\r
- $elementID = 'wcf' + this._idCounter++; \r
- }\r
- while ($.wcfIsset($elementID));\r
- \r
- return $elementID;\r
- },\r
- \r
- /**\r
- * Wrapper for $.inArray which returns boolean value instead of\r
- * index value, similar to PHP's in_array().\r
- * \r
- * @param mixed needle\r
- * @param array haystack\r
- * @return boolean\r
- */\r
- inArray: function(needle, haystack) {\r
- return ($.inArray(needle, haystack) != -1);\r
- },\r
- \r
- /**\r
- * Adjusts effect for partially supported elements.\r
- * \r
- * @param object object\r
- * @param string effect\r
- * @return string\r
- */\r
- getEffect: function(tagName, effect) {\r
- // most effects are not properly supported on table rows, use highlight instead\r
- if (tagName == 'tr') {\r
- return 'highlight';\r
- }\r
- \r
- return effect;\r
- }\r
-});\r
-\r
-/**\r
- * Provides a simple call for periodical executed functions. Based upon\r
- * ideas by Prototype's PeriodicalExecuter.\r
- * \r
- * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/periodical_executer.js\r
- * @param function callback\r
- * @param integer delay\r
- */\r
-WCF.PeriodicalExecuter = function(callback, delay) { this.init(callback, delay); };\r
-WCF.PeriodicalExecuter.prototype = {\r
- /**\r
- * Initializes a periodical executer.\r
- * \r
- * @param function callback\r
- * @param integer delay\r
- */\r
- init: function(callback, delay) {\r
- this.callback = callback;\r
- this.delay = delay;\r
- this.loop = true;\r
- \r
- this.intervalID = setInterval($.proxy(this._execute, this), this.delay);\r
- },\r
- \r
- /**\r
- * Executes callback.\r
- */\r
- _execute: function() {\r
- this.callback(this);\r
- \r
- if (!this.loop) {\r
- clearInterval(this.intervalID);\r
- }\r
- },\r
- \r
- /**\r
- * Terminates loop.\r
- */\r
- stop: function() {\r
- this.loop = false;\r
- }\r
-};\r
-\r
-/**\r
- * Namespace for AJAXProxies\r
- */\r
-WCF.Action = {};\r
-\r
-/**\r
- * Basic implementation for AJAX-based proxyies\r
- * \r
- * @param object options\r
- */\r
-WCF.Action.Proxy = function(options) { this.init(options); };\r
-WCF.Action.Proxy.prototype = {\r
- /**\r
- * Initializes AJAXProxy.\r
- * \r
- * @param object options\r
- */\r
- init: function(options) {\r
- // initialize default values\r
- this.options = $.extend(true, {\r
- autoSend: false,\r
- data: { },\r
- after: null,\r
- init: null,\r
- failure: null,\r
- success: null,\r
- type: 'POST',\r
- url: 'index.php?action=AJAXProxy&t=' + SECURITY_TOKEN + SID_ARG_2ND\r
- }, options);\r
- \r
- this.confirmationDialog = null;\r
- this.loading = null;\r
- \r
- // send request immediately after initialization\r
- if (this.options.autoSend) {\r
- this.sendRequest();\r
- }\r
- },\r
- \r
- /**\r
- * Sends an AJAX request.\r
- */\r
- sendRequest: function() {\r
- this._init();\r
- \r
- $.ajax({\r
- data: this.options.data,\r
- dataType: 'json',\r
- type: this.options.type,\r
- url: this.options.url,\r
- success: $.proxy(this._success, this),\r
- error: $.proxy(this._failure, this)\r
- });\r
- },\r
- \r
- /**\r
- * Fires before request is send, displays global loading status.\r
- */\r
- _init: function() {\r
- if ($.isFunction(this.options.init)) {\r
- this.options.init();\r
- }\r
- \r
- $('<div id="actionProxyLoading" style="display: none;">Loading …</div>').appendTo($('body'));\r
- this.loading = $('#actionProxyLoading');\r
- this.loading.wcfDropIn();\r
- },\r
- \r
- /**\r
- * Handles AJAX errors.\r
- * \r
- * @param object jqXHR\r
- * @param string textStatus\r
- * @param string errorThrown\r
- */\r
- _failure: function(jqXHR, textStatus, errorThrown) {\r
- try {\r
- var data = $.parseJSON(jqXHR.responseText);\r
- \r
- // call child method if applicable\r
- if ($.isFunction(this.options.failure)) {\r
- this.options.failure(jqXHR, textStatus, errorThrown, data);\r
- }\r
- \r
- var $randomID = WCF.getRandomID();\r
- $('<div id="' + $randomID + '" title="HTTP/1.0 ' + jqXHR.status + ' ' + errorThrown + '"><p>Der Server antwortete: ' + data.message + '.</p></div>').wcfDialog();\r
- }\r
- // failed to parse JSON\r
- catch (e) {\r
- var $randomID = WCF.getRandomID();\r
- $('<div id="' + $randomID + '" title="HTTP/1.0 ' + jqXHR.status + ' ' + errorThrown + '"><p>Der Server antwortete: ' + jqXHR.responseText + '.</p></div>').wcfDialog();\r
- }\r
- \r
- this._after();\r
- },\r
- \r
- /**\r
- * Handles successful AJAX requests.\r
- * \r
- * @param object data\r
- * @param string textStatus\r
- * @param object jqXHR\r
- */\r
- _success: function(data, textStatus, jqXHR) {\r
- // call child method if applicable\r
- if ($.isFunction(this.options.success)) {\r
- this.options.success(data, textStatus, jqXHR);\r
- }\r
- \r
- this._after();\r
- },\r
- \r
- /**\r
- * Fires after an AJAX request, hides global loading status.\r
- */\r
- _after: function() {\r
- if ($.isFunction(this.options.after)) {\r
- this.options.after();\r
- }\r
- \r
- this.loading.wcfDropOut('up');\r
- },\r
- \r
- /**\r
- * Sets options, MUST be used to set parameters before sending request\r
- * if calling from child classes.\r
- * \r
- * @param string optionName\r
- * @param mixed optionData\r
- */\r
- setOption: function(optionName, optionData) {\r
- this.options[optionName] = optionData;\r
- }\r
-};\r
-\r
-/**\r
- * Basic implementation for simple proxy access using bound elements.\r
- * \r
- * @param object options\r
- * @param object callbacks\r
- */\r
-WCF.Action.SimpleProxy = function(options, callbacks) { this.init(options, callbacks); };\r
-WCF.Action.SimpleProxy.prototype = {\r
- /**\r
- * Initializes SimpleProxy.\r
- * \r
- * @param object options\r
- * @param object callbacks\r
- */\r
- init: function(options, callbacks) {\r
- /**\r
- * action-specific options\r
- */\r
- this.options = $.extend(true, {\r
- action: '',\r
- className: '',\r
- elements: null,\r
- eventName: 'click'\r
- }, options);\r
- \r
- /**\r
- * proxy-specific options\r
- */\r
- this.callbacks = $.extend(true, {\r
- after: null,\r
- failure: null,\r
- init: null,\r
- success: null\r
- }, callbacks);\r
- \r
- if (!this.options.elements) return;\r
- \r
- // initialize proxy\r
- this.proxy = new WCF.Action.Proxy(this.callbacks);\r
- \r
- // bind event listener\r
- this.options.elements.each($.proxy(function(index, element) {\r
- $(element).bind(this.options.eventName, $.proxy(this._handleEvent, this));\r
- }, this));\r
- },\r
- \r
- /**\r
- * Handles event actions.\r
- * \r
- * @param object event\r
- */\r
- _handleEvent: function(event) {\r
- this.proxy.setOption('data', {\r
- actionName: this.options.action,\r
- className: this.options.className,\r
- objectIDs: [ $(event.target).data('objectID') ]\r
- });\r
- \r
- this.proxy.sendRequest();\r
- }\r
-};\r
-\r
-/**\r
- * Basic implementation for AJAXProxy-based deletion.\r
- * \r
- * @param string className\r
- * @param jQuery containerList\r
- */\r
-WCF.Action.Delete = function(className, containerList) { this.init(className, containerList); };\r
-WCF.Action.Delete.prototype = {\r
- /**\r
- * Initializes 'delete'-Proxy.\r
- * \r
- * @param string className\r
- * @param jQuery containerList\r
- */\r
- init: function(className, containerList) {\r
- if (!containerList.length) return;\r
- this.containerList = containerList;\r
- this.className = className;\r
- \r
- // initialize proxy\r
- var options = {\r
- success: $.proxy(this._success, this)\r
- };\r
- this.proxy = new WCF.Action.Proxy(options);\r
- \r
- // bind event listener\r
- this.containerList.each($.proxy(function(index, container) {\r
- $(container).find('.deleteButton').bind('click', $.proxy(this._click, this));\r
- }, this));\r
- },\r
- \r
- /**\r
- * Sends AJAX request.\r
- * \r
- * @param object event\r
- */\r
- _click: function(event) {\r
- var $target = $(event.target);\r
- \r
- if ($target.data('confirmMessage')) {\r
- if (confirm($target.data('confirmMessage'))) {\r
- this._sendRequest($target);\r
- }\r
- }\r
- else {\r
- this._sendRequest($target);\r
- }\r
- \r
- },\r
- \r
- _sendRequest: function(object) {\r
- this.proxy.setOption('data', {\r
- actionName: 'delete',\r
- className: this.className,\r
- objectIDs: [ $(object).data('objectID') ]\r
- });\r
- \r
- this.proxy.sendRequest();\r
- },\r
- \r
- /**\r
- * Deletes items from containers.\r
- * \r
- * @param object data\r
- * @param string textStatus\r
- * @param object jqXHR\r
- */\r
- _success: function(data, textStatus, jqXHR) {\r
- // remove items\r
- this.containerList.each(function(index, container) {\r
- var $objectID = $(container).find('.deleteButton').data('objectID');\r
- if (WCF.inArray($objectID, data.objectIDs)) {\r
- $(container).wcfBlindOut('up', function() {\r
- $(container).empty().remove();\r
- }, container);\r
- }\r
- });\r
- }\r
-};\r
-\r
-/**\r
- * Basic implementation for AJAXProxy-based toggle actions.\r
- * \r
- * @param string className\r
- * @param jQuery containerList\r
- */\r
-WCF.Action.Toggle = function(className, containerList) { this.init(className, containerList); };\r
-WCF.Action.Toggle.prototype = {\r
- /**\r
- * Initializes 'toggle'-Proxy\r
- * \r
- * @param string className\r
- * @param jQuery containerList\r
- */\r
- init: function(className, containerList) {\r
- if (!containerList.length) return;\r
- this.containerList = containerList;\r
- this.className = className;\r
- \r
- // initialize proxy\r
- var options = {\r
- success: $.proxy(this._success, this)\r
- };\r
- this.proxy = new WCF.Action.Proxy(options);\r
- \r
- // bind event listener\r
- this.containerList.each($.proxy(function(index, container) {\r
- $(container).find('.toggleButton').bind('click', $.proxy(this._click, this));\r
- }, this));\r
- },\r
- \r
- /**\r
- * Sends AJAX request.\r
- * \r
- * @param object event\r
- */\r
- _click: function(event) {\r
- this.proxy.setOption('data', {\r
- actionName: 'toggle',\r
- className: this.className,\r
- objectIDs: [ $(event.target).data('objectID') ]\r
- });\r
- \r
- this.proxy.sendRequest();\r
- },\r
- \r
- /**\r
- * Toggles status icons.\r
- * \r
- * @param object data\r
- * @param string textStatus\r
- * @param object jqXHR\r
- */\r
- _success: function(data, textStatus, jqXHR) {\r
- // remove items\r
- this.containerList.each(function(index, container) {\r
- var $toggleButton = $(container).find('.toggleButton');\r
- if (WCF.inArray($toggleButton.data('objectID'), data.objectIDs)) {\r
- $(container).wcfHighlight();\r
- \r
- // toggle icon source\r
- $toggleButton.attr('src', function() {\r
- if (this.src.match(/enabled(S|M|L)\.png$/)) {\r
- return this.src.replace(/enabled(S|M|L)\.png$/, 'disabled$1\.png');\r
- }\r
- else {\r
- return this.src.replace(/disabled(S|M|L)\.png$/, 'enabled$1\.png');\r
- }\r
- });\r
- // toogle icon title\r
- $toggleButton.attr('title', function() {\r
- if (this.src.match(/enabled(S|M|L)\.png$/)) {\r
- return $(this).data('disableMessage');\r
- }\r
- else {\r
- return $(this).data('enableMessage');\r
- }\r
- });\r
- }\r
- });\r
- }\r
-};\r
-\r
-/**\r
- * Namespace for date-related functions.\r
- */\r
-WCF.Date = {};\r
-\r
-/**\r
- * Provides utility functions for date operations.\r
- */\r
-WCF.Date.Util = {\r
- /**\r
- * Returns UTC timestamp, if date is not given, current time will be used.\r
- * \r
- * @param Date date\r
- * @return integer\r
- */\r
- gmdate: function(date) {\r
- var $date = (date) ? date : new Date();\r
- \r
- return Math.round(Date.UTC(\r
- $date.getUTCFullYear(),\r
- $date.getUTCMonth(),\r
- $date.getUTCDay(),\r
- $date.getUTCHours(),\r
- $date.getUTCMinutes(),\r
- $date.getUTCSeconds()\r
- ) / 1000);\r
- },\r
- \r
- /**\r
- * Returns a Date object with precise offset (including timezone and local timezone).\r
- * Parameter timestamp must be in miliseconds!\r
- * \r
- * @param integer timestamp\r
- * @param integer offset\r
- * @return Date\r
- */\r
- getTimezoneDate: function(timestamp, offset) {\r
- var $date = new Date(timestamp);\r
- var $localOffset = $date.getTimezoneOffset() * -1 * 60000;\r
- \r
- return new Date((timestamp - $localOffset - offset));\r
- }\r
-};\r
-\r
-/**\r
- * Handles relative time designations.\r
- */\r
-WCF.Date.Time = function() { this.init(); };\r
-WCF.Date.Time.prototype = {\r
- /**\r
- * Initializes relative datetimes.\r
- */\r
- init: function() {\r
- // initialize variables\r
- this.elements = $('time.datetime');\r
- this.timestamp = 0;\r
- \r
- // calculate relative datetime on init\r
- this._refresh();\r
- \r
- // re-calculate relative datetime every minute\r
- new WCF.PeriodicalExecuter($.proxy(this._refresh, this), 60000);\r
- },\r
- \r
- /**\r
- * Refreshes relative datetime for each element.\r
- */\r
- _refresh: function() {\r
- // TESTING ONLY!\r
- var $date = new Date();\r
- this.timestamp = ($date.getTime() - $date.getMilliseconds()) / 1000;\r
- // TESTING ONLY!\r
- \r
- this.elements.each($.proxy(this._refreshElement, this));\r
- },\r
- \r
- /**\r
- * Refreshes relative datetime for current element.\r
- * \r
- * @param integer index\r
- * @param object element\r
- */\r
- _refreshElement: function(index, element) {\r
- if (!$(element).attr('title')) {\r
- $(element).attr('title', $(element).text());\r
- }\r
- \r
- var $timestamp = $(element).data('timestamp');\r
- var $date = $(element).data('date');\r
- var $time = $(element).data('time');\r
- var $offset = $(element).data('offset');\r
- \r
- // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago)\r
- if (this.timestamp < ($timestamp + 3540)) {\r
- var $minutes = Math.round((this.timestamp - $timestamp) / 60);\r
- $(element).text(eval(WCF.Language.get('wcf.global.date.relative.minutes')));\r
- }\r
- // timestamp is less than 24 hours ago\r
- else if (this.timestamp < ($timestamp + 86400)) {\r
- var $hours = Math.round((this.timestamp - $timestamp) / 3600);\r
- $(element).text(eval(WCF.Language.get('wcf.global.date.relative.hours')));\r
- }\r
- // timestamp is less than a week ago\r
- else if (this.timestamp < ($timestamp + 604800)) {\r
- var $days = Math.round((this.timestamp - $timestamp) / 86400);\r
- var $string = eval(WCF.Language.get('wcf.global.date.relative.pastDays'));\r
- \r
- // get day of week\r
- var $dateObj = WCF.Date.Util.getTimezoneDate(($timestamp * 1000), $offset);\r
- var $dow = $dateObj.getDay();\r
- \r
- $(element).text($string.replace(/\%day\%/, WCF.Language.get('__days')[$dow]).replace(/\%time\%/, $time));\r
- }\r
- // timestamp is between ~700 million years BC and last week\r
- else {\r
- var $string = WCF.Language.get('wcf.global.date.dateTimeFormat');\r
- $(element).text($string.replace(/\%date\%/, $date).replace(/\%time\%/, $time));\r
- }\r
- }\r
-};\r
-\r
-/**\r
- * Hash-like dictionary. Based upon idead from Prototype's hash\r
- * \r
- * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js\r
- */\r
-WCF.Dictionary = function() { this.init(); };\r
-WCF.Dictionary.prototype = {\r
- /**\r
- * Initializes a new dictionary.\r
- */\r
- init: function() {\r
- this.variables = { };\r
- },\r
- \r
- /**\r
- * Adds an entry.\r
- * \r
- * @param string key\r
- * @param mixed value\r
- */\r
- add: function(key, value) {\r
- this.variables[key] = value;\r
- },\r
- \r
- /**\r
- * Adds a traditional object to current dataset.\r
- * \r
- * @param object object\r
- */\r
- addObject: function(object) {\r
- for (var $key in object) {\r
- this.add($key, object[$key]);\r
- }\r
- },\r
- \r
- /**\r
- * Adds a dictionary to current dataset.\r
- * \r
- * @param object dictionary\r
- */\r
- addDictionary: function(dictionary) {\r
- dictionary.each($.proxy(function(pair) {\r
- this.add(pair.key, pair.value);\r
- }, this));\r
- },\r
- \r
- /**\r
- * Retrieves the value of an entry or returns null if key is not found.\r
- * \r
- * @param string key\r
- * @returns mixed\r
- */\r
- get: function(key) {\r
- if (this.isset(key)) {\r
- return this.variables[key];\r
- }\r
- \r
- return null;\r
- },\r
- \r
- /**\r
- * Returns true if given key is a valid entry.\r
- * \r
- * @param string key\r
- */\r
- isset: function(key) {\r
- return this.variables.hasOwnProperty(key);\r
- },\r
- \r
- /**\r
- * Removes an entry.\r
- * \r
- * @param string key\r
- */\r
- remove: function(key) {\r
- delete this.variables[key];\r
- },\r
- \r
- /**\r
- * Iterates through dictionary.\r
- * \r
- * Usage:\r
- * var $hash = new WCF.Dictionary();\r
- * $hash.add('foo', 'bar');\r
- * $hash.each(function(pair) {\r
- * // alerts: foo = bar\r
- * alert(pair.key + ' = ' + pair.value);\r
- * });\r
- * \r
- * @param function callback\r
- */\r
- each: function(callback) {\r
- if (!$.isFunction(callback)) {\r
- return;\r
- }\r
- \r
- for (var $key in this.variables) {\r
- var $value = this.variables[$key];\r
- var $pair = {\r
- key: $key,\r
- value: $value\r
- };\r
- \r
- callback($pair);\r
- }\r
- }\r
-};\r
-\r
-/**\r
- * Global language storage.\r
- * \r
- * @see WCF.Dictionary\r
- */\r
-WCF.Language = {\r
- _variables: new WCF.Dictionary(),\r
- \r
- /**\r
- * @see WCF.Dictionary.addObject()\r
- */\r
- add: function(key, value) {\r
- this._variables.add(key, value);\r
- },\r
- \r
- /**\r
- * @see WCF.Dictionary.addObject()\r
- */\r
- addObject: function(object) {\r
- this._variables.addObject(object);\r
- },\r
- \r
- /**\r
- * Retrieves a variable.\r
- * \r
- * @param string key\r
- * @return mixed\r
- */\r
- get: function(key) {\r
- return this._variables.get(key);\r
- }\r
-};\r
-\r
-/**\r
- * String utilities.\r
- */\r
-WCF.String = {\r
- /**\r
- * Makes a string's first character uppercase\r
- * \r
- * @param string string\r
- * @return string\r
- */\r
- ucfirst: function(string) {\r
- return string.substring(0, 1).toUpperCase() + string.substring(1);\r
- }\r
-};\r
-\r
-/**\r
- * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the\r
- * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute\r
- * which will be filled with the currently selected tab.\r
- */\r
-WCF.TabMenu = {\r
- /**\r
- * Initializes all TabMenus\r
- */\r
- init: function() {\r
- $('.tabMenuContainer').each(function(index, tabMenu) {\r
- if (!$(tabMenu).attr('id')) {\r
- var $randomID = WCF.getRandomID();\r
- $(tabMenu).attr('id', $randomID);\r
- }\r
- \r
- // init jQuery UI TabMenu\r
- $(tabMenu).wcfTabs({\r
- select: function(event, ui) {\r
- var $panel = $(ui.panel);\r
- var $container = $panel.closest('.tabMenuContainer');\r
- \r
- // store currently selected item\r
- if ($container.data('store')) {\r
- if ($.wcfIsset($container.data('store'))) {\r
- $('#' + $container.data('store')).attr('value', $panel.attr('id'));\r
- }\r
- }\r
- }\r
- });\r
- \r
- // display active item on init\r
- if ($(tabMenu).data('active')) {\r
- $(tabMenu).find('.tabMenuContent').each(function(index, tabMenuItem) {\r
- if ($(tabMenuItem).attr('id') == $(tabMenu).data('active')) {\r
- $(tabMenu).wcfTabs('select', index);\r
- }\r
- });\r
- }\r
- });\r
- }\r
-};\r
-\r
-/**\r
- * Toggles options.\r
- * \r
- * @param string element\r
- * @param array showItems\r
- * @param array hideItems\r
- */\r
-WCF.ToggleOptions = function(element, showItems, hideItems) { this.init(element, showItems, hideItems); };\r
-WCF.ToggleOptions.prototype = {\r
- /**\r
- * target item\r
- * \r
- * @var jQuery\r
- */\r
- _element: null,\r
- \r
- /**\r
- * list of items to be shown\r
- * \r
- * @var array\r
- */\r
- _showItems: [],\r
- \r
- /**\r
- * list of items to be hidden\r
- * \r
- * @var array\r
- */\r
- _hideItems: [],\r
- \r
- /**\r
- * Initializes option toggle.\r
- * \r
- * @param string element\r
- * @param array showItems\r
- * @param array hideItems\r
- */\r
- init: function(element, showItems, hideItems) {\r
- this._element = $('#' + element);\r
- this._showItems = showItems;\r
- this._hideItems = hideItems;\r
- \r
- // bind event\r
- this._element.click($.proxy(this._toggle, this));\r
- \r
- // execute toggle on init\r
- this._toggle();\r
- },\r
- \r
- /**\r
- * Toggles items.\r
- */\r
- _toggle: function() {\r
- if (!this._element.attr('checked')) return;\r
- \r
- for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) {\r
- var $item = this._showItems[$i];\r
- \r
- $('#' + $item).show();\r
- }\r
- \r
- for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) {\r
- var $item = this._hideItems[$i];\r
- \r
- $('#' + $item).hide();\r
- }\r
- }\r
-}\r
-\r
-/**\r
- * Basic implementation for WCF dialogs.\r
- */\r
-$.widget('ui.wcfDialog', $.ui.dialog, {\r
- _init: function() {\r
- this.options.autoOpen = true;\r
- this.options.close = function(event, ui) {\r
- $(this).parent('.ui-dialog').wcfDropOut('down', $.proxy(function() {\r
- $(this).parent('.ui-dialog').empty().remove();\r
- }, this));\r
- };\r
- this.options.height = 'auto';\r
- this.options.minHeight = 0;\r
- this.options.modal = true;\r
- this.options.width = 'auto';\r
- \r
- $.ui.dialog.prototype._init.apply(this, arguments);\r
- }\r
-});\r
-\r
-/**\r
- * Basic implementation for WCF dialogs loading content\r
- * via AJAX before calling dialog itself.\r
- */\r
-$.widget('ui.wcfAJAXDialog', $.ui.dialog, {\r
- /**\r
- * Indicates wether callback was already executed\r
- * \r
- * @var boolean\r
- */\r
- _callbackExecuted: false,\r
- \r
- /**\r
- * Initializes AJAX-request to fetch content.\r
- */\r
- _init: function() {\r
- if (this.options.ajax) {\r
- this._loadContent();\r
- }\r
- \r
- // force dialog to be placed centered\r
- this.options.position = {\r
- my: 'center center',\r
- at: 'center center'\r
- };\r
- \r
- // dialog should display a spinner-like image, thus immediately fire up dialog\r
- this.options.autoOpen = true;\r
- this.options.width = 'auto';\r
- this.options.minHeight = 80;\r
- \r
- // disable ability to move dialog\r
- this.options.resizable = false;\r
- this.options.draggable = false;\r
- \r
- this.options.modal = true;\r
- this.options.hide = {\r
- effect: 'drop',\r
- direction: 'down'\r
- };\r
- \r
- this.options.close = function(event, ui) {\r
- // loading ajax content seems to block properly closing\r
- $(this).parent('.ui-dialog').empty().remove();\r
- };\r
- \r
- if (this.options.preventClose) {\r
- this.options.closeOnEscape = false;\r
- }\r
- \r
- $.ui.dialog.prototype._init.apply(this, arguments);\r
- \r
- // remove complete node instead of removing node-by-node\r
- if (this.options.hideTitle && this.options.preventClose) {\r
- this.element.parent('.ui-dialog').find('div.ui-dialog-titlebar').empty().remove();\r
- }\r
- else {\r
- if (this.options.hideTitle) {\r
- // remove title element\r
- $('#ui-dialog-title-' + this.element.attr('id')).empty().remove();\r
- }\r
- \r
- if (this.options.preventClose) {\r
- // remove close-button\r
- this.element.parent('.ui-dialog').find('a.ui-dialog-titlebar-close').empty().remove();\r
- }\r
- }\r
- },\r
- \r
- /**\r
- * Loads content via AJAX.\r
- * \r
- * @todo Enforce JSON\r
- */\r
- _loadContent: function() {\r
- var $type = 'GET';\r
- if (this.options.ajax.type) {\r
- $type = this.options.ajax.type;\r
- \r
- if (this.options.ajax.type != 'GET' && this.options.ajax.type != 'POST') {\r
- $type = 'GET';\r
- }\r
- }\r
- \r
- var $data = this.options.ajax.data || {};\r
- \r
- $.ajax({\r
- url: this.options.ajax.url,\r
- context: this,\r
- dataType: 'json',\r
- type: $type,\r
- data: $data,\r
- success: $.proxy(this._createDialog, this),\r
- error: function(transport) {\r
- alert(transport.responseText);\r
- }\r
- });\r
- },\r
- \r
- /**\r
- * Inserts content.\r
- * \r
- * @param string data\r
- */\r
- _createDialog: function(data) {\r
- data.ignoreTemplate = true;\r
- this.element.data('responseData', data);\r
- \r
- this.element.wcfGrow({\r
- content: data.template,\r
- parent: this.element.parent('.ui-dialog')\r
- }, {\r
- duration: 600,\r
- complete: $.proxy(function(data) {\r
- this.element.css({\r
- height: 'auto'\r
- });\r
- \r
- // prevent double execution due to two complete-calls (two times animate)\r
- if (this._callbackExecuted) {\r
- return;\r
- }\r
- \r
- this._callbackExecuted = true;\r
- \r
- this.element.removeClass('overlayLoading');\r
- this.element.html(this.element.data('responseData').template);\r
- \r
- if (this.options.ajax.success) {\r
- this.options.ajax.success();\r
- }\r
- }, this)\r
- });\r
- },\r
- \r
- /**\r
- * Redraws dialog, should be executed everytime content is changed.\r
- */\r
- redraw: function() {\r
- var $dimensions = this.element.getDimensions();\r
- \r
- if ($dimensions.height > 200) {\r
- this.element.wcfGrow({\r
- content: this.element.html(),\r
- parent: this.element.parent('.ui-dialog')\r
- }, {\r
- duration: 600,\r
- complete: function() {\r
- $(this).css({ height: 'auto' });\r
- }\r
- });\r
- }\r
- }\r
-});\r
-\r
-/**\r
- * Workaround for ids containing a dot ".", until jQuery UI devs learn\r
- * to properly escape ids ... (it took 18 months until they finally\r
- * fixed it!)\r
- * \r
- * @see http://bugs.jqueryui.com/ticket/4681\r
- */\r
-$.widget('ui.wcfTabs', $.ui.tabs, {\r
- _init: function() {\r
- $.ui.dialog.prototype._init.apply(this, arguments);\r
- },\r
- \r
- _sanitizeSelector: function(hash) {\r
- return hash.replace(/([:\.])/g, '\\$1');\r
- }\r
-});\r
-\r
-/**\r
- * Encapsulate eval() within an own function to prevent problems\r
- * with optimizing and minifiny JS.\r
- * \r
- * @param mixed expression\r
- * @returns mixed\r
- */\r
-function wcfEval(expression) {\r
- return eval(expression);\r
-}
\ No newline at end of file
+/**
+ * Class and function collection for WCF
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+
+/**
+ * Initialize WCF namespace
+ */
+var WCF = {};
+
+/**
+ * Extends jQuery with additional methods.
+ */
+$.extend(true, {
+ /**
+ * Escapes an ID to work with jQuery selectors.
+ *
+ * @see http://docs.jquery.com/Frequently_Asked_Questions#How_do_I_select_an_element_by_an_ID_that_has_characters_used_in_CSS_notation.3F
+ * @param string id
+ * @return string
+ */
+ wcfEscapeID: function(id) {
+ return id.replace(/(:|\.)/g, '\\$1');
+ },
+
+ /**
+ * Returns true if given ID exists within DOM.
+ *
+ * @param string id
+ * @return boolean
+ */
+ wcfIsset: function(id) {
+ return !!$('#' + $.wcfEscapeID(id)).length;
+ }
+});
+
+/**
+ * Extends jQuery's chainable methods.
+ */
+$.fn.extend({
+ /**
+ * Returns tag name of current jQuery element.
+ *
+ * @returns string
+ */
+ getTagName: function() {
+ return this.get(0).tagName.toLowerCase();
+ },
+
+ /**
+ * Returns the dimensions for current element.
+ *
+ * @see http://api.jquery.com/hidden-selector/
+ * @param string type
+ * @return object
+ */
+ getDimensions: function(type) {
+ var dimensions = css = {};
+ var wasHidden = false;
+
+ // show element to retrieve dimensions and restore them later
+ if (this.is(':hidden')) {
+ css = {
+ display: this.css('display'),
+ visibility: this.css('visibility')
+ };
+
+ wasHidden = true;
+
+ this.css({
+ display: 'block',
+ visibility: 'hidden'
+ });
+ }
+
+ switch (type) {
+ case 'inner':
+ dimensions = {
+ height: this.innerHeight(),
+ width: this.innerWidth()
+ };
+ break;
+
+ case 'outer':
+ dimensions = {
+ height: this.outerHeight(),
+ width: this.innerWidth()
+ };
+ break;
+
+ default:
+ dimensions = {
+ height: this.height(),
+ width: this.width()
+ };
+ break;
+ }
+
+ // restore previous settings
+ if (wasHidden) {
+ this.css(css);
+ }
+
+ return dimensions;
+ },
+
+ /**
+ * Returns the offsets for current element, defaults to position
+ * relative to document.
+ *
+ * @see http://api.jquery.com/hidden-selector/
+ * @param string type
+ * @return object
+ */
+ getOffsets: function(type) {
+ var offsets = css = {};
+ var wasHidden = false;
+
+ // show element to retrieve dimensions and restore them later
+ if (this.is(':hidden')) {
+ css = {
+ display: this.css('display'),
+ visibility: this.css('visibility')
+ };
+
+ wasHidden = true;
+
+ this.css({
+ display: 'block',
+ visibility: 'hidden'
+ });
+ }
+
+ switch (type) {
+ case 'offset':
+ offsets = this.offset();
+ break;
+
+ case 'position':
+ default:
+ offsets = this.position();
+ break;
+ }
+
+ // restore previous settings
+ if (wasHidden) {
+ this.css(css);
+ }
+
+ return offsets;
+ },
+
+ /**
+ * Changes element's position to 'absolute' or 'fixed' while maintaining it's
+ * current position relative to viewport. Optionally removes element from
+ * current DOM-node and moving it into body-element (useful for drag & drop)
+ *
+ * @param boolean rebase
+ * @return object
+ */
+ makePositioned: function(position, rebase) {
+ if (position != 'absolute' && position != 'fixed') {
+ position = 'absolute';
+ }
+
+ var $currentPosition = this.getOffsets('position');
+ this.css({
+ position: position,
+ left: $currentPosition.left,
+ margin: 0,
+ top: $currentPosition.top
+ });
+
+ if (rebase) {
+ this.remove().appentTo('body');
+ }
+
+ return this;
+ },
+
+ /**
+ * Disables a form element.
+ *
+ * @return jQuery
+ */
+ disable: function() {
+ return this.attr('disabled', 'disabled');
+ },
+
+ /**
+ * Enables a form element.
+ *
+ * @return jQuery
+ */
+ enable: function() {
+ return this.removeAttr('disabled');
+ },
+
+ /**
+ * Applies a grow-effect by resizing element while moving the
+ * element appropriately
+ *
+ * @param object data
+ * @param object options
+ * @return jQuery
+ */
+ wcfGrow: function(data, options) {
+ // create temporarily element to determine dimensions
+ var $tempElementID = WCF.getRandomID();
+ $('body').append('<div id="' + $tempElementID + '" class="wcfDimensions">' + data.content + '</div>');
+ var $tempElement = $('#' + $tempElementID);
+
+ // get content dimensions
+ var $dimensions = $tempElement.getDimensions();
+
+ // remove temporarily element
+ $tempElement.empty().remove();
+
+ // move parent element, used if applying effect on dialogs
+ if (!data.parent) {
+ data.parent = this;
+ }
+
+ // calculate values for grow-effect
+ var $borderHeight = parseInt(data.parent.css('borderTopWidth')) + parseInt(data.parent.css('borderBottomWidth'));
+ var $borderWidth = parseInt(data.parent.css('borderLeftWidth')) + parseInt(data.parent.css('borderRightWidth'));
+
+ var $windowDimensions = $(window).getDimensions();
+ var $leftOffset = Math.round(($windowDimensions.width - ($dimensions.width + $borderWidth)) / 2);
+ var $topOffset = Math.round(($windowDimensions.height - ($dimensions.height + $borderHeight)) / 2);
+
+ data.parent.makePositioned('fixed', false);
+ data.parent.animate({
+ left: $leftOffset + 'px',
+ top: $topOffset + 'px'
+ }, options);
+
+ return this.animate({
+ height: $dimensions.height,
+ width: $dimensions.width
+ }, options);
+ },
+
+ /**
+ * Shows an element by sliding and fading it into viewport.
+ *
+ * @param string direction
+ * @param object callback
+ * @returns jQuery
+ */
+ wcfDropIn: function(direction, callback) {
+ if (!direction) direction = 'up';
+
+ return this.show(WCF.getEffect(this.getTagName(), 'drop'), { direction: direction }, 600, callback);
+ },
+
+ /**
+ * Hides an element by sliding and fading it out the viewport.
+ *
+ * @param string direction
+ * @param object callback
+ * @returns jQuery
+ */
+ wcfDropOut: function(direction, callback) {
+ if (!direction) direction = 'down';
+
+ return this.hide(WCF.getEffect(this.getTagName(), 'drop'), { direction: direction }, 600, callback);
+ },
+
+ /**
+ * Shows an element by blinding it up.
+ *
+ * @param string direction
+ * @param object callback
+ * @returns jQuery
+ */
+ wcfBlindIn: function(direction, callback) {
+ if (!direction) direction = 'vertical';
+
+ return this.show(WCF.getEffect(this.getTagName(), 'blind'), { direction: direction }, 200, callback);
+ },
+
+ /**
+ * Hides an element by blinding it down.
+ *
+ * @param string direction
+ * @param object callback
+ * @returns jQuery
+ */
+ wcfBlindOut: function(direction, callback) {
+ if (!direction) direction = 'vertical';
+
+ return this.hide(WCF.getEffect(this.getTagName(), 'blind'), { direction: direction }, 200, callback);
+ },
+
+ /**
+ * Highlights an element.
+ *
+ * @param object options
+ * @param object callback
+ * @returns jQuery
+ */
+ wcfHighlight: function(options, callback) {
+ return this.effect('highlight', options, 600, callback);
+ }
+});
+
+/**
+ * WoltLab Community Framework core methods
+ */
+$.extend(WCF, {
+ /**
+ * Counter for dynamic element id's
+ *
+ * @var integer
+ */
+ _idCounter: 0,
+
+ /**
+ * Shows a modal dialog with a built-in AJAX-loader.
+ *
+ * @param string dialogID
+ * @param boolean resetDialog
+ */
+ showAJAXDialog: function(dialogID, resetDialog) {
+ if (!dialogID) {
+ dialogID = this.getRandomID();
+ }
+
+ if (!$.wcfIsset(dialogID)) {
+ $('body').append($('<div id="' + dialogID + '"></div>'));
+ }
+
+ var dialog = $('#' + $.wcfEscapeID(dialogID));
+
+ if (resetDialog) {
+ dialog.empty();
+ }
+
+ dialog.addClass('overlayLoading');
+
+ var dialogOptions = arguments[2] || {};
+ dialog.wcfAJAXDialog(dialogOptions);
+ },
+
+ /**
+ * Shows a modal dialog.
+ * @param string dialogID
+ */
+ showDialog: function(dialogID) {
+ // we cannot work with a non-existant dialog, if you wish to
+ // load content via AJAX, see showAJAXDialog() instead
+ if (!$.wcfIsset(dialogID)) return;
+
+ var $dialog = $('#' + $.wcfEscapeID(dialogID));
+
+ var dialogOptions = arguments[2] || {};
+ $dialog.wcfDialog(dialogOptions);
+ },
+
+ /**
+ * Returns a dynamically created id.
+ *
+ * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/dom/dom.js#L1789
+ * @return string
+ */
+ getRandomID: function() {
+ var $elementID = '';
+
+ do {
+ $elementID = 'wcf' + this._idCounter++;
+ }
+ while ($.wcfIsset($elementID));
+
+ return $elementID;
+ },
+
+ /**
+ * Wrapper for $.inArray which returns boolean value instead of
+ * index value, similar to PHP's in_array().
+ *
+ * @param mixed needle
+ * @param array haystack
+ * @return boolean
+ */
+ inArray: function(needle, haystack) {
+ return ($.inArray(needle, haystack) != -1);
+ },
+
+ /**
+ * Adjusts effect for partially supported elements.
+ *
+ * @param object object
+ * @param string effect
+ * @return string
+ */
+ getEffect: function(tagName, effect) {
+ // most effects are not properly supported on table rows, use highlight instead
+ if (tagName == 'tr') {
+ return 'highlight';
+ }
+
+ return effect;
+ }
+});
+
+/**
+ * Provides a simple call for periodical executed functions. Based upon
+ * ideas by Prototype's PeriodicalExecuter.
+ *
+ * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/periodical_executer.js
+ * @param function callback
+ * @param integer delay
+ */
+WCF.PeriodicalExecuter = function(callback, delay) { this.init(callback, delay); };
+WCF.PeriodicalExecuter.prototype = {
+ /**
+ * Initializes a periodical executer.
+ *
+ * @param function callback
+ * @param integer delay
+ */
+ init: function(callback, delay) {
+ this.callback = callback;
+ this.delay = delay;
+ this.loop = true;
+
+ this.intervalID = setInterval($.proxy(this._execute, this), this.delay);
+ },
+
+ /**
+ * Executes callback.
+ */
+ _execute: function() {
+ this.callback(this);
+
+ if (!this.loop) {
+ clearInterval(this.intervalID);
+ }
+ },
+
+ /**
+ * Terminates loop.
+ */
+ stop: function() {
+ this.loop = false;
+ }
+};
+
+/**
+ * Namespace for AJAXProxies
+ */
+WCF.Action = {};
+
+/**
+ * Basic implementation for AJAX-based proxyies
+ *
+ * @param object options
+ */
+WCF.Action.Proxy = function(options) { this.init(options); };
+WCF.Action.Proxy.prototype = {
+ /**
+ * Initializes AJAXProxy.
+ *
+ * @param object options
+ */
+ init: function(options) {
+ // initialize default values
+ this.options = $.extend(true, {
+ autoSend: false,
+ data: { },
+ after: null,
+ init: null,
+ failure: null,
+ success: null,
+ type: 'POST',
+ url: 'index.php?action=AJAXProxy&t=' + SECURITY_TOKEN + SID_ARG_2ND
+ }, options);
+
+ this.confirmationDialog = null;
+ this.loading = null;
+
+ // send request immediately after initialization
+ if (this.options.autoSend) {
+ this.sendRequest();
+ }
+ },
+
+ /**
+ * Sends an AJAX request.
+ */
+ sendRequest: function() {
+ this._init();
+
+ $.ajax({
+ data: this.options.data,
+ dataType: 'json',
+ type: this.options.type,
+ url: this.options.url,
+ success: $.proxy(this._success, this),
+ error: $.proxy(this._failure, this)
+ });
+ },
+
+ /**
+ * Fires before request is send, displays global loading status.
+ */
+ _init: function() {
+ if ($.isFunction(this.options.init)) {
+ this.options.init();
+ }
+
+ $('<div id="actionProxyLoading" style="display: none;">Loading …</div>').appendTo($('body'));
+ this.loading = $('#actionProxyLoading');
+ this.loading.wcfDropIn();
+ },
+
+ /**
+ * Handles AJAX errors.
+ *
+ * @param object jqXHR
+ * @param string textStatus
+ * @param string errorThrown
+ */
+ _failure: function(jqXHR, textStatus, errorThrown) {
+ try {
+ var data = $.parseJSON(jqXHR.responseText);
+
+ // call child method if applicable
+ if ($.isFunction(this.options.failure)) {
+ this.options.failure(jqXHR, textStatus, errorThrown, data);
+ }
+
+ var $randomID = WCF.getRandomID();
+ $('<div id="' + $randomID + '" title="HTTP/1.0 ' + jqXHR.status + ' ' + errorThrown + '"><p>Der Server antwortete: ' + data.message + '.</p></div>').wcfDialog();
+ }
+ // failed to parse JSON
+ catch (e) {
+ var $randomID = WCF.getRandomID();
+ $('<div id="' + $randomID + '" title="HTTP/1.0 ' + jqXHR.status + ' ' + errorThrown + '"><p>Der Server antwortete: ' + jqXHR.responseText + '.</p></div>').wcfDialog();
+ }
+
+ this._after();
+ },
+
+ /**
+ * Handles successful AJAX requests.
+ *
+ * @param object data
+ * @param string textStatus
+ * @param object jqXHR
+ */
+ _success: function(data, textStatus, jqXHR) {
+ // call child method if applicable
+ if ($.isFunction(this.options.success)) {
+ this.options.success(data, textStatus, jqXHR);
+ }
+
+ this._after();
+ },
+
+ /**
+ * Fires after an AJAX request, hides global loading status.
+ */
+ _after: function() {
+ if ($.isFunction(this.options.after)) {
+ this.options.after();
+ }
+
+ this.loading.wcfDropOut('up');
+ },
+
+ /**
+ * Sets options, MUST be used to set parameters before sending request
+ * if calling from child classes.
+ *
+ * @param string optionName
+ * @param mixed optionData
+ */
+ setOption: function(optionName, optionData) {
+ this.options[optionName] = optionData;
+ }
+};
+
+/**
+ * Basic implementation for simple proxy access using bound elements.
+ *
+ * @param object options
+ * @param object callbacks
+ */
+WCF.Action.SimpleProxy = function(options, callbacks) { this.init(options, callbacks); };
+WCF.Action.SimpleProxy.prototype = {
+ /**
+ * Initializes SimpleProxy.
+ *
+ * @param object options
+ * @param object callbacks
+ */
+ init: function(options, callbacks) {
+ /**
+ * action-specific options
+ */
+ this.options = $.extend(true, {
+ action: '',
+ className: '',
+ elements: null,
+ eventName: 'click'
+ }, options);
+
+ /**
+ * proxy-specific options
+ */
+ this.callbacks = $.extend(true, {
+ after: null,
+ failure: null,
+ init: null,
+ success: null
+ }, callbacks);
+
+ if (!this.options.elements) return;
+
+ // initialize proxy
+ this.proxy = new WCF.Action.Proxy(this.callbacks);
+
+ // bind event listener
+ this.options.elements.each($.proxy(function(index, element) {
+ $(element).bind(this.options.eventName, $.proxy(this._handleEvent, this));
+ }, this));
+ },
+
+ /**
+ * Handles event actions.
+ *
+ * @param object event
+ */
+ _handleEvent: function(event) {
+ this.proxy.setOption('data', {
+ actionName: this.options.action,
+ className: this.options.className,
+ objectIDs: [ $(event.target).data('objectID') ]
+ });
+
+ this.proxy.sendRequest();
+ }
+};
+
+/**
+ * Basic implementation for AJAXProxy-based deletion.
+ *
+ * @param string className
+ * @param jQuery containerList
+ */
+WCF.Action.Delete = function(className, containerList) { this.init(className, containerList); };
+WCF.Action.Delete.prototype = {
+ /**
+ * Initializes 'delete'-Proxy.
+ *
+ * @param string className
+ * @param jQuery containerList
+ */
+ init: function(className, containerList) {
+ if (!containerList.length) return;
+ this.containerList = containerList;
+ this.className = className;
+
+ // initialize proxy
+ var options = {
+ success: $.proxy(this._success, this)
+ };
+ this.proxy = new WCF.Action.Proxy(options);
+
+ // bind event listener
+ this.containerList.each($.proxy(function(index, container) {
+ $(container).find('.deleteButton').bind('click', $.proxy(this._click, this));
+ }, this));
+ },
+
+ /**
+ * Sends AJAX request.
+ *
+ * @param object event
+ */
+ _click: function(event) {
+ var $target = $(event.target);
+
+ if ($target.data('confirmMessage')) {
+ if (confirm($target.data('confirmMessage'))) {
+ this._sendRequest($target);
+ }
+ }
+ else {
+ this._sendRequest($target);
+ }
+
+ },
+
+ _sendRequest: function(object) {
+ this.proxy.setOption('data', {
+ actionName: 'delete',
+ className: this.className,
+ objectIDs: [ $(object).data('objectID') ]
+ });
+
+ this.proxy.sendRequest();
+ },
+
+ /**
+ * Deletes items from containers.
+ *
+ * @param object data
+ * @param string textStatus
+ * @param object jqXHR
+ */
+ _success: function(data, textStatus, jqXHR) {
+ // remove items
+ this.containerList.each(function(index, container) {
+ var $objectID = $(container).find('.deleteButton').data('objectID');
+ if (WCF.inArray($objectID, data.objectIDs)) {
+ $(container).wcfBlindOut('up', function() {
+ $(container).empty().remove();
+ }, container);
+ }
+ });
+ }
+};
+
+/**
+ * Basic implementation for AJAXProxy-based toggle actions.
+ *
+ * @param string className
+ * @param jQuery containerList
+ */
+WCF.Action.Toggle = function(className, containerList) { this.init(className, containerList); };
+WCF.Action.Toggle.prototype = {
+ /**
+ * Initializes 'toggle'-Proxy
+ *
+ * @param string className
+ * @param jQuery containerList
+ */
+ init: function(className, containerList) {
+ if (!containerList.length) return;
+ this.containerList = containerList;
+ this.className = className;
+
+ // initialize proxy
+ var options = {
+ success: $.proxy(this._success, this)
+ };
+ this.proxy = new WCF.Action.Proxy(options);
+
+ // bind event listener
+ this.containerList.each($.proxy(function(index, container) {
+ $(container).find('.toggleButton').bind('click', $.proxy(this._click, this));
+ }, this));
+ },
+
+ /**
+ * Sends AJAX request.
+ *
+ * @param object event
+ */
+ _click: function(event) {
+ this.proxy.setOption('data', {
+ actionName: 'toggle',
+ className: this.className,
+ objectIDs: [ $(event.target).data('objectID') ]
+ });
+
+ this.proxy.sendRequest();
+ },
+
+ /**
+ * Toggles status icons.
+ *
+ * @param object data
+ * @param string textStatus
+ * @param object jqXHR
+ */
+ _success: function(data, textStatus, jqXHR) {
+ // remove items
+ this.containerList.each(function(index, container) {
+ var $toggleButton = $(container).find('.toggleButton');
+ if (WCF.inArray($toggleButton.data('objectID'), data.objectIDs)) {
+ $(container).wcfHighlight();
+
+ // toggle icon source
+ $toggleButton.attr('src', function() {
+ if (this.src.match(/enabled(S|M|L)\.png$/)) {
+ return this.src.replace(/enabled(S|M|L)\.png$/, 'disabled$1\.png');
+ }
+ else {
+ return this.src.replace(/disabled(S|M|L)\.png$/, 'enabled$1\.png');
+ }
+ });
+ // toogle icon title
+ $toggleButton.attr('title', function() {
+ if (this.src.match(/enabled(S|M|L)\.png$/)) {
+ return $(this).data('disableMessage');
+ }
+ else {
+ return $(this).data('enableMessage');
+ }
+ });
+ }
+ });
+ }
+};
+
+/**
+ * Namespace for date-related functions.
+ */
+WCF.Date = {};
+
+/**
+ * Provides utility functions for date operations.
+ */
+WCF.Date.Util = {
+ /**
+ * Returns UTC timestamp, if date is not given, current time will be used.
+ *
+ * @param Date date
+ * @return integer
+ */
+ gmdate: function(date) {
+ var $date = (date) ? date : new Date();
+
+ return Math.round(Date.UTC(
+ $date.getUTCFullYear(),
+ $date.getUTCMonth(),
+ $date.getUTCDay(),
+ $date.getUTCHours(),
+ $date.getUTCMinutes(),
+ $date.getUTCSeconds()
+ ) / 1000);
+ },
+
+ /**
+ * Returns a Date object with precise offset (including timezone and local timezone).
+ * Parameter timestamp must be in miliseconds!
+ *
+ * @param integer timestamp
+ * @param integer offset
+ * @return Date
+ */
+ getTimezoneDate: function(timestamp, offset) {
+ var $date = new Date(timestamp);
+ var $localOffset = $date.getTimezoneOffset() * -1 * 60000;
+
+ return new Date((timestamp - $localOffset - offset));
+ }
+};
+
+/**
+ * Handles relative time designations.
+ */
+WCF.Date.Time = function() { this.init(); };
+WCF.Date.Time.prototype = {
+ /**
+ * Initializes relative datetimes.
+ */
+ init: function() {
+ // initialize variables
+ this.elements = $('time.datetime');
+ this.timestamp = 0;
+
+ // calculate relative datetime on init
+ this._refresh();
+
+ // re-calculate relative datetime every minute
+ new WCF.PeriodicalExecuter($.proxy(this._refresh, this), 60000);
+ },
+
+ /**
+ * Refreshes relative datetime for each element.
+ */
+ _refresh: function() {
+ // TESTING ONLY!
+ var $date = new Date();
+ this.timestamp = ($date.getTime() - $date.getMilliseconds()) / 1000;
+ // TESTING ONLY!
+
+ this.elements.each($.proxy(this._refreshElement, this));
+ },
+
+ /**
+ * Refreshes relative datetime for current element.
+ *
+ * @param integer index
+ * @param object element
+ */
+ _refreshElement: function(index, element) {
+ if (!$(element).attr('title')) {
+ $(element).attr('title', $(element).text());
+ }
+
+ var $timestamp = $(element).data('timestamp');
+ var $date = $(element).data('date');
+ var $time = $(element).data('time');
+ var $offset = $(element).data('offset');
+
+ // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago)
+ if (this.timestamp < ($timestamp + 3540)) {
+ var $minutes = Math.round((this.timestamp - $timestamp) / 60);
+ $(element).text(eval(WCF.Language.get('wcf.global.date.relative.minutes')));
+ }
+ // timestamp is less than 24 hours ago
+ else if (this.timestamp < ($timestamp + 86400)) {
+ var $hours = Math.round((this.timestamp - $timestamp) / 3600);
+ $(element).text(eval(WCF.Language.get('wcf.global.date.relative.hours')));
+ }
+ // timestamp is less than a week ago
+ else if (this.timestamp < ($timestamp + 604800)) {
+ var $days = Math.round((this.timestamp - $timestamp) / 86400);
+ var $string = eval(WCF.Language.get('wcf.global.date.relative.pastDays'));
+
+ // get day of week
+ var $dateObj = WCF.Date.Util.getTimezoneDate(($timestamp * 1000), $offset);
+ var $dow = $dateObj.getDay();
+
+ $(element).text($string.replace(/\%day\%/, WCF.Language.get('__days')[$dow]).replace(/\%time\%/, $time));
+ }
+ // timestamp is between ~700 million years BC and last week
+ else {
+ var $string = WCF.Language.get('wcf.global.date.dateTimeFormat');
+ $(element).text($string.replace(/\%date\%/, $date).replace(/\%time\%/, $time));
+ }
+ }
+};
+
+/**
+ * Hash-like dictionary. Based upon idead from Prototype's hash
+ *
+ * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js
+ */
+WCF.Dictionary = function() { this.init(); };
+WCF.Dictionary.prototype = {
+ /**
+ * Initializes a new dictionary.
+ */
+ init: function() {
+ this.variables = { };
+ },
+
+ /**
+ * Adds an entry.
+ *
+ * @param string key
+ * @param mixed value
+ */
+ add: function(key, value) {
+ this.variables[key] = value;
+ },
+
+ /**
+ * Adds a traditional object to current dataset.
+ *
+ * @param object object
+ */
+ addObject: function(object) {
+ for (var $key in object) {
+ this.add($key, object[$key]);
+ }
+ },
+
+ /**
+ * Adds a dictionary to current dataset.
+ *
+ * @param object dictionary
+ */
+ addDictionary: function(dictionary) {
+ dictionary.each($.proxy(function(pair) {
+ this.add(pair.key, pair.value);
+ }, this));
+ },
+
+ /**
+ * Retrieves the value of an entry or returns null if key is not found.
+ *
+ * @param string key
+ * @returns mixed
+ */
+ get: function(key) {
+ if (this.isset(key)) {
+ return this.variables[key];
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns true if given key is a valid entry.
+ *
+ * @param string key
+ */
+ isset: function(key) {
+ return this.variables.hasOwnProperty(key);
+ },
+
+ /**
+ * Removes an entry.
+ *
+ * @param string key
+ */
+ remove: function(key) {
+ delete this.variables[key];
+ },
+
+ /**
+ * Iterates through dictionary.
+ *
+ * Usage:
+ * var $hash = new WCF.Dictionary();
+ * $hash.add('foo', 'bar');
+ * $hash.each(function(pair) {
+ * // alerts: foo = bar
+ * alert(pair.key + ' = ' + pair.value);
+ * });
+ *
+ * @param function callback
+ */
+ each: function(callback) {
+ if (!$.isFunction(callback)) {
+ return;
+ }
+
+ for (var $key in this.variables) {
+ var $value = this.variables[$key];
+ var $pair = {
+ key: $key,
+ value: $value
+ };
+
+ callback($pair);
+ }
+ }
+};
+
+/**
+ * Global language storage.
+ *
+ * @see WCF.Dictionary
+ */
+WCF.Language = {
+ _variables: new WCF.Dictionary(),
+
+ /**
+ * @see WCF.Dictionary.addObject()
+ */
+ add: function(key, value) {
+ this._variables.add(key, value);
+ },
+
+ /**
+ * @see WCF.Dictionary.addObject()
+ */
+ addObject: function(object) {
+ this._variables.addObject(object);
+ },
+
+ /**
+ * Retrieves a variable.
+ *
+ * @param string key
+ * @return mixed
+ */
+ get: function(key) {
+ return this._variables.get(key);
+ }
+};
+
+/**
+ * String utilities.
+ */
+WCF.String = {
+ /**
+ * Makes a string's first character uppercase
+ *
+ * @param string string
+ * @return string
+ */
+ ucfirst: function(string) {
+ return string.substring(0, 1).toUpperCase() + string.substring(1);
+ }
+};
+
+/**
+ * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the
+ * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute
+ * which will be filled with the currently selected tab.
+ */
+WCF.TabMenu = {
+ /**
+ * Initializes all TabMenus
+ */
+ init: function() {
+ $('.tabMenuContainer').each(function(index, tabMenu) {
+ if (!$(tabMenu).attr('id')) {
+ var $randomID = WCF.getRandomID();
+ $(tabMenu).attr('id', $randomID);
+ }
+
+ // init jQuery UI TabMenu
+ $(tabMenu).wcfTabs({
+ select: function(event, ui) {
+ var $panel = $(ui.panel);
+ var $container = $panel.closest('.tabMenuContainer');
+
+ // store currently selected item
+ if ($container.data('store')) {
+ if ($.wcfIsset($container.data('store'))) {
+ $('#' + $container.data('store')).attr('value', $panel.attr('id'));
+ }
+ }
+ }
+ });
+
+ // display active item on init
+ if ($(tabMenu).data('active')) {
+ $(tabMenu).find('.tabMenuContent').each(function(index, tabMenuItem) {
+ if ($(tabMenuItem).attr('id') == $(tabMenu).data('active')) {
+ $(tabMenu).wcfTabs('select', index);
+ }
+ });
+ }
+ });
+ }
+};
+
+/**
+ * Toggles options.
+ *
+ * @param string element
+ * @param array showItems
+ * @param array hideItems
+ */
+WCF.ToggleOptions = function(element, showItems, hideItems) { this.init(element, showItems, hideItems); };
+WCF.ToggleOptions.prototype = {
+ /**
+ * target item
+ *
+ * @var jQuery
+ */
+ _element: null,
+
+ /**
+ * list of items to be shown
+ *
+ * @var array
+ */
+ _showItems: [],
+
+ /**
+ * list of items to be hidden
+ *
+ * @var array
+ */
+ _hideItems: [],
+
+ /**
+ * Initializes option toggle.
+ *
+ * @param string element
+ * @param array showItems
+ * @param array hideItems
+ */
+ init: function(element, showItems, hideItems) {
+ this._element = $('#' + element);
+ this._showItems = showItems;
+ this._hideItems = hideItems;
+
+ // bind event
+ this._element.click($.proxy(this._toggle, this));
+
+ // execute toggle on init
+ this._toggle();
+ },
+
+ /**
+ * Toggles items.
+ */
+ _toggle: function() {
+ if (!this._element.attr('checked')) return;
+
+ for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) {
+ var $item = this._showItems[$i];
+
+ $('#' + $item).show();
+ }
+
+ for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) {
+ var $item = this._hideItems[$i];
+
+ $('#' + $item).hide();
+ }
+ }
+}
+
+/**
+ * Basic implementation for WCF dialogs.
+ */
+$.widget('ui.wcfDialog', $.ui.dialog, {
+ _init: function() {
+ this.options.autoOpen = true;
+ this.options.close = function(event, ui) {
+ $(this).parent('.ui-dialog').wcfDropOut('down', $.proxy(function() {
+ $(this).parent('.ui-dialog').empty().remove();
+ }, this));
+ };
+ this.options.height = 'auto';
+ this.options.minHeight = 0;
+ this.options.modal = true;
+ this.options.width = 'auto';
+
+ $.ui.dialog.prototype._init.apply(this, arguments);
+ }
+});
+
+/**
+ * Basic implementation for WCF dialogs loading content
+ * via AJAX before calling dialog itself.
+ */
+$.widget('ui.wcfAJAXDialog', $.ui.dialog, {
+ /**
+ * Indicates wether callback was already executed
+ *
+ * @var boolean
+ */
+ _callbackExecuted: false,
+
+ /**
+ * Initializes AJAX-request to fetch content.
+ */
+ _init: function() {
+ if (this.options.ajax) {
+ this._loadContent();
+ }
+
+ // force dialog to be placed centered
+ this.options.position = {
+ my: 'center center',
+ at: 'center center'
+ };
+
+ // dialog should display a spinner-like image, thus immediately fire up dialog
+ this.options.autoOpen = true;
+ this.options.width = 'auto';
+ this.options.minHeight = 80;
+
+ // disable ability to move dialog
+ this.options.resizable = false;
+ this.options.draggable = false;
+
+ this.options.modal = true;
+ this.options.hide = {
+ effect: 'drop',
+ direction: 'down'
+ };
+
+ this.options.close = function(event, ui) {
+ // loading ajax content seems to block properly closing
+ $(this).parent('.ui-dialog').empty().remove();
+ };
+
+ if (this.options.preventClose) {
+ this.options.closeOnEscape = false;
+ }
+
+ $.ui.dialog.prototype._init.apply(this, arguments);
+
+ // remove complete node instead of removing node-by-node
+ if (this.options.hideTitle && this.options.preventClose) {
+ this.element.parent('.ui-dialog').find('div.ui-dialog-titlebar').empty().remove();
+ }
+ else {
+ if (this.options.hideTitle) {
+ // remove title element
+ $('#ui-dialog-title-' + this.element.attr('id')).empty().remove();
+ }
+
+ if (this.options.preventClose) {
+ // remove close-button
+ this.element.parent('.ui-dialog').find('a.ui-dialog-titlebar-close').empty().remove();
+ }
+ }
+ },
+
+ /**
+ * Loads content via AJAX.
+ *
+ * @todo Enforce JSON
+ */
+ _loadContent: function() {
+ var $type = 'GET';
+ if (this.options.ajax.type) {
+ $type = this.options.ajax.type;
+
+ if (this.options.ajax.type != 'GET' && this.options.ajax.type != 'POST') {
+ $type = 'GET';
+ }
+ }
+
+ var $data = this.options.ajax.data || {};
+
+ $.ajax({
+ url: this.options.ajax.url,
+ context: this,
+ dataType: 'json',
+ type: $type,
+ data: $data,
+ success: $.proxy(this._createDialog, this),
+ error: function(transport) {
+ alert(transport.responseText);
+ }
+ });
+ },
+
+ /**
+ * Inserts content.
+ *
+ * @param string data
+ */
+ _createDialog: function(data) {
+ data.ignoreTemplate = true;
+ this.element.data('responseData', data);
+
+ this.element.wcfGrow({
+ content: data.template,
+ parent: this.element.parent('.ui-dialog')
+ }, {
+ duration: 600,
+ complete: $.proxy(function(data) {
+ this.element.css({
+ height: 'auto'
+ });
+
+ // prevent double execution due to two complete-calls (two times animate)
+ if (this._callbackExecuted) {
+ return;
+ }
+
+ this._callbackExecuted = true;
+
+ this.element.removeClass('overlayLoading');
+ this.element.html(this.element.data('responseData').template);
+
+ if (this.options.ajax.success) {
+ this.options.ajax.success();
+ }
+ }, this)
+ });
+ },
+
+ /**
+ * Redraws dialog, should be executed everytime content is changed.
+ */
+ redraw: function() {
+ var $dimensions = this.element.getDimensions();
+
+ if ($dimensions.height > 200) {
+ this.element.wcfGrow({
+ content: this.element.html(),
+ parent: this.element.parent('.ui-dialog')
+ }, {
+ duration: 600,
+ complete: function() {
+ $(this).css({ height: 'auto' });
+ }
+ });
+ }
+ }
+});
+
+/**
+ * Workaround for ids containing a dot ".", until jQuery UI devs learn
+ * to properly escape ids ... (it took 18 months until they finally
+ * fixed it!)
+ *
+ * @see http://bugs.jqueryui.com/ticket/4681
+ */
+$.widget('ui.wcfTabs', $.ui.tabs, {
+ _init: function() {
+ $.ui.dialog.prototype._init.apply(this, arguments);
+ },
+
+ _sanitizeSelector: function(hash) {
+ return hash.replace(/([:\.])/g, '\\$1');
+ }
+});
+
+/**
+ * Encapsulate eval() within an own function to prevent problems
+ * with optimizing and minifiny JS.
+ *
+ * @param mixed expression
+ * @returns mixed
+ */
+function wcfEval(expression) {
+ return eval(expression);
+}