it-swarm.com.ru

Точное перетаскивание в contenteditable

Настройка

Итак, у меня есть спорный div - я делаю WYSIWYG-редактор: полужирный, курсив, форматирование, что угодно, и в последнее время: вставка причудливых изображений (в причудливом поле с подписью).

<a class="fancy" href="i.jpg" target="_blank">
    <img alt="" src="i.jpg" />
    Optional Caption goes Here!
</a>

Пользователь добавляет эти причудливые изображения с помощью диалогового окна, с которым я их представляю: они заполняют детали, загружают изображение, а затем, подобно другим функциям редактора, я использую document.execCommand('insertHTML',false,fancy_image_html);, чтобы отобразить его по выбору пользователя.

Желаемая функциональность

Итак, теперь, когда мой пользователь может получить фантастическое изображение, ему нужно иметь возможность перемещать его. Пользователь должен иметь возможность щелкать и перетаскивать изображение (причудливую рамку и все), чтобы поместить его в любое удобное для него место. Они должны иметь возможность перемещать его между абзацами или даже внутри абзацев - между двумя словами, если они хотят.

Что дает мне надежду

Имейте в виду - в contenteditable, простых старых <img> теги уже наделены пользовательским агентом с этой прекрасной возможностью перетаскивания. По умолчанию вы можете перетаскивать <img> теги везде, где пожелаете; стандартная операция перетаскивания ведет себя так, как хотелось бы.

Итак, учитывая то, как это поведение по умолчанию уже работает так потрясающе на наших собеседниках <img> - и я только хочу немного расширить это поведение, добавив немного больше HTML - это кажется чем-то, что должно быть легко возможным.

Мои усилия пока что

Сначала я установил свой причудливый тег <a> с атрибутом draggable и отключил contenteditable (не уверен, если это необходимо, но кажется, что он также может быть отключен):

<a class="fancy" [...] draggable="true" contenteditable="false">

Затем, поскольку пользователь все еще мог перетаскивать изображение из необычного поля <a>, мне пришлось немного поработать с CSS. Я работаю в Chrome, поэтому я показываю вам только префиксы -webkit-, хотя я использовал и другие.

.fancy {
    -webkit-user-select:none;
    -webkit-user-drag:element; }
    .fancy>img {
        -webkit-user-drag:none; }

Теперь пользователь может перетащить весь модный блок, и это отражает небольшое частично выцветшее изображение перетаскивания кликом - я вижу, что теперь я поднимаю весь блок :)

Я пробовал несколько комбинаций различных свойств CSS, вышеупомянутая комбинация, кажется, имеет смысл для меня, и, кажется, работает лучше всего.

Я надеялся, что одного только этого CSS будет достаточно, чтобы браузер использовал весь элемент в качестве перетаскиваемого элемента, автоматически предоставляя пользователю функциональность, о которой я мечтал ... Однако, это действительно выглядит быть более сложным, чем это.

API-интерфейс перетаскивания JavaScript в HTML5

Этот материал Drag and Drop кажется более сложным, чем нужно.

Итак, я начал углубляться в документацию DnD api, и теперь я застрял. Итак, вот что я подстроил (да, jQuery):

$('.fancy')
    .bind('dragstart',function(event){
        //console.log('dragstart');
        var dt=event.originalEvent.dataTransfer;
        dt.effectAllowed = 'all';
        dt.setData('text/html',event.target.outerHTML);
    });

$('.myContentEditable')
    .bind('dragenter',function(event){
        //console.log('dragenter');
        event.preventDefault();
    })
    .bind('dragleave',function(event){
        //console.log('dragleave');
    })
    .bind('dragover',function(event){
        //console.log('dragover');
        event.preventDefault();
    })
    .bind('drop',function(event){
        //console.log('drop');      
        var dt = event.originalEvent.dataTransfer;
        var content = dt.getData('text/html');
        document.execCommand('insertHTML',false,content);
        event.preventDefault();
    })
    .bind('dragend',function(event){ 
        //console.log('dragend');
    });

Так вот, где я застрял: Это почти полностью работает. Почти полностью. У меня все работает, до самого конца. В событии перетаскивания у меня теперь есть доступ к HTML-содержимому модного ящика, которое я пытаюсь вставить в место перетаскивания. Все, что мне нужно сделать сейчас, это вставить его в правильное место!

Проблема в том, что я не могу найти правильное место размещения или какой-либо способ вставить в него. Я надеялся найти какой-то вид 'dropLocation' объект, в который можно выгрузить мой модный ящик, что-то вроде dropEvent.dropLocation.content=myFancyBoxHTML;, или, возможно, по крайней мере, какие-то сбрасываемые значения местоположения, с помощью которых я могу найти свой собственный способ размещения контента? Мне что-нибудь дали?

Я делаю это совершенно неправильно? Я что-то упустил?

Я пытался использовать document.execCommand('insertHTML',false,content); так, как я ожидал, но смогу, но, к сожалению, у меня это не получается, поскольку курсор выбора не находится в точном отбрасывании место, как я надеюсь.

Я обнаружил, что, если я закомментирую все event.preventDefault();, каретка выбора становится видимой, и, как можно было бы надеяться, когда пользователь готовится отбросить при наведении их курсора на предмет содержания можно увидеть небольшую каретку выделения, бегущую между символами после операции курсора и перетаскивания пользователя, указывающую пользователю, что каретка выделения представляет точное местоположение перетаскивания. Мне нужно расположение этой каретки выбора.

В некоторых экспериментах я попытался выполнить execCommand-insertHTML во время события drop и события dragend - ни вставить HTML-код там, где был drop-selection-caret, вместо этого он использует любое местоположение, выбранное до операции перетаскивания.

Поскольку каретка выбора видна во время перехода, я разработал план.

Какое-то время я пытался в событии dragover вставить временный маркер, например <span class="selection-marker">|</span>, сразу после $('.selection-marker').remove();, чтобы браузер постоянно (во время dragover) удалял все маркеры выделения, а затем добавлял один в точке вставки - по существу, оставляя один маркер, где бы ни находилась точка вставки, в любой момент. План, конечно, состоял в том, чтобы затем заменить этот временный маркер на перетаскиваемый контент, который у меня есть.

Конечно, ничего из этого не сработало: я не смог заставить маркер выделения вставлять в явно видимую каретку выделения, как планировалось - опять же, execCommand-вставленный HTML размещал себя там, где была каретка выделения, до операции перетаскивания.

Хафф. Так что я пропустил? Как это сделать?

Как получить или вставить точное местоположение операции перетаскивания? Я чувствую себя так это обычная операция среди перетаскиваний - наверняка я упустил важную и вопиющую деталь? Пришлось ли мне даже углубляться в JavaScript, или, может быть, есть способ сделать это только с такими атрибутами, как перетаскиваемый, сбрасываемый, contenteditable и некоторые причудливые CSS3?

Я все еще на охоте - все еще ковыряюсь - я отправлю сообщение, как только узнаю, что мне не удалось :)


Охота продолжается (редактируется после оригинального сообщения)


Фаррух опубликовал хорошее предложение - используйте:

console.log( window.getSelection().getRangeAt(0) );

Чтобы увидеть, где выбор каретки на самом деле. Я включил это в событие dragover , когда я понял, что каретка выбора явно прыгает между моим редактируемым контентом в contenteditable.

Увы, возвращаемый объект Range сообщает о смещенных индексах, которые принадлежат каретке выбора, до операции перетаскивания.

Это было доблестное усилие. Спасибо Фаррух.

Так что же здесь происходит? У меня возникает ощущение, что маленькая каретка выбора, которую я вижу, прыгает вокруг, разве это не каретка выбора вообще! Я думаю, что это самозванец!

После дальнейшего осмотра!

Оказывается, это самозванец! реальная каретка выбора остается на месте в течение всей операции перетаскивания! Вы можете увидеть маленький педераст!

Я читал MDN Drag and Drop Docs и нашел это:

Естественно, вам также может понадобиться переместить маркер вставки вокруг события перетаскивания. Вы можете использовать свойства clientX и clientY события, как и другие события мыши, чтобы определить местоположение указателя мыши.

Да, это значит, что я должен сам разобраться, основываясь на clientX и clientY ?? Используя координаты мыши, чтобы определить местоположение выделенной области? Страшно !!

Я займусь этим завтра - если только я или кто-то еще, кто читает это, не найдет разумного решения :)

60
ChaseMoskal

Dragon Drop

Я сделал нелепое количество возни. Итак, так много jsFiddling.

Это не надежное или полное решение; Я, возможно, никогда не придумаю один. Если у кого-то есть более удачные решения, я весь в ушах - я не хотел делать это таким образом, но это единственный способ, который я смог раскрыть до сих пор. Следующий jsFiddle и информация, которую я собираюсь вырвать, работали для меня в этом конкретном случае с моими конкретными версиями Firefox и Chrome на моей конкретной установке WAMP и компьютере. Не приходи ко мне плакать, когда это не работает на вашем сайте. Эта чушь с перетаскиванием явно каждый сам за себя.

jsFiddle: Погоня за Драконом Москалем

Итак, я вымотал мозги моей подруги, и она подумала, что я продолжал говорить "падение дракона", хотя на самом деле я просто говорил "перетаскивание". Он застрял, поэтому я и назвал своего маленького приятеля по JavaScript, который я создал для обработки таких ситуаций перетаскивания.

Оказывается - это немного кошмар. API перетаскивания в HTML5, даже на первый взгляд, ужасен. Затем вы почти согреваетесь, когда начинаете понимать и принимать то, как оно есть предполагается работать .. Тогда вы поймете, какой это ужасный кошмар на самом деле, когда вы узнаете, как Firefox и Chrome используют эту спецификацию по-своему, и, кажется, полностью игнорируют все ваши потребности. Вы обнаруживаете, что задаете вопросы типа: "Подождите, какой элемент сейчас перетаскивается? Как мне получить эту информацию? Как отменить эту операцию перетаскивания? Как я могу остановить обработку этого случая уникальным браузером по умолчанию?" ... Ответы на ваши вопросы: "Ты сам, ЛОЗЕР! Продолжай взламывать вещи, пока что-нибудь не заработает!".

Итак, вот как я выполнил Точное перетаскивание произвольных элементов HTML внутри, вокруг и между несколькими contenteditable. (примечание : Я не буду вдаваться в подробности с каждой деталью, для этого вам придется взглянуть на jsFiddle - я просто вычеркиваю, казалось бы, важные детали, которые я помню из опыта, поскольку у меня есть ограниченное время)

Мое решение

  • Сначала я применил CSS к draggables (fancybox) - нам нужно было user-select:none; user-drag:element; для причудливой рамки, а затем конкретно user-drag:none; для изображения внутри причудливой рамки (и любых других элементов, почему бы и нет?). К сожалению, этого было недостаточно для Firefox, который требовал, чтобы атрибут draggable="false" был явно установлен на изображении, чтобы предотвратить его перетаскивание.
  • Затем я применил атрибуты draggable="true" и dropzone="copy" к contenteditables.

Для draggables (fancyboxes) я привязываю обработчик для dragstart. Мы устанавливаем dataTransfer для копирования пустой строки HTML '' - потому что нам нужно чтобы заставить его думать, что мы собираемся перетащить HTML, но мы отменяем любое поведение по умолчанию. Иногда поведение по умолчанию каким-то образом проскальзывает, и это приводит к дублированию (как мы делаем вставку самостоятельно), так что теперь худший глюк - это вставка '' (пробел) при неудачном перетаскивании. Мы не могли полагаться на поведение по умолчанию, так как это часто не удавалось, поэтому я нашел это наиболее универсальным решением.

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');
    }
});

Для дропзон я привязываю обработчик для dragleave и drop. Обработчик перетаскивания существует только для Firefox, так как в Firefox перетаскивание будет работать ( По умолчанию Chrome запрещает вам), когда вы пытались перетащить его за пределы контенто-торгуемого объекта, поэтому он выполняет быструю проверку только relatedTarget только для Firefox. Huff.

Chrome и Firefox имеют разные способы получения объекта Range, , поэтому пришлось приложить усилия, чтобы сделать это по-разному для каждого браузера в событии удаления. Chrome строит диапазон на основе координат мыши (да, это верно) , но Firefox предоставляет это в данных события. document.execCommand('insertHTML',false,blah) оказывается, как мы справляемся с отбрасыванием. О, я забыл упомянуть - мы не можем использовать dataTransfer.getData() для Chrome, чтобы получить HTML-код нашего драгстарта - в спецификации это выглядит странной ошибкой. Firefox вызывает спецификацию на его bullcrap и в любом случае дает нам данные - но Chrome не делает, поэтому мы склоняемся назад и устанавливаем глобальный контент, и идем через ад, чтобы убить всех поведение по умолчанию ...

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();
});

Хорошо, мне это надоело. Я написал все, что хочу об этом. Я называю это днем, и могу обновить это, если думаю о чем-то важном.

Спасибо всем. //Гнаться.

39
ChaseMoskal

Поскольку я хотел увидеть это в нативном решении JS, я немного поработал над удалением всех зависимостей jQuery. Надеюсь, это может кому-то помочь.

Первая разметка

    <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>

Тогда некоторые помощники

    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();

Ниже приведен функционирующий фрагмент

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>
1
25r43q
  1. событие dragstart; dataTransfer.setData("text/html", "<div class='whatever'></div>");
  2. удаление события: 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
holistic