vuedraggable.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. import Sortable from "sortablejs";
  2. import { insertNodeAt, camelize, console, removeNode } from "./util/helper";
  3. function buildAttribute(object, propName, value) {
  4. if (value === undefined) {
  5. return object;
  6. }
  7. object = object || {};
  8. object[propName] = value;
  9. return object;
  10. }
  11. function computeVmIndex(vnodes, element) {
  12. return vnodes.map(elt => elt.elm).indexOf(element);
  13. }
  14. function computeIndexes(slots, children, isTransition, footerOffset) {
  15. if (!slots) {
  16. return [];
  17. }
  18. const elmFromNodes = slots.map(elt => elt.elm);
  19. const footerIndex = children.length - footerOffset;
  20. const rawIndexes = [...children].map((elt, idx) =>
  21. idx >= footerIndex ? elmFromNodes.length : elmFromNodes.indexOf(elt)
  22. );
  23. return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes;
  24. }
  25. function emit(evtName, evtData) {
  26. this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData));
  27. }
  28. function delegateAndEmit(evtName) {
  29. return evtData => {
  30. if (this.realList !== null) {
  31. this["onDrag" + evtName](evtData);
  32. }
  33. emit.call(this, evtName, evtData);
  34. };
  35. }
  36. function isTransitionName(name) {
  37. return ["transition-group", "TransitionGroup"].includes(name);
  38. }
  39. function isTransition(slots) {
  40. if (!slots || slots.length !== 1) {
  41. return false;
  42. }
  43. const [{ componentOptions }] = slots;
  44. if (!componentOptions) {
  45. return false;
  46. }
  47. return isTransitionName(componentOptions.tag);
  48. }
  49. function getSlot(slot, scopedSlot, key) {
  50. return slot[key] || (scopedSlot[key] ? scopedSlot[key]() : undefined);
  51. }
  52. function computeChildrenAndOffsets(children, slot, scopedSlot) {
  53. let headerOffset = 0;
  54. let footerOffset = 0;
  55. const header = getSlot(slot, scopedSlot, "header");
  56. if (header) {
  57. headerOffset = header.length;
  58. children = children ? [...header, ...children] : [...header];
  59. }
  60. const footer = getSlot(slot, scopedSlot, "footer");
  61. if (footer) {
  62. footerOffset = footer.length;
  63. children = children ? [...children, ...footer] : [...footer];
  64. }
  65. return { children, headerOffset, footerOffset };
  66. }
  67. function getComponentAttributes($attrs, componentData) {
  68. let attributes = null;
  69. const update = (name, value) => {
  70. attributes = buildAttribute(attributes, name, value);
  71. };
  72. const attrs = Object.keys($attrs)
  73. .filter(key => key === "id" || key.startsWith("data-"))
  74. .reduce((res, key) => {
  75. res[key] = $attrs[key];
  76. return res;
  77. }, {});
  78. update("attrs", attrs);
  79. if (!componentData) {
  80. return attributes;
  81. }
  82. const { on, props, attrs: componentDataAttrs } = componentData;
  83. update("on", on);
  84. update("props", props);
  85. Object.assign(attributes.attrs, componentDataAttrs);
  86. return attributes;
  87. }
  88. const eventsListened = ["Start", "Add", "Remove", "Update", "End"];
  89. const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"];
  90. const readonlyProperties = ["Move", ...eventsListened, ...eventsToEmit].map(
  91. evt => "on" + evt
  92. );
  93. var draggingElement = null;
  94. const props = {
  95. options: Object,
  96. list: {
  97. type: Array,
  98. required: false,
  99. default: null
  100. },
  101. value: {
  102. type: Array,
  103. required: false,
  104. default: null
  105. },
  106. noTransitionOnDrag: {
  107. type: Boolean,
  108. default: false
  109. },
  110. clone: {
  111. type: Function,
  112. default: original => {
  113. return original;
  114. }
  115. },
  116. element: {
  117. type: String,
  118. default: "div"
  119. },
  120. tag: {
  121. type: String,
  122. default: null
  123. },
  124. move: {
  125. type: Function,
  126. default: null
  127. },
  128. componentData: {
  129. type: Object,
  130. required: false,
  131. default: null
  132. }
  133. };
  134. const draggableComponent = {
  135. name: "draggable",
  136. inheritAttrs: false,
  137. props,
  138. data() {
  139. return {
  140. transitionMode: false,
  141. noneFunctionalComponentMode: false
  142. };
  143. },
  144. render(h) {
  145. const slots = this.$slots.default;
  146. this.transitionMode = isTransition(slots);
  147. const { children, headerOffset, footerOffset } = computeChildrenAndOffsets(
  148. slots,
  149. this.$slots,
  150. this.$scopedSlots
  151. );
  152. this.headerOffset = headerOffset;
  153. this.footerOffset = footerOffset;
  154. const attributes = getComponentAttributes(this.$attrs, this.componentData);
  155. return h(this.getTag(), attributes, children);
  156. },
  157. created() {
  158. if (this.list !== null && this.value !== null) {
  159. console.error(
  160. "Value and list props are mutually exclusive! Please set one or another."
  161. );
  162. }
  163. if (this.element !== "div") {
  164. console.warn(
  165. "Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props"
  166. );
  167. }
  168. if (this.options !== undefined) {
  169. console.warn(
  170. "Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props"
  171. );
  172. }
  173. },
  174. mounted() {
  175. this.noneFunctionalComponentMode =
  176. this.getTag().toLowerCase() !== this.$el.nodeName.toLowerCase() &&
  177. !this.getIsFunctional();
  178. if (this.noneFunctionalComponentMode && this.transitionMode) {
  179. throw new Error(
  180. `Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}`
  181. );
  182. }
  183. const optionsAdded = {};
  184. eventsListened.forEach(elt => {
  185. optionsAdded["on" + elt] = delegateAndEmit.call(this, elt);
  186. });
  187. eventsToEmit.forEach(elt => {
  188. optionsAdded["on" + elt] = emit.bind(this, elt);
  189. });
  190. const attributes = Object.keys(this.$attrs).reduce((res, key) => {
  191. res[camelize(key)] = this.$attrs[key];
  192. return res;
  193. }, {});
  194. const options = Object.assign({}, this.options, attributes, optionsAdded, {
  195. onMove: (evt, originalEvent) => {
  196. return this.onDragMove(evt, originalEvent);
  197. }
  198. });
  199. !("draggable" in options) && (options.draggable = ">*");
  200. this._sortable = new Sortable(this.rootContainer, options);
  201. this.computeIndexes();
  202. },
  203. beforeDestroy() {
  204. if (this._sortable !== undefined) this._sortable.destroy();
  205. },
  206. computed: {
  207. rootContainer() {
  208. return this.transitionMode ? this.$el.children[0] : this.$el;
  209. },
  210. realList() {
  211. return this.list ? this.list : this.value;
  212. }
  213. },
  214. watch: {
  215. options: {
  216. handler(newOptionValue) {
  217. this.updateOptions(newOptionValue);
  218. },
  219. deep: true
  220. },
  221. $attrs: {
  222. handler(newOptionValue) {
  223. this.updateOptions(newOptionValue);
  224. },
  225. deep: true
  226. },
  227. realList() {
  228. this.computeIndexes();
  229. }
  230. },
  231. methods: {
  232. getIsFunctional() {
  233. const { fnOptions } = this._vnode;
  234. return fnOptions && fnOptions.functional;
  235. },
  236. getTag() {
  237. return this.tag || this.element;
  238. },
  239. updateOptions(newOptionValue) {
  240. for (var property in newOptionValue) {
  241. const value = camelize(property);
  242. if (readonlyProperties.indexOf(value) === -1) {
  243. this._sortable.option(value, newOptionValue[property]);
  244. }
  245. }
  246. },
  247. getChildrenNodes() {
  248. if (this.noneFunctionalComponentMode) {
  249. return this.$children[0].$slots.default;
  250. }
  251. const rawNodes = this.$slots.default;
  252. return this.transitionMode ? rawNodes[0].child.$slots.default : rawNodes;
  253. },
  254. computeIndexes() {
  255. this.$nextTick(() => {
  256. this.visibleIndexes = computeIndexes(
  257. this.getChildrenNodes(),
  258. this.rootContainer.children,
  259. this.transitionMode,
  260. this.footerOffset
  261. );
  262. });
  263. },
  264. getUnderlyingVm(htmlElt) {
  265. const index = computeVmIndex(this.getChildrenNodes() || [], htmlElt);
  266. if (index === -1) {
  267. //Edge case during move callback: related element might be
  268. //an element different from collection
  269. return null;
  270. }
  271. const element = this.realList[index];
  272. return { index, element };
  273. },
  274. getUnderlyingPotencialDraggableComponent({ __vue__: vue }) {
  275. if (
  276. !vue ||
  277. !vue.$options ||
  278. !isTransitionName(vue.$options._componentTag)
  279. ) {
  280. if (
  281. !("realList" in vue) &&
  282. vue.$children.length === 1 &&
  283. "realList" in vue.$children[0]
  284. )
  285. return vue.$children[0];
  286. return vue;
  287. }
  288. return vue.$parent;
  289. },
  290. emitChanges(evt) {
  291. this.$nextTick(() => {
  292. this.$emit("change", evt);
  293. });
  294. },
  295. alterList(onList) {
  296. if (this.list) {
  297. onList(this.list);
  298. return;
  299. }
  300. const newList = [...this.value];
  301. onList(newList);
  302. this.$emit("input", newList);
  303. },
  304. spliceList() {
  305. const spliceList = list => list.splice(...arguments);
  306. this.alterList(spliceList);
  307. },
  308. updatePosition(oldIndex, newIndex) {
  309. const updatePosition = list =>
  310. list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
  311. this.alterList(updatePosition);
  312. },
  313. getRelatedContextFromMoveEvent({ to, related }) {
  314. const component = this.getUnderlyingPotencialDraggableComponent(to);
  315. if (!component) {
  316. return { component };
  317. }
  318. const list = component.realList;
  319. const context = { list, component };
  320. if (to !== related && list && component.getUnderlyingVm) {
  321. const destination = component.getUnderlyingVm(related);
  322. if (destination) {
  323. return Object.assign(destination, context);
  324. }
  325. }
  326. return context;
  327. },
  328. getVmIndex(domIndex) {
  329. const indexes = this.visibleIndexes;
  330. const numberIndexes = indexes.length;
  331. return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex];
  332. },
  333. getComponent() {
  334. return this.$slots.default[0].componentInstance;
  335. },
  336. resetTransitionData(index) {
  337. if (!this.noTransitionOnDrag || !this.transitionMode) {
  338. return;
  339. }
  340. var nodes = this.getChildrenNodes();
  341. nodes[index].data = null;
  342. const transitionContainer = this.getComponent();
  343. transitionContainer.children = [];
  344. transitionContainer.kept = undefined;
  345. },
  346. onDragStart(evt) {
  347. this.context = this.getUnderlyingVm(evt.item);
  348. evt.item._underlying_vm_ = this.clone(this.context.element);
  349. draggingElement = evt.item;
  350. },
  351. onDragAdd(evt) {
  352. const element = evt.item._underlying_vm_;
  353. if (element === undefined) {
  354. return;
  355. }
  356. removeNode(evt.item);
  357. const newIndex = this.getVmIndex(evt.newIndex);
  358. this.spliceList(newIndex, 0, element);
  359. this.computeIndexes();
  360. const added = { element, newIndex };
  361. this.emitChanges({ added });
  362. },
  363. onDragRemove(evt) {
  364. insertNodeAt(this.rootContainer, evt.item, evt.oldIndex);
  365. if (evt.pullMode === "clone") {
  366. removeNode(evt.clone);
  367. return;
  368. }
  369. const oldIndex = this.context.index;
  370. this.spliceList(oldIndex, 1);
  371. const removed = { element: this.context.element, oldIndex };
  372. this.resetTransitionData(oldIndex);
  373. this.emitChanges({ removed });
  374. },
  375. onDragUpdate(evt) {
  376. removeNode(evt.item);
  377. insertNodeAt(evt.from, evt.item, evt.oldIndex);
  378. const oldIndex = this.context.index;
  379. const newIndex = this.getVmIndex(evt.newIndex);
  380. this.updatePosition(oldIndex, newIndex);
  381. const moved = { element: this.context.element, oldIndex, newIndex };
  382. this.emitChanges({ moved });
  383. },
  384. updateProperty(evt, propertyName) {
  385. evt.hasOwnProperty(propertyName) &&
  386. (evt[propertyName] += this.headerOffset);
  387. },
  388. computeFutureIndex(relatedContext, evt) {
  389. if (!relatedContext.element) {
  390. return 0;
  391. }
  392. const domChildren = [...evt.to.children].filter(
  393. el => el.style["display"] !== "none"
  394. );
  395. const currentDOMIndex = domChildren.indexOf(evt.related);
  396. const currentIndex = relatedContext.component.getVmIndex(currentDOMIndex);
  397. const draggedInList = domChildren.indexOf(draggingElement) !== -1;
  398. return draggedInList || !evt.willInsertAfter
  399. ? currentIndex
  400. : currentIndex + 1;
  401. },
  402. onDragMove(evt, originalEvent) {
  403. const onMove = this.move;
  404. if (!onMove || !this.realList) {
  405. return true;
  406. }
  407. const relatedContext = this.getRelatedContextFromMoveEvent(evt);
  408. const draggedContext = this.context;
  409. const futureIndex = this.computeFutureIndex(relatedContext, evt);
  410. Object.assign(draggedContext, { futureIndex });
  411. const sendEvt = Object.assign({}, evt, {
  412. relatedContext,
  413. draggedContext
  414. });
  415. return onMove(sendEvt, originalEvent);
  416. },
  417. onDragEnd() {
  418. this.computeIndexes();
  419. draggingElement = null;
  420. }
  421. }
  422. };
  423. if (typeof window !== "undefined" && "Vue" in window) {
  424. window.Vue.component("draggable", draggableComponent);
  425. }
  426. export default draggableComponent;