diff --git a/BTCPayServer.Abstractions/Form/Form.cs b/BTCPayServer.Abstractions/Form/Form.cs index fc444937b..909594b66 100644 --- a/BTCPayServer.Abstractions/Form/Form.cs +++ b/BTCPayServer.Abstractions/Form/Form.cs @@ -69,7 +69,6 @@ public class Form if (!nameReturned.Add(fullName)) { errors.Add($"Form contains duplicate field names '{fullName}'"); - continue; } } return errors.Count == 0; @@ -86,15 +85,10 @@ public class Form thisPath.Add(field.Name); yield return (thisPath, field); } - - foreach (var child in field.Fields) + foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields)) { - if (field.Constant) - child.Constant = true; - foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields)) - { - yield return descendant; - } + descendant.Field.Constant = field.Constant || descendant.Field.Constant; + yield return descendant; } } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index d5cd60dd2..353628dfe 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -129,16 +129,20 @@ namespace BTCPayServer.Tests Assert.Contains("There are no forms yet.", s.Driver.PageSource); s.Driver.FindElement(By.Id("CreateForm")).Click(); s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 1"); - s.Driver.FindElement((By.CssSelector("[data-form-template='email']"))).Click(); - var emailtemplate = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value"); - Assert.Contains("buyerEmail", emailtemplate); + s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click(); + + s.Driver.FindElement(By.Id("CodeTabButton")).Click(); + s.Driver.WaitForElement(By.Id("CodeTabPane")); + + var config = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value"); + Assert.Contains("buyerEmail", config); + s.Driver.FindElement(By.Name("FormConfig")).Clear(); s.Driver.FindElement(By.Name("FormConfig")) - .SendKeys(emailtemplate.Replace("Enter your email", "CustomFormInputTest")); + .SendKeys(config.Replace("Enter your email", "CustomFormInputTest")); s.Driver.FindElement(By.Id("SaveButton")).Click(); s.Driver.FindElement(By.Id("ViewForm")).Click(); - var formurl = s.Driver.Url; Assert.Contains("CustomFormInputTest", s.Driver.PageSource); s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com"); @@ -157,12 +161,16 @@ namespace BTCPayServer.Tests Assert.DoesNotContain("Custom Form 1", s.Driver.PageSource); s.Driver.FindElement(By.Id("CreateForm")).Click(); s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 2"); - s.Driver.FindElement((By.CssSelector("[data-form-template='email']"))).Click(); + s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click(); + + s.Driver.FindElement(By.Id("CodeTabButton")).Click(); + s.Driver.WaitForElement(By.Id("CodeTabPane")); + s.Driver.SetCheckbox(By.Name("Public"), true); s.Driver.FindElement(By.Name("FormConfig")).Clear(); s.Driver.FindElement(By.Name("FormConfig")) - .SendKeys(emailtemplate.Replace("Enter your email", "CustomFormInputTest2")); + .SendKeys(config.Replace("Enter your email", "CustomFormInputTest2")); s.Driver.FindElement(By.Id("SaveButton")).Click(); s.Driver.FindElement(By.Id("ViewForm")).Click(); formurl = s.Driver.Url; diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index ae20411e4..125e8ec88 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -358,6 +358,11 @@ retry: version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value; expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim(); Assert.Equal(expected, actual); + + actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim(); + version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value; + expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim(); + Assert.Equal(expected, actual); } string GetFileContent(params string[] path) diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 653aaa447..51bc02617 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -111,9 +111,6 @@ - - - diff --git a/BTCPayServer/Forms/UIFormsController.cs b/BTCPayServer/Forms/UIFormsController.cs index 9e8d86da3..82d47bc8b 100644 --- a/BTCPayServer/Forms/UIFormsController.cs +++ b/BTCPayServer/Forms/UIFormsController.cs @@ -77,7 +77,6 @@ public class UIFormsController : Controller if (!_formDataService.IsFormSchemaValid(modifyForm.FormConfig, out var form, out var error)) { - ModelState.AddModelError(nameof(modifyForm.FormConfig), $"Form config was invalid: {error})"); } @@ -86,7 +85,6 @@ public class UIFormsController : Controller modifyForm.FormConfig = form.ToString(); } - if (!ModelState.IsValid) { return View(modifyForm); diff --git a/BTCPayServer/Views/UIForms/Modify.cshtml b/BTCPayServer/Views/UIForms/Modify.cshtml index 731337ed8..9a01cadfe 100644 --- a/BTCPayServer/Views/UIForms/Modify.cshtml +++ b/BTCPayServer/Views/UIForms/Modify.cshtml @@ -1,43 +1,164 @@ @using BTCPayServer.Forms @using Microsoft.AspNetCore.Mvc.TagHelpers -@using BTCPayServer.Abstractions.TagHelpers -@using Newtonsoft.Json +@inject BTCPayServer.Security.ContentSecurityPolicies Csp @model BTCPayServer.Forms.ModifyForm - @{ + Csp.UnsafeEval(); var formId = Context.GetRouteValue("id"); var isNew = formId is null; Layout = "../Shared/_NavLayout.cshtml"; ViewData["NavPartialName"] = "../UIStores/_Nav"; ViewData.SetActivePage(StoreNavPages.Forms, $"{(isNew ? "Create" : "Edit")} Form", Model.Name); - var storeId = Context.GetCurrentStoreId(); } -@section PageFootContent { - - +@section PageHeadCOntent { + } - - +@section PageFootContent { + + + + + + + + + + + + + +}
-
-
+
+

@ViewData["Title"] @@ -54,31 +175,63 @@

-
+
- +
-
+
Standalone mode, which can be used to generate invoices independent of payment requests or apps.
-
-
- -
- Templates: - - +
+
+ +
+
+ +
+ Templates + + +
+
+
+
+
+
+ +
+
+
- +
+
+
+ +
+
diff --git a/BTCPayServer/wwwroot/img/icon-sprite.svg b/BTCPayServer/wwwroot/img/icon-sprite.svg index 1e332c4c2..b8ded5efd 100644 --- a/BTCPayServer/wwwroot/img/icon-sprite.svg +++ b/BTCPayServer/wwwroot/img/icon-sprite.svg @@ -12,7 +12,13 @@ + + + + + + @@ -56,6 +62,7 @@ + @@ -63,4 +70,4 @@ - \ No newline at end of file + diff --git a/BTCPayServer/wwwroot/js/form-editor.js b/BTCPayServer/wwwroot/js/form-editor.js new file mode 100644 index 000000000..f61de57c9 --- /dev/null +++ b/BTCPayServer/wwwroot/js/form-editor.js @@ -0,0 +1,190 @@ +document.addEventListener('DOMContentLoaded', () => { + const parseConfig = str => { + try { + return JSON.parse(str) + } catch (err) { + console.error('Error deserializing form config:', err) + } + } + const $config = document.getElementById('FormConfig') + let config = parseConfig($config.value) || {} + + const specialFieldTypeOptions = ['fieldset', 'textarea', 'select'] + const inputFieldTypeOptions = ['text', 'number', 'password', 'email', 'url', 'tel', 'date', 'hidden'] + const fieldTypeOptions = inputFieldTypeOptions.concat(specialFieldTypeOptions) + + const getFieldComponent = type => `field-type-${specialFieldTypeOptions.includes(type) ? type : 'input'}` + + const fieldProps = { + type: String, + name: String, + label: String, + value: String, + helpText: String, + required: Boolean, + constant: Boolean, + options: Array, + fields: Array, + validationErrors: Array + } + + const fieldTypeBase = { + props: { + // internal + path: Array, + // field config + ...fieldProps + } + } + + const FieldTypeInput = Vue.extend({ + mixins: [fieldTypeBase], + name: 'field-type-input', + template: '#field-type-input' + }) + + const FieldTypeTextarea = Vue.extend({ + mixins: [fieldTypeBase], + name: 'field-type-textarea', + template: '#field-type-textarea' + }) + + const FieldTypeSelect = Vue.extend({ + mixins: [fieldTypeBase], + name: 'field-type-select', + template: '#field-type-select', + props: { + options: Array + } + }) + + const components = { + FieldTypeInput, + FieldTypeSelect, + FieldTypeTextarea + } + + // register fields-editor and field-type-fieldset globally in order to use them recursively + Vue.component('field-type-fieldset', { + mixins: [fieldTypeBase], + template: '#field-type-fieldset', + components, + props: { + fields: Array, + selectedField: fieldProps + } + }) + + Vue.component('fields-editor', { + template: '#fields-editor', + components, + props: { + path: Array, + fields: Array, + selectedField: fieldProps + }, + methods: { + getFieldComponent + } + }) + + Vue.component('field-editor', { + template: '#field-editor', + components, + data () { + return { + fieldTypeOptions + } + }, + props: { + path: Array, + field: fieldProps + }, + methods: { + getFieldComponent, + addOption (event) { + if (!this.field.options) this.$set(this.field, 'options', []) + const index = this.field.options.length + 1 + this.field.options.push({ value: `newOption${index}`, text: `New option ${index}` }) + }, + removeOption(event, index) { + console.log(this.field.options, index) + this.field.options.splice(index, 1) + }, + sortOptions (event) { + const { newIndex, oldIndex } = event + this.field.options.splice(newIndex, 0, this.field.options.splice(oldIndex, 1)[0]) + } + } + }) + + Vue.use(vSortable) + + new Vue({ + el: '#FormEditor', + name: 'form-editor', + data () { + return { + config, + selectedField: null + } + }, + computed: { + fields() { + return this.config.fields || [] + }, + configJSON() { + return JSON.stringify(this.config, null, 2) + } + }, + methods: { + applyTemplate(id) { + const $template = document.getElementById(`form-template-${id}`) + this.config = JSON.parse($template.innerHTML.trim()) + this.selectedField = null + }, + updateFromJSON(event) { + const config = parseConfig(event.target.value) + if (!config) return + this.config = config + this.selectedField = null + }, + addField(event, path) { + const fields = this.getFieldsForPath(path) + const index = fields.length + 1 + const length = fields.push({ type: 'text', name: `newField${index}`, label: `New field ${index}`, fields: [], options: [] }) + this.selectedField = fields[length - 1] + }, + selectField(event, path, index) { + const fields = this.getFieldsForPath(path) + this.selectedField = fields[index] + }, + removeField(event, path, index) { + const fields = this.getFieldsForPath(path) + fields.splice(index, 1) + this.selectedField = null + }, + sortFields(event, path) { + const { newIndex, oldIndex } = event + const fields = this.getFieldsForPath(path) + fields.splice(newIndex, 0, fields.splice(oldIndex, 1)[0]) + }, + getFieldsForPath (path) { + if (!this.config.fields) this.$set(this.config, 'fields', []) + let fields = this.config.fields + while (path.length) { + const name = path.shift() + const field = fields.find(field => field.name === name) + if (!field.fields) this.$set(field, 'fields', []) + fields = field.fields + } + return fields + } + }, + mounted () { + if (!this.config.fields || this.config.fields.length === 0) { + this.addField(null,[]) + } + } + }) +}) diff --git a/BTCPayServer/wwwroot/vendor/vue-sortable/sortable.min.js b/BTCPayServer/wwwroot/vendor/vue-sortable/sortable.min.js new file mode 100644 index 000000000..fefa0c71c --- /dev/null +++ b/BTCPayServer/wwwroot/vendor/vue-sortable/sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.0 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function M(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function N(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&p(t,e)||o&&t===n)return t}while(t!==n&&(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode))}var i;return null}var g,m=/\s+/g;function I(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(m," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(m," ")))}function P(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=P(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function b(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[j]._onDragOver(o)}}var i,r,a}function Yt(t){q&&q.parentNode[j]._isOutsideThisEl(t.target)}function Bt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return It(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Bt.supportPointer&&"PointerEvent"in window&&!u,emptyInsertThreshold:5};for(n in K.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Pt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Mt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),Et.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,x())}function Ft(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||k(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function jt(t){t.draggable=!1}function Ht(){Ct=!1}function Lt(t){return setTimeout(t,0)}function Kt(t){return clearTimeout(t)}Bt.prototype={constructor:Bt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(gt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,q):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Tt.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Tt.push(o)}}(o),!q&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=N(l,t.draggable,o,!1))&&l.animated||J===l)){if(nt=B(l),it=B(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return U({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),z("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=N(s,t.trim(),o,!1))return U({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),z("filter",n,{evt:e}),!0}))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!N(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!q&&n.parentNode===r&&(o=k(n),$=r,V=(q=n).parentNode,Q=q.nextSibling,J=n,at=a.group,st={target:Bt.dragged=q,clientX:(e||t).clientX,clientY:(e||t).clientY},ht=st.clientX-o.left,ft=st.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,q.style["will-change"]="all",o=function(){z("delayEnded",i,{evt:t}),Bt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(q.draggable=!0),i._triggerDragStart(t,e),U({sortable:i,name:"choose",originalEvent:t}),I(q,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){b(q,t.trim(),jt)}),h(l,"dragover",Xt),h(l,"mousemove",Xt),h(l,"touchmove",Xt),h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,q.draggable=!0),z("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Bt.eventCanceled?this._onDrop():(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){q&&jt(q),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;f(t,"mouseup",this._disableDelayedDrag),f(t,"touchend",this._disableDelayedDrag),f(t,"touchcancel",this._disableDelayedDrag),f(t,"mousemove",this._delayedDragTouchMoveHandler),f(t,"touchmove",this._delayedDragTouchMoveHandler),f(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(q,"dragend",this),h($,"dragstart",this._onDragStart));try{document.selection?Lt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;yt=!1,$&&q?(z("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Yt),n=this.options,t||I(q,n.dragClass,!1),I(q,n.ghostClass,!0),Bt.active=this,t&&this._appendGhost(),U({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ct){this._lastX=ct.clientX,this._lastY=ct.clientY,kt();for(var t=document.elementFromPoint(ct.clientX,ct.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ct.clientX,ct.clientY))!==e;)e=t;if(q.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j])if(e[j]._onDragOver({clientX:ct.clientX,clientY:ct.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=(t=e).parentNode);Rt()}},_onTouchMove:function(t){if(st){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Z&&v(Z,!0),a=Z&&r&&r.a,l=Z&&r&&r.d,e=Ot&&bt&&E(bt),a=(i.clientX-st.clientX+o.x)/(a||1)+(e?e[0]-_t[0]:0)/(a||1),l=(i.clientY-st.clientY+o.y)/(l||1)+(e?e[1]-_t[1]:0)/(l||1);if(!Bt.active&&!yt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))n.right+10||t.clientX<=n.right&&t.clientY>n.bottom&&t.clientX>=n.left:t.clientX>n.right&&t.clientY>n.top||t.clientX<=n.right&&t.clientY>n.bottom+10}(n,r,this)&&!g.animated){if(g===q)return O(!1);if((l=g&&a===n.target?g:l)&&(w=k(l)),!1!==Ft($,a,q,o,l,w,n,!!l))return x(),g&&g.nextSibling?a.insertBefore(q,g.nextSibling):a.appendChild(q),V=a,A(),O(!0)}else if(g&&function(t,e,n){n=k(X(n.el,0,n.options,!0));return e?t.clientX