├── .github ├── CONTRIBUTING.md ├── GIT-WORKFLOW.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── CHANGE.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Sortable.php ├── SortableAsset.php └── assets ├── css ├── kv-html5-sortable.css └── kv-html5-sortable.min.css └── js ├── html5sortable.js ├── html5sortable.min.js ├── kv-html5-sortable.js └── kv-html5-sortable.min.js /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to yii2-sortable 2 | ============================= 3 | Looking to contribute something to yii2-sortable? **Here's how you can help.** 4 | 5 | Using the issue tracker 6 | ----------------------- 7 | When [reporting bugs][reporting-bugs] or 8 | [requesting features][requesting-features], the 9 | [issue tracker on GitHub][issue-tracker] is the recommended channel to use. 10 | 11 | The issue tracker **is not** a place for support requests. Refer the 12 | [extension documentation and demos](http://demos.krajee.com/sortable) and/or refer to the 13 | [webtips Q & A forum](http://webtips.krajee.com/questions) which are the better places to get help. 14 | 15 | How to contribute via a pull request? 16 | ------------------------------------- 17 | Refer this [git workflow for contributors](.github/GIT-WORKFLOW.md). 18 | 19 | Reporting bugs with yii2-sortable 20 | --------------------------------- 21 | We really appreciate clear bug reports that _consistently_ show an issue 22 | within _yii2-sortable_. 23 | 24 | The ideal bug report follows these guidelines: 25 | 26 | 1. **Use the [GitHub issue search][issue-search]** — Check if the issue 27 | has already been reported. 28 | 2. **Check if the issue has been fixed** — Try to reproduce the problem 29 | using the code in the `master` branch. 30 | 3. **Isolate the problem** — Try to share a demo or a test case that 31 | consistently reproduces the problem. 32 | 33 | Please try to be as detailed as possible in your bug report, especially if an 34 | isolated test case cannot be made. Some useful questions to include the answer 35 | to are: 36 | 37 | - What steps can be used to reproduce the issue? 38 | - What is the bug and what is the expected outcome? 39 | - What browser(s) and Operating System have you tested with? 40 | - Does the bug happen consistently across all tested browsers? 41 | - What version of jQuery are you using? And what version of yii2-sortable? 42 | - Are you using yii2-sortable with other plugins? 43 | 44 | All of these questions will help others fix and identify any potential bugs. 45 | 46 | Requesting features in yii2-sortable 47 | ------------------------------------ 48 | Before starting work on a major feature for yii2-sortable, **read the 49 | [documentation](http://demos.krajee.com/sortable) first** or you may risk spending a considerable amount of 50 | time on something which the project developers are not interested in bringing into the project. 51 | 52 | Licensing 53 | --------- 54 | 55 | It should also be made clear that **all code contributed to yii2-sortable** must be 56 | licensable under the [BSD-3 license][licensing]. Code that cannot be released 57 | under this license **cannot be accepted** into the project. 58 | 59 | [issue-search]: https://github.com/kartik-v/yii2-sortable/search?q=&type=Issues 60 | [issue-tracker]: https://github.com/kartik-v/yii2-sortable/issues 61 | [licensing]: https://github.com/kartik-v/yii2-sortable/blob/master/LICENSE.md 62 | [reporting-bugs]: #reporting-bugs-with-yii2-sortable 63 | [requesting-features]: #requesting-features-in-yii2-sortable -------------------------------------------------------------------------------- /.github/GIT-WORKFLOW.md: -------------------------------------------------------------------------------- 1 | Git workflow for yii2-sortable contributors 2 | ========================================= 3 | 4 | So you want to contribute to yii2-sortable? Great! But to increase the chances of your changes being accepted quickly, please 5 | follow the following steps. If you are new to Git and GitHub, you might want to first check out [GitHub help](http://help.github.com/), [try Git](https://try.github.com) 6 | or learn something about [Git internal data model](http://nfarina.com/post/9868516270/git-is-simpler). 7 | 8 | Setup the development environment 9 | --------------------------------- 10 | 11 | Assuming you already have a yii2 development environment, carry out the following steps to create a development environment for the repo. 12 | 13 | ### 1. [Fork](http://help.github.com/fork-a-repo/) the yii2-sortable repository on GitHub and clone your fork to your development environment 14 | 15 | ``` 16 | git clone git@github.com:YOUR-GITHUB-USERNAME/yii2-sortable.git 17 | ``` 18 | 19 | If you have trouble setting up Git with GitHub in Linux, or are getting errors like "Permission Denied (publickey)", 20 | then you must [setup your Git installation to work with GitHub](http://help.github.com/linux-set-up-git/) 21 | 22 | > Tip: if you're not fluent with Git, we recommend reading excellent free [Pro Git book](https://git-scm.com/book/en/v2). 23 | 24 | ### 2. Add the main yii2-sortable repository as an additional git remote called "upstream" 25 | 26 | Change to the directory where you cloned yii2-sortable, normally, "yii2-sortable". Then enter the following command: 27 | 28 | ``` 29 | git remote add upstream git://github.com/kartik-v/yii2-sortable.git 30 | ``` 31 | 32 | ### 3. Prepare the testing environment 33 | 34 | - You should have a working yii 2 development environment in which you have already installed `yii2-sortable` and includes latest and updated `yii2-sortable` fork from source. 35 | - Ensure you have the latest `dev-master` releases of all dependent extensions via your composer updates 36 | - Ensure you use the above cloned latest `yii2-sortable` code in your testing environment 37 | 38 | **Now you have a working playground for hacking on yii2-sortable.** 39 | 40 | Working on bugs and features 41 | ---------------------------- 42 | 43 | Having prepared your development environment as explained above you can now start working on the feature or bugfix. 44 | 45 | ### 1. Make sure there is an issue created for the thing you are working on if it requires significant effort to fix 46 | 47 | All new features and bug fixes should have an associated issue to provide a single point of reference for discussion 48 | and documentation. Take a few minutes to look through the existing issue list for one that matches the contribution you 49 | intend to make. If you find one already on the issue list, then please leave a comment on that issue indicating you 50 | intend to work on that item. If you do not find an existing issue matching what you intend to work on, please 51 | open a new issue or create a pull request directly if it is straightforward fix. This will allow the team to 52 | review your suggestion, and provide appropriate feedback along the way. 53 | 54 | > For small changes or documentation issues or straightforward fixes, you don't need to create an issue, a pull request is enough in this case. 55 | 56 | ### 2. Fetch the latest code from the main yii2-sortable branch 57 | 58 | ``` 59 | git fetch upstream 60 | ``` 61 | 62 | You should start at this point for every new contribution to make sure you are working on the latest code. 63 | 64 | ### 3. Create a new branch for your feature based on the current yii2-sortable master branch 65 | 66 | > That's very important since you will not be able to submit more than one pull request from your account if you'll 67 | use master. 68 | 69 | Each separate bug fix or change should go in its own branch. Branch names should be descriptive and start with 70 | the sortable of the issue that your code relates to. If you aren't fixing any particular issue, just skip sortable. 71 | For example: 72 | 73 | ``` 74 | git checkout upstream/master 75 | git checkout -b 999-name-of-your-branch-goes-here 76 | ``` 77 | 78 | ### 4. Do your magic, write your code 79 | 80 | Make sure you have first updated the testing environment as mentioned in [prepare-the-testing-environment][prepare-the-testing-environment]. 81 | 82 | Then make sure you have the updated code with your change and it works :). 83 | 84 | Unit tests are always welcome. Tested and well covered code greatly simplifies the task of checking your contributions. 85 | Failing unit tests as issue description are also accepted. 86 | 87 | ### 5. Update the CHANGE log 88 | 89 | Edit the `CHANGE.md` file to include your change, you should insert this at the top of the file under the 90 | first heading (the version that is currently under development), the line in the change log should look like one of the following: 91 | 92 | ``` 93 | Bug #999: a description of the bug fix (Your Name) 94 | Enh #999: a description of the enhancement (Your Name) 95 | ``` 96 | 97 | `#999` is the issue sortable that the `Bug` or `Enh` is referring to. 98 | The changelog should be grouped by type (`Bug`,`Enh`) and ordered by issue sortable. 99 | 100 | For very small fixes, e.g. typos and documentation changes, there is no need to update the `CHANGE.md`. 101 | 102 | ### 6. Commit your changes 103 | 104 | add the files/changes you want to commit to the [staging area](http://gitref.org/basic/#add) with 105 | 106 | ``` 107 | git add path/to/my/file.php 108 | ``` 109 | 110 | You can use the `-p` option to select the changes you want to have in your commit. 111 | 112 | Commit your changes with a descriptive commit message. Make sure to mention the ticket sortable with `#XXX` so GitHub will 113 | automatically link your commit with the ticket: 114 | 115 | ``` 116 | git commit -m "A brief description of this change which fixes #999 goes here" 117 | ``` 118 | 119 | ### 7. Pull the latest yii2-sortable code from upstream into your branch 120 | 121 | ``` 122 | git pull upstream master 123 | ``` 124 | 125 | This ensures you have the latest code in your branch before you open your pull request. If there are any merge conflicts, 126 | you should fix them now and commit the changes again. This ensures that it's easy for the yii2-sortable team to merge your changes 127 | with one click. 128 | 129 | ### 8. Having resolved any conflicts, push your code to GitHub 130 | 131 | ``` 132 | git push -u origin 999-name-of-your-branch-goes-here 133 | ``` 134 | 135 | The `-u` parameter ensures that your branch will now automatically push and pull from the GitHub branch. That means 136 | if you type `git push` the next time it will know where to push to. This is useful if you want to later add more commits 137 | to the pull request. 138 | 139 | ### 9. Open a [pull request](http://help.github.com/send-pull-requests/) against upstream. 140 | 141 | Go to your repository on GitHub and click "Pull Request", choose your branch on the right and enter some more details 142 | in the comment box. To link the pull request to the issue put anywhere in the pull comment `#999` where 999 is the 143 | issue sortable. 144 | 145 | > Note that each pull-request should fix a single change. For multiple, unrelated changes, please open multiple pull requests. 146 | 147 | ### 10. Someone will review your code 148 | 149 | Someone will review your code, and you might be asked to make some changes, if so go to step #6 (you don't need to open 150 | another pull request if your current one is still open). If your code is accepted it will be merged into the main branch 151 | and become part of the next yii2-sortable release. If not, don't be disheartened, different people need different features and yii2-sortable 152 | can't be everything to everyone, your code will still be available on GitHub as a reference for people who need it. 153 | 154 | ### 11. Cleaning it up 155 | 156 | After your code was either accepted or declined you can delete branches you've worked with from your local repository 157 | and `origin`. 158 | 159 | ``` 160 | git checkout master 161 | git branch -D 999-name-of-your-branch-goes-here 162 | git push origin --delete 999-name-of-your-branch-goes-here 163 | ``` 164 | 165 | ### Command overview (for advanced contributors) 166 | 167 | ``` 168 | git clone git@github.com:YOUR-GITHUB-USERNAME/yii2-sortable.git 169 | git remote add upstream git://github.com/kartik-v/yii2-sortable.git 170 | ``` 171 | 172 | ``` 173 | git fetch upstream 174 | git checkout upstream/master 175 | git checkout -b 999-name-of-your-branch-goes-here 176 | 177 | /* do your magic, update changelog if needed */ 178 | 179 | git add path/to/my/file.php 180 | git commit -m "A brief description of this change which fixes #999 goes here" 181 | git pull upstream master 182 | git push -u origin 999-name-of-your-branch-goes-here 183 | ``` 184 | 185 | [prepare-the-testing-environment]: #3-prepare-the-testing-environment -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | - [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate. 4 | - [ ] The issue still exists against the latest `master` branch of yii2-sortable. 5 | - [ ] This is not an usage question. I confirm having gone through and read the [documentation and demos](http://demos.krajee.com/sortable). 6 | - [ ] This is not a general programming / coding question. (Those should be directed to the [webtips Q & A forum](http://webtips.krajee.com/questions)). 7 | - [ ] I have attempted to find the simplest possible steps to reproduce the issue. 8 | - [ ] I have included a failing test as a pull request (Optional). 9 | 10 | ## Steps to reproduce the issue 11 | 12 | 1. 13 | 2. 14 | 3. 15 | 16 | ## Expected behavior and actual behavior 17 | 18 | When I follow those steps, I see... 19 | 20 | I was expecting... 21 | 22 | ## Environment 23 | 24 | #### Browsers 25 | 26 | - [ ] Google Chrome 27 | - [ ] Mozilla Firefox 28 | - [ ] Internet Explorer 29 | - [ ] Safari 30 | 31 | #### Operating System 32 | 33 | - [ ] Windows 34 | - [ ] Mac OS X 35 | - [ ] Linux 36 | - [ ] Mobile 37 | 38 | #### Libraries 39 | 40 | - jQuery version: 41 | - yii2-sortable version: 42 | 43 | ## Isolating the problem 44 | 45 | - [ ] This bug happens [on the docs and demos page](https://demos.krajee.com/sortable) 46 | - [ ] The bug happens consistently across all tested browsers 47 | - [ ] This bug happens when using yii2-sortable without other plugins. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Scope 2 | This pull request includes a 3 | 4 | - [ ] Bug fix 5 | - [ ] New feature 6 | - [ ] Translation 7 | 8 | ## Changes 9 | The following changes were made (this change is also documented in the [change log](https://github.com/kartik-v/yii2-sortable/blob/master/CHANGE.md)): 10 | 11 | - 12 | - 13 | - 14 | 15 | ## Related Issues 16 | If this is related to an existing ticket, include a link to it as well. -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - bug 8 | - enhancement 9 | - pinned 10 | - security 11 | # Label to use when marking an issue as stale 12 | staleLabel: wontfix 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false -------------------------------------------------------------------------------- /CHANGE.md: -------------------------------------------------------------------------------- 1 | Change Log: `yii2-sortable` 2 | =========================== 3 | 4 | ## Version 1.2.2 5 | 6 | **Date:** 09-Oct-2018 7 | 8 | - Bump up composer dependencies. 9 | 10 | ## Version 1.2.1 11 | 12 | **Date:** 03-Sep-2018 13 | 14 | - (enh #17): Update to use newer plugin methods. 15 | - Updates to support bootstrap v4.x. 16 | - (enh #15, #16): Create new jquery plugin `kvHtml5Sortable` based on `html5sortable`. 17 | - Update to use latest release library of [html5sortable](https://github.com/lukasoppermann/html5sortable). 18 | - Move all relevant code to new `src` directory. 19 | - Add github contribution and issue/PR logging templates. 20 | 21 | ## Version 1.2.0 22 | 23 | **Date:** 17-Jun-2015 24 | 25 | - (enh #9): Set composer ## Version dependencies. 26 | - (enh #7): Correct documentation link. 27 | - (enh #5): Allow multiple connected sortables on single page. With this enhancement, the `connected` 28 | property will follow these rules: 29 | - if set to `false` or null/empty this widget will not be connected to any other sortable widget. 30 | - if set to `true`, this widget will be connected to all other sortable widgets on the page with `connected` property set to `true`. 31 | - if set to a string - this widget will be connected with other sortable widgets matching the same connected string value. 32 | - Upgrade sortable plugin to [use new fork]((https://github.com/voidberg/html5sortable). 33 | 34 | ## Version 1.1.0 35 | 36 | **Date:** 10-Nov-2014 37 | 38 | - Set dependency on Krajee base components 39 | - Set release to stable 40 | 41 | ## Version 1.0.0 42 | 43 | **Date:** 01-Jul-2014 44 | 45 | - Initial release 46 | - PSR4 alias change -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 - 2018, Kartik Visweswaran 2 | Krajee.com 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, this 12 | list of conditions and the following disclaimer in the documentation and/or 13 | other materials provided with the distribution. 14 | 15 | * Neither the names of Kartik Visweswaran or Krajee nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Krajee Logo 4 | 5 |
6 | yii2-sortable 7 |
8 | 10 | Donate 11 | 12 |

13 | 14 | [![Stable Version](https://poser.pugx.org/kartik-v/yii2-widget-sortable/v/stable)](https://packagist.org/packages/kartik-v/yii2-widget-sortable) 15 | [![Unstable Version](https://poser.pugx.org/kartik-v/yii2-widget-sortable/v/unstable)](https://packagist.org/packages/kartik-v/yii2-widget-sortable) 16 | [![License](https://poser.pugx.org/kartik-v/yii2-sortable/license)](https://packagist.org/packages/kartik-v/yii2-sortable) 17 | [![Total Downloads](https://poser.pugx.org/kartik-v/yii2-sortable/downloads)](https://packagist.org/packages/kartik-v/yii2-sortable) 18 | [![Monthly Downloads](https://poser.pugx.org/kartik-v/yii2-sortable/d/monthly)](https://packagist.org/packages/kartik-v/yii2-sortable) 19 | [![Daily Downloads](https://poser.pugx.org/kartik-v/yii2-sortable/d/daily)](https://packagist.org/packages/kartik-v/yii2-sortable) 20 | 21 | A Yii 2.0 widget that allows you to create sortable lists and grids and manipulate them using simple drag and drop. 22 | It is based on the lightweight [html5sortable](https://github.com/voidberg/html5sortable) jQuery plugin, which uses native HTML5 API for drag and drop. 23 | It is a leaner alternative for the JUI Sortable plugin and offers very similar functionality. The **yii2-sortable widget** offers these features: 24 | 25 | - Less than 1KB of javascript used (minified and gzipped). 26 | - Built using native HTML5 drag and drop API. 27 | - Supports both list and grid style layouts. 28 | - Similar API and behaviour to jquery-ui sortable plugin. 29 | - Works in IE 5.5+, Firefox 3.5+, Chrome 3+, Safari 3+ and, Opera 12+. 30 | 31 | ### Demo 32 | You can see detailed [documentation](http://demos.krajee.com/sortable) on usage of the extension. 33 | 34 | ## Installation 35 | 36 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 37 | 38 | > NOTE: Check the [composer.json](https://github.com/kartik-v/yii2-sortable/blob/master/composer.json) for this extension's requirements and dependencies. Read this [web tip /wiki](http://webtips.krajee.com/setting-composer-minimum-stability-application/) on setting the `minimum-stability` settings for your application's composer.json. 39 | 40 | Either run 41 | 42 | ``` 43 | $ php composer.phar require kartik-v/yii2-sortable "@dev" 44 | ``` 45 | 46 | or add 47 | 48 | ``` 49 | "kartik-v/yii2-sortable": "@dev" 50 | ``` 51 | 52 | to the ```require``` section of your `composer.json` file. 53 | 54 | ## Usage 55 | 56 | ### Sortable 57 | 58 | ```php 59 | use kartik\sortable\Sortable; 60 | echo Sortable::widget([ 61 | 'type' => Sortable::TYPE_LIST, 62 | 'items' => [ 63 | ['content' => 'Item # 1'], 64 | ['content' => 'Item # 2'], 65 | ['content' => 'Item # 3'], 66 | ] 67 | ]); 68 | ``` 69 | 70 | ## License 71 | 72 | **yii2-sortable** is released under the BSD-3-Clause License. See the bundled `LICENSE.md` for details. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kartik-v/yii2-sortable", 3 | "description": "Create sortable lists and grids using HTML5 drag and drop API for Yii 2.0.", 4 | "keywords": ["yii2", "extension", "widget", "sortable", "range", "jquery", "bootstrap"], 5 | "homepage": "https://github.com/kartik-v/yii2-sortable", 6 | "type": "yii2-extension", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Kartik Visweswaran", 11 | "email": "kartikv2@gmail.com", 12 | "homepage": "http://www.krajee.com/" 13 | } 14 | ], 15 | "require": { 16 | "kartik-v/yii2-krajee-base": ">=2.0.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "kartik\\sortable\\": "src" 21 | } 22 | }, 23 | "extra": { 24 | "branch-alias": { 25 | "dev-master": "1.2.x-dev" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Sortable.php: -------------------------------------------------------------------------------- 1 | 22 | * @since 1.0 23 | */ 24 | class Sortable extends Widget 25 | { 26 | /** 27 | * @var string list type sortable 28 | */ 29 | const TYPE_LIST = 'list'; 30 | 31 | /** 32 | * @var string grid type sortable 33 | */ 34 | const TYPE_GRID = 'grid'; 35 | 36 | /** 37 | * @var string the type of the sortable widget 38 | * Should be one of [[Sortable::TYPE]] constants. 39 | * Defaults to Sortable::TYPE_LIST 40 | */ 41 | public $type = self::TYPE_LIST; 42 | 43 | /** 44 | * @var boolean|string, whether this widget is connected to another Sortable widget. Defaults to `false`. 45 | * - if set to `false` or null/empty this widget will not be connected to any other sortable widget. 46 | * - if set to `true`, this widget will be connected to all other sortable widgets on the page with 47 | * `connected` property set to `true`. 48 | * - if set to a string - this widget will be connected with other sortable widgets matching 49 | * the same connected string value. 50 | */ 51 | public $connected = false; 52 | 53 | /** 54 | * @var boolean, whether this widget is disabled 55 | */ 56 | public $disabled = false; 57 | 58 | /** 59 | * @var boolean, whether to show handle for each sortable item. 60 | */ 61 | public $showHandle = false; 62 | 63 | /** 64 | * @var string, the handle label, this is not HTML encoded. If not set this will default to: 65 | * - ' ' if [[bsVersion]] is `4.x` 66 | * - ' ' if [[bsVersion]] is `3.x` 67 | */ 68 | public $handleLabel; 69 | 70 | /** 71 | * @var array the HTML attributes to be applied to all items. 72 | * This will be overridden by the [[options]] property within [[$items]]. 73 | */ 74 | public $itemOptions = []; 75 | 76 | /** 77 | * @var array the sortable items configuration for rendering elements within the sortable 78 | * list / grid. You can set the following properties: 79 | * - content: string, the list item content (this is not HTML encoded) 80 | * - disabled: bool, whether the list item is disabled 81 | * - options: array, the HTML attributes for the list item. 82 | */ 83 | public $items = []; 84 | 85 | /** 86 | * Initializes the widget 87 | * @throws InvalidConfigException 88 | */ 89 | public function init() 90 | { 91 | parent::init(); 92 | if (!isset($this->handleLabel)) { 93 | $icon = $this->isBs4() ? '' : ' '; 94 | $this->handleLabel = $icon . ' '; 95 | } 96 | Html::addCssClass($this->options, 'sortable ' . $this->type); 97 | if (($this->connected && is_string($this->connected)) || $this->connected === true) { 98 | $css = ($this->connected === true) ? 'kv-connected' : $this->connected; 99 | Html::addCssClass($this->options, $css); 100 | $this->pluginOptions['acceptFrom'] = ".{$css}"; 101 | } 102 | if ($this->showHandle) { 103 | $this->pluginOptions['handle'] = '.handle'; 104 | } else { 105 | Html::addCssClass($this->options, 'cursor-move'); 106 | } 107 | if ($this->hasDisabledItem() && empty($this->pluginOptions['items'])) { 108 | $this->pluginOptions['items'] = ':not(.disabled)'; 109 | } 110 | $this->registerAssets(); 111 | echo Html::beginTag('ul', $this->options); 112 | } 113 | 114 | /** 115 | * Runs the widget 116 | * 117 | * @return string|void 118 | */ 119 | public function run() 120 | { 121 | echo $this->renderItems(); 122 | echo Html::endTag('ul'); 123 | } 124 | 125 | /** 126 | * Check if there is any disabled item 127 | * 128 | * @return bool 129 | */ 130 | protected function hasDisabledItem() 131 | { 132 | foreach ($this->items as $item) { 133 | if (ArrayHelper::getValue($item, 'disabled', false)) { 134 | return true; 135 | } 136 | } 137 | return false; 138 | } 139 | 140 | /** 141 | * Render the list items for the sortable widget 142 | * 143 | * @return string 144 | */ 145 | protected function renderItems() 146 | { 147 | $items = ''; 148 | $handle = ($this->showHandle) ? Html::tag('span', $this->handleLabel, ['class' => 'handle']) : ''; 149 | foreach ($this->items as $item) { 150 | $options = ArrayHelper::getValue($item, 'options', []); 151 | $options = ArrayHelper::merge($this->itemOptions, $options); 152 | if (ArrayHelper::getValue($item, 'disabled', false)) { 153 | Html::addCssClass($options, 'disabled'); 154 | } 155 | $content = $handle . ArrayHelper::getValue($item, 'content', ''); 156 | $items .= Html::tag('li', $content, $options) . PHP_EOL; 157 | } 158 | return $items; 159 | } 160 | 161 | /** 162 | * Register client assets 163 | */ 164 | public function registerAssets() 165 | { 166 | $view = $this->getView(); 167 | SortableAsset::register($view); 168 | $this->registerPlugin('kvHtml5Sortable'); 169 | $id = 'jQuery("#' . $this->options['id'] . '")'; 170 | if ($this->disabled) { 171 | $js = "{$id}.kvHtml5Sortable('disable');"; 172 | } else { 173 | $js = "{$id}.kvHtml5Sortable('enable');"; 174 | } 175 | $view->registerJs($js); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/SortableAsset.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 1.0 17 | */ 18 | class SortableAsset extends AssetBundle 19 | { 20 | /** 21 | * @inheritdoc 22 | */ 23 | public function init() 24 | { 25 | $this->setSourcePath(__DIR__ . '/assets'); 26 | $this->setupAssets('css', ['css/kv-html5-sortable']); 27 | $this->setupAssets('js', ['js/html5sortable', 'js/kv-html5-sortable']); 28 | parent::init(); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/assets/css/kv-html5-sortable.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2018 3 | * @package yii2-sortable 4 | * @version 1.2.2 5 | * 6 | * Sortable widget styling using Bootstrap 3.0 theme 7 | * Built for Yii Framework 2.0 8 | * Author: Kartik Visweswaran 9 | * Year: 2014 10 | * For more Yii related demos visit http://demos.krajee.com 11 | */ 12 | .sortable { 13 | -moz-user-select: none; 14 | padding:0; 15 | border-radius: 4px; 16 | border: 1px solid #ddd; 17 | } 18 | 19 | .sortable li { 20 | border: 1px solid #ddd; 21 | list-style: none outside none; 22 | margin: 4px; 23 | padding: 8px; 24 | cursor: normal; 25 | } 26 | 27 | .cursor-move li{ 28 | cursor: move; 29 | } 30 | 31 | .sortable .handle { 32 | cursor: move; 33 | } 34 | 35 | .sortable li.disabled { 36 | cursor: not-allowed; 37 | } 38 | 39 | .sortable li:focus, .sortable li:hover { 40 | background-color: #eee; 41 | } 42 | 43 | .sortable.grid { 44 | overflow: hidden; 45 | } 46 | 47 | .sortable.grid li { 48 | float: left; 49 | min-width: 80px; 50 | min-height: 80px; 51 | text-align: center; 52 | } 53 | 54 | .sortable .handle { 55 | cursor: move; 56 | } 57 | 58 | .sortable.connected { 59 | min-height: 100px; 60 | min-width: 200px; 61 | } 62 | 63 | .sortable li.disabled { 64 | opacity: 0.5; 65 | } 66 | 67 | .sortable li.sortable-placeholder { 68 | background: none repeat scroll 0 0 rgba(0, 0, 0, 0); 69 | border: 1px dashed #CCCCCC; 70 | padding: 16px; 71 | } 72 | 73 | .sortable li.sortable-dragging { 74 | } -------------------------------------------------------------------------------- /src/assets/css/kv-html5-sortable.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2018 3 | * @package yii2-sortable 4 | * @version 1.2.2 5 | * 6 | * Sortable widget styling using Bootstrap 3.0 theme 7 | * Built for Yii Framework 2.0 8 | * Author: Kartik Visweswaran 9 | * Year: 2014 10 | * For more Yii related demos visit http://demos.krajee.com 11 | */.sortable{-moz-user-select:none;padding:0;border-radius:4px;border:1px solid #ddd}.sortable li{border:1px solid #ddd;list-style:none outside none;margin:4px;padding:8px;cursor:normal}.cursor-move li{cursor:move}.sortable li.disabled{cursor:not-allowed}.sortable li:focus,.sortable li:hover{background-color:#eee}.sortable.grid{overflow:hidden}.sortable.grid li{float:left;min-width:80px;min-height:80px;text-align:center}.sortable .handle{cursor:move}.sortable.connected{min-height:100px;min-width:200px}.sortable li.disabled{opacity:.5}.sortable li.sortable-placeholder{background:none repeat scroll 0 0 rgba(0,0,0,0);border:1px dashed #CCC;padding:16px} -------------------------------------------------------------------------------- /src/assets/js/html5sortable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HTML5Sortable package 3 | * https://github.com/lukasoppermann/html5sortable 4 | * 5 | * Maintained by Lukas Oppermann 6 | * 7 | * Released under the MIT license. 8 | */ 9 | var sortable = (function () { 10 | 'use strict'; 11 | 12 | /** 13 | * Get or set data on element 14 | * @param {HTMLElement} element 15 | * @param {string} key 16 | * @param {any} value 17 | * @return {*} 18 | */ 19 | function addData(element, key, value) { 20 | if (value === undefined) { 21 | return element && element.h5s && element.h5s.data && element.h5s.data[key]; 22 | } 23 | else { 24 | element.h5s = element.h5s || {}; 25 | element.h5s.data = element.h5s.data || {}; 26 | element.h5s.data[key] = value; 27 | } 28 | } 29 | /** 30 | * Remove data from element 31 | * @param {HTMLElement} element 32 | */ 33 | function removeData(element) { 34 | if (element.h5s) { 35 | delete element.h5s.data; 36 | } 37 | } 38 | 39 | function _filter (nodes, selector) { 40 | if (!(nodes instanceof NodeList || nodes instanceof HTMLCollection || nodes instanceof Array)) { 41 | throw new Error('You must provide a nodeList/HTMLCollection/Array of elements to be filtered.'); 42 | } 43 | if (typeof selector !== 'string') { 44 | return Array.from(nodes); 45 | } 46 | return Array.from(nodes).filter(function (item) { return item.nodeType === 1 && item.matches(selector); }); 47 | } 48 | 49 | /* eslint-env browser */ 50 | var stores = new Map(); 51 | /** 52 | * Stores data & configurations per Sortable 53 | * @param {Object} config 54 | */ 55 | var Store = (function () { 56 | function Store() { 57 | this._config = new Map(); // eslint-disable-line no-undef 58 | this._placeholder = undefined; // eslint-disable-line no-undef 59 | this._data = new Map(); // eslint-disable-line no-undef 60 | } 61 | Object.defineProperty(Store.prototype, "config", { 62 | /** 63 | * get the configuration map of a class instance 64 | * @method config 65 | * @return {object} 66 | */ 67 | get: function () { 68 | // transform Map to object 69 | var config = {}; 70 | this._config.forEach(function (value, key) { 71 | config[key] = value; 72 | }); 73 | // return object 74 | return config; 75 | }, 76 | /** 77 | * set the configuration of a class instance 78 | * @method config 79 | * @param {object} config object of configurations 80 | */ 81 | set: function (config) { 82 | if (typeof config !== 'object') { 83 | throw new Error('You must provide a valid configuration object to the config setter.'); 84 | } 85 | // combine config with default 86 | var mergedConfig = Object.assign({}, config); 87 | // add config to map 88 | this._config = new Map(Object.entries(mergedConfig)); 89 | }, 90 | enumerable: true, 91 | configurable: true 92 | }); 93 | /** 94 | * set individual configuration of a class instance 95 | * @method setConfig 96 | * @param key valid configuration key 97 | * @param value any value 98 | * @return void 99 | */ 100 | Store.prototype.setConfig = function (key, value) { 101 | if (!this._config.has(key)) { 102 | throw new Error("Trying to set invalid configuration item: " + key); 103 | } 104 | // set config 105 | this._config.set(key, value); 106 | }; 107 | /** 108 | * get an individual configuration of a class instance 109 | * @method getConfig 110 | * @param key valid configuration key 111 | * @return any configuration value 112 | */ 113 | Store.prototype.getConfig = function (key) { 114 | if (!this._config.has(key)) { 115 | throw new Error("Invalid configuration item requested: " + key); 116 | } 117 | return this._config.get(key); 118 | }; 119 | Object.defineProperty(Store.prototype, "placeholder", { 120 | /** 121 | * get the placeholder for a class instance 122 | * @method placeholder 123 | * @return {HTMLElement|null} 124 | */ 125 | get: function () { 126 | return this._placeholder; 127 | }, 128 | /** 129 | * set the placeholder for a class instance 130 | * @method placeholder 131 | * @param {HTMLElement} placeholder 132 | * @return {void} 133 | */ 134 | set: function (placeholder) { 135 | if (!(placeholder instanceof HTMLElement) && placeholder !== null) { 136 | throw new Error('A placeholder must be an html element or null.'); 137 | } 138 | this._placeholder = placeholder; 139 | }, 140 | enumerable: true, 141 | configurable: true 142 | }); 143 | /** 144 | * set an data entry 145 | * @method setData 146 | * @param {string} key 147 | * @param {any} value 148 | * @return {void} 149 | */ 150 | Store.prototype.setData = function (key, value) { 151 | if (typeof key !== 'string') { 152 | throw new Error("The key must be a string."); 153 | } 154 | this._data.set(key, value); 155 | }; 156 | /** 157 | * get an data entry 158 | * @method getData 159 | * @param {string} key an existing key 160 | * @return {any} 161 | */ 162 | Store.prototype.getData = function (key) { 163 | if (typeof key !== 'string') { 164 | throw new Error("The key must be a string."); 165 | } 166 | return this._data.get(key); 167 | }; 168 | /** 169 | * delete an data entry 170 | * @method deleteData 171 | * @param {string} key an existing key 172 | * @return {boolean} 173 | */ 174 | Store.prototype.deleteData = function (key) { 175 | if (typeof key !== 'string') { 176 | throw new Error("The key must be a string."); 177 | } 178 | return this._data.delete(key); 179 | }; 180 | return Store; 181 | }()); 182 | function store (sortableElement) { 183 | // if sortableElement is wrong type 184 | if (!(sortableElement instanceof HTMLElement)) { 185 | throw new Error('Please provide a sortable to the store function.'); 186 | } 187 | // create new instance if not avilable 188 | if (!stores.has(sortableElement)) { 189 | stores.set(sortableElement, new Store()); 190 | } 191 | // return instance 192 | return stores.get(sortableElement); 193 | } 194 | 195 | /** 196 | * @param {Array|HTMLElement} element 197 | * @param {Function} callback 198 | * @param {string} event 199 | */ 200 | function addEventListener(element, eventName, callback) { 201 | if (element instanceof Array) { 202 | for (var i = 0; i < element.length; ++i) { 203 | addEventListener(element[i], eventName, callback); 204 | } 205 | return; 206 | } 207 | element.addEventListener(eventName, callback); 208 | store(element).setData("event" + eventName, callback); 209 | } 210 | /** 211 | * @param {Array|HTMLElement} element 212 | * @param {string} eventName 213 | */ 214 | function removeEventListener(element, eventName) { 215 | if (element instanceof Array) { 216 | for (var i = 0; i < element.length; ++i) { 217 | removeEventListener(element[i], eventName); 218 | } 219 | return; 220 | } 221 | element.removeEventListener(eventName, store(element).getData("event" + eventName)); 222 | store(element).deleteData("event" + eventName); 223 | } 224 | 225 | /** 226 | * @param {Array|HTMLElement} element 227 | * @param {string} attribute 228 | * @param {string} value 229 | */ 230 | function addAttribute(element, attribute, value) { 231 | if (element instanceof Array) { 232 | for (var i = 0; i < element.length; ++i) { 233 | addAttribute(element[i], attribute, value); 234 | } 235 | return; 236 | } 237 | element.setAttribute(attribute, value); 238 | } 239 | /** 240 | * @param {Array|HTMLElement} element 241 | * @param {string} attribute 242 | */ 243 | function removeAttribute(element, attribute) { 244 | if (element instanceof Array) { 245 | for (var i = 0; i < element.length; ++i) { 246 | removeAttribute(element[i], attribute); 247 | } 248 | return; 249 | } 250 | element.removeAttribute(attribute); 251 | } 252 | 253 | function offset (element) { 254 | if (!element.parentElement || element.getClientRects().length === 0) { 255 | throw new Error('target element must be part of the dom'); 256 | } 257 | var rect = element.getClientRects()[0]; 258 | return { 259 | left: rect.left + window.scrollX, 260 | right: rect.right + window.scrollX, 261 | top: rect.top + window.scrollY, 262 | bottom: rect.bottom + window.scrollY 263 | }; 264 | } 265 | 266 | function _debounce (func, wait) { 267 | if (wait === void 0) { wait = 0; } 268 | var timeout; 269 | return function () { 270 | var args = []; 271 | for (var _i = 0; _i < arguments.length; _i++) { 272 | args[_i - 0] = arguments[_i]; 273 | } 274 | clearTimeout(timeout); 275 | timeout = setTimeout(function () { 276 | func.apply(void 0, args); 277 | }, wait); 278 | }; 279 | } 280 | 281 | function index (element, elementList) { 282 | if (!(element instanceof HTMLElement) || !(elementList instanceof NodeList || elementList instanceof HTMLCollection || elementList instanceof Array)) { 283 | throw new Error('You must provide an element and a list of elements.'); 284 | } 285 | return Array.from(elementList).indexOf(element); 286 | } 287 | 288 | function isInDom (element) { 289 | if (!(element instanceof HTMLElement)) { 290 | throw new Error('Element is not a node element.'); 291 | } 292 | return element.parentNode !== null; 293 | } 294 | 295 | /* eslint-env browser */ 296 | /** 297 | * Insert node before or after target 298 | * @param {HTMLElement} referenceNode - reference element 299 | * @param {HTMLElement} newElement - element to be inserted 300 | * @param {String} position - insert before or after reference element 301 | */ 302 | var insertNode = function (referenceNode, newElement, position) { 303 | if (!(referenceNode instanceof HTMLElement) || !(referenceNode.parentElement instanceof HTMLElement)) { 304 | throw new Error('target and element must be a node'); 305 | } 306 | referenceNode.parentElement.insertBefore(newElement, (position === 'before' ? referenceNode : referenceNode.nextElementSibling)); 307 | }; 308 | /** 309 | * Insert before target 310 | * @param {HTMLElement} target 311 | * @param {HTMLElement} element 312 | */ 313 | var insertBefore = function (target, element) { return insertNode(target, element, 'before'); }; 314 | /** 315 | * Insert after target 316 | * @param {HTMLElement} target 317 | * @param {HTMLElement} element 318 | */ 319 | var insertAfter = function (target, element) { return insertNode(target, element, 'after'); }; 320 | 321 | function _serialize (sortableContainer, customItemSerializer, customContainerSerializer) { 322 | if (customItemSerializer === void 0) { customItemSerializer = function (serializedItem, sortableContainer) { return serializedItem; }; } 323 | if (customContainerSerializer === void 0) { customContainerSerializer = function (serializedContainer) { return serializedContainer; }; } 324 | // check for valid sortableContainer 325 | if (!(sortableContainer instanceof HTMLElement) || !sortableContainer.isSortable === true) { 326 | throw new Error('You need to provide a sortableContainer to be serialized.'); 327 | } 328 | // check for valid serializers 329 | if (typeof customItemSerializer !== 'function' || typeof customContainerSerializer !== 'function') { 330 | throw new Error('You need to provide a valid serializer for items and the container.'); 331 | } 332 | // get options 333 | var options = addData(sortableContainer, 'opts'); 334 | var item = options.items; 335 | // serialize container 336 | var items = _filter(sortableContainer.children, item); 337 | var serializedItems = items.map(function (item) { 338 | return { 339 | parent: sortableContainer, 340 | node: item, 341 | html: item.outerHTML, 342 | index: index(item, items) 343 | }; 344 | }); 345 | // serialize container 346 | var container = { 347 | node: sortableContainer, 348 | itemCount: serializedItems.length 349 | }; 350 | return { 351 | container: customContainerSerializer(container), 352 | items: serializedItems.map(function (item) { return customItemSerializer(item, sortableContainer); }) 353 | }; 354 | } 355 | 356 | function _makePlaceholder (sortableElement, placeholder, placeholderClass) { 357 | if (placeholderClass === void 0) { placeholderClass = 'sortable-placeholder'; } 358 | if (!(sortableElement instanceof HTMLElement)) { 359 | throw new Error('You must provide a valid element as a sortable.'); 360 | } 361 | // if placeholder is not an element 362 | if (!(placeholder instanceof HTMLElement) && placeholder !== undefined) { 363 | throw new Error('You must provide a valid element as a placeholder or set ot to undefined.'); 364 | } 365 | // if no placeholder element is given 366 | if (placeholder === undefined) { 367 | if (['UL', 'OL'].includes(sortableElement.tagName)) { 368 | placeholder = document.createElement('li'); 369 | } 370 | else if (['TABLE', 'TBODY'].includes(sortableElement.tagName)) { 371 | placeholder = document.createElement('tr'); 372 | // set colspan to always all rows, otherwise the item can only be dropped in first column 373 | placeholder.innerHTML = ''; 374 | } 375 | else { 376 | placeholder = document.createElement('div'); 377 | } 378 | } 379 | // add classes to placeholder 380 | if (typeof placeholderClass === 'string') { 381 | (_a = placeholder.classList).add.apply(_a, placeholderClass.split(' ')); 382 | } 383 | return placeholder; 384 | var _a; 385 | } 386 | 387 | function _getElementHeight (element) { 388 | if (!(element instanceof HTMLElement)) { 389 | throw new Error('You must provide a valid dom element'); 390 | } 391 | // get calculated style of element 392 | var style = window.getComputedStyle(element); 393 | // pick applicable properties, convert to int and reduce by adding 394 | return ['height', 'padding-top', 'padding-bottom'] 395 | .map(function (key) { 396 | var int = parseInt(style.getPropertyValue(key), 10); 397 | return isNaN(int) ? 0 : int; 398 | }) 399 | .reduce(function (sum, value) { return sum + value; }); 400 | } 401 | 402 | function _getHandles (items, selector) { 403 | if (!(items instanceof Array)) { 404 | throw new Error('You must provide a Array of HTMLElements to be filtered.'); 405 | } 406 | if (typeof selector !== 'string') { 407 | return items; 408 | } 409 | return items 410 | .filter(function (item) { 411 | return item.querySelector(selector) instanceof HTMLElement; 412 | }) 413 | .map(function (item) { 414 | return item.querySelector(selector); 415 | }); 416 | } 417 | 418 | /** 419 | * defaultDragImage returns the current item as dragged image 420 | * @param {HTMLElement} draggedElement - the item that the user drags 421 | * @param {object} elementOffset - an object with the offsets top, left, right & bottom 422 | * @param {Event} event - the original drag event object 423 | * @return {object} with element, posX and posY properties 424 | */ 425 | var defaultDragImage = function (draggedElement, elementOffset, event) { 426 | return { 427 | element: draggedElement, 428 | posX: event.pageX - elementOffset.left, 429 | posY: event.pageY - elementOffset.top 430 | }; 431 | }; 432 | function setDragImage (event, draggedElement, customDragImage) { 433 | // check if event is provided 434 | if (!(event instanceof Event)) { 435 | throw new Error('setDragImage requires a DragEvent as the first argument.'); 436 | } 437 | // check if draggedElement is provided 438 | if (!(draggedElement instanceof HTMLElement)) { 439 | throw new Error('setDragImage requires the dragged element as the second argument.'); 440 | } 441 | // set default function of none provided 442 | if (!customDragImage) { 443 | customDragImage = defaultDragImage; 444 | } 445 | // check if setDragImage method is available 446 | if (event.dataTransfer && event.dataTransfer.setDragImage) { 447 | // get the elements offset 448 | var elementOffset = offset(draggedElement); 449 | // get the dragImage 450 | var dragImage = customDragImage(draggedElement, elementOffset, event); 451 | // check if custom function returns correct values 452 | if (!(dragImage.element instanceof HTMLElement) || typeof dragImage.posX !== 'number' || typeof dragImage.posY !== 'number') { 453 | throw new Error('The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].'); 454 | } 455 | // needs to be set for HTML5 drag & drop to work 456 | event.dataTransfer.effectAllowed = 'copyMove'; 457 | // Firefox requires arbitrary content in setData for the drag & drop functionality to work 458 | event.dataTransfer.setData('text/plain', 'arbitrary'); 459 | // set the drag image on the event 460 | event.dataTransfer.setDragImage(dragImage.element, dragImage.posX, dragImage.posY); 461 | } 462 | } 463 | 464 | function _listsConnected (destination, origin) { 465 | // check if valid sortable 466 | if (destination.isSortable === true) { 467 | var acceptFrom = store(destination).getConfig('acceptFrom'); 468 | // check if acceptFrom is valid 469 | if (acceptFrom !== null && acceptFrom !== false && typeof acceptFrom !== 'string') { 470 | throw new Error('HTML5Sortable: Wrong argument, "acceptFrom" must be "null", "false", or a valid selector string.'); 471 | } 472 | if (acceptFrom !== null) { 473 | return acceptFrom !== false && acceptFrom.split(',').filter(function (sel) { 474 | return sel.length > 0 && origin.matches(sel); 475 | }).length > 0; 476 | } 477 | // drop in same list 478 | if (destination === origin) { 479 | return true; 480 | } 481 | // check if lists are connected with connectWith 482 | if (store(destination).getConfig('connectWith') !== undefined && store(destination).getConfig('connectWith') !== null) { 483 | return store(destination).getConfig('connectWith') === store(origin).getConfig('connectWith'); 484 | } 485 | } 486 | return false; 487 | } 488 | 489 | var defaultConfiguration = { 490 | items: null, 491 | // deprecated 492 | connectWith: null, 493 | // deprecated 494 | disableIEFix: null, 495 | acceptFrom: null, 496 | copy: false, 497 | placeholder: null, 498 | placeholderClass: 'sortable-placeholder', 499 | draggingClass: 'sortable-dragging', 500 | hoverClass: false, 501 | debounce: 0, 502 | throttleTime: 100, 503 | maxItems: 0, 504 | itemSerializer: undefined, 505 | containerSerializer: undefined, 506 | customDragImage: null 507 | }; 508 | 509 | /** 510 | * make sure a function is only called once within the given amount of time 511 | * @param {Function} fn the function to throttle 512 | * @param {number} threshold time limit for throttling 513 | */ 514 | // must use function to keep this context 515 | function _throttle (fn, threshold) { 516 | var _this = this; 517 | if (threshold === void 0) { threshold = 250; } 518 | // check function 519 | if (typeof fn !== 'function') { 520 | throw new Error('You must provide a function as the first argument for throttle.'); 521 | } 522 | // check threshold 523 | if (typeof threshold !== 'number') { 524 | throw new Error('You must provide a number as the second argument for throttle.'); 525 | } 526 | var lastEventTimestamp = null; 527 | return function () { 528 | var args = []; 529 | for (var _i = 0; _i < arguments.length; _i++) { 530 | args[_i - 0] = arguments[_i]; 531 | } 532 | var now = Date.now(); 533 | if (lastEventTimestamp === null || now - lastEventTimestamp >= threshold) { 534 | lastEventTimestamp = now; 535 | fn.apply(_this, args); 536 | } 537 | }; 538 | } 539 | 540 | function enableHoverClass (sortableContainer, enable) { 541 | if (typeof store(sortableContainer).getConfig('hoverClass') === 'string') { 542 | var hoverClasses_1 = store(sortableContainer).getConfig('hoverClass').split(' '); 543 | // add class on hover 544 | if (enable === true) { 545 | addEventListener(sortableContainer, 'mousemove', _throttle(function (event) { 546 | // check of no mouse button was pressed when mousemove started == no drag 547 | if (event.buttons === 0) { 548 | _filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(function (item) { 549 | if (item !== event.target) { 550 | (_a = item.classList).remove.apply(_a, hoverClasses_1); 551 | } 552 | else { 553 | (_b = item.classList).add.apply(_b, hoverClasses_1); 554 | } 555 | var _a, _b; 556 | }); 557 | } 558 | }, store(sortableContainer).getConfig('throttleTime'))); 559 | // remove class on leave 560 | addEventListener(sortableContainer, 'mouseleave', function () { 561 | _filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(function (item) { 562 | (_a = item.classList).remove.apply(_a, hoverClasses_1); 563 | var _a; 564 | }); 565 | }); 566 | } 567 | else { 568 | removeEventListener(sortableContainer, 'mousemove'); 569 | removeEventListener(sortableContainer, 'mouseleave'); 570 | } 571 | } 572 | } 573 | 574 | /* eslint-env browser */ 575 | /* 576 | * variables global to the plugin 577 | */ 578 | var dragging; 579 | var draggingHeight; 580 | /* 581 | * Keeps track of the initialy selected list, where 'dragstart' event was triggered 582 | * It allows us to move the data in between individual Sortable List instances 583 | */ 584 | // Origin List - data from before any item was changed 585 | var originContainer; 586 | var originIndex; 587 | var originElementIndex; 588 | var originItemsBeforeUpdate; 589 | // Destination List - data from before any item was changed 590 | var destinationItemsBeforeUpdate; 591 | /** 592 | * remove event handlers from items 593 | * @param {Array|NodeList} items 594 | */ 595 | var _removeItemEvents = function (items) { 596 | removeEventListener(items, 'dragstart'); 597 | removeEventListener(items, 'dragend'); 598 | removeEventListener(items, 'dragover'); 599 | removeEventListener(items, 'dragenter'); 600 | removeEventListener(items, 'drop'); 601 | removeEventListener(items, 'mouseenter'); 602 | removeEventListener(items, 'mouseleave'); 603 | }; 604 | /** 605 | * _getDragging returns the current element to drag or 606 | * a copy of the element. 607 | * Is Copy Active for sortable 608 | * @param {HTMLElement} draggedItem - the item that the user drags 609 | * @param {HTMLElement} sortable a single sortable 610 | */ 611 | var _getDragging = function (draggedItem, sortable) { 612 | var ditem = draggedItem; 613 | if (store(sortable).getConfig('copy') === true) { 614 | ditem = draggedItem.cloneNode(true); 615 | addAttribute(ditem, 'aria-copied', 'true'); 616 | draggedItem.parentElement.appendChild(ditem); 617 | ditem.style.display = 'none'; 618 | ditem.oldDisplay = draggedItem.style.display; 619 | } 620 | return ditem; 621 | }; 622 | /** 623 | * Remove data from sortable 624 | * @param {HTMLElement} sortable a single sortable 625 | */ 626 | var _removeSortableData = function (sortable) { 627 | removeData(sortable); 628 | removeAttribute(sortable, 'aria-dropeffect'); 629 | }; 630 | /** 631 | * Remove data from items 632 | * @param {Array|HTMLElement} items 633 | */ 634 | var _removeItemData = function (items) { 635 | removeAttribute(items, 'aria-grabbed'); 636 | removeAttribute(items, 'aria-copied'); 637 | removeAttribute(items, 'draggable'); 638 | removeAttribute(items, 'role'); 639 | }; 640 | /** 641 | * find sortable from element. travels up parent element until found or null. 642 | * @param {HTMLElement} element a single sortable 643 | */ 644 | function findSortable(element) { 645 | while (element.isSortable !== true) { 646 | element = element.parentElement; 647 | } 648 | return element; 649 | } 650 | /** 651 | * Dragging event is on the sortable element. finds the top child that 652 | * contains the element. 653 | * @param {HTMLElement} sortableElement a single sortable 654 | * @param {HTMLElement} element is that being dragged 655 | */ 656 | function findDragElement(sortableElement, element) { 657 | var options = addData(sortableElement, 'opts'); 658 | var items = _filter(sortableElement.children, options.items); 659 | var itemlist = items.filter(function (ele) { 660 | return ele.contains(element); 661 | }); 662 | return itemlist.length > 0 ? itemlist[0] : element; 663 | } 664 | /** 665 | * Destroy the sortable 666 | * @param {HTMLElement} sortableElement a single sortable 667 | */ 668 | var _destroySortable = function (sortableElement) { 669 | var opts = addData(sortableElement, 'opts') || {}; 670 | var items = _filter(sortableElement.children, opts.items); 671 | var handles = _getHandles(items, opts.handle); 672 | // remove event handlers & data from sortable 673 | removeEventListener(sortableElement, 'dragover'); 674 | removeEventListener(sortableElement, 'dragenter'); 675 | removeEventListener(sortableElement, 'drop'); 676 | // remove event data from sortable 677 | _removeSortableData(sortableElement); 678 | // remove event handlers & data from items 679 | removeEventListener(handles, 'mousedown'); 680 | _removeItemEvents(items); 681 | _removeItemData(items); 682 | }; 683 | /** 684 | * Enable the sortable 685 | * @param {HTMLElement} sortableElement a single sortable 686 | */ 687 | var _enableSortable = function (sortableElement) { 688 | var opts = addData(sortableElement, 'opts'); 689 | var items = _filter(sortableElement.children, opts.items); 690 | var handles = _getHandles(items, opts.handle); 691 | addAttribute(sortableElement, 'aria-dropeffect', 'move'); 692 | addData(sortableElement, '_disabled', 'false'); 693 | addAttribute(handles, 'draggable', 'true'); 694 | // @todo: remove this fix 695 | // IE FIX for ghost 696 | // can be disabled as it has the side effect that other events 697 | // (e.g. click) will be ignored 698 | if (opts.disableIEFix === false) { 699 | var spanEl = (document || window.document).createElement('span'); 700 | if (typeof spanEl.dragDrop === 'function') { 701 | addEventListener(handles, 'mousedown', function () { 702 | if (items.indexOf(this) !== -1) { 703 | this.dragDrop(); 704 | } 705 | else { 706 | var parent = this.parentElement; 707 | while (items.indexOf(parent) === -1) { 708 | parent = parent.parentElement; 709 | } 710 | parent.dragDrop(); 711 | } 712 | }); 713 | } 714 | } 715 | }; 716 | /** 717 | * Disable the sortable 718 | * @param {HTMLElement} sortableElement a single sortable 719 | */ 720 | var _disableSortable = function (sortableElement) { 721 | var opts = addData(sortableElement, 'opts'); 722 | var items = _filter(sortableElement.children, opts.items); 723 | var handles = _getHandles(items, opts.handle); 724 | addAttribute(sortableElement, 'aria-dropeffect', 'none'); 725 | addData(sortableElement, '_disabled', 'true'); 726 | addAttribute(handles, 'draggable', 'false'); 727 | removeEventListener(handles, 'mousedown'); 728 | }; 729 | /** 730 | * Reload the sortable 731 | * @param {HTMLElement} sortableElement a single sortable 732 | * @description events need to be removed to not be double bound 733 | */ 734 | var _reloadSortable = function (sortableElement) { 735 | var opts = addData(sortableElement, 'opts'); 736 | var items = _filter(sortableElement.children, opts.items); 737 | var handles = _getHandles(items, opts.handle); 738 | addData(sortableElement, '_disabled', 'false'); 739 | // remove event handlers from items 740 | _removeItemEvents(items); 741 | removeEventListener(handles, 'mousedown'); 742 | // remove event handlers from sortable 743 | removeEventListener(sortableElement, 'dragover'); 744 | removeEventListener(sortableElement, 'dragenter'); 745 | removeEventListener(sortableElement, 'drop'); 746 | }; 747 | /** 748 | * Public sortable object 749 | * @param {Array|NodeList} sortableElements 750 | * @param {object|string} options|method 751 | */ 752 | function sortable(sortableElements, options) { 753 | // get method string to see if a method is called 754 | var method = String(options); 755 | // merge user options with defaultss 756 | options = Object.assign({ 757 | connectWith: null, 758 | acceptFrom: null, 759 | copy: false, 760 | placeholder: null, 761 | disableIEFix: null, 762 | placeholderClass: 'sortable-placeholder', 763 | draggingClass: 'sortable-dragging', 764 | hoverClass: false, 765 | debounce: 0, 766 | maxItems: 0, 767 | itemSerializer: undefined, 768 | containerSerializer: undefined, 769 | customDragImage: null, 770 | items: null 771 | }, (typeof options === 'object') ? options : {}); 772 | // check if the user provided a selector instead of an element 773 | if (typeof sortableElements === 'string') { 774 | sortableElements = document.querySelectorAll(sortableElements); 775 | } 776 | // if the user provided an element, return it in an array to keep the return value consistant 777 | if (sortableElements instanceof HTMLElement) { 778 | sortableElements = [sortableElements]; 779 | } 780 | sortableElements = Array.prototype.slice.call(sortableElements); 781 | if (/serialize/.test(method)) { 782 | return sortableElements.map(function (sortableContainer) { 783 | var opts = addData(sortableContainer, 'opts'); 784 | return _serialize(sortableContainer, opts.itemSerializer, opts.containerSerializer); 785 | }); 786 | } 787 | sortableElements.forEach(function (sortableElement) { 788 | if (/enable|disable|destroy/.test(method)) { 789 | return sortable[method](sortableElement); 790 | } 791 | // log deprecation 792 | ['connectWith', 'disableIEFix'].forEach(function (configKey) { 793 | if (options.hasOwnProperty(configKey) && options[configKey] !== null) { 794 | console.warn("HTML5Sortable: You are using the deprecated configuration \"" + configKey + "\". This will be removed in an upcoming version, make sure to migrate to the new options when updating."); 795 | } 796 | }); 797 | // merge options with default options 798 | options = Object.assign({}, defaultConfiguration, options); 799 | // init data store for sortable 800 | store(sortableElement).config = options; 801 | // get options & set options on sortable 802 | options = addData(sortableElement, 'opts') || options; 803 | addData(sortableElement, 'opts', options); 804 | // property to define as sortable 805 | sortableElement.isSortable = true; 806 | // reset sortable 807 | _reloadSortable(sortableElement); 808 | // initialize 809 | var listItems = _filter(sortableElement.children, options.items); 810 | // create element if user defined a placeholder element as a string 811 | var customPlaceholder; 812 | if (options.placeholder !== null && options.placeholder !== undefined) { 813 | var tempContainer = document.createElement(sortableElement.tagName); 814 | tempContainer.innerHTML = options.placeholder; 815 | customPlaceholder = tempContainer.children[0]; 816 | } 817 | // add placeholder 818 | store(sortableElement).placeholder = _makePlaceholder(sortableElement, customPlaceholder, options.placeholderClass); 819 | addData(sortableElement, 'items', options.items); 820 | if (options.acceptFrom) { 821 | addData(sortableElement, 'acceptFrom', options.acceptFrom); 822 | } 823 | else if (options.connectWith) { 824 | addData(sortableElement, 'connectWith', options.connectWith); 825 | } 826 | _enableSortable(sortableElement); 827 | addAttribute(listItems, 'role', 'option'); 828 | addAttribute(listItems, 'aria-grabbed', 'false'); 829 | // enable hover class 830 | enableHoverClass(sortableElement, true); 831 | /* 832 | Handle drag events on draggable items 833 | Handle is set at the sortableElement level as it will bubble up 834 | from the item 835 | */ 836 | addEventListener(sortableElement, 'dragstart', function (e) { 837 | // ignore dragstart events 838 | if (e.target.isSortable === true) { 839 | return; 840 | } 841 | e.stopImmediatePropagation(); 842 | if ((options.handle && !e.target.matches(options.handle)) || e.target.getAttribute('draggable') === 'false') { 843 | return; 844 | } 845 | var sortableContainer = findSortable(e.target); 846 | var dragItem = findDragElement(sortableContainer, e.target); 847 | // grab values 848 | originItemsBeforeUpdate = _filter(sortableContainer.children, options.items); 849 | originIndex = originItemsBeforeUpdate.indexOf(dragItem); 850 | originElementIndex = index(dragItem, sortableContainer.children); 851 | originContainer = sortableContainer; 852 | // add transparent clone or other ghost to cursor 853 | setDragImage(e, dragItem, options.customDragImage); 854 | // cache selsection & add attr for dragging 855 | draggingHeight = _getElementHeight(dragItem); 856 | dragItem.classList.add(options.draggingClass); 857 | dragging = _getDragging(dragItem, sortableContainer); 858 | addAttribute(dragging, 'aria-grabbed', 'true'); 859 | // dispatch sortstart event on each element in group 860 | sortableContainer.dispatchEvent(new CustomEvent('sortstart', { 861 | detail: { 862 | origin: { 863 | elementIndex: originElementIndex, 864 | index: originIndex, 865 | container: originContainer 866 | }, 867 | item: dragging 868 | } 869 | })); 870 | }); 871 | /* 872 | We are capturing targetSortable before modifications with 'dragenter' event 873 | */ 874 | addEventListener(sortableElement, 'dragenter', function (e) { 875 | if (e.target.isSortable === true) { 876 | return; 877 | } 878 | var sortableContainer = findSortable(e.target); 879 | destinationItemsBeforeUpdate = _filter(sortableContainer.children, addData(sortableContainer, 'items')) 880 | .filter(function (item) { return item !== store(sortableElement).placeholder; }); 881 | }); 882 | /* 883 | * Dragend Event - https://developer.mozilla.org/en-US/docs/Web/Events/dragend 884 | * Fires each time dragEvent end, or ESC pressed 885 | * We are using it to clean up any draggable elements and placeholders 886 | */ 887 | addEventListener(sortableElement, 'dragend', function (e) { 888 | if (!dragging) { 889 | return; 890 | } 891 | dragging.classList.remove(options.draggingClass); 892 | addAttribute(dragging, 'aria-grabbed', 'false'); 893 | if (dragging.getAttribute('aria-copied') === 'true' && addData(dragging, 'dropped') !== 'true') { 894 | dragging.remove(); 895 | } 896 | dragging.style.display = dragging.oldDisplay; 897 | delete dragging.oldDisplay; 898 | var visiblePlaceholder = Array.from(stores.values()).map(function (data) { return data.placeholder; }) 899 | .filter(function (placeholder) { return placeholder instanceof HTMLElement; }) 900 | .filter(isInDom)[0]; 901 | if (visiblePlaceholder) { 902 | visiblePlaceholder.remove(); 903 | } 904 | // dispatch sortstart event on each element in group 905 | sortableElement.dispatchEvent(new CustomEvent('sortstop', { 906 | detail: { 907 | origin: { 908 | elementIndex: originElementIndex, 909 | index: originIndex, 910 | container: originContainer 911 | }, 912 | item: dragging 913 | } 914 | })); 915 | dragging = null; 916 | draggingHeight = null; 917 | }); 918 | /* 919 | * Drop Event - https://developer.mozilla.org/en-US/docs/Web/Events/drop 920 | * Fires when valid drop target area is hit 921 | */ 922 | addEventListener(sortableElement, 'drop', function (e) { 923 | if (!_listsConnected(sortableElement, dragging.parentElement)) { 924 | return; 925 | } 926 | e.preventDefault(); 927 | e.stopPropagation(); 928 | addData(dragging, 'dropped', 'true'); 929 | // get the one placeholder that is currently visible 930 | var visiblePlaceholder = Array.from(stores.values()).map(function (data) { 931 | return data.placeholder; 932 | }) 933 | .filter(function (placeholder) { return placeholder instanceof HTMLElement; }) 934 | .filter(isInDom)[0]; 935 | // attach element after placeholder 936 | insertAfter(visiblePlaceholder, dragging); 937 | // remove placeholder from dom 938 | visiblePlaceholder.remove(); 939 | /* 940 | * Fires Custom Event - 'sortstop' 941 | */ 942 | sortableElement.dispatchEvent(new CustomEvent('sortstop', { 943 | detail: { 944 | origin: { 945 | elementIndex: originElementIndex, 946 | index: originIndex, 947 | container: originContainer 948 | }, 949 | item: dragging 950 | } 951 | })); 952 | var placeholder = store(sortableElement).placeholder; 953 | var originItems = _filter(originContainer.children, options.items) 954 | .filter(function (item) { return item !== placeholder; }); 955 | var destinationContainer = this.isSortable === true ? this : this.parentElement; 956 | var destinationItems = _filter(destinationContainer.children, addData(destinationContainer, 'items')) 957 | .filter(function (item) { return item !== placeholder; }); 958 | var destinationElementIndex = index(dragging, Array.from(dragging.parentElement.children) 959 | .filter(function (item) { return item !== placeholder; })); 960 | var destinationIndex = index(dragging, destinationItems); 961 | /* 962 | * When a list item changed container lists or index within a list 963 | * Fires Custom Event - 'sortupdate' 964 | */ 965 | if (originElementIndex !== destinationElementIndex || originContainer !== destinationContainer) { 966 | sortableElement.dispatchEvent(new CustomEvent('sortupdate', { 967 | detail: { 968 | origin: { 969 | elementIndex: originElementIndex, 970 | index: originIndex, 971 | container: originContainer, 972 | itemsBeforeUpdate: originItemsBeforeUpdate, 973 | items: originItems 974 | }, 975 | destination: { 976 | index: destinationIndex, 977 | elementIndex: destinationElementIndex, 978 | container: destinationContainer, 979 | itemsBeforeUpdate: destinationItemsBeforeUpdate, 980 | items: destinationItems 981 | }, 982 | item: dragging 983 | } 984 | })); 985 | } 986 | }); 987 | var debouncedDragOverEnter = _debounce(function (sortableElement, element, pageY) { 988 | if (!dragging) { 989 | return; 990 | } 991 | // set placeholder height if forcePlaceholderSize option is set 992 | if (options.forcePlaceholderSize) { 993 | store(sortableElement).placeholder.style.height = draggingHeight + 'px'; 994 | } 995 | // if element the draggedItem is dragged onto is within the array of all elements in list 996 | // (not only items, but also disabled, etc.) 997 | if (Array.from(sortableElement.children).indexOf(element) > -1) { 998 | var thisHeight = _getElementHeight(element); 999 | var placeholderIndex = index(store(sortableElement).placeholder, element.parentElement.children); 1000 | var thisIndex = index(element, element.parentElement.children); 1001 | // Check if `element` is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering 1002 | if (thisHeight > draggingHeight) { 1003 | // Dead zone? 1004 | var deadZone = thisHeight - draggingHeight; 1005 | var offsetTop = offset(element).top; 1006 | if (placeholderIndex < thisIndex && pageY < offsetTop) { 1007 | return; 1008 | } 1009 | if (placeholderIndex > thisIndex && 1010 | pageY > offsetTop + thisHeight - deadZone) { 1011 | return; 1012 | } 1013 | } 1014 | if (dragging.oldDisplay === undefined) { 1015 | dragging.oldDisplay = dragging.style.display; 1016 | } 1017 | if (dragging.style.display !== 'none') { 1018 | dragging.style.display = 'none'; 1019 | } 1020 | // To avoid flicker, determine where to position the placeholder 1021 | // based on where the mouse pointer is relative to the elements 1022 | // vertical center. 1023 | var placeAfter = false; 1024 | try { 1025 | var elementMiddle = offset(element).top + element.offsetHeight / 2; 1026 | placeAfter = pageY >= elementMiddle; 1027 | } 1028 | catch (e) { 1029 | placeAfter = placeholderIndex < thisIndex; 1030 | } 1031 | if (placeAfter) { 1032 | insertAfter(element, store(sortableElement).placeholder); 1033 | } 1034 | else { 1035 | insertBefore(element, store(sortableElement).placeholder); 1036 | } 1037 | // get placeholders from all stores & remove all but current one 1038 | Array.from(stores.values()) 1039 | .filter(function (data) { return data.placeholder !== undefined; }) 1040 | .forEach(function (data) { 1041 | if (data.placeholder !== store(sortableElement).placeholder) { 1042 | data.placeholder.remove(); 1043 | } 1044 | }); 1045 | } 1046 | else { 1047 | // get all placeholders from store 1048 | var placeholders = Array.from(stores.values()) 1049 | .filter(function (data) { return data.placeholder !== undefined; }) 1050 | .map(function (data) { 1051 | return data.placeholder; 1052 | }); 1053 | // check if element is not in placeholders 1054 | if (placeholders.indexOf(element) === -1 && sortableElement === element && !_filter(element.children, options.items).length) { 1055 | placeholders.forEach(function (element) { return element.remove(); }); 1056 | element.appendChild(store(sortableElement).placeholder); 1057 | } 1058 | } 1059 | }, options.debounce); 1060 | // Handle dragover and dragenter events on draggable items 1061 | var onDragOverEnter = function (e) { 1062 | var element = e.target; 1063 | var sortableElement = element.isSortable === true ? element : findSortable(element); 1064 | element = findDragElement(sortableElement, element); 1065 | if (!dragging || !_listsConnected(sortableElement, dragging.parentElement) || addData(sortableElement, '_disabled') === 'true') { 1066 | return; 1067 | } 1068 | var options = addData(sortableElement, 'opts'); 1069 | if (parseInt(options.maxItems) && _filter(sortableElement.children, addData(sortableElement, 'items')).length >= parseInt(options.maxItems) && dragging.parentElement !== sortableElement) { 1070 | return; 1071 | } 1072 | e.preventDefault(); 1073 | e.stopPropagation(); 1074 | e.dataTransfer.dropEffect = store(sortableElement).getConfig('copy') === true ? 'copy' : 'move'; 1075 | debouncedDragOverEnter(sortableElement, element, e.pageY); 1076 | }; 1077 | addEventListener(listItems.concat(sortableElement), 'dragover', onDragOverEnter); 1078 | addEventListener(listItems.concat(sortableElement), 'dragenter', onDragOverEnter); 1079 | }); 1080 | return sortableElements; 1081 | } 1082 | sortable.destroy = function (sortableElement) { 1083 | _destroySortable(sortableElement); 1084 | }; 1085 | sortable.enable = function (sortableElement) { 1086 | _enableSortable(sortableElement); 1087 | }; 1088 | sortable.disable = function (sortableElement) { 1089 | _disableSortable(sortableElement); 1090 | }; 1091 | 1092 | return sortable; 1093 | 1094 | }()); -------------------------------------------------------------------------------- /src/assets/js/html5sortable.min.js: -------------------------------------------------------------------------------- 1 | var sortable=function(){"use strict";function d(e,t,n){if(void 0===n)return e&&e.h5s&&e.h5s.data&&e.h5s.data[t];e.h5s=e.h5s||{},e.h5s.data=e.h5s.data||{},e.h5s.data[t]=n}function u(e,t){if(!(e instanceof NodeList||e instanceof HTMLCollection||e instanceof Array))throw new Error("You must provide a nodeList/HTMLCollection/Array of elements to be filtered.");return"string"!=typeof t?Array.from(e):Array.from(e).filter(function(e){return 1===e.nodeType&&e.matches(t)})}var p=new Map,t=function(){function e(){this._config=new Map,this._placeholder=void 0,this._data=new Map}return Object.defineProperty(e.prototype,"config",{get:function(){var n={};return this._config.forEach(function(e,t){n[t]=e}),n},set:function(e){if("object"!=typeof e)throw new Error("You must provide a valid configuration object to the config setter.");var t=Object.assign({},e);this._config=new Map(Object.entries(t))},enumerable:!0,configurable:!0}),e.prototype.setConfig=function(e,t){if(!this._config.has(e))throw new Error("Trying to set invalid configuration item: "+e);this._config.set(e,t)},e.prototype.getConfig=function(e){if(!this._config.has(e))throw new Error("Invalid configuration item requested: "+e);return this._config.get(e)},Object.defineProperty(e.prototype,"placeholder",{get:function(){return this._placeholder},set:function(e){if(!(e instanceof HTMLElement)&&null!==e)throw new Error("A placeholder must be an html element or null.");this._placeholder=e},enumerable:!0,configurable:!0}),e.prototype.setData=function(e,t){if("string"!=typeof e)throw new Error("The key must be a string.");this._data.set(e,t)},e.prototype.getData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.get(e)},e.prototype.deleteData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.delete(e)},e}();function m(e){if(!(e instanceof HTMLElement))throw new Error("Please provide a sortable to the store function.");return p.has(e)||p.set(e,new t),p.get(e)}function g(e,t,n){if(e instanceof Array)for(var r=0;r':t=document.createElement("div")),"string"==typeof n&&(r=t.classList).add.apply(r,n.split(" ")),t;var r}(s,e,f.placeholderClass),d(s,"items",f.items),f.acceptFrom?d(s,"acceptFrom",f.acceptFrom):f.connectWith&&d(s,"connectWith",f.connectWith),j(s),h(t,"role","option"),h(t,"aria-grabbed","false"),_(s,!0),g(s,"dragstart",function(e){if(!0!==e.target.isSortable&&(e.stopImmediatePropagation(),(!f.handle||e.target.matches(f.handle))&&"false"!==e.target.getAttribute("draggable"))){var t=F(e.target),n=W(t,e.target);I=u(t.children,f.items),A=I.indexOf(n),H=y(n,t.children),D=t,function(e,t,n){if(!(e instanceof Event))throw new Error("setDragImage requires a DragEvent as the first argument.");if(!(t instanceof HTMLElement))throw new Error("setDragImage requires the dragged element as the second argument.");if(n||(n=C),e.dataTransfer&&e.dataTransfer.setDragImage){var r=n(t,v(t),e);if(!(r.element instanceof HTMLElement)||"number"!=typeof r.posX||"number"!=typeof r.posY)throw new Error("The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].");e.dataTransfer.effectAllowed="copyMove",e.dataTransfer.setData("text/plain","arbitrary"),e.dataTransfer.setDragImage(r.element,r.posX,r.posY)}}(e,n,f.customDragImage),x=T(n),n.classList.add(f.draggingClass),h(M=O(n,t),"aria-grabbed","true"),t.dispatchEvent(new CustomEvent("sortstart",{detail:{origin:{elementIndex:H,index:A,container:D},item:M}}))}}),g(s,"dragenter",function(e){if(!0!==e.target.isSortable){var t=F(e.target);S=u(t.children,d(t,"items")).filter(function(e){return e!==m(s).placeholder})}}),g(s,"dragend",function(e){if(M){M.classList.remove(f.draggingClass),h(M,"aria-grabbed","false"),"true"===M.getAttribute("aria-copied")&&"true"!==d(M,"dropped")&&M.remove(),M.style.display=M.oldDisplay,delete M.oldDisplay;var t=Array.from(p.values()).map(function(e){return e.placeholder}).filter(function(e){return e instanceof HTMLElement}).filter(E)[0];t&&t.remove(),s.dispatchEvent(new CustomEvent("sortstop",{detail:{origin:{elementIndex:H,index:A,container:D},item:M}})),x=M=null}}),g(s,"drop",function(e){if(L(s,M.parentElement)){e.preventDefault(),e.stopPropagation(),d(M,"dropped","true");var t=Array.from(p.values()).map(function(e){return e.placeholder}).filter(function(e){return e instanceof HTMLElement}).filter(E)[0];w(t,M),t.remove(),s.dispatchEvent(new CustomEvent("sortstop",{detail:{origin:{elementIndex:H,index:A,container:D},item:M}}));var n=m(s).placeholder,r=u(D.children,f.items).filter(function(e){return e!==n}),o=!0===this.isSortable?this:this.parentElement,i=u(o.children,d(o,"items")).filter(function(e){return e!==n}),a=y(M,Array.from(M.parentElement.children).filter(function(e){return e!==n})),l=y(M,i);H===a&&D===o||s.dispatchEvent(new CustomEvent("sortupdate",{detail:{origin:{elementIndex:H,index:A,container:D,itemsBeforeUpdate:I,items:r},destination:{index:l,elementIndex:a,container:o,itemsBeforeUpdate:S,items:i},item:M}}))}});var r,o,i,a=(r=function(t,e,n){if(M)if(f.forcePlaceholderSize&&(m(t).placeholder.style.height=x+"px"),-1=parseInt(r.maxItems)&&M.parentElement!==n||(e.preventDefault(),e.stopPropagation(),e.dataTransfer.dropEffect=!0===m(n).getConfig("copy")?"copy":"move",a(n,t,e.pageY))}};g(t.concat(s),"dragover",l),g(t.concat(s),"dragenter",l)}),e)}return N.destroy=function(e){r(e)},N.enable=function(e){j(e)},N.disable=function(e){var t,n,r;n=d(t=e,"opts"),r=c(u(t.children,n.items),n.handle),h(t,"aria-dropeffect","none"),d(t,"_disabled","true"),h(r,"draggable","false"),l(r,"mousedown")},N}(); -------------------------------------------------------------------------------- /src/assets/js/kv-html5-sortable.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2018 3 | * @package yii2-sortable 4 | * @version 1.2.2 5 | * 6 | * jQuery Plugin Wrapper for HTML5 Sortable 7 | * 8 | * Author: Kartik Visweswaran 9 | * Licensed under the BSD 3-Clause 10 | */ 11 | (function (factory) { 12 | "use strict"; 13 | //noinspection JSUnresolvedVariable 14 | if (typeof define === 'function' && define.amd) { // jshint ignore:line 15 | // AMD. Register as an anonymous module. 16 | define(['jquery'], factory); // jshint ignore:line 17 | } else { // noinspection JSUnresolvedVariable 18 | if (typeof module === 'object' && module.exports) { // jshint ignore:line 19 | // Node/CommonJS 20 | // noinspection JSUnresolvedVariable 21 | module.exports = factory(require('jquery')); // jshint ignore:line 22 | } else { 23 | // Browser globals 24 | factory(window.jQuery); 25 | } 26 | } 27 | }(function ($) { 28 | "use strict"; 29 | var KvHtml5Sortable; 30 | 31 | KvHtml5Sortable = function (element, options) { 32 | var self = this; 33 | self.$element = $(element); 34 | self._init(options); 35 | }; 36 | 37 | KvHtml5Sortable.prototype = { 38 | constructor: KvHtml5Sortable, 39 | _init: function (options) { 40 | var self = this, f, $el = self.$element, sLib = window['sortable'], 41 | methods = ['destroy', 'disable', 'enable', 'serialize', 'reload']; 42 | self.options = options; 43 | $.each(options, function (opt, value) { 44 | self[opt] = value; 45 | }); 46 | if (!sLib) { 47 | throw "Sortable Library not initialized!"; 48 | return; 49 | } 50 | self.id = '#' + $el.attr('id'); 51 | sLib(self.id, self.options); 52 | $.each(methods, function(key, method) { 53 | self[method] = function() { 54 | sLib(self.id, method); 55 | }; 56 | }); 57 | } 58 | }; 59 | 60 | $.fn.kvHtml5Sortable = function (option) { 61 | var args = Array.apply(null, arguments), retvals = []; 62 | args.shift(); 63 | this.each(function () { 64 | var self = $(this), data = self.data('kvHtml5Sortable'), options = typeof option === 'object' && option, opt; 65 | if (!data) { 66 | opt = $.extend(true, {}, $.fn.kvHtml5Sortable.defaults, options, self.data()); 67 | data = new KvHtml5Sortable(this, opt); 68 | self.data('kvHtml5Sortable', data); 69 | } 70 | if (typeof option === 'string') { 71 | retvals.push(data[option].apply(data, args)); 72 | } 73 | }); 74 | switch (retvals.length) { 75 | case 0: 76 | return this; 77 | case 1: 78 | return retvals[0]; 79 | default: 80 | return retvals; 81 | } 82 | }; 83 | $.fn.kvHtml5Sortable.defaults = {}; 84 | $.fn.kvHtml5Sortable.Constructor = KvHtml5Sortable; 85 | })); -------------------------------------------------------------------------------- /src/assets/js/kv-html5-sortable.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014 - 2018 3 | * @package yii2-sortable 4 | * @version 1.2.2 5 | * 6 | * jQuery Plugin Wrapper for HTML5 Sortable 7 | * 8 | * Author: Kartik Visweswaran 9 | * Licensed under the BSD 3-Clause 10 | */!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(window.jQuery)}(function(t){"use strict";var e;(e=function(e,n){this.$element=t(e),this._init(n)}).prototype={constructor:e,_init:function(e){var n=this,i=n.$element,o=window.sortable;if(n.options=e,t.each(e,function(t,e){n[t]=e}),!o)throw"Sortable Library not initialized!";n.id="#"+i.attr("id"),o(n.id,n.options),t.each(["destroy","disable","enable","serialize","reload"],function(t,e){n[e]=function(){o(n.id,e)}})}},t.fn.kvHtml5Sortable=function(n){var i=Array.apply(null,arguments),o=[];switch(i.shift(),this.each(function(){var r,a=t(this),l=a.data("kvHtml5Sortable"),s="object"==typeof n&&n;l||(r=t.extend(!0,{},t.fn.kvHtml5Sortable.defaults,s,a.data()),l=new e(this,r),a.data("kvHtml5Sortable",l)),"string"==typeof n&&o.push(l[n].apply(l,i))}),o.length){case 0:return this;case 1:return o[0];default:return o}},t.fn.kvHtml5Sortable.defaults={},t.fn.kvHtml5Sortable.Constructor=e}); --------------------------------------------------------------------------------