├── .babelrc
├── .gitignore
├── README.md
├── TODO.md
├── dist
└── selectable.js
├── examples-vue1
├── example1.html
├── example2.html
└── example3.html
├── examples-vue2
├── example.css
├── example1.html
├── example2.html
├── example3.html
└── example4.html
├── package.json
├── selectable.js
├── v-selectable.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env"],
3 | "plugins": ["@babel/plugin-proposal-class-properties"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
3 | .vscode/
4 | /package-lock.json
5 | /yarn.lock
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-selectable
2 |
3 | ## Overview
4 |
5 | It's common task to make mouse selection of some objects on the page. This directive makes this task
6 | extremely easy, all you need to implement object selection is few lines of code.
7 | It was designed after jQuery Selectable interaction, with some details borrowed from `angular-multiple-selection`.
8 | It supports plain mouse selection and adding to previously selected values with
9 | `Ctrl` key pressed during selection. Single items can be excluded from selection
10 | with Ctrl + click on them. Scrolling of document or some specified block while selecting
11 | is also supported now, but only on Y axis.
12 |
13 | ## Requirements
14 |
15 | * vue: ^1.0
16 | * vue2: ^2.0
17 | * Browsers - briefly tested on Chrome 58 & 83, IE 11, Firefox 53
18 |
19 | ## Install
20 |
21 | From npm:
22 | ```
23 | $ npm install vue-selectable --save
24 | ```
25 |
26 | ## Usage
27 |
28 | To use directive normally you'll need two arrays, one for selected items - with boolean values for
29 | every selectable item, another for items under selection box. By default elements identified by
30 | `selectable` class will be considered as selectable items.
31 | Another thing that you'll definitely need is a element that will be selection box. Directive
32 | will change `height`, `width`, `top`, and `left` attributes of this element, and toggle its
33 | visibility by changing `display` attribute from `block` to `none` and vise versa.
34 |
35 | ### Vue 1.x (ES6 syntax)
36 |
37 | NB: for Vue 2.x all you need is to write `v-for` as `v-for="(item, i) in items"`
38 |
39 | ```html
40 |
45 |
46 |
{{ item }}
49 |
50 | ```
51 |
52 | ```js
53 | import selectable from 'vue-selectable';
54 |
55 | new Vue({
56 | el: '#app',
57 | data: {
58 | selected: [],
59 | selecting: [],
60 | items: ['abc', 'bcd', 'cde']
61 | },
62 | directives: { selectable },
63 | methods: {
64 | selectedGetter() { return this.selected; },
65 | selectedSetter(v) { this.selected = v; },
66 | selectingSetter(v) { this.selecting = v; }
67 | }
68 | });
69 | ```
70 |
71 | Selection items list in directive can be updated by calling `setSelectableItems(el, itemSelector)`,
72 | where `el` is element where directive applied. Optional argument `itemSelector` can be used to change
73 | selectable items selector. Function returns number of selectable items added or -1 in case of error.
74 |
75 | ## Options
76 |
77 | `v-selectable` requires one mandatory parameter - directive parameter - object with 3 functions,
78 | `selectedGetter`, `selectedSetter`, and `selectingSetter`, to get/set arrays 'selected' (selection
79 | status - array of boolean), 'selecting' (array of items selection status during selection drag,
80 | array of boolean; must be used to display realtime selection visual feedback).
81 |
82 | `selectedSetter` function also receives "selecting" array as a second argument. This could help when
83 | we need some custom selection logic.
84 |
85 | If you have 5 selectable items, `selected` array will have 5 elements initially
86 | set to `false`. When user selects some item(s), values change to `true` accordingly. The same applies
87 | for `selecting` array.
88 |
89 | Also you can specify additional parameters in the object for JS `selectable` component underneath.
90 | For example you can set `rootElement` to some element other than "document"
91 | (to attach event listeners to it). Also it's possible to set `rootElement` to `null` initially and then
92 | set it when necessary element appears in the DOM tree; event listeners will attach right after
93 | (but the trick will work only once). Or you can set `renderSelecting/renderSelection` options to true to have
94 | directive manage CSS classes instead of Vue.js framework.
95 |
96 | Other parameters available:
97 | * `data-items` - CSS selector to identify selectable items, by default it is set to `.selectable`
98 | (elements with CSS class "selectable")
99 | * `data-box` - selection box element. By default it tries to use element with `selection` CSS class
100 | * `data-constraint` - box that constrains selection area (selection box can be only inside area
101 | limited to this element), by default selection area limited to element with directive
102 |
103 | ## Exported util functions
104 | Two utility functions are exported with a directive to help configuring directive. Both require DOM node
105 | with directive as a first argument ("el").
106 | * `setSelectableItems(el, [itemSelector])` - if used without second argument, rereads DOM to fetch
107 | selectable items (useful after e.g. AJAX load of items). Another CSS selector can be specified to
108 | create a new list of selectable items.
109 | * `setOptions(el, options)` - sets directive options on the fly. For now is required to set
110 | `scrollingFrame` internal parameter for Vue.js v2 (see `examples-vue2/example3.html`).
111 |
112 | ## Internal options
113 | Except already described `selectedGetter`, `selectedSetter`, and `selectingSetter`, directive has these
114 | internal options, that can be set using directive declaration (as getters/setters) or on the fly:
115 | * `disableTextSelection` (boolean) - disable browser text selection when selection box is active (turned on by default)
116 | * `scrollingFrame` (DOM node) - element with scrollbar, that contains list of selectable items
117 | * `scrollSpeed` (int) - speed of scroll (in px per 16ms, default 10px)
118 | * `scrollDistance` (int) - distance from borders (in px, default 10px) when scroll begins to work
119 | * `scrollDocumentEnabled` (boolean) - enable (default)/disable document scrolling while selecting items, ignored when scrollingFrame is configured
120 | * `renderSelected` (boolean) - add CSS selectedClass to elements currently selected (w/o framework)
121 | * `renderSelecting` (boolean) - add CSS selectedClass to elements currently under selection box (w/o framework)
122 | * `selectingClass` (string) - CSS class used to mark items under selection box (".selecting" by default)
123 | * `selectedClass` (string) - CSS class used to mark selected items (".selected" by default)
124 | * `overrideAddMode` (boolean) - selection frame always adds items to selection when this flag is true, despite "Ctrl" or "Meta" keys being pressed (false by default)
125 |
126 | ## Examples
127 |
128 | Example usages can be found in `examples-vue1` directory for Vue.js v1 and `examples-vue2` for Vue.js v2.
129 | Examples for v2 were tested against version 2.3.3 and 2.6.11.
130 |
131 | ## License
132 |
133 | [MIT](http://opensource.org/licenses/MIT)
134 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # List of known issues and planned features
2 |
3 | * Perform scroll for entire document automatically (works for frame already) - fix needed.
4 | * Make absBox() results on `selectables[]` and `boundingBox` cacheable (ala autoRefresh: off in
5 | jQuery Selectable) - add flag `.static` to directive
6 | * Make selection work with `SHIFT` key modifier.
7 | * Add possibility to require 'full element cover' for selection.
8 |
--------------------------------------------------------------------------------
/dist/selectable.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("vueSelectable",[],t):"object"==typeof exports?exports.vueSelectable=t():e.vueSelectable=t()}(window,(function(){return function(e){var t={};function s(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,s),i.l=!0,i.exports}return s.m=e,s.c=t,s.d=function(e,t,n){s.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},s.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},s.t=function(e,t){if(1&t&&(e=s(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(s.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)s.d(n,i,function(t){return e[t]}.bind(null,i));return n},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,"a",t),t},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},s.p="",s(s.s=0)}([function(e,t,s){"use strict";function n(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var s=[],n=!0,i=!1,l=void 0;try{for(var r,o=e[Symbol.iterator]();!(n=(r=o.next()).done)&&(s.push(r.value),!t||s.length!==t);n=!0);}catch(e){i=!0,l=e}finally{try{n||null==o.return||o.return()}finally{if(i)throw l}}return s}(e,t)||function(e,t){if(!e)return;if("string"==typeof e)return i(e,t);var s=Object.prototype.toString.call(e).slice(8,-1);"Object"===s&&e.constructor&&(s=e.constructor.name);if("Map"===s||"Set"===s)return Array.from(e);if("Arguments"===s||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(s))return i(e,t)}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var s=0,n=new Array(t);s1)for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};l(this,e),o(this,"el",null),o(this,"selectBox",null),o(this,"selectBoxSelector",".selection"),o(this,"rootElement",document),o(this,"boundingBox",document),o(this,"boundingBoxSelector",null),o(this,"dragging",!1),o(this,"startX",null),o(this,"startY",null),o(this,"endX",null),o(this,"endY",null),o(this,"selectables",[]),o(this,"selected",[]),o(this,"selectedSetter",null),o(this,"selectedGetter",null),o(this,"selectingSetter",null),o(this,"selecting",[]),o(this,"addMode",!1),o(this,"disableTextSelection",!0),o(this,"handlers",{mousedown:null,mouseup:null,mousemove:null}),o(this,"scrollingFrame",null),o(this,"scrollSpeed",10),o(this,"scrollDistance",10),o(this,"scrollDocumentEnabled",!0),o(this,"scrollRepeater",null),o(this,"renderSelected",!1),o(this,"renderSelecting",!1),o(this,"overrideAddMode",!1),o(this,"selectingClass","selecting"),o(this,"selectedClass","selected"),o(this,"firstRun",!0),this.handlers.mousedown=this.mouseDown.bind(this),this.handlers.mouseup=this.mouseUp.bind(this),this.handlers.mousemove=this.mouseMove.bind(this),h(this,t),this.attach()}var t,s,i;return t=e,i=[{key:"disableTextSelection",value:function(e){return e.preventDefault(),!1}},{key:"absBox",value:function(e){var t=e.getBoundingClientRect();return{top:t.top+window.pageYOffset,left:t.left+window.pageXOffset,width:t.width,height:t.height}}}],(s=[{key:"attach",value:function(){var e=this;this.rootElement&&Object.keys(this.handlers).forEach((function(t){return e.rootElement.addEventListener(t,e.handlers[t])}))}},{key:"detach",value:function(){var t=this;Object.keys(this.handlers).forEach((function(e){return t.rootElement.removeEventListener(e,t.handlers[e])})),this.disableTextSelection&&this.dragging&&this.rootElement.removeEventListener("selectstart",e.disableTextSelection),this.scrollRepeater&&clearInterval(this.scrollRepeater),this.selectables=[],this.selectBox=null,this.boundingBox=null,this.rootElement=null,this.scrollingFrame=null,this.element=null}},{key:"setSelectables",value:function(e){this.selectables=e,this.selected=e.map((function(e){return!1})),"function"==typeof this.selectedSetter&&this.selectedSetter(this.selected,this.selected)}},{key:"mouseDown",value:function(t){var s=this.el===t.target||this.el.contains(t.target);if(0===t.button&&s){this.boundingBoxSelector&&(this.boundingBox=document.querySelector(this.boundingBoxSelector));var i=e.absBox(this.boundingBox);if(!(t.pageXi.width+i.left||t.pageYi.height+i.top)){this.disableTextSelection&&this.rootElement.addEventListener("selectstart",e.disableTextSelection),this.scrollRepeater&&(clearInterval(this.scrollRepeater),this.scrollRepeater=null);var l=n(this.bound(t),2),r=l[0],o=l[1];if(this.selectBox=document.querySelector(this.selectBoxSelector),this.scrollingFrame&&(o+=this.scrollingFrame.scrollTop),this.startX=r,this.startY=o,this.endX=r,this.endY=o,this.dragging=!0,this.selecting=this.selectables.map((function(e){return!1})),"function"==typeof this.selectingSetter&&this.selectingSetter(this.selecting),this.addMode=this.overrideAddMode||t.ctrlKey||t.metaKey,this.addMode){if("function"==typeof this.selectedGetter){var c=this.selectedGetter()||[];this.selected=this.selectables.map((function(e,t){return!!c[t]}))}}else this.selected=this.selecting,"function"==typeof this.selectedSetter&&this.selectedSetter(this.selected,this.selecting);this.updateSelection(),this.render()}}}},{key:"mouseUp",value:function(t){var s=this;if(this.dragging){if(0!==t.button)return;this.disableTextSelection&&this.rootElement.removeEventListener("selectstart",e.disableTextSelection);var i=n(this.bound(t),2),l=i[0],r=i[1];if(this.endX=l,this.endY=r,this.scrollingFrame&&(this.endY+=this.scrollingFrame.scrollTop),this.scrollRepeater&&(clearInterval(this.scrollRepeater),this.scrollRepeater=null),this.dragging=!1,this.updateSelection(),"function"==typeof this.selectedGetter){var o=this.selectedGetter()||[];this.selected=this.selectables.map((function(e,t){return!!o[t]}))}if(this.addMode){var c=this.selecting.reduce((function(e,t){return e+t?1:0}),0),a=this.selecting.findIndex((function(e){return!!e}));1===c&&this.selected[a]?this.selected[a]=!1:this.selected=this.selected.map((function(e,t){return e||s.selecting[t]}))}else this.selected=this.selecting;"function"==typeof this.selectedSetter&&this.selectedSetter(this.selected,this.selecting),this.selecting=[],this.selectingSetter&&this.selectingSetter(this.selecting),this.render()}}},{key:"mouseMove",value:function(e){if(this.dragging){var t=n(this.bound(e),2),s=t[0],i=t[1];this.endX=s,this.endY=i,this.scrollRepeater&&(clearInterval(this.scrollRepeater),this.scrollRepeater=null),this.scrollingFrame?this.endY+=this.scrollFrame(e):this.scrollDocumentEnabled&&this.scrollDocument(e),this.updateSelection(),this.render()}}},{key:"scrollFrame",value:function(e){var t=this,s=this.scrollingFrame,n=s.getBoundingClientRect(),i=0;return e.pageY>=n.bottom-this.scrollDistance?i=this.scrollSpeed:e.pageY<=n.top+this.scrollDistance&&(i=-this.scrollSpeed),s.scrollTop+=i,(e.pageY>=n.bottom||e.pageY<=n.top)&&(this.scrollRepeater&&clearInterval(this.scrollRepeater),this.scrollRepeater=setInterval((function(){return t.mouseMove(e)}),16)),s.scrollTop}},{key:"scrollDocument",value:function(e){var t=this,s=0;this.endY<=window.pageYOffset?s=-this.scrollSpeed:this.endY>=window.pageYOffset+window.innerHeight&&(s=this.scrollSpeed),0!==s&&(window.scrollBy(0,s),this.scrollRepeater&&clearInterval(this.scrollRepeater),this.scrollRepeater=setInterval((function(){return t.mouseMove(e)}),16))}},{key:"bound",value:function(t){var s=e.absBox(this.boundingBox);return[Math.min(Math.max(s.left,t.pageX),s.width+s.left),Math.min(Math.max(s.top,t.pageY),s.height+s.top)]}},{key:"updateSelection",value:function(){var t=this.getSelectionBox();t.top-=this.scrollingFrame?this.scrollingFrame.scrollTop:0,this.selecting=this.selectables.map(e.absBox).map((function(e){return Math.abs(2*(t.left-e.left)+t.width-e.width)
2 |
3 |
4 |
5 | Test vue
6 |
7 |
8 |
56 |
57 |
58 |