From b6bd1c4148ea402059d5c0fe168382863cf7c30f Mon Sep 17 00:00:00 2001 From: Brandon Kelly Date: Wed, 5 Feb 2020 12:47:55 -0800 Subject: [PATCH] Craft.downloadFromUrl() + better element exporting Resolves #5558 --- CHANGELOG-v3.md | 7 ++ src/controllers/BaseElementsController.php | 1 - .../ElementIndexSettingsController.php | 14 +++ src/controllers/ElementIndexesController.php | 97 ++++++++++++++----- src/controllers/ElementsController.php | 14 +++ src/controllers/ExportController.php | 1 + .../_components/utilities/DbBackup.html | 1 + src/web/assets/cp/dist/js/Craft.js | 72 +++++++++++--- src/web/assets/cp/dist/js/Craft.min.js | 2 +- src/web/assets/cp/dist/js/Craft.min.js.map | 2 +- src/web/assets/cp/src/js/BaseElementIndex.js | 21 ++-- src/web/assets/cp/src/js/Craft.js | 49 ++++++++++ .../assets/dbbackup/dist/DbBackupUtility.js | 56 ++++------- .../dbbackup/dist/DbBackupUtility.min.js | 2 +- .../dbbackup/dist/DbBackupUtility.min.js.map | 2 +- 15 files changed, 249 insertions(+), 92 deletions(-) diff --git a/CHANGELOG-v3.md b/CHANGELOG-v3.md index fead4b84037..bb48ebb0294 100644 --- a/CHANGELOG-v3.md +++ b/CHANGELOG-v3.md @@ -6,6 +6,12 @@ - Added the ability to limit multiple selections in admin tables. - Added an event to admin tables when selections are changed. - Added an event to admin tables to retrieve currently visible data. +- Added `craft\controllers\ElementIndexesController::actionExport()`. +- Added the `Craft.downloadFromUrl()` JavaScript method. + +### Deprecated +- Deprecated `craft\controllers\ElementIndexesController::actionCreateExportToken()`. +- Deprecated `craft\controllers\ExportController`. ### Fixed - Fixed a bug where data tables weren’t getting horizontal scrollbars in Firefox. ([#5574](https://github.com/craftcms/cms/issues/5574)) @@ -15,6 +21,7 @@ - Fixed a bug where the `_count` field would sometimes not work correctly when using GraphQL. ([#4847](https://github.com/craftcms/cms/issues/4847)) - Fixed a bug where assets that had been drag-uploaded to an Assets field would be hyperlinked. ([#5584](https://github.com/craftcms/cms/issues/5584)) - Fixed a bug where `CustomFieldBehavior.php` was getting created with restricted permissions. ([#5570](https://github.com/craftcms/cms/issues/5570)) +- Fixed a bug where element exporting would redirect the browser window if the export request didn’t immediately return the export data. ([#5558](https://github.com/craftcms/cms/issues/5558)) ## 3.4.3 - 2020-02-03 diff --git a/src/controllers/BaseElementsController.php b/src/controllers/BaseElementsController.php index 2272f19caf9..2841432f530 100644 --- a/src/controllers/BaseElementsController.php +++ b/src/controllers/BaseElementsController.php @@ -29,7 +29,6 @@ abstract class BaseElementsController extends Controller */ public function init() { - $this->requireAcceptsJson(); $this->requireCpRequest(); parent::init(); } diff --git a/src/controllers/ElementIndexSettingsController.php b/src/controllers/ElementIndexSettingsController.php index 59a4af7e11e..ae68b443494 100644 --- a/src/controllers/ElementIndexSettingsController.php +++ b/src/controllers/ElementIndexSettingsController.php @@ -19,6 +19,20 @@ */ class ElementIndexSettingsController extends BaseElementsController { + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if (!parent::beforeAction($action)) { + return false; + } + + $this->requireAcceptsJson(); + + return true; + } + /** * Returns all the info needed by the Customize Sources modal. * diff --git a/src/controllers/ElementIndexesController.php b/src/controllers/ElementIndexesController.php index b23ff76d44e..3fd7cb5f10d 100644 --- a/src/controllers/ElementIndexesController.php +++ b/src/controllers/ElementIndexesController.php @@ -88,6 +88,10 @@ public function beforeAction($action) return false; } + if ($action->id !== 'export') { + $this->requireAcceptsJson(); + } + $request = Craft::$app->getRequest(); $this->elementType = $this->elementType(); $this->context = $this->context(); @@ -257,6 +261,41 @@ public function actionGetSourceTreeHtml(): Response ]); } + /** + * Exports element data. + * + * @return Response + * @throws BadRequestHttpException + * @since 3.4.4 + */ + public function actionExport(): Response + { + $exporter = $this->_exporter(); + $exporter->setElementType($this->elementType); + + $request = Craft::$app->getRequest(); + $response = Craft::$app->getResponse(); + + $response->data = $exporter->export($this->elementQuery); + $response->format = $request->getBodyParam('format', 'csv'); + $response->setDownloadHeaders($exporter->getFilename() . ".{$response->format}"); + + switch ($response->format) { + case Response::FORMAT_JSON: + $response->formatters[Response::FORMAT_JSON]['prettyPrint'] = true; + break; + case Response::FORMAT_XML: + Craft::$app->language = 'en-US'; + /** @var string|ElementInterface $elementType */ + $elementType = $this->elementType; + $response->formatters[Response::FORMAT_XML]['rootTag'] = $elementType::pluralLowerDisplayName(); + $response->formatters[Response::FORMAT_XML]['itemTag'] = $elementType::lowerDisplayName(); + break; + } + + return $response; + } + /** * Creates an export token. * @@ -264,43 +303,20 @@ public function actionGetSourceTreeHtml(): Response * @throws BadRequestHttpException * @throws ServerErrorHttpException * @since 3.2.0 + * @deprecated in 3.4.4 */ public function actionCreateExportToken(): Response { - if (!$this->sourceKey) { - throw new BadRequestHttpException('Request missing required body param'); - } - - if ($this->context !== 'index') { - throw new BadRequestHttpException('Request missing index context'); - } - + $exporter = $this->_exporter(); $request = Craft::$app->getRequest(); - // Find that exporter from the list of available exporters for the source - $exporterClass = $request->getBodyParam('type', Raw::class); - if (!empty($this->exporters)) { - foreach ($this->exporters as $availableExporter) { - /** @var ElementAction $availableExporter */ - if ($exporterClass === get_class($availableExporter)) { - $exporter = $availableExporter; - break; - } - } - } - - /** @noinspection UnSafeIsSetOverArrayInspection - FP */ - if (!isset($exporter)) { - throw new BadRequestHttpException('Element exporter is not supported by the element type'); - } - $token = Craft::$app->getTokens()->createToken([ 'export/export', [ 'elementType' => $this->elementType, 'sourceKey' => $this->sourceKey, 'criteria' => $request->getBodyParam('criteria', []), - 'exporter' => $exporterClass, + 'exporter' => get_class($exporter), 'format' => $request->getBodyParam('format', 'csv'), ] ], 1, (new \DateTime())->add(new \DateInterval('PT1H'))); @@ -312,6 +328,35 @@ public function actionCreateExportToken(): Response return $this->asJson(compact('token')); } + /** + * Returns the exporter for the request. + * + * @throws BadRequestHttpException + * @return ElementExporterInterface + */ + private function _exporter(): ElementExporterInterface + { + if (!$this->sourceKey) { + throw new BadRequestHttpException('Request missing required body param'); + } + + if ($this->context !== 'index') { + throw new BadRequestHttpException('Request missing index context'); + } + + // Find that exporter from the list of available exporters for the source + $exporterClass = Craft::$app->getRequest()->getBodyParam('type', Raw::class); + if (!empty($this->exporters)) { + foreach ($this->exporters as $exporter) { + if ($exporterClass === get_class($exporter)) { + return $exporter; + } + } + } + + throw new BadRequestHttpException('Element exporter is not supported by the element type'); + } + /** * Identify whether index actions should be included in the element index * diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 8fc74ededa3..a201692cc27 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -30,6 +30,20 @@ */ class ElementsController extends BaseElementsController { + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if (!parent::beforeAction($action)) { + return false; + } + + $this->requireAcceptsJson(); + + return true; + } + /** * Renders and returns the body of an ElementSelectorModal. * diff --git a/src/controllers/ExportController.php b/src/controllers/ExportController.php index bb14914df3e..403faba9bc0 100644 --- a/src/controllers/ExportController.php +++ b/src/controllers/ExportController.php @@ -20,6 +20,7 @@ * * @author Pixel & Tonic, Inc. * @since 3.2.0 + * @deprecated in 3.4.4 */ class ExportController extends Controller { diff --git a/src/templates/_components/utilities/DbBackup.html b/src/templates/_components/utilities/DbBackup.html index c342e4a3c77..d7c5d9735cb 100644 --- a/src/templates/_components/utilities/DbBackup.html +++ b/src/templates/_components/utilities/DbBackup.html @@ -6,6 +6,7 @@ {{ forms.checkbox({ name: 'downloadBackup', + id: 'download-backup', label: 'Download backup?'|t('app'), checked: true, }) }} diff --git a/src/web/assets/cp/dist/js/Craft.js b/src/web/assets/cp/dist/js/Craft.js index 6ea72b15feb..2f708f983d6 100644 --- a/src/web/assets/cp/dist/js/Craft.js +++ b/src/web/assets/cp/dist/js/Craft.js @@ -1,4 +1,4 @@ -/*! - 2020-01-31 */ +/*! - 2020-02-05 */ (function($){ /** global: Craft */ @@ -612,6 +612,55 @@ $.extend(Craft, }.bind(this)); }, + /** + * Requests a URL and downloads the response. + * + * @param {string} method the request method to use + * @param {string} url the URL + * @param {string|Object} [body] the request body, if method = POST + * @return {Promise} + */ + downloadFromUrl: function(method, url, body) { + return new Promise((resolve, reject) => { + // h/t https://nehalist.io/downloading-files-from-post-requests/ + let request = new XMLHttpRequest(); + request.open(method, url, true); + if (typeof body === 'object') { + request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + body = JSON.stringify(body); + } else { + request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + } + request.responseType = 'blob'; + + request.onload = function() { + // Only handle status code 200 + if (request.status === 200) { + // Try to find out the filename from the content disposition `filename` value + let disposition = request.getResponseHeader('content-disposition'); + let matches = /"([^"]*)"/.exec(disposition); + let filename = (matches != null && matches[1] ? matches[1] : 'Download'); + + // Encode the download into an anchor href + let contentType = request.getResponseHeader('content-type'); + let blob = new Blob([request.response], {type: contentType}); + let link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + resolve(); + } else { + reject(); + } + }.bind(this); + + request.send(body); + }); + }, + /** * Converts a comma-delimited string into an array. * @@ -3864,19 +3913,16 @@ Craft.BaseElementIndex = Garnish.Base.extend( } } - Craft.postActionRequest('element-indexes/create-export-token', params, $.proxy(function(response, textStatus) { - submitting = false; - $spinner.addClass('hidden'); - - if (textStatus === 'success') { - var params = {}; - params[Craft.tokenParam] = response.token; - var url = Craft.getCpUrl('', params); - document.location.href = url; - } else { + Craft.downloadFromUrl('POST', Craft.getActionUrl('element-indexes/export'), params) + .then(function() { + submitting = false; + $spinner.addClass('hidden'); + }) + .catch(function() { + submitting = false; + $spinner.addClass('hidden'); Craft.cp.displayError(Craft.t('app', 'A server error occurred.')); - } - }, this)); + }); }); }, diff --git a/src/web/assets/cp/dist/js/Craft.min.js b/src/web/assets/cp/dist/js/Craft.min.js index d99f484237e..3d389f248c4 100644 --- a/src/web/assets/cp/dist/js/Craft.min.js +++ b/src/web/assets/cp/dist/js/Craft.min.js @@ -1,2 +1,2 @@ -!function(t){t.extend(Craft,{navHeight:48,t:function(t,e,i){return void 0!==Craft.translations[t]&&void 0!==Craft.translations[t][e]&&(e=Craft.translations[t][e]),i?this.formatMessage(e,i):e},formatMessage:function(t,e){let i;if(!1===(i=this._tokenizePattern(t)))throw"Message pattern is invalid.";for(let t=0;tt?(s++,i=t):(s--,i=n),0===s&&(tokens.push(a.slice(e+1,i).join("").split(",",3)),e=i+1,tokens.push(a.slice(e,t).join("")),e=t),0!==s&&(!1===t||!1===n))break}return 0===s&&tokens},_parseToken:function(t,e){const i=Craft.trim(t[0]);if(void 0===e[i])return`{${t.join(",")}}`;const s=e[i],a=void 0!==t[1]?Craft.trim(t[1]):"none";switch(a){case"number":let i=void 0!==t[2]?Craft.trim(t[2]):null;if(null!==i&&"integer"!==i)throw"Message format 'number' is only supported for integer values.";let n,r=Craft.formatNumber(s);return null===i&&-1!==(n=`${s}`.indexOf("."))&&(r+=`.${s.substr(n+1)}`),r;case"none":return s;case"plural":if(void 0===t[2])return!1;plural=this._tokenizePattern(t[2]);const o=plural.length;let l=!1,h=0;for(let t=0;t+1t.replace("#",s-h)).join(","))}if(!1!==l)return this.formatMessage(l,e);break;default:throw`Message format '${a}' is not supported.`}return!1},formatDate:function(e){return"object"!=typeof e&&(e=new Date(e)),t.datepicker.formatDate(Craft.datepickerOptions.dateFormat,e)},formatNumber:function(t,e){return void 0===e&&(e=",.0f"),d3.formatLocale(d3FormatLocaleDefinition).format(e)(t)},escapeHtml:function(e){return t("
").text(e).html()},escapeRegex:function(t){return t.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},getText:function(e){return t("
").html(e).text()},encodeUriComponent:function(t){t=encodeURIComponent(t);var e={"!":"%21","*":"%2A","'":"%27","(":"%28",")":"%29"};for(var i in e){var s=new RegExp("\\"+i,"g");t=t.replace(s,e[i])}return t},selectFullValue:function(e){var i=t(e),s=i.val();if(void 0!==i[0].setSelectionRange){var a=2*s.length;i[0].setSelectionRange(0,a)}else i.val(s)},formatInputId:function(t){return this.rtrim(t.replace(/[\[\]\\]+/g,"-"),"-")},getUrl:function(e,i,s){"string"!=typeof e&&(e="");var a="";if(t.isPlainObject(i)){var n=[];for(var r in i)if(i.hasOwnProperty(r)){var o=i[r];"#"===r?a=o:null!==o&&""!==o&&n.push(r+"="+o)}i=n}i=Garnish.isArray(i)?i.join("&"):Craft.trim(i,"&?");var l=e.indexOf("#");-1!==l&&(a||(a=e.substr(l+1)),e=e.substr(0,l));var h,d=e.indexOf("?");if(-1!==d&&(i=e.substr(d+1)+(i?"&"+i:""),e=e.substr(0,d)),-1!==e.search("://")||"/"===e[0])return e+(i?"?"+i:"")+(a?"#"+a:"");if(e=Craft.trim(e,"/"),s){if(h=s,e){var c=h.match(new RegExp("[&?]"+Craft.escapeRegex(Craft.pathParam)+"=[^&]+"));c&&(h=h.replace(c[0],Craft.rtrim(c[0],"/")+"/"+e),e="")}}else h=Craft.baseUrl;if(-1!==(d=h.indexOf("?"))&&(i=h.substr(d+1)+(i?"&"+i:""),h=h.substr(0,d)),!Craft.omitScriptNameInUrls&&e)if(Craft.usePathInfo)-1===h.search(Craft.scriptName)&&(h=Craft.rtrim(h,"/")+"/"+Craft.scriptName);else{if(i&&i.substr(0,Craft.pathParam.length+1)===Craft.pathParam+"="){var u,p=i.indexOf("&");-1!==p?(u=i.substring(2,p),i=i.substr(p+1)):(u=i.substr(2),i=null),e=(u=Craft.rtrim(u))+(e?"/"+e:"")}i=Craft.pathParam+"="+e+(i?"&"+i:""),e=null}return e&&(h=Craft.rtrim(h,"/")+"/"+e),i&&(h+="?"+i),a&&(h+="#"+a),h},getCpUrl:function(t,e){return this.getUrl(t,e,Craft.baseCpUrl)},getSiteUrl:function(t,e){return this.getUrl(t,e,Craft.baseSiteUrl)},getActionUrl:function(t,e){return Craft.getUrl(t,e,Craft.actionUrl)},redirectTo:function(t){document.location.href=this.getUrl(t)},getCsrfInput:function(){return Craft.csrfTokenName?'':""},postActionRequest:function(e,i,s,a){"function"==typeof i&&(a=s,s=i,i={}),(a=a||{}).contentType&&a.contentType.match(/\bjson\b/)&&("object"==typeof i&&(i=JSON.stringify(i)),a.contentType="application/json; charset=utf-8");var n={"X-Registered-Asset-Bundles":Object.keys(Craft.registeredAssetBundles).join(","),"X-Registered-Js-Files":Object.keys(Craft.registeredJsFiles).join(",")};Craft.csrfTokenValue&&Craft.csrfTokenName&&(n["X-CSRF-Token"]=Craft.csrfTokenValue);var r=t.ajax(t.extend({url:Craft.getActionUrl(e),type:"POST",dataType:"json",headers:n,data:i,success:s,error:function(t,e,i){4===t.readyState&&(void 0!==Craft.cp?Craft.cp.displayError():alert(Craft.t("app","A server error occurred.")),s&&s(null,e,t))}},a));return"function"==typeof a.send&&a.send(r),r},_waitingOnAjax:!1,_ajaxQueue:[],queueActionRequest:function(t,e,i,s){"function"==typeof e&&(s=i,i=e,e=void 0),Craft._ajaxQueue.push([t,e,i,s]),Craft._waitingOnAjax||Craft._postNextActionRequestInQueue()},_postNextActionRequestInQueue:function(){Craft._waitingOnAjax=!0;var t=Craft._ajaxQueue.shift();Craft.postActionRequest(t[0],t[1],function(e,i,s){t[2]&&"function"==typeof t[2]&&t[2](e,i,s),Craft._ajaxQueue.length?Craft._postNextActionRequestInQueue():Craft._waitingOnAjax=!1},t[3])},sendApiRequest:function(e,i,s){return new Promise(function(a,n){this.postActionRequest("app/api-headers",function(r,o){if("success"===o){s=s||{},r=t.extend(r,s.headers||{});var l=t.extend(Craft.apiParams||{},s.params||{});axios.request(t.extend({},s,{url:i,method:e,baseURL:Craft.baseApiUrl,headers:r,params:l})).then(function(t){Craft.postActionRequest("app/process-api-response-headers",{headers:t.headers},function(){a(t.data)})}).catch(n)}else n()}.bind(this))}.bind(this))},stringToArray:function(e){if("string"!=typeof e)return e;for(var i=e.split(","),s=0;se.length?1:-1});for(var s=this._groupParamsByDeltaNames(t.split("&"),i,!1),a=this._groupParamsByDeltaNames(e.split("&"),i,!0),n=a.__root__,r=0;r=0;s--)if((a=decodeURIComponent(t[r]).substr(0,e[s].length+1))===e[s]+"="||a===e[s]+"["){void 0===n[e[s]]&&(n[e[s]]=[]),n[e[s]].push(t[r]);continue t}i&&n.__root__.push(t[r])}return n},expandPostArray:function(t){var e,i={};for(var s in t)if(t.hasOwnProperty(s)){var a,n=t[s],r=s.match(/^(\w+)(\[.*)?/);if(r[2])for(a=r[2].match(/\[[^\[\]]*\]/g),e=0;e",{attr:{method:"post",action:"","accept-charset":"UTF-8"}});if("string"==typeof e)for(var s,a=e.split("&"),n=0;n",{type:"hidden",name:decodeURIComponent(s[0]),value:decodeURIComponent(s[1]||"")}).appendTo(i);return i},compare:function(t,e,i){if(typeof t!=typeof e)return!1;if("object"==typeof t){if(t.length!==e.length)return!1;if(t instanceof Array!=e instanceof Array)return!1;if(!(t instanceof Array))if(void 0===i||!0===i){if(!Craft.compare(Craft.getObjectKeys(t).sort(),Craft.getObjectKeys(e).sort()))return!1}else if(!Craft.compare(Craft.getObjectKeys(t),Craft.getObjectKeys(e)))return!1;for(var s in t)if(t.hasOwnProperty(s)&&!Craft.compare(t[s],e[s]))return!1;return!0}return t===e},getObjectKeys:function(t){var e=[];for(var i in t)t.hasOwnProperty(i)&&e.push(i);return e},escapeChars:function(t){Garnish.isArray(t)||(t=t.split());for(var e="",i=0;i]*href="(?:'+a.join("|")+')".*?><\/script>',"g");e=e.replace(r,"")}t("head").append(e)}},appendFootHtml:function(e){if(e){var i=t("script[src]");if(i.length){for(var s,a=[],n=0;n]*src="(?:'+a.join("|")+')".*?><\/script>',"g");e=e.replace(r,"")}Garnish.$bod.append(e)}},initUiElements:function(e){t(".grid",e).grid(),t(".info",e).infoicon(),t(".checkbox-select",e).checkboxselect(),t(".fieldtoggle",e).fieldtoggle(),t(".lightswitch",e).lightswitch(),t(".nicetext",e).nicetext(),t(".pill",e).pill(),t(".formsubmit",e).formsubmit(),t(".menubtn",e).menubtn()},_elementIndexClasses:{},_elementSelectorModalClasses:{},_elementEditorClasses:{},registerElementIndexClass:function(t,e){if(void 0!==this._elementIndexClasses[t])throw"An element index class has already been registered for the element type “"+t+"”.";this._elementIndexClasses[t]=e},registerElementSelectorModalClass:function(t,e){if(void 0!==this._elementSelectorModalClasses[t])throw"An element selector modal class has already been registered for the element type “"+t+"”.";this._elementSelectorModalClasses[t]=e},registerElementEditorClass:function(t,e){if(void 0!==this._elementEditorClasses[t])throw"An element editor class has already been registered for the element type “"+t+"”.";this._elementEditorClasses[t]=e},createElementIndex:function(t,e,i){return new(void 0!==this._elementIndexClasses[t]?this._elementIndexClasses[t]:Craft.BaseElementIndex)(t,e,i)},createElementSelectorModal:function(t,e){return new(void 0!==this._elementSelectorModalClasses[t]?this._elementSelectorModalClasses[t]:Craft.BaseElementSelectorModal)(t,e)},createElementEditor:function(t,e,i){return new(void 0!==this._elementEditorClasses[t]?this._elementEditorClasses[t]:Craft.BaseElementEditor)(e,i)},getLocalStorage:function(t,e){return t="Craft-"+Craft.systemUid+"."+t,"undefined"!=typeof localStorage&&void 0!==localStorage[t]?JSON.parse(localStorage[t]):e},setLocalStorage:function(t,e){if("undefined"!=typeof localStorage){t="Craft-"+Craft.systemUid+"."+t;try{localStorage[t]=JSON.stringify(e)}catch(t){}}},getElementInfo:function(e){var i=t(e);return i.hasClass("element")||(i=i.find(".element:first")),{id:i.data("id"),siteId:i.data("site-id"),label:i.data("label"),status:i.data("status"),url:i.data("url"),hasThumb:i.hasClass("hasthumb"),$element:i}},setElementSize:function(e,i){var s=t(e);if("small"!==i&&"large"!==i&&(i="small"),!s.hasClass(i)){var a="small"===i?"large":"small";if(s.addClass(i).removeClass(a),s.hasClass("hasthumb")){var n=s.find("> .elementthumb > img"),r=t("",{sizes:("small"===i?"30":"100")+"px",srcset:n.attr("srcset")||n.attr("data-pfsrcset")});n.replaceWith(r),picturefill({elements:[r[0]]})}}}}),t.extend(t.fn,{animateLeft:function(t,e,i,s){return"ltr"===Craft.orientation?this.velocity({left:t},e,i,s):this.velocity({right:t},e,i,s)},animateRight:function(t,e,i,s){return"ltr"===Craft.orientation?this.velocity({right:t},e,i,s):this.velocity({left:t},e,i,s)},disable:function(){return this.each(function(){var e=t(this);e.addClass("disabled"),e.data("activatable")&&e.removeAttr("tabindex")})},enable:function(){return this.each(function(){var e=t(this);e.removeClass("disabled"),e.data("activatable")&&e.attr("tabindex","0")})},grid:function(){return this.each(function(){var e=t(this),i={};e.data("item-selector")&&(i.itemSelector=e.data("item-selector")),e.data("cols")&&(i.cols=parseInt(e.data("cols"))),e.data("max-cols")&&(i.maxCols=parseInt(e.data("max-cols"))),e.data("min-col-width")&&(i.minColWidth=parseInt(e.data("min-col-width"))),e.data("mode")&&(i.mode=e.data("mode")),e.data("fill-mode")&&(i.fillMode=e.data("fill-mode")),e.data("col-class")&&(i.colClass=e.data("col-class")),e.data("snap-to-grid")&&(i.snapToGrid=!!e.data("snap-to-grid")),new Craft.Grid(this,i)})},infoicon:function(){return this.each(function(){new Craft.InfoIcon(this)})},checkboxselect:function(){return this.each(function(){t.data(this,"checkboxselect")||new Garnish.CheckboxSelect(this)})},fieldtoggle:function(){return this.each(function(){t.data(this,"fieldtoggle")||new Craft.FieldToggle(this)})},lightswitch:function(e,i,s){return"settings"===e?("string"==typeof i?(e={})[i]=s:e=i,this.each(function(){var i=t.data(this,"lightswitch");i&&i.setSettings(e)})):(t.isPlainObject(e)||(e={}),this.each(function(){var i=t.extend({},e);Garnish.hasAttr(this,"data-value")&&(i.value=t(this).attr("data-value")),Garnish.hasAttr(this,"data-indeterminate-value")&&(i.indeterminateValue=t(this).attr("data-indeterminate-value")),t.data(this,"lightswitch")||new Craft.LightSwitch(this,i)}))},nicetext:function(){return this.each(function(){t.data(this,"nicetext")||new Garnish.NiceText(this)})},pill:function(){return this.each(function(){t.data(this,"pill")||new Garnish.Pill(this)})},formsubmit:function(){this.on("click",function(e){var i=t(e.currentTarget);if(!i.attr("data-confirm")||confirm(i.attr("data-confirm"))){var s=i.data("menu")?i.data("menu").$anchor:i,a=s.attr("data-form")?t("#"+s.attr("data-form")):s.closest("form");i.data("action")&&t('').val(i.data("action")).appendTo(a),i.data("redirect")&&t('').val(i.data("redirect")).appendTo(a),i.data("param")&&t('').attr({name:i.data("param"),value:i.data("value")}).appendTo(a),a.trigger({type:"submit",customTrigger:i})}})},menubtn:function(){return this.each(function(){var e=t(this);if(!e.data("menubtn")&&e.next().hasClass("menu")){var i={};e.data("menu-anchor")&&(i.menuAnchor=e.data("menu-anchor")),new Garnish.MenuBtn(e,i)}})}}),Garnish.$doc.ready(function(){Craft.initUiElements()}),Craft.BaseElementEditor=Garnish.Base.extend({$element:null,elementId:null,siteId:null,deltaNames:null,initialData:null,$form:null,$fieldsContainer:null,$cancelBtn:null,$saveBtn:null,$spinner:null,$siteSelect:null,$siteSpinner:null,hud:null,init:function(e,i){void 0===i&&t.isPlainObject(e)&&(i=e,e=null),this.$element=t(e),this.setSettings(i,Craft.BaseElementEditor.defaults),this.loadHud()},setElementAttribute:function(t,e){this.settings.attributes||(this.settings.attributes={}),null===e?delete this.settings.attributes[t]:this.settings.attributes[t]=e},getBaseData:function(){var e=t.extend({},this.settings.params);return this.settings.siteId?e.siteId=this.settings.siteId:this.$element&&this.$element.data("site-id")&&(e.siteId=this.$element.data("site-id")),this.settings.elementId?e.elementId=this.settings.elementId:this.$element&&this.$element.data("id")&&(e.elementId=this.$element.data("id")),this.settings.elementType&&(e.elementType=this.settings.elementType),this.settings.attributes&&(e.attributes=this.settings.attributes),this.settings.prevalidate&&(e.prevalidate=1),e},loadHud:function(){this.onBeginLoading();var e=this.getBaseData();e.includeSites=Craft.isMultiSite&&this.settings.showSiteSwitcher,Craft.postActionRequest("elements/get-editor-html",e,t.proxy(this,"showHud"))},showHud:function(e,i){if(this.onEndLoading(),"success"===i){var s=t();if(e.sites){var a=t('
');if(1===e.sites.length)t("
",{text:e.sites[0].name}).appendTo(a);else{var n=t('
').appendTo(a);this.$siteSelect=t("').appendTo(h),this.$spinner=t('