{"componentChunkName":"component---src-templates-post-js","path":"/javascript-drag-and-drop-to-sort-locations-and-render-routes","result":{"data":{"mdx":{"frontmatter":{"title":"JavaScript Drag and Drop to Sort Locations and Render Routes","date":"08/23/2020"},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"JavaScript Drag and Drop to Sort Locations and Render Routes\",\n  \"slug\": \"javascript-drag-and-drop-to-sort-locations-and-render-routes\",\n  \"date\": \"2020-08-23T00:00:00.000Z\",\n  \"image\": \"./images/direction-plant.jpg\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"This is a write-up on the subject feature of the Trip Planner app that I recently built for the Flatiron School\\u2019s software engineering program. With this feature, users can drag and drop saved places in their daily planners to determine the order of visiting each place. Based on this order, the app displays daily routing and directions on click.\"), mdx(\"p\", null, \"A full implementation of the feature involves working with:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API\"\n  }), \"HTML Drag and Drop API\"), \"; and\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://cloud.google.com/maps-platform\"\n  }), \"Google Maps Platform\"), \", especially the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/documentation/javascript/places\"\n  }), \"places library\"), \" and the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/documentation/javascript/directions\"\n  }), \"directions service\"))), mdx(\"h3\", null, \"Drag and Drop Elements with Sorting\"), mdx(\"h4\", null, \"Draggable Elements\"), mdx(\"p\", null, \"First, identify draggable items, which will have a draggable attribute set as true. They will also listen for the dragstart and dragend events. In my example, the draggable items are dynamically added to a list when user clicks on the save button, like so:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const addPlaceToPlanner = () => {\\n  // get place_id\\n  const placeId = document.querySelector(\\\".place-details\\\").id\\n\\n  const placeItem = document.createElement(\\\"div\\\")\\n\\n  // set place_id on item for later google map directions queries\\n  placeItem.setAttribute(\\\"data-place-id\\\", placeId)\\n\\n  placeItem.setAttribute(\\\"draggable\\\", true)\\n  placeItem.innerHTML = `...` // omitted HTML\\n\\n  document.querySelector(\\\".place-bucket\\\").appendChild(placeItem)\\n}\\n\")), mdx(\"p\", null, \"The following points are worth noting:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Make sure to preserve the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"pace_id\"), \" (a unique id assigned by Google), which is important for route rendering; more details on this later\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Duplication of draggable elements can be achieved through the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"cloneNode()\"), \" method as follows:\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const duplicateListItem = e => {\\n  const itemNode = e.target.parentNode.parentNode\\n  const clone = itemNode.cloneNode(true)\\n  itemNode.after(clone)\\n}\\n\")), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"In case duplication is needed, place_id should be saved with the data instead of id attribute; this is because \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"cloneNode()\"), \" also clones the element\\u2019s id, which needs to be unique on the DOM\")), mdx(\"h4\", null, \"Assign dragstart and dragend Event Listeners\"), mdx(\"p\", null, \"In my case, the dragstart and dragend event listeners are delegated from the entire panel to individual list items. It is necessary for the duplication feature. The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"cloneNode()\"), \" method does not clone event listeners from the original elements. If duplication is not needed, you can add the event listeners on the item as you append it to the parent element.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"document.querySelector(\\\".planner-content\\\").addEventListener(\\\"dragstart\\\", e => {\\n  if (e.target.closest(\\\".list-item\\\")) {\\n    e.target.closest(\\\".list-item\\\").classList.add(\\\"dragging\\\")\\n  }\\n})\\n\\ndocument.querySelector(\\\".planner-content\\\").addEventListener(\\\"dragend\\\", e => {\\n  if (e.target.closest(\\\".list-item\\\")) {\\n    e.target.closest(\\\".list-item\\\").classList.remove(\\\"dragging\\\")\\n  }\\n})\\n\")), mdx(\"p\", null, \"The above code allows for the targeting of the single element that is being dragged on the DOM through the class name of \\u201Cdragging\\u201D, which facilitates style changes and item sorting in the receiving container when drag ends.\"), mdx(\"h4\", null, \"Assign dragover Event Listeners to Containers\"), mdx(\"p\", null, \"Identify containers that will receive draggable elements to assign dragover event listeners, like so:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"dayBox.addEventListener(\\\"dragover\\\", e => {\\n  // if you don't use arrow function, you can refer to e.currentTarget by 'this'\\n  const container = e.currentTarget \\n\\n  // there is only one single element with this className\\n  const item = document.querySelector(\\\".dragging\\\") \\n\\n  // e.clientY returns the vertical coordinate within client area where the event occured\\n  // the dragover event continuously occurs\\n  const afterElement = getDragAfterElement(container, e.clientY)\\n\\n  if (afterElement) {\\n    container.insertBefore(item, afterElement)\\n  } else {\\n    container.appendChild(item)\\n  }\\n\\n  // the default handling of the dragover event is not to allow a drop\\n  e.preventDefault()\\n})\\n\\nconst getDragAfterElement = (container, y) => {\\n  const draggableElms = [\\n    ...container.querySelectorAll(\\\".list-item:not(.dragging)\\\"),\\n  ]\\n\\n  // think of arguments in the reduce as:\\n  // closest element which we insert the dragging element before\\n  // current child of container\\n  return draggableElms.reduce(\\n    (closest, child) => {\\n\\n      // the size of the element and its position relative to the viewport\\n      const rect = child.getBoundingClientRect() \\n\\n      // (rect.top + rect.height/2) returns the y of the container's child element's middle point\\n      const offset = y - (rect.top + rect.height / 2)\\n\\n      // if the dragging element is immediately above the child's middle point\\n      if (offset < 0 && offset > closest.offset) {\\n        return { offset: offset, element: child }\\n      } else {\\n        return closest\\n      }\\n    },\\n    // the initial value in the reduce function is negative infinity\\n    { offset: Number.NEGATIVE_INFINITY }\\n  ).element \\n}\\n\")), mdx(\"p\", null, \"I provide comments in the above code to explain, especially, the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"getDragAfterElement()\"), \" function, which involves comparing the Y coordinates of the dragging element to those of the existing elements in the receiving container. The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"reduce()\"), \" function within \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"getDragAfterElement()\"), \" returns the element whose middle point the dragging element is immediately above. If the code is unclear, please test with console.logging the y and offset values.\"), mdx(\"p\", null, \"This concludes the actions for drag and drop with sorting. The below section explains how to work with the Google Maps Platform to render routes for a given day\\u2019s locations.\"), mdx(\"h3\", null, \"Route and Directions Rendering\"), mdx(\"h4\", null, \"Set up Google Maps API\"), mdx(\"p\", null, \"Sign up for the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://cloud.google.com/maps-platform\"\n  }), \"Google Maps Platform\"), \", which provides a trial period. Please refer to \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/gmp-get-started\"\n  }), \"Google\\u2019s documentation\"), \" on how to get an API key, enable services, and restrict API call access. Make sure to enable the following APIs:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Maps JavaScript API\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Places API\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Directions API\")), mdx(\"p\", null, \"After everything is squared away with Google, include the following script tag in your index.html\\u2019s head.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-html\"\n  }), \"<script defer src=\\\"https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places\\\"></script>\\n\")), mdx(\"p\", null, \"If you include \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"callback=initMap\"), \" in the script tag, make sure you have an \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"initMap()\"), \" function available globally and your \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"index.js\"), \" tag is before the Google maps API script tag.\"), mdx(\"p\", null, \"In my example, I call \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"initMap()\"), \" after the user enters a location so I can pass in the latitude and longitude, like so:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"// set these variables globally or in the module\\nlet map; \\nlet service;\\nlet directionsService;\\nlet directionsRenderer;\\n\\nconst markers = {};  // only if there is a need to display markers\\n\\nconst initMap = center => {\\n  // a div with #map is required in the HTML\\n  map = new google.maps.Map(document.querySelector('#map'), {\\n\\n    // pass a center like so: { lat: ... , lgn: ... }\\n    center: center, \\n    zoom: 13,\\n    // this parameter is not required\\n    styles: GMStyles.mapStyles\\n  });\\n}\\n\")), mdx(\"p\", null, \"Note:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Declare the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"map\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"service\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"directionsService\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"directionsRenderer\"), \" so that consecutive routing can be made with the previous one cleared; this may not be the best practice and I will make updates after I further study managing state through React\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Only the center and zoom parameters are required to initialize the map; if styles are needed, I recommend generating a JSON style with the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://mapstyle.withgoogle.com/\"\n  }), \"style wizard\"))), mdx(\"h4\", null, \"Get place_ids for Route Mapping and Set Up directionsPanel for Displaying Directions\"), mdx(\"p\", null, \"With the place items\\u2019 order already sorted through drag and drop on the DOM and the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"place_id\"), \" saved with the elements\\u2019 data attribute, we can get an array of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"place_id\"), \"s like so:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const getPlaceIds = e => {\\n  // title element is before the list of place items\\n  const titleElm = e.target.closest('.title'); \\n  const itemElms = (titleElm.nextElementSibling.children ? [...titleElm.nextElementSibling.children] : null);\\n  // no action if there are less than 2 places\\n  if (!itemElms || itemElms.length < 2) return null;\\n  return itemElms.map(item => item.dataset.placeId);\\n}\\n\")), mdx(\"p\", null, \"Before we can display the directions, we need to ensure that there is a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"div\"), \" with the id \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"directionsPanel\"), \" on the DOM. In my case, I needed to dynamically replace the place overview panel or the place details panel with the directions panel, so I created a function like so:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-JavaScript\"\n  }), \"function createDirectionsPanel() {\\n  const directionsPanel = document.createElement('div');\\n  // google requires a div with #directionsPanel\\n  directionsPanel.id = 'directionsPanel'; \\n  directionsPanel.innerHTML = `...`  // omitted HTML\\n\\n  return directionsPanel;\\n}\\n\")), mdx(\"h4\", null, \"Render Routes and Directions\"), mdx(\"p\", null, \"With all the setup and preparations done, we can finally render the routes and directions! Below is the function for this step:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-javascript\"\n  }), \"const renderRoute = placeIds => {\\n  removeDirectionsRenderer();  // remove previous directions\\n  \\n  directionsService = new google.maps.DirectionsService(); // resets the global variable\\n  directionsRenderer = new google.maps.DirectionsRenderer();\\n\\n  // construct an array of waypoint objects\\n  const stopovers = placeIds.slice(1, placeIds.length - 1).map(id => {\\n    return {stopover: true, location: {placeId: id}}  // passing the place_id is an easy way to query directions\\n  });\\n  \\n  const request = {\\n    origin: {placeId: placeIds[0]},\\n    destination: {placeId: placeIds[placeIds.length - 1]},\\n    waypoints: stopovers,\\n    travelMode: google.maps.TravelMode.WALKING,  // pass the desired travel mode\\n    unitSystem: google.maps.UnitSystem.IMPERIAL  // pass the desired unit system\\n  };\\n\\n  directionsService.route(request, function(result, status) {\\n    if (status == 'OK') {\\n      directionsRenderer.setMap(map); // map accessible globally\\n     \\n      clearMarkers(); // only necessary if there are existing markers on the map\\n      \\n      // clear all elements that may be displaying where the directions panel should be rendered \\n      document.querySelector('.place-overview').style.display = 'none';\\n      removeItem(document.querySelector('.place-details'));\\n      removeItem(document.querySelector('#directionsPanel'));\\n      \\n      // set up directions panel for rendering directions\\n      const directionsPanel = createDirectionsPanel();\\n      directionsRenderer.setPanel(directionsPanel);\\n      directionsRenderer.setDirections(result);\\n    } else {\\n      alert(status);\\n    }\\n  });\\n}\\n\\n// this is necessary so there are not multiple routes displaying on the map\\nconst removeDirectionsRenderer = () => {\\n  if (directionsRenderer != null) {\\n    directionsRenderer.setMap(null);\\n    directionsRenderer = null;\\n  }\\n}\\n\\nconst removeItem = item => {\\n  if(item) item.remove();\\n}\\n\")), mdx(\"p\", null, \"Some of the code in the above example use Google Map API\\u2019s classes and methods. I include links to the API reference with brief and easy-to-understand explanations below:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/documentation/javascript/reference/directions#DirectionsService\"\n  }), \"google.maps.DirectionsService\"), \" class\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/documentation/javascript/reference/directions#DirectionsService.route\"\n  }), \"route(request, callback)\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/documentation/javascript/reference/directions#DirectionsRenderer.setMap\"\n  }), \"setMap(map)\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/documentation/javascript/reference/directions#DirectionsRenderer.setPanel\"\n  }), \"setPanel(panel)\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://developers.google.com/maps/documentation/javascript/reference/directions#DirectionsRenderer.setDirections\"\n  }), \"setDirections(directions)\"))), mdx(\"h3\", null, \"Conclusion\"), mdx(\"p\", null, \"This is the entire process for implementing drag and drop to sort locations and render routes. The Google Maps Platform includes several big APIs worth exploring. With their well-written guides and references, we can easily practice our problem-solving skills, and as a bonus, create cool things!\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"slug":"javascript-drag-and-drop-to-sort-locations-and-render-routes"}},"staticQueryHashes":["1063564771","2248308066","2468095761"]}