Skip to content

Commit a312acb

Browse files
committed
feat: drag drop live preview implementation
1 parent e6fbfb4 commit a312acb

File tree

2 files changed

+218
-21
lines changed

2 files changed

+218
-21
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 157 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,157 @@ function RemoteFunctions(config) {
323323
delete element._originalDragOpacity;
324324
}
325325

326+
// CSS class name for drop markers
327+
let DROP_MARKER_CLASSNAME = "__brackets-drop-marker";
328+
329+
/**
330+
* This function creates a marker to indicate a valid drop position
331+
* @param {DOMElement} element - The element where the drop is possible
332+
*/
333+
function _createDropMarker(element) {
334+
// clean any existing marker from that element
335+
_removeDropMarkerFromElement(element);
336+
337+
// create the marker element
338+
let marker = window.document.createElement("div");
339+
marker.className = DROP_MARKER_CLASSNAME;
340+
341+
// position the marker at the top of the element
342+
let rect = element.getBoundingClientRect();
343+
let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
344+
let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
345+
346+
// marker styling
347+
marker.style.position = "absolute";
348+
marker.style.top = (rect.top + scrollTop - 5) + "px";
349+
marker.style.left = (rect.left + scrollLeft) + "px";
350+
marker.style.width = rect.width + "px";
351+
marker.style.height = "2px";
352+
marker.style.backgroundColor = "#4285F4";
353+
marker.style.zIndex = "2147483646";
354+
355+
element._dropMarker = marker; // we need this in the _removeDropMarkerFromElement function
356+
window.document.body.appendChild(marker);
357+
}
358+
359+
/**
360+
* This function removes a drop marker from a specific element
361+
* @param {DOMElement} element - The element to remove the marker from
362+
*/
363+
function _removeDropMarkerFromElement(element) {
364+
if (element._dropMarker && element._dropMarker.parentNode) {
365+
element._dropMarker.parentNode.removeChild(element._dropMarker);
366+
delete element._dropMarker;
367+
}
368+
}
369+
370+
/**
371+
* this function is to clear all the drop markers from the document
372+
*/
373+
function _clearDropMarkers() {
374+
let markers = window.document.querySelectorAll("." + DROP_MARKER_CLASSNAME);
375+
for (let i = 0; i < markers.length; i++) {
376+
if (markers[i].parentNode) {
377+
markers[i].parentNode.removeChild(markers[i]);
378+
}
379+
}
380+
381+
// Also clear any element references
382+
let elements = window.document.querySelectorAll("[data-brackets-id]");
383+
for (let j = 0; j < elements.length; j++) {
384+
delete elements[j]._dropMarker;
385+
}
386+
}
387+
388+
/**
389+
* Handle dragover events on the document
390+
* Shows drop markers on valid drop targets
391+
* @param {Event} event - The dragover event
392+
*/
393+
function onDragOver(event) {
394+
// we set this on dragStart
395+
if (!window._currentDraggedElement) {
396+
return;
397+
}
398+
399+
event.preventDefault();
400+
401+
// get the element under the cursor
402+
let target = document.elementFromPoint(event.clientX, event.clientY);
403+
if (!target || target === window._currentDraggedElement) {
404+
return;
405+
}
406+
407+
// get the closest element with a data-brackets-id
408+
while (target && !target.hasAttribute("data-brackets-id")) {
409+
target = target.parentElement;
410+
}
411+
412+
// skip if no valid target found or if it's the dragged element
413+
if (!target || target === window._currentDraggedElement) {
414+
return;
415+
}
416+
417+
// Skip BODY and HTML tags
418+
if (target.tagName === "BODY" || target.tagName === "HTML") {
419+
return;
420+
}
421+
422+
// before creating a drop marker, make sure that we clear all the drop markers
423+
_clearDropMarkers();
424+
_createDropMarker(target);
425+
}
426+
427+
/**
428+
* Handle drop events on the document
429+
* Processes the drop of a dragged element onto a valid target
430+
* @param {Event} event - The drop event
431+
*/
432+
function onDrop(event) {
433+
if (!window._currentDraggedElement) {
434+
return;
435+
}
436+
437+
event.preventDefault();
438+
event.stopPropagation();
439+
440+
// get the element under the cursor
441+
let target = document.elementFromPoint(event.clientX, event.clientY);
442+
443+
// get the closest element with a data-brackets-id
444+
while (target && !target.hasAttribute("data-brackets-id")) {
445+
target = target.parentElement;
446+
}
447+
448+
// skip if no valid target found or if it's the dragged element
449+
if (!target || target === window._currentDraggedElement) {
450+
return;
451+
}
452+
453+
// Skip BODY and HTML tags
454+
if (target.tagName === "BODY" || target.tagName === "HTML") {
455+
return;
456+
}
457+
458+
// IDs of the source and target elements
459+
const sourceId = window._currentDraggedElement.getAttribute("data-brackets-id");
460+
const targetId = target.getAttribute("data-brackets-id");
461+
462+
// send message to the editor
463+
window._Brackets_MessageBroker.send({
464+
livePreviewEditEnabled: true,
465+
sourceElement: window._currentDraggedElement,
466+
targetElement: target,
467+
sourceId: Number(sourceId),
468+
targetId: Number(targetId),
469+
move: true
470+
});
471+
472+
_clearDropMarkers();
473+
_dragEndChores(window._currentDraggedElement);
474+
delete window._currentDraggedElement;
475+
}
476+
326477
/**
327478
* This function is to calculate the width of the info box based on the number of chars in the box
328479
* @param {String} tagName - the element's tag name
@@ -406,26 +557,16 @@ function RemoteFunctions(config) {
406557
event.stopPropagation();
407558
event.dataTransfer.setData("text/plain", this.element.getAttribute("data-brackets-id"));
408559
_dragStartChores(this.element);
409-
console.log("pluto- dragstart: ", this.element.getAttribute("data-brackets-id"));
410-
});
411-
412-
this.element.addEventListener("dragover", (event) => {
413-
event.preventDefault();
414-
event.stopPropagation();
415-
console.log("pluto- dragover");
560+
_clearDropMarkers();
561+
window._currentDraggedElement = this.element;
416562
});
417563

418564
this.element.addEventListener("dragend", (event) => {
419565
event.preventDefault();
420566
event.stopPropagation();
421567
_dragEndChores(this.element);
422-
console.log("pluto- dragend");
423-
});
424-
425-
this.element.addEventListener("drop", (event) => {
426-
event.preventDefault();
427-
event.stopPropagation();
428-
console.log("pluto- drop");
568+
_clearDropMarkers();
569+
delete window._currentDraggedElement;
429570
});
430571
},
431572

@@ -1778,6 +1919,8 @@ function RemoteFunctions(config) {
17781919
window.document.addEventListener("mouseover", onElementHover);
17791920
window.document.addEventListener("mouseout", onElementHoverOut);
17801921
window.document.addEventListener("click", onClick);
1922+
window.document.addEventListener("dragover", onDragOver);
1923+
window.document.addEventListener("drop", onDrop);
17811924

17821925
if (experimental) {
17831926
window.document.addEventListener("keydown", onKeyDown);

src/LiveDevelopment/LivePreviewEdit.js

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ define(function (require, exports, module) {
88
* @return {Object} - Object with openTag and closeTag properties
99
*/
1010
function _findContentBoundaries(html) {
11-
const openTagEnd = html.indexOf('>') + 1;
12-
const closeTagStart = html.lastIndexOf('<');
11+
const openTagEnd = html.indexOf(">") + 1;
12+
const closeTagStart = html.lastIndexOf("<");
1313

1414
if (openTagEnd > 0 && closeTagStart > openTagEnd) {
1515
return {
@@ -38,7 +38,7 @@ define(function (require, exports, module) {
3838
*/
3939
function _editTextInSource(message) {
4040
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
41-
if(!currLiveDoc || !currLiveDoc.editor || !message.tagId) {
41+
if (!currLiveDoc || !currLiveDoc.editor || !message.tagId) {
4242
return;
4343
}
4444

@@ -73,7 +73,7 @@ define(function (require, exports, module) {
7373
function _duplicateElementInSourceByTagId(tagId) {
7474
// this is to get the currently live document that is being served in the live preview
7575
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
76-
if(!currLiveDoc) {
76+
if (!currLiveDoc) {
7777
return;
7878
}
7979

@@ -121,7 +121,7 @@ define(function (require, exports, module) {
121121
function _deleteElementInSourceByTagId(tagId) {
122122
// this is to get the currently live document that is being served in the live preview
123123
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
124-
if(!currLiveDoc) {
124+
if (!currLiveDoc) {
125125
return;
126126
}
127127

@@ -140,6 +140,50 @@ define(function (require, exports, module) {
140140
editor.replaceRange("", range.from, range.to);
141141
}
142142

143+
/**
144+
* This function is responsible for moving an element from one position to another in the source code
145+
* it is called when there is drag-drop in the live preview
146+
* @param {Number} sourceId - the data-brackets-id of the element being moved
147+
* @param {Number} targetId - the data-brackets-id of the target element where to move
148+
*/
149+
function _moveElementInSource(sourceId, targetId) {
150+
// this is to get the currently live document that is being served in the live preview
151+
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
152+
if (!currLiveDoc) {
153+
return;
154+
}
155+
156+
const editor = currLiveDoc.editor;
157+
if (!editor || !sourceId || !targetId) {
158+
return;
159+
}
160+
161+
// position of source and target elements in the editor
162+
const sourceRange = HTMLInstrumentation.getPositionFromTagId(editor, sourceId);
163+
164+
if (!sourceRange) {
165+
return;
166+
}
167+
168+
const sourceText = editor.getTextBetween(sourceRange.from, sourceRange.to);
169+
170+
// creating a batch operation so that undo in live preview works fine
171+
editor.document.batchOperation(function () {
172+
// first, we need to remove the source code from its initial position
173+
editor.replaceRange("", sourceRange.from, sourceRange.to);
174+
175+
// get the target range, this is where we want to insert the text
176+
const targetRange = HTMLInstrumentation.getPositionFromTagId(editor, targetId);
177+
if(!targetRange) {
178+
return;
179+
}
180+
const targetText = editor.getTextBetween(targetRange.from, targetRange.to);
181+
182+
// sourceText + targetText is done so that new source text can maintain the indentation
183+
editor.replaceRange(sourceText + targetText, targetRange.from, targetRange.to);
184+
});
185+
}
186+
143187
/**
144188
* This is the main function that is exported.
145189
* it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js
@@ -153,15 +197,25 @@ define(function (require, exports, module) {
153197
tagId: tagId,
154198
delete || duplicate || livePreviewTextEdit: true
155199
undoLivePreviewOperation: true (this property is available only for undo operation)
200+
201+
sourceId: sourceId, (these are for move (drag & drop))
202+
targetId: targetId,
203+
move: true
156204
}
157205
* these are the main properties that are passed through the message
158206
*/
159207
function handleLivePreviewEditOperation(message) {
208+
// handle move(drag & drop)
209+
if (message.move && message.sourceId && message.targetId) {
210+
_moveElementInSource(message.sourceId, message.targetId);
211+
return;
212+
}
213+
160214
if (!message.element || !message.tagId) {
161215
// check for undo
162-
if(message.undoLivePreviewOperation) {
216+
if (message.undoLivePreviewOperation) {
163217
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
164-
if(!currLiveDoc || !currLiveDoc.editor) {
218+
if (!currLiveDoc || !currLiveDoc.editor) {
165219
return;
166220
}
167221

0 commit comments

Comments
 (0)