diff --git a/app/assets/config/alchemy_manifest.js b/app/assets/config/alchemy_manifest.js
index e812956c07..383a389638 100644
--- a/app/assets/config/alchemy_manifest.js
+++ b/app/assets/config/alchemy_manifest.js
@@ -5,7 +5,6 @@
//= link alchemy/menubar.js
//= link alchemy/print.css
//= link alchemy/welcome.css
-//= link alchemy/favicon.ico
//= link tinymce/plugins/alchemy_link/plugin.min.js
//= link tinymce/tinymce.min.js
//= link_directory ../stylesheets/tinymce/skins/alchemy/ .css
diff --git a/app/assets/javascripts/alchemy/admin.js b/app/assets/javascripts/alchemy/admin.js
index 921c556f26..61eedbf56a 100644
--- a/app/assets/javascripts/alchemy/admin.js
+++ b/app/assets/javascripts/alchemy/admin.js
@@ -15,10 +15,8 @@
//= require requestAnimationFrame
//= require select2
//= require handlebars
-//= require sortable/Sortable.min
//= require alchemy/templates
//= require alchemy/alchemy.base
-//= require alchemy/alchemy.utils
//= require alchemy/alchemy.autocomplete
//= require alchemy/alchemy.browser
//= require alchemy/alchemy.buttons
@@ -40,7 +38,6 @@
//= require alchemy/alchemy.link_dialog
//= require alchemy/alchemy.list_filter
//= require alchemy/alchemy.initializer
-//= require alchemy/alchemy.node_tree
//= require alchemy/alchemy.page_sorter
//= require alchemy/alchemy.uploader
//= require alchemy/alchemy.preview_window
diff --git a/app/assets/javascripts/alchemy/alchemy.node_tree.js b/app/assets/javascripts/alchemy/alchemy.node_tree.js
deleted file mode 100644
index 9213d8bb70..0000000000
--- a/app/assets/javascripts/alchemy/alchemy.node_tree.js
+++ /dev/null
@@ -1,66 +0,0 @@
-Alchemy.NodeTree = {
- onFinishDragging: function (evt) {
- var url = Alchemy.routes.move_api_node_path(evt.item.dataset.id)
- var data = {
- target_parent_id: evt.to.dataset.nodeId,
- new_position: evt.newIndex
- };
- var ajax = Alchemy.ajax('PATCH', url, data)
-
- ajax.then(function(response) {
- Alchemy.growl('Successfully moved menu item.')
- Alchemy.NodeTree.displayNodeFolders()
- }).catch(function() {
- Alchemy.growl(error.message || error);
- })
- },
-
- displayNodeFolders: function () {
- document.querySelectorAll('li.menu-item').forEach(function (el) {
- var leftIconArea = el.querySelector('.nodes_tree-left_images')
- var list = el.querySelector('ul')
- var node = { folded: el.dataset.folded === 'true', id: el.dataset.id }
-
- if (list.children.length > 0 || node.folded ) {
- leftIconArea.innerHTML = HandlebarsTemplates.node_folder({ node: node })
- } else {
- leftIconArea.innerHTML = ' '
- }
- });
- },
-
- handleNodeFolders: function() {
- Alchemy.on('click', '.nodes_tree', '.node_folder', function(evt) {
- var nodeId = this.dataset.nodeId
- var menu_item = this.closest('li.menu-item')
- var url = Alchemy.routes.toggle_folded_api_node_path(nodeId)
- var list = menu_item.querySelector('.children')
- var ajax = Alchemy.ajax('PATCH', url)
-
- ajax.then(function() {
- list.classList.toggle('folded')
- menu_item.dataset.folded = menu_item.dataset.folded == 'true' ? 'false' : 'true'
- Alchemy.NodeTree.displayNodeFolders();
- }).catch(function(error){
- Alchemy.growl(error.message || error);
- });
- });
- },
-
- init: function() {
- this.handleNodeFolders()
- this.displayNodeFolders()
-
- document.querySelectorAll('.nodes_tree ul.children').forEach(function (el) {
- new Sortable(el, {
- group: 'nodes',
- animation: 150,
- fallbackOnBody: true,
- swapThreshold: 0.65,
- handle: '.node_name',
- invertSwap: true,
- onEnd: Alchemy.NodeTree.onFinishDragging
- });
- });
- }
-}
diff --git a/app/assets/javascripts/alchemy/alchemy.utils.js b/app/assets/javascripts/alchemy/alchemy.utils.js
deleted file mode 100644
index 5c35fa08e0..0000000000
--- a/app/assets/javascripts/alchemy/alchemy.utils.js
+++ /dev/null
@@ -1,45 +0,0 @@
-Alchemy.on = function (eventName, baseSelector, targetSelector, callback) {
- var baseNode = document.querySelector(baseSelector)
- baseNode.addEventListener(eventName, function (evt) {
- var targets = Array.from(baseNode.querySelectorAll(targetSelector))
- var currentNode = evt.target
- while (currentNode !== baseNode) {
- if (targets.includes(currentNode)) {
- callback.call(currentNode, evt)
- return
- }
- currentNode = currentNode.parentElement
- }
- });
-}
-
-Alchemy.ajax = function(method, url, data) {
- var xhr = new XMLHttpRequest()
- var token = document.querySelector('meta[name="csrf-token"]').attributes.content.textContent
- var promise = new Promise(function (resolve, reject) {
- xhr.onload = function() {
- try {
- resolve({
- data: JSON.parse(xhr.responseText),
- status: xhr.status
- })
- } catch (error) {
- reject(new Error(JSON.parse(xhr.responseText).error))
- }
- };
- xhr.onerror = function() {
- reject(new Error(xhr.statusText))
- }
- });
- xhr.open(method, url);
- xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
- xhr.setRequestHeader('Accept', 'application/json');
- xhr.setRequestHeader('X-CSRF-Token', token)
- if (data) {
- xhr.send(JSON.stringify(data))
- } else {
- xhr.send()
- }
-
- return promise
-}
diff --git a/app/assets/stylesheets/alchemy/nodes.scss b/app/assets/stylesheets/alchemy/nodes.scss
index c35731998c..898cf6718a 100644
--- a/app/assets/stylesheets/alchemy/nodes.scss
+++ b/app/assets/stylesheets/alchemy/nodes.scss
@@ -64,7 +64,7 @@
margin: 0;
padding: 0;
- .folded > li {
+ &.folded > li {
display: none;
}
}
diff --git a/app/javascript/alchemy/admin/node_tree.js b/app/javascript/alchemy/admin/node_tree.js
new file mode 100644
index 0000000000..f44447c9cf
--- /dev/null
+++ b/app/javascript/alchemy/admin/node_tree.js
@@ -0,0 +1,72 @@
+import Sortable from "sortablejs"
+import ajax from "./utils/ajax"
+import { on } from "./utils/events"
+
+function displayNodeFolders() {
+ document.querySelectorAll("li.menu-item").forEach((el) => {
+ const leftIconArea = el.querySelector(".nodes_tree-left_images")
+ const list = el.querySelector(".children")
+ const node = { folded: el.dataset.folded === "true", id: el.dataset.id }
+
+ if (list.children.length > 0 || node.folded) {
+ leftIconArea.innerHTML = HandlebarsTemplates.node_folder({ node: node })
+ } else {
+ leftIconArea.innerHTML = " "
+ }
+ })
+}
+
+function onFinishDragging(evt) {
+ const url = Alchemy.routes.move_api_node_path(evt.item.dataset.id)
+ const data = {
+ target_parent_id: evt.to.dataset.nodeId,
+ new_position: evt.newIndex
+ }
+
+ ajax("PATCH", url, data)
+ .then(() => {
+ const message = Alchemy.t("Successfully moved menu item")
+ Alchemy.growl(message)
+ displayNodeFolders()
+ })
+ .catch((error) => {
+ Alchemy.growl(error.message || error, "error")
+ })
+}
+
+function handleNodeFolders() {
+ on("click", ".nodes_tree", ".node_folder", function () {
+ const nodeId = this.dataset.nodeId
+ const menu_item = this.closest("li.menu-item")
+ const url = Alchemy.routes.toggle_folded_api_node_path(nodeId)
+ const list = menu_item.querySelector(".children")
+
+ ajax("PATCH", url)
+ .then(() => {
+ list.classList.toggle("folded")
+ menu_item.dataset.folded =
+ menu_item.dataset.folded == "true" ? "false" : "true"
+ displayNodeFolders()
+ })
+ .catch((error) => {
+ Alchemy.growl(error.message || error)
+ })
+ })
+}
+
+export default function NodeTree() {
+ handleNodeFolders()
+ displayNodeFolders()
+
+ document.querySelectorAll(".nodes_tree ul.children").forEach((el) => {
+ new Sortable(el, {
+ group: "nodes",
+ animation: 150,
+ fallbackOnBody: true,
+ swapThreshold: 0.65,
+ handle: ".node_name",
+ invertSwap: true,
+ onEnd: onFinishDragging
+ })
+ })
+}
diff --git a/app/javascript/alchemy/admin/utils/__tests__/ajax.spec.js b/app/javascript/alchemy/admin/utils/__tests__/ajax.spec.js
new file mode 100644
index 0000000000..52f3077e0b
--- /dev/null
+++ b/app/javascript/alchemy/admin/utils/__tests__/ajax.spec.js
@@ -0,0 +1,124 @@
+import xhrMock from "xhr-mock"
+import ajax from "../ajax"
+
+const token = "s3cr3t"
+
+beforeEach(() => {
+ document.head.innerHTML = ``
+ xhrMock.setup()
+})
+
+describe("ajax('get')", () => {
+ it("sends X-CSRF-TOKEN header", async () => {
+ xhrMock.get("/users", (req, res) => {
+ expect(req.header("X-CSRF-TOKEN")).toEqual(token)
+ return res.status(200).body('{"message":"Ok"}')
+ })
+ await ajax("get", "/users")
+ })
+
+ it("sends Content-Type header", async () => {
+ xhrMock.get("/users", (req, res) => {
+ expect(req.header("Content-Type")).toEqual(
+ "application/json; charset=utf-8"
+ )
+ return res.status(200).body('{"message":"Ok"}')
+ })
+ await ajax("get", "/users")
+ })
+
+ it("sends Accept header", async () => {
+ xhrMock.get("/users", (req, res) => {
+ expect(req.header("Accept")).toEqual("application/json")
+ return res.status(200).body('{"message":"Ok"}')
+ })
+ await ajax("get", "/users")
+ })
+
+ it("returns JSON", async () => {
+ xhrMock.get("/users", (_req, res) => {
+ return res.status(200).body('{"email":"mail@example.com"}')
+ })
+ await ajax("get", "/users").then((res) => {
+ expect(res.data).toEqual({ email: "mail@example.com" })
+ })
+ })
+
+ it("JSON parse errors get rejected", async () => {
+ xhrMock.get("/users", (_req, res) => {
+ return res.status(200).body('email => "mail@example.com"')
+ })
+ expect.assertions(1)
+ await ajax("get", "/users").catch((e) => {
+ expect(e.message).toMatch("Unexpected token")
+ })
+ })
+
+ it("network errors get rejected", async () => {
+ xhrMock.get("/users", () => {
+ return Promise.reject(new Error())
+ })
+ expect.assertions(1)
+ await ajax("get", "/users").catch((e) => {
+ expect(e.message).toEqual("An error occurred during the transaction")
+ })
+ })
+
+ it("server errors get rejected", async () => {
+ xhrMock.get("/users", (_req, res) => {
+ return res.status(401).body('{"error":"Unauthorized"}')
+ })
+ expect.assertions(1)
+ await ajax("get", "/users").catch((e) => {
+ expect(e.error).toEqual("Unauthorized")
+ })
+ })
+
+ it("server errors parsing errors get rejected", async () => {
+ xhrMock.get("/users", (_req, res) => {
+ return res.status(401).body("Unauthorized")
+ })
+ expect.assertions(1)
+ await ajax("get", "/users").catch((e) => {
+ expect(e.message).toMatch("Unexpected token")
+ })
+ })
+})
+
+describe("ajax('post')", () => {
+ it("sends X-CSRF-TOKEN header", async () => {
+ xhrMock.post("/users", (req, res) => {
+ expect(req.header("X-CSRF-TOKEN")).toEqual(token)
+ return res.status(200).body('{"message":"Ok"}')
+ })
+ await ajax("post", "/users")
+ })
+
+ it("sends Content-Type header", async () => {
+ xhrMock.post("/users", (req, res) => {
+ expect(req.header("Content-Type")).toEqual(
+ "application/json; charset=utf-8"
+ )
+ return res.status(200).body('{"message":"Ok"}')
+ })
+ await ajax("post", "/users")
+ })
+
+ it("sends Accept header", async () => {
+ xhrMock.post("/users", (req, res) => {
+ expect(req.header("Accept")).toEqual("application/json")
+ return res.status(200).body('{"message":"Ok"}')
+ })
+ await ajax("post", "/users")
+ })
+
+ it("sends JSON data", async () => {
+ xhrMock.post("/users", (req, res) => {
+ expect(req.body()).toEqual('{"email":"mail@example.com"}')
+ return res.status(200).body('{"message":"Ok"}')
+ })
+ await ajax("post", "/users", { email: "mail@example.com" })
+ })
+})
+
+afterEach(() => xhrMock.teardown())
diff --git a/app/javascript/alchemy/admin/utils/__tests__/events.spec.js b/app/javascript/alchemy/admin/utils/__tests__/events.spec.js
new file mode 100644
index 0000000000..e9b56634eb
--- /dev/null
+++ b/app/javascript/alchemy/admin/utils/__tests__/events.spec.js
@@ -0,0 +1,38 @@
+import { on } from "../events"
+
+describe("on", () => {
+ const callback = jest.fn()
+
+ beforeEach(() => {
+ document.body.innerHTML = `
+
+ `
+ })
+
+ it("adds event listener to base node", () => {
+ const baseNode = document.querySelector(".list")
+ const spy = jest.spyOn(baseNode, "addEventListener")
+ on("click", ".list", ".item", callback)
+ expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
+ spy.mockReset()
+ })
+
+ it("event triggered on matching child node calls callback", () => {
+ const childNode = document.querySelector(".first.item")
+ on("click", ".list", ".item", callback)
+ childNode.click()
+ expect(callback).toHaveBeenCalledWith(expect.any(MouseEvent))
+ })
+
+ it("event triggered on child of registered target still calls callback", () => {
+ const child = document.querySelector(".first.item span")
+ on("click", ".list", ".item", callback)
+ child.click()
+ expect(callback).toHaveBeenCalledWith(expect.any(MouseEvent))
+ })
+
+ afterEach(() => callback.mockReset())
+})
diff --git a/app/javascript/alchemy/admin/utils/ajax.js b/app/javascript/alchemy/admin/utils/ajax.js
new file mode 100644
index 0000000000..3266a7a4ad
--- /dev/null
+++ b/app/javascript/alchemy/admin/utils/ajax.js
@@ -0,0 +1,48 @@
+function buildPromise(xhr) {
+ return new Promise((resolve, reject) => {
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 400) {
+ try {
+ resolve({
+ data: JSON.parse(xhr.responseText),
+ status: xhr.status
+ })
+ } catch (error) {
+ reject(error)
+ }
+ } else {
+ try {
+ reject(JSON.parse(xhr.responseText))
+ } catch (error) {
+ reject(error)
+ }
+ }
+ }
+ xhr.onerror = () => {
+ reject(new Error("An error occurred during the transaction"))
+ }
+ })
+}
+
+function getToken() {
+ const metaTag = document.querySelector('meta[name="csrf-token"]')
+ return metaTag.attributes.content.textContent
+}
+
+export default function ajax(method, url, data) {
+ const xhr = new XMLHttpRequest()
+ const promise = buildPromise(xhr)
+
+ xhr.open(method, url)
+ xhr.setRequestHeader("Content-type", "application/json; charset=utf-8")
+ xhr.setRequestHeader("Accept", "application/json")
+ xhr.setRequestHeader("X-CSRF-Token", getToken())
+
+ if (data) {
+ xhr.send(JSON.stringify(data))
+ } else {
+ xhr.send()
+ }
+
+ return promise
+}
diff --git a/app/javascript/alchemy/admin/utils/events.js b/app/javascript/alchemy/admin/utils/events.js
new file mode 100644
index 0000000000..8d085ae993
--- /dev/null
+++ b/app/javascript/alchemy/admin/utils/events.js
@@ -0,0 +1,16 @@
+export function on(eventName, baseSelector, targetSelector, callback) {
+ document.querySelectorAll(baseSelector).forEach((baseNode) => {
+ baseNode.addEventListener(eventName, (evt) => {
+ const targets = Array.from(baseNode.querySelectorAll(targetSelector))
+ let currentNode = evt.target
+
+ while (currentNode !== baseNode) {
+ if (targets.includes(currentNode)) {
+ callback.call(currentNode, evt)
+ return
+ }
+ currentNode = currentNode.parentElement
+ }
+ })
+ })
+}
diff --git a/app/javascript/packs/alchemy/admin.js b/app/javascript/packs/alchemy/admin.js
index 142bbc44ed..97dd75b412 100644
--- a/app/javascript/packs/alchemy/admin.js
+++ b/app/javascript/packs/alchemy/admin.js
@@ -1,12 +1,14 @@
import translate from "alchemy/admin/i18n"
+import NodeTree from "alchemy/admin/node_tree"
// Global Alchemy object
if (typeof window.Alchemy === "undefined") {
window.Alchemy = {}
}
-// Global utility method for translating a given string
-//
-Alchemy.t = (key, replacement) => {
- return translate(key, replacement)
-}
+// Enhance the global Alchemy object with imported features
+Object.assign(Alchemy, {
+ // Global utility method for translating a given string
+ t: translate,
+ NodeTree
+})
diff --git a/app/views/alchemy/admin/nodes/index.html.erb b/app/views/alchemy/admin/nodes/index.html.erb
index 38e6a701c1..41259a463b 100644
--- a/app/views/alchemy/admin/nodes/index.html.erb
+++ b/app/views/alchemy/admin/nodes/index.html.erb
@@ -43,6 +43,5 @@
diff --git a/package.json b/package.json
index c189675af2..4739c05b0f 100644
--- a/package.json
+++ b/package.json
@@ -25,14 +25,16 @@
"homepage": "https://github.com/AlchemyCMS/alchemy_cms#readme",
"dependencies": {
"@rails/webpacker": "^5.0.1",
- "core-js": "^3"
+ "core-js": "^3",
+ "sortablejs": "^1.10.2"
},
"devDependencies": {
"babel-jest": "^25.2.6",
"jest": "^25.2.7",
"prettier": "^2.0.2",
"webpack": "^4.42.1",
- "webpack-dev-server": "^3.10.3"
+ "webpack-dev-server": "^3.10.3",
+ "xhr-mock": "^2.5.1"
},
"jest": {
"globals": {
diff --git a/vendor/assets/javascripts/sortable/Sortable.min.js b/vendor/assets/javascripts/sortable/Sortable.min.js
deleted file mode 100644
index eba0614973..0000000000
--- a/vendor/assets/javascripts/sortable/Sortable.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! Sortable 1.10.2 - 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 o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e"===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!1}return!1}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&h(t,e):h(t,e))||o&&t===n)return t;if(t===n)break}while(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode)}var i;return null}var f,p=/\s+/g;function k(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(p," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(p," ")}}function R(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];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function g(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=e.left-n&&r<=e.right+n,i=a>=e.top-n&&a<=e.bottom+n;return n&&o&&i?l=t:void 0}}),l}((t=t.touches?t.touches[0]:t).clientX,t.clientY);if(e){var n={};for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[j]._onDragOver(n)}}}function kt(t){z&&z.parentNode[j]._isOutsideThisEl(t.target)}function Rt(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={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 Ot(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!==Rt.supportPointer&&"PointerEvent"in window,emptyInsertThreshold:5};for(var o in O.initializePlugins(this,t,n),n)o in e||(e[o]=n[o]);for(var i in At(e),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&xt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?u(t,"pointerdown",this._onTapStart):(u(t,"mousedown",this._onTapStart),u(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(u(t,"dragover",this),u(t,"dragenter",this)),bt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,T())}function Xt(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||w||E?(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||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),d&&(c=d.call(u,s,a)),c}function Yt(t){t.draggable=!1}function Bt(){Dt=!1}function Ft(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function Ht(t){return setTimeout(t,0)}function Lt(t){return clearTimeout(t)}Rt.prototype={constructor:Rt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(ht=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,z):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){St.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&St.push(o)}}(o),!z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled||s.isContentEditable||(l=P(l,t.draggable,o,!1))&&l.animated||Z===l)){if(J=F(l),et=F(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return W({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),K("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c&&(c=c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return W({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),K("filter",n,{evt:e}),!0})))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(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;if(n&&!z&&n.parentNode===r){var s=X(n);if(q=r,G=(z=n).parentNode,V=z.nextSibling,Z=n,ot=a.group,rt={target:Rt.dragged=z,clientX:(e||t).clientX,clientY:(e||t).clientY},ct=rt.clientX-s.left,ut=rt.clientY-s.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,z.style["will-change"]="all",o=function(){K("delayEnded",i,{evt:t}),Rt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&(z.draggable=!0),i._triggerDragStart(t,e),W({sortable:i,name:"choose",originalEvent:t}),k(z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){g(z,t.trim(),Yt)}),u(l,"dragover",Pt),u(l,"mousemove",Pt),u(l,"touchmove",Pt),u(l,"mouseup",i._onDrop),u(l,"touchend",i._onDrop),u(l,"touchcancel",i._onDrop),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,z.draggable=!0),K("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(E||w))o();else{if(Rt.eventCanceled)return void this._onDrop();u(l,"mouseup",i._disableDelayedDrag),u(l,"touchend",i._disableDelayedDrag),u(l,"touchcancel",i._disableDelayedDrag),u(l,"mousemove",i._delayedDragTouchMoveHandler),u(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&u(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){z&&Yt(z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;d(t,"mouseup",this._disableDelayedDrag),d(t,"touchend",this._disableDelayedDrag),d(t,"touchcancel",this._disableDelayedDrag),d(t,"mousemove",this._delayedDragTouchMoveHandler),d(t,"touchmove",this._delayedDragTouchMoveHandler),d(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?u(document,"pointermove",this._onTouchMove):u(document,e?"touchmove":"mousemove",this._onTouchMove):(u(z,"dragend",this),u(q,"dragstart",this._onDragStart));try{document.selection?Ht(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){if(vt=!1,q&&z){K("dragStarted",this,{evt:e}),this.nativeDraggable&&u(document,"dragover",kt);var n=this.options;t||k(z,n.dragClass,!1),k(z,n.ghostClass,!0),Rt.active=this,t&&this._appendGhost(),W({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,Nt();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY))!==e;)e=t;if(z.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){if(e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);It()}},_onTouchMove:function(t){if(rt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=U&&v(U,!0),a=U&&r&&r.a,l=U&&r&&r.d,s=Ct&>&&b(gt),c=(i.clientX-rt.clientX+o.x)/(a||1)+(s?s[0]-Et[0]:0)/(a||1),u=(i.clientY-rt.clientY+o.y)/(l||1)+(s?s[1]-Et[1]:0)/(l||1);if(!Rt.active&&!vt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))o.right+10||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+10}(n,a,this)&&!g.animated){if(g===z)return A(!1);if(g&&l===n.target&&(s=g),s&&(i=X(s)),!1!==Xt(q,l,z,o,s,i,n,!!s))return O(),l.appendChild(z),G=l,N(),A(!0)}else if(s.parentNode===l){i=X(s);var v,m,b,y=z.parentNode!==l,w=!function(t,e,n){var o=n?t.left:t.top,i=n?t.right:t.bottom,r=n?t.width:t.height,a=n?e.left:e.top,l=n?e.right:e.bottom,s=n?e.width:e.height;return o===a||i===l||o+r/2===a+s/2}(z.animated&&z.toRect||o,s.animated&&s.toRect||i,a),E=a?"top":"left",D=Y(s,"top","top")||Y(z,"top","top"),S=D?D.scrollTop:void 0;if(ht!==s&&(m=i[E],yt=!1,wt=!w&&e.invertSwap||y),0!==(v=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,u=o?n.top:n.left,d=o?n.bottom:n.right,h=!1;if(!a)if(l&&pt