4c5626919b86d9370d3917cae437f2f562729fb1953d4bc4e2d8363b3724c7edb687d1156c4633caff089935f9f5607380a30e7b189bc406dcd7b387ac48de 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import Sortable from "sortablejs";
  2. import { insertNodeAt, removeNode } from "./util/htmlHelper";
  3. import { console } from "./util/console";
  4. import {
  5. getComponentAttributes,
  6. createSortableOption,
  7. getValidSortableEntries
  8. } from "./core/componentBuilderHelper";
  9. import { computeComponentStructure } from "./core/renderHelper";
  10. import { events } from "./core/sortableEvents";
  11. import { h, defineComponent, nextTick } from "vue";
  12. function emit(evtName, evtData) {
  13. nextTick(() => this.$emit(evtName.toLowerCase(), evtData));
  14. }
  15. function manage(evtName) {
  16. return (evtData, originalElement) => {
  17. if (this.realList !== null) {
  18. return this[`onDrag${evtName}`](evtData, originalElement);
  19. }
  20. };
  21. }
  22. function manageAndEmit(evtName) {
  23. const delegateCallBack = manage.call(this, evtName);
  24. return (evtData, originalElement) => {
  25. delegateCallBack.call(this, evtData, originalElement);
  26. emit.call(this, evtName, evtData);
  27. };
  28. }
  29. let draggingElement = null;
  30. const props = {
  31. list: {
  32. type: Array,
  33. required: false,
  34. default: null
  35. },
  36. modelValue: {
  37. type: Array,
  38. required: false,
  39. default: null
  40. },
  41. itemKey: {
  42. type: [String, Function],
  43. required: true
  44. },
  45. clone: {
  46. type: Function,
  47. default: original => {
  48. return original;
  49. }
  50. },
  51. tag: {
  52. type: String,
  53. default: "div"
  54. },
  55. move: {
  56. type: Function,
  57. default: null
  58. },
  59. componentData: {
  60. type: Object,
  61. required: false,
  62. default: null
  63. }
  64. };
  65. const emits = [
  66. "update:modelValue",
  67. "change",
  68. ...[...events.manageAndEmit, ...events.emit].map(evt => evt.toLowerCase())
  69. ];
  70. const draggableComponent = defineComponent({
  71. name: "draggable",
  72. inheritAttrs: false,
  73. props,
  74. emits,
  75. data() {
  76. return {
  77. error: false
  78. };
  79. },
  80. render() {
  81. try {
  82. this.error = false;
  83. const { $slots, $attrs, tag, componentData, realList, getKey } = this;
  84. const componentStructure = computeComponentStructure({
  85. $slots,
  86. tag,
  87. realList,
  88. getKey
  89. });
  90. this.componentStructure = componentStructure;
  91. const attributes = getComponentAttributes({ $attrs, componentData });
  92. return componentStructure.render(h, attributes);
  93. } catch (err) {
  94. this.error = true;
  95. return h("pre", { style: { color: "red" } }, err.stack);
  96. }
  97. },
  98. created() {
  99. if (this.list !== null && this.modelValue !== null) {
  100. console.error(
  101. "modelValue and list props are mutually exclusive! Please set one or another."
  102. );
  103. }
  104. },
  105. mounted() {
  106. if (this.error) {
  107. return;
  108. }
  109. const { $attrs, $el, componentStructure } = this;
  110. componentStructure.updated();
  111. const sortableOptions = createSortableOption({
  112. $attrs,
  113. callBackBuilder: {
  114. manageAndEmit: event => manageAndEmit.call(this, event),
  115. emit: event => emit.bind(this, event),
  116. manage: event => manage.call(this, event)
  117. }
  118. });
  119. const targetDomElement = $el.nodeType === 1 ? $el : $el.parentElement;
  120. this._sortable = new Sortable(targetDomElement, sortableOptions);
  121. this.targetDomElement = targetDomElement;
  122. targetDomElement.__draggable_component__ = this;
  123. },
  124. updated() {
  125. this.componentStructure.updated();
  126. },
  127. beforeUnmount() {
  128. if (this._sortable !== undefined) this._sortable.destroy();
  129. },
  130. computed: {
  131. realList() {
  132. const { list } = this;
  133. return list ? list : this.modelValue;
  134. },
  135. getKey() {
  136. const { itemKey } = this;
  137. if (typeof itemKey === "function") {
  138. return itemKey;
  139. }
  140. return element => element[itemKey];
  141. }
  142. },
  143. watch: {
  144. $attrs: {
  145. handler(newOptionValue) {
  146. const { _sortable } = this;
  147. if (!_sortable) return;
  148. getValidSortableEntries(newOptionValue).forEach(([key, value]) => {
  149. _sortable.option(key, value);
  150. });
  151. },
  152. deep: true
  153. }
  154. },
  155. methods: {
  156. getUnderlyingVm(domElement) {
  157. return this.componentStructure.getUnderlyingVm(domElement) || null;
  158. },
  159. getUnderlyingPotencialDraggableComponent(htmElement) {
  160. //TODO check case where you need to see component children
  161. return htmElement.__draggable_component__;
  162. },
  163. emitChanges(evt) {
  164. nextTick(() => this.$emit("change", evt));
  165. },
  166. alterList(onList) {
  167. if (this.list) {
  168. onList(this.list);
  169. return;
  170. }
  171. const newList = [...this.modelValue];
  172. onList(newList);
  173. this.$emit("update:modelValue", newList);
  174. },
  175. spliceList() {
  176. const spliceList = list => list.splice(...arguments);
  177. this.alterList(spliceList);
  178. },
  179. updatePosition(oldIndex, newIndex) {
  180. const updatePosition = list =>
  181. list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
  182. this.alterList(updatePosition);
  183. },
  184. getRelatedContextFromMoveEvent({ to, related }) {
  185. const component = this.getUnderlyingPotencialDraggableComponent(to);
  186. if (!component) {
  187. return { component };
  188. }
  189. const list = component.realList;
  190. const context = { list, component };
  191. if (to !== related && list) {
  192. const destination = component.getUnderlyingVm(related) || {};
  193. return { ...destination, ...context };
  194. }
  195. return context;
  196. },
  197. getVmIndexFromDomIndex(domIndex) {
  198. return this.componentStructure.getVmIndexFromDomIndex(
  199. domIndex,
  200. this.targetDomElement
  201. );
  202. },
  203. onDragStart(evt) {
  204. this.context = this.getUnderlyingVm(evt.item);
  205. evt.item._underlying_vm_ = this.clone(this.context.element);
  206. draggingElement = evt.item;
  207. },
  208. onDragAdd(evt) {
  209. const element = evt.item._underlying_vm_;
  210. if (element === undefined) {
  211. return;
  212. }
  213. removeNode(evt.item);
  214. const newIndex = this.getVmIndexFromDomIndex(evt.newIndex);
  215. this.spliceList(newIndex, 0, element);
  216. const added = { element, newIndex };
  217. this.emitChanges({ added });
  218. },
  219. onDragRemove(evt) {
  220. insertNodeAt(this.$el, evt.item, evt.oldIndex);
  221. if (evt.pullMode === "clone") {
  222. removeNode(evt.clone);
  223. return;
  224. }
  225. const { index: oldIndex, element } = this.context;
  226. this.spliceList(oldIndex, 1);
  227. const removed = { element, oldIndex };
  228. this.emitChanges({ removed });
  229. },
  230. onDragUpdate(evt) {
  231. removeNode(evt.item);
  232. insertNodeAt(evt.from, evt.item, evt.oldIndex);
  233. const oldIndex = this.context.index;
  234. const newIndex = this.getVmIndexFromDomIndex(evt.newIndex);
  235. this.updatePosition(oldIndex, newIndex);
  236. const moved = { element: this.context.element, oldIndex, newIndex };
  237. this.emitChanges({ moved });
  238. },
  239. computeFutureIndex(relatedContext, evt) {
  240. if (!relatedContext.element) {
  241. return 0;
  242. }
  243. const domChildren = [...evt.to.children].filter(
  244. el => el.style["display"] !== "none"
  245. );
  246. const currentDomIndex = domChildren.indexOf(evt.related);
  247. const currentIndex = relatedContext.component.getVmIndexFromDomIndex(
  248. currentDomIndex
  249. );
  250. const draggedInList = domChildren.indexOf(draggingElement) !== -1;
  251. return draggedInList || !evt.willInsertAfter
  252. ? currentIndex
  253. : currentIndex + 1;
  254. },
  255. onDragMove(evt, originalEvent) {
  256. const { move, realList } = this;
  257. if (!move || !realList) {
  258. return true;
  259. }
  260. const relatedContext = this.getRelatedContextFromMoveEvent(evt);
  261. const futureIndex = this.computeFutureIndex(relatedContext, evt);
  262. const draggedContext = {
  263. ...this.context,
  264. futureIndex
  265. };
  266. const sendEvent = {
  267. ...evt,
  268. relatedContext,
  269. draggedContext
  270. };
  271. return move(sendEvent, originalEvent);
  272. },
  273. onDragEnd() {
  274. draggingElement = null;
  275. }
  276. }
  277. });
  278. export default draggableComponent;