diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..dd84ea7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md
new file mode 100644
index 0000000..48d5f81
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/custom.md
@@ -0,0 +1,10 @@
+---
+name: Custom issue template
+about: Describe this issue template's purpose here.
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..df81be2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.gitignore b/.gitignore
index ce38f27..e0e9510 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-_ext
+/_ext/*
+!/_ext/svg/
routes
build
node_modules
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..3f5fc21
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v15.14.0
\ No newline at end of file
diff --git a/Model.drawio b/Model.drawio
new file mode 100644
index 0000000..629b961
--- /dev/null
+++ b/Model.drawio
@@ -0,0 +1 @@
+7V1bc+K4Ev41VOU8kPI98BjIZGb3MHuyk9nM7KOCBWhiWxxZBJhfv5ItG2zJxgSMsx5XpSq4Jd/0tVqfultyzxz7m48ELBefsQu9nqG5m5551zMMfWAN2D8u2QqJMxSSOUGukO0Ej+gnFEJNSFfIhWGmIsXYo2iZFU5xEMApzcgAIXidrTbDXvauSzCHkuBxCjxZ+g25dBFLB8bNTv4JovkiuTN7wbjEB0ll8SbhArh4vScyP/TMMcGYxr/8zRh6vPWSdvn22/abN3lxPv7+Z/h/8Nfov1//eOrHF7s/5pT0FQgM6Jsv3X+6tz5PHp5mqz/+fnn83+Oy/xX3LUe8G90mDQZd1n7iEBO6wHMcAO/DTjoieBW4kF9WY0e7OhOMl0yoM+EPSOlWKANYUcxEC+p7opS9Btl+Zwd97VpLBX/zC15rw1RwtxG3iI+2+0cPkCAfUkiEMH4N/uw52A+0magX4hWZilN/vszuPz1ZP74vH+7/XGsfb59/74u21Sggc0hLGnRPM1ifgpg9I9myEwn0AEWv2acDQrfnab0dfuyHgFANZ9lTvgJvJe70lfW18DMMVhLO4Rr5Hgg4oDMc0ARy9vAj4KF5wH5PWdvxFh69QkIR61O3ooByoEfTBfLcCdjiFW+RkILpS3I0WmCCfrLLggRzVkyo0AjDydR45GcKHAkMWZ2HBDY9FU1ASEWdKfY8sAzRc/TAvIrPYEHBCFOK/eRCGSVN+250QAl+Sa2BnmpPVWXhrQE3e0jKcItSJ7GCifU0xfF6Z4p0R8gW+2bI0opVRNzuCzOXIJizRkjvZ5i5+1kV72c42dsBjwEfAApHvBlDSTHTV327rpqSrvbF2BBKusqam+7ppQdntFArwyWYomA+iercWTvJF/G6XITZuTMv0ogFcl0YRBpDAQWxUnE1WWIU0Kg97BH7Y6021q7tns0eaMyO9d0x++PVCR3jgCkXQJEWQaaxa8i1tkwZhVnUlFpY2skPa+E2C+6xSnA2s2RJUEsIeyiyRHuDhNxLD8DvMyCj8Ung/TUajfq6pBOmrBOmAn8PPEPvAYeIIsyvT+K6Ob04BH1doNpGNVAHNWFqKzAdcUyhx+wSH3aukPufHtcpU4+1mxczLpir0LKuXhfeTkVLPigZOKoCrqaKg0apIm//faJ4gCSekQ8qW6MqH9SdC/FB5d1tu1HMctTesP8loJlmo6DdNAHae2l7Y9hk2zuNzIffS9s7dpNtL09eH9AGspZGFBMJlm76mp++SlyiqgYVTl9T11fJ9NVQziTMEtr5r5u+qgd2SVv59NXD83CP8/YjQkwpo6R58ZLpBaRhhiBnKsyQB/MngSnvhveIN1rZebHDJyOmO0dQ9jmAxx5QdYYHtpCoTnERYO+5go/bkEK/+Elipg9dRpUpV8O2Uf1jZvUHOmexQTxmVq/ui5pVk8E2pC4gQdzyaX0NqFY1sWeY16vZpwziLzzNsyoyJ91okjnJ/rV4iOgo08UpUz/lLMe6/NOIaXs5k+wz7Ee9lxN8HnrO8QcU3kZ8p5hhxBSluDzmSxNeS8lN2AigKnydbVTi6QJOXyB5xoC4orxjNEeNfdaxjEYdHDPrYjROx2jOjqpe2XNdE6g3ClB5KAIHkXlhFu9KjlPggLXNXnHLOnp9YA+qgW0aNaE9kND+8MreaLSSo8odH6qfDzk5OmQZFa2BrrWeDg2VdIgpmffM1OHXzoJ4i2kanIVdnCMLQj1HlB3cHbs4FdULpkGoQZX9wAmBuIKvkddzvNepZaIBfUTjmh3LqAp6VUZZlkR3Guqy61M9IewoxiVcLo59kGOk3pWMqW9VlqVaU9VplgHwJW+Le9j9EoufUKwWefkET19g/irTJPhT5KNxCVgzMzUGwStQBMleEVwnZS0zj3XzoSKndEkASdVL6nO36F1iaA24qhwuSmBr40RFuaEUz5khjc0HoluF2yWuERsSRfFq6TJr+bBvFXK1WmYialQS2VGjVJKbuhw1tiWhddHcxGtNy4Yar7Ubo9fs0qOSFNJ3H4TUZed5xIm7xUdNZW+92fV20/7sLTkooIwnciG3Hh886HcpTG8YhQrS34/0ydXHQOVwQcdAT4a16YifLnvWY/4I3Dhwr6CWBPpYpAqoFy6tlh6DlJZU8SEboEeQ9V5l8YyNwmwqfOt5ituLQjG1Vj4fn7OnT98yO1SfKlaMR+pl2TenZUmqF2V3lKgBSjSwsxSlqqHSh613FBpyIKPIURg+isTuNs+862ZGRRtFHMmMrNqSu+UQR8eMTob1pmFmZMjhALFsmw8HKo9bqCIjIeu3qsooJ5SSqmLLUZBvFaaFLbMl9alTZXZTlxfPkF34Xzg/4A3Z0ZyGaI6uDd/Ic8z28xx1Evp0RXiTR5rSNvNTO5U5T1p3bU4eQ/ZMd1TmZFibdvIYRXndKJgS1gCQ92UF03BhaXEIIyNwxY1/x0Sqa0NFJuLURkRkn9/D/tLmjoZcnIbktw/op5sT7WvJQMULjNbzkORR8zwEe7iLQJ2TnBTsJ1NCTlQaWZufxVQlEHfk5ERYVX4WFax1kZNkeZMqAjWOuvhV3NOLAlEllXL1uXIA8pEAF7Emi8/QRdY5/20oOA6DZLSdrHwUcARViVhxlTtAXgIY5pOsdhUeAV0RwHWgsMoY+0scxI/mLxW1fD40YtI5fo7RbwXdUum3btXFt0zZk3if7jzTka2Lk61+nmylC+gO6kj7V9qZqozUbpA9ZISK9kE8kjvVNsiq/Dp8SAngmtsiVZhhCYOCohC8wgeCf6jjE3DDm3ivvBupqilJ4tU7pCSOU5OW2LoE1q+8B6op3GYHs4mtgvHmMtnEpuzd+4RCisWlO3ZxYXZh5iJK1b9k0P5tjUw5iTTaiJEAH3ar1o4eT26OJh1KPajNYaNKLu245Imw2hW7d11c0pLdsSKnJXCxMh1XKeYkUoxTHUk8Av2K0aNkt4fzwy97YQWM9yTKgOw4x+U5h2O/kXO0P1s3ceup1i/dse6v2idRbMOcDy5No2W0XeLLW/ZCO893l+y6qIqlyvbsqMqJsKqC2BelKs181GWD6Pe937E7wxZHO2cGP8gsl/6+fxCfpVvmAS9IMwute5JnpPjbTU05RqxGvg5T9sVOXXPeI5pJqvvhL84UZDZdCM9ho3jqDE4ni6c2OOSjfC+985gNSi/07SatUTS16+FwkOud9rvc1MKuiGbRxPFCn4OSafZd7uMn3cQwObjYxNDOfVVXHepWOqPbn1foqDd8E+sbEu2V9Lab6pXPCWJLcLpXuq4op9NtpVYDrE3vLusUfmV3gddJX77ia7RlVzTrdfBAlamHQ3jreUm1sPNZV9eNqutfnJIhp0A52CHBmO4PDAQsF/wTKbzGPw==
\ No newline at end of file
diff --git a/README.md b/README.md
index bf2c1c9..2239b2c 100644
--- a/README.md
+++ b/README.md
@@ -12,16 +12,14 @@ The next version is mostly focused on adding missing essential features and port
Suggestions / Planned features:
+- Documentation
+
+- Possibility to hide and resize menus (layers, palette)
- Line tool
- Tiled mode
- Load palette from LPE file
-- Move colours in (advanced) palette editor
- Symmetry options
-- Custom color picker
- - custom code without dependencies
- - more features such as sliders / color modes
-
- Mobile
- Touch equivalent for mouse clicks
- Hide or scale ui
@@ -32,25 +30,29 @@ Suggestions / Planned features:
- Possibly add collaborate function
- Polish:
- - ctrl + a to select everything / selection -> all, same for deselection
- - Show colors which would need to be added to palette
+ - ctrl a to select everything / selection -> all, same for deselection
- Warning windows for wrong inputs
- Palette option remove unused colors
- Move selection with arrows
- - Update pivot buttons when resizing canvas
- Update borders by dragging the canvas' edges with the mouse when resizing canvas
- Move the canvases so they're centered after resizing the canvas (maybe a .center() method in layer class)
- - Trim canvas
+ - Scale selection
## How to Contribute
-Requirements: you must have node.js and git installed.
+### Requirements
+
+You must have node.js and git installed.
+
+You also need `npm` in version 7 (because of 2nd version of lockfile which was introduced there) which comes with Node.js 15 or newer. To simplify installation of proper versions you can make use of [nvm](https://github.com/nvm-sh/nvm#installing-and-updating) and run `nvm install` – it will activate proper Node.js version in your current command prompt session.
+
+### Contribution Workflow
1. Click **Fork** above. It will automatically create a copy of this repository and add it to your account.
2. Clone the repository to your computer.
-3. Open the folder in command prompt and run **npm install**
+3. Open the folder in command prompt and run **`npm install`**
4. Make any changes you would like to suggest.
-5. In command prompt run **node build.js** which will compile it to the */build* folder, where you can make sure it works
+5. In command prompt run **`npm run hot`** which will compile app to the `/build` folder, serve under [http://localhost:3000](http://localhost:3000), then open in your browser. Moreover, it restarts server every time you save your changes in a codebase. You can go even further by running `npm run hot:reload`, which will also trigger webpage reloads.
6. Add, Commit and Push your changes to your fork.
7. On the github page for your fork, click **New Pull Request** above the file list.
8. Change the **head repository** dropdown to your fork.
@@ -59,6 +61,12 @@ Requirements: you must have node.js and git installed.
If you have any trouble, see this page: https://help.github.com/en/articles/creating-a-pull-request-from-a-fork
+### Feature Toggles
+
+Some feature might be hidden by default. Functions to enable/disable them are available inside global `featureToggles` and operate on a `window.localStorage`.
+
+For example use `featureToggles.enableEllipseTool()` to make ellipse tool button visible. Then `featureToggles.disableEllipseTool()` to hide it.
+
## License
This software may not be resold, redistributed, rehosted or otherwise conveyed to a third party.
diff --git a/_ext/scripts/utilities/getSetText.js b/_ext/scripts/utilities/getSetText.js
index 4604c19..fbee0f9 100644
--- a/_ext/scripts/utilities/getSetText.js
+++ b/_ext/scripts/utilities/getSetText.js
@@ -7,4 +7,4 @@ function getText(elementId) {
function setText(elementId, text) {
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
element.textContent = text;
-}
+}
\ No newline at end of file
diff --git a/_ext/svg/ellipse.svg b/_ext/svg/ellipse.svg
new file mode 100644
index 0000000..e2f7391
--- /dev/null
+++ b/_ext/svg/ellipse.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/_ext/svg/filledellipse.svg b/_ext/svg/filledellipse.svg
new file mode 100644
index 0000000..803b053
--- /dev/null
+++ b/_ext/svg/filledellipse.svg
@@ -0,0 +1,22 @@
+
\ No newline at end of file
diff --git a/_ext/svg/line.svg b/_ext/svg/line.svg
new file mode 100644
index 0000000..f5aa17b
--- /dev/null
+++ b/_ext/svg/line.svg
@@ -0,0 +1,41 @@
+
+
+
diff --git a/_ext/svg/lospec_logo_1x.svg b/_ext/svg/lospec_logo_1x.svg
new file mode 100644
index 0000000..d48ff76
--- /dev/null
+++ b/_ext/svg/lospec_logo_1x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_ext/svg/newfile.svg b/_ext/svg/newfile.svg
new file mode 100644
index 0000000..ba18c86
--- /dev/null
+++ b/_ext/svg/newfile.svg
@@ -0,0 +1,17 @@
+
+
+
diff --git a/_ext/svg/openfile.svg b/_ext/svg/openfile.svg
new file mode 100644
index 0000000..e69f4af
--- /dev/null
+++ b/_ext/svg/openfile.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/build.js b/build.js
index 933fbba..47444a8 100644
--- a/build.js
+++ b/build.js
@@ -15,8 +15,16 @@ console.log('Building Pixel Editor');
function copy_images(){
- gulp.src('./images/')
- .pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
+ // Icons
+ gulp.src('./images/*.png').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
+ // Splash images
+ gulp.src('./images/Splash images/*.png').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
+ // Logs images
+ gulp.src('./images/Logs/*.gif').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
+}
+
+function copy_logs() {
+ gulp.src('logs/*.html').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
}
function render_js(){
@@ -56,4 +64,4 @@ function compile_page(){
// empty the build folder, or create it
fs.rmdirSync(BUILDDIR, { recursive: true });
fs.mkdirSync(BUILDDIR);
-gulp.parallel(copy_images, render_js, render_css, compile_page)();
+gulp.parallel(copy_images, copy_logs, render_js, render_css, compile_page)();
diff --git a/css/pixel-editor.scss b/css/pixel-editor.scss
index 98d3b9e..305cccc 100644
--- a/css/pixel-editor.scss
+++ b/css/pixel-editor.scss
@@ -40,6 +40,33 @@ body {
/* Disable Android and iOS callouts*/
}
+#help {
+ max-height: 500px;
+ overflow-y:scroll;
+
+ li {
+ margin-top:5px;
+ }
+
+ // Fancy scrollbar
+ &::-webkit-scrollbar {
+ background: #232125;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-track {
+ margin-top: -0.125em;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: #332f35;
+ border-radius: 0.25em;
+ border: solid 0.125em #232125; //same color as scrollbar back to fake padding
+ }
+ &::-webkit-scrollbar-corner {
+ background: #232125;
+ }
+}
+
#layer-properties-menu {
visibility: hidden;
margin: 0;
@@ -125,7 +152,7 @@ body {
z-index: 1120;
list-style-type: none;
overflow-y:scroll;
- overflow-x:hidden; // TODO: make the scroll bar a bit fancier
+ overflow-x:hidden;
#add-layer-button {
path {
fill: $baseicon;
@@ -178,7 +205,7 @@ body {
.layers-menu-entry {
cursor: pointer;
- margin-top: 2px;
+ margin-bottom: 2px;
font-size: 1em;
color: $basetext;
background-color: lighten($basecolor, 4%);
@@ -301,7 +328,7 @@ svg {
}
#pixel-canvas {
- z-index: 2;
+ z-index: 3;
background: transparent;
}
@@ -311,7 +338,7 @@ svg {
}
#tmp-canvas {
- z-index: 4;
+ z-index: 5;
background: transparent;
}
@@ -354,7 +381,7 @@ svg {
left: 64px;
right: 48px;
top: 48px;
- cursor: none;
+ cursor: default;
position: fixed;
display: block;
}
@@ -687,14 +714,18 @@ svg {
#tools-menu li button#pencil-bigger-button,
#tools-menu li button#zoom-in-button,
#tools-menu li button#eraser-bigger-button,
-#tools-menu li button#rectangle-bigger-button {
+#tools-menu li button#rectangle-bigger-button,
+#tools-menu li button#ellipse-bigger-button,
+#tools-menu li button#line-bigger-button {
left: 0;
}
#tools-menu li button#pencil-smaller-button,
#tools-menu li button#zoom-out-button,
#tools-menu li button#eraser-smaller-button,
-#tools-menu li button#rectangle-smaller-button {
+#tools-menu li button#rectangle-smaller-button,
+#tools-menu li button#ellipse-smaller-button,
+#tools-menu li button#line-smaller-button {
right: 0;
}
@@ -705,7 +736,11 @@ svg {
#tools-menu li.selected button#eraser-bigger-button,
#tools-menu li.selected button#eraser-smaller-button,
#tools-menu li.selected button#rectangle-bigger-button,
-#tools-menu li.selected button#rectangle-smaller-button {
+#tools-menu li.selected button#rectangle-smaller-button,
+#tools-menu li.selected button#ellipse-bigger-button,
+#tools-menu li.selected button#ellipse-smaller-button,
+#tools-menu li.selected button#line-bigger-button,
+#tools-menu li.selected button#line-smaller-button {
display: block;
}
@@ -775,8 +810,32 @@ svg {
}
}
+ div.update {
+ input {
+ background: $indent;
+ border: none;
+ border-radius: 4px;
+ color: $indenttext;
+ padding: 10px 20px;
+ margin: 0;
+ width: 60px;
+ text-align: center;
+ }
+ }
+ /*
+ input {
+ background: $indent;
+ border: none;
+ border-radius: 4px;
+ color: $indenttext;
+ padding: 10px 20px;
+ margin: 0;
+ width: 60px;
+ text-align: center;
+ }
+ */
+
button.default {
- float: right;
background: $basehover;
border: none;
border-radius: 4px;
@@ -789,17 +848,6 @@ svg {
}
}
- input {
- background: $indent;
- border: none;
- border-radius: 4px;
- color: $indenttext;
- padding: 10px 20px;
- margin: 0;
- width: 60px;
- text-align: center;
- }
-
.dropdown-button {
background: $basehover url('/pixel-editor/dropdown-arrow.png') right center no-repeat;
border: none;
@@ -1038,9 +1086,9 @@ svg {
input[type=number] {
position:relative;
margin-left:10px;
- height:15px;
- width:40px;
- padding:8px;
+ height:15px !important;
+ width:40px !important;
+ padding:8px !important;
}
input[type=number]::-webkit-outer-spin-button,
@@ -1212,4 +1260,602 @@ svg {
background: $baseselected;
}
}
+}
+/***********************COLOUR PICKER*****************************/
+#colour-picker {
+ background-color:$basecolor;
+ width:250px;
+ height:350px;
+ position:absolute;
+ display:inline-block;
+
+ input[type=text] {
+ background-color:$basetext;
+ color:$basecolor;
+ box-shadow:none;
+ border:none;
+ }
+
+ input[type=range] {
+ width: 100%;
+ margin: 2.2px 0;
+ background-color: transparent;
+ -webkit-appearance: none;
+ }
+ input[type=range]::-webkit-slider-runnable-track {
+ background: #484d4d;
+ border: 0;
+ width: 100%;
+ height: 25.6px;
+ cursor: pointer;
+ }
+ input[type=range]::-webkit-slider-thumb {
+ margin-top: -2.2px;
+ width: 18px;
+ height: 30px;
+ background: $basetext;
+ border: 0;
+ cursor: pointer;
+ -webkit-appearance: none;
+ }
+ input[type=range]::-moz-range-track {
+ background: #484d4d;
+ border: 0;
+ width: 100%;
+ height: 25.6px;
+ cursor: pointer;
+ }
+ input[type=range]::-moz-range-thumb {
+ width: 18px;
+ height: 30px;
+ background: $basetextweak;
+ border: 0;
+ cursor: pointer;
+ }
+
+ /*TODO: Use one of the selectors from https://stackoverflow.com/a/20541859/7077589 and figure out
+ how to remove the vertical space around the range input in IE*/
+ @supports (-ms-ime-align:auto) {
+ /* Pre-Chromium Edge only styles, selector taken from hhttps://stackoverflow.com/a/32202953/7077589 */
+ input[type=range].slider {
+ margin: 0;
+ /*Edge starts the margin from the thumb, not the track as other browsers do*/
+ }
+ }
+}
+
+#cp-modes {
+ margin: 0 0 0 0;
+ font-size:0;
+ height:40px;
+ float:left;
+ display:flex;
+ font-family: 'Roboto', sans-serif;
+ background-color:$basetextweak;
+ width:100%;
+
+ button {
+ font-size:14px;
+ left:0;
+ right:0;
+ margin:0 0 0 0;
+ border: none;
+ border-radius: 0;
+ height:100%;
+ width:37x;
+ background-color:$basehover;
+ color:$basetext;
+ cursor:pointer;
+ }
+
+ button:hover {
+ background-color:$baseicon;
+ color:$basetext;
+ }
+
+ button.cp-selected-mode {
+ background-color:$baseicon;
+ color:$basetext;
+ }
+
+ input {
+ width:60px;
+ }
+
+ div {
+ background-color:yellow;
+ width:100%;
+ height:100%;
+ z-index:2;
+ position:relative;
+ }
+}
+
+#sliders-container {
+ padding:10px;
+}
+
+.cp-slider-entry {
+ width:100%;
+ height:30px;
+ display:flex;
+ align-items:center;
+ margin-top:2px;
+ position:relative;
+
+ label {
+ width: 20px;
+ font-size:15px;
+ font-style: bold;
+ }
+
+ input[type=text] {
+ text-align:center;
+ width: 30px;
+ overflow:visible;
+ margin-left:4px;
+ }
+}
+
+.colour-picker-slider {
+ width:90%;
+}
+
+#cp-minipicker {
+ width:100%;
+ height:100px;
+ position:relative;
+ margin: 0 0 0 0;
+ z-index: inherit 2000;
+
+ input {
+ width:100%;
+ margin: none;
+ padding: none;
+ }
+
+ .cp-colours-previews {
+ width:100%;
+ position:relative;
+ }
+
+ .cp-colour-preview {
+ width:100%;
+ position:relative;
+ background-color:blue;
+ color:$basecolor;
+ float:left;
+ height:30px;
+ justify-content: center;
+ display:flex;
+ align-items: center;
+ font-size:12px;
+ }
+
+ #cp-colour-picking-modes {
+ width:100%;
+ position:relative;
+ }
+
+ button {
+ font-size:14px;
+ left:0;
+ right:0;
+ margin:0 0 0 0;
+ border: none;
+ border-radius: 0;
+ height:30px;
+ width:16.66666%;
+ float:left;
+ overflow:hidden;
+ background-color:$basehover;
+ color:$basetext;
+ cursor:pointer;
+ }
+
+ button:hover {
+ background-color:$baseicon;
+ color:$basetext;
+ }
+
+ button.cp-selected-mode {
+ background-color:$baseicon;
+ color:$basetext;
+ }
+}
+
+#cp-canvas-container {
+ width:100%;
+ height:100%;
+ position:relative;
+}
+
+#cp-spectrum {
+ width:100%;
+ height:100px;
+ position:absolute;
+ background-color: transparent;
+}
+
+.cp-picker-icon{
+ width:16px;
+ height:16px;
+ border-radius:100%;
+ position:absolute;
+ background-color:white;
+ z-index:2;
+ border:2px solid black;
+}
+
+
+/***************PALETTE BLOCK****************/
+div#palette-block {
+ z-index:1000;
+ position:relative;
+ resize: horizontal;
+ margin: 0 0 0 0;
+ width:600px;
+ height:400px;
+}
+
+div#palette-container {
+ display:inline-block;
+ background-color: #232125;
+ position:absolute;
+ scrollbar-color: #332f35 #232125;
+ scroll-behavior: smooth;
+ left:300px;
+ width:300px;
+ height:320px;
+ overflow-y: scroll;
+
+ &::-webkit-scrollbar {
+ background: #232125;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-track {
+ margin-top: -0.125em;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: #332f35;
+ border-radius: 0.25em;
+ border: solid 0.125em #232125; //same color as scrollbar back to fake padding
+ }
+ &::-webkit-scrollbar-corner {
+ background: #232125;
+ }
+}
+
+ul#palette-list {
+ list-style:none;
+ margin: 0 0 0 0;
+ padding: 0 0 0 0;
+ position:relative;
+ display:inline-block;
+
+ li {
+ float:left;
+ width:50px;
+ height:50px;
+ border:none;
+
+ min-width:20px;
+ min-height:20px;
+ max-width:75px;
+ max-height:75px;
+ }
+}
+
+
+div#pb-options {
+ position:relative;
+ left:280px;
+ height:30px;
+ width:300px;
+ top:300px;
+
+ button {
+ border-radius:none;
+ position:relative;
+ float:left;
+ width:50%;
+ height:100%;
+ text-align:center;
+ cursor: pointer;
+ font-size:16px;
+ background-color:$baseicon;
+ border:none;
+ }
+
+ button:hover {
+ color: $basehovertext;
+ background-color: $basehover;
+ }
+}
+
+/********SPLASH PAGE*************/
+#splash {
+ width:100% !important;
+ height:100%!important;
+
+ background-color: #232125 !important;
+ opacity: 1 !important;
+
+ #splash-input {
+ width:74%;
+ height:100% !important;
+ color:$baselink;
+
+ #splash-menu {
+ position:relative;
+ height:100%;
+ left:0;
+ top:0;
+ }
+
+ #editor-logo {
+ font-weight:bold;
+ text-transform:uppercase;
+ font-size:20px;
+ height:35vh;
+ width:100%;
+ position:relative;
+
+ background-image:url('https://cdn.discordapp.com/attachments/506277390050131978/795660870221955082/final.png');
+ background-size:cover;
+ background-position:center;
+ background-repeat:no-repeat;
+ }
+
+ #black {
+ width:100%;
+ height:100%;
+ position:relative;
+ background-color:rgba(0,0,0,0.2);
+ }
+
+ #sp-coverdata {
+ padding:20px;
+
+ p, a {
+ font-size:15px;
+ position:absolute;
+ text-transform:none;
+ right:20px;
+ }
+
+ p {
+ top:0px;
+ }
+
+ a {
+ font-size:17px;
+ bottom:20px;
+ text-decoration:underline;
+ }
+ }
+ }
+
+ #sp-quickstart-container {
+ height:100%;
+ max-height: 500px;
+ width:70%;
+ float:right;
+ padding:40px;
+
+ -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
+ -moz-box-sizing: border-box; /* Firefox, other Gecko */
+ box-sizing: border-box; /* Opera/IE 8+ */
+
+ overflow-y: scroll;
+ &::-webkit-scrollbar {
+ background: #232125;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-track {
+ margin-top: -0.125em;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: #332f35;
+ border-radius: 0.25em;
+ border: solid 0.125em #232125; //same color as scrollbar back to fake padding
+ }
+ &::-webkit-scrollbar-corner {
+ background: #232125;
+ }
+ }
+
+ #sp-quickstart {
+ display:flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ height:100%;
+
+ // Fancy scrollbar
+ &::-webkit-scrollbar {
+ background: #232125;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-track {
+ margin-top: -0.125em;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: #332f35;
+ border-radius: 0.25em;
+ border: solid 0.125em #232125; //same color as scrollbar back to fake padding
+ }
+ &::-webkit-scrollbar-corner {
+ background: #232125;
+ }
+ }
+
+ #sp-quickstart-title {
+ font-size:27px;
+ text-transform: uppercase;
+ font-weight: bold;
+ }
+
+ .sp-template {
+ display: flex;
+ align-items: center;
+ text-transform: uppercase;
+ width:16%;
+ border-radius:5%;
+ margin-right:4%;
+ margin-top:4%;
+ background-color:$basecolor;
+
+ font-size: 18px;
+ text-align:center;
+ font-weight: bold;
+
+ &:hover {
+ cursor:pointer;
+ background-color:$baseselected;
+ }
+
+ p {
+ span {
+ display:block;
+ font-size:14px !important;
+ margin: 0 0 0 0;
+ padding: 0 0 0 0;
+ }
+ width:100%;
+
+ float:left;
+ position:relative;
+ }
+ }
+
+ .sp-template:before {
+ content:'';
+ float:left;
+ padding-top:100%;
+ }
+
+ #sp-newpixel {
+ -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
+ -moz-box-sizing: border-box; /* Firefox, other Gecko */
+ box-sizing: border-box; /* Opera/IE 8+ */
+ display: inline-block;
+ width: 30% !important;
+ height:65vh;
+ padding:20px;
+ position:relative;
+ background-color:$basecolor;
+
+ #palette-button-splash {
+ left:5%;
+ width:90%;
+ }
+
+ .sp-np-entry {
+ width:100%;
+ text-align:center;
+ }
+
+ input {
+ border:none;
+ background-color: #232125;
+ color:$basetext;
+ font-size:14px;
+ width:40px;
+ padding:7px;
+ text-align:center;
+ }
+
+ #create-button {
+ font-size:18px;
+ width:150px;
+ margin-top:40px;
+ font-weight: bold;
+ }
+
+ #sp-mode-palette {
+ text-align: center;
+ position: relative;
+ float:bottom;
+ font-size:16px;
+ font-weight: bold;
+
+ div.button-menu {
+ border:2px solid $basetextweak;
+ border-radius:5px;
+ position:relative;
+ display:inline-block;
+ padding: 0 0 0 0;
+ text-align:left;
+ width:90%;
+
+ div {
+ border:none;
+ padding:none;
+ margin:none;
+ background-color:transparent;
+ width:50%;
+ float:left;
+ text-align: center;
+ height:25px;
+ cursor:pointer;
+ z-index:1;
+
+ p {
+ z-index:0;
+ -ms-transform: translateY(-60%);
+ transform: translateY(-60%);
+ }
+ }
+
+ .sp-interface-selected {
+ background-color: $basetextweak;
+ }
+ }
+ }
+ }
+
+ #splash-news {
+ box-sizing: border-box;
+ padding-left:20px;
+ width:26%;
+ height:100%;
+ background-color:#151516 !important;
+ float: right;
+ }
+
+ #latest-update {
+ width:100%;
+ font-size:15px;
+ height:90%;
+ line-height: 1.5;
+ position:relative;
+ top:20px;
+
+ overflow-y:scroll;
+ box-sizing: border-box;
+
+ img {
+ width:100%;
+ }
+
+ &::-webkit-scrollbar {
+ background: #232125;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-track {
+ margin-top: -0.125em;
+ width: 0.5em;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: #332f35;
+ border-radius: 0.25em;
+ border: solid 0.125em #232125; //same color as scrollbar back to fake padding
+ }
+ &::-webkit-scrollbar-corner {
+ background: #232125;
+ }
+ }
}
\ No newline at end of file
diff --git a/debug.log b/debug.log
new file mode 100644
index 0000000..3b8e60d
--- /dev/null
+++ b/debug.log
@@ -0,0 +1 @@
+[0114/110029.536:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
diff --git a/images/Logs/line-tool.gif b/images/Logs/line-tool.gif
new file mode 100644
index 0000000..6a793f0
Binary files /dev/null and b/images/Logs/line-tool.gif differ
diff --git a/images/Logs/palette-block.gif b/images/Logs/palette-block.gif
new file mode 100644
index 0000000..d42d45d
Binary files /dev/null and b/images/Logs/palette-block.gif differ
diff --git a/images/Logs/resize-canvas.gif b/images/Logs/resize-canvas.gif
new file mode 100644
index 0000000..b0ffb4c
Binary files /dev/null and b/images/Logs/resize-canvas.gif differ
diff --git a/images/Logs/scale-sprite.gif b/images/Logs/scale-sprite.gif
new file mode 100644
index 0000000..d3cb602
Binary files /dev/null and b/images/Logs/scale-sprite.gif differ
diff --git a/images/Logs/splash.gif b/images/Logs/splash.gif
new file mode 100644
index 0000000..946ddc5
Binary files /dev/null and b/images/Logs/splash.gif differ
diff --git a/images/Logs/trim-canvas.gif b/images/Logs/trim-canvas.gif
new file mode 100644
index 0000000..60bd3a8
Binary files /dev/null and b/images/Logs/trim-canvas.gif differ
diff --git a/images/Splash images/Caves.png b/images/Splash images/Caves.png
new file mode 100644
index 0000000..d90689c
Binary files /dev/null and b/images/Splash images/Caves.png differ
diff --git a/images/Splash images/Eye.png b/images/Splash images/Eye.png
new file mode 100644
index 0000000..3a938f2
Binary files /dev/null and b/images/Splash images/Eye.png differ
diff --git a/images/Splash images/Fusionnist.png b/images/Splash images/Fusionnist.png
new file mode 100644
index 0000000..1a97b49
Binary files /dev/null and b/images/Splash images/Fusionnist.png differ
diff --git a/images/Splash images/Glacier.png b/images/Splash images/Glacier.png
new file mode 100644
index 0000000..da4ddb4
Binary files /dev/null and b/images/Splash images/Glacier.png differ
diff --git a/images/Splash images/Mountains.png b/images/Splash images/Mountains.png
new file mode 100644
index 0000000..f59132d
Binary files /dev/null and b/images/Splash images/Mountains.png differ
diff --git a/images/Splash images/Polyphorge1.png b/images/Splash images/Polyphorge1.png
new file mode 100644
index 0000000..88c859f
Binary files /dev/null and b/images/Splash images/Polyphorge1.png differ
diff --git a/images/Splash images/Polyphorge2.png b/images/Splash images/Polyphorge2.png
new file mode 100644
index 0000000..2643f81
Binary files /dev/null and b/images/Splash images/Polyphorge2.png differ
diff --git a/images/Splash images/Rayquaza.png b/images/Splash images/Rayquaza.png
new file mode 100644
index 0000000..5b93c46
Binary files /dev/null and b/images/Splash images/Rayquaza.png differ
diff --git a/images/Splash images/Sweetie.png b/images/Splash images/Sweetie.png
new file mode 100644
index 0000000..1681973
Binary files /dev/null and b/images/Splash images/Sweetie.png differ
diff --git a/images/sked.png b/images/sked.png
new file mode 100644
index 0000000..2e9cb88
Binary files /dev/null and b/images/sked.png differ
diff --git a/js/Util.js b/js/Util.js
new file mode 100644
index 0000000..760ca03
--- /dev/null
+++ b/js/Util.js
@@ -0,0 +1,35 @@
+// Acts as a public static class
+class Util {
+ static getElement(elementOrElementId) {
+ return typeof elementOrElementId
+ ? document.getElementById(elementOrElementId)
+ : elementOrElementId;
+ }
+ static getText(elementId) {
+ return this.getElement(elementId).textContent;
+ }
+
+ static setText(elementId, text) {
+ this.getElement(elementId).textContent = text;
+ }
+ static getValue(elementId) {
+ return this.getElement(elementId).value;
+ }
+
+ static setValue(elementId, value) {
+ this.getElement(elementId).value = value;
+ }
+ //add class .selected to specified element
+ static select(elementId) {
+ this.getElement(elementId).classList.add('selected');
+ }
+
+ //remove .selected class from specified element
+ static deselect(elementId) {
+ this.getElement(elementId).classList.remove('selected');
+ }
+ //toggle the status of the .selected class on the specified element
+ static toggle(elementId) {
+ this.getElement(elementId).classList.toggle('selected');
+ }
+}
\ No newline at end of file
diff --git a/js/_addColor.js b/js/_addColor.js
index ac95af0..943bccd 100644
--- a/js/_addColor.js
+++ b/js/_addColor.js
@@ -1,8 +1,10 @@
let currentPalette = [];
-//adds the given color to the palette
-//input hex color string
-//returns list item element
+/** Adds the given color to the palette
+ *
+ * @param {*} newColor the colour to add
+ * @return the list item containing the added colour
+ */
function addColor (newColor) {
//add # at beginning if not present
if (newColor.charAt(0) != '#')
@@ -17,6 +19,7 @@ function addColor (newColor) {
button.style.backgroundColor = newColor;
button.addEventListener('mouseup', clickedColor);
listItem.appendChild(button);
+ listItem.classList.add("draggable-colour")
//insert new listItem element at the end of the colors menu (right before add button)
colorsMenu.insertBefore(listItem, colorsMenu.children[colorsMenu.children.length-1]);
@@ -35,11 +38,19 @@ function addColor (newColor) {
//hide edit button
button.parentElement.lastChild.classList.add('hidden');
- //show jscolor picker
- button.parentElement.firstChild.jscolor.show();
+ //show jscolor picker, if basic mode is enabled
+ if (pixelEditorMode == 'Basic')
+ button.parentElement.firstChild.jscolor.show();
+ else
+ showDialogue("palette-block", false);
});
- console.log(currentPalette);
-
return listItem;
}
+
+new Sortable(document.getElementById("colors-menu"), {
+ animation:100,
+ filter: ".noshrink",
+ draggable: ".draggable-colour",
+ onEnd: makeIsDraggingFalse
+});
\ No newline at end of file
diff --git a/js/_addColorButton.js b/js/_addColorButton.js
index 9da3169..7cb5f3f 100644
--- a/js/_addColorButton.js
+++ b/js/_addColorButton.js
@@ -1,4 +1,4 @@
-//add color button
+// add-color-button management
on('click', 'add-color-button', function(){
if (!documentCreated) return;
diff --git a/js/_algorithms.js b/js/_algorithms.js
new file mode 100644
index 0000000..7e94fc8
--- /dev/null
+++ b/js/_algorithms.js
@@ -0,0 +1,185 @@
+// CONSTS
+
+// Degrees to radiants
+let degreesToRad = Math.PI / 180;
+// I'm pretty sure that precision is necessary
+let referenceWhite = {x: 95.05, y: 100, z: 108.89999999999999};
+
+/**********************SECTION: COLOUR CONVERSIONS****************************** */
+
+/**
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes h, s, and l are contained in the set [0, 1] and
+ * returns r, g, and b in the set [0, 255].
+ *
+ * @param {number} h The hue
+ * @param {number} s The saturation
+ * @param {number} l The lightness
+ * @return {Array} The RGB representation
+ */
+function cpHslToRgb(h, s, l){
+ var r, g, b;
+
+ h /= 360;
+ s /= 100;
+ l /= 100;
+
+ if(s == 0){
+ r = g = b = l; // achromatic
+ }else{
+ var hue2rgb = function hue2rgb(p, q, t){
+ if(t < 0) t += 1;
+ if(t > 1) t -= 1;
+ if(t < 1/6) return p + (q - p) * 6 * t;
+ if(t < 1/2) return q;
+ if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+ return p;
+ }
+
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1/3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1/3);
+ }
+
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+}
+
+function hsvToRgb(h, s, v) {
+ var r, g, b;
+
+ h /= 360;
+ s /= 100;
+ v /= 100;
+ var i = Math.floor(h * 6);
+ var f = h * 6 - i;
+ var p = v * (1 - s);
+ var q = v * (1 - f * s);
+ var t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0: r = v, g = t, b = p; break;
+ case 1: r = q, g = v, b = p; break;
+ case 2: r = p, g = v, b = t; break;
+ case 3: r = p, g = q, b = v; break;
+ case 4: r = t, g = p, b = v; break;
+ case 5: r = v, g = p, b = q; break;
+ }
+
+ return [ r * 255, g * 255, b * 255 ];
+}
+
+function hslToHex(h, s, l) {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+ let r, g, b;
+ if (s === 0) {
+ r = g = b = l; // achromatic
+ } else {
+ const hue2rgb = (p, q, t) => {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ };
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+ const toHex = x => {
+ const hex = Math.round(x * 255).toString(16);
+ return hex.length === 1 ? '0' + hex : hex;
+ };
+
+ return `${toHex(r)}${toHex(g)}${toHex(b)}`;
+}
+
+function rgbToHsl(col) {
+ let r = col.r;
+ let g = col.g;
+ let b = col.b;
+
+ r /= 255, g /= 255, b /= 255;
+
+ let max = Math.max(r, g, b), min = Math.min(r, g, b);
+ let myH, myS, myL = (max + min) / 2;
+
+ if (max == min) {
+ myH = myS = 0; // achromatic
+ }
+ else {
+ let d = max - min;
+ myS = myL > 0.5 ? d / (2 - max - min) : d / (max + min);
+
+ switch (max) {
+ case r: myH = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: myH = (b - r) / d + 2; break;
+ case b: myH = (r - g) / d + 4; break;
+ }
+
+ myH /= 6;
+ }
+
+ return {h: myH, s: myS, l: myL };
+}
+
+ function rgbToHsv(col) {
+ let r = col.r;
+ let g = col.g;
+ let b = col.b;
+
+ r /= 255, g /= 255, b /= 255;
+
+ let max = Math.max(r, g, b), min = Math.min(r, g, b);
+ let myH, myS, myV = max;
+
+ let d = max - min;
+ myS = max == 0 ? 0 : d / max;
+
+ if (max == min) {
+ myH = 0; // achromatic
+ }
+ else {
+ switch (max) {
+ case r: myH = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: myH = (b - r) / d + 2; break;
+ case b: myH = (r - g) / d + 4; break;
+ }
+
+ myH /= 6;
+ }
+
+ return {h: myH, s: myS, v: myV};
+ }
+
+ function RGBtoCIELAB(rgbColour) {
+ // Convert to XYZ first via matrix transformation
+ let x = 0.412453 * rgbColour.r + 0.357580 * rgbColour.g + 0.180423 * rgbColour.b;
+ let y = 0.212671 * rgbColour.r + 0.715160 * rgbColour.g + 0.072169 * rgbColour.b;
+ let z = 0.019334 * rgbColour.r + 0.119193 * rgbColour.g + 0.950227 * rgbColour.b;
+
+ let xFunc = CIELABconvF(x / referenceWhite.x);
+ let yFunc = CIELABconvF(y / referenceWhite.y);
+ let zFunc = CIELABconvF(z / referenceWhite.z);
+
+ let myL = 116 * yFunc - 16;
+ let myA = 500 * (xFunc - yFunc);
+ let myB = 200 * (yFunc - zFunc);
+
+ return {l: myL, a: myA, b: myB};
+
+}
+function CIELABconvF(value) {
+ if (value > Math.pow(6/29, 3)) {
+ return Math.cbrt(value);
+ }
+
+ return 1/3 * Math.pow(6/29, 2) * value + 4/29;
+}
\ No newline at end of file
diff --git a/js/_changeZoom.js b/js/_changeZoom.js
index 4dc9eff..8ff9759 100644
--- a/js/_changeZoom.js
+++ b/js/_changeZoom.js
@@ -1,4 +1,9 @@
-function changeZoom (layer, direction, cursorLocation) {
+/** Changes the zoom level of the canvas
+ * @param {*} direction 'in' or 'out'
+ * @param {*} cursorLocation The position of the cursor when the user zoomed
+ */
+function changeZoom (direction, cursorLocation) {
+ // Computing current width and height
var oldWidth = canvasSize[0] * zoom;
var oldHeight = canvasSize[1] * zoom;
var newWidth, newHeight;
@@ -11,7 +16,9 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom;
//adjust canvas position
- layer.setCanvasOffset(layer.canvas.offsetLeft + (oldWidth - newWidth) *cursorLocation[0]/oldWidth, layer.canvas.offsetTop + (oldHeight - newHeight) *cursorLocation[1]/oldWidth);
+ layers[0].setCanvasOffset(
+ layers[0].canvas.offsetLeft + (oldWidth - newWidth) * cursorLocation[0]/oldWidth,
+ layers[0].canvas.offsetTop + (oldHeight - newHeight) * cursorLocation[1]/oldWidth);
}
//if you want to zoom in
else if (direction == 'in' && zoom + Math.ceil(zoom/10) < window.innerHeight/4){
@@ -20,11 +27,13 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom;
//adjust canvas position
- layer.setCanvasOffset(layer.canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth), layer.canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight));
+ layers[0].setCanvasOffset(
+ layers[0].canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth),
+ layers[0].canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight));
}
//resize canvas
- layer.resize();
+ layers[0].resize();
// adjust brush size
currentTool.updateCursor();
diff --git a/js/_checkerboard.js b/js/_checkerboard.js
index 28c28b6..28b15c8 100644
--- a/js/_checkerboard.js
+++ b/js/_checkerboard.js
@@ -13,7 +13,9 @@ var currentColor = firstCheckerBoardColor;
// Saving number of squares filled until now
var nSquaresFilled = 0;
-
+/** Fills the checkerboard canvas with squares with alternating colours
+ *
+ */
function fillCheckerboard() {
// Getting checkerboard context
var context = checkerBoard.context;
diff --git a/js/_clickedColor.js b/js/_clickedColor.js
index d326818..1f852c7 100644
--- a/js/_clickedColor.js
+++ b/js/_clickedColor.js
@@ -17,7 +17,7 @@ function clickedColor (e){
e.target.parentElement.classList.add('selected');
} else if (e.which == 3) { //right clicked color
- console.log('right clicked color button');
+ //console.log('right clicked color button');
//hide edit color button (to prevent it from showing)
e.target.parentElement.lastChild.classList.add('hidden');
diff --git a/js/_colorChanged.js b/js/_colorChanged.js
index c042dc0..4e31957 100644
--- a/js/_colorChanged.js
+++ b/js/_colorChanged.js
@@ -23,7 +23,7 @@ on('input', 'jscolor-hex-input', function (e) {
//changes all of one color to another after being changed from color picker
function colorChanged(e) {
- console.log('colorChanged() to ' + e.target.value);
+ //console.log('colorChanged() to ' + e.target.value);
//get colors
var newColor = hexToRgb(e.target.value);
var oldColor = e.target.oldColor;
@@ -41,7 +41,7 @@ function colorChanged(e) {
//check if selected color already matches another color
colors = document.getElementsByClassName('color-button');
- console.log(colors);
+ //console.log(colors);
var colorCheckingStyle = 'background: #bc60c4; color: white';
var newColorHex = e.target.value.toLowerCase();
@@ -54,11 +54,11 @@ function colorChanged(e) {
//if generated color matches this color
if (newColorHex == colors[i].jscolor.toString()) {
- console.log('%ccolor already exists'+(colors[i].parentElement.classList.contains('jscolor-active')?' (but is the current color)':''), colorCheckingStyle);
+ //console.log('%ccolor already exists'+(colors[i].parentElement.classList.contains('jscolor-active')?' (but is the current color)':''), colorCheckingStyle);
//if the color isnt the one that has the picker currently open
if (!colors[i].parentElement.classList.contains('jscolor-active')) {
- console.log('%cColor is duplicate', colorCheckingStyle);
+ //console.log('%cColor is duplicate', colorCheckingStyle);
//show the duplicate color warning
duplicateColorWarning.style.visibility = 'visible';
diff --git a/js/_colorPicker.js b/js/_colorPicker.js
new file mode 100644
index 0000000..007f204
--- /dev/null
+++ b/js/_colorPicker.js
@@ -0,0 +1,806 @@
+let sliders = document.getElementsByClassName("cp-slider-entry");
+let colourPreview = document.getElementById("cp-colour-preview");
+let colourValue = document.getElementById("cp-hex");
+let currentPickerMode = "rgb";
+let currentPickingMode = "mono";
+let styleElement = document.createElement("style");
+let miniPickerCanvas = document.getElementById("cp-spectrum");
+let miniPickerSlider = document.getElementById("cp-minipicker-slider");
+let activePickerIcon = document.getElementById("cp-active-icon");
+let pickerIcons = [activePickerIcon];
+let hexContainers = [document.getElementById("cp-colours-previews").children[0],null,null,null];
+let startPickerIconPos = [[0,0],[0,0],[0,0],[0,0]];
+let currPickerIconPos = [[0,0], [0,0],[0,0],[0,0]];
+let styles = ["",""];
+let draggingCursor = false;
+
+cpInit();
+
+function cpInit() {
+ // Appending the palette styles
+ document.getElementsByTagName("head")[0].appendChild(styleElement);
+
+ // Saving first icon position
+ startPickerIconPos[0] = [miniPickerCanvas.getBoundingClientRect().left, miniPickerCanvas.getBoundingClientRect().top];
+ // Set the correct size of the canvas
+ miniPickerCanvas.height = miniPickerCanvas.getBoundingClientRect().height;
+ miniPickerCanvas.width = miniPickerCanvas.getBoundingClientRect().width;
+
+ // Update picker position
+ updatePickerByHex(colourValue.value);
+ // Startup updating
+ updateAllSliders();
+ // Fill minislider
+ updateMiniSlider(colourValue.value);
+ // Fill minipicker
+ updatePickerByHex(colourValue.value);
+
+ updateMiniPickerSpectrum();
+}
+
+function hexUpdated() {
+ updatePickerByHex(colourValue.value);
+ updateSlidersByHex(colourValue.value);
+}
+
+// Applies the styles saved in the style array to the style element in the head of the document
+function updateStyles() {
+ styleElement.innerHTML = styles[0] + styles[1];
+}
+
+/** Updates the background gradients of the sliders given their value
+ * Updates the hex colour and its preview
+ * Updates the minipicker according to the computed hex colour
+ *
+ */
+function updateSliderValue (sliderIndex, updateMini = true) {
+ let toUpdate;
+ let slider;
+ let input;
+ let hexColour;
+ let sliderValues;
+
+ toUpdate = sliders[sliderIndex - 1];
+
+ slider = toUpdate.getElementsByTagName("input")[0];
+ input = toUpdate.getElementsByTagName("input")[1];
+
+ // Update label value
+ input.value = slider.value;
+
+ // Update preview colour
+ // get slider values
+ sliderValues = getSlidersValues();
+
+ // Generate preview colour
+ switch (currentPickerMode) {
+ case 'rgb':
+ hexColour = rgbToHex(sliderValues[0], sliderValues[1], sliderValues[2]);
+ break;
+ case 'hsv':
+ let tmpRgb = hsvToRgb(sliderValues[0], sliderValues[1], sliderValues[2]);
+ hexColour = rgbToHex(parseInt(tmpRgb[0]), parseInt(tmpRgb[1]), parseInt(tmpRgb[2]));
+ break;
+ case 'hsl':
+ hexColour = hslToHex(sliderValues[0], sliderValues[1], sliderValues[2]);
+ break;
+ default:
+ console.log("wtf select a decent picker mode");
+ return;
+ }
+ // Update preview colour div
+ colourPreview.style.backgroundColor = '#' + hexColour;
+ colourValue.value = '#' + hexColour;
+
+ // Update sliders background
+ // there's no other way than creating a custom css file, appending it to the head and
+ // specify the sliders' backgrounds here
+
+ styles[0] = '';
+ for (let i=0; i -8 && top > -8 && left < canvasRect.width-8 && top < canvasRect.height-8){
+ activePickerIcon.style["left"] = "" + left + "px";
+ activePickerIcon.style["top"]= "" + top + "px";
+
+ currPickerIconPos[0] = [left, top];
+ }
+
+ updateMiniPickerColour();
+ updateOtherIcons();
+ }
+}
+
+// Updates the main sliders given a hex value computed with the minipicker
+function updateSlidersByHex(hex, updateMini = true) {
+ let colour;
+ let mySliders = [sliders[0].getElementsByTagName("input")[0],
+ sliders[1].getElementsByTagName("input")[0],
+ sliders[2].getElementsByTagName("input")[0]];
+
+ switch (currentPickerMode) {
+ case 'rgb':
+ colour = hexToRgb(hex);
+
+ mySliders[0].value = colour.r;
+ mySliders[1].value = colour.g;
+ mySliders[2].value = colour.b;
+
+ break;
+ case 'hsv':
+ colour = rgbToHsv(hexToRgb(hex));
+
+ mySliders[0].value = colour.h * 360;
+ mySliders[1].value = colour.s * 100;
+ mySliders[2].value = colour.v * 100;
+
+ break;
+ case 'hsl':
+ colour = rgbToHsl(hexToRgb(hex));
+
+ mySliders[0].value = colour.h * 360;
+ mySliders[1].value = colour.s * 100;
+ mySliders[2].value = colour.l * 100;
+
+ break;
+ default:
+ break;
+ }
+
+ updateAllSliders(false);
+}
+
+// Gets the position of the picker cursor relative to the canvas
+function getCursorPosMinipicker(e) {
+ var x;
+ var y;
+
+ if (e.pageX != undefined && e.pageY != undefined) {
+ x = e.pageX;
+ y = e.pageY;
+ }
+ else {
+ x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
+ y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
+ }
+
+ x -= miniPickerCanvas.offsetLeft;
+ y -= miniPickerCanvas.offsetTop;
+
+ return [Math.round(x), Math.round(y)];
+}
+
+// Updates the minipicker given a hex computed by the main sliders
+// Moves the cursor
+function updatePickerByHex(hex) {
+ let hsv = rgbToHsv(hexToRgb(hex));
+ let xPos = miniPickerCanvas.width * hsv.h - 8;
+ let yPos = miniPickerCanvas.height * hsv.s + 8;
+
+ miniPickerSlider.value = hsv.v * 100;
+
+ currPickerIconPos[0][0] = xPos;
+ currPickerIconPos[0][1] = miniPickerCanvas.height - yPos;
+
+ if (currPickerIconPos[0][1] >= 92)
+ {
+ currPickerIconPos[0][1] = 91.999;
+ }
+
+ activePickerIcon.style.left = '' + xPos + 'px';
+ activePickerIcon.style.top = '' + (miniPickerCanvas.height - yPos) + 'px';
+ activePickerIcon.style.backgroundColor = '#' + getMiniPickerColour();
+
+ colourPreview.style.backgroundColor = hex;
+
+ updateOtherIcons();
+ updateMiniSlider(hex);
+}
+
+// Fired when the value of the minislider changes: updates the spectrum gradient and the hex colour
+function miniSliderInput(event) {
+ let newHex;
+ let newHsv = rgbToHsv(hexToRgb(getMiniPickerColour()));
+ let rgb;
+
+ // Adding slider value to value
+ newHsv.v = parseInt(event.target.value);
+ // Updating hex
+ rgb = hsvToRgb(newHsv.h * 360, newHsv.s * 100, newHsv.v);
+ newHex = rgbToHex(Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]));
+
+ colourValue.value = newHex;
+
+ updateMiniPickerSpectrum();
+ updateMiniPickerColour();
+}
+
+// Updates the hex colour after having changed the minislider (MERGE)
+function updateMiniPickerColour() {
+ let hex = getMiniPickerColour();
+
+ activePickerIcon.style.backgroundColor = '#' + hex;
+
+ // Update hex and sliders based on hex
+ colourValue.value = '#' + hex;
+ colourPreview.style.backgroundColor = '#' + hex;
+
+ updateSlidersByHex(hex);
+ updateMiniSlider(hex);
+ updateOtherIcons();
+}
+
+// Returns the current colour of the minipicker
+function getMiniPickerColour() {
+ let hex;
+ let pickedColour;
+
+ pickedColour = miniPickerCanvas.getContext('2d').getImageData(currPickerIconPos[0][0] + 8,
+ currPickerIconPos[0][1] + 8, 1, 1).data;
+
+ hex = rgbToHex(pickedColour[0], pickedColour[1], pickedColour[2]);
+
+ return hex;
+}
+
+// Update the background gradient of the slider in the minipicker
+function updateMiniSlider(hex) {
+ let rgb = hexToRgb(hex);
+
+ styles[1] = "input[type=range]#cp-minipicker-slider::-webkit-slider-runnable-track { background: rgb(2,0,36);";
+ styles[1] += "background: linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(0,0,0,1) 0%, " +
+ "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ",1) 100%);}";
+
+ updateMiniPickerSpectrum();
+ updateStyles();
+}
+
+// Updates the gradient of the spectrum canvas in the minipicker
+function updateMiniPickerSpectrum() {
+ let ctx = miniPickerCanvas.getContext('2d');
+ let hsv = rgbToHsv(hexToRgb(colourValue.value));
+ let tmp;
+ let white = {h:hsv.h * 360, s:0, v: parseInt(miniPickerSlider.value)};
+
+ white = hsvToRgb(white.h, white.s, white.v);
+
+ ctx.clearRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
+
+ // Drawing hues
+ var hGrad = ctx.createLinearGradient(0, 0, miniPickerCanvas.width, 0);
+
+ for (let i=0; i<7; i++) {
+ tmp = hsvToRgb(60 * i, 100, hsv.v * 100);
+ hGrad.addColorStop(i / 6, '#' + rgbToHex(Math.round(tmp[0]), Math.round(tmp[1]), Math.round(tmp[2])));
+ }
+ ctx.fillStyle = hGrad;
+ ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
+
+ // Drawing sat / lum
+ var vGrad = ctx.createLinearGradient(0, 0, 0, miniPickerCanvas.height);
+ vGrad.addColorStop(0, 'rgba(' + white[0] +',' + white[1] + ',' + white[2] + ',0)');
+ /*
+ vGrad.addColorStop(0.1, 'rgba(255,255,255,0)');
+ vGrad.addColorStop(0.9, 'rgba(255,255,255,1)');
+ */
+ vGrad.addColorStop(1, 'rgba(' + white[0] +',' + white[1] + ',' + white[2] + ',1)');
+
+ ctx.fillStyle = vGrad;
+ ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
+}
+
+function toggleDraggingCursor() {
+ draggingCursor = !draggingCursor;
+}
+
+function changePickingMode(event, newMode) {
+ let nIcons = pickerIcons.length;
+ let canvasContainer = document.getElementById("cp-canvas-container");
+ // Number of hex containers to add
+ let nHexContainers;
+
+ // Remove selected class from previous mode
+ document.getElementById("cp-colour-picking-modes").getElementsByClassName("cp-selected-mode")[0].classList.remove("cp-selected-mode");
+ // Updating mode
+ currentPickingMode = newMode;
+ // Adding selected class to new mode
+ event.target.classList.add("cp-selected-mode");
+
+ for (let i=1; i 110) {
+ return '#332f35'
+ }
+ else {
+ return '#c2bbc7';
+ }
+
+ //take in a color and return its brightness
+ function colorBrightness (color) {
+ var r = parseInt(color.slice(1, 3), 16);
+ var g = parseInt(color.slice(3, 5), 16);
+ var b = parseInt(color.slice(5, 7), 16);
+ return Math.round(((parseInt(r) * 299) + (parseInt(g) * 587) + (parseInt(b) * 114)) / 1000);
+ }
+}
\ No newline at end of file
diff --git a/js/_copyPaste.js b/js/_copyPaste.js
index 90e2a70..e97215f 100644
--- a/js/_copyPaste.js
+++ b/js/_copyPaste.js
@@ -1,11 +1,17 @@
+// Data saved when copying or cutting
let clipboardData;
+// Tells if the user is pasting something or not
let isPasting = false;
+// Coordinates of the copied (or cut) selection
let copiedStartX;
let copiedStartY;
let copiedEndX;
let copiedEndY;
+/** Copies the current selection to the clipboard
+ *
+ */
function copySelection() {
copiedEndX = endX;
copiedEndY = endY;
@@ -16,13 +22,19 @@ function copySelection() {
clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
}
+/** Pastes the clipboard data onto the current layer
+ *
+ */
function pasteSelection() {
// Can't paste if the layer is locked
if (currentLayer.isLocked) {
return;
}
+
+ // Cancel the current selection
endSelection();
+ // I'm pasting
isPasting = true;
// Putting the image data on the tmp layer
TMPLayer.context.putImageData(clipboardData, copiedStartX, copiedStartY);
@@ -43,9 +55,11 @@ function pasteSelection() {
//drawRect(copiedStartX, copiedEndX, copiedStartY, copiedEndY);
}
+/** Cuts the current selection and copies it to the clipboard
+ *
+ */
function cutSelectionTool() {
- console.log("Taglio");
-
+ // Saving the coordinates
copiedEndX = endX;
copiedEndY = endY;
@@ -53,13 +67,19 @@ function cutSelectionTool() {
copiedStartY = startY;
// Getting the selected pixels
+ // If I'm already moving a selection
if (imageDataToMove !== undefined) {
+ // I just save that selection in the clipboard
clipboardData = imageDataToMove;
+ // And clear the underlying space
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
+ // The image has been cleared, so I don't have anything to move anymore
imageDataToMove = undefined;
}
else {
- clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
+ // Otherwise, I copy the current selection into the clipboard
+ copySelection();
+ // And clear the selection
currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1);
}
}
\ No newline at end of file
diff --git a/js/_createButton.js b/js/_createButton.js
index 866edc4..23b807f 100644
--- a/js/_createButton.js
+++ b/js/_createButton.js
@@ -1,7 +1,51 @@
+function create(isSplash) {
+ var splashPostfix = '';
+ // If I'm creating from the splash menu, I append '-splash' so I get the corresponding values
+ if (isSplash) {
+ splashPostfix = '-splash';
+ }
+
+ var width = getValue('size-width' + splashPostfix);
+ var height = getValue('size-height' + splashPostfix);
+
+ // If I'm creating from the splash screen, I use the splashMode variable
+ var mode = isSplash ? splashMode : getValue('editor-mode');
+
+ newPixel(width, height, mode);
+
+ // If I'm not creating from the splash page, then this is not the first project I've created
+ if (!isSplash)
+ document.getElementById('new-pixel-warning').style.display = 'block';
+
+ //get selected palette name
+ var selectedPalette = getText('palette-button' + splashPostfix);
+ if (selectedPalette == 'Choose a palette...')
+ selectedPalette = 'none';
+
+ //track google event
+ ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
+
+
+ //reset new form
+ setValue('size-width', 64);
+ setValue('size-height', 64);
+ setValue("editor-mode", 'Advanced')
+
+ setText('editor-mode-button', 'Choose a mode...');
+ setText('palette-button', 'Choose a palette...');
+ setText('preset-button', 'Choose a preset...');
+}
+
+/** Triggered when the "Create" button in the new pixel dialogue is pressed
+ *
+ */
on('click', 'create-button', function (){
+ // Getting the values of the form
var width = getValue('size-width');
var height = getValue('size-height');
var mode = getValue("editor-mode");
+
+ // Creating a new pixel with those properties
newPixel(width, height, mode);
document.getElementById('new-pixel-warning').style.display = 'block';
@@ -23,3 +67,37 @@ on('click', 'create-button', function (){
setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...');
});
+
+/** Triggered when the "Create" button in the new pixel dialogue is pressed
+ *
+ */
+on('click', 'create-button-splash', function (){
+ // Getting the values of the form
+ var width = getValue('size-width-splash');
+ var height = getValue('size-height-splash');
+ var mode = pixelEditorMode;
+
+ if (mode == 'Advanced')
+ mode = "Basic";
+ else
+ mode = "Advanced";
+
+ // Creating a new pixel with those properties
+ newPixel(width, height, mode);
+
+ //track google event
+ ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
+ document.getElementById('new-pixel-warning').style.display = 'block';
+
+ // Resetting the new pixel values
+ selectedPalette = 'none';
+
+ //reset new pixel form
+ setValue('size-width-splash', 64);
+ setValue('size-height-splash', 64);
+ setValue("editor-mode", 'Advanced')
+
+ setText('editor-mode-button', 'Choose a mode...');
+ setText('palette-button', 'Choose a palette...');
+ setText('preset-button', 'Choose a preset...');
+});
diff --git a/js/_createColorPalette.js b/js/_createColorPalette.js
index 8f7352b..a8cf769 100644
--- a/js/_createColorPalette.js
+++ b/js/_createColorPalette.js
@@ -1,5 +1,11 @@
-function createColorPalette(paletteColors, fillBackground, deletePreviousPalette = true) {
+/** Creates the colour palette
+ *
+ * @param {*} paletteColors The colours of the palette
+ * @param {*} deletePreviousPalette Tells if the app should delete the previous palette or not
+ * (used when opening a file, for example)
+ */
+function createColorPalette(paletteColors, deletePreviousPalette = true) {
//remove current palette
if (deletePreviousPalette) {
colors = document.getElementsByClassName('color-button');
@@ -11,6 +17,7 @@ function createColorPalette(paletteColors, fillBackground, deletePreviousPalette
var lightestColor = '#000000';
var darkestColor = '#ffffff';
+ // Adding all the colours in the array
for (var i = 0; i < paletteColors.length; i++) {
var newColor = paletteColors[i];
var newColorElement = addColor(newColor);
@@ -42,6 +49,9 @@ function createColorPalette(paletteColors, fillBackground, deletePreviousPalette
currentLayer.context.fillStyle = darkestColor;
}
+/** Creates the palette with the colours used in all the layers
+ *
+ */
function createPaletteFromLayers() {
let colors = {};
@@ -68,8 +78,6 @@ function createPaletteFromLayers() {
}
}
- console.log(colors);
-
//create array out of colors object
let colorPaletteArray = [];
for (let color in colors) {
@@ -77,8 +85,7 @@ function createPaletteFromLayers() {
colorPaletteArray.push('#'+rgbToHex(colors[color]));
}
}
- console.log('COLOR PALETTE ARRAY', colorPaletteArray);
- //create palette form colors array
- createColorPalette(colorPaletteArray, false);
+ //create palette from colors array
+ createColorPalette(colorPaletteArray, true);
}
\ No newline at end of file
diff --git a/js/_deleteColor.js b/js/_deleteColor.js
index 1abb9dd..7656d30 100644
--- a/js/_deleteColor.js
+++ b/js/_deleteColor.js
@@ -9,19 +9,19 @@ function deleteColor (color) {
//if color is a string, then find the corresponding button
if (typeof color === 'string') {
- console.log('trying to find ',color);
+ //console.log('trying to find ',color);
//get all colors in palette
colors = document.getElementsByClassName('color-button');
//loop through colors
for (var i = 0; i < colors.length; i++) {
- console.log(color,'=',colors[i].jscolor.toString());
+ //console.log(color,'=',colors[i].jscolor.toString());
if (color == colors[i].jscolor.toString()) {
- console.log('match');
+ //console.log('match');
//set color to the color button
color = colors[i];
- console.log('found color', color);
+ //console.log('found color', color);
//exit loop
break;
@@ -30,7 +30,7 @@ function deleteColor (color) {
//if the color wasn't found
if (typeof color === 'string') {
- console.log('color not found');
+ //console.log('color not found');
//exit function
return;
}
diff --git a/js/_dialogue.js b/js/_dialogue.js
index ba3a741..8157040 100644
--- a/js/_dialogue.js
+++ b/js/_dialogue.js
@@ -1,27 +1,55 @@
+let currentOpenDialogue = "";
+
+/** Shows the dialogue window called dialogueName, which is a child of pop-up-container in pixel-editor.hbs
+ *
+ * @param {*} dialogueName The name of the window to show
+ * @param {*} trackEvent Should I track the GA event?
+ */
function showDialogue (dialogueName, trackEvent) {
if (typeof trackEvent === 'undefined') trackEvent = true;
+ // Updating currently open dialogue
+ currentOpenDialogue = dialogueName;
+ // The pop up window is open
dialogueOpen = true;
+ // Showing the pop up container
popUpContainer.style.display = 'block';
+ // Showing the window
document.getElementById(dialogueName).style.display = 'block';
+ // If I'm opening the palette window, I initialize the colour picker
+ if (dialogueName == 'palette-block' && documentCreated) {
+ cpInit();
+ pbInit();
+ }
+
//track google event
if (trackEvent)
ga('send', 'event', 'Palette Editor Dialogue', dialogueName); /*global ga*/
}
+/** Closes the current dialogue by hiding the window and the pop-up-container
+ *
+ */
function closeDialogue () {
popUpContainer.style.display = 'none';
-
var popups = popUpContainer.children;
+
for (var i = 0; i < popups.length; i++) {
popups[i].style.display = 'none';
}
dialogueOpen = false;
+
+ if (currentOpenDialogue == "palette-block") {
+ pbAddToSimplePalette();
+ }
}
+/** Closes a dialogue window if the user clicks everywhere but in the current window
+ *
+ */
popUpContainer.addEventListener('click', function (e) {
if (e.target == popUpContainer)
closeDialogue();
@@ -31,6 +59,6 @@ popUpContainer.addEventListener('click', function (e) {
var cancelButtons = popUpContainer.getElementsByClassName('close-button');
for (var i = 0; i < cancelButtons.length; i++) {
cancelButtons[i].addEventListener('click', function () {
- closeDialogue();
+ closeDialogue();
});
}
diff --git a/js/_drawLine.js b/js/_drawLine.js
index e7ce712..ab2f392 100644
--- a/js/_drawLine.js
+++ b/js/_drawLine.js
@@ -1,4 +1,4 @@
-//draw a line between two points on canvas
+//draws a line between two points on canvas
function line(x0,y0,x1,y1, brushSize) {
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
@@ -9,7 +9,7 @@ function line(x0,y0,x1,y1, brushSize) {
while (true) {
//set pixel
// If the current tool is the brush
- if (currentTool.name == 'pencil' || currentTool.name == 'rectangle') {
+ if (currentTool.name == 'pencil' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse') {
// I fill the rect
currentLayer.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else if (currentTool.name == 'eraser') {
diff --git a/js/_editorMode.js b/js/_editorMode.js
index e89626c..ccb8dcf 100644
--- a/js/_editorMode.js
+++ b/js/_editorMode.js
@@ -3,27 +3,46 @@ let modes = {
description: 'Basic mode is perfect if you want to create simple sprites or try out palettes.'
},
'Advanced' : {
- description: 'Choose advanced mode to gain access to features such as layers.'
+ description: 'Choose advanced mode to gain access to more advanced features such as layers.'
}
}
let infoBox = document.getElementById('editor-mode-info');
+let currentSplashButton = document.getElementById("sp-mode-palette").children[0].children[1];
-function switchMode(currentMode, mustConfirm = true) {
+function splashMode(mouseEvent, mode) {
+ if (currentSplashButton == undefined) {
+ currentSplashButton = mouseEvent.target.parentElement;
+ }
+
+ if (mode !== pixelEditorMode) {
+ // Remove selected class to old button
+ currentSplashButton.classList.remove("sp-interface-selected");
+ // Add selected class to new button
+ mouseEvent.target.parentElement.classList.add("sp-interface-selected");
+
+ // Setting the new mode
+ pixelEditorMode = mode;
+ }
+
+ // Setting the new selected button
+ currentSplashButton = mouseEvent.target.parentElement;
+}
+
+function switchMode(mustConfirm = true) {
//switch to advanced mode
- if (currentMode == 'Basic') {
+ if (pixelEditorMode == 'Basic') {
// Switch to advanced ez pez lemon squez
document.getElementById('switch-mode-button').innerHTML = 'Switch to basic mode';
// Show the layer menus
layerList.style.display = "inline-block";
document.getElementById('layer-button').style.display = 'inline-block';
- // Move the palette menu
- document.getElementById('colors-menu').style.right = '200px';
+ // Hide the palette menu
+ document.getElementById('colors-menu').style.right = '200px'
pixelEditorMode = 'Advanced';
}
-
//switch to basic mode
else {
//if there is a current layer (a document is active)
@@ -47,15 +66,17 @@ function switchMode(currentMode, mustConfirm = true) {
// Hide the layer menus
layerList.style.display = 'none';
document.getElementById('layer-button').style.display = 'none';
- // Move the palette menu
- document.getElementById('colors-menu').style.right = '0px';
+ // Show the palette menu
+ document.getElementById('colors-menu').style.display = 'flex';
+ // Move the palette menu
+ document.getElementById('colors-menu').style.right = '0px';
pixelEditorMode = 'Basic';
}
}
on('click', 'switch-mode-button', function (e) {
- switchMode(pixelEditorMode);
+ switchMode();
});
// Makes the menu open
diff --git a/js/_ellipse.js b/js/_ellipse.js
new file mode 100644
index 0000000..11a6ae9
--- /dev/null
+++ b/js/_ellipse.js
@@ -0,0 +1,146 @@
+// Saving the empty rect svg
+var emptyEllipseSVG = document.getElementById("ellipse-empty-button-svg");
+// and the full rect svg so that I can change them when the user changes rect modes
+var fullEllipseSVG = document.getElementById("ellipse-full-button-svg");
+
+// The start mode is empty ellipse
+var ellipseDrawMode = 'empty';
+// I'm not drawing a ellipse at the beginning
+var isDrawingEllipse = false;
+
+// Ellipse coordinates
+let startEllipseX;
+let startEllipseY;
+let endEllipseX;
+let endEllipseY;
+
+// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
+/** Starts drawing the ellipse, saves the start coordinates
+ *
+ * @param {*} mouseEvent
+ */
+function startEllipseDrawing(mouseEvent) {
+ // Putting the vfx layer on top of everything
+ VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
+ // Updating flag
+ isDrawingEllipse = true;
+
+ // Saving the start coords of the ellipse
+ let cursorPos = getCursorPosition(mouseEvent);
+ startEllipseX = Math.floor(cursorPos[0] / zoom) + 0.5;
+ startEllipseY = Math.floor(cursorPos[1] / zoom) + 0.5;
+
+ drawEllipse(startEllipseX, startEllipseY);
+}
+
+// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
+/** Updates the ellipse preview depending on the position of the mouse
+ *
+ * @param {*} mouseEvent The mouseEvent from which we'll get the mouse position
+ */
+function updateEllipseDrawing(mouseEvent) {
+ let pos = getCursorPosition(mouseEvent);
+
+ // Drawing the ellipse at the right position
+ drawEllipse(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
+}
+
+// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
+/** Finishes drawing the ellipse, decides the end coordinates and moves the preview ellipse to the
+ * current layer
+ *
+ * @param {*} mouseEvent event from which we'll get the mouse position
+ */
+function endEllipseDrawing(mouseEvent) {
+ // Getting the end position
+ let currentPos = getCursorPosition(mouseEvent);
+ let vfxContext = VFXCanvas.getContext("2d");
+
+ endEllipseX = Math.round(currentPos[0] / zoom) + 0.5;
+ endEllipseY = Math.round(currentPos[1] / zoom) + 0.5;
+
+ // Inverting end and start (start must always be the top left corner)
+ if (endEllipseX < startEllipseX) {
+ let tmp = endEllipseX;
+ endEllipseX = startEllipseX;
+ startEllipseX = tmp;
+ }
+ // Same for the y
+ if (endEllipseY < startEllipseY) {
+ let tmp = endEllipseY;
+ endEllipseY = startEllipseY;
+ startEllipseY = tmp;
+ }
+
+ // Resetting this
+ isDrawingEllipse = false;
+ // Drawing the ellipse
+ startEllipseY -= 0.5;
+ endEllipseY -= 0.5;
+ endEllipseX -= 0.5;
+ startEllipseX -= 0.5;
+
+ // Setting the correct linewidth and colour
+ currentLayer.context.lineWidth = tool.ellipse.brushSize;
+ currentLayer.context.fillStyle = currentGlobalColor;
+
+ // Drawing the ellipse using 4 lines
+ line(startEllipseX, startEllipseY, endEllipseX, startEllipseY, tool.ellipse.brushSize);
+ line(endEllipseX, startEllipseY, endEllipseX, endEllipseY, tool.ellipse.brushSize);
+ line(endEllipseX, endEllipseY, startEllipseX, endEllipseY, tool.ellipse.brushSize);
+ line(startEllipseX, endEllipseY, startEllipseX, startEllipseY, tool.ellipse.brushSize);
+
+ // If I have to fill it, I do so
+ if (ellipseDrawMode == 'fill') {
+ currentLayer.context.fillRect(startEllipseX, startEllipseY, endEllipseX - startEllipseX, endEllipseY - startEllipseY);
+ }
+
+ // Clearing the vfx canvas
+ vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
+}
+
+// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
+/** Draws a ellipse with end coordinates given by x and y on the VFX layer (draws
+ * the preview for the ellipse tool)
+ *
+ * @param {*} x The current end x of the ellipse
+ * @param {*} y The current end y of the ellipse
+ */
+function drawEllipse(x, y) {
+ // Getting the vfx context
+ let vfxContext = VFXCanvas.getContext("2d");
+
+ // Clearing the vfx canvas
+ vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
+
+ // Drawing the ellipse
+ vfxContext.lineWidth = tool.ellipse.brushSize;
+ vfxContext.strokeStyle = currentGlobalColor;
+
+ // Drawing the ellipse
+ vfxContext.beginPath();
+ if ((tool.ellipse.brushSize % 2 ) == 0) {
+ vfxContext.rect(startEllipseX - 0.5, startEllipseY - 0.5, x - startEllipseX, y - startEllipseY);
+ }
+ else {
+ vfxContext.rect(startEllipseX, startEllipseY, x - startEllipseX, y - startEllipseY);
+ }
+
+ vfxContext.setLineDash([]);
+ vfxContext.stroke();
+}
+
+/** Sets the correct tool icon depending on its mode
+ *
+ */
+function setEllipseToolSvg() {
+ console.log("set eilipse svg");
+ if (ellipseDrawMode == 'empty') {
+ emptyEllipseSVG.setAttribute('display', 'visible');
+ fullEllipseSVG.setAttribute('display', 'none');
+ }
+ else {
+ emptyEllipseSVG.setAttribute('display', 'none');
+ fullEllipseSVG.setAttribute('display', 'visible');
+ }
+}
diff --git a/js/_featureToggles.js b/js/_featureToggles.js
new file mode 100644
index 0000000..5b77f57
--- /dev/null
+++ b/js/_featureToggles.js
@@ -0,0 +1,33 @@
+const featureToggles = (function featureTogglesModule() {
+
+ const ellipseToolLocalStorageKey = 'feature_ellipseTool';
+
+ return {
+ onLoad: () => {
+ updateEllipseToolVisibility()
+ },
+ enableEllipseTool,
+ disableEllipseTool
+ }
+
+ ////////
+
+ function updateEllipseToolVisibility() {
+ // TODO: [ELLIPSE] Once ellipse is ready for release make it enabled by default
+ const isEllipseToolEnabled = (window.localStorage.getItem(ellipseToolLocalStorageKey) === "yes") || false;
+ const ellipseToolElement = document.getElementById("tools-menu--ellipse");
+ ellipseToolElement.style.display = isEllipseToolEnabled ? 'block' : 'none';
+ }
+
+ function enableEllipseTool() {
+ window.localStorage.setItem(ellipseToolLocalStorageKey, "yes");
+ updateEllipseToolVisibility();
+ }
+
+ function disableEllipseTool() {
+ window.localStorage.setItem(ellipseToolLocalStorageKey, "no");
+ updateEllipseToolVisibility();
+ }
+
+})();
+
diff --git a/js/_featuresLog.js b/js/_featuresLog.js
new file mode 100644
index 0000000..5fd6b89
--- /dev/null
+++ b/js/_featuresLog.js
@@ -0,0 +1 @@
+showDialogue("splash", false);
\ No newline at end of file
diff --git a/js/_fileMenu.js b/js/_fileMenu.js
index f680dcf..16c6b1c 100644
--- a/js/_fileMenu.js
+++ b/js/_fileMenu.js
@@ -7,19 +7,14 @@ for (var i = 1; i < mainMenuItems.length; i++) {
var menuItem = mainMenuItems[i];
var menuButton = menuItem.children[0];
- console.log(mainMenuItems);
-
//when you click a main menu items button
on('click', menuButton, function (e, button) {
- console.log('parent ', button.parentElement);
select(button.parentElement);
});
var subMenu = menuItem.children[1];
var subMenuItems = subMenu.children;
-
-
//when you click an item within a menu button
for (var j = 0; j < subMenuItems.length; j++) {
diff --git a/js/_fill.js b/js/_fill.js
index aa8a7d9..266c364 100644
--- a/js/_fill.js
+++ b/js/_fill.js
@@ -7,11 +7,6 @@ function fill(cursorLocation) {
tempImage.data[pixelPos + 1] = fillColor.g;
tempImage.data[pixelPos + 2] = fillColor.b;
tempImage.data[pixelPos + 3] = 255;
- /*
- tempImage.data[pixelPos] = fillColor.r;
- tempImage.data[pixelPos + 1] = fillColor.g;
- tempImage.data[pixelPos + 2] = fillColor.b;
- */
}
//change x y to color value passed from the function and use that as the original color
@@ -21,8 +16,9 @@ function fill(cursorLocation) {
var r = tempImage.data[pixelPos];
var g = tempImage.data[pixelPos + 1];
var b = tempImage.data[pixelPos + 2];
+ var a = tempImage.data[pixelPos + 3];
//console.log(r == color[0] && g == color[1] && b == color[2]);
- return (r == color[0] && g == color[1] && b == color[2]);
+ return (r == color[0] && g == color[1] && b == color[2] && a == color[3]);
}
//save history state
@@ -41,16 +37,21 @@ function fill(cursorLocation) {
var startingPosition = (topmostPixelsArray[0][1] * canvasSize[0] + topmostPixelsArray[0][0]) * 4;
//the color of the cluster that is being filled
- var clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2]];
+ var clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2], tempImage.data[startingPosition+3]];
//the new color to fill with
var fillColor = hexToRgb(currentLayer.context.fillStyle);
+ console.log("here");
//if you try to fill with the same color that's already there, exit the function
if (clusterColor[0] == fillColor.r &&
clusterColor[1] == fillColor.g &&
- clusterColor[2] == fillColor.b )
- return;
+ clusterColor[2] == fillColor.b &&
+ clusterColor[3] != 0) {
+ console.log("Returned");
+ return;
+ }
+
//loop until there are no more values left in this array
while (topmostPixelsArray.length) {
diff --git a/js/_getCursorPosition.js b/js/_getCursorPosition.js
index 4c9cdca..6aaed4e 100644
--- a/js/_getCursorPosition.js
+++ b/js/_getCursorPosition.js
@@ -1,4 +1,4 @@
-//get cursor position relative to canvas
+//gets cursor position relative to canvas
function getCursorPosition(e) {
var x;
var y;
diff --git a/js/_history.js b/js/_history.js
index 656d05d..ffc5fb6 100644
--- a/js/_history.js
+++ b/js/_history.js
@@ -1,3 +1,17 @@
+/** How the history works
+ * - undoStates stores the states that can be undone
+ * - redoStates stores the states that can be redone
+ * - undo() undoes an action and adds it to the redoStates
+ * - redo() redoes an action and adds it to the undoStates
+ * - Each HistoryState must implement an undo() and redo() function
+ * Those functions actually implement the undo and redo mechanism for that action,
+ * so you'll need to save the data you need as attributes in the constructor. For example,
+ * for the HistoryStateAddColour, the added colour is saved so that it can be removed in
+ * undo() or added back in redo().
+ * - Each HistoryState must call saveHistoryState(this) so that it gets added to the stack
+ *
+ */
+
var undoStates = [];
var redoStates = [];
@@ -28,6 +42,8 @@ function HistoryStateResizeSprite(xRatio, yRatio, algo, oldData) {
};
this.redo = function() {
+ console.log("REDOOOO");
+ console.log("Ratio: " + this.xRatio + "," + this.yRatio);
currentAlgo = algo;
resizeSprite(null, [this.xRatio, this.yRatio]);
undoStates.push(this);
@@ -44,7 +60,6 @@ function HistoryStateResizeCanvas(newSize, oldSize, imageDatas, trim) {
this.undo = function() {
let dataIndex = 0;
- console.log("breakpoint");
// Resizing the canvas
resizeCanvas(null, oldSize, null, false);
// Putting the image datas
@@ -59,7 +74,6 @@ function HistoryStateResizeCanvas(newSize, oldSize, imageDatas, trim) {
};
this.redo = function() {
- console.log("trim: " + this.trim);
if (!this.trim) {
resizeCanvas(null, newSize, null, false);
}
@@ -101,7 +115,6 @@ function HistoryStateFlattenTwoVisibles(belowImageData, afterAbove, layerIndex,
this.belowImageData = belowImageData;
this.undo = function() {
- console.log(afterAbove.menuEntry);
canvasView.append(aboveLayer.canvas);
layerList.insertBefore(aboveLayer.menuEntry, afterAbove);
@@ -300,7 +313,6 @@ function HistoryStateAddLayer(layerData, index) {
this.index = index;
this.undo = function() {
- console.log("uo");
redoStates.push(this);
if (layers.length - nAppLayers > this.index + 1) {
@@ -473,8 +485,6 @@ function saveHistoryState (state) {
}
function undo () {
- //console.log('%cundo', undoLogStyle);
-
//if there are any states saved to undo
if (undoStates.length > 0) {
document.getElementById('redo-button').classList.remove('disabled');
@@ -496,8 +506,6 @@ function undo () {
}
function redo () {
- //console.log('%credo', undoLogStyle);
-
if (redoStates.length > 0) {
//enable undo button
diff --git a/js/_hotkeyListener.js b/js/_hotkeyListener.js
index 73ea4bc..ecb74b8 100644
--- a/js/_hotkeyListener.js
+++ b/js/_hotkeyListener.js
@@ -1,5 +1,9 @@
var spacePressed = false;
+/** Just listens to hotkeys and calls the linked functions
+ *
+ * @param {*} e
+ */
function KeyPress(e) {
var keyboardEvent = window.event? event : e;
@@ -46,6 +50,9 @@ function KeyPress(e) {
case 52: case 80:
tool.pan.switchTo();
break;
+ case 76:
+ tool.line.switchTo();
+ break;
//zoom - 5
case 53:
tool.zoom.switchTo();
@@ -58,6 +65,15 @@ function KeyPress(e) {
case 77: case 109:
tool.rectselect.switchTo()
break;
+ // TODO: [ELLIPSE] Decide on a shortcut to use. "s" was chosen without any in-team consultation.
+ // ellipse tool, s
+ case 83:
+ tool.ellipse.switchTo()
+ break;
+ // rectangle tool, u
+ case 85:
+ tool.rectangle.switchTo()
+ break;
// Paste tool
case 86: case 118:
if (keyboardEvent.ctrlKey && !dragging) {
diff --git a/js/_initColor.js b/js/_initColor.js
index 955abf4..c1bcf60 100644
--- a/js/_initColor.js
+++ b/js/_initColor.js
@@ -1,5 +1,7 @@
+// NEXTPULL: to remove when the new palette system is added
-//format a color button
+
+//formats a color button
function initColor (colorElement) {
//console.log('initColor()');
//console.log(document.getElementById('jscolor-hex-input'))
diff --git a/js/_jscolor.js b/js/_jscolor.js
index e6c8c3b..2cbf910 100644
--- a/js/_jscolor.js
+++ b/js/_jscolor.js
@@ -1,3 +1,5 @@
+// NEXTPULL: to remove when the new palette system is added
+
/**
* jscolor - JavaScript Color Picker
*
diff --git a/js/_layer.js b/js/_layer.js
index 3500c43..d18a8e8 100644
--- a/js/_layer.js
+++ b/js/_layer.js
@@ -1,31 +1,29 @@
-/** TODO LIST FOR LAYERS
-
- GENERAL REQUIREMENTS:
- - Saving the state of an artwork to a .lospec file so that people can work on it later keeping
- the layers they created? That'd be cool, even for the app users, that could just double click on a lospec
- file for it to be opened right in the pixel editor
-
- OPTIONAL:
-
- 1 - Fix issues
-*/
-
+// HTML element that contains the layer entries
let layerList;
+// A single layer entry (used as a prototype to create the new ones)
let layerListEntry;
-
+// NEXTPULL: remove the drag n drop system and use Sortable.js instead
let layerDragSource = null;
+// Number of layers at the beginning
let layerCount = 1;
+// Current max z index (so that I know which z-index to assign to new layers)
let maxZIndex = 3;
+// When a layer is deleted, its id is added to this array and can be reused
let unusedIDs = [];
+// Id for the next added layer
let currentID = layerCount;
-let idToDelete;
+// Layer menu
let layerOptions = document.getElementById("layer-properties-menu");
-
+// Is the user currently renaming a layer?
let isRenamingLayer = false;
+// I need to save this, trust me
let oldLayerName = null;
+let dragStartLayer;
+
+// Binding the add layer button to the function
on('click',"add-layer-button", addLayer, false);
/** Handler class for a single canvas (a single layer)
@@ -54,6 +52,7 @@ class Layer {
this.id = "layer" + id;
+ // Binding the events
if (menuEntry != null) {
this.name = menuEntry.getElementsByTagName("p")[0].innerHTML;
menuEntry.id = "layer" + id;
@@ -78,20 +77,13 @@ class Layer {
// Initializes the canvas
initialize() {
- /*
- var maxHorizontalZoom = Math.floor(window.innerWidth/this.canvasSize[0]*0.75);
- var maxVerticalZoom = Math.floor(window.innerHeight/this.canvasSize[1]*0.75);
-
- zoom = Math.min(maxHorizontalZoom,maxVerticalZoom);
- if (zoom < 1) zoom = 1;
- */
//resize canvas
this.canvas.width = this.canvasSize[0];
this.canvas.height = this.canvasSize[1];
this.canvas.style.width = (this.canvas.width*zoom)+'px';
this.canvas.style.height = (this.canvas.height*zoom)+'px';
- //unhide canvas
+ //show canvas
this.canvas.style.display = 'block';
//center canvas in window
@@ -103,7 +95,7 @@ class Layer {
}
hover() {
- // Hide all the layers but the current one
+ // Hides all the layers but the current one
for (let i=1; i=0; i--) {
getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
}
- maxZIndex++;
+ maxZIndex+=2;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
canvasView.append(newCanvas);
- newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 1;
+ newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 2;
newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document');
@@ -545,78 +486,6 @@ function renameLayer(event) {
isRenamingLayer = true;
}
-// Swaps two layer entries in the layer menu
-function moveLayers(toDropLayer, staticLayer, saveHistory = true) {
- let toDrop = getLayerByID(toDropLayer);
- let staticc = getLayerByID(staticLayer);
- let layerCopy = layers.slice();
-
- let beforeToDrop = toDrop.menuEntry.nextElementSibling;
- let nMoved = 0;
-
- layerCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
-
- let toDropIndex = layerCopy.indexOf(toDrop);
- let staticIndex = layerCopy.indexOf(staticc);
-
- layerList.insertBefore(toDrop.menuEntry, staticc.menuEntry);
-
- if (toDropIndex < staticIndex) {
- let tmp = toDrop.canvas.style.zIndex;
- let tmp2;
-
- for (let i=toDropIndex+1; i<=staticIndex; i++) {
- tmp2 = layerCopy[i].canvas.style.zIndex;
-
- if (saveHistory) {
- new HistoryStateMoveTwoLayers(layerCopy[i], tmp2, tmp);
- }
-
- layerCopy[i].canvas.style.zIndex = tmp;
- tmp = tmp2;
- nMoved++;
- }
-
- if (saveHistory) {
- new HistoryStateMoveTwoLayers(toDrop, toDrop.canvas.style.zIndex, tmp);
- }
-
- toDrop.canvas.style.zIndex = tmp;
-
- if (saveHistory) {
- new HistoryStateMoveLayer(beforeToDrop, toDrop, staticc, nMoved);
- }
- }
- else {
- // BUG QUI
- let tmp = toDrop.canvas.style.zIndex;
- let tmp2;
-
- for (let i=toDropIndex-1; i>staticIndex; i--) {
- tmp2 = layerCopy[i].canvas.style.zIndex;
-
- if (saveHistory) {
- new HistoryStateMoveTwoLayers(layerCopy[i], tmp2, tmp);
- }
-
- layerCopy[i].canvas.style.zIndex = tmp;
- tmp = tmp2;
- nMoved++;
- }
-
- if (saveHistory) {
- new HistoryStateMoveTwoLayers(toDrop, toDrop.canvas.style.zIndex, tmp);
- }
-
- toDrop.canvas.style.zIndex = tmp;
-
- if (saveHistory) {
- new HistoryStateMoveLayer(beforeToDrop, toDrop, staticc, nMoved);
- }
-
- }
-}
-
function getMenuEntryIndex(list, entry) {
for (let i=0; i newIndex)
+ {
+ for (let i=newIndex; ioldIndex; i--) {
+ getLayerByID(layerList.children[i].id).canvas.style.zIndex = getLayerByID(layerList.children[i - 1].id).canvas.style.zIndex;
+ }
+ }
+
+ getLayerByID(layerList.children[oldIndex].id).canvas.style.zIndex = movedZIndex;
+
+ dragging = false;
+}
+
+layerList = document.getElementById("layers-menu");
+
+// Making the layers list sortable
+new Sortable(document.getElementById("layers-menu"), {
+ animation: 100,
+ filter: ".layer-button",
+ draggable: ".layers-menu-entry",
+ onStart: layerDragStart,
+ onEnd: layerDragDrop
+});
\ No newline at end of file
diff --git a/js/_line.js b/js/_line.js
new file mode 100644
index 0000000..aa9f828
--- /dev/null
+++ b/js/_line.js
@@ -0,0 +1,45 @@
+function diagLine(lastMouseClickPos, zoom, cursorLocation) {
+
+ let x0 = Math.floor(lastMouseClickPos[0]/zoom);
+ let y0 = Math.floor(lastMouseClickPos[1]/zoom);
+ let x1 = Math.floor(cursorLocation[0]/zoom);
+ let y1 = Math.floor(cursorLocation[1]/zoom);
+
+ let dx = Math.abs(x1-x0);
+ let dy = Math.abs(y1-y0);
+ let sx = (x0 < x1 ? 1 : -1);
+ let sy = (y0 < y1 ? 1 : -1);
+ let err = dx-dy;
+
+ const brushSize = tool.line.brushSize;
+
+ const canvas = document.getElementById('tmp-canvas');
+ const context = canvas.getContext('2d');
+
+ context.fillStyle=currentGlobalColor;
+ context.clearRect(0, 0, canvas.width, canvas.height);
+
+ canvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;
+
+ //console.log(canvas.style.zIndex, currentLayer.canvas.style.zIndex);
+
+ while (true) {
+ if (currentTool.name !== 'line') return;
+
+ context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
+
+ //if we've reached the end goal, exit the loop
+ if ((x0==x1) && (y0==y1)) break;
+ var e2 = 2*err;
+
+ if (e2 >-dy) {
+ err -=dy;
+ x0+=sx;
+ }
+
+ if (e2 < dx) {
+ err +=dx;
+ y0+=sy;
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/_loadImage.js b/js/_loadImage.js
index fccdd81..a9897b6 100644
--- a/js/_loadImage.js
+++ b/js/_loadImage.js
@@ -1,18 +1,27 @@
+/** Loads a file (.png or .lpe)
+ *
+ */
document.getElementById('open-image-browse-holder').addEventListener('change', function () {
let fileName = document.getElementById("open-image-browse-holder").value;
+ // Getting the extension
let extension = fileName.substring(fileName.lastIndexOf('.')+1, fileName.length) || fileName;
+ // I didn't write this check and I have no idea what it does
if (this.files && this.files[0]) {
+ // Btw, checking if the extension is supported
if (extension == 'png' || extension == 'gif' || extension == 'lpe') {
+ // If it's a Lospec Pixel Editor tm file, I load the project
if (extension == 'lpe') {
let file = this.files[0];
let reader = new FileReader();
+ // Getting all the data
reader.readAsText(file, "UTF-8");
+ // Converting the data to a json object and creating a new pixel (see _newPixel.js for more)
reader.onload = function (e) {
let dictionary = JSON.parse(e.target.result);
-
- newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary['editorMode'], dictionary);
+ let mode = dictionary['editorMode'] == 'Advanced' ? 'Basic' : 'Advanced';
+ newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], mode, dictionary);
}
}
else {
diff --git a/js/_loadPalette.js b/js/_loadPalette.js
index a12d2b5..035bd2a 100644
--- a/js/_loadPalette.js
+++ b/js/_loadPalette.js
@@ -1,8 +1,8 @@
//this is called when a user picks a file after selecting "load palette" from the new pixel dialogue
+// TODO: load palette from .lpe file
document.getElementById('load-palette-browse-holder').addEventListener('change', function () {
if (this.files && this.files[0]) {
-
//make sure file is allowed filetype
var fileContentType = this.files[0].type;
if (fileContentType == 'image/png' || fileContentType == 'image/gif') {
@@ -26,8 +26,6 @@ document.getElementById('load-palette-browse-holder').addEventListener('change',
var colorPalette = [];
var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data;
- console.log(imagePixelData);
-
//loop through pixels looking for colors to add to palette
for (var i = 0; i < imagePixelData.length; i += 4) {
var color = '#'+rgbToHex(imagePixelData[i],imagePixelData[i + 1],imagePixelData[i + 2]);
@@ -40,6 +38,8 @@ document.getElementById('load-palette-browse-holder').addEventListener('change',
palettes['Loaded palette'] = {};
palettes['Loaded palette'].colors = colorPalette;
setText('palette-button', 'Loaded palette');
+ setText('palette-button-splash', 'Loaded palette');
+ toggle('palette-menu-splash');
};
img.src = e.target.result;
};
diff --git a/js/_logs.js b/js/_logs.js
new file mode 100644
index 0000000..30ee743
--- /dev/null
+++ b/js/_logs.js
@@ -0,0 +1,15 @@
+var rawFile = new XMLHttpRequest();
+rawFile.open("GET", '/pixel-editor/latestLog.html', false);
+rawFile.onreadystatechange = function ()
+{
+ if(rawFile.readyState === 4)
+ {
+ if(rawFile.status === 200 || rawFile.status == 0)
+ {
+ var allText = rawFile.responseText;
+
+ document.getElementById("latest-update").innerHTML = allText;
+ }
+ }
+}
+rawFile.send(null);
\ No newline at end of file
diff --git a/js/_mouseEvents.js b/js/_mouseEvents.js
index 124b18a..82c5853 100644
--- a/js/_mouseEvents.js
+++ b/js/_mouseEvents.js
@@ -22,7 +22,7 @@ window.addEventListener("mousedown", function (mouseEvent) {
else if (mouseEvent.altKey)
currentTool = tool.eyedropper;
else if (mouseEvent.target.className == 'drawingCanvas' &&
- (currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle'))
+ (currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse' || currentTool.name === 'line'))
new HistoryStateEditCanvas();
else if (currentTool.name == 'moveselection') {
if (!cursorInSelectedArea() &&
@@ -30,14 +30,15 @@ window.addEventListener("mousedown", function (mouseEvent) {
tool.pencil.switchTo();
canDraw = false;
}
- }
-
- currentTool.updateCursor();
+ }
if (!currentLayer.isLocked && !currentLayer.isVisible && canDraw) {
draw(mouseEvent);
}
}
+ else if (mouseEvent.which == 2) {
+ currentTool = tool.pan;
+ }
else if (currentTool.name == 'pencil' && mouseEvent.which == 3) {
currentTool = tool.resizebrush;
tool.pencil.previousBrushSize = tool.pencil.brushSize;
@@ -46,10 +47,15 @@ window.addEventListener("mousedown", function (mouseEvent) {
currentTool = tool.resizeeraser;
tool.eraser.previousBrushSize = tool.eraser.brushSize;
}
+ // TODO: [ELLIPSE] Do we need similar logic related to ellipse?
else if (currentTool.name == 'rectangle' && mouseEvent.which == 3) {
currentTool = tool.resizerectangle;
tool.rectangle.previousBrushSize = tool.rectangle.brushSize;
}
+ else if (currentTool.name == 'line' && mouseEvent.which == 3) {
+ currentTool = tool.resizeline;
+ tool.line.previousBrushSize = tool.line.brushSize;
+ }
if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas')
eyedropperPreview.style.display = 'block';
@@ -70,6 +76,15 @@ window.addEventListener("mouseup", function (mouseEvent) {
currentLayer.closeOptionsMenu();
}
+ // If the user finished placing down a line, clear the tmp canvas and copy the data to the current layer
+ if (currentTool.name === "line") {
+ const tmpCanvas = document.getElementById('tmp-canvas');
+ currentLayer.context.drawImage(tmpCanvas, 0, 0);
+
+ const tmpContext = tmpCanvas.getContext('2d');
+ tmpContext.clearRect(0, 0, tmpCanvas.width, tmpCanvas.height);
+ }
+
if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas') {
@@ -134,7 +149,7 @@ window.addEventListener("mouseup", function (mouseEvent) {
mode = 'out';
}
- changeZoom(layers[0], mode, getCursorPosition(mouseEvent));
+ changeZoom(mode, getCursorPosition(mouseEvent));
for (let i=1; i endIndex) {
+ let tmp = startIndex;
+ startIndex = endIndex;
+ endIndex = tmp;
+ }
+
+ for (let i=startIndex; i<=endIndex; i++) {
+ coloursList.removeChild(coloursList.children[startIndex]);
+ }
+ clearBorders();
+}
+
+/** Starts selecting a ramp. Saves the data needed to draw the outline.
+ *
+ * @param {*} mouseEvent
+ */
+function startRampSelection(mouseEvent) {
+ if (mouseEvent.which == 3) {
+ let index = getElementIndex(mouseEvent.target);
+
+ isRampSelecting = true;
+
+ currentSelection.startIndex = index;
+ currentSelection.endIndex = index;
+
+ currentSelection.startCoords = getColourCoordinates(index);
+ currentSelection.endCoords = getColourCoordinates(index);
+ }
+}
+
+/** Updates the outline for the current selection.
+ *
+ * @param {*} mouseEvent
+ */
+function updateRampSelection(mouseEvent) {
+ if (mouseEvent != null && mouseEvent.which == 3) {
+ currentSelection.endIndex = getElementIndex(mouseEvent.target);
+ }
+
+ if (mouseEvent == null || mouseEvent.which == 3) {
+ let startCoords = getColourCoordinates(currentSelection.startIndex);
+ let endCoords = getColourCoordinates(currentSelection.endIndex);
+
+ let startIndex = currentSelection.startIndex;
+ let endIndex = currentSelection.endIndex;
+
+ if (currentSelection.startIndex > endIndex) {
+ let tmp = startIndex;
+ startIndex = endIndex;
+ endIndex = tmp;
+
+ tmp = startCoords;
+ startCoords = endCoords;
+ endCoords = tmp;
+ }
+
+ clearBorders();
+
+ for (let i=startIndex; i<=endIndex; i++) {
+ let currentSquare = coloursList.children[i];
+ let currentCoords = getColourCoordinates(i);
+ let borderStyle = "3px solid white";
+ let bordersToSet = [];
+
+ // Deciding which borders to use to make the outline
+ if (i == 0 || i == startIndex) {
+ bordersToSet.push("border-left");
+ }
+ if (currentCoords[1] == startCoords[1] || ((currentCoords[1] == startCoords[1] + 1)) && currentCoords[0] < startCoords[0]) {
+ bordersToSet.push("border-top");
+ }
+ if (currentCoords[1] == endCoords[1] || ((currentCoords[1] == endCoords[1] - 1)) && currentCoords[0] > endCoords[0]) {
+ bordersToSet.push("border-bottom");
+ }
+ if ((i == coloursList.childElementCount - 1) || (currentCoords[0] == Math.floor(blockData.blockWidth / blockData.squareSize) - 1)
+ || i == endIndex) {
+ bordersToSet.push("border-right");
+ }
+ if (bordersToSet != []) {
+ currentSquare.style["box-sizing"] = "border-box";
+
+ for (let i=0; i 0 ? -5 : 5;
+ currentSquareSize += amount;
+
+ for (let i=0; i {
+ const palettesMenu = document.getElementById('palette-menu');
+ const splashPalettes = document.getElementById('palette-menu-splash');
+ const noPaletteButton = document.getElementById('no-palette-button');
+ const newPixelElement = document.getElementById('new-pixel');
+ const paletteButton = document.getElementById('palette-button');
+ const paletteButtonSplash = document.getElementById('palette-button-splash');
+ const loadPaletteButton = document.getElementById('load-palette-button');
+ const loadPaletteButtonSplash = document.getElementById('load-palette-button-splash');
- var palettesMenu = document.getElementById('palette-menu');
+ Object.keys(palettes).forEach((paletteName,) => {
- //create button
- var button = document.createElement('button');
- button.appendChild(document.createTextNode(paletteName));
+ const button = document.createElement('button');
+ button.appendChild(document.createTextNode(paletteName));
- //insert new element
- palettesMenu.appendChild(button);
+ //if the palette was specified by the user, change the dropdown to it
+ if (palettes[paletteName].specified) {
+ Util.setText('palette-button', paletteName);
+ Util.setText('palette-button-splash', paletteName)
+ //Show empty palette option
+ noPaletteButton.style.display = 'block';
+ }
- //if the palette was specified by the user, change the dropdown to it
- if (palettes[paletteName].specified == true) {
- setText('palette-button', paletteName);
- //Show empty palette option
- document.getElementById('no-palette-button').style.display = 'block';
- }
+ const buttonEvent = () => {
+ //hide the dropdown menu
+ Util.deselect('palette-menu');
+ Util.deselect('palette-button');
+ Util.deselect('palette-menu-splash');
+ Util.deselect('palette-button-splash');
- on('click', button, function() {
+ //show empty palette option
+ noPaletteButton.style.display = 'block';
- //hide the dropdown menu
- deselect('palette-menu');
- deselect('palette-button');
+ //set the text of the dropdown to the newly selected preset
+ Util.setText('palette-button', paletteName);
+ Util.setText('palette-button-splash', paletteName);
+ }
- //show empty palette option
- document.getElementById('no-palette-button').style.display = 'block';
+ // Making a copy for the splash page too
+ const copyButton = button.cloneNode(true);
+ copyButton.addEventListener('click', buttonEvent);
+ button.addEventListener('click', buttonEvent);
- //set the text of the dropdown to the newly selected preset
- setText('palette-button', paletteName);
+ // Appending it to the splash palette menu
+ splashPalettes.appendChild(copyButton);
+ palettesMenu.appendChild(button);
});
-});
-
-//select no palette
-on('click', 'no-palette-button', function () {
- document.getElementById('no-palette-button').style.display = 'none';
- setText('palette-button', 'Choose a palette...');
-});
-
-//select load palette
-on('click', 'load-palette-button', function () {
- document.getElementById('load-palette-browse-holder').click();
-});
+ const loadPaletteButtonEvent = () => {
+ document.getElementById('load-palette-browse-holder').click();
+ }
+ const clickPaletteButtonEvent = (e) => {
+ Util.toggle('palette-button');
+ Util.toggle('palette-menu');
-on('click', 'palette-button', function (e){
- toggle('palette-button');
- toggle('palette-menu');
+ Util.deselect('preset-button');
+ Util.deselect('preset-menu');
- deselect('preset-button');
- deselect('preset-menu');
- e.stopPropagation();
-});
+ // Splash version
+ Util.toggle('palette-button-splash');
+ Util.toggle('palette-menu-splash');
-on('click', 'new-pixel', function (){
- deselect('editor-mode-menu');
- deselect('preset-button');
- deselect('preset-menu');
- deselect('palette-button');
- deselect('palette-menu');
-});
+ e.stopPropagation();
+ }
+ // Load Palettes
+ loadPaletteButton.addEventListener('click', loadPaletteButtonEvent);
+ loadPaletteButtonSplash.addEventListener('click', loadPaletteButtonEvent);
+
+ // Palette menu click
+ paletteButtonSplash.addEventListener('click', clickPaletteButtonEvent);
+ paletteButton.addEventListener('click', clickPaletteButtonEvent);
+
+ noPaletteButton.addEventListener('click', () => {
+ noPaletteButton.style.display = 'none';
+ Util.setText('palette-button', 'Choose a palette...');
+ })
+
+ newPixelElement.addEventListener('click', () => {
+ Util.deselect('editor-mode-menu');
+ Util.deselect('preset-button');
+ Util.deselect('preset-menu');
+ Util.deselect('palette-button');
+ Util.deselect('palette-menu');
+
+ // Splash version
+ Util.deselect('palette-button-splash');
+ Util.deselect('palette-menu-splash');
+ })
+})();
\ No newline at end of file
diff --git a/js/_pixelEditorUtility.js b/js/_pixelEditorUtility.js
index 2708664..73fa2b8 100644
--- a/js/_pixelEditorUtility.js
+++ b/js/_pixelEditorUtility.js
@@ -1,3 +1,10 @@
+/***********MISCELLANEOUS UTILITY FUNCTIONS**************/
+
+/** Merges topLayer onto belowLayer
+ *
+ * @param {*} belowLayer The layer on the bottom of the layer stack
+ * @param {*} topLayer The layer on the top of the layer stack
+ */
function mergeLayers(belowLayer, topLayer) {
// Copying the above content on the layerBelow
let belowImageData = belowLayer.getImageData(0, 0, canvas.width, canvas.height);
@@ -24,10 +31,19 @@ function mergeLayers(belowLayer, topLayer) {
}
}
+ // Putting the top data into the belowdata
belowLayer.putImageData(toMergeImageData, 0, 0);
}
+/** Used to programmatically create an input event
+ *
+ * @param {*} keyCode KeyCode of the key to press
+ * @param {*} ctrl Is ctrl pressed?
+ * @param {*} alt Is alt pressed?
+ * @param {*} shift Is shift pressed?
+ */
function simulateInput(keyCode, ctrl, alt, shift) {
+ // I just copy pasted this from stack overflow lol please have mercy
let keyboardEvent = document.createEvent("KeyboardEvent");
let initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
@@ -46,38 +62,62 @@ function simulateInput(keyCode, ctrl, alt, shift) {
document.dispatchEvent(keyboardEvent);
}
+/** Tells if a pixel is empty (has alpha = 0)
+ *
+ * @param {*} pixel
+ */
function isPixelEmpty(pixel) {
if (pixel == null || pixel === undefined) {
return false;
}
- if ((pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0) || pixel[3] == 0) {
+ // If the alpha channel is 0, the current pixel is empty
+ if (pixel[3] == 0) {
return true;
}
return false;
}
+/** Tells if element is a child of an element with class className
+ *
+ * @param {*} element
+ * @param {*} className
+ */
function isChildOfByClass(element, className) {
+ // Getting the element with class className
while (element != null && element.classList != null && !element.classList.contains(className)) {
element = element.parentElement;
}
+ // If that element exists and its class is the correct one
if (element != null && element.classList != null && element.classList.contains(className)) {
+ // Then element is a chld of an element with class className
return true;
}
return false;
}
+/** Gets the eyedropped colour (the colour of the pixel pointed by the cursor when the user is using the eyedropper).
+ * It takes the colour of the canvas with the biggest z-index, basically the one the user can see, since it doesn't
+ * make much sense to sample a colour which is hidden behind a different layer
+ *
+ * @param {*} cursorLocation The position of the cursor
+ */
function getEyedropperColor(cursorLocation) {
+ // Making sure max will take some kind of value
let max = -1;
+ // Using tmpColour to sample the sprite
let tmpColour;
+ // Returned colour
let selectedColor;
for (let i=1; i max || isPixelEmpty(selectedColor) || selectedColor === undefined) {
max = layers[i].canvas.style.zIndex;
@@ -87,6 +127,7 @@ function getEyedropperColor(cursorLocation) {
}
}
+ // If the final colour was empty, I return black
if (isPixelEmpty(tmpColour) && selectedColor === undefined) {
selectedColor = [0, 0, 0];
}
@@ -94,7 +135,12 @@ function getEyedropperColor(cursorLocation) {
return selectedColor;
}
+/** Gets the absolute position of the element (position on the screen)
+ *
+ * @param {*} element The element of which we have to get the position
+ */
function getElementAbsolutePosition(element) {
+ // Probably copy pasted this from stack overflow too, if not I don't recall how it works
let curleft = curtop = 0;
if (element.offsetParent) {
@@ -107,9 +153,15 @@ function getElementAbsolutePosition(element) {
return [curleft,curtop];
}
+/** Nearest neighbor algorithm to scale a sprite
+ *
+ * @param {*} src The source imageData
+ * @param {*} dst The destination imageData
+ */
function nearestNeighbor (src, dst) {
let pos = 0
+ // Just applying the nearest neighbor algorithm
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = Math.floor(x * src.width / dst.width)
@@ -125,7 +177,14 @@ function nearestNeighbor (src, dst) {
}
}
+/** Bilinear interpolation used to scale a sprite
+ *
+ * @param {*} src The source imageData
+ * @param {*} dst The destination imageData
+ */
function bilinearInterpolation (src, dst) {
+ // Applying the bilinear interpolation algorithm
+
function interpolate (k, kMin, kMax, vMin, vMax) {
return Math.round((k - kMin) * vMax + (kMax - k) * vMin)
}
@@ -167,14 +226,21 @@ function bilinearInterpolation (src, dst) {
}
}
+/** Resizes an imageData depending on the algorithm and on the new width and height
+ *
+ * @param {*} image The imageData to scale
+ * @param {*} width The new width of the imageData
+ * @param {*} height The new height of the imageData
+ * @param {*} algorithm Scaling algorithm chosen by the user in the dialogue
+ */
function resizeImageData (image, width, height, algorithm) {
algorithm = algorithm || 'bilinear-interpolation'
- let resize
+ let resize;
switch (algorithm) {
case 'nearest-neighbor': resize = nearestNeighbor; break
case 'bilinear-interpolation': resize = bilinearInterpolation; break
- default: throw new Error(`Unknown algorithm: ${algorithm}`)
+ default: return image;
}
const result = new ImageData(width, height)
@@ -184,10 +250,21 @@ function resizeImageData (image, width, height, algorithm) {
return result
}
+/** Gets the position in (x, y) format of the pixel with index "index"
+ *
+ * @param {*} index The index of the pixel of which we need the (x, y) position
+ */
function getPixelPosition(index) {
let linearIndex = index / 4;
let x = linearIndex % layers[0].canvasSize[0];
let y = Math.floor(linearIndex / layers[0].canvasSize[0]);
return [Math.ceil(x), Math.ceil(y)];
+}
+
+/** Sets isDragging to false, used when the user interacts with sortable lists
+ *
+ */
+function makeIsDraggingFalse(event) {
+ dragging = false;
}
\ No newline at end of file
diff --git a/js/_pixelGrid.js b/js/_pixelGrid.js
index 0c36c98..0ce55ce 100644
--- a/js/_pixelGrid.js
+++ b/js/_pixelGrid.js
@@ -1,27 +1,44 @@
+// Start colour of the pixel grid (can be changed in the preferences)
let pixelGridColor = "#0000FF";
+// Distance between one line and another in HTML pixels
let lineDistance = 12;
+// The grid is not visible at the beginning
let pixelGridVisible = false;
+// Saving the canvas containing the pixel grid
pixelGridCanvas = document.getElementById("pixel-grid");
+/** Shows or hides the pixel grid depening on its current visibility
+ * (triggered by the show pixel grid button in the top menu)
+ *
+ */
function togglePixelGrid(event) {
+ // Getting the button because I have to change its text
let button = document.getElementById("toggle-pixelgrid-button");
+ // Toggling the state
pixelGridVisible = !pixelGridVisible;
+ // If it was visible, I hide it
if (pixelGridVisible) {
button.innerHTML = "Hide pixel grid";
pixelGridCanvas.style.display = "inline-block";
}
+ // Otherwise, I show it
else {
button.innerHTML = "Show pixel grid";
pixelGridCanvas.style.display = "none";
}
}
+/** Fills the pixelGridCanvas with the pixelgrid
+ *
+ */
function fillPixelGrid() {
let context = pixelGridCanvas.getContext("2d");
let originalSize = layers[0].canvasSize;
+ // The pixelGridCanvas is lineDistance times bigger so that the lines don't take 1 canvas pixel
+ // (which would cover the whole canvas with the pixelGridColour), but they take 1/lineDistance canvas pixels
pixelGridCanvas.width = originalSize[0] * lineDistance;
pixelGridCanvas.height = originalSize[1] * lineDistance;
diff --git a/js/_presets.js b/js/_presets.js
index c0f4714..955f243 100644
--- a/js/_presets.js
+++ b/js/_presets.js
@@ -1,63 +1,60 @@
-//prests
-var presets = {
- 'Gameboy Color': {
- width: 240,
- height: 203,
- palette: 'Gameboy Color'
- },
- 'PICO-8': {
- width: 128,
- height: 128,
- palette: 'PICO-8'
- },
- 'Commodore 64': {
- width: 40,
- height: 80,
- palette: 'Commodore 64'
+const PresetModule = (() => {
+ const presets = {
+ 'Gameboy Color': {width: 240, height: 203, palette: 'Gameboy Color'},
+ 'PICO-8': {width: 128, height: 128, palette: 'PICO-8'},
+ 'Commodore 64': {width: 40, height: 80, palette: 'Commodore 64'}
+ };
+
+ function instrumentPresetMenu() {
+ console.info("Initializing presets..");
+ // Add a button for all the presets available
+ const presetsMenu = document.getElementById('preset-menu');
+ Object.keys(presets).forEach((presetName,) => {
+
+ const button = document.createElement('button');
+ button.appendChild(document.createTextNode(presetName));
+
+ presetsMenu.appendChild(button);
+
+ button.addEventListener('click', () => {
+ //change dimentions on new pixel form
+ Util.setValue('size-width', presets[presetName].width);
+ Util.setValue('size-height', presets[presetName].height);
+
+ //set the text of the dropdown to the newly selected preset
+ Util.setText('palette-button', presets[presetName].palette);
+
+ //hide the dropdown menu
+ Util.deselect('preset-menu');
+ Util.deselect('preset-button');
+
+ //set the text of the dropdown to the newly selected preset
+ Util.setText('preset-button', presetName);
+
+ });
+ });
+
+ const presetButton = document.getElementById('preset-button');
+ presetButton.addEventListener('click', (e) => {
+ //open or close the preset menu
+ Util.toggle('preset-button');
+ Util.toggle('preset-menu');
+
+ //close the palette menu
+ Util.deselect('palette-button');
+ Util.deselect('palette-menu');
+
+ e.stopPropagation();
+ });
}
-};
-//populate preset list in new pixel menu
-Object.keys(presets).forEach(function(presetName,index) {
+ function propertiesOf(presetId) {
+ return presets[presetId];
+ }
- var presetsMenu = document.getElementById('preset-menu');
-
- //create button
- var button = document.createElement('button');
- button.appendChild(document.createTextNode(presetName));
-
- //insert new element
- presetsMenu.appendChild(button);
-
- //add click event listener
- on('click', button, function() {
-
- //change dimentions on new pixel form
- setValue('size-width', presets[presetName].width);
- setValue('size-height', presets[presetName].height);
-
- //set the text of the dropdown to the newly selected preset
- setText('palette-button', presets[presetName].palette);
-
- //hide the dropdown menu
- deselect('preset-menu');
- deselect('preset-button');
-
- //set the text of the dropdown to the newly selected preset
- setText('preset-button', presetName);
- });
-
-});
-
-on('click', 'preset-button', function (e){
- //open or close the preset menu
- toggle('preset-button');
- toggle('preset-menu');
-
- //close the palette menu
- deselect('palette-button');
- deselect('palette-menu');
-
- //stop the click from propogating to the parent element
- e.stopPropagation();
-});
+ return {
+ propertiesOf,
+ instrumentPresetMenu
+ };
+
+})();
\ No newline at end of file
diff --git a/js/_rectSelect.js b/js/_rectSelect.js
index a2705cc..ade0ba1 100644
--- a/js/_rectSelect.js
+++ b/js/_rectSelect.js
@@ -4,6 +4,10 @@ let startY;
let endX;
let endY;
+/** Starts the selection: saves the canvas, sets the start coordinates
+ *
+ * @param {*} mouseEvent
+ */
function startRectSelection(mouseEvent) {
// Saving the canvas
new HistoryStateEditCanvas();
@@ -35,6 +39,10 @@ function startRectSelection(mouseEvent) {
selectionCanceled = false;
}
+/** Updates the selection
+ *
+ * @param {*} mouseEvent
+ */
function updateRectSelection(mouseEvent) {
let pos = getCursorPosition(mouseEvent);
@@ -42,6 +50,10 @@ function updateRectSelection(mouseEvent) {
drawRect(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
}
+/** Ends the selection: sets the end coordiantes
+ *
+ * @param {*} mouseEvent
+ */
function endRectSelection(mouseEvent) {
// Getting the end position
let currentPos = getCursorPosition(mouseEvent);
@@ -72,6 +84,10 @@ function endRectSelection(mouseEvent) {
currentTool.updateCursor();
}
+/** Cuts the selection from its canvas and puts it in the tmp layer so it can be moved
+ *
+ * @param {*} mousePosition
+ */
function cutSelection(mousePosition) {
// Getting the selected pixels
imageDataToMove = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
@@ -79,10 +95,13 @@ function cutSelection(mousePosition) {
currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1);
// Moving those pixels from the current layer to the tmp layer
TMPLayer.context.putImageData(imageDataToMove, startX + 1, startY);
-
- //originalDataPosition = [currentPos[0], currentPos[1]];
}
+/** Draws a dashed rectangle representing the selection
+ *
+ * @param {*} x Current end x coordinate of the selection
+ * @param {*} y Current end y coordinate of the selection
+ */
function drawRect(x, y) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d');
@@ -106,10 +125,13 @@ function applyChanges() {
// Checks whether the pointer is inside the selected area or not
function cursorInSelectedArea() {
+ // Getting the cursor position
let cursorPos = getCursorPosition(currentMouseEvent);
+ // Getting the coordinates relatively to the canvas
let x = cursorPos[0] / zoom;
let y = cursorPos[1] / zoom;
+ // This is to avoid rightX or topY being less than leftX or bottomY
let leftX = Math.min(startX, endX);
let rightX = Math.max(startX, endX);
let topY = Math.max(startY, endY);
@@ -126,6 +148,13 @@ function cursorInSelectedArea() {
return false;
}
+/** Moves the rect ants to the specified position
+ *
+ * @param {*} x X coordinate of the rect ants
+ * @param {*} y Y coordinat of the rect ants
+ * @param {*} width Width of the selection
+ * @param {*} height Height of the selectin
+ */
function moveSelection(x, y, width, height) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d');
@@ -135,6 +164,7 @@ function moveSelection(x, y, width, height) {
vfxContext.lineWidth = 1;
vfxContext.setLineDash([4]);
+ // Fixing the coordinates
startX = Math.round(Math.round(x) - 0.5 - Math.round(width / 2)) + 0.5;
startY = Math.round(Math.round(y) - 0.5 - Math.round(height / 2)) + 0.5;
endX = startX + Math.round(width);
diff --git a/js/_rectangle.js b/js/_rectangle.js
index 516f730..f4ab4c5 100644
--- a/js/_rectangle.js
+++ b/js/_rectangle.js
@@ -1,19 +1,26 @@
+// Saving the empty rect svg
+var emptyRectangleSVG = document.getElementById("rectangle-empty-button-svg");
+// and the full rect svg so that I can change them when the user changes rect modes
+var fullRectangleSVG = document.getElementById("rectangle-full-button-svg");
-var emptySVG = document.getElementById("empty-button-svg");
-var fullSVG = document.getElementById("full-button-svg");
-
-var drawMode = 'empty';
+// The start mode is empty rectangle
+var rectangleDrawMode = 'empty';
+// I'm not drawing a rectangle at the beginning
var isDrawingRect = false;
+// Rect coordinates
let startRectX;
let startRectY;
let endRectX;
let endRectY;
-
+/** Starts drawing the rect, saves the start coordinates
+ *
+ * @param {*} mouseEvent
+ */
function startRectDrawing(mouseEvent) {
- // Putting the vfx layer on top of everything
- VFXCanvas.style.zIndex = MAX_Z_INDEX;
+ // Putting the vfx layer on top of everything
+ VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
// Updating flag
isDrawingRect = true;
@@ -25,20 +32,29 @@ function startRectDrawing(mouseEvent) {
drawRectangle(startRectX, startRectY);
}
+/** Updates the rectangle preview depending on the position of the mouse
+ *
+ * @param {*} mouseEvent The mouseEvent from which we'll get the mouse position
+ */
function updateRectDrawing(mouseEvent) {
let pos = getCursorPosition(mouseEvent);
- // Drawing the rect
- drawRectangle(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
+ // Drawing the rect at the right position
+ drawRectangle(Math.floor(pos[0] / zoom) + 0.5, Math.floor(pos[1] / zoom) + 0.5);
}
+/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
+ * current layer
+ *
+ * @param {*} mouseEvent event from which we'll get the mouse position
+ */
function endRectDrawing(mouseEvent) {
// Getting the end position
let currentPos = getCursorPosition(mouseEvent);
let vfxContext = VFXCanvas.getContext("2d");
- endRectX = Math.round(currentPos[0] / zoom) + 0.5;
- endRectY = Math.round(currentPos[1] / zoom) + 0.5;
+ endRectX = Math.floor(currentPos[0] / zoom) + 0.5;
+ endRectY = Math.floor(currentPos[1] / zoom) + 0.5;
// Inverting end and start (start must always be the top left corner)
if (endRectX < startRectX) {
@@ -61,15 +77,18 @@ function endRectDrawing(mouseEvent) {
endRectX -= 0.5;
startRectX -= 0.5;
+ // Setting the correct linewidth and colour
currentLayer.context.lineWidth = tool.rectangle.brushSize;
currentLayer.context.fillStyle = currentGlobalColor;
+ // Drawing the rect using 4 lines
line(startRectX, startRectY, endRectX, startRectY, tool.rectangle.brushSize);
line(endRectX, startRectY, endRectX, endRectY, tool.rectangle.brushSize);
line(endRectX, endRectY, startRectX, endRectY, tool.rectangle.brushSize);
line(startRectX, endRectY, startRectX, startRectY, tool.rectangle.brushSize);
- if (drawMode == 'fill') {
+ // If I have to fill it, I do so
+ if (rectangleDrawMode == 'fill') {
currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
}
@@ -77,6 +96,12 @@ function endRectDrawing(mouseEvent) {
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
}
+/** Draws a rectangle with end coordinates given by x and y on the VFX layer (draws
+ * the preview for the rectangle tool)
+ *
+ * @param {*} x The current end x of the rectangle
+ * @param {*} y The current end y of the rectangle
+ */
function drawRectangle(x, y) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext("2d");
@@ -98,21 +123,23 @@ function drawRectangle(x, y) {
}
vfxContext.setLineDash([]);
-
vfxContext.stroke();
}
+/** Sets the correct tool icon depending on its mode
+ *
+ */
function setRectToolSvg() {
- if (drawMode == 'empty') {
- emptySVG.setAttribute('display', 'visible');
- fullSVG.setAttribute('display', 'none');
+ if (rectangleDrawMode == 'empty') {
+ emptyRectangleSVG.setAttribute('display', 'visible');
+ fullRectangleSVG.setAttribute('display', 'none');
}
else {
- emptySVG.setAttribute('display', 'none');
- fullSVG.setAttribute('display', 'visible');
+ emptyRectangleSVG.setAttribute('display', 'none');
+ fullRectangleSVG.setAttribute('display', 'visible');
}
}
function applyChanges() {
- VFXCanvas.style.zIndex = MIN_Z_INDEX;
+ //VFXCanvas.style.zIndex = MIN_Z_INDEX;
}
diff --git a/js/_resizeCanvas.js b/js/_resizeCanvas.js
index 42c4aa0..faa6641 100644
--- a/js/_resizeCanvas.js
+++ b/js/_resizeCanvas.js
@@ -1,16 +1,31 @@
-let resizeCanvasContainer = document.getElementById("resize-canvas");
-let rcPivot = "middle";
-let currentPivotObject;
-let borders = {left: 0, right: 0, top: 0, bottom: 0};
+/* This scripts contains all the code used to handle the canvas resizing */
+// Resize canvas pop up window
+let resizeCanvasContainer = document.getElementById("resize-canvas");
+// Start pivot
+let rcPivot = "middle";
+// Selected pivot button
+let currentPivotObject;
+// Border offsets
+let rcBorders = {left: 0, right: 0, top: 0, bottom: 0};
+
+/** Opens the canvas resize window
+ *
+ */
function openResizeCanvasWindow() {
+ // Initializes the inputs
initResizeCanvasInputs();
showDialogue('resize-canvas');
}
+/** Initializes the canvas resizing input
+ *
+ */
function initResizeCanvasInputs() {
+ // Getting the pivot buttons
let buttons = document.getElementsByClassName("pivot-button");
+ // Adding the event handlers for them
for (let i=0; i 1)
+ tool.ellipse.brushSize--;
+}, false);
+
//fill
on('click',"fill-button", function(){
tool.fill.switchTo();
@@ -75,33 +104,24 @@ on('click',"eyedropper-button", function(){
tool.eyedropper.switchTo();
}, false);
-//zoom tool button
-on('click',"zoom-button", function(){
- tool.zoom.switchTo();
-}, false);
-
-//zoom in button
-on('click','zoom-in-button', function(){
- //changeZoom('in',[window.innerWidth/2-canvas.offsetLeft,window.innerHeight/2-canvas.offsetTop]);
- changeZoom(layers[0],'in', [canvasSize[0] * zoom / 2, canvasSize[1] * zoom / 2]);
-
- for (let i=1; i 1)
+ tool.line.brushSize--;
+}, false);
+
+
/*global on */
diff --git a/js/_tools.js b/js/_tools.js
index 3c10d70..a6d374d 100644
--- a/js/_tools.js
+++ b/js/_tools.js
@@ -95,15 +95,14 @@ class Tool {
}
moveBrushPreview(cursorLocation) {
- let toSub = 0;
- // Prevents the brush to be put in the middle of pixels
- if (this.currentBrushSize % 2 == 0) {
- toSub = 0.5;
- }
-
- brushPreview.style.left = (Math.ceil(cursorLocation[0] / zoom) * zoom + currentLayer.canvas.offsetLeft - this.currentBrushSize * zoom / 2 - zoom / 2 - toSub * zoom) + 'px';
- brushPreview.style.top = (Math.ceil(cursorLocation[1] / zoom) * zoom + currentLayer.canvas.offsetTop - this.currentBrushSize * zoom / 2 - zoom / 2 - toSub * zoom) + 'px';
- }
+ let toSub = 1;
+ // Prevents the brush to be put in the middle of pixels
+ if (this.currentBrushSize % 2 == 0) {
+ toSub = 0.5;
+ }
+ brushPreview.style.left = (Math.floor(cursorLocation[0] / zoom) * zoom + currentLayer.canvas.offsetLeft - this.currentBrushSize * zoom / 2 - zoom / 2 + toSub * zoom) + 'px';
+ brushPreview.style.top = (Math.floor(cursorLocation[1] / zoom) * zoom + currentLayer.canvas.offsetTop - this.currentBrushSize * zoom / 2 - zoom / 2 + toSub * zoom) + 'px';
+ }
}
diff --git a/js/_variables.js b/js/_variables.js
index b84b1a3..4c85ab9 100644
--- a/js/_variables.js
+++ b/js/_variables.js
@@ -5,7 +5,7 @@ var dragging = false;
var lastMouseClickPos = [0,0];
var dialogueOpen = false;
var documentCreated = false;
-var pixelEditorMode;
+var pixelEditorMode = "Advanced";
//common elements
var brushPreview = document.getElementById("brush-preview");
diff --git a/js/pixel-editor-splash-page.js b/js/pixel-editor-splash-page.js
index c671905..2d798d8 100644
--- a/js/pixel-editor-splash-page.js
+++ b/js/pixel-editor-splash-page.js
@@ -1,8 +1,6 @@
ajax('https://api.github.com/repos/lospec/pixel-editor/contributors', response => {
- console.log(response)
-
if (Array.isArray(response)) {
var html = '';
diff --git a/js/pixel-editor.js b/js/pixel-editor.js
index 52b1f4b..137a9bf 100644
--- a/js/pixel-editor.js
+++ b/js/pixel-editor.js
@@ -12,6 +12,9 @@
//=include utilities/hslToRgb.js
//=include libraries/cookies.js
//=include _pixelEditorUtility.js
+//=include sortable.js
+//=include _algorithms.js
+//=include Util.js
/**init**/
//=include _consts.js
@@ -28,16 +31,16 @@
//=include tools/*.js
//=include _newPixel.js
//=include _createColorPalette.js
-//=include _setCanvasOffset.js
//=include _changeZoom.js
//=include _addColor.js
//=include _colorChanged.js
//=include _initColor.js
//=include _dialogue.js
-//=include _updateCursor.js
+//=include _featuresLog.js
//=include _drawLine.js
//=include _getCursorPosition.js
//=include _fill.js
+//=include _line.js
//=include _history.js
//=include _deleteColor.js
//=include _replaceAllOfColor.js
@@ -47,6 +50,10 @@
//=include _copyPaste.js
//=include _resizeCanvas.js
//=include _resizeSprite.js
+//=include _colorPicker.js
+//=include _paletteBlock.js
+//=include _splashPage.js
+//=include _logs.js
/**load file**/
//=include _loadImage.js
@@ -65,6 +72,7 @@
//=include _rectSelect.js
//=include _move.js
//=include _rectangle.js
+//=include _ellipse.js
/**onload**/
//=include _onLoad.js
@@ -72,3 +80,9 @@
/**libraries**/
//=include _jscolor.js
+
+/**feature toggles**/
+//=include _featureToggles.js
+
+// Controls execution of this preset module
+PresetModule.instrumentPresetMenu();
\ No newline at end of file
diff --git a/js/sortable.js b/js/sortable.js
new file mode 100644
index 0000000..c6d7f9d
--- /dev/null
+++ b/js/sortable.js
@@ -0,0 +1,3709 @@
+/**!
+ * Sortable 1.10.2
+ * @author RubaXa
+ * @author owenm
+ * @license MIT
+ */
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global = global || self, global.Sortable = factory());
+}(this, function () { 'use strict';
+
+ function _typeof(obj) {
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
+ _typeof = function (obj) {
+ return typeof obj;
+ };
+ } else {
+ _typeof = function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+ };
+ }
+
+ return _typeof(obj);
+ }
+
+ function _defineProperty(obj, key, value) {
+ if (key in obj) {
+ Object.defineProperty(obj, key, {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ });
+ } else {
+ obj[key] = value;
+ }
+
+ return obj;
+ }
+
+ function _extends() {
+ _extends = Object.assign || function (target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i];
+
+ for (var key in source) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ return target;
+ };
+
+ return _extends.apply(this, arguments);
+ }
+
+ function _objectSpread(target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i] != null ? arguments[i] : {};
+ var ownKeys = Object.keys(source);
+
+ if (typeof Object.getOwnPropertySymbols === 'function') {
+ ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
+ }));
+ }
+
+ ownKeys.forEach(function (key) {
+ _defineProperty(target, key, source[key]);
+ });
+ }
+
+ return target;
+ }
+
+ function _objectWithoutPropertiesLoose(source, excluded) {
+ if (source == null) return {};
+ var target = {};
+ var sourceKeys = Object.keys(source);
+ var key, i;
+
+ for (i = 0; i < sourceKeys.length; i++) {
+ key = sourceKeys[i];
+ if (excluded.indexOf(key) >= 0) continue;
+ target[key] = source[key];
+ }
+
+ return target;
+ }
+
+ function _objectWithoutProperties(source, excluded) {
+ if (source == null) return {};
+
+ var target = _objectWithoutPropertiesLoose(source, excluded);
+
+ var key, i;
+
+ if (Object.getOwnPropertySymbols) {
+ var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
+
+ for (i = 0; i < sourceSymbolKeys.length; i++) {
+ key = sourceSymbolKeys[i];
+ if (excluded.indexOf(key) >= 0) continue;
+ if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
+ target[key] = source[key];
+ }
+ }
+
+ return target;
+ }
+
+ function _toConsumableArray(arr) {
+ return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
+ }
+
+ function _arrayWithoutHoles(arr) {
+ if (Array.isArray(arr)) {
+ for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
+
+ return arr2;
+ }
+ }
+
+ function _iterableToArray(iter) {
+ if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
+ }
+
+ function _nonIterableSpread() {
+ throw new TypeError("Invalid attempt to spread non-iterable instance");
+ }
+
+ var version = "1.10.2";
+
+ function userAgent(pattern) {
+ if (typeof window !== 'undefined' && window.navigator) {
+ return !!
+ /*@__PURE__*/
+ navigator.userAgent.match(pattern);
+ }
+ }
+
+ var IE11OrLess = userAgent(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i);
+ var Edge = userAgent(/Edge/i);
+ var FireFox = userAgent(/firefox/i);
+ var Safari = userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i);
+ var IOS = userAgent(/iP(ad|od|hone)/i);
+ var ChromeForAndroid = userAgent(/chrome/i) && userAgent(/android/i);
+
+ var captureMode = {
+ capture: false,
+ passive: false
+ };
+
+ function on(el, event, fn) {
+ el.addEventListener(event, fn, !IE11OrLess && captureMode);
+ }
+
+ function off(el, event, fn) {
+ el.removeEventListener(event, fn, !IE11OrLess && captureMode);
+ }
+
+ function matches(
+ /**HTMLElement*/
+ el,
+ /**String*/
+ selector) {
+ if (!selector) return;
+ selector[0] === '>' && (selector = selector.substring(1));
+
+ if (el) {
+ try {
+ if (el.matches) {
+ return el.matches(selector);
+ } else if (el.msMatchesSelector) {
+ return el.msMatchesSelector(selector);
+ } else if (el.webkitMatchesSelector) {
+ return el.webkitMatchesSelector(selector);
+ }
+ } catch (_) {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ function getParentOrHost(el) {
+ return el.host && el !== document && el.host.nodeType ? el.host : el.parentNode;
+ }
+
+ function closest(
+ /**HTMLElement*/
+ el,
+ /**String*/
+ selector,
+ /**HTMLElement*/
+ ctx, includeCTX) {
+ if (el) {
+ ctx = ctx || document;
+
+ do {
+ if (selector != null && (selector[0] === '>' ? el.parentNode === ctx && matches(el, selector) : matches(el, selector)) || includeCTX && el === ctx) {
+ return el;
+ }
+
+ if (el === ctx) break;
+ /* jshint boss:true */
+ } while (el = getParentOrHost(el));
+ }
+
+ return null;
+ }
+
+ var R_SPACE = /\s+/g;
+
+ function toggleClass(el, name, state) {
+ if (el && name) {
+ if (el.classList) {
+ el.classList[state ? 'add' : 'remove'](name);
+ } else {
+ var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' ');
+ el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' ');
+ }
+ }
+ }
+
+ function css(el, prop, val) {
+ var style = el && el.style;
+
+ if (style) {
+ if (val === void 0) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ val = document.defaultView.getComputedStyle(el, '');
+ } else if (el.currentStyle) {
+ val = el.currentStyle;
+ }
+
+ return prop === void 0 ? val : val[prop];
+ } else {
+ if (!(prop in style) && prop.indexOf('webkit') === -1) {
+ prop = '-webkit-' + prop;
+ }
+
+ style[prop] = val + (typeof val === 'string' ? '' : 'px');
+ }
+ }
+ }
+
+ function matrix(el, selfOnly) {
+ var appliedTransforms = '';
+
+ if (typeof el === 'string') {
+ appliedTransforms = el;
+ } else {
+ do {
+ var transform = css(el, 'transform');
+
+ if (transform && transform !== 'none') {
+ appliedTransforms = transform + ' ' + appliedTransforms;
+ }
+ /* jshint boss:true */
+
+ } while (!selfOnly && (el = el.parentNode));
+ }
+
+ var matrixFn = window.DOMMatrix || window.WebKitCSSMatrix || window.CSSMatrix || window.MSCSSMatrix;
+ /*jshint -W056 */
+
+ return matrixFn && new matrixFn(appliedTransforms);
+ }
+
+ function find(ctx, tagName, iterator) {
+ if (ctx) {
+ var list = ctx.getElementsByTagName(tagName),
+ i = 0,
+ n = list.length;
+
+ if (iterator) {
+ for (; i < n; i++) {
+ iterator(list[i], i);
+ }
+ }
+
+ return list;
+ }
+
+ return [];
+ }
+
+ function getWindowScrollingElement() {
+ var scrollingElement = document.scrollingElement;
+
+ if (scrollingElement) {
+ return scrollingElement;
+ } else {
+ return document.documentElement;
+ }
+ }
+ /**
+ * Returns the "bounding client rect" of given element
+ * @param {HTMLElement} el The element whose boundingClientRect is wanted
+ * @param {[Boolean]} relativeToContainingBlock Whether the rect should be relative to the containing block of (including) the container
+ * @param {[Boolean]} relativeToNonStaticParent Whether the rect should be relative to the relative parent of (including) the contaienr
+ * @param {[Boolean]} undoScale Whether the container's scale() should be undone
+ * @param {[HTMLElement]} container The parent the element will be placed in
+ * @return {Object} The boundingClientRect of el, with specified adjustments
+ */
+
+
+ function getRect(el, relativeToContainingBlock, relativeToNonStaticParent, undoScale, container) {
+ if (!el.getBoundingClientRect && el !== window) return;
+ var elRect, top, left, bottom, right, height, width;
+
+ if (el !== window && el !== getWindowScrollingElement()) {
+ elRect = el.getBoundingClientRect();
+ top = elRect.top;
+ left = elRect.left;
+ bottom = elRect.bottom;
+ right = elRect.right;
+ height = elRect.height;
+ width = elRect.width;
+ } else {
+ top = 0;
+ left = 0;
+ bottom = window.innerHeight;
+ right = window.innerWidth;
+ height = window.innerHeight;
+ width = window.innerWidth;
+ }
+
+ if ((relativeToContainingBlock || relativeToNonStaticParent) && el !== window) {
+ // Adjust for translate()
+ container = container || el.parentNode; // solves #1123 (see: https://stackoverflow.com/a/37953806/6088312)
+ // Not needed on <= IE11
+
+ if (!IE11OrLess) {
+ do {
+ if (container && container.getBoundingClientRect && (css(container, 'transform') !== 'none' || relativeToNonStaticParent && css(container, 'position') !== 'static')) {
+ var containerRect = container.getBoundingClientRect(); // Set relative to edges of padding box of container
+
+ top -= containerRect.top + parseInt(css(container, 'border-top-width'));
+ left -= containerRect.left + parseInt(css(container, 'border-left-width'));
+ bottom = top + elRect.height;
+ right = left + elRect.width;
+ break;
+ }
+ /* jshint boss:true */
+
+ } while (container = container.parentNode);
+ }
+ }
+
+ if (undoScale && el !== window) {
+ // Adjust for scale()
+ var elMatrix = matrix(container || el),
+ scaleX = elMatrix && elMatrix.a,
+ scaleY = elMatrix && elMatrix.d;
+
+ if (elMatrix) {
+ top /= scaleY;
+ left /= scaleX;
+ width /= scaleX;
+ height /= scaleY;
+ bottom = top + height;
+ right = left + width;
+ }
+ }
+
+ return {
+ top: top,
+ left: left,
+ bottom: bottom,
+ right: right,
+ width: width,
+ height: height
+ };
+ }
+ /**
+ * Checks if a side of an element is scrolled past a side of its parents
+ * @param {HTMLElement} el The element who's side being scrolled out of view is in question
+ * @param {String} elSide Side of the element in question ('top', 'left', 'right', 'bottom')
+ * @param {String} parentSide Side of the parent in question ('top', 'left', 'right', 'bottom')
+ * @return {HTMLElement} The parent scroll element that the el's side is scrolled past, or null if there is no such element
+ */
+
+
+ function isScrolledPast(el, elSide, parentSide) {
+ var parent = getParentAutoScrollElement(el, true),
+ elSideVal = getRect(el)[elSide];
+ /* jshint boss:true */
+
+ while (parent) {
+ var parentSideVal = getRect(parent)[parentSide],
+ visible = void 0;
+
+ if (parentSide === 'top' || parentSide === 'left') {
+ visible = elSideVal >= parentSideVal;
+ } else {
+ visible = elSideVal <= parentSideVal;
+ }
+
+ if (!visible) return parent;
+ if (parent === getWindowScrollingElement()) break;
+ parent = getParentAutoScrollElement(parent, false);
+ }
+
+ return false;
+ }
+ /**
+ * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible)
+ * and non-draggable elements
+ * @param {HTMLElement} el The parent element
+ * @param {Number} childNum The index of the child
+ * @param {Object} options Parent Sortable's options
+ * @return {HTMLElement} The child at index childNum, or null if not found
+ */
+
+
+ function getChild(el, childNum, options) {
+ var currentChild = 0,
+ i = 0,
+ children = el.children;
+
+ while (i < children.length) {
+ if (children[i].style.display !== 'none' && children[i] !== Sortable.ghost && children[i] !== Sortable.dragged && closest(children[i], options.draggable, el, false)) {
+ if (currentChild === childNum) {
+ return children[i];
+ }
+
+ currentChild++;
+ }
+
+ i++;
+ }
+
+ return null;
+ }
+ /**
+ * Gets the last child in the el, ignoring ghostEl or invisible elements (clones)
+ * @param {HTMLElement} el Parent element
+ * @param {selector} selector Any other elements that should be ignored
+ * @return {HTMLElement} The last child, ignoring ghostEl
+ */
+
+
+ function lastChild(el, selector) {
+ var last = el.lastElementChild;
+
+ while (last && (last === Sortable.ghost || css(last, 'display') === 'none' || selector && !matches(last, selector))) {
+ last = last.previousElementSibling;
+ }
+
+ return last || null;
+ }
+ /**
+ * Returns the index of an element within its parent for a selected set of
+ * elements
+ * @param {HTMLElement} el
+ * @param {selector} selector
+ * @return {number}
+ */
+
+
+ function index(el, selector) {
+ var index = 0;
+
+ if (!el || !el.parentNode) {
+ return -1;
+ }
+ /* jshint boss:true */
+
+
+ while (el = el.previousElementSibling) {
+ if (el.nodeName.toUpperCase() !== 'TEMPLATE' && el !== Sortable.clone && (!selector || matches(el, selector))) {
+ index++;
+ }
+ }
+
+ return index;
+ }
+ /**
+ * Returns the scroll offset of the given element, added with all the scroll offsets of parent elements.
+ * The value is returned in real pixels.
+ * @param {HTMLElement} el
+ * @return {Array} Offsets in the format of [left, top]
+ */
+
+
+ function getRelativeScrollOffset(el) {
+ var offsetLeft = 0,
+ offsetTop = 0,
+ winScroller = getWindowScrollingElement();
+
+ if (el) {
+ do {
+ var elMatrix = matrix(el),
+ scaleX = elMatrix.a,
+ scaleY = elMatrix.d;
+ offsetLeft += el.scrollLeft * scaleX;
+ offsetTop += el.scrollTop * scaleY;
+ } while (el !== winScroller && (el = el.parentNode));
+ }
+
+ return [offsetLeft, offsetTop];
+ }
+ /**
+ * Returns the index of the object within the given array
+ * @param {Array} arr Array that may or may not hold the object
+ * @param {Object} obj An object that has a key-value pair unique to and identical to a key-value pair in the object you want to find
+ * @return {Number} The index of the object in the array, or -1
+ */
+
+
+ function indexOfObject(arr, obj) {
+ for (var i in arr) {
+ if (!arr.hasOwnProperty(i)) continue;
+
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key) && obj[key] === arr[i][key]) return Number(i);
+ }
+ }
+
+ return -1;
+ }
+
+ function getParentAutoScrollElement(el, includeSelf) {
+ // skip to window
+ if (!el || !el.getBoundingClientRect) return getWindowScrollingElement();
+ var elem = el;
+ var gotSelf = false;
+
+ do {
+ // we don't need to get elem css if it isn't even overflowing in the first place (performance)
+ if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) {
+ var elemCSS = css(elem);
+
+ if (elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll') || elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')) {
+ if (!elem.getBoundingClientRect || elem === document.body) return getWindowScrollingElement();
+ if (gotSelf || includeSelf) return elem;
+ gotSelf = true;
+ }
+ }
+ /* jshint boss:true */
+
+ } while (elem = elem.parentNode);
+
+ return getWindowScrollingElement();
+ }
+
+ function extend(dst, src) {
+ if (dst && src) {
+ for (var key in src) {
+ if (src.hasOwnProperty(key)) {
+ dst[key] = src[key];
+ }
+ }
+ }
+
+ return dst;
+ }
+
+ function isRectEqual(rect1, rect2) {
+ return Math.round(rect1.top) === Math.round(rect2.top) && Math.round(rect1.left) === Math.round(rect2.left) && Math.round(rect1.height) === Math.round(rect2.height) && Math.round(rect1.width) === Math.round(rect2.width);
+ }
+
+ var _throttleTimeout;
+
+ function throttle(callback, ms) {
+ return function () {
+ if (!_throttleTimeout) {
+ var args = arguments,
+ _this = this;
+
+ if (args.length === 1) {
+ callback.call(_this, args[0]);
+ } else {
+ callback.apply(_this, args);
+ }
+
+ _throttleTimeout = setTimeout(function () {
+ _throttleTimeout = void 0;
+ }, ms);
+ }
+ };
+ }
+
+ function cancelThrottle() {
+ clearTimeout(_throttleTimeout);
+ _throttleTimeout = void 0;
+ }
+
+ function scrollBy(el, x, y) {
+ el.scrollLeft += x;
+ el.scrollTop += y;
+ }
+
+ function clone(el) {
+ var Polymer = window.Polymer;
+ var $ = window.jQuery || window.Zepto;
+
+ if (Polymer && Polymer.dom) {
+ return Polymer.dom(el).cloneNode(true);
+ } else if ($) {
+ return $(el).clone(true)[0];
+ } else {
+ return el.cloneNode(true);
+ }
+ }
+
+ function setRect(el, rect) {
+ css(el, 'position', 'absolute');
+ css(el, 'top', rect.top);
+ css(el, 'left', rect.left);
+ css(el, 'width', rect.width);
+ css(el, 'height', rect.height);
+ }
+
+ function unsetRect(el) {
+ css(el, 'position', '');
+ css(el, 'top', '');
+ css(el, 'left', '');
+ css(el, 'width', '');
+ css(el, 'height', '');
+ }
+
+ var expando = 'Sortable' + new Date().getTime();
+
+ function AnimationStateManager() {
+ var animationStates = [],
+ animationCallbackId;
+ return {
+ captureAnimationState: function captureAnimationState() {
+ animationStates = [];
+ if (!this.options.animation) return;
+ var children = [].slice.call(this.el.children);
+ children.forEach(function (child) {
+ if (css(child, 'display') === 'none' || child === Sortable.ghost) return;
+ animationStates.push({
+ target: child,
+ rect: getRect(child)
+ });
+
+ var fromRect = _objectSpread({}, animationStates[animationStates.length - 1].rect); // If animating: compensate for current animation
+
+
+ if (child.thisAnimationDuration) {
+ var childMatrix = matrix(child, true);
+
+ if (childMatrix) {
+ fromRect.top -= childMatrix.f;
+ fromRect.left -= childMatrix.e;
+ }
+ }
+
+ child.fromRect = fromRect;
+ });
+ },
+ addAnimationState: function addAnimationState(state) {
+ animationStates.push(state);
+ },
+ removeAnimationState: function removeAnimationState(target) {
+ animationStates.splice(indexOfObject(animationStates, {
+ target: target
+ }), 1);
+ },
+ animateAll: function animateAll(callback) {
+ var _this = this;
+
+ if (!this.options.animation) {
+ clearTimeout(animationCallbackId);
+ if (typeof callback === 'function') callback();
+ return;
+ }
+
+ var animating = false,
+ animationTime = 0;
+ animationStates.forEach(function (state) {
+ var time = 0,
+ target = state.target,
+ fromRect = target.fromRect,
+ toRect = getRect(target),
+ prevFromRect = target.prevFromRect,
+ prevToRect = target.prevToRect,
+ animatingRect = state.rect,
+ targetMatrix = matrix(target, true);
+
+ if (targetMatrix) {
+ // Compensate for current animation
+ toRect.top -= targetMatrix.f;
+ toRect.left -= targetMatrix.e;
+ }
+
+ target.toRect = toRect;
+
+ if (target.thisAnimationDuration) {
+ // Could also check if animatingRect is between fromRect and toRect
+ if (isRectEqual(prevFromRect, toRect) && !isRectEqual(fromRect, toRect) && // Make sure animatingRect is on line between toRect & fromRect
+ (animatingRect.top - toRect.top) / (animatingRect.left - toRect.left) === (fromRect.top - toRect.top) / (fromRect.left - toRect.left)) {
+ // If returning to same place as started from animation and on same axis
+ time = calculateRealTime(animatingRect, prevFromRect, prevToRect, _this.options);
+ }
+ } // if fromRect != toRect: animate
+
+
+ if (!isRectEqual(toRect, fromRect)) {
+ target.prevFromRect = fromRect;
+ target.prevToRect = toRect;
+
+ if (!time) {
+ time = _this.options.animation;
+ }
+
+ _this.animate(target, animatingRect, toRect, time);
+ }
+
+ if (time) {
+ animating = true;
+ animationTime = Math.max(animationTime, time);
+ clearTimeout(target.animationResetTimer);
+ target.animationResetTimer = setTimeout(function () {
+ target.animationTime = 0;
+ target.prevFromRect = null;
+ target.fromRect = null;
+ target.prevToRect = null;
+ target.thisAnimationDuration = null;
+ }, time);
+ target.thisAnimationDuration = time;
+ }
+ });
+ clearTimeout(animationCallbackId);
+
+ if (!animating) {
+ if (typeof callback === 'function') callback();
+ } else {
+ animationCallbackId = setTimeout(function () {
+ if (typeof callback === 'function') callback();
+ }, animationTime);
+ }
+
+ animationStates = [];
+ },
+ animate: function animate(target, currentRect, toRect, duration) {
+ if (duration) {
+ css(target, 'transition', '');
+ css(target, 'transform', '');
+ var elMatrix = matrix(this.el),
+ scaleX = elMatrix && elMatrix.a,
+ scaleY = elMatrix && elMatrix.d,
+ translateX = (currentRect.left - toRect.left) / (scaleX || 1),
+ translateY = (currentRect.top - toRect.top) / (scaleY || 1);
+ target.animatingX = !!translateX;
+ target.animatingY = !!translateY;
+ css(target, 'transform', 'translate3d(' + translateX + 'px,' + translateY + 'px,0)');
+ repaint(target); // repaint
+
+ css(target, 'transition', 'transform ' + duration + 'ms' + (this.options.easing ? ' ' + this.options.easing : ''));
+ css(target, 'transform', 'translate3d(0,0,0)');
+ typeof target.animated === 'number' && clearTimeout(target.animated);
+ target.animated = setTimeout(function () {
+ css(target, 'transition', '');
+ css(target, 'transform', '');
+ target.animated = false;
+ target.animatingX = false;
+ target.animatingY = false;
+ }, duration);
+ }
+ }
+ };
+ }
+
+ function repaint(target) {
+ return target.offsetWidth;
+ }
+
+ function calculateRealTime(animatingRect, fromRect, toRect, options) {
+ return Math.sqrt(Math.pow(fromRect.top - animatingRect.top, 2) + Math.pow(fromRect.left - animatingRect.left, 2)) / Math.sqrt(Math.pow(fromRect.top - toRect.top, 2) + Math.pow(fromRect.left - toRect.left, 2)) * options.animation;
+ }
+
+ var plugins = [];
+ var defaults = {
+ initializeByDefault: true
+ };
+ var PluginManager = {
+ mount: function mount(plugin) {
+ // Set default static properties
+ for (var option in defaults) {
+ if (defaults.hasOwnProperty(option) && !(option in plugin)) {
+ plugin[option] = defaults[option];
+ }
+ }
+
+ plugins.push(plugin);
+ },
+ pluginEvent: function pluginEvent(eventName, sortable, evt) {
+ var _this = this;
+
+ this.eventCanceled = false;
+
+ evt.cancel = function () {
+ _this.eventCanceled = true;
+ };
+
+ var eventNameGlobal = eventName + 'Global';
+ plugins.forEach(function (plugin) {
+ if (!sortable[plugin.pluginName]) return; // Fire global events if it exists in this sortable
+
+ if (sortable[plugin.pluginName][eventNameGlobal]) {
+ sortable[plugin.pluginName][eventNameGlobal](_objectSpread({
+ sortable: sortable
+ }, evt));
+ } // Only fire plugin event if plugin is enabled in this sortable,
+ // and plugin has event defined
+
+
+ if (sortable.options[plugin.pluginName] && sortable[plugin.pluginName][eventName]) {
+ sortable[plugin.pluginName][eventName](_objectSpread({
+ sortable: sortable
+ }, evt));
+ }
+ });
+ },
+ initializePlugins: function initializePlugins(sortable, el, defaults, options) {
+ plugins.forEach(function (plugin) {
+ var pluginName = plugin.pluginName;
+ if (!sortable.options[pluginName] && !plugin.initializeByDefault) return;
+ var initialized = new plugin(sortable, el, sortable.options);
+ initialized.sortable = sortable;
+ initialized.options = sortable.options;
+ sortable[pluginName] = initialized; // Add default options from plugin
+
+ _extends(defaults, initialized.defaults);
+ });
+
+ for (var option in sortable.options) {
+ if (!sortable.options.hasOwnProperty(option)) continue;
+ var modified = this.modifyOption(sortable, option, sortable.options[option]);
+
+ if (typeof modified !== 'undefined') {
+ sortable.options[option] = modified;
+ }
+ }
+ },
+ getEventProperties: function getEventProperties(name, sortable) {
+ var eventProperties = {};
+ plugins.forEach(function (plugin) {
+ if (typeof plugin.eventProperties !== 'function') return;
+
+ _extends(eventProperties, plugin.eventProperties.call(sortable[plugin.pluginName], name));
+ });
+ return eventProperties;
+ },
+ modifyOption: function modifyOption(sortable, name, value) {
+ var modifiedValue;
+ plugins.forEach(function (plugin) {
+ // Plugin must exist on the Sortable
+ if (!sortable[plugin.pluginName]) return; // If static option listener exists for this option, call in the context of the Sortable's instance of this plugin
+
+ if (plugin.optionListeners && typeof plugin.optionListeners[name] === 'function') {
+ modifiedValue = plugin.optionListeners[name].call(sortable[plugin.pluginName], value);
+ }
+ });
+ return modifiedValue;
+ }
+ };
+
+ function dispatchEvent(_ref) {
+ var sortable = _ref.sortable,
+ rootEl = _ref.rootEl,
+ name = _ref.name,
+ targetEl = _ref.targetEl,
+ cloneEl = _ref.cloneEl,
+ toEl = _ref.toEl,
+ fromEl = _ref.fromEl,
+ oldIndex = _ref.oldIndex,
+ newIndex = _ref.newIndex,
+ oldDraggableIndex = _ref.oldDraggableIndex,
+ newDraggableIndex = _ref.newDraggableIndex,
+ originalEvent = _ref.originalEvent,
+ putSortable = _ref.putSortable,
+ extraEventProperties = _ref.extraEventProperties;
+ sortable = sortable || rootEl && rootEl[expando];
+ if (!sortable) return;
+ var evt,
+ options = sortable.options,
+ onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); // Support for new CustomEvent feature
+
+ if (window.CustomEvent && !IE11OrLess && !Edge) {
+ evt = new CustomEvent(name, {
+ bubbles: true,
+ cancelable: true
+ });
+ } else {
+ evt = document.createEvent('Event');
+ evt.initEvent(name, true, true);
+ }
+
+ evt.to = toEl || rootEl;
+ evt.from = fromEl || rootEl;
+ evt.item = targetEl || rootEl;
+ evt.clone = cloneEl;
+ evt.oldIndex = oldIndex;
+ evt.newIndex = newIndex;
+ evt.oldDraggableIndex = oldDraggableIndex;
+ evt.newDraggableIndex = newDraggableIndex;
+ evt.originalEvent = originalEvent;
+ evt.pullMode = putSortable ? putSortable.lastPutMode : undefined;
+
+ var allEventProperties = _objectSpread({}, extraEventProperties, PluginManager.getEventProperties(name, sortable));
+
+ for (var option in allEventProperties) {
+ evt[option] = allEventProperties[option];
+ }
+
+ if (rootEl) {
+ rootEl.dispatchEvent(evt);
+ }
+
+ if (options[onName]) {
+ options[onName].call(sortable, evt);
+ }
+ }
+
+ var pluginEvent = function pluginEvent(eventName, sortable) {
+ var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
+ originalEvent = _ref.evt,
+ data = _objectWithoutProperties(_ref, ["evt"]);
+
+ PluginManager.pluginEvent.bind(Sortable)(eventName, sortable, _objectSpread({
+ dragEl: dragEl,
+ parentEl: parentEl,
+ ghostEl: ghostEl,
+ rootEl: rootEl,
+ nextEl: nextEl,
+ lastDownEl: lastDownEl,
+ cloneEl: cloneEl,
+ cloneHidden: cloneHidden,
+ dragStarted: moved,
+ putSortable: putSortable,
+ activeSortable: Sortable.active,
+ originalEvent: originalEvent,
+ oldIndex: oldIndex,
+ oldDraggableIndex: oldDraggableIndex,
+ newIndex: newIndex,
+ newDraggableIndex: newDraggableIndex,
+ hideGhostForTarget: _hideGhostForTarget,
+ unhideGhostForTarget: _unhideGhostForTarget,
+ cloneNowHidden: function cloneNowHidden() {
+ cloneHidden = true;
+ },
+ cloneNowShown: function cloneNowShown() {
+ cloneHidden = false;
+ },
+ dispatchSortableEvent: function dispatchSortableEvent(name) {
+ _dispatchEvent({
+ sortable: sortable,
+ name: name,
+ originalEvent: originalEvent
+ });
+ }
+ }, data));
+ };
+
+ function _dispatchEvent(info) {
+ dispatchEvent(_objectSpread({
+ putSortable: putSortable,
+ cloneEl: cloneEl,
+ targetEl: dragEl,
+ rootEl: rootEl,
+ oldIndex: oldIndex,
+ oldDraggableIndex: oldDraggableIndex,
+ newIndex: newIndex,
+ newDraggableIndex: newDraggableIndex
+ }, info));
+ }
+
+ var dragEl,
+ parentEl,
+ ghostEl,
+ rootEl,
+ nextEl,
+ lastDownEl,
+ cloneEl,
+ cloneHidden,
+ oldIndex,
+ newIndex,
+ oldDraggableIndex,
+ newDraggableIndex,
+ activeGroup,
+ putSortable,
+ awaitingDragStarted = false,
+ ignoreNextClick = false,
+ sortables = [],
+ tapEvt,
+ touchEvt,
+ lastDx,
+ lastDy,
+ tapDistanceLeft,
+ tapDistanceTop,
+ moved,
+ lastTarget,
+ lastDirection,
+ pastFirstInvertThresh = false,
+ isCircumstantialInvert = false,
+ targetMoveDistance,
+ // For positioning ghost absolutely
+ ghostRelativeParent,
+ ghostRelativeParentInitialScroll = [],
+ // (left, top)
+ _silent = false,
+ savedInputChecked = [];
+ /** @const */
+
+ var documentExists = typeof document !== 'undefined',
+ PositionGhostAbsolutely = IOS,
+ CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float',
+ // This will not pass for IE9, because IE9 DnD only works on anchors
+ supportDraggable = documentExists && !ChromeForAndroid && !IOS && 'draggable' in document.createElement('div'),
+ supportCssPointerEvents = function () {
+ if (!documentExists) return; // false when <= IE11
+
+ if (IE11OrLess) {
+ return false;
+ }
+
+ var el = document.createElement('x');
+ el.style.cssText = 'pointer-events:auto';
+ return el.style.pointerEvents === 'auto';
+ }(),
+ _detectDirection = function _detectDirection(el, options) {
+ var elCSS = css(el),
+ elWidth = parseInt(elCSS.width) - parseInt(elCSS.paddingLeft) - parseInt(elCSS.paddingRight) - parseInt(elCSS.borderLeftWidth) - parseInt(elCSS.borderRightWidth),
+ child1 = getChild(el, 0, options),
+ child2 = getChild(el, 1, options),
+ firstChildCSS = child1 && css(child1),
+ secondChildCSS = child2 && css(child2),
+ firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + getRect(child1).width,
+ secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + getRect(child2).width;
+
+ if (elCSS.display === 'flex') {
+ return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse' ? 'vertical' : 'horizontal';
+ }
+
+ if (elCSS.display === 'grid') {
+ return elCSS.gridTemplateColumns.split(' ').length <= 1 ? 'vertical' : 'horizontal';
+ }
+
+ if (child1 && firstChildCSS["float"] && firstChildCSS["float"] !== 'none') {
+ var touchingSideChild2 = firstChildCSS["float"] === 'left' ? 'left' : 'right';
+ return child2 && (secondChildCSS.clear === 'both' || secondChildCSS.clear === touchingSideChild2) ? 'vertical' : 'horizontal';
+ }
+
+ return child1 && (firstChildCSS.display === 'block' || firstChildCSS.display === 'flex' || firstChildCSS.display === 'table' || firstChildCSS.display === 'grid' || firstChildWidth >= elWidth && elCSS[CSSFloatProperty] === 'none' || child2 && elCSS[CSSFloatProperty] === 'none' && firstChildWidth + secondChildWidth > elWidth) ? 'vertical' : 'horizontal';
+ },
+ _dragElInRowColumn = function _dragElInRowColumn(dragRect, targetRect, vertical) {
+ var dragElS1Opp = vertical ? dragRect.left : dragRect.top,
+ dragElS2Opp = vertical ? dragRect.right : dragRect.bottom,
+ dragElOppLength = vertical ? dragRect.width : dragRect.height,
+ targetS1Opp = vertical ? targetRect.left : targetRect.top,
+ targetS2Opp = vertical ? targetRect.right : targetRect.bottom,
+ targetOppLength = vertical ? targetRect.width : targetRect.height;
+ return dragElS1Opp === targetS1Opp || dragElS2Opp === targetS2Opp || dragElS1Opp + dragElOppLength / 2 === targetS1Opp + targetOppLength / 2;
+ },
+
+ /**
+ * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold.
+ * @param {Number} x X position
+ * @param {Number} y Y position
+ * @return {HTMLElement} Element of the first found nearest Sortable
+ */
+ _detectNearestEmptySortable = function _detectNearestEmptySortable(x, y) {
+ var ret;
+ sortables.some(function (sortable) {
+ if (lastChild(sortable)) return;
+ var rect = getRect(sortable),
+ threshold = sortable[expando].options.emptyInsertThreshold,
+ insideHorizontally = x >= rect.left - threshold && x <= rect.right + threshold,
+ insideVertically = y >= rect.top - threshold && y <= rect.bottom + threshold;
+
+ if (threshold && insideHorizontally && insideVertically) {
+ return ret = sortable;
+ }
+ });
+ return ret;
+ },
+ _prepareGroup = function _prepareGroup(options) {
+ function toFn(value, pull) {
+ return function (to, from, dragEl, evt) {
+ var sameGroup = to.options.group.name && from.options.group.name && to.options.group.name === from.options.group.name;
+
+ if (value == null && (pull || sameGroup)) {
+ // Default pull value
+ // Default pull and put value if same group
+ return true;
+ } else if (value == null || value === false) {
+ return false;
+ } else if (pull && value === 'clone') {
+ return value;
+ } else if (typeof value === 'function') {
+ return toFn(value(to, from, dragEl, evt), pull)(to, from, dragEl, evt);
+ } else {
+ var otherGroup = (pull ? to : from).options.group.name;
+ return value === true || typeof value === 'string' && value === otherGroup || value.join && value.indexOf(otherGroup) > -1;
+ }
+ };
+ }
+
+ var group = {};
+ var originalGroup = options.group;
+
+ if (!originalGroup || _typeof(originalGroup) != 'object') {
+ originalGroup = {
+ name: originalGroup
+ };
+ }
+
+ group.name = originalGroup.name;
+ group.checkPull = toFn(originalGroup.pull, true);
+ group.checkPut = toFn(originalGroup.put);
+ group.revertClone = originalGroup.revertClone;
+ options.group = group;
+ },
+ _hideGhostForTarget = function _hideGhostForTarget() {
+ if (!supportCssPointerEvents && ghostEl) {
+ css(ghostEl, 'display', 'none');
+ }
+ },
+ _unhideGhostForTarget = function _unhideGhostForTarget() {
+ if (!supportCssPointerEvents && ghostEl) {
+ css(ghostEl, 'display', '');
+ }
+ }; // #1184 fix - Prevent click event on fallback if dragged but item not changed position
+
+
+ if (documentExists) {
+ document.addEventListener('click', function (evt) {
+ if (ignoreNextClick) {
+ evt.preventDefault();
+ evt.stopPropagation && evt.stopPropagation();
+ evt.stopImmediatePropagation && evt.stopImmediatePropagation();
+ ignoreNextClick = false;
+ return false;
+ }
+ }, true);
+ }
+
+ var nearestEmptyInsertDetectEvent = function nearestEmptyInsertDetectEvent(evt) {
+ if (dragEl) {
+ evt = evt.touches ? evt.touches[0] : evt;
+
+ var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY);
+
+ if (nearest) {
+ // Create imitation event
+ var event = {};
+
+ for (var i in evt) {
+ if (evt.hasOwnProperty(i)) {
+ event[i] = evt[i];
+ }
+ }
+
+ event.target = event.rootEl = nearest;
+ event.preventDefault = void 0;
+ event.stopPropagation = void 0;
+
+ nearest[expando]._onDragOver(event);
+ }
+ }
+ };
+
+ var _checkOutsideTargetEl = function _checkOutsideTargetEl(evt) {
+ if (dragEl) {
+ dragEl.parentNode[expando]._isOutsideThisEl(evt.target);
+ }
+ };
+ /**
+ * @class Sortable
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+
+
+ function Sortable(el, options) {
+ if (!(el && el.nodeType && el.nodeType === 1)) {
+ throw "Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(el));
+ }
+
+ this.el = el; // root element
+
+ this.options = options = _extends({}, options); // Export instance
+
+ el[expando] = this;
+ var defaults = {
+ group: null,
+ sort: true,
+ disabled: false,
+ store: null,
+ handle: null,
+ draggable: /^[uo]l$/i.test(el.nodeName) ? '>li' : '>*',
+ swapThreshold: 1,
+ // percentage; 0 <= x <= 1
+ invertSwap: false,
+ // invert always
+ invertedSwapThreshold: null,
+ // will be set to same as swapThreshold if default
+ removeCloneOnHide: true,
+ direction: function direction() {
+ return _detectDirection(el, this.options);
+ },
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ dragClass: 'sortable-drag',
+ ignore: 'a, img',
+ filter: null,
+ preventOnFilter: true,
+ animation: 0,
+ easing: null,
+ setData: function setData(dataTransfer, dragEl) {
+ dataTransfer.setData('Text', dragEl.textContent);
+ },
+ dropBubble: false,
+ dragoverBubble: false,
+ dataIdAttr: 'data-id',
+ delay: 0,
+ delayOnTouchOnly: false,
+ touchStartThreshold: (Number.parseInt ? Number : window).parseInt(window.devicePixelRatio, 10) || 1,
+ forceFallback: false,
+ fallbackClass: 'sortable-fallback',
+ fallbackOnBody: false,
+ fallbackTolerance: 0,
+ fallbackOffset: {
+ x: 0,
+ y: 0
+ },
+ supportPointer: Sortable.supportPointer !== false && 'PointerEvent' in window,
+ emptyInsertThreshold: 5
+ };
+ PluginManager.initializePlugins(this, el, defaults); // Set default options
+
+ for (var name in defaults) {
+ !(name in options) && (options[name] = defaults[name]);
+ }
+
+ _prepareGroup(options); // Bind all private methods
+
+
+ for (var fn in this) {
+ if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+ this[fn] = this[fn].bind(this);
+ }
+ } // Setup drag mode
+
+
+ this.nativeDraggable = options.forceFallback ? false : supportDraggable;
+
+ if (this.nativeDraggable) {
+ // Touch start threshold cannot be greater than the native dragstart threshold
+ this.options.touchStartThreshold = 1;
+ } // Bind events
+
+
+ if (options.supportPointer) {
+ on(el, 'pointerdown', this._onTapStart);
+ } else {
+ on(el, 'mousedown', this._onTapStart);
+ on(el, 'touchstart', this._onTapStart);
+ }
+
+ if (this.nativeDraggable) {
+ on(el, 'dragover', this);
+ on(el, 'dragenter', this);
+ }
+
+ sortables.push(this.el); // Restore sorting
+
+ options.store && options.store.get && this.sort(options.store.get(this) || []); // Add animation state manager
+
+ _extends(this, AnimationStateManager());
+ }
+
+ Sortable.prototype =
+ /** @lends Sortable.prototype */
+ {
+ constructor: Sortable,
+ _isOutsideThisEl: function _isOutsideThisEl(target) {
+ if (!this.el.contains(target) && target !== this.el) {
+ lastTarget = null;
+ }
+ },
+ _getDirection: function _getDirection(evt, target) {
+ return typeof this.options.direction === 'function' ? this.options.direction.call(this, evt, target, dragEl) : this.options.direction;
+ },
+ _onTapStart: function _onTapStart(
+ /** Event|TouchEvent */
+ evt) {
+ if (!evt.cancelable) return;
+
+ var _this = this,
+ el = this.el,
+ options = this.options,
+ preventOnFilter = options.preventOnFilter,
+ type = evt.type,
+ touch = evt.touches && evt.touches[0] || evt.pointerType && evt.pointerType === 'touch' && evt,
+ target = (touch || evt).target,
+ originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0] || evt.composedPath && evt.composedPath()[0]) || target,
+ filter = options.filter;
+
+ _saveInputCheckedState(el); // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
+
+
+ if (dragEl) {
+ return;
+ }
+
+ if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) {
+ return; // only left button and enabled
+ } // cancel dnd if original target is content editable
+
+
+ if (originalTarget.isContentEditable) {
+ return;
+ }
+
+ target = closest(target, options.draggable, el, false);
+
+ if (target && target.animated) {
+ return;
+ }
+
+ if (lastDownEl === target) {
+ // Ignoring duplicate `down`
+ return;
+ } // Get the index of the dragged element within its parent
+
+
+ oldIndex = index(target);
+ oldDraggableIndex = index(target, options.draggable); // Check filter
+
+ if (typeof filter === 'function') {
+ if (filter.call(this, evt, target, this)) {
+ _dispatchEvent({
+ sortable: _this,
+ rootEl: originalTarget,
+ name: 'filter',
+ targetEl: target,
+ toEl: el,
+ fromEl: el
+ });
+
+ pluginEvent('filter', _this, {
+ evt: evt
+ });
+ preventOnFilter && evt.cancelable && evt.preventDefault();
+ return; // cancel dnd
+ }
+ } else if (filter) {
+ filter = filter.split(',').some(function (criteria) {
+ criteria = closest(originalTarget, criteria.trim(), el, false);
+
+ if (criteria) {
+ _dispatchEvent({
+ sortable: _this,
+ rootEl: criteria,
+ name: 'filter',
+ targetEl: target,
+ fromEl: el,
+ toEl: el
+ });
+
+ pluginEvent('filter', _this, {
+ evt: evt
+ });
+ return true;
+ }
+ });
+
+ if (filter) {
+ preventOnFilter && evt.cancelable && evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+
+ if (options.handle && !closest(originalTarget, options.handle, el, false)) {
+ return;
+ } // Prepare `dragstart`
+
+
+ this._prepareDragStart(evt, touch, target);
+ },
+ _prepareDragStart: function _prepareDragStart(
+ /** Event */
+ evt,
+ /** Touch */
+ touch,
+ /** HTMLElement */
+ target) {
+ var _this = this,
+ el = _this.el,
+ options = _this.options,
+ ownerDocument = el.ownerDocument,
+ dragStartFn;
+
+ if (target && !dragEl && target.parentNode === el) {
+ var dragRect = getRect(target);
+ rootEl = el;
+ dragEl = target;
+ parentEl = dragEl.parentNode;
+ nextEl = dragEl.nextSibling;
+ lastDownEl = target;
+ activeGroup = options.group;
+ Sortable.dragged = dragEl;
+ tapEvt = {
+ target: dragEl,
+ clientX: (touch || evt).clientX,
+ clientY: (touch || evt).clientY
+ };
+ tapDistanceLeft = tapEvt.clientX - dragRect.left;
+ tapDistanceTop = tapEvt.clientY - dragRect.top;
+ this._lastX = (touch || evt).clientX;
+ this._lastY = (touch || evt).clientY;
+ dragEl.style['will-change'] = 'all';
+
+ dragStartFn = function dragStartFn() {
+ pluginEvent('delayEnded', _this, {
+ evt: evt
+ });
+
+ if (Sortable.eventCanceled) {
+ _this._onDrop();
+
+ return;
+ } // Delayed drag has been triggered
+ // we can re-enable the events: touchmove/mousemove
+
+
+ _this._disableDelayedDragEvents();
+
+ if (!FireFox && _this.nativeDraggable) {
+ dragEl.draggable = true;
+ } // Bind the events: dragstart/dragend
+
+
+ _this._triggerDragStart(evt, touch); // Drag start event
+
+
+ _dispatchEvent({
+ sortable: _this,
+ name: 'choose',
+ originalEvent: evt
+ }); // Chosen item
+
+
+ toggleClass(dragEl, options.chosenClass, true);
+ }; // Disable "draggable"
+
+
+ options.ignore.split(',').forEach(function (criteria) {
+ find(dragEl, criteria.trim(), _disableDraggable);
+ });
+ on(ownerDocument, 'dragover', nearestEmptyInsertDetectEvent);
+ on(ownerDocument, 'mousemove', nearestEmptyInsertDetectEvent);
+ on(ownerDocument, 'touchmove', nearestEmptyInsertDetectEvent);
+ on(ownerDocument, 'mouseup', _this._onDrop);
+ on(ownerDocument, 'touchend', _this._onDrop);
+ on(ownerDocument, 'touchcancel', _this._onDrop); // Make dragEl draggable (must be before delay for FireFox)
+
+ if (FireFox && this.nativeDraggable) {
+ this.options.touchStartThreshold = 4;
+ dragEl.draggable = true;
+ }
+
+ pluginEvent('delayStart', this, {
+ evt: evt
+ }); // Delay is impossible for native DnD in Edge or IE
+
+ if (options.delay && (!options.delayOnTouchOnly || touch) && (!this.nativeDraggable || !(Edge || IE11OrLess))) {
+ if (Sortable.eventCanceled) {
+ this._onDrop();
+
+ return;
+ } // If the user moves the pointer or let go the click or touch
+ // before the delay has been reached:
+ // disable the delayed drag
+
+
+ on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
+ on(ownerDocument, 'touchend', _this._disableDelayedDrag);
+ on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
+ on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler);
+ on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler);
+ options.supportPointer && on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler);
+ _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
+ } else {
+ dragStartFn();
+ }
+ }
+ },
+ _delayedDragTouchMoveHandler: function _delayedDragTouchMoveHandler(
+ /** TouchEvent|PointerEvent **/
+ e) {
+ var touch = e.touches ? e.touches[0] : e;
+
+ if (Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) >= Math.floor(this.options.touchStartThreshold / (this.nativeDraggable && window.devicePixelRatio || 1))) {
+ this._disableDelayedDrag();
+ }
+ },
+ _disableDelayedDrag: function _disableDelayedDrag() {
+ dragEl && _disableDraggable(dragEl);
+ clearTimeout(this._dragStartTimer);
+
+ this._disableDelayedDragEvents();
+ },
+ _disableDelayedDragEvents: function _disableDelayedDragEvents() {
+ var ownerDocument = this.el.ownerDocument;
+ off(ownerDocument, 'mouseup', this._disableDelayedDrag);
+ off(ownerDocument, 'touchend', this._disableDelayedDrag);
+ off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
+ off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler);
+ off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler);
+ off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler);
+ },
+ _triggerDragStart: function _triggerDragStart(
+ /** Event */
+ evt,
+ /** Touch */
+ touch) {
+ touch = touch || evt.pointerType == 'touch' && evt;
+
+ if (!this.nativeDraggable || touch) {
+ if (this.options.supportPointer) {
+ on(document, 'pointermove', this._onTouchMove);
+ } else if (touch) {
+ on(document, 'touchmove', this._onTouchMove);
+ } else {
+ on(document, 'mousemove', this._onTouchMove);
+ }
+ } else {
+ on(dragEl, 'dragend', this);
+ on(rootEl, 'dragstart', this._onDragStart);
+ }
+
+ try {
+ if (document.selection) {
+ // Timeout neccessary for IE9
+ _nextTick(function () {
+ document.selection.empty();
+ });
+ } else {
+ window.getSelection().removeAllRanges();
+ }
+ } catch (err) {}
+ },
+ _dragStarted: function _dragStarted(fallback, evt) {
+
+ awaitingDragStarted = false;
+
+ if (rootEl && dragEl) {
+ pluginEvent('dragStarted', this, {
+ evt: evt
+ });
+
+ if (this.nativeDraggable) {
+ on(document, 'dragover', _checkOutsideTargetEl);
+ }
+
+ var options = this.options; // Apply effect
+
+ !fallback && toggleClass(dragEl, options.dragClass, false);
+ toggleClass(dragEl, options.ghostClass, true);
+ Sortable.active = this;
+ fallback && this._appendGhost(); // Drag start event
+
+ _dispatchEvent({
+ sortable: this,
+ name: 'start',
+ originalEvent: evt
+ });
+ } else {
+ this._nulling();
+ }
+ },
+ _emulateDragOver: function _emulateDragOver() {
+ if (touchEvt) {
+ this._lastX = touchEvt.clientX;
+ this._lastY = touchEvt.clientY;
+
+ _hideGhostForTarget();
+
+ var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
+ var parent = target;
+
+ while (target && target.shadowRoot) {
+ target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
+ if (target === parent) break;
+ parent = target;
+ }
+
+ dragEl.parentNode[expando]._isOutsideThisEl(target);
+
+ if (parent) {
+ do {
+ if (parent[expando]) {
+ var inserted = void 0;
+ inserted = parent[expando]._onDragOver({
+ clientX: touchEvt.clientX,
+ clientY: touchEvt.clientY,
+ target: target,
+ rootEl: parent
+ });
+
+ if (inserted && !this.options.dragoverBubble) {
+ break;
+ }
+ }
+
+ target = parent; // store last element
+ }
+ /* jshint boss:true */
+ while (parent = parent.parentNode);
+ }
+
+ _unhideGhostForTarget();
+ }
+ },
+ _onTouchMove: function _onTouchMove(
+ /**TouchEvent*/
+ evt) {
+ if (tapEvt) {
+ var options = this.options,
+ fallbackTolerance = options.fallbackTolerance,
+ fallbackOffset = options.fallbackOffset,
+ touch = evt.touches ? evt.touches[0] : evt,
+ ghostMatrix = ghostEl && matrix(ghostEl, true),
+ scaleX = ghostEl && ghostMatrix && ghostMatrix.a,
+ scaleY = ghostEl && ghostMatrix && ghostMatrix.d,
+ relativeScrollOffset = PositionGhostAbsolutely && ghostRelativeParent && getRelativeScrollOffset(ghostRelativeParent),
+ dx = (touch.clientX - tapEvt.clientX + fallbackOffset.x) / (scaleX || 1) + (relativeScrollOffset ? relativeScrollOffset[0] - ghostRelativeParentInitialScroll[0] : 0) / (scaleX || 1),
+ dy = (touch.clientY - tapEvt.clientY + fallbackOffset.y) / (scaleY || 1) + (relativeScrollOffset ? relativeScrollOffset[1] - ghostRelativeParentInitialScroll[1] : 0) / (scaleY || 1); // only set the status to dragging, when we are actually dragging
+
+ if (!Sortable.active && !awaitingDragStarted) {
+ if (fallbackTolerance && Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) < fallbackTolerance) {
+ return;
+ }
+
+ this._onDragStart(evt, true);
+ }
+
+ if (ghostEl) {
+ if (ghostMatrix) {
+ ghostMatrix.e += dx - (lastDx || 0);
+ ghostMatrix.f += dy - (lastDy || 0);
+ } else {
+ ghostMatrix = {
+ a: 1,
+ b: 0,
+ c: 0,
+ d: 1,
+ e: dx,
+ f: dy
+ };
+ }
+
+ var cssMatrix = "matrix(".concat(ghostMatrix.a, ",").concat(ghostMatrix.b, ",").concat(ghostMatrix.c, ",").concat(ghostMatrix.d, ",").concat(ghostMatrix.e, ",").concat(ghostMatrix.f, ")");
+ css(ghostEl, 'webkitTransform', cssMatrix);
+ css(ghostEl, 'mozTransform', cssMatrix);
+ css(ghostEl, 'msTransform', cssMatrix);
+ css(ghostEl, 'transform', cssMatrix);
+ lastDx = dx;
+ lastDy = dy;
+ touchEvt = touch;
+ }
+
+ evt.cancelable && evt.preventDefault();
+ }
+ },
+ _appendGhost: function _appendGhost() {
+ // Bug if using scale(): https://stackoverflow.com/questions/2637058
+ // Not being adjusted for
+ if (!ghostEl) {
+ var container = this.options.fallbackOnBody ? document.body : rootEl,
+ rect = getRect(dragEl, true, PositionGhostAbsolutely, true, container),
+ options = this.options; // Position absolutely
+
+ if (PositionGhostAbsolutely) {
+ // Get relatively positioned parent
+ ghostRelativeParent = container;
+
+ while (css(ghostRelativeParent, 'position') === 'static' && css(ghostRelativeParent, 'transform') === 'none' && ghostRelativeParent !== document) {
+ ghostRelativeParent = ghostRelativeParent.parentNode;
+ }
+
+ if (ghostRelativeParent !== document.body && ghostRelativeParent !== document.documentElement) {
+ if (ghostRelativeParent === document) ghostRelativeParent = getWindowScrollingElement();
+ rect.top += ghostRelativeParent.scrollTop;
+ rect.left += ghostRelativeParent.scrollLeft;
+ } else {
+ ghostRelativeParent = getWindowScrollingElement();
+ }
+
+ ghostRelativeParentInitialScroll = getRelativeScrollOffset(ghostRelativeParent);
+ }
+
+ ghostEl = dragEl.cloneNode(true);
+ toggleClass(ghostEl, options.ghostClass, false);
+ toggleClass(ghostEl, options.fallbackClass, true);
+ toggleClass(ghostEl, options.dragClass, true);
+ css(ghostEl, 'transition', '');
+ css(ghostEl, 'transform', '');
+ css(ghostEl, 'box-sizing', 'border-box');
+ css(ghostEl, 'margin', 0);
+ css(ghostEl, 'top', rect.top);
+ css(ghostEl, 'left', rect.left);
+ css(ghostEl, 'width', rect.width);
+ css(ghostEl, 'height', rect.height);
+ css(ghostEl, 'opacity', '0.8');
+ css(ghostEl, 'position', PositionGhostAbsolutely ? 'absolute' : 'fixed');
+ css(ghostEl, 'zIndex', '100000');
+ css(ghostEl, 'pointerEvents', 'none');
+ Sortable.ghost = ghostEl;
+ container.appendChild(ghostEl); // Set transform-origin
+
+ css(ghostEl, 'transform-origin', tapDistanceLeft / parseInt(ghostEl.style.width) * 100 + '% ' + tapDistanceTop / parseInt(ghostEl.style.height) * 100 + '%');
+ }
+ },
+ _onDragStart: function _onDragStart(
+ /**Event*/
+ evt,
+ /**boolean*/
+ fallback) {
+ var _this = this;
+
+ var dataTransfer = evt.dataTransfer;
+ var options = _this.options;
+ pluginEvent('dragStart', this, {
+ evt: evt
+ });
+
+ if (Sortable.eventCanceled) {
+ this._onDrop();
+
+ return;
+ }
+
+ pluginEvent('setupClone', this);
+
+ if (!Sortable.eventCanceled) {
+ cloneEl = clone(dragEl);
+ cloneEl.draggable = false;
+ cloneEl.style['will-change'] = '';
+
+ this._hideClone();
+
+ toggleClass(cloneEl, this.options.chosenClass, false);
+ Sortable.clone = cloneEl;
+ } // #1143: IFrame support workaround
+
+
+ _this.cloneId = _nextTick(function () {
+ pluginEvent('clone', _this);
+ if (Sortable.eventCanceled) return;
+
+ if (!_this.options.removeCloneOnHide) {
+ rootEl.insertBefore(cloneEl, dragEl);
+ }
+
+ _this._hideClone();
+
+ _dispatchEvent({
+ sortable: _this,
+ name: 'clone'
+ });
+ });
+ !fallback && toggleClass(dragEl, options.dragClass, true); // Set proper drop events
+
+ if (fallback) {
+ ignoreNextClick = true;
+ _this._loopId = setInterval(_this._emulateDragOver, 50);
+ } else {
+ // Undo what was set in _prepareDragStart before drag started
+ off(document, 'mouseup', _this._onDrop);
+ off(document, 'touchend', _this._onDrop);
+ off(document, 'touchcancel', _this._onDrop);
+
+ if (dataTransfer) {
+ dataTransfer.effectAllowed = 'move';
+ options.setData && options.setData.call(_this, dataTransfer, dragEl);
+ }
+
+ on(document, 'drop', _this); // #1276 fix:
+
+ css(dragEl, 'transform', 'translateZ(0)');
+ }
+
+ awaitingDragStarted = true;
+ _this._dragStartId = _nextTick(_this._dragStarted.bind(_this, fallback, evt));
+ on(document, 'selectstart', _this);
+ moved = true;
+
+ if (Safari) {
+ css(document.body, 'user-select', 'none');
+ }
+ },
+ // Returns true - if no further action is needed (either inserted or another condition)
+ _onDragOver: function _onDragOver(
+ /**Event*/
+ evt) {
+ var el = this.el,
+ target = evt.target,
+ dragRect,
+ targetRect,
+ revert,
+ options = this.options,
+ group = options.group,
+ activeSortable = Sortable.active,
+ isOwner = activeGroup === group,
+ canSort = options.sort,
+ fromSortable = putSortable || activeSortable,
+ vertical,
+ _this = this,
+ completedFired = false;
+
+ if (_silent) return;
+
+ function dragOverEvent(name, extra) {
+ pluginEvent(name, _this, _objectSpread({
+ evt: evt,
+ isOwner: isOwner,
+ axis: vertical ? 'vertical' : 'horizontal',
+ revert: revert,
+ dragRect: dragRect,
+ targetRect: targetRect,
+ canSort: canSort,
+ fromSortable: fromSortable,
+ target: target,
+ completed: completed,
+ onMove: function onMove(target, after) {
+ return _onMove(rootEl, el, dragEl, dragRect, target, getRect(target), evt, after);
+ },
+ changed: changed
+ }, extra));
+ } // Capture animation state
+
+
+ function capture() {
+ dragOverEvent('dragOverAnimationCapture');
+
+ _this.captureAnimationState();
+
+ if (_this !== fromSortable) {
+ fromSortable.captureAnimationState();
+ }
+ } // Return invocation when dragEl is inserted (or completed)
+
+
+ function completed(insertion) {
+ dragOverEvent('dragOverCompleted', {
+ insertion: insertion
+ });
+
+ if (insertion) {
+ // Clones must be hidden before folding animation to capture dragRectAbsolute properly
+ if (isOwner) {
+ activeSortable._hideClone();
+ } else {
+ activeSortable._showClone(_this);
+ }
+
+ if (_this !== fromSortable) {
+ // Set ghost class to new sortable's ghost class
+ toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : activeSortable.options.ghostClass, false);
+ toggleClass(dragEl, options.ghostClass, true);
+ }
+
+ if (putSortable !== _this && _this !== Sortable.active) {
+ putSortable = _this;
+ } else if (_this === Sortable.active && putSortable) {
+ putSortable = null;
+ } // Animation
+
+
+ if (fromSortable === _this) {
+ _this._ignoreWhileAnimating = target;
+ }
+
+ _this.animateAll(function () {
+ dragOverEvent('dragOverAnimationComplete');
+ _this._ignoreWhileAnimating = null;
+ });
+
+ if (_this !== fromSortable) {
+ fromSortable.animateAll();
+ fromSortable._ignoreWhileAnimating = null;
+ }
+ } // Null lastTarget if it is not inside a previously swapped element
+
+
+ if (target === dragEl && !dragEl.animated || target === el && !target.animated) {
+ lastTarget = null;
+ } // no bubbling and not fallback
+
+
+ if (!options.dragoverBubble && !evt.rootEl && target !== document) {
+ dragEl.parentNode[expando]._isOutsideThisEl(evt.target); // Do not detect for empty insert if already inserted
+
+
+ !insertion && nearestEmptyInsertDetectEvent(evt);
+ }
+
+ !options.dragoverBubble && evt.stopPropagation && evt.stopPropagation();
+ return completedFired = true;
+ } // Call when dragEl has been inserted
+
+
+ function changed() {
+ newIndex = index(dragEl);
+ newDraggableIndex = index(dragEl, options.draggable);
+
+ _dispatchEvent({
+ sortable: _this,
+ name: 'change',
+ toEl: el,
+ newIndex: newIndex,
+ newDraggableIndex: newDraggableIndex,
+ originalEvent: evt
+ });
+ }
+
+ if (evt.preventDefault !== void 0) {
+ evt.cancelable && evt.preventDefault();
+ }
+
+ target = closest(target, options.draggable, el, true);
+ dragOverEvent('dragOver');
+ if (Sortable.eventCanceled) return completedFired;
+
+ if (dragEl.contains(evt.target) || target.animated && target.animatingX && target.animatingY || _this._ignoreWhileAnimating === target) {
+ return completed(false);
+ }
+
+ ignoreNextClick = false;
+
+ if (activeSortable && !options.disabled && (isOwner ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
+ : putSortable === this || (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && group.checkPut(this, activeSortable, dragEl, evt))) {
+ vertical = this._getDirection(evt, target) === 'vertical';
+ dragRect = getRect(dragEl);
+ dragOverEvent('dragOverValid');
+ if (Sortable.eventCanceled) return completedFired;
+
+ if (revert) {
+ parentEl = rootEl; // actualization
+
+ capture();
+
+ this._hideClone();
+
+ dragOverEvent('revert');
+
+ if (!Sortable.eventCanceled) {
+ if (nextEl) {
+ rootEl.insertBefore(dragEl, nextEl);
+ } else {
+ rootEl.appendChild(dragEl);
+ }
+ }
+
+ return completed(true);
+ }
+
+ var elLastChild = lastChild(el, options.draggable);
+
+ if (!elLastChild || _ghostIsLast(evt, vertical, this) && !elLastChild.animated) {
+ // If already at end of list: Do not insert
+ if (elLastChild === dragEl) {
+ return completed(false);
+ } // assign target only if condition is true
+
+
+ if (elLastChild && el === evt.target) {
+ target = elLastChild;
+ }
+
+ if (target) {
+ targetRect = getRect(target);
+ }
+
+ if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
+ capture();
+ el.appendChild(dragEl);
+ parentEl = el; // actualization
+
+ changed();
+ return completed(true);
+ }
+ } else if (target.parentNode === el) {
+ targetRect = getRect(target);
+ var direction = 0,
+ targetBeforeFirstSwap,
+ differentLevel = dragEl.parentNode !== el,
+ differentRowCol = !_dragElInRowColumn(dragEl.animated && dragEl.toRect || dragRect, target.animated && target.toRect || targetRect, vertical),
+ side1 = vertical ? 'top' : 'left',
+ scrolledPastTop = isScrolledPast(target, 'top', 'top') || isScrolledPast(dragEl, 'top', 'top'),
+ scrollBefore = scrolledPastTop ? scrolledPastTop.scrollTop : void 0;
+
+ if (lastTarget !== target) {
+ targetBeforeFirstSwap = targetRect[side1];
+ pastFirstInvertThresh = false;
+ isCircumstantialInvert = !differentRowCol && options.invertSwap || differentLevel;
+ }
+
+ direction = _getSwapDirection(evt, target, targetRect, vertical, differentRowCol ? 1 : options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, isCircumstantialInvert, lastTarget === target);
+ var sibling;
+
+ if (direction !== 0) {
+ // Check if target is beside dragEl in respective direction (ignoring hidden elements)
+ var dragIndex = index(dragEl);
+
+ do {
+ dragIndex -= direction;
+ sibling = parentEl.children[dragIndex];
+ } while (sibling && (css(sibling, 'display') === 'none' || sibling === ghostEl));
+ } // If dragEl is already beside target: Do not insert
+
+
+ if (direction === 0 || sibling === target) {
+ return completed(false);
+ }
+
+ lastTarget = target;
+ lastDirection = direction;
+ var nextSibling = target.nextElementSibling,
+ after = false;
+ after = direction === 1;
+
+ var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after);
+
+ if (moveVector !== false) {
+ if (moveVector === 1 || moveVector === -1) {
+ after = moveVector === 1;
+ }
+
+ _silent = true;
+ setTimeout(_unsilent, 30);
+ capture();
+
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
+ } else {
+ target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+ } // Undo chrome's scroll adjustment (has no effect on other browsers)
+
+
+ if (scrolledPastTop) {
+ scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);
+ }
+
+ parentEl = dragEl.parentNode; // actualization
+ // must be done before animation
+
+ if (targetBeforeFirstSwap !== undefined && !isCircumstantialInvert) {
+ targetMoveDistance = Math.abs(targetBeforeFirstSwap - getRect(target)[side1]);
+ }
+
+ changed();
+ return completed(true);
+ }
+ }
+
+ if (el.contains(dragEl)) {
+ return completed(false);
+ }
+ }
+
+ return false;
+ },
+ _ignoreWhileAnimating: null,
+ _offMoveEvents: function _offMoveEvents() {
+ off(document, 'mousemove', this._onTouchMove);
+ off(document, 'touchmove', this._onTouchMove);
+ off(document, 'pointermove', this._onTouchMove);
+ off(document, 'dragover', nearestEmptyInsertDetectEvent);
+ off(document, 'mousemove', nearestEmptyInsertDetectEvent);
+ off(document, 'touchmove', nearestEmptyInsertDetectEvent);
+ },
+ _offUpEvents: function _offUpEvents() {
+ var ownerDocument = this.el.ownerDocument;
+ off(ownerDocument, 'mouseup', this._onDrop);
+ off(ownerDocument, 'touchend', this._onDrop);
+ off(ownerDocument, 'pointerup', this._onDrop);
+ off(ownerDocument, 'touchcancel', this._onDrop);
+ off(document, 'selectstart', this);
+ },
+ _onDrop: function _onDrop(
+ /**Event*/
+ evt) {
+ var el = this.el,
+ options = this.options; // Get the index of the dragged element within its parent
+
+ newIndex = index(dragEl);
+ newDraggableIndex = index(dragEl, options.draggable);
+ pluginEvent('drop', this, {
+ evt: evt
+ });
+ parentEl = dragEl && dragEl.parentNode; // Get again after plugin event
+
+ newIndex = index(dragEl);
+ newDraggableIndex = index(dragEl, options.draggable);
+
+ if (Sortable.eventCanceled) {
+ this._nulling();
+
+ return;
+ }
+
+ awaitingDragStarted = false;
+ isCircumstantialInvert = false;
+ pastFirstInvertThresh = false;
+ clearInterval(this._loopId);
+ clearTimeout(this._dragStartTimer);
+
+ _cancelNextTick(this.cloneId);
+
+ _cancelNextTick(this._dragStartId); // Unbind events
+
+
+ if (this.nativeDraggable) {
+ off(document, 'drop', this);
+ off(el, 'dragstart', this._onDragStart);
+ }
+
+ this._offMoveEvents();
+
+ this._offUpEvents();
+
+ if (Safari) {
+ css(document.body, 'user-select', '');
+ }
+
+ css(dragEl, 'transform', '');
+
+ if (evt) {
+ if (moved) {
+ evt.cancelable && evt.preventDefault();
+ !options.dropBubble && evt.stopPropagation();
+ }
+
+ ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl);
+
+ if (rootEl === parentEl || putSortable && putSortable.lastPutMode !== 'clone') {
+ // Remove clone(s)
+ cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl);
+ }
+
+ if (dragEl) {
+ if (this.nativeDraggable) {
+ off(dragEl, 'dragend', this);
+ }
+
+ _disableDraggable(dragEl);
+
+ dragEl.style['will-change'] = ''; // Remove classes
+ // ghostClass is added in dragStarted
+
+ if (moved && !awaitingDragStarted) {
+ toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : this.options.ghostClass, false);
+ }
+
+ toggleClass(dragEl, this.options.chosenClass, false); // Drag stop event
+
+ _dispatchEvent({
+ sortable: this,
+ name: 'unchoose',
+ toEl: parentEl,
+ newIndex: null,
+ newDraggableIndex: null,
+ originalEvent: evt
+ });
+
+ if (rootEl !== parentEl) {
+ if (newIndex >= 0) {
+ // Add event
+ _dispatchEvent({
+ rootEl: parentEl,
+ name: 'add',
+ toEl: parentEl,
+ fromEl: rootEl,
+ originalEvent: evt
+ }); // Remove event
+
+
+ _dispatchEvent({
+ sortable: this,
+ name: 'remove',
+ toEl: parentEl,
+ originalEvent: evt
+ }); // drag from one list and drop into another
+
+
+ _dispatchEvent({
+ rootEl: parentEl,
+ name: 'sort',
+ toEl: parentEl,
+ fromEl: rootEl,
+ originalEvent: evt
+ });
+
+ _dispatchEvent({
+ sortable: this,
+ name: 'sort',
+ toEl: parentEl,
+ originalEvent: evt
+ });
+ }
+
+ putSortable && putSortable.save();
+ } else {
+ if (newIndex !== oldIndex) {
+ if (newIndex >= 0) {
+ // drag & drop within the same list
+ _dispatchEvent({
+ sortable: this,
+ name: 'update',
+ toEl: parentEl,
+ originalEvent: evt
+ });
+
+ _dispatchEvent({
+ sortable: this,
+ name: 'sort',
+ toEl: parentEl,
+ originalEvent: evt
+ });
+ }
+ }
+ }
+
+ if (Sortable.active) {
+ /* jshint eqnull:true */
+ if (newIndex == null || newIndex === -1) {
+ newIndex = oldIndex;
+ newDraggableIndex = oldDraggableIndex;
+ }
+
+ _dispatchEvent({
+ sortable: this,
+ name: 'end',
+ toEl: parentEl,
+ originalEvent: evt
+ }); // Save sorting
+
+
+ this.save();
+ }
+ }
+ }
+
+ this._nulling();
+ },
+ _nulling: function _nulling() {
+ pluginEvent('nulling', this);
+ rootEl = dragEl = parentEl = ghostEl = nextEl = cloneEl = lastDownEl = cloneHidden = tapEvt = touchEvt = moved = newIndex = newDraggableIndex = oldIndex = oldDraggableIndex = lastTarget = lastDirection = putSortable = activeGroup = Sortable.dragged = Sortable.ghost = Sortable.clone = Sortable.active = null;
+ savedInputChecked.forEach(function (el) {
+ el.checked = true;
+ });
+ savedInputChecked.length = lastDx = lastDy = 0;
+ },
+ handleEvent: function handleEvent(
+ /**Event*/
+ evt) {
+ switch (evt.type) {
+ case 'drop':
+ case 'dragend':
+ this._onDrop(evt);
+
+ break;
+
+ case 'dragenter':
+ case 'dragover':
+ if (dragEl) {
+ this._onDragOver(evt);
+
+ _globalDragOver(evt);
+ }
+
+ break;
+
+ case 'selectstart':
+ evt.preventDefault();
+ break;
+ }
+ },
+
+ /**
+ * Serializes the item into an array of string.
+ * @returns {String[]}
+ */
+ toArray: function toArray() {
+ var order = [],
+ el,
+ children = this.el.children,
+ i = 0,
+ n = children.length,
+ options = this.options;
+
+ for (; i < n; i++) {
+ el = children[i];
+
+ if (closest(el, options.draggable, this.el, false)) {
+ order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
+ }
+ }
+
+ return order;
+ },
+
+ /**
+ * Sorts the elements according to the array.
+ * @param {String[]} order order of the items
+ */
+ sort: function sort(order) {
+ var items = {},
+ rootEl = this.el;
+ this.toArray().forEach(function (id, i) {
+ var el = rootEl.children[i];
+
+ if (closest(el, this.options.draggable, rootEl, false)) {
+ items[id] = el;
+ }
+ }, this);
+ order.forEach(function (id) {
+ if (items[id]) {
+ rootEl.removeChild(items[id]);
+ rootEl.appendChild(items[id]);
+ }
+ });
+ },
+
+ /**
+ * Save the current sorting
+ */
+ save: function save() {
+ var store = this.options.store;
+ store && store.set && store.set(this);
+ },
+
+ /**
+ * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+ * @param {HTMLElement} el
+ * @param {String} [selector] default: `options.draggable`
+ * @returns {HTMLElement|null}
+ */
+ closest: function closest$1(el, selector) {
+ return closest(el, selector || this.options.draggable, this.el, false);
+ },
+
+ /**
+ * Set/get option
+ * @param {string} name
+ * @param {*} [value]
+ * @returns {*}
+ */
+ option: function option(name, value) {
+ var options = this.options;
+
+ if (value === void 0) {
+ return options[name];
+ } else {
+ var modifiedValue = PluginManager.modifyOption(this, name, value);
+
+ if (typeof modifiedValue !== 'undefined') {
+ options[name] = modifiedValue;
+ } else {
+ options[name] = value;
+ }
+
+ if (name === 'group') {
+ _prepareGroup(options);
+ }
+ }
+ },
+
+ /**
+ * Destroy
+ */
+ destroy: function destroy() {
+ pluginEvent('destroy', this);
+ var el = this.el;
+ el[expando] = null;
+ off(el, 'mousedown', this._onTapStart);
+ off(el, 'touchstart', this._onTapStart);
+ off(el, 'pointerdown', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ off(el, 'dragover', this);
+ off(el, 'dragenter', this);
+ } // Remove draggable attributes
+
+
+ Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
+ el.removeAttribute('draggable');
+ });
+
+ this._onDrop();
+
+ this._disableDelayedDragEvents();
+
+ sortables.splice(sortables.indexOf(this.el), 1);
+ this.el = el = null;
+ },
+ _hideClone: function _hideClone() {
+ if (!cloneHidden) {
+ pluginEvent('hideClone', this);
+ if (Sortable.eventCanceled) return;
+ css(cloneEl, 'display', 'none');
+
+ if (this.options.removeCloneOnHide && cloneEl.parentNode) {
+ cloneEl.parentNode.removeChild(cloneEl);
+ }
+
+ cloneHidden = true;
+ }
+ },
+ _showClone: function _showClone(putSortable) {
+ if (putSortable.lastPutMode !== 'clone') {
+ this._hideClone();
+
+ return;
+ }
+
+ if (cloneHidden) {
+ pluginEvent('showClone', this);
+ if (Sortable.eventCanceled) return; // show clone at dragEl or original position
+
+ if (rootEl.contains(dragEl) && !this.options.group.revertClone) {
+ rootEl.insertBefore(cloneEl, dragEl);
+ } else if (nextEl) {
+ rootEl.insertBefore(cloneEl, nextEl);
+ } else {
+ rootEl.appendChild(cloneEl);
+ }
+
+ if (this.options.group.revertClone) {
+ this.animate(dragEl, cloneEl);
+ }
+
+ css(cloneEl, 'display', '');
+ cloneHidden = false;
+ }
+ }
+ };
+
+ function _globalDragOver(
+ /**Event*/
+ evt) {
+ if (evt.dataTransfer) {
+ evt.dataTransfer.dropEffect = 'move';
+ }
+
+ evt.cancelable && evt.preventDefault();
+ }
+
+ function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvent, willInsertAfter) {
+ var evt,
+ sortable = fromEl[expando],
+ onMoveFn = sortable.options.onMove,
+ retVal; // Support for new CustomEvent feature
+
+ if (window.CustomEvent && !IE11OrLess && !Edge) {
+ evt = new CustomEvent('move', {
+ bubbles: true,
+ cancelable: true
+ });
+ } else {
+ evt = document.createEvent('Event');
+ evt.initEvent('move', true, true);
+ }
+
+ evt.to = toEl;
+ evt.from = fromEl;
+ evt.dragged = dragEl;
+ evt.draggedRect = dragRect;
+ evt.related = targetEl || toEl;
+ evt.relatedRect = targetRect || getRect(toEl);
+ evt.willInsertAfter = willInsertAfter;
+ evt.originalEvent = originalEvent;
+ fromEl.dispatchEvent(evt);
+
+ if (onMoveFn) {
+ retVal = onMoveFn.call(sortable, evt, originalEvent);
+ }
+
+ return retVal;
+ }
+
+ function _disableDraggable(el) {
+ el.draggable = false;
+ }
+
+ function _unsilent() {
+ _silent = false;
+ }
+
+ function _ghostIsLast(evt, vertical, sortable) {
+ var rect = getRect(lastChild(sortable.el, sortable.options.draggable));
+ var spacer = 10;
+ return vertical ? evt.clientX > rect.right + spacer || evt.clientX <= rect.right && evt.clientY > rect.bottom && evt.clientX >= rect.left : evt.clientX > rect.right && evt.clientY > rect.top || evt.clientX <= rect.right && evt.clientY > rect.bottom + spacer;
+ }
+
+ function _getSwapDirection(evt, target, targetRect, vertical, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) {
+ var mouseOnAxis = vertical ? evt.clientY : evt.clientX,
+ targetLength = vertical ? targetRect.height : targetRect.width,
+ targetS1 = vertical ? targetRect.top : targetRect.left,
+ targetS2 = vertical ? targetRect.bottom : targetRect.right,
+ invert = false;
+
+ if (!invertSwap) {
+ // Never invert or create dragEl shadow when target movemenet causes mouse to move past the end of regular swapThreshold
+ if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) {
+ // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2
+ // check if past first invert threshold on side opposite of lastDirection
+ if (!pastFirstInvertThresh && (lastDirection === 1 ? mouseOnAxis > targetS1 + targetLength * invertedSwapThreshold / 2 : mouseOnAxis < targetS2 - targetLength * invertedSwapThreshold / 2)) {
+ // past first invert threshold, do not restrict inverted threshold to dragEl shadow
+ pastFirstInvertThresh = true;
+ }
+
+ if (!pastFirstInvertThresh) {
+ // dragEl shadow (target move distance shadow)
+ if (lastDirection === 1 ? mouseOnAxis < targetS1 + targetMoveDistance // over dragEl shadow
+ : mouseOnAxis > targetS2 - targetMoveDistance) {
+ return -lastDirection;
+ }
+ } else {
+ invert = true;
+ }
+ } else {
+ // Regular
+ if (mouseOnAxis > targetS1 + targetLength * (1 - swapThreshold) / 2 && mouseOnAxis < targetS2 - targetLength * (1 - swapThreshold) / 2) {
+ return _getInsertDirection(target);
+ }
+ }
+ }
+
+ invert = invert || invertSwap;
+
+ if (invert) {
+ // Invert of regular
+ if (mouseOnAxis < targetS1 + targetLength * invertedSwapThreshold / 2 || mouseOnAxis > targetS2 - targetLength * invertedSwapThreshold / 2) {
+ return mouseOnAxis > targetS1 + targetLength / 2 ? 1 : -1;
+ }
+ }
+
+ return 0;
+ }
+ /**
+ * Gets the direction dragEl must be swapped relative to target in order to make it
+ * seem that dragEl has been "inserted" into that element's position
+ * @param {HTMLElement} target The target whose position dragEl is being inserted at
+ * @return {Number} Direction dragEl must be swapped
+ */
+
+
+ function _getInsertDirection(target) {
+ if (index(dragEl) < index(target)) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ /**
+ * Generate id
+ * @param {HTMLElement} el
+ * @returns {String}
+ * @private
+ */
+
+
+ function _generateId(el) {
+ var str = el.tagName + el.className + el.src + el.href + el.textContent,
+ i = str.length,
+ sum = 0;
+
+ while (i--) {
+ sum += str.charCodeAt(i);
+ }
+
+ return sum.toString(36);
+ }
+
+ function _saveInputCheckedState(root) {
+ savedInputChecked.length = 0;
+ var inputs = root.getElementsByTagName('input');
+ var idx = inputs.length;
+
+ while (idx--) {
+ var el = inputs[idx];
+ el.checked && savedInputChecked.push(el);
+ }
+ }
+
+ function _nextTick(fn) {
+ return setTimeout(fn, 0);
+ }
+
+ function _cancelNextTick(id) {
+ return clearTimeout(id);
+ } // Fixed #973:
+
+
+ if (documentExists) {
+ on(document, 'touchmove', function (evt) {
+ if ((Sortable.active || awaitingDragStarted) && evt.cancelable) {
+ evt.preventDefault();
+ }
+ });
+ } // Export utils
+
+
+ Sortable.utils = {
+ on: on,
+ off: off,
+ css: css,
+ find: find,
+ is: function is(el, selector) {
+ return !!closest(el, selector, el, false);
+ },
+ extend: extend,
+ throttle: throttle,
+ closest: closest,
+ toggleClass: toggleClass,
+ clone: clone,
+ index: index,
+ nextTick: _nextTick,
+ cancelNextTick: _cancelNextTick,
+ detectDirection: _detectDirection,
+ getChild: getChild
+ };
+ /**
+ * Get the Sortable instance of an element
+ * @param {HTMLElement} element The element
+ * @return {Sortable|undefined} The instance of Sortable
+ */
+
+ Sortable.get = function (element) {
+ return element[expando];
+ };
+ /**
+ * Mount a plugin to Sortable
+ * @param {...SortablePlugin|SortablePlugin[]} plugins Plugins being mounted
+ */
+
+
+ Sortable.mount = function () {
+ for (var _len = arguments.length, plugins = new Array(_len), _key = 0; _key < _len; _key++) {
+ plugins[_key] = arguments[_key];
+ }
+
+ if (plugins[0].constructor === Array) plugins = plugins[0];
+ plugins.forEach(function (plugin) {
+ if (!plugin.prototype || !plugin.prototype.constructor) {
+ throw "Sortable: Mounted plugin must be a constructor function, not ".concat({}.toString.call(plugin));
+ }
+
+ if (plugin.utils) Sortable.utils = _objectSpread({}, Sortable.utils, plugin.utils);
+ PluginManager.mount(plugin);
+ });
+ };
+ /**
+ * Create sortable instance
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+
+
+ Sortable.create = function (el, options) {
+ return new Sortable(el, options);
+ }; // Export
+
+
+ Sortable.version = version;
+
+ var autoScrolls = [],
+ scrollEl,
+ scrollRootEl,
+ scrolling = false,
+ lastAutoScrollX,
+ lastAutoScrollY,
+ touchEvt$1,
+ pointerElemChangedInterval;
+
+ function AutoScrollPlugin() {
+ function AutoScroll() {
+ this.defaults = {
+ scroll: true,
+ scrollSensitivity: 30,
+ scrollSpeed: 10,
+ bubbleScroll: true
+ }; // Bind all private methods
+
+ for (var fn in this) {
+ if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+ this[fn] = this[fn].bind(this);
+ }
+ }
+ }
+
+ AutoScroll.prototype = {
+ dragStarted: function dragStarted(_ref) {
+ var originalEvent = _ref.originalEvent;
+
+ if (this.sortable.nativeDraggable) {
+ on(document, 'dragover', this._handleAutoScroll);
+ } else {
+ if (this.options.supportPointer) {
+ on(document, 'pointermove', this._handleFallbackAutoScroll);
+ } else if (originalEvent.touches) {
+ on(document, 'touchmove', this._handleFallbackAutoScroll);
+ } else {
+ on(document, 'mousemove', this._handleFallbackAutoScroll);
+ }
+ }
+ },
+ dragOverCompleted: function dragOverCompleted(_ref2) {
+ var originalEvent = _ref2.originalEvent;
+
+ // For when bubbling is canceled and using fallback (fallback 'touchmove' always reached)
+ if (!this.options.dragOverBubble && !originalEvent.rootEl) {
+ this._handleAutoScroll(originalEvent);
+ }
+ },
+ drop: function drop() {
+ if (this.sortable.nativeDraggable) {
+ off(document, 'dragover', this._handleAutoScroll);
+ } else {
+ off(document, 'pointermove', this._handleFallbackAutoScroll);
+ off(document, 'touchmove', this._handleFallbackAutoScroll);
+ off(document, 'mousemove', this._handleFallbackAutoScroll);
+ }
+
+ clearPointerElemChangedInterval();
+ clearAutoScrolls();
+ cancelThrottle();
+ },
+ nulling: function nulling() {
+ touchEvt$1 = scrollRootEl = scrollEl = scrolling = pointerElemChangedInterval = lastAutoScrollX = lastAutoScrollY = null;
+ autoScrolls.length = 0;
+ },
+ _handleFallbackAutoScroll: function _handleFallbackAutoScroll(evt) {
+ this._handleAutoScroll(evt, true);
+ },
+ _handleAutoScroll: function _handleAutoScroll(evt, fallback) {
+ var _this = this;
+
+ var x = (evt.touches ? evt.touches[0] : evt).clientX,
+ y = (evt.touches ? evt.touches[0] : evt).clientY,
+ elem = document.elementFromPoint(x, y);
+ touchEvt$1 = evt; // IE does not seem to have native autoscroll,
+ // Edge's autoscroll seems too conditional,
+ // MACOS Safari does not have autoscroll,
+ // Firefox and Chrome are good
+
+ if (fallback || Edge || IE11OrLess || Safari) {
+ autoScroll(evt, this.options, elem, fallback); // Listener for pointer element change
+
+ var ogElemScroller = getParentAutoScrollElement(elem, true);
+
+ if (scrolling && (!pointerElemChangedInterval || x !== lastAutoScrollX || y !== lastAutoScrollY)) {
+ pointerElemChangedInterval && clearPointerElemChangedInterval(); // Detect for pointer elem change, emulating native DnD behaviour
+
+ pointerElemChangedInterval = setInterval(function () {
+ var newElem = getParentAutoScrollElement(document.elementFromPoint(x, y), true);
+
+ if (newElem !== ogElemScroller) {
+ ogElemScroller = newElem;
+ clearAutoScrolls();
+ }
+
+ autoScroll(evt, _this.options, newElem, fallback);
+ }, 10);
+ lastAutoScrollX = x;
+ lastAutoScrollY = y;
+ }
+ } else {
+ // if DnD is enabled (and browser has good autoscrolling), first autoscroll will already scroll, so get parent autoscroll of first autoscroll
+ if (!this.options.bubbleScroll || getParentAutoScrollElement(elem, true) === getWindowScrollingElement()) {
+ clearAutoScrolls();
+ return;
+ }
+
+ autoScroll(evt, this.options, getParentAutoScrollElement(elem, false), false);
+ }
+ }
+ };
+ return _extends(AutoScroll, {
+ pluginName: 'scroll',
+ initializeByDefault: true
+ });
+ }
+
+ function clearAutoScrolls() {
+ autoScrolls.forEach(function (autoScroll) {
+ clearInterval(autoScroll.pid);
+ });
+ autoScrolls = [];
+ }
+
+ function clearPointerElemChangedInterval() {
+ clearInterval(pointerElemChangedInterval);
+ }
+
+ var autoScroll = throttle(function (evt, options, rootEl, isFallback) {
+ // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
+ if (!options.scroll) return;
+ var x = (evt.touches ? evt.touches[0] : evt).clientX,
+ y = (evt.touches ? evt.touches[0] : evt).clientY,
+ sens = options.scrollSensitivity,
+ speed = options.scrollSpeed,
+ winScroller = getWindowScrollingElement();
+ var scrollThisInstance = false,
+ scrollCustomFn; // New scroll root, set scrollEl
+
+ if (scrollRootEl !== rootEl) {
+ scrollRootEl = rootEl;
+ clearAutoScrolls();
+ scrollEl = options.scroll;
+ scrollCustomFn = options.scrollFn;
+
+ if (scrollEl === true) {
+ scrollEl = getParentAutoScrollElement(rootEl, true);
+ }
+ }
+
+ var layersOut = 0;
+ var currentParent = scrollEl;
+
+ do {
+ var el = currentParent,
+ rect = getRect(el),
+ top = rect.top,
+ bottom = rect.bottom,
+ left = rect.left,
+ right = rect.right,
+ width = rect.width,
+ height = rect.height,
+ canScrollX = void 0,
+ canScrollY = void 0,
+ scrollWidth = el.scrollWidth,
+ scrollHeight = el.scrollHeight,
+ elCSS = css(el),
+ scrollPosX = el.scrollLeft,
+ scrollPosY = el.scrollTop;
+
+ if (el === winScroller) {
+ canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll' || elCSS.overflowX === 'visible');
+ canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll' || elCSS.overflowY === 'visible');
+ } else {
+ canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll');
+ canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll');
+ }
+
+ var vx = canScrollX && (Math.abs(right - x) <= sens && scrollPosX + width < scrollWidth) - (Math.abs(left - x) <= sens && !!scrollPosX);
+ var vy = canScrollY && (Math.abs(bottom - y) <= sens && scrollPosY + height < scrollHeight) - (Math.abs(top - y) <= sens && !!scrollPosY);
+
+ if (!autoScrolls[layersOut]) {
+ for (var i = 0; i <= layersOut; i++) {
+ if (!autoScrolls[i]) {
+ autoScrolls[i] = {};
+ }
+ }
+ }
+
+ if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) {
+ autoScrolls[layersOut].el = el;
+ autoScrolls[layersOut].vx = vx;
+ autoScrolls[layersOut].vy = vy;
+ clearInterval(autoScrolls[layersOut].pid);
+
+ if (vx != 0 || vy != 0) {
+ scrollThisInstance = true;
+ /* jshint loopfunc:true */
+
+ autoScrolls[layersOut].pid = setInterval(function () {
+ // emulate drag over during autoscroll (fallback), emulating native DnD behaviour
+ if (isFallback && this.layer === 0) {
+ Sortable.active._onTouchMove(touchEvt$1); // To move ghost if it is positioned absolutely
+
+ }
+
+ var scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0;
+ var scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0;
+
+ if (typeof scrollCustomFn === 'function') {
+ if (scrollCustomFn.call(Sortable.dragged.parentNode[expando], scrollOffsetX, scrollOffsetY, evt, touchEvt$1, autoScrolls[this.layer].el) !== 'continue') {
+ return;
+ }
+ }
+
+ scrollBy(autoScrolls[this.layer].el, scrollOffsetX, scrollOffsetY);
+ }.bind({
+ layer: layersOut
+ }), 24);
+ }
+ }
+
+ layersOut++;
+ } while (options.bubbleScroll && currentParent !== winScroller && (currentParent = getParentAutoScrollElement(currentParent, false)));
+
+ scrolling = scrollThisInstance; // in case another function catches scrolling as false in between when it is not
+ }, 30);
+
+ var drop = function drop(_ref) {
+ var originalEvent = _ref.originalEvent,
+ putSortable = _ref.putSortable,
+ dragEl = _ref.dragEl,
+ activeSortable = _ref.activeSortable,
+ dispatchSortableEvent = _ref.dispatchSortableEvent,
+ hideGhostForTarget = _ref.hideGhostForTarget,
+ unhideGhostForTarget = _ref.unhideGhostForTarget;
+ if (!originalEvent) return;
+ var toSortable = putSortable || activeSortable;
+ hideGhostForTarget();
+ var touch = originalEvent.changedTouches && originalEvent.changedTouches.length ? originalEvent.changedTouches[0] : originalEvent;
+ var target = document.elementFromPoint(touch.clientX, touch.clientY);
+ unhideGhostForTarget();
+
+ if (toSortable && !toSortable.el.contains(target)) {
+ dispatchSortableEvent('spill');
+ this.onSpill({
+ dragEl: dragEl,
+ putSortable: putSortable
+ });
+ }
+ };
+
+ function Revert() {}
+
+ Revert.prototype = {
+ startIndex: null,
+ dragStart: function dragStart(_ref2) {
+ var oldDraggableIndex = _ref2.oldDraggableIndex;
+ this.startIndex = oldDraggableIndex;
+ },
+ onSpill: function onSpill(_ref3) {
+ var dragEl = _ref3.dragEl,
+ putSortable = _ref3.putSortable;
+ this.sortable.captureAnimationState();
+
+ if (putSortable) {
+ putSortable.captureAnimationState();
+ }
+
+ var nextSibling = getChild(this.sortable.el, this.startIndex, this.options);
+
+ if (nextSibling) {
+ this.sortable.el.insertBefore(dragEl, nextSibling);
+ } else {
+ this.sortable.el.appendChild(dragEl);
+ }
+
+ this.sortable.animateAll();
+
+ if (putSortable) {
+ putSortable.animateAll();
+ }
+ },
+ drop: drop
+ };
+
+ _extends(Revert, {
+ pluginName: 'revertOnSpill'
+ });
+
+ function Remove() {}
+
+ Remove.prototype = {
+ onSpill: function onSpill(_ref4) {
+ var dragEl = _ref4.dragEl,
+ putSortable = _ref4.putSortable;
+ var parentSortable = putSortable || this.sortable;
+ parentSortable.captureAnimationState();
+ dragEl.parentNode && dragEl.parentNode.removeChild(dragEl);
+ parentSortable.animateAll();
+ },
+ drop: drop
+ };
+
+ _extends(Remove, {
+ pluginName: 'removeOnSpill'
+ });
+
+ var lastSwapEl;
+
+ function SwapPlugin() {
+ function Swap() {
+ this.defaults = {
+ swapClass: 'sortable-swap-highlight'
+ };
+ }
+
+ Swap.prototype = {
+ dragStart: function dragStart(_ref) {
+ var dragEl = _ref.dragEl;
+ lastSwapEl = dragEl;
+ },
+ dragOverValid: function dragOverValid(_ref2) {
+ var completed = _ref2.completed,
+ target = _ref2.target,
+ onMove = _ref2.onMove,
+ activeSortable = _ref2.activeSortable,
+ changed = _ref2.changed,
+ cancel = _ref2.cancel;
+ if (!activeSortable.options.swap) return;
+ var el = this.sortable.el,
+ options = this.options;
+
+ if (target && target !== el) {
+ var prevSwapEl = lastSwapEl;
+
+ if (onMove(target) !== false) {
+ toggleClass(target, options.swapClass, true);
+ lastSwapEl = target;
+ } else {
+ lastSwapEl = null;
+ }
+
+ if (prevSwapEl && prevSwapEl !== lastSwapEl) {
+ toggleClass(prevSwapEl, options.swapClass, false);
+ }
+ }
+
+ changed();
+ completed(true);
+ cancel();
+ },
+ drop: function drop(_ref3) {
+ var activeSortable = _ref3.activeSortable,
+ putSortable = _ref3.putSortable,
+ dragEl = _ref3.dragEl;
+ var toSortable = putSortable || this.sortable;
+ var options = this.options;
+ lastSwapEl && toggleClass(lastSwapEl, options.swapClass, false);
+
+ if (lastSwapEl && (options.swap || putSortable && putSortable.options.swap)) {
+ if (dragEl !== lastSwapEl) {
+ toSortable.captureAnimationState();
+ if (toSortable !== activeSortable) activeSortable.captureAnimationState();
+ swapNodes(dragEl, lastSwapEl);
+ toSortable.animateAll();
+ if (toSortable !== activeSortable) activeSortable.animateAll();
+ }
+ }
+ },
+ nulling: function nulling() {
+ lastSwapEl = null;
+ }
+ };
+ return _extends(Swap, {
+ pluginName: 'swap',
+ eventProperties: function eventProperties() {
+ return {
+ swapItem: lastSwapEl
+ };
+ }
+ });
+ }
+
+ function swapNodes(n1, n2) {
+ var p1 = n1.parentNode,
+ p2 = n2.parentNode,
+ i1,
+ i2;
+ if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return;
+ i1 = index(n1);
+ i2 = index(n2);
+
+ if (p1.isEqualNode(p2) && i1 < i2) {
+ i2++;
+ }
+
+ p1.insertBefore(n2, p1.children[i1]);
+ p2.insertBefore(n1, p2.children[i2]);
+ }
+
+ var multiDragElements = [],
+ multiDragClones = [],
+ lastMultiDragSelect,
+ // for selection with modifier key down (SHIFT)
+ multiDragSortable,
+ initialFolding = false,
+ // Initial multi-drag fold when drag started
+ folding = false,
+ // Folding any other time
+ dragStarted = false,
+ dragEl$1,
+ clonesFromRect,
+ clonesHidden;
+
+ function MultiDragPlugin() {
+ function MultiDrag(sortable) {
+ // Bind all private methods
+ for (var fn in this) {
+ if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+ this[fn] = this[fn].bind(this);
+ }
+ }
+
+ if (sortable.options.supportPointer) {
+ on(document, 'pointerup', this._deselectMultiDrag);
+ } else {
+ on(document, 'mouseup', this._deselectMultiDrag);
+ on(document, 'touchend', this._deselectMultiDrag);
+ }
+
+ on(document, 'keydown', this._checkKeyDown);
+ on(document, 'keyup', this._checkKeyUp);
+ this.defaults = {
+ selectedClass: 'sortable-selected',
+ multiDragKey: null,
+ setData: function setData(dataTransfer, dragEl) {
+ var data = '';
+
+ if (multiDragElements.length && multiDragSortable === sortable) {
+ multiDragElements.forEach(function (multiDragElement, i) {
+ data += (!i ? '' : ', ') + multiDragElement.textContent;
+ });
+ } else {
+ data = dragEl.textContent;
+ }
+
+ dataTransfer.setData('Text', data);
+ }
+ };
+ }
+
+ MultiDrag.prototype = {
+ multiDragKeyDown: false,
+ isMultiDrag: false,
+ delayStartGlobal: function delayStartGlobal(_ref) {
+ var dragged = _ref.dragEl;
+ dragEl$1 = dragged;
+ },
+ delayEnded: function delayEnded() {
+ this.isMultiDrag = ~multiDragElements.indexOf(dragEl$1);
+ },
+ setupClone: function setupClone(_ref2) {
+ var sortable = _ref2.sortable,
+ cancel = _ref2.cancel;
+ if (!this.isMultiDrag) return;
+
+ for (var i = 0; i < multiDragElements.length; i++) {
+ multiDragClones.push(clone(multiDragElements[i]));
+ multiDragClones[i].sortableIndex = multiDragElements[i].sortableIndex;
+ multiDragClones[i].draggable = false;
+ multiDragClones[i].style['will-change'] = '';
+ toggleClass(multiDragClones[i], this.options.selectedClass, false);
+ multiDragElements[i] === dragEl$1 && toggleClass(multiDragClones[i], this.options.chosenClass, false);
+ }
+
+ sortable._hideClone();
+
+ cancel();
+ },
+ clone: function clone(_ref3) {
+ var sortable = _ref3.sortable,
+ rootEl = _ref3.rootEl,
+ dispatchSortableEvent = _ref3.dispatchSortableEvent,
+ cancel = _ref3.cancel;
+ if (!this.isMultiDrag) return;
+
+ if (!this.options.removeCloneOnHide) {
+ if (multiDragElements.length && multiDragSortable === sortable) {
+ insertMultiDragClones(true, rootEl);
+ dispatchSortableEvent('clone');
+ cancel();
+ }
+ }
+ },
+ showClone: function showClone(_ref4) {
+ var cloneNowShown = _ref4.cloneNowShown,
+ rootEl = _ref4.rootEl,
+ cancel = _ref4.cancel;
+ if (!this.isMultiDrag) return;
+ insertMultiDragClones(false, rootEl);
+ multiDragClones.forEach(function (clone) {
+ css(clone, 'display', '');
+ });
+ cloneNowShown();
+ clonesHidden = false;
+ cancel();
+ },
+ hideClone: function hideClone(_ref5) {
+ var _this = this;
+
+ var sortable = _ref5.sortable,
+ cloneNowHidden = _ref5.cloneNowHidden,
+ cancel = _ref5.cancel;
+ if (!this.isMultiDrag) return;
+ multiDragClones.forEach(function (clone) {
+ css(clone, 'display', 'none');
+
+ if (_this.options.removeCloneOnHide && clone.parentNode) {
+ clone.parentNode.removeChild(clone);
+ }
+ });
+ cloneNowHidden();
+ clonesHidden = true;
+ cancel();
+ },
+ dragStartGlobal: function dragStartGlobal(_ref6) {
+ var sortable = _ref6.sortable;
+
+ if (!this.isMultiDrag && multiDragSortable) {
+ multiDragSortable.multiDrag._deselectMultiDrag();
+ }
+
+ multiDragElements.forEach(function (multiDragElement) {
+ multiDragElement.sortableIndex = index(multiDragElement);
+ }); // Sort multi-drag elements
+
+ multiDragElements = multiDragElements.sort(function (a, b) {
+ return a.sortableIndex - b.sortableIndex;
+ });
+ dragStarted = true;
+ },
+ dragStarted: function dragStarted(_ref7) {
+ var _this2 = this;
+
+ var sortable = _ref7.sortable;
+ if (!this.isMultiDrag) return;
+
+ if (this.options.sort) {
+ // Capture rects,
+ // hide multi drag elements (by positioning them absolute),
+ // set multi drag elements rects to dragRect,
+ // show multi drag elements,
+ // animate to rects,
+ // unset rects & remove from DOM
+ sortable.captureAnimationState();
+
+ if (this.options.animation) {
+ multiDragElements.forEach(function (multiDragElement) {
+ if (multiDragElement === dragEl$1) return;
+ css(multiDragElement, 'position', 'absolute');
+ });
+ var dragRect = getRect(dragEl$1, false, true, true);
+ multiDragElements.forEach(function (multiDragElement) {
+ if (multiDragElement === dragEl$1) return;
+ setRect(multiDragElement, dragRect);
+ });
+ folding = true;
+ initialFolding = true;
+ }
+ }
+
+ sortable.animateAll(function () {
+ folding = false;
+ initialFolding = false;
+
+ if (_this2.options.animation) {
+ multiDragElements.forEach(function (multiDragElement) {
+ unsetRect(multiDragElement);
+ });
+ } // Remove all auxiliary multidrag items from el, if sorting enabled
+
+
+ if (_this2.options.sort) {
+ removeMultiDragElements();
+ }
+ });
+ },
+ dragOver: function dragOver(_ref8) {
+ var target = _ref8.target,
+ completed = _ref8.completed,
+ cancel = _ref8.cancel;
+
+ if (folding && ~multiDragElements.indexOf(target)) {
+ completed(false);
+ cancel();
+ }
+ },
+ revert: function revert(_ref9) {
+ var fromSortable = _ref9.fromSortable,
+ rootEl = _ref9.rootEl,
+ sortable = _ref9.sortable,
+ dragRect = _ref9.dragRect;
+
+ if (multiDragElements.length > 1) {
+ // Setup unfold animation
+ multiDragElements.forEach(function (multiDragElement) {
+ sortable.addAnimationState({
+ target: multiDragElement,
+ rect: folding ? getRect(multiDragElement) : dragRect
+ });
+ unsetRect(multiDragElement);
+ multiDragElement.fromRect = dragRect;
+ fromSortable.removeAnimationState(multiDragElement);
+ });
+ folding = false;
+ insertMultiDragElements(!this.options.removeCloneOnHide, rootEl);
+ }
+ },
+ dragOverCompleted: function dragOverCompleted(_ref10) {
+ var sortable = _ref10.sortable,
+ isOwner = _ref10.isOwner,
+ insertion = _ref10.insertion,
+ activeSortable = _ref10.activeSortable,
+ parentEl = _ref10.parentEl,
+ putSortable = _ref10.putSortable;
+ var options = this.options;
+
+ if (insertion) {
+ // Clones must be hidden before folding animation to capture dragRectAbsolute properly
+ if (isOwner) {
+ activeSortable._hideClone();
+ }
+
+ initialFolding = false; // If leaving sort:false root, or already folding - Fold to new location
+
+ if (options.animation && multiDragElements.length > 1 && (folding || !isOwner && !activeSortable.options.sort && !putSortable)) {
+ // Fold: Set all multi drag elements's rects to dragEl's rect when multi-drag elements are invisible
+ var dragRectAbsolute = getRect(dragEl$1, false, true, true);
+ multiDragElements.forEach(function (multiDragElement) {
+ if (multiDragElement === dragEl$1) return;
+ setRect(multiDragElement, dragRectAbsolute); // Move element(s) to end of parentEl so that it does not interfere with multi-drag clones insertion if they are inserted
+ // while folding, and so that we can capture them again because old sortable will no longer be fromSortable
+
+ parentEl.appendChild(multiDragElement);
+ });
+ folding = true;
+ } // Clones must be shown (and check to remove multi drags) after folding when interfering multiDragElements are moved out
+
+
+ if (!isOwner) {
+ // Only remove if not folding (folding will remove them anyways)
+ if (!folding) {
+ removeMultiDragElements();
+ }
+
+ if (multiDragElements.length > 1) {
+ var clonesHiddenBefore = clonesHidden;
+
+ activeSortable._showClone(sortable); // Unfold animation for clones if showing from hidden
+
+
+ if (activeSortable.options.animation && !clonesHidden && clonesHiddenBefore) {
+ multiDragClones.forEach(function (clone) {
+ activeSortable.addAnimationState({
+ target: clone,
+ rect: clonesFromRect
+ });
+ clone.fromRect = clonesFromRect;
+ clone.thisAnimationDuration = null;
+ });
+ }
+ } else {
+ activeSortable._showClone(sortable);
+ }
+ }
+ }
+ },
+ dragOverAnimationCapture: function dragOverAnimationCapture(_ref11) {
+ var dragRect = _ref11.dragRect,
+ isOwner = _ref11.isOwner,
+ activeSortable = _ref11.activeSortable;
+ multiDragElements.forEach(function (multiDragElement) {
+ multiDragElement.thisAnimationDuration = null;
+ });
+
+ if (activeSortable.options.animation && !isOwner && activeSortable.multiDrag.isMultiDrag) {
+ clonesFromRect = _extends({}, dragRect);
+ var dragMatrix = matrix(dragEl$1, true);
+ clonesFromRect.top -= dragMatrix.f;
+ clonesFromRect.left -= dragMatrix.e;
+ }
+ },
+ dragOverAnimationComplete: function dragOverAnimationComplete() {
+ if (folding) {
+ folding = false;
+ removeMultiDragElements();
+ }
+ },
+ drop: function drop(_ref12) {
+ var evt = _ref12.originalEvent,
+ rootEl = _ref12.rootEl,
+ parentEl = _ref12.parentEl,
+ sortable = _ref12.sortable,
+ dispatchSortableEvent = _ref12.dispatchSortableEvent,
+ oldIndex = _ref12.oldIndex,
+ putSortable = _ref12.putSortable;
+ var toSortable = putSortable || this.sortable;
+ if (!evt) return;
+ var options = this.options,
+ children = parentEl.children; // Multi-drag selection
+
+ if (!dragStarted) {
+ if (options.multiDragKey && !this.multiDragKeyDown) {
+ this._deselectMultiDrag();
+ }
+
+ toggleClass(dragEl$1, options.selectedClass, !~multiDragElements.indexOf(dragEl$1));
+
+ if (!~multiDragElements.indexOf(dragEl$1)) {
+ multiDragElements.push(dragEl$1);
+ dispatchEvent({
+ sortable: sortable,
+ rootEl: rootEl,
+ name: 'select',
+ targetEl: dragEl$1,
+ originalEvt: evt
+ }); // Modifier activated, select from last to dragEl
+
+ if (evt.shiftKey && lastMultiDragSelect && sortable.el.contains(lastMultiDragSelect)) {
+ var lastIndex = index(lastMultiDragSelect),
+ currentIndex = index(dragEl$1);
+
+ if (~lastIndex && ~currentIndex && lastIndex !== currentIndex) {
+ // Must include lastMultiDragSelect (select it), in case modified selection from no selection
+ // (but previous selection existed)
+ var n, i;
+
+ if (currentIndex > lastIndex) {
+ i = lastIndex;
+ n = currentIndex;
+ } else {
+ i = currentIndex;
+ n = lastIndex + 1;
+ }
+
+ for (; i < n; i++) {
+ if (~multiDragElements.indexOf(children[i])) continue;
+ toggleClass(children[i], options.selectedClass, true);
+ multiDragElements.push(children[i]);
+ dispatchEvent({
+ sortable: sortable,
+ rootEl: rootEl,
+ name: 'select',
+ targetEl: children[i],
+ originalEvt: evt
+ });
+ }
+ }
+ } else {
+ lastMultiDragSelect = dragEl$1;
+ }
+
+ multiDragSortable = toSortable;
+ } else {
+ multiDragElements.splice(multiDragElements.indexOf(dragEl$1), 1);
+ lastMultiDragSelect = null;
+ dispatchEvent({
+ sortable: sortable,
+ rootEl: rootEl,
+ name: 'deselect',
+ targetEl: dragEl$1,
+ originalEvt: evt
+ });
+ }
+ } // Multi-drag drop
+
+
+ if (dragStarted && this.isMultiDrag) {
+ // Do not "unfold" after around dragEl if reverted
+ if ((parentEl[expando].options.sort || parentEl !== rootEl) && multiDragElements.length > 1) {
+ var dragRect = getRect(dragEl$1),
+ multiDragIndex = index(dragEl$1, ':not(.' + this.options.selectedClass + ')');
+ if (!initialFolding && options.animation) dragEl$1.thisAnimationDuration = null;
+ toSortable.captureAnimationState();
+
+ if (!initialFolding) {
+ if (options.animation) {
+ dragEl$1.fromRect = dragRect;
+ multiDragElements.forEach(function (multiDragElement) {
+ multiDragElement.thisAnimationDuration = null;
+
+ if (multiDragElement !== dragEl$1) {
+ var rect = folding ? getRect(multiDragElement) : dragRect;
+ multiDragElement.fromRect = rect; // Prepare unfold animation
+
+ toSortable.addAnimationState({
+ target: multiDragElement,
+ rect: rect
+ });
+ }
+ });
+ } // Multi drag elements are not necessarily removed from the DOM on drop, so to reinsert
+ // properly they must all be removed
+
+
+ removeMultiDragElements();
+ multiDragElements.forEach(function (multiDragElement) {
+ if (children[multiDragIndex]) {
+ parentEl.insertBefore(multiDragElement, children[multiDragIndex]);
+ } else {
+ parentEl.appendChild(multiDragElement);
+ }
+
+ multiDragIndex++;
+ }); // If initial folding is done, the elements may have changed position because they are now
+ // unfolding around dragEl, even though dragEl may not have his index changed, so update event
+ // must be fired here as Sortable will not.
+
+ if (oldIndex === index(dragEl$1)) {
+ var update = false;
+ multiDragElements.forEach(function (multiDragElement) {
+ if (multiDragElement.sortableIndex !== index(multiDragElement)) {
+ update = true;
+ return;
+ }
+ });
+
+ if (update) {
+ dispatchSortableEvent('update');
+ }
+ }
+ } // Must be done after capturing individual rects (scroll bar)
+
+
+ multiDragElements.forEach(function (multiDragElement) {
+ unsetRect(multiDragElement);
+ });
+ toSortable.animateAll();
+ }
+
+ multiDragSortable = toSortable;
+ } // Remove clones if necessary
+
+
+ if (rootEl === parentEl || putSortable && putSortable.lastPutMode !== 'clone') {
+ multiDragClones.forEach(function (clone) {
+ clone.parentNode && clone.parentNode.removeChild(clone);
+ });
+ }
+ },
+ nullingGlobal: function nullingGlobal() {
+ this.isMultiDrag = dragStarted = false;
+ multiDragClones.length = 0;
+ },
+ destroyGlobal: function destroyGlobal() {
+ this._deselectMultiDrag();
+
+ off(document, 'pointerup', this._deselectMultiDrag);
+ off(document, 'mouseup', this._deselectMultiDrag);
+ off(document, 'touchend', this._deselectMultiDrag);
+ off(document, 'keydown', this._checkKeyDown);
+ off(document, 'keyup', this._checkKeyUp);
+ },
+ _deselectMultiDrag: function _deselectMultiDrag(evt) {
+ if (typeof dragStarted !== "undefined" && dragStarted) return; // Only deselect if selection is in this sortable
+
+ if (multiDragSortable !== this.sortable) return; // Only deselect if target is not item in this sortable
+
+ if (evt && closest(evt.target, this.options.draggable, this.sortable.el, false)) return; // Only deselect if left click
+
+ if (evt && evt.button !== 0) return;
+
+ while (multiDragElements.length) {
+ var el = multiDragElements[0];
+ toggleClass(el, this.options.selectedClass, false);
+ multiDragElements.shift();
+ dispatchEvent({
+ sortable: this.sortable,
+ rootEl: this.sortable.el,
+ name: 'deselect',
+ targetEl: el,
+ originalEvt: evt
+ });
+ }
+ },
+ _checkKeyDown: function _checkKeyDown(evt) {
+ if (evt.key === this.options.multiDragKey) {
+ this.multiDragKeyDown = true;
+ }
+ },
+ _checkKeyUp: function _checkKeyUp(evt) {
+ if (evt.key === this.options.multiDragKey) {
+ this.multiDragKeyDown = false;
+ }
+ }
+ };
+ return _extends(MultiDrag, {
+ // Static methods & properties
+ pluginName: 'multiDrag',
+ utils: {
+ /**
+ * Selects the provided multi-drag item
+ * @param {HTMLElement} el The element to be selected
+ */
+ select: function select(el) {
+ var sortable = el.parentNode[expando];
+ if (!sortable || !sortable.options.multiDrag || ~multiDragElements.indexOf(el)) return;
+
+ if (multiDragSortable && multiDragSortable !== sortable) {
+ multiDragSortable.multiDrag._deselectMultiDrag();
+
+ multiDragSortable = sortable;
+ }
+
+ toggleClass(el, sortable.options.selectedClass, true);
+ multiDragElements.push(el);
+ },
+
+ /**
+ * Deselects the provided multi-drag item
+ * @param {HTMLElement} el The element to be deselected
+ */
+ deselect: function deselect(el) {
+ var sortable = el.parentNode[expando],
+ index = multiDragElements.indexOf(el);
+ if (!sortable || !sortable.options.multiDrag || !~index) return;
+ toggleClass(el, sortable.options.selectedClass, false);
+ multiDragElements.splice(index, 1);
+ }
+ },
+ eventProperties: function eventProperties() {
+ var _this3 = this;
+
+ var oldIndicies = [],
+ newIndicies = [];
+ multiDragElements.forEach(function (multiDragElement) {
+ oldIndicies.push({
+ multiDragElement: multiDragElement,
+ index: multiDragElement.sortableIndex
+ }); // multiDragElements will already be sorted if folding
+
+ var newIndex;
+
+ if (folding && multiDragElement !== dragEl$1) {
+ newIndex = -1;
+ } else if (folding) {
+ newIndex = index(multiDragElement, ':not(.' + _this3.options.selectedClass + ')');
+ } else {
+ newIndex = index(multiDragElement);
+ }
+
+ newIndicies.push({
+ multiDragElement: multiDragElement,
+ index: newIndex
+ });
+ });
+ return {
+ items: _toConsumableArray(multiDragElements),
+ clones: [].concat(multiDragClones),
+ oldIndicies: oldIndicies,
+ newIndicies: newIndicies
+ };
+ },
+ optionListeners: {
+ multiDragKey: function multiDragKey(key) {
+ key = key.toLowerCase();
+
+ if (key === 'ctrl') {
+ key = 'Control';
+ } else if (key.length > 1) {
+ key = key.charAt(0).toUpperCase() + key.substr(1);
+ }
+
+ return key;
+ }
+ }
+ });
+ }
+
+ function insertMultiDragElements(clonesInserted, rootEl) {
+ multiDragElements.forEach(function (multiDragElement, i) {
+ var target = rootEl.children[multiDragElement.sortableIndex + (clonesInserted ? Number(i) : 0)];
+
+ if (target) {
+ rootEl.insertBefore(multiDragElement, target);
+ } else {
+ rootEl.appendChild(multiDragElement);
+ }
+ });
+ }
+ /**
+ * Insert multi-drag clones
+ * @param {[Boolean]} elementsInserted Whether the multi-drag elements are inserted
+ * @param {HTMLElement} rootEl
+ */
+
+
+ function insertMultiDragClones(elementsInserted, rootEl) {
+ multiDragClones.forEach(function (clone, i) {
+ var target = rootEl.children[clone.sortableIndex + (elementsInserted ? Number(i) : 0)];
+
+ if (target) {
+ rootEl.insertBefore(clone, target);
+ } else {
+ rootEl.appendChild(clone);
+ }
+ });
+ }
+
+ function removeMultiDragElements() {
+ multiDragElements.forEach(function (multiDragElement) {
+ if (multiDragElement === dragEl$1) return;
+ multiDragElement.parentNode && multiDragElement.parentNode.removeChild(multiDragElement);
+ });
+ }
+
+ Sortable.mount(new AutoScrollPlugin());
+ Sortable.mount(Remove, Revert);
+
+ Sortable.mount(new SwapPlugin());
+ Sortable.mount(new MultiDragPlugin());
+
+ return Sortable;
+
+}));
diff --git a/js/tools/_all.js b/js/tools/_all.js
new file mode 100644
index 0000000..e250549
--- /dev/null
+++ b/js/tools/_all.js
@@ -0,0 +1,68 @@
+new Tool('eraser', {
+ cursor: 'none',
+ brushPreview: true,
+});
+new Tool('resizeeraser', {
+ cursor: 'default',
+});
+
+new Tool('eyedropper', {
+ imageCursor: 'eyedropper',
+});
+
+new Tool('fill', {
+ imageCursor: 'fill',
+});
+
+new Tool('line', {
+ cursor: 'none',
+ brushPreview: true,
+});
+new Tool('resizeline', {
+ cursor: 'default',
+});
+
+new Tool('pan', {
+ cursor: function () {
+ if (dragging) return 'url(\'/pixel-editor/pan-held.png\'), auto';
+ else return 'url(\'/pixel-editor/pan.png\'), auto';
+ },
+});
+
+new Tool('pencil', {
+ cursor: 'none',
+ brushPreview: true,
+});
+new Tool('resizebrush', {
+ cursor: 'default',
+});
+
+new Tool('rectangle', {
+ cursor: 'none',
+ brushPreview: true,
+});
+new Tool('ellipse', {
+ cursor: 'none',
+ brushPreview: true,
+});
+new Tool('resizerectangle', {
+ cursor: 'default',
+});
+
+new Tool('rectselect', {
+ cursor: 'crosshair',
+ brushPreview: true,
+});
+
+
+new Tool('moveselection', {
+ cursor: 'crosshair',
+});
+
+new Tool('zoom', {
+ imageCursor: 'zoom-in',
+});
+
+//set a default tool
+var currentTool = tool.pencil;
+var currentToolTemp = tool.pencil;
\ No newline at end of file
diff --git a/js/tools/_eraser.js b/js/tools/_eraser.js
deleted file mode 100644
index fd888fb..0000000
--- a/js/tools/_eraser.js
+++ /dev/null
@@ -1,13 +0,0 @@
-
-new Tool('eraser', {
- cursor: 'crosshair',
- brushPreview: true,
-});
-
-
-
-new Tool('resizeeraser', {
- cursor: 'default',
-});
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/js/tools/_eyedropper.js b/js/tools/_eyedropper.js
deleted file mode 100644
index cf1b0b6..0000000
--- a/js/tools/_eyedropper.js
+++ /dev/null
@@ -1,7 +0,0 @@
-
-new Tool('eyedropper', {
- imageCursor: 'eyedropper',
-});
-
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/js/tools/_fill.js b/js/tools/_fill.js
deleted file mode 100644
index 090a38e..0000000
--- a/js/tools/_fill.js
+++ /dev/null
@@ -1,7 +0,0 @@
-
-new Tool('fill', {
- imageCursor: 'fill',
-});
-
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/js/tools/_pan.js b/js/tools/_pan.js
deleted file mode 100644
index ead4854..0000000
--- a/js/tools/_pan.js
+++ /dev/null
@@ -1,10 +0,0 @@
-
-new Tool('pan', {
- cursor: function () {
- if (dragging) return 'url(\'/pixel-editor/pan-held.png\'), auto';
- else return 'url(\'/pixel-editor/pan.png\'), auto';
- },
-});
-
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/js/tools/_pencil.js b/js/tools/_pencil.js
deleted file mode 100644
index 707c177..0000000
--- a/js/tools/_pencil.js
+++ /dev/null
@@ -1,17 +0,0 @@
-
-new Tool('pencil', {
- cursor: 'crosshair',
- brushPreview: true,
-});
-
-
-new Tool('resizebrush', {
- cursor: 'default',
-});
-
-
-//set as default tool
-var currentTool = tool.pencil;
-var currentToolTemp = tool.pencil;
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/js/tools/_rectangle.js b/js/tools/_rectangle.js
deleted file mode 100644
index 677bccd..0000000
--- a/js/tools/_rectangle.js
+++ /dev/null
@@ -1,14 +0,0 @@
-
-new Tool('rectangle', {
- cursor: 'crosshair',
- brushPreview: true,
-});
-
-
-
-new Tool('resizerectangle', {
- cursor: 'default',
-});
-
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/js/tools/_select.js b/js/tools/_select.js
deleted file mode 100644
index 89052d5..0000000
--- a/js/tools/_select.js
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-new Tool('rectselect', {
- cursor: 'crosshair',
- brushPreview: true,
-});
-
-
-new Tool('moveselection', {
- cursor: 'crosshair',
-});
-
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/js/tools/_zoom.js b/js/tools/_zoom.js
deleted file mode 100644
index b3e5c72..0000000
--- a/js/tools/_zoom.js
+++ /dev/null
@@ -1,6 +0,0 @@
-
-new Tool('zoom', {
- imageCursor: 'zoom-in',
-});
-
-/*global Tool, tool*/
\ No newline at end of file
diff --git a/logs/latestLog.html b/logs/latestLog.html
new file mode 100644
index 0000000..9871410
--- /dev/null
+++ b/logs/latestLog.html
@@ -0,0 +1,66 @@
+
Latest update
+Hello there, welcome to the latest version of the Lospec Pixel Editor. As you can see, we changed
+quite a lot of things. Let's go through all them, starting from this page.
+
+
Splash page
+
+The editor now has a splash page! Besides a fancy cover image with beautiful art, on the bottom
+left of the page you'll be able to create a new custom pixel. You can also use the quickstart
+menu to quickly select a preset or load an existing file. It was designed by Skeddles himself!
+
+
+
+Pro tip: once you've created a new project, you can go back to the splash page
+by clicking on Editor -> Splash page
+
+
Canvas resizing
+
+We implemented canvas resizing! If you wanted to draw a t-rex but created a 4x4 canvas, we've got you covered!
+You can now click on Edit -> Resize canvas to decrease the size of the project. Same goes if you
+drew an ant on a 1024x1024 canvas, just go to Edit -> Resize canvas and decrease
+the dimensions.
+
+
+
+
Sprite scaling
+
+In addition to the Lospec Pixel Art Scaler,
+you can now take advantage of the editor's built-in scaling function. Just click on Edit -> Scale sprite
+to scale up or down your work. With the nearest-neighbour algorithm you'll be able to scale sprites
+in a pixel-perfect manner, while with bilinear interpolation it's possible to add (or remove, if you're scaling
+down a sprite) antialiasing.
+
+
+
+
Line tool
+
+Our contributor Liam added a new line tool! Quality of
+life improvement are planned for it, the rectangle and the rectangular selection tools.
+
+
+
+
Advanced mode: colour picker and palette block
+
+If you're a proud user of the advanced mode, you'll be able to try out the new colour picker: it
+supports 3 colour models, 6 colour harmonies and multiple ways to input data. Next to it,
+you'll find the new palette block, which lets you arrange your colours however you want, add and
+remove multiple colours at once. Changes made in the palette block will update the palette list
+you've always been familiar with.
+
+
+
+
Other changes:
+
+
You can now move colours in the palette menu
+
Use View -> Pixel grid to show the pixel grid
+
Fixed a bunch of bugs, made the brush preview pixel perfect
+
Added Layer -> Duplicate to duplicate a layer
+
Quality of life development improvements by Nkoder and Pongles
+
Canvas trimming to get rid of all the extra space you have in your sprite