studio-engine 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -0
- data/README.md +22 -0
- data/app/assets/javascripts/studio/sortable.js +11 -0
- data/app/assets/tailwind/studio_engine/engine.css +59 -0
- data/app/controllers/concerns/studio/board/reorderable.rb +97 -0
- data/app/controllers/concerns/studio/error_handling.rb +34 -6
- data/app/models/concerns/studio/board/rankable.rb +84 -0
- data/app/views/components/_badge.html.erb +4 -2
- data/app/views/layouts/studio/_head.html.erb +10 -2
- data/app/views/layouts/studio/_smooth_load.html.erb +15 -0
- data/app/views/studio/_board_assets.html.erb +341 -0
- data/app/views/studio/board/_board.html.erb +151 -0
- data/app/views/studio/board/_card_shell.html.erb +47 -0
- data/app/views/studio/board/_column.html.erb +80 -0
- data/app/views/style/_tasks.html.erb +82 -11
- data/app/views/style/board/_demo_card.html.erb +22 -0
- data/lib/studio/engine.rb +1 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +14 -0
- metadata +12 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2aed8dd0e8d45e41602035462f0b60d01414ac3ea2f41484cbff9d4a40384cb8
|
|
4
|
+
data.tar.gz: e7aa1bfef279150c47cf3bd709de81b5b174f9027252a177a58a19e4609cf85c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b0ed377abc9501ce2a0fa19cc17d547e02498673ac15265ceac38555bf1fa241996e52c7dc5af337677a3436cfeb2209dd47f75fe70757bea96cebca21194e1
|
|
7
|
+
data.tar.gz: 1bd498dc182950382bfe3dca7fb5056bf6e2ff5760161ee8edf5481ef3c444a35ca7d4c51fd682b5d62f8e6c30aec787297cf537daba0956b8aefcc810b6172c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,109 @@
|
|
|
2
2
|
|
|
3
3
|
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
|
|
4
4
|
|
|
5
|
+
## 0.24.0 — 2026-07-28
|
|
6
|
+
|
|
7
|
+
The **smooth-load convention**, opt-in per app. Pages materialize behind the
|
|
8
|
+
current one and present themselves with a view transition — navigation renders
|
|
9
|
+
exactly once, with no stale-preview flash. Modifies an existing primitive:
|
|
10
|
+
`layouts/studio/_head` gains the convention partial and a configurable nav
|
|
11
|
+
spinner minimum.
|
|
12
|
+
|
|
13
|
+
**One ungated visual change on the bump, opt-in or not:** `.turbo-progress-bar`
|
|
14
|
+
in `engine.css` restyles every consumer's Turbo progress bar from Turbo's
|
|
15
|
+
default blue to the app's theme CTA color (3px, `var(--color-cta)` — defined in
|
|
16
|
+
both themes by the theme resolver, and the rule wins Turbo's injected-first
|
|
17
|
+
stylesheet cascade). Everything else is inert until an app sets
|
|
18
|
+
`Studio.smooth_load = true`.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Smooth-load convention (opt-in)** — `Studio.smooth_load` (default OFF)
|
|
23
|
+
renders `layouts/studio/_smooth_load` from `_head`: the `view-transition`
|
|
24
|
+
meta (Turbo 8 wraps page swaps in `document.startViewTransition`) plus
|
|
25
|
+
`turbo-cache-control: no-preview`, so the next page materializes behind the
|
|
26
|
+
current one and presents itself — navigation renders exactly once. An app
|
|
27
|
+
with known multi-second pages should fix those before opting in (no-preview
|
|
28
|
+
holds the old page until the fresh response arrives).
|
|
29
|
+
- **`Studio.nav_spinner_min_ms`** — the nav spinner's minimum display time,
|
|
30
|
+
previously hardcoded to 2500 in `_head`. Default unchanged (2500); smooth-load
|
|
31
|
+
apps typically drop it to ~300 so fast loads never linger on a spinner.
|
|
32
|
+
- **Smooth-load CSS in `engine.css`** — root fade-out / rise-in view-transition
|
|
33
|
+
keyframes (inert until the metas render), the `.vt-pinned-header` opt-in
|
|
34
|
+
utility (exactly one per page — a duplicate `view-transition-name` silently
|
|
35
|
+
disables the transition), the theme-colored `.turbo-progress-bar` noted
|
|
36
|
+
above, and a `prefers-reduced-motion` kill switch app e2e suites can lean on
|
|
37
|
+
(`reducedMotion: "reduce"`).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`rescue_from` order in `Studio::ErrorHandling`** — `RecordNotFound` now
|
|
42
|
+
resolves to `handle_not_found` (Rescuable matches last-registered first, so
|
|
43
|
+
the catch-all must register first). A missing record renders a real 404 from
|
|
44
|
+
`public/404.html` and **creates no ErrorLog row**; previously it was shadowed
|
|
45
|
+
by the catch-all, logged as an unexpected error, and soft-404'd (302 to root)
|
|
46
|
+
in production HTML. Per-host behavior changes only as each app bumps to this
|
|
47
|
+
release.
|
|
48
|
+
|
|
49
|
+
## 0.23.0 — 2026-07-28
|
|
50
|
+
|
|
51
|
+
Phase D, slice 1 — the **board primitive**. The three near-identical McRitchie
|
|
52
|
+
Studio kanban boards (tasks / news / content) each hand-rolled the SAME SortableJS
|
|
53
|
+
init, the SAME optimistic move → revert → toast, and a BYTE-IDENTICAL `reorder`
|
|
54
|
+
controller action; the depth chart hand-rolled a within-lane variant. This slice
|
|
55
|
+
lifts that convergence into a neutral engine primitive, following the same seam as
|
|
56
|
+
the leveling modals and tricks (documented `local_assigns.fetch` locals → a
|
|
57
|
+
page-level Alpine factory in an `_assets` partial → capability / `fx` / event hooks
|
|
58
|
+
→ a `demo:` local for `/admin/style` → vendored JS).
|
|
59
|
+
|
|
60
|
+
**Purely additive — new engine files only, no existing primitive's default
|
|
61
|
+
changes. MS and TM render identically on the bump** (they do not use
|
|
62
|
+
`studio/board/*` yet; the board rebases are separate later deploys).
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **`studio/board/_board`** — the board shell: a row of columns (drop zones), an
|
|
67
|
+
optional live `turbo_stream_from`, and a toast host. Documented `local_assigns`
|
|
68
|
+
locals: `columns:` `card_partial:` `card_as:` `card_locals:` `reorder_url:`
|
|
69
|
+
`reorder_payload:` (`:slugs` | `:ids`) `move_url:` `move_param:` `group:`
|
|
70
|
+
(`false` = within-zone) `id_attr:` `zone_attr:` `handle:` `filter:`
|
|
71
|
+
`zone_selector:` `draggable:` `empty_selector:` `live_channel:` `optimistic:`
|
|
72
|
+
`toasts:` `empty_label:` `header_slot:` `above_board_slot:` `fx:` `on_move_hook:`
|
|
73
|
+
`on_drop_hook:` `demo:`.
|
|
74
|
+
- **`studio/board/_column`** — one column: header + count badge + the
|
|
75
|
+
`.kanban-dropzone` (`id="dropzone-<key>"`, `data-<zone_attr>`) + `.kanban-empty`
|
|
76
|
+
empty state (the ZONE half of the identity contract).
|
|
77
|
+
- **`studio/board/_card_shell`** — OPTIONAL `render layout:` chrome that emits the
|
|
78
|
+
CARD half of the contract (`id="card-<id>"`, `.kanban-card`, `data-<id_attr>`,
|
|
79
|
+
`data-<zone_attr>`). Apps may hand-roll a bespoke card that satisfies the same
|
|
80
|
+
contract instead.
|
|
81
|
+
- **`studioBoard(opts)` factory — `studio/_board_assets`.** The Alpine data behind
|
|
82
|
+
the board, shipped at page level (a `<script>` cloned from a component template
|
|
83
|
+
never runs), like `levelingActionModal`. ONE factory drives BOTH shapes — it
|
|
84
|
+
reads `group` / `handle` / `filter` / `draggable` / `id_attr` / `zone_attr` from
|
|
85
|
+
opts, so the cross-column kanban and the within-lane depth chart are the same
|
|
86
|
+
code. Owns `initSortables`, `handleSortEnd` (cross-column move → PATCH →
|
|
87
|
+
optimistic revert + red-ring + toast; a same-zone drop NEVER PATCHes the zone),
|
|
88
|
+
`saveOrder` (reorder POST), `updateCounts`, `observeLive`,
|
|
89
|
+
`installExitStreamFallback`, `animateIn`, and toasts. Preserves the
|
|
90
|
+
`$nextTick` → `data-alpine-ready` init ordering (a documented flake fix). The
|
|
91
|
+
primary extension seam is the dispatched window events
|
|
92
|
+
`studio:board-moved` / `:board-reordered` / `:card-added` (detail
|
|
93
|
+
`{ record, from, to }`) — apps LISTEN, never patch the factory.
|
|
94
|
+
- **SortableJS v1.15.6 (MIT) VENDORED** — `app/assets/javascripts/studio/sortable.js`,
|
|
95
|
+
added to `config.assets.precompile` and loaded (deferred) by
|
|
96
|
+
`layouts/studio/_head`, mirroring the vendored canvas-confetti. CSP-safe, no CDN;
|
|
97
|
+
any board consumer gets `Sortable` with zero per-app wiring.
|
|
98
|
+
- **`Studio::Board::Rankable`** — the shared 100-gap rank read-model: a
|
|
99
|
+
`board_ordered` scope (`position DESC NULLS LAST, created_at DESC`),
|
|
100
|
+
`set_initial_position` (max + 100 within the zone), and `reposition!(ids, gap:,
|
|
101
|
+
direction: :desc | :asc)`.
|
|
102
|
+
- **`Studio::Board::Reorderable`** — the shared `reorder` controller action,
|
|
103
|
+
neutral param, delegating the 100-gap restamp to `Rankable#reposition!`.
|
|
104
|
+
- **`/admin/style` Board specimen** — the Tasks section now renders the REAL
|
|
105
|
+
`studio/board/board` primitive in `demo: true` mode (drag works, no POST),
|
|
106
|
+
above the static stage-palette reference.
|
|
107
|
+
|
|
5
108
|
## 0.22.0 — 2026-07-28
|
|
6
109
|
|
|
7
110
|
Two additive batches for the living style guide, converged under one release.
|
data/README.md
CHANGED
|
@@ -42,6 +42,16 @@ Studio.configure do |config|
|
|
|
42
42
|
)
|
|
43
43
|
config.theme_primary = "#4BAF50" # Override default violet
|
|
44
44
|
config.theme_logos = ["logo.svg"]
|
|
45
|
+
|
|
46
|
+
# Smooth-load convention (default OFF). Renders the view-transition +
|
|
47
|
+
# no-preview metas: Turbo page swaps materialize behind the current page and
|
|
48
|
+
# present with a view transition, exactly one render per navigation. Fix any
|
|
49
|
+
# multi-second pages BEFORE opting in — no-preview holds the old page until
|
|
50
|
+
# the fresh response arrives.
|
|
51
|
+
config.smooth_load = true
|
|
52
|
+
# Nav spinner minimum display (default 2500). Smooth-load apps typically
|
|
53
|
+
# drop to ~300; keep the high floor if multi-second ops ride the spinner.
|
|
54
|
+
config.nav_spinner_min_ms = 300
|
|
45
55
|
end
|
|
46
56
|
```
|
|
47
57
|
|
|
@@ -114,6 +124,18 @@ privileges while impersonating.
|
|
|
114
124
|
|
|
115
125
|
## UI Primitives
|
|
116
126
|
|
|
127
|
+
### Smooth-load header pin — `.vt-pinned-header`
|
|
128
|
+
|
|
129
|
+
When `Studio.smooth_load` is on, put `vt-pinned-header` on the app's sticky
|
|
130
|
+
header: it gets its own named view-transition group, so page content
|
|
131
|
+
transitions beneath a navbar that stays put (or smoothly morphs heights).
|
|
132
|
+
**Exactly one element per page** — a duplicate `view-transition-name` makes
|
|
133
|
+
the browser silently skip the whole transition, with no error and no animation.
|
|
134
|
+
|
|
135
|
+
```erb
|
|
136
|
+
<header class="sticky top-0 vt-pinned-header ...">
|
|
137
|
+
```
|
|
138
|
+
|
|
117
139
|
Render `components/emoji_swap` inside a link or button with the `group` class to
|
|
118
140
|
slide between two emoji on hover and keyboard focus. The CSS ships through
|
|
119
141
|
`studio_theme_css_tag`, including a reduced-motion fade fallback.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
SortableJS v1.15.6 — VENDORED into studio-engine (no CDN, CSP-safe).
|
|
3
|
+
Source: https://github.com/SortableJS/Sortable (MIT, (c) All Sortable contributors).
|
|
4
|
+
Minified build: https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js
|
|
5
|
+
Shipped as an engine asset so consumers get zero-per-app drag-and-drop with no
|
|
6
|
+
third-party CDN dependency. Defines the global `Sortable`; the studio/board
|
|
7
|
+
primitive (studio/_board_assets -> window.studioBoard) drives it. To re-vendor on
|
|
8
|
+
a bump: re-fetch the pinned dist file above, keep this header, bump the version.
|
|
9
|
+
*/
|
|
10
|
+
/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */
|
|
11
|
+
!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 I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}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<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),c=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function p(t,e,n){t.removeEventListener(e,n,!y&&d)}function f(t,e){if(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}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}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];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(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 D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,"border-top-width")),l-=h.left+parseInt(R(i,"border-left-width")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=b(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(t,!0),i=X(t)[e];o;){var r=X(o)[n];if(!("top"===n||"left"===n?r<=i:i<=r))return o;if(o===O())break;o=M(o,!1)}return!1}function B(t,e,n,o){for(var i=0,r=0,a=t.children;r<a.length;){if("none"!==a[r].style.display&&a[r]!==jt.ghost&&(o||a[r]!==jt.dragged)&&P(a[r],n.draggable,t,!1)){if(i===e)return a[r];i++}r++}return null}function F(t,e){for(var n=t.lastElementChild;n&&(n===jt.ghost||"none"===R(n,"display")||e&&!f(n,e));)n=n.previousElementSibling;return n||null}function j(t,e){var n=0;if(!t||!t.parentNode)return-1;for(;t=t.previousElementSibling;)"TEMPLATE"===t.nodeName.toUpperCase()||t===jt.clone||e&&!f(t,e)||n++;return n}function E(t){var e=0,n=0,o=O();if(t)do{var i=b(t),r=i.a,i=i.d}while(e+=t.scrollLeft*r,n+=t.scrollTop*i,t!==o&&(t=t.parentNode));return[e,n]}function M(t,e){if(!t||!t.getBoundingClientRect)return O();var n=t,o=!1;do{if(n.clientWidth<n.scrollWidth||n.clientHeight<n.scrollHeight){var i=R(n);if(n.clientWidth<n.scrollWidth&&("auto"==i.overflowX||"scroll"==i.overflowX)||n.clientHeight<n.scrollHeight&&("auto"==i.overflowY||"scroll"==i.overflowY)){if(!n.getBoundingClientRect||n===document.body)return O();if(o||e)return n;o=!0}}}while(n=n.parentNode);return O()}function S(t,e){return Math.round(t.top)===Math.round(e.top)&&Math.round(t.left)===Math.round(e.left)&&Math.round(t.height)===Math.round(e.height)&&Math.round(t.width)===Math.round(e.width)}function _(e,n){return function(){var t;m||(1===(t=arguments).length?e.call(this,t[0]):e.apply(this,t),m=setTimeout(function(){m=void 0},n))}}function H(t,e,n){t.scrollLeft+=e,t.scrollTop+=n}function C(t){var e=window.Polymer,n=window.jQuery||window.Zepto;return e&&e.dom?e.dom(t).cloneNode(!0):n?n(t).clone(!0)[0]:t.cloneNode(!0)}function T(t,e){R(t,"position","absolute"),R(t,"top",e.top),R(t,"left",e.left),R(t,"width",e.width),R(t,"height",e.height)}function x(t){R(t,"position",""),R(t,"top",""),R(t,"left",""),R(t,"width",""),R(t,"height","")}function L(n,o,i){var r={};return Array.from(n.children).forEach(function(t){var e;P(t,o.draggable,n,!1)&&!t.animated&&t!==i&&(e=X(t),r.left=Math.min(null!==(t=r.left)&&void 0!==t?t:1/0,e.left),r.top=Math.min(null!==(t=r.top)&&void 0!==t?t:1/0,e.top),r.right=Math.max(null!==(t=r.right)&&void 0!==t?t:-1/0,e.right),r.bottom=Math.max(null!==(t=r.bottom)&&void 0!==t?t:-1/0,e.bottom))}),r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}var K="Sortable"+(new Date).getTime();function A(){var e,o=[];return{captureAnimationState:function(){o=[],this.options.animation&&[].slice.call(this.el.children).forEach(function(t){var e,n;"none"!==R(t,"display")&&t!==jt.ghost&&(o.push({target:t,rect:X(t)}),e=I({},o[o.length-1].rect),!t.thisAnimationDuration||(n=b(t,!0))&&(e.top-=n.f,e.left-=n.e),t.fromRect=e)})},addAnimationState:function(t){o.push(t)},removeAnimationState:function(t){o.splice(function(t,e){for(var n in t)if(t.hasOwnProperty(n))for(var o in e)if(e.hasOwnProperty(o)&&e[o]===t[n][o])return Number(n);return-1}(o,{target:t}),1)},animateAll:function(t){var c=this;if(!this.options.animation)return clearTimeout(e),void("function"==typeof t&&t());var u=!1,d=0;o.forEach(function(t){var e=0,n=t.target,o=n.fromRect,i=X(n),r=n.prevFromRect,a=n.prevToRect,l=t.rect,s=b(n,!0);s&&(i.top-=s.f,i.left-=s.e),n.toRect=i,n.thisAnimationDuration&&S(r,i)&&!S(o,i)&&(l.top-i.top)/(l.left-i.left)==(o.top-i.top)/(o.left-i.left)&&(t=l,s=r,r=a,a=c.options,e=Math.sqrt(Math.pow(s.top-t.top,2)+Math.pow(s.left-t.left,2))/Math.sqrt(Math.pow(s.top-r.top,2)+Math.pow(s.left-r.left,2))*a.animation),S(i,o)||(n.prevFromRect=o,n.prevToRect=i,e=e||c.options.animation,c.animate(n,l,i,e)),e&&(u=!0,d=Math.max(d,e),clearTimeout(n.animationResetTimer),n.animationResetTimer=setTimeout(function(){n.animationTime=0,n.prevFromRect=null,n.fromRect=null,n.prevToRect=null,n.thisAnimationDuration=null},e),n.thisAnimationDuration=e)}),clearTimeout(e),u?e=setTimeout(function(){"function"==typeof t&&t()},d):"function"==typeof t&&t(),o=[]},animate:function(t,e,n,o){var i,r;o&&(R(t,"transition",""),R(t,"transform",""),i=(r=b(this.el))&&r.a,r=r&&r.d,i=(e.left-n.left)/(i||1),r=(e.top-n.top)/(r||1),t.animatingX=!!i,t.animatingY=!!r,R(t,"transform","translate3d("+i+"px,"+r+"px,0)"),this.forRepaintDummy=t.offsetWidth,R(t,"transition","transform "+o+"ms"+(this.options.easing?" "+this.options.easing:"")),R(t,"transform","translate3d(0,0,0)"),"number"==typeof t.animated&&clearTimeout(t.animated),t.animated=setTimeout(function(){R(t,"transition",""),R(t,"transform",""),t.animated=!1,t.animatingX=!1,t.animatingY=!1},o))}}}var N=[],W={initializeByDefault:!0},z={mount:function(e){for(var t in W)!W.hasOwnProperty(t)||t in e||(e[t]=W[t]);N.forEach(function(t){if(t.pluginName===e.pluginName)throw"Sortable: Cannot mount plugin ".concat(e.pluginName," more than once")}),N.push(e)},pluginEvent:function(e,n,o){var t=this;this.eventCanceled=!1,o.cancel=function(){t.eventCanceled=!0};var i=e+"Global";N.forEach(function(t){n[t.pluginName]&&(n[t.pluginName][i]&&n[t.pluginName][i](I({sortable:n},o)),n.options[t.pluginName]&&n[t.pluginName][e]&&n[t.pluginName][e](I({sortable:n},o)))})},initializePlugins:function(n,o,i,t){for(var e in N.forEach(function(t){var e=t.pluginName;(n.options[e]||t.initializeByDefault)&&((t=new t(n,o,n.options)).sortable=n,t.options=n.options,n[e]=t,a(i,t.defaults))}),n.options){var r;n.options.hasOwnProperty(e)&&(void 0!==(r=this.modifyOption(n,e,n.options[e]))&&(n.options[e]=r))}},getEventProperties:function(e,n){var o={};return N.forEach(function(t){"function"==typeof t.eventProperties&&a(o,t.eventProperties.call(n[t.pluginName],e))}),o},modifyOption:function(e,n,o){var i;return N.forEach(function(t){e[t.pluginName]&&t.optionListeners&&"function"==typeof t.optionListeners[n]&&(i=t.optionListeners[n].call(e[t.pluginName],o))}),i}};function G(t){var e=t.sortable,n=t.rootEl,o=t.name,i=t.targetEl,r=t.cloneEl,a=t.toEl,l=t.fromEl,s=t.oldIndex,c=t.newIndex,u=t.oldDraggableIndex,d=t.newDraggableIndex,h=t.originalEvent,p=t.putSortable,f=t.extraEventProperties;if(e=e||n&&n[K]){var g,m=e.options,t="on"+o.charAt(0).toUpperCase()+o.substr(1);!window.CustomEvent||y||w?(g=document.createEvent("Event")).initEvent(o,!0,!0):g=new CustomEvent(o,{bubbles:!0,cancelable:!0}),g.to=a||n,g.from=l||n,g.item=i||n,g.clone=r,g.oldIndex=s,g.newIndex=c,g.oldDraggableIndex=u,g.newDraggableIndex=d,g.originalEvent=h,g.pullMode=p?p.lastPutMode:void 0;var v,b=I(I({},f),z.getEventProperties(o,e));for(v in b)g[v]=b[v];n&&n.dispatchEvent(g),m[t]&&m[t].call(e,g)}}function U(t,e){var n=(o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{}).evt,o=i(o,q);z.pluginEvent.bind(jt)(t,e,I({dragEl:Z,parentEl:$,ghostEl:Q,rootEl:J,nextEl:tt,lastDownEl:et,cloneEl:nt,cloneHidden:ot,dragStarted:mt,putSortable:ct,activeSortable:jt.active,originalEvent:n,oldIndex:it,oldDraggableIndex:at,newIndex:rt,newDraggableIndex:lt,hideGhostForTarget:Xt,unhideGhostForTarget:Yt,cloneNowHidden:function(){ot=!0},cloneNowShown:function(){ot=!1},dispatchSortableEvent:function(t){V({sortable:e,name:t,originalEvent:n})}},o))}var q=["evt"];function V(t){G(I({putSortable:ct,cloneEl:nt,targetEl:Z,rootEl:J,oldIndex:it,oldDraggableIndex:at,newIndex:rt,newDraggableIndex:lt},t))}var Z,$,Q,J,tt,et,nt,ot,it,rt,at,lt,st,ct,ut,dt,ht,pt,ft,gt,mt,vt,bt,yt,wt,Dt=!1,Et=!1,St=[],_t=!1,Ct=!1,Tt=[],xt=!1,Ot=[],Mt="undefined"!=typeof document,At=c,Nt=w||y?"cssFloat":"float",It=Mt&&!n&&!c&&"draggable"in document.createElement("div"),Pt=function(){if(Mt){if(y)return!1;var t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}}(),kt=function(t,e){var n=R(t),o=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),i=B(t,0,e),r=B(t,1,e),a=i&&R(i),l=r&&R(r),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+X(i).width,t=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+X(r).width;if("flex"===n.display)return"column"===n.flexDirection||"column-reverse"===n.flexDirection?"vertical":"horizontal";if("grid"===n.display)return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(i&&a.float&&"none"!==a.float){e="left"===a.float?"left":"right";return!r||"both"!==l.clear&&l.clear!==e?"horizontal":"vertical"}return i&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||o<=s&&"none"===n[Nt]||r&&"none"===n[Nt]&&o<s+t)?"vertical":"horizontal"},Rt=function(t){function l(r,a){return function(t,e,n,o){var i=t.options.group.name&&e.options.group.name&&t.options.group.name===e.options.group.name;if(null==r&&(a||i))return!0;if(null==r||!1===r)return!1;if(a&&"clone"===r)return r;if("function"==typeof r)return l(r(t,e,n,o),a)(t,e,n,o);e=(a?t:e).options.group.name;return!0===r||"string"==typeof r&&r===e||r.join&&-1<r.indexOf(e)}}var e={},n=t.group;n&&"object"==o(n)||(n={name:n}),e.name=n.name,e.checkPull=l(n.pull,!0),e.checkPut=l(n.put),e.revertClone=n.revertClone,t.group=e},Xt=function(){!Pt&&Q&&R(Q,"display","none")},Yt=function(){!Pt&&Q&&R(Q,"display","")};Mt&&!n&&document.addEventListener("click",function(t){if(Et)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),Et=!1},!0);function Bt(t){if(Z){t=t.touches?t.touches[0]:t;var e=(i=t.clientX,r=t.clientY,St.some(function(t){var e=t[K].options.emptyInsertThreshold;if(e&&!F(t)){var n=X(t),o=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[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(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[K]=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 kt(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!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,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)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],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||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=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){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&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;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(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(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(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(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))<n)return;this._onDragStart(t,!0)}Q&&(r?(r.e+=a-(ht||0),r.f+=l-(pt||0)):r={a:1,b:0,c:0,d:1,e:a,f:l},r="matrix(".concat(r.a,",").concat(r.b,",").concat(r.c,",").concat(r.d,",").concat(r.e,",").concat(r.f,")"),R(Q,"webkitTransform",r),R(Q,"mozTransform",r),R(Q,"msTransform",r),R(Q,"transform",r),ht=a,pt=l,dt=i),t.cancelable&&t.preventDefault()}},_appendGhost:function(){if(!Q){var t=this.options.fallbackOnBody?document.body:J,e=X(Z,!0,At,!0,t),n=this.options;if(At){for(wt=t;"static"===R(wt,"position")&&"none"===R(wt,"transform")&&wt!==document;)wt=wt.parentNode;wt!==document.body&&wt!==document.documentElement?(wt===document&&(wt=O()),e.top+=wt.scrollTop,e.left+=wt.scrollLeft):wt=O(),Tt=E(wt)}k(Q=Z.cloneNode(!0),n.ghostClass,!1),k(Q,n.fallbackClass,!0),k(Q,n.dragClass,!0),R(Q,"transition",""),R(Q,"transform",""),R(Q,"box-sizing","border-box"),R(Q,"margin",0),R(Q,"top",e.top),R(Q,"left",e.left),R(Q,"width",e.width),R(Q,"height",e.height),R(Q,"opacity","0.8"),R(Q,"position",At?"absolute":"fixed"),R(Q,"zIndex","100000"),R(Q,"pointerEvents","none"),jt.ghost=Q,t.appendChild(Q),R(Q,"transform-origin",ft/parseInt(Q.style.width)*100+"% "+gt/parseInt(Q.style.height)*100+"%")}},_onDragStart:function(t,e){var n=this,o=t.dataTransfer,i=n.options;U("dragStart",this,{evt:t}),jt.eventCanceled?this._onDrop():(U("setupClone",this),jt.eventCanceled||((nt=C(Z)).removeAttribute("id"),nt.draggable=!1,nt.style["will-change"]="",this._hideClone(),k(nt,this.options.chosenClass,!1),jt.clone=nt),n.cloneId=Wt(function(){U("clone",n),jt.eventCanceled||(n.options.removeCloneOnHide||J.insertBefore(nt,Z),n._hideClone(),V({sortable:n,name:"clone"}))}),e||k(Z,i.dragClass,!0),e?(Et=!0,n._loopId=setInterval(n._emulateDragOver,50)):(p(document,"mouseup",n._onDrop),p(document,"touchend",n._onDrop),p(document,"touchcancel",n._onDrop),o&&(o.effectAllowed="move",i.setData&&i.setData.call(n,o,Z)),h(document,"drop",n),R(Z,"transform","translateZ(0)")),Dt=!0,n._dragStartId=Wt(n._dragStarted.bind(n,e,t)),h(document,"selectstart",n),mt=!0,window.getSelection().removeAllRanges(),u&&R(document.body,"user-select","none"))},_onDragOver:function(n){var o,i,r,t,e,a=this.el,l=n.target,s=this.options,c=s.group,u=jt.active,d=st===c,h=s.sort,p=ct||u,f=this,g=!1;if(!xt){if(void 0!==n.preventDefault&&n.cancelable&&n.preventDefault(),l=P(l,s.draggable,a,!0),O("dragOver"),jt.eventCanceled)return g;if(Z.contains(n.target)||l.animated&&l.animatingX&&l.animatingY||f._ignoreWhileAnimating===l)return A(!1);if(Et=!1,u&&!s.disabled&&(d?h||(i=$!==J):ct===this||(this.lastPutMode=st.checkPull(this,u,Z,n))&&c.checkPut(this,u,Z,n))){if(r="vertical"===this._getDirection(n,l),o=X(Z),O("dragOverValid"),jt.eventCanceled)return g;if(i)return $=J,M(),this._hideClone(),O("revert"),jt.eventCanceled||(tt?J.insertBefore(Z,tt):J.appendChild(Z)),A(!0);var m=F(a,s.draggable);if(m&&(S=n,c=r,x=X(F((E=this).el,E.options.draggable)),E=L(E.el,E.options,Q),!(c?S.clientX>E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY<C.top&&t.clientX<C.right:t.clientY<_.top-10||t.clientY<C.bottom&&t.clientX<C.left)){var v=B(a,0,s,!0);if(v===Z)return A(!1);if(D=X(l=v),!1!==Ht(J,a,Z,o,l,D,n,!1))return M(),a.insertBefore(Z,v),$=a,N(),A(!0)}else if(l.parentNode===a){var b,y,w,D=X(l),E=Z.parentNode!==a,S=(S=Z.animated&&Z.toRect||o,x=l.animated&&l.toRect||D,_=(e=r)?S.left:S.top,t=e?S.right:S.bottom,C=e?S.width:S.height,v=e?x.left:x.top,S=e?x.right:x.bottom,x=e?x.width:x.height,!(_===v||t===S||_+C/2===v+x/2)),_=r?"top":"left",C=Y(l,"top","top")||Y(Z,"top","top"),v=C?C.scrollTop:void 0;if(vt!==l&&(y=D[_],_t=!1,Ct=!S&&s.invertSwap||E),0!==(b=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,t=o?n.top:n.left,o=o?n.bottom:n.right,n=!1;if(!a)if(l&&yt<c*i){if(_t=!_t&&(1===bt?t+c*r/2<s:s<o-c*r/2)?!0:_t)n=!0;else if(1===bt?s<t+yt:o-yt<s)return-bt}else if(t+c*(1-i)/2<s&&s<o-c*(1-i)/2)return function(t){return j(Z)<j(t)?1:-1}(e);if((n=n||a)&&(s<t+c*r/2||o-c*r/2<s))return t+c/2<s?1:-1;return 0}(n,l,D,r,S?1:s.swapThreshold,null==s.invertedSwapThreshold?s.swapThreshold:s.invertedSwapThreshold,Ct,vt===l)))for(var T=j(Z);(w=$.children[T-=b])&&("none"===R(w,"display")||w===Q););if(0===b||w===l)return A(!1);bt=b;var x=(vt=l).nextElementSibling,E=!1,S=Ht(J,a,Z,o,l,D,n,E=1===b);if(!1!==S)return 1!==S&&-1!==S||(E=1===S),xt=!0,setTimeout(Kt,30),M(),E&&!x?a.appendChild(Z):l.parentNode.insertBefore(Z,E?x:l),C&&H(C,0,v-C.scrollTop),$=Z.parentNode,void 0===y||Ct||(yt=Math.abs(y-X(l)[_])),N(),A(!0)}}else{if(m===Z)return A(!1);if((l=m&&a===n.target?m:l)&&(D=X(l)),!1!==Ht(J,a,Z,o,l,D,n,!!l))return M(),m&&m.nextSibling?a.insertBefore(Z,m.nextSibling):a.appendChild(Z),$=a,N(),A(!0)}if(a.contains(Z))return A(!1)}return!1}function O(t,e){U(t,f,I({evt:n,isOwner:d,axis:r?"vertical":"horizontal",revert:i,dragRect:o,targetRect:D,canSort:h,fromSortable:p,target:l,completed:A,onMove:function(t,e){return Ht(J,a,Z,o,t,X(t),n,e)},changed:N},e))}function M(){O("dragOverAnimationCapture"),f.captureAnimationState(),f!==p&&p.captureAnimationState()}function A(t){return O("dragOverCompleted",{insertion:t}),t&&(d?u._hideClone():u._showClone(f),f!==p&&(k(Z,(ct||u).options.ghostClass,!1),k(Z,s.ghostClass,!0)),ct!==f&&f!==jt.active?ct=f:f===jt.active&&ct&&(ct=null),p===f&&(f._ignoreWhileAnimating=l),f.animateAll(function(){O("dragOverAnimationComplete"),f._ignoreWhileAnimating=null}),f!==p&&(p.animateAll(),p._ignoreWhileAnimating=null)),(l===Z&&!Z.animated||l===a&&!l.animated)&&(vt=null),s.dragoverBubble||n.rootEl||l===document||(Z.parentNode[K]._isOutsideThisEl(n.target),t||Bt(n)),!s.dragoverBubble&&n.stopPropagation&&n.stopPropagation(),g=!0}function N(){rt=j(Z),lt=j(Z,s.draggable),V({sortable:f,name:"change",toEl:a,newIndex:rt,newDraggableIndex:lt,originalEvent:n})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){p(document,"mousemove",this._onTouchMove),p(document,"touchmove",this._onTouchMove),p(document,"pointermove",this._onTouchMove),p(document,"dragover",Bt),p(document,"mousemove",Bt),p(document,"touchmove",Bt)},_offUpEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._onDrop),p(t,"touchend",this._onDrop),p(t,"pointerup",this._onDrop),p(t,"pointercancel",this._onDrop),p(t,"touchcancel",this._onDrop),p(document,"selectstart",this)},_onDrop:function(t){var e=this.el,n=this.options;rt=j(Z),lt=j(Z,n.draggable),U("drop",this,{evt:t}),$=Z&&Z.parentNode,rt=j(Z),lt=j(Z,n.draggable),jt.eventCanceled||(_t=Ct=Dt=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),zt(this.cloneId),zt(this._dragStartId),this.nativeDraggable&&(p(document,"drop",this),p(e,"dragstart",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),u&&R(document.body,"user-select",""),R(Z,"transform",""),t&&(mt&&(t.cancelable&&t.preventDefault(),n.dropBubble||t.stopPropagation()),Q&&Q.parentNode&&Q.parentNode.removeChild(Q),(J===$||ct&&"clone"!==ct.lastPutMode)&&nt&&nt.parentNode&&nt.parentNode.removeChild(nt),Z&&(this.nativeDraggable&&p(Z,"dragend",this),Lt(Z),Z.style["will-change"]="",mt&&!Dt&&k(Z,(ct||this).options.ghostClass,!1),k(Z,this.options.chosenClass,!1),V({sortable:this,name:"unchoose",toEl:$,newIndex:null,newDraggableIndex:null,originalEvent:t}),J!==$?(0<=rt&&(V({rootEl:$,name:"add",toEl:$,fromEl:J,originalEvent:t}),V({sortable:this,name:"remove",toEl:$,originalEvent:t}),V({rootEl:$,name:"sort",toEl:$,fromEl:J,originalEvent:t}),V({sortable:this,name:"sort",toEl:$,originalEvent:t})),ct&&ct.save()):rt!==it&&0<=rt&&(V({sortable:this,name:"update",toEl:$,originalEvent:t}),V({sortable:this,name:"sort",toEl:$,originalEvent:t})),jt.active&&(null!=rt&&-1!==rt||(rt=it,lt=at),V({sortable:this,name:"end",toEl:$,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){U("nulling",this),J=Z=$=Q=tt=nt=et=ot=ut=dt=mt=rt=lt=it=at=vt=bt=ct=st=jt.dragged=jt.ghost=jt.clone=jt.active=null,Ot.forEach(function(t){t.checked=!0}),Ot.length=ht=pt=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":Z&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move");t.cancelable&&t.preventDefault()}(t));break;case"selectstart":t.preventDefault()}},toArray:function(){for(var t,e=[],n=this.el.children,o=0,i=n.length,r=this.options;o<i;o++)P(t=n[o],r.draggable,this.el,!1)&&e.push(t.getAttribute(r.dataIdAttr)||function(t){var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;for(;n--;)o+=e.charCodeAt(n);return o.toString(36)}(t));return e},sort:function(t,e){var n={},o=this.el;this.toArray().forEach(function(t,e){e=o.children[e];P(e,this.options.draggable,o,!1)&&(n[t]=e)},this),e&&this.captureAnimationState(),t.forEach(function(t){n[t]&&(o.removeChild(n[t]),o.appendChild(n[t]))}),e&&this.animateAll()},save:function(){var t=this.options.store;t&&t.set&&t.set(this)},closest:function(t,e){return P(t,e||this.options.draggable,this.el,!1)},option:function(t,e){var n=this.options;if(void 0===e)return n[t];var o=z.modifyOption(this,t,e);n[t]=void 0!==o?o:e,"group"===t&&Rt(n)},destroy:function(){U("destroy",this);var t=this.el;t[K]=null,p(t,"mousedown",this._onTapStart),p(t,"touchstart",this._onTapStart),p(t,"pointerdown",this._onTapStart),this.nativeDraggable&&(p(t,"dragover",this),p(t,"dragenter",this)),Array.prototype.forEach.call(t.querySelectorAll("[draggable]"),function(t){t.removeAttribute("draggable")}),this._onDrop(),this._disableDelayedDragEvents(),St.splice(St.indexOf(this.el),1),this.el=t=null},_hideClone:function(){ot||(U("hideClone",this),jt.eventCanceled||(R(nt,"display","none"),this.options.removeCloneOnHide&&nt.parentNode&&nt.parentNode.removeChild(nt),ot=!0))},_showClone:function(t){"clone"===t.lastPutMode?ot&&(U("showClone",this),jt.eventCanceled||(Z.parentNode!=J||this.options.group.revertClone?tt?J.insertBefore(nt,tt):J.appendChild(nt):J.insertBefore(nt,Z),this.options.group.revertClone&&this.animate(Z,nt),R(nt,"display",""),ot=!1)):this._hideClone()}},Mt&&h(document,"touchmove",function(t){(jt.active||Dt)&&t.cancelable&&t.preventDefault()}),jt.utils={on:h,off:p,css:R,find:D,is:function(t,e){return!!P(t,e,t,!1)},extend:function(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t},throttle:_,closest:P,toggleClass:k,clone:C,index:j,nextTick:Wt,cancelNextTick:zt,detectDirection:kt,getChild:B,expando:K},jt.get=function(t){return t[K]},jt.mount=function(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];(e=e[0].constructor===Array?e[0]:e).forEach(function(t){if(!t.prototype||!t.prototype.constructor)throw"Sortable: Mounted plugin must be a constructor function, not ".concat({}.toString.call(t));t.utils&&(jt.utils=I(I({},jt.utils),t.utils)),z.mount(t)})},jt.create=function(t,e){return new jt(t,e)};var Gt,Ut,qt,Vt,Zt,$t,Qt=[],Jt=!(jt.version="1.15.6");function te(){Qt.forEach(function(t){clearInterval(t.pid)}),Qt=[]}function ee(){clearInterval($t)}var ne,oe=_(function(n,t,e,o){if(t.scroll){var i,r=(n.touches?n.touches[0]:n).clientX,a=(n.touches?n.touches[0]:n).clientY,l=t.scrollSensitivity,s=t.scrollSpeed,c=O(),u=!1;Ut!==e&&(Ut=e,te(),Gt=t.scroll,i=t.scrollFn,!0===Gt&&(Gt=M(e,!0)));var d=0,h=Gt;do{var p=h,f=X(p),g=f.top,m=f.bottom,v=f.left,b=f.right,y=f.width,w=f.height,D=void 0,E=void 0,S=p.scrollWidth,_=p.scrollHeight,C=R(p),T=p.scrollLeft,f=p.scrollTop,E=p===c?(D=y<S&&("auto"===C.overflowX||"scroll"===C.overflowX||"visible"===C.overflowX),w<_&&("auto"===C.overflowY||"scroll"===C.overflowY||"visible"===C.overflowY)):(D=y<S&&("auto"===C.overflowX||"scroll"===C.overflowX),w<_&&("auto"===C.overflowY||"scroll"===C.overflowY)),T=D&&(Math.abs(b-r)<=l&&T+y<S)-(Math.abs(v-r)<=l&&!!T),f=E&&(Math.abs(m-a)<=l&&f+w<_)-(Math.abs(g-a)<=l&&!!f);if(!Qt[d])for(var x=0;x<=d;x++)Qt[x]||(Qt[x]={});Qt[d].vx==T&&Qt[d].vy==f&&Qt[d].el===p||(Qt[d].el=p,Qt[d].vx=T,Qt[d].vy=f,clearInterval(Qt[d].pid),0==T&&0==f||(u=!0,Qt[d].pid=setInterval(function(){o&&0===this.layer&&jt.active._onTouchMove(Zt);var t=Qt[this.layer].vy?Qt[this.layer].vy*s:0,e=Qt[this.layer].vx?Qt[this.layer].vx*s:0;"function"==typeof i&&"continue"!==i.call(jt.dragged.parentNode[K],e,t,n,Zt,Qt[this.layer].el)||H(Qt[this.layer].el,e,t)}.bind({layer:d}),24))),d++}while(t.bubbleScroll&&h!==c&&(h=M(h,!1)));Jt=u}},30),n=function(t){var e=t.originalEvent,n=t.putSortable,o=t.dragEl,i=t.activeSortable,r=t.dispatchSortableEvent,a=t.hideGhostForTarget,t=t.unhideGhostForTarget;e&&(i=n||i,a(),e=e.changedTouches&&e.changedTouches.length?e.changedTouches[0]:e,e=document.elementFromPoint(e.clientX,e.clientY),t(),i&&!i.el.contains(e)&&(r("spill"),this.onSpill({dragEl:o,putSortable:n})))};function ie(){}function re(){}ie.prototype={startIndex:null,dragStart:function(t){t=t.oldDraggableIndex;this.startIndex=t},onSpill:function(t){var e=t.dragEl,n=t.putSortable;this.sortable.captureAnimationState(),n&&n.captureAnimationState();t=B(this.sortable.el,this.startIndex,this.options);t?this.sortable.el.insertBefore(e,t):this.sortable.el.appendChild(e),this.sortable.animateAll(),n&&n.animateAll()},drop:n},a(ie,{pluginName:"revertOnSpill"}),re.prototype={onSpill:function(t){var e=t.dragEl,t=t.putSortable||this.sortable;t.captureAnimationState(),e.parentNode&&e.parentNode.removeChild(e),t.animateAll()},drop:n},a(re,{pluginName:"removeOnSpill"});var ae,le,se,ce,ue,de=[],he=[],pe=!1,fe=!1,ge=!1;function me(n,o){he.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)})}function ve(){de.forEach(function(t){t!==se&&t.parentNode&&t.parentNode.removeChild(t)})}return jt.mount(new function(){function t(){for(var t in this.defaults={scroll:!0,forceAutoScrollFallback:!1,scrollSensitivity:30,scrollSpeed:10,bubbleScroll:!0},this)"_"===t.charAt(0)&&"function"==typeof this[t]&&(this[t]=this[t].bind(this))}return t.prototype={dragStarted:function(t){t=t.originalEvent;this.sortable.nativeDraggable?h(document,"dragover",this._handleAutoScroll):this.options.supportPointer?h(document,"pointermove",this._handleFallbackAutoScroll):t.touches?h(document,"touchmove",this._handleFallbackAutoScroll):h(document,"mousemove",this._handleFallbackAutoScroll)},dragOverCompleted:function(t){t=t.originalEvent;this.options.dragOverBubble||t.rootEl||this._handleAutoScroll(t)},drop:function(){this.sortable.nativeDraggable?p(document,"dragover",this._handleAutoScroll):(p(document,"pointermove",this._handleFallbackAutoScroll),p(document,"touchmove",this._handleFallbackAutoScroll),p(document,"mousemove",this._handleFallbackAutoScroll)),ee(),te(),clearTimeout(m),m=void 0},nulling:function(){Zt=Ut=Gt=Jt=$t=qt=Vt=null,Qt.length=0},_handleFallbackAutoScroll:function(t){this._handleAutoScroll(t,!0)},_handleAutoScroll:function(e,n){var o,i=this,r=(e.touches?e.touches[0]:e).clientX,a=(e.touches?e.touches[0]:e).clientY,t=document.elementFromPoint(r,a);Zt=e,n||this.options.forceAutoScrollFallback||w||y||u?(oe(e,this.options,t,n),o=M(t,!0),!Jt||$t&&r===qt&&a===Vt||($t&&ee(),$t=setInterval(function(){var t=M(document.elementFromPoint(r,a),!0);t!==o&&(o=t,te()),oe(e,i.options,t,n)},10),qt=r,Vt=a)):this.options.bubbleScroll&&M(t,!0)!==O()?oe(e,this.options,M(t,!1),!1):te()}},a(t,{pluginName:"scroll",initializeByDefault:!0})}),jt.mount(re,ie),jt.mount(new function(){function t(){this.defaults={swapClass:"sortable-swap-highlight"}}return t.prototype={dragStart:function(t){t=t.dragEl;ne=t},dragOverValid:function(t){var e=t.completed,n=t.target,o=t.onMove,i=t.activeSortable,r=t.changed,a=t.cancel;i.options.swap&&(t=this.sortable.el,i=this.options,n&&n!==t&&(t=ne,ne=!1!==o(n)?(k(n,i.swapClass,!0),n):null,t&&t!==ne&&k(t,i.swapClass,!1)),r(),e(!0),a())},drop:function(t){var e,n,o=t.activeSortable,i=t.putSortable,r=t.dragEl,a=i||this.sortable,l=this.options;ne&&k(ne,l.swapClass,!1),ne&&(l.swap||i&&i.options.swap)&&r!==ne&&(a.captureAnimationState(),a!==o&&o.captureAnimationState(),n=ne,t=(e=r).parentNode,l=n.parentNode,t&&l&&!t.isEqualNode(n)&&!l.isEqualNode(e)&&(i=j(e),r=j(n),t.isEqualNode(l)&&i<r&&r++,t.insertBefore(n,t.children[i]),l.insertBefore(e,l.children[r])),a.animateAll(),a!==o&&o.animateAll())},nulling:function(){ne=null}},a(t,{pluginName:"swap",eventProperties:function(){return{swapItem:ne}}})}),jt.mount(new function(){function t(o){for(var t in this)"_"===t.charAt(0)&&"function"==typeof this[t]&&(this[t]=this[t].bind(this));o.options.avoidImplicitDeselect||(o.options.supportPointer?h(document,"pointerup",this._deselectMultiDrag):(h(document,"mouseup",this._deselectMultiDrag),h(document,"touchend",this._deselectMultiDrag))),h(document,"keydown",this._checkKeyDown),h(document,"keyup",this._checkKeyUp),this.defaults={selectedClass:"sortable-selected",multiDragKey:null,avoidImplicitDeselect:!1,setData:function(t,e){var n="";de.length&&le===o?de.forEach(function(t,e){n+=(e?", ":"")+t.textContent}):n=e.textContent,t.setData("Text",n)}}}return t.prototype={multiDragKeyDown:!1,isMultiDrag:!1,delayStartGlobal:function(t){t=t.dragEl;se=t},delayEnded:function(){this.isMultiDrag=~de.indexOf(se)},setupClone:function(t){var e=t.sortable,t=t.cancel;if(this.isMultiDrag){for(var n=0;n<de.length;n++)he.push(C(de[n])),he[n].sortableIndex=de[n].sortableIndex,he[n].draggable=!1,he[n].style["will-change"]="",k(he[n],this.options.selectedClass,!1),de[n]===se&&k(he[n],this.options.chosenClass,!1);e._hideClone(),t()}},clone:function(t){var e=t.sortable,n=t.rootEl,o=t.dispatchSortableEvent,t=t.cancel;this.isMultiDrag&&(this.options.removeCloneOnHide||de.length&&le===e&&(me(!0,n),o("clone"),t()))},showClone:function(t){var e=t.cloneNowShown,n=t.rootEl,t=t.cancel;this.isMultiDrag&&(me(!1,n),he.forEach(function(t){R(t,"display","")}),e(),ue=!1,t())},hideClone:function(t){var e=this,n=(t.sortable,t.cloneNowHidden),t=t.cancel;this.isMultiDrag&&(he.forEach(function(t){R(t,"display","none"),e.options.removeCloneOnHide&&t.parentNode&&t.parentNode.removeChild(t)}),n(),ue=!0,t())},dragStartGlobal:function(t){t.sortable;!this.isMultiDrag&&le&&le.multiDrag._deselectMultiDrag(),de.forEach(function(t){t.sortableIndex=j(t)}),de=de.sort(function(t,e){return t.sortableIndex-e.sortableIndex}),ge=!0},dragStarted:function(t){var e,n=this,t=t.sortable;this.isMultiDrag&&(this.options.sort&&(t.captureAnimationState(),this.options.animation&&(de.forEach(function(t){t!==se&&R(t,"position","absolute")}),e=X(se,!1,!0,!0),de.forEach(function(t){t!==se&&T(t,e)}),pe=fe=!0)),t.animateAll(function(){pe=fe=!1,n.options.animation&&de.forEach(function(t){x(t)}),n.options.sort&&ve()}))},dragOver:function(t){var e=t.target,n=t.completed,t=t.cancel;fe&&~de.indexOf(e)&&(n(!1),t())},revert:function(t){var n,o,e=t.fromSortable,i=t.rootEl,r=t.sortable,a=t.dragRect;1<de.length&&(de.forEach(function(t){r.addAnimationState({target:t,rect:fe?X(t):a}),x(t),t.fromRect=a,e.removeAnimationState(t)}),fe=!1,n=!this.options.removeCloneOnHide,o=i,de.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)}))},dragOverCompleted:function(t){var e,n=t.sortable,o=t.isOwner,i=t.insertion,r=t.activeSortable,a=t.parentEl,l=t.putSortable,t=this.options;i&&(o&&r._hideClone(),pe=!1,t.animation&&1<de.length&&(fe||!o&&!r.options.sort&&!l)&&(e=X(se,!1,!0,!0),de.forEach(function(t){t!==se&&(T(t,e),a.appendChild(t))}),fe=!0),o||(fe||ve(),1<de.length?(o=ue,r._showClone(n),r.options.animation&&!ue&&o&&he.forEach(function(t){r.addAnimationState({target:t,rect:ce}),t.fromRect=ce,t.thisAnimationDuration=null})):r._showClone(n)))},dragOverAnimationCapture:function(t){var e=t.dragRect,n=t.isOwner,t=t.activeSortable;de.forEach(function(t){t.thisAnimationDuration=null}),t.options.animation&&!n&&t.multiDrag.isMultiDrag&&(ce=a({},e),e=b(se,!0),ce.top-=e.f,ce.left-=e.e)},dragOverAnimationComplete:function(){fe&&(fe=!1,ve())},drop:function(t){var o,i,r,a,n,e,l,s=t.originalEvent,c=t.rootEl,u=t.parentEl,d=t.sortable,h=t.dispatchSortableEvent,p=t.oldIndex,t=t.putSortable,f=t||this.sortable;s&&(o=this.options,i=u.children,ge||(o.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),k(se,o.selectedClass,!~de.indexOf(se)),~de.indexOf(se)?(de.splice(de.indexOf(se),1),ae=null,G({sortable:d,rootEl:c,name:"deselect",targetEl:se,originalEvent:s})):(de.push(se),G({sortable:d,rootEl:c,name:"select",targetEl:se,originalEvent:s}),s.shiftKey&&ae&&d.el.contains(ae)?(r=j(ae),a=j(se),~r&&~a&&r!==a&&function(){for(var e,t=r<a?(e=r,a):(e=a,r+1),n=o.filter;e<t;e++)~de.indexOf(i[e])||P(i[e],o.draggable,u,!1)&&(n&&("function"==typeof n?n.call(d,s,i[e],d):n.split(",").some(function(t){return P(i[e],t.trim(),u,!1)}))||(k(i[e],o.selectedClass,!0),de.push(i[e]),G({sortable:d,rootEl:c,name:"select",targetEl:i[e],originalEvent:s})))}()):ae=se,le=f)),ge&&this.isMultiDrag&&(fe=!1,(u[K].options.sort||u!==c)&&1<de.length&&(n=X(se),e=j(se,":not(."+this.options.selectedClass+")"),!pe&&o.animation&&(se.thisAnimationDuration=null),f.captureAnimationState(),pe||(o.animation&&(se.fromRect=n,de.forEach(function(t){var e;t.thisAnimationDuration=null,t!==se&&(e=fe?X(t):n,t.fromRect=e,f.addAnimationState({target:t,rect:e}))})),ve(),de.forEach(function(t){i[e]?u.insertBefore(t,i[e]):u.appendChild(t),e++}),p===j(se)&&(l=!1,de.forEach(function(t){t.sortableIndex!==j(t)&&(l=!0)}),l&&(h("update"),h("sort")))),de.forEach(function(t){x(t)}),f.animateAll()),le=f),(c===u||t&&"clone"!==t.lastPutMode)&&he.forEach(function(t){t.parentNode&&t.parentNode.removeChild(t)}))},nullingGlobal:function(){this.isMultiDrag=ge=!1,he.length=0},destroyGlobal:function(){this._deselectMultiDrag(),p(document,"pointerup",this._deselectMultiDrag),p(document,"mouseup",this._deselectMultiDrag),p(document,"touchend",this._deselectMultiDrag),p(document,"keydown",this._checkKeyDown),p(document,"keyup",this._checkKeyUp)},_deselectMultiDrag:function(t){if(!(void 0!==ge&&ge||le!==this.sortable||t&&P(t.target,this.options.draggable,this.sortable.el,!1)||t&&0!==t.button))for(;de.length;){var e=de[0];k(e,this.options.selectedClass,!1),de.shift(),G({sortable:this.sortable,rootEl:this.sortable.el,name:"deselect",targetEl:e,originalEvent:t})}},_checkKeyDown:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},a(t,{pluginName:"multiDrag",utils:{select:function(t){var e=t.parentNode[K];e&&e.options.multiDrag&&!~de.indexOf(t)&&(le&&le!==e&&(le.multiDrag._deselectMultiDrag(),le=e),k(t,e.options.selectedClass,!0),de.push(t))},deselect:function(t){var e=t.parentNode[K],n=de.indexOf(t);e&&e.options.multiDrag&&~n&&(k(t,e.options.selectedClass,!1),de.splice(n,1))}},eventProperties:function(){var n=this,o=[],i=[];return de.forEach(function(t){var e;o.push({multiDragElement:t,index:t.sortableIndex}),e=fe&&t!==se?-1:fe?j(t,":not(."+n.options.selectedClass+")"):j(t),i.push({multiDragElement:t,index:e})}),{items:r(de),clones:[].concat(he),oldIndicies:o,newIndicies:i}},optionListeners:{multiDragKey:function(t){return"ctrl"===(t=t.toLowerCase())?t="Control":1<t.length&&(t=t.charAt(0).toUpperCase()+t.substr(1)),t}}})}),jt});
|
|
@@ -180,3 +180,62 @@
|
|
|
180
180
|
@utility btn-lg {
|
|
181
181
|
@apply px-8 py-3 text-base rounded-xl shadow-sm;
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/* -- Smooth-load convention ------------------------------------------------
|
|
185
|
+
Paired with layouts/studio/_smooth_load.html.erb (the Studio.smooth_load
|
|
186
|
+
metas that opt Turbo into document.startViewTransition). These rules are
|
|
187
|
+
inert until an app renders those metas, so shipping them in the auto-bundled
|
|
188
|
+
layer costs nothing. The outgoing page fades while the incoming one rises in
|
|
189
|
+
underneath — quick enough to read as polish, not as a wait. */
|
|
190
|
+
::view-transition-old(root) {
|
|
191
|
+
animation: vt-page-out 160ms cubic-bezier(0.4, 0, 1, 1) both;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
::view-transition-new(root) {
|
|
195
|
+
animation: vt-page-in 220ms cubic-bezier(0, 0, 0.2, 1) both;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes vt-page-out {
|
|
199
|
+
to {
|
|
200
|
+
opacity: 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes vt-page-in {
|
|
205
|
+
from {
|
|
206
|
+
opacity: 0;
|
|
207
|
+
transform: translateY(8px);
|
|
208
|
+
}
|
|
209
|
+
to {
|
|
210
|
+
opacity: 1;
|
|
211
|
+
transform: none;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Opt-in header pin: put this on the app's sticky header so it gets its own
|
|
216
|
+
named transition group, lifted out of the root snapshot — page content
|
|
217
|
+
transitions beneath a navbar that stays put (or smoothly morphs heights).
|
|
218
|
+
EXACTLY ONE element per page: a duplicate view-transition-name makes the
|
|
219
|
+
browser silently skip the whole transition. */
|
|
220
|
+
@utility vt-pinned-header {
|
|
221
|
+
view-transition-name: studio-header;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Turbo's progress bar is the slow-load feedback under no-preview (the old
|
|
225
|
+
page holds until the fresh response). Theme it so every app's bar matches
|
|
226
|
+
its brand instead of Turbo's default. */
|
|
227
|
+
.turbo-progress-bar {
|
|
228
|
+
height: 3px;
|
|
229
|
+
background: var(--color-cta);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Reduced motion: keep the single-render swap, drop the choreography. Also
|
|
233
|
+
what the apps' e2e suites lean on (reducedMotion: "reduce") for stable
|
|
234
|
+
clicks. */
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
::view-transition-group(*),
|
|
237
|
+
::view-transition-old(*),
|
|
238
|
+
::view-transition-new(*) {
|
|
239
|
+
animation: none !important;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
module Board
|
|
5
|
+
# Shared `reorder` action for board controllers. The three McRitchie Studio
|
|
6
|
+
# kanban controllers (tasks / news / content) each carried a BYTE-IDENTICAL
|
|
7
|
+
# `reorder` — guard the incoming id array, restamp the column with 100-gaps
|
|
8
|
+
# INSIDE `rescue_and_log` (so a failure lands in ErrorLog — backend write
|
|
9
|
+
# discipline), render `{ success: true }`, and rescue to a 422. This concern is
|
|
10
|
+
# that action, neutral: the including controller declares its model, id column,
|
|
11
|
+
# and the incoming param, and the ranking rule itself is delegated to the model's
|
|
12
|
+
# Studio::Board::Rankable#reposition! (one place owns the 100-gap math). This is
|
|
13
|
+
# THE designated shared write action, so it owns the ErrorLog logging here rather
|
|
14
|
+
# than leaving each host to re-add it.
|
|
15
|
+
#
|
|
16
|
+
# class TasksController < ApplicationController
|
|
17
|
+
# include Studio::Board::Reorderable
|
|
18
|
+
# board_reorderable model: Task, id_attr: :slug, param: :slugs
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # Route it however the app names the endpoint:
|
|
22
|
+
# post "tasks/reorder", to: "tasks#reorder"
|
|
23
|
+
#
|
|
24
|
+
# The POST body the studio/board factory sends is `{ <param>: [...ids],
|
|
25
|
+
# zone: "<zone-key>" }`; this action reads only `<param>` (the ordered id list)
|
|
26
|
+
# and ignores the advisory `zone`, so a kanban and a depth-chart board share it.
|
|
27
|
+
module Reorderable
|
|
28
|
+
extend ActiveSupport::Concern
|
|
29
|
+
|
|
30
|
+
included do
|
|
31
|
+
class_attribute :board_reorder_model, instance_accessor: false
|
|
32
|
+
class_attribute :board_reorder_id_attr, instance_accessor: false, default: :slug
|
|
33
|
+
class_attribute :board_reorder_param, instance_accessor: false, default: :slugs
|
|
34
|
+
class_attribute :board_reorder_gap, instance_accessor: false, default: 100
|
|
35
|
+
class_attribute :board_reorder_direction, instance_accessor: false, default: :desc
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class_methods do
|
|
39
|
+
# Configure the reorder action for this controller's board.
|
|
40
|
+
def board_reorderable(model:, id_attr: :slug, param: :slugs, gap: 100, direction: :desc)
|
|
41
|
+
self.board_reorder_model = model
|
|
42
|
+
self.board_reorder_id_attr = id_attr
|
|
43
|
+
self.board_reorder_param = param
|
|
44
|
+
self.board_reorder_gap = gap
|
|
45
|
+
self.board_reorder_direction = direction
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# POST — restamp a column from its DOM-ordered id list. Neutral param
|
|
50
|
+
# (`slugs` or `ids`), 100-gap, DESC by default. Mirrors the MS hand-rolled
|
|
51
|
+
# `reorder` exactly, INCLUDING its ErrorLog logging: the restamp runs inside
|
|
52
|
+
# `rescue_and_log(target: nil)` (Studio::ErrorHandling), which captures a
|
|
53
|
+
# failure to ErrorLog and RE-RAISES to the outer 422 net below — the "every
|
|
54
|
+
# write path logs" discipline, owned here since this is the one shared write
|
|
55
|
+
# action. `respond_to?` guards it so a host whose ApplicationController somehow
|
|
56
|
+
# lacks the concern still degrades to a bare restamp + 422 (no NoMethodError).
|
|
57
|
+
def reorder
|
|
58
|
+
model = self.class.board_reorder_model
|
|
59
|
+
raise "board_reorderable model not configured" unless model
|
|
60
|
+
|
|
61
|
+
param = self.class.board_reorder_param
|
|
62
|
+
ids = params[param]
|
|
63
|
+
return render(json: { error: "#{param} required" }, status: :unprocessable_entity) unless ids.is_a?(Array)
|
|
64
|
+
|
|
65
|
+
if respond_to?(:rescue_and_log)
|
|
66
|
+
rescue_and_log(target: nil) { board_reorder_apply(model, ids) }
|
|
67
|
+
else
|
|
68
|
+
board_reorder_apply(model, ids)
|
|
69
|
+
end
|
|
70
|
+
render json: { success: true }
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Delegate to the model's Rankable#reposition! when available (the shared
|
|
78
|
+
# 100-gap rule), else fall back to the same per-id update_all loop inline so
|
|
79
|
+
# a model that only carries a `position` column still reorders.
|
|
80
|
+
def board_reorder_apply(model, ids)
|
|
81
|
+
id_attr = self.class.board_reorder_id_attr
|
|
82
|
+
gap = self.class.board_reorder_gap
|
|
83
|
+
direction = self.class.board_reorder_direction
|
|
84
|
+
|
|
85
|
+
if model.respond_to?(:reposition!)
|
|
86
|
+
model.reposition!(ids, gap: gap, direction: direction, id_attr: id_attr)
|
|
87
|
+
else
|
|
88
|
+
length = ids.length
|
|
89
|
+
ids.each_with_index do |id, index|
|
|
90
|
+
rank = direction.to_sym == :asc ? (index + 1) * gap : (length - index) * gap
|
|
91
|
+
model.where(id_attr => id).update_all(position: rank)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -3,8 +3,15 @@ module Studio
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
-
|
|
6
|
+
# ORDER IS LOAD-BEARING — ActiveSupport::Rescuable resolves handlers with
|
|
7
|
+
# `reverse_each`, so the LAST matching rescue_from registered wins. The
|
|
8
|
+
# catch-all must therefore be declared FIRST and the specific handlers
|
|
9
|
+
# after it, or StandardError shadows every one of them: RecordNotFound
|
|
10
|
+
# rendered "Something went wrong" and wrote an ErrorLog row on every 404,
|
|
11
|
+
# fleet-wide, while handle_not_found sat unreachable. Add new specific
|
|
12
|
+
# handlers BELOW this line, never above it.
|
|
7
13
|
rescue_from StandardError, with: :handle_unexpected_error
|
|
14
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
|
|
8
15
|
|
|
9
16
|
before_action :require_authentication
|
|
10
17
|
|
|
@@ -195,12 +202,33 @@ module Studio
|
|
|
195
202
|
raise e
|
|
196
203
|
end
|
|
197
204
|
|
|
198
|
-
# Layer 1:
|
|
205
|
+
# Layer 1: RecordNotFound — Rails' own 404, never an ErrorLog row.
|
|
206
|
+
#
|
|
207
|
+
# This handler exists for exactly ONE reason: to stop `handle_unexpected_error`
|
|
208
|
+
# from claiming a RecordNotFound and writing an ErrorLog row for it. It
|
|
209
|
+
# deliberately renders NOTHING and re-raises, which hands the exception to
|
|
210
|
+
# `ActionDispatch::PublicExceptions` — the same path an app with no rescue_from
|
|
211
|
+
# at all takes. Registering the handler and re-raising is therefore not a
|
|
212
|
+
# no-op: matching here is what keeps the catch-all's `create_error_log` from
|
|
213
|
+
# running (a raise inside a rescue_from handler propagates out; Rescuable does
|
|
214
|
+
# not re-dispatch it into another handler).
|
|
215
|
+
#
|
|
216
|
+
# Why nothing is rendered here. The method was unreachable from the initial
|
|
217
|
+
# commit until the rescue_from order was fixed (see the note in `included`),
|
|
218
|
+
# so whatever it did had never actually run — consumers have been served
|
|
219
|
+
# Rails' standard 404 all along. Anything rendered here would therefore be a
|
|
220
|
+
# NEW fleet-wide behaviour, not a restoration: an earlier attempt at this fix
|
|
221
|
+
# rendered public/404.html and silently flipped `Accept: */*` clients (curl,
|
|
222
|
+
# uptime monitors, crawlers, link previewers) from `404 text/html` to
|
|
223
|
+
# `404 application/json`, changed the JSON body shape, and dropped the
|
|
224
|
+
# DebugExceptions backtrace in development. Re-raising keeps every one of
|
|
225
|
+
# those intact, along with `public/404.<locale>.html` and any
|
|
226
|
+
# `config.exceptions_app` a host configures.
|
|
227
|
+
#
|
|
228
|
+
# The bug being fixed is the ErrorLog pollution and the "Something went wrong"
|
|
229
|
+
# copy — never the status code, the format, or the body.
|
|
199
230
|
def handle_not_found(exception)
|
|
200
|
-
|
|
201
|
-
format.html { redirect_to root_path, alert: "Not found" }
|
|
202
|
-
format.json { render json: { error: "Not found" }, status: :not_found }
|
|
203
|
-
end
|
|
231
|
+
raise exception
|
|
204
232
|
end
|
|
205
233
|
|
|
206
234
|
# Layer 1: Catch-all for unexpected errors — log + friendly response.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
module Board
|
|
5
|
+
# Shared 100-gap rank read-model for board-orderable records — the kanban
|
|
6
|
+
# columns (McRitchie Studio tasks / news / content) and the depth-chart lanes.
|
|
7
|
+
# A record carries an integer `position`; the board renders by `board_ordered`
|
|
8
|
+
# (highest position on top), and a drag-reorder restamps the whole column with
|
|
9
|
+
# 100-gaps via `reposition!`. Ranking is ZONE-SCOPED: a fresh rank is computed
|
|
10
|
+
# within the record's zone (a kanban `stage`, a depth-chart position group), so
|
|
11
|
+
# every column ranks independently and 100-spacing leaves room for the next
|
|
12
|
+
# drag insert without a full re-stamp.
|
|
13
|
+
#
|
|
14
|
+
# This is the read/rank half of the board primitive; the write half is
|
|
15
|
+
# Studio::Board::Reorderable (the controller action that calls reposition!).
|
|
16
|
+
#
|
|
17
|
+
# class Task < ApplicationRecord
|
|
18
|
+
# include Studio::Board::Rankable
|
|
19
|
+
# before_create :set_initial_position # seeds the genesis rank
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# class DepthChartEntry < ApplicationRecord
|
|
23
|
+
# include Studio::Board::Rankable
|
|
24
|
+
# self.board_zone_attr = :position_group # a lane, not a kanban stage
|
|
25
|
+
# end
|
|
26
|
+
module Rankable
|
|
27
|
+
extend ActiveSupport::Concern
|
|
28
|
+
|
|
29
|
+
included do
|
|
30
|
+
# The column the record ranks WITHIN. Default matches the three MS kanban
|
|
31
|
+
# boards (stage); a within-lane board overrides it, and `nil` ranks globally.
|
|
32
|
+
class_attribute :board_zone_attr, instance_accessor: false, default: :stage
|
|
33
|
+
|
|
34
|
+
# Board order: highest `position` first (freshest/top card wins), NULLS last,
|
|
35
|
+
# then newest by created_at as a stable tiebreak. Arel.sql: the fragment is a
|
|
36
|
+
# fixed literal, not user input.
|
|
37
|
+
scope :board_ordered, -> { order(Arel.sql("position DESC NULLS LAST, created_at DESC")) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Seed the genesis rank on create: max(position) within this record's zone plus
|
|
41
|
+
# a 100 gap. A no-op when `position` is already set, so an explicit rank is
|
|
42
|
+
# never clobbered. Wire from the host via `before_create :set_initial_position`.
|
|
43
|
+
def set_initial_position
|
|
44
|
+
return if position.present?
|
|
45
|
+
|
|
46
|
+
self.position = self.class.board_next_position(zone_value_for_rank)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# This record's zone value (nil when the model ranks globally).
|
|
50
|
+
def zone_value_for_rank
|
|
51
|
+
attr = self.class.board_zone_attr
|
|
52
|
+
attr && respond_to?(attr) ? public_send(attr) : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class_methods do
|
|
56
|
+
# max(position) + gap within a zone (or globally when the zone value / attr
|
|
57
|
+
# is blank). The genesis seed and the "bump to top on a stage move" both read
|
|
58
|
+
# from here, so the 100-gap rule lives in one place.
|
|
59
|
+
def board_next_position(zone_value = nil, gap: 100)
|
|
60
|
+
scope = board_zone_attr && zone_value ? where(board_zone_attr => zone_value) : all
|
|
61
|
+
(scope.maximum(:position) || 0) + gap
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Restamp a column top-to-bottom in ONE pass. `ids` arrive in DOM order (top
|
|
65
|
+
# first). Under the `position DESC` board sort the TOP card must hold the
|
|
66
|
+
# HIGHEST rank, so with `direction: :desc` (the board default) the first id
|
|
67
|
+
# gets the largest value and each next one drops by `gap` — 100-spacing that
|
|
68
|
+
# leaves gaps for the next drag insert. `direction: :asc` ranks the other way
|
|
69
|
+
# (first id smallest) for an ascending board. This is exactly the per-id
|
|
70
|
+
# `update_all(position: ...)` loop the MS tasks/news/content reorder actions
|
|
71
|
+
# each ran by hand, lifted into the model. Returns the ids it stamped.
|
|
72
|
+
def reposition!(ids, gap: 100, direction: :desc, id_attr: primary_key)
|
|
73
|
+
ids = Array(ids)
|
|
74
|
+
length = ids.length
|
|
75
|
+
ids.each_with_index do |id, index|
|
|
76
|
+
rank = direction.to_sym == :asc ? (index + 1) * gap : (length - index) * gap
|
|
77
|
+
where(id_attr => id).update_all(position: rank)
|
|
78
|
+
end
|
|
79
|
+
ids
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# locals: (text:, scheme: "neutral") %>
|
|
1
|
+
<%# locals: (text:, scheme: "neutral", data_board_count: nil) %>
|
|
2
2
|
<%#
|
|
3
3
|
Stage roles (stage-fresh ... stage-closed) form a shared pipeline palette
|
|
4
4
|
used by News and Content (and any future pipelines). Mapping:
|
|
@@ -32,4 +32,6 @@
|
|
|
32
32
|
else "bg-surface-alt text-body border-subtle"
|
|
33
33
|
end
|
|
34
34
|
%>
|
|
35
|
-
|
|
35
|
+
<%# data_board_count wires the badge as a studio/board count target (updateCounts sets
|
|
36
|
+
its textContent) — additive + opt-in, so every existing caller renders identically. %>
|
|
37
|
+
<span class="badge <%= scheme_classes %>"<% if local_assigns[:data_board_count] %> data-board-count="<%= data_board_count %>"<% end %>><%= text %></span>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
11
11
|
<%= csrf_meta_tags %>
|
|
12
12
|
<%= csp_meta_tag %>
|
|
13
|
+
<%= render "layouts/studio/smooth_load" %>
|
|
13
14
|
<link rel="icon" type="image/png" href="/favicon.png">
|
|
14
15
|
|
|
15
16
|
<%= yield :head %>
|
|
@@ -28,6 +29,12 @@
|
|
|
28
29
|
is unchanged and rides the same global. %>
|
|
29
30
|
<%= javascript_include_tag "studio/canvas_confetti", "data-turbo-track": "reload" %>
|
|
30
31
|
<%= javascript_include_tag "studio/studio_confetti", "data-turbo-track": "reload" %>
|
|
32
|
+
<%# SortableJS is VENDORED into the engine (studio/sortable.js) and shipped through
|
|
33
|
+
the asset pipeline — no CDN, CSP-safe (same-origin :self). Defines the global
|
|
34
|
+
`Sortable`; the studio/board primitive (studio/_board_assets -> window.studioBoard)
|
|
35
|
+
drives it for drag-reorder kanban AND depth-chart boards. Loaded here (deferred)
|
|
36
|
+
so any board consumer has it with zero per-app wiring, mirroring canvas_confetti. %>
|
|
37
|
+
<%= javascript_include_tag "studio/sortable", defer: true, "data-turbo-track": "reload" %>
|
|
31
38
|
<script>
|
|
32
39
|
window.fireSuccessConfetti = function() {
|
|
33
40
|
if (typeof confetti === 'undefined') return;
|
|
@@ -40,9 +47,10 @@
|
|
|
40
47
|
</script>
|
|
41
48
|
<script>
|
|
42
49
|
// Nav spinner — scale morph between theme toggle and loading spinner
|
|
43
|
-
// Minimum display time prevents quick flashes
|
|
50
|
+
// Minimum display time prevents quick flashes; per-app via
|
|
51
|
+
// Studio.nav_spinner_min_ms (smooth-load apps drop it to ~300).
|
|
44
52
|
window._navSpinnerShownAt = 0;
|
|
45
|
-
window._navSpinnerMinMs =
|
|
53
|
+
window._navSpinnerMinMs = <%= Studio.nav_spinner_min_ms.to_i %>;
|
|
46
54
|
window.showNavSpinner = function() {
|
|
47
55
|
window._navSpinnerShownAt = Date.now();
|
|
48
56
|
document.querySelectorAll('.nav-toggle-icon').forEach(function(e) { e.style.opacity = '0'; e.style.transform = 'scale(0) rotate(90deg)'; });
|