Precise Drag and Drop within a contenteditable

后端 未结 3 388
礼貌的吻别
礼貌的吻别 2020-12-22 23:42

The Setup

So, I have a contenteditable div -- I\'m making a WYSIWYG editor: bold, italics, formatting, whatever, and most lately: inserting fancy images (in a fanc

相关标签:
3条回答
  • 2020-12-23 00:04

    Dragon Drop

    I've done a ridiculous amount of fiddling. So, so much jsFiddling.

    This is not a robust, or complete solution; I may never quite come up with one. If anyone has any better solutions, I'm all ears -- I didn't want to have to do it this way, but it's the only way I've been able to uncover so far. The following jsFiddle, and the information I am about to vomit up, worked for me in this particular instance with my particular versions of Firefox and Chrome on my particular WAMP setup and computer. Don't come crying to me when it doesn't work on your website. This drag-and-drop crap is clearly every man for himself.

    jsFiddle: Chase Moskal's Dragon Drop

    So, I was boring my girlfriend's brains out, and she thought I kept saying "dragon drop" when really, I was just saying "drag-and-drop". It stuck, so that's what I call my little JavaScript buddy I've created for handling these drag-and-drop situations.

    Turns out -- it's a bit of a nightmare. The HTML5 Drag-and-Drop API even at first glance, is horrible. Then, you almost warm up to it, as you start to understand and accept the way it's supposed to work.. Then you realize what a terrifying nightmare it actually is, as you learn how Firefox and Chrome go about this specification in their own special way, and seem to completely ignore all of your needs. You find yourself asking questions like: "Wait, what element is even being dragged right now? How to do I get that information? How do I cancel this drag operation? How can I stop this particular browser's unique default handling of this situation?"... The answers to your questions: "You're on your own, LOSER! Keep hacking things in, until something works!".

    So, here's how I accomplished Precise Drag and Drop of Arbitrary HTML Elements within, around, and between multiple contenteditable's. (note: I'm not going fully in-depth with every detail, you'll have to look at the jsFiddle for that -- I'm just rambling off seemingly relevant details that I remember from the experience, as I have limited time)

    My Solution

    • First, I applied CSS to the draggables (fancybox) -- we needed user-select:none; user-drag:element; on the fancy box, and then specifically user-drag:none; on the image within the fancy box (and any other elements, why not?). Unfortunately, this was not quite enough for Firefox, which required attribute draggable="false" to be explicitly set on the image to prevent it from being draggable.
    • Next, I applied attributes draggable="true" and dropzone="copy" onto the contenteditables.

    To the draggables (fancyboxes), I bind a handler for dragstart. We set the dataTransfer to copy a blank string of HTML ' ' -- because we need to trick it into thinking we are going to drag HTML, but we are cancelling out any default behavior. Sometimes default behavior slips in somehow, and it results in a duplicate (as we do the insertion ourselves), so now the worst glitch is a ' ' (space) being inserted when a drag fails. We couldn't rely on the default behavior, as it would fail to often, so I found this to be the most versatile solution.

    DD.$draggables.off('dragstart').on('dragstart',function(event){
        var e=event.originalEvent;
        $(e.target).removeAttr('dragged');
        var dt=e.dataTransfer,
            content=e.target.outerHTML;
        var is_draggable = DD.$draggables.is(e.target);
        if (is_draggable) {
            dt.effectAllowed = 'copy';
            dt.setData('text/plain',' ');
            DD.dropLoad=content;
            $(e.target).attr('dragged','dragged');
        }
    });
    

    To the dropzones, I bind a handler for dragleave and drop. The dragleave handler exists only for Firefox, as in Firefox, the drag-drop would work (Chrome denies you by default) when you tried to drag it outside the contenteditable, so it performs a quick check against the Firefox-only relatedTarget. Huff.

    Chrome and Firefox have different ways of acquiring the Range object, so effort had to be put in to do it differently for each browser in the drop event. Chrome builds a range based on mouse-coordinates (yup that's right), but Firefox provides it in the event data. document.execCommand('insertHTML',false,blah) turns out to be how we handle the drop. OH, I forgot to mention -- we can't use dataTransfer.getData() on Chrome to get our dragstart set HTML -- it appears to be some kind of weird bug in the specification. Firefox calls the spec out on it's bullcrap and gives us the data anyways -- but Chrome doesn't, so we bend over backwards and to set the content to a global, and go through hell to kill all the default behavior...

    DD.$dropzones.off('dragleave').on('dragleave',function(event){
        var e=event.originalEvent;
    
        var dt=e.dataTransfer;
        var relatedTarget_is_dropzone = DD.$dropzones.is(e.relatedTarget);
        var relatedTarget_within_dropzone = DD.$dropzones.has(e.relatedTarget).length>0;
        var acceptable = relatedTarget_is_dropzone||relatedTarget_within_dropzone;
        if (!acceptable) {
            dt.dropEffect='none';
            dt.effectAllowed='null';
        }
    });
    DD.$dropzones.off('drop').on('drop',function(event){
        var e=event.originalEvent;
    
        if (!DD.dropLoad) return false;
        var range=null;
        if (document.caretRangeFromPoint) { // Chrome
            range=document.caretRangeFromPoint(e.clientX,e.clientY);
        }
        else if (e.rangeParent) { // Firefox
            range=document.createRange(); range.setStart(e.rangeParent,e.rangeOffset);
        }
        var sel = window.getSelection();
        sel.removeAllRanges(); sel.addRange(range);
    
        $(sel.anchorNode).closest(DD.$dropzones.selector).get(0).focus(); // essential
        document.execCommand('insertHTML',false,'<param name="dragonDropMarker" />'+DD.dropLoad);
        sel.removeAllRanges();
    
        // verification with dragonDropMarker
        var $DDM=$('param[name="dragonDropMarker"]');
        var insertSuccess = $DDM.length>0;
        if (insertSuccess) {
            $(DD.$draggables.selector).filter('[dragged]').remove();
            $DDM.remove();
        }
    
        DD.dropLoad=null;
        DD.bindDraggables();
        e.preventDefault();
    });
    

    Okay, I'm sick of this. I've wrote all I want to about this. I'm calling it a day, and might update this if I think of anything important.

    Thanks everybody. //Chase.

    0 讨论(0)
  • 2020-12-23 00:18
    1. event dragstart; dataTransfer.setData("text/html", "<div class='whatever'></div>");
    2. event drop: var me = this; setTimeout(function () { var el = me.element.getElementsByClassName("whatever")[0]; if (el) { //do stuff here, el is your location for the fancy img } }, 0);
    0 讨论(0)
  • 2020-12-23 00:29

    Since I wanted to see this in a native JS solution I worked a bit to remove all jQuery dependencies. Hopefully it can help someone.

    First the markup

        <div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
          WAITING  FOR STUFF
        </div>
        <div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
          <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
            Block 1
          </span>
          <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
            Second Blk
          </span>
        </div>
    

    Then some helpers

        function addClass( elem, className ){
            var classNames = elem.className.split( " " )
            if( classNames.indexOf( className ) === -1 ){
                classNames.push( className )
            }
            elem.className = classNames.join( " " )
        }
        function selectElem( selector ){
            return document.querySelector( selector )
        }
        function selectAllElems( selector ){
            return document.querySelectorAll( selector )
        }
        function removeElem( elem ){
             return elem ? elem.parentNode.removeChild( elem ) : false
        }
    

    Then the actual methods

        function nativeBindDraggable( elems = false ){
            elems = elems || selectAllElems( '.native_drag' );
            if( !elems ){
                // No element exists, abort
                return false;
            }else if( elems.outerHTML ){
                // if only a single element, put in array
                elems = [ elems ];
            }
            // else it is html-collection already (as good as array)
    
            for( let i = 0 ; i < elems.length ; i++ ){
                // For every elem in list, attach or re-attach event handling
                elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
                elems[i].ondragstart = function(e){
                    if (!e.target.id){
                        e.target.id = (new Date()).getTime();
                    }
    
                    window.inTransferMarkup = e.target.outerHTML;
                    window.transferreference = elems[i].dataset.transferreference;
                    addClass( e.target, 'dragged');
                };
            };
        }
    
        function nativeBindWriteRegion( elems = false ){
            elems = elems || selectAllElems( '.native_receiver' );
            if( !elems ){
                // No element exists, abort
                return false;
            }else if( elems.outerHTML ){
                // if only a single element, put in array
                elems = [ elems ];
            }
            // else it is html-collection
    
            for( let i = 0 ; i < elems.length ; i++ ){
                elems[i].ondragover = function(e){
                    e.preventDefault();
                    return false;
                };
                elems[i].ondrop = function(e){
                    receiveBlock(e);
                };
            }
        }
    
        function receiveBlock(e){
            e.preventDefault();
            let content = window.inTransferMarkup;
    
            window.inTransferMarkup = "";
    
            let range = null;
            if (document.caretRangeFromPoint) { // Chrome
                range = document.caretRangeFromPoint(e.clientX, e.clientY);
            }else if (e.rangeParent) { // Firefox
                range = document.createRange();
                range.setStart(e.rangeParent, e.rangeOffset);
            }
            let sel = window.getSelection();
            sel.removeAllRanges(); 
            sel.addRange( range );
            e.target.focus();
    
            document.execCommand('insertHTML',false, content);
            sel.removeAllRanges();
    
            // reset draggable on all blocks, esp the recently created
            nativeBindDraggable(
              document.querySelector(
                `[data-transferreference='${window.transferreference}']`
              )
            );
            removeElem( selectElem( '.dragged' ) );
            return false;
        }
    

    And lastly instantiate

    nativeBindDraggable();
    nativeBindWriteRegion();
    

    Below is the functioning snippet

    function addClass( elem, className ){
                var classNames = elem.className.split( " " )
                if( classNames.indexOf( className ) === -1 ){
                    classNames.push( className )
                }
                elem.className = classNames.join( " " )
            }
            function selectElem( selector ){
                return document.querySelector( selector )
            }
            function selectAllElems( selector ){
                return document.querySelectorAll( selector )
            }
            function removeElem( elem ){
                 return elem ? elem.parentNode.removeChild( elem ) : false
            }
            
          
        	function nativeBindDraggable( elems = false ){
        		elems = elems || selectAllElems( '.native_drag' );
        		if( !elems ){
        			// No element exists, abort
        			return false;
        		}else if( elems.outerHTML ){
        			// if only a single element, put in array
        			elems = [ elems ];
        		}
        		// else it is html-collection already (as good as array)
                
        		for( let i = 0 ; i < elems.length ; i++ ){
        			// For every elem in list, attach or re-attach event handling
        			elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
        			elems[i].ondragstart = function(e){
        				if (!e.target.id){
        					e.target.id = (new Date()).getTime();
        				}
    
        				window.inTransferMarkup = e.target.outerHTML;
        				window.transferreference = elems[i].dataset.transferreference;
        				addClass( e.target, 'dragged');
        			};
        		};
        	}
            
        	function nativeBindWriteRegion( elems = false ){
        		elems = elems || selectAllElems( '.native_receiver' );
        		if( !elems ){
        			// No element exists, abort
        			return false;
        		}else if( elems.outerHTML ){
        			// if only a single element, put in array
        			elems = [ elems ];
        		}
        		// else it is html-collection
        		
        		for( let i = 0 ; i < elems.length ; i++ ){
        			elems[i].ondragover = function(e){
        				e.preventDefault();
        				return false;
        			};
        			elems[i].ondrop = function(e){
        				receiveBlock(e);
        			};
        		}
        	}
            
            function receiveBlock(e){
        		e.preventDefault();
        		let content = window.inTransferMarkup;
        		
        		window.inTransferMarkup = "";
        		
        		let range = null;
        		if (document.caretRangeFromPoint) { // Chrome
        			range = document.caretRangeFromPoint(e.clientX, e.clientY);
        		}else if (e.rangeParent) { // Firefox
        			range = document.createRange();
        			range.setStart(e.rangeParent, e.rangeOffset);
        		}
        		let sel = window.getSelection();
        		sel.removeAllRanges(); 
        		sel.addRange( range );
        		e.target.focus();
        		
        		document.execCommand('insertHTML',false, content);
        		sel.removeAllRanges();
        		
                // reset draggable on all blocks, esp the recently created
        		nativeBindDraggable(
                  document.querySelector(
                    `[data-transferreference='${window.transferreference}']`
                  )
                );
        		removeElem( selectElem( '.dragged' ) );
        		return false;
        	}
    
    
        nativeBindDraggable();
        nativeBindWriteRegion();
            <div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
              WAITING  FOR STUFF
            </div>
            <div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
              <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
                Block 1
              </span>
              <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
                Second Blk
              </span>
            </div>

    0 讨论(0)
提交回复
热议问题