Search code examples
javascriptjqueryjquery-uidrag-and-dropdraggable

how drag and drop component as first word


I'm using jQuery UI draggable component to add to content editable

.This code works to find and I have a little issue. The problem is I cannot drag and drop draggable component as first word, How can I solve this? Please help me to solve this.

Note : the current functionlity cannot be break.

My code as follows:

$(function() {
  function textWrapper(str, sp, btn) {
    if (sp == undefined) {
      sp = [0, 0];
    }
    var txt = "";
    if (btn) {
      txt = "<span class='w b'>" + str + "</span>";
    } else {
      txt = "<span class='w'>" + str + "</span>";
    }

    if (sp[0]) {
      txt = "&nbsp;" + txt;
    }

    if (sp[1]) {
      txt = txt + "&nbsp;";
    }

    return txt;
  }

  function chunkWords(p) {
    var words = p.split(" ");
    words[0] = textWrapper(words[0], [0, 1]);
    var i;
    for (i = 1; i < words.length; i++) {
      var re = /\[.+\]/;
      if (re.test(words[i])) {
        var b = makeTextBox(words[i].slice(1, -1));
        words[i] = "&nbsp;" + b.prop("outerHTML") + "&nbsp;";
      } else {
        if (words[0].indexOf(".")) {
          words[i] = textWrapper(words[i], [1, 0]);
        } else {
          words[i] = textWrapper(words[i], [1, 1]);
        }
      }
    }
    return words.join("");
  }

function unChunkWords(tObj) {
var words = "";
$(tObj).contents().each(function (i, el) {
  if ($(el).hasClass("b")) {
    words += "[" + $(el).text() + "]";
  } else {
    words += $(el).text();
  }
});
return words.replace(/\s+/g, " ").trim();
  }

  function makeBtn(tObj) {
    var btn = $("<span>", {
      class: "ui-icon ui-icon-close"
    }).appendTo(tObj);
  }

  function makeTextBox(txt) {
    var sp = $("<span>", {
      class: "w b"
    }).html(txt);
    makeBtn(sp);
    return sp;
  }

  function makeDropText(obj) {
    return obj.droppable({
      drop: function(e, ui) {
        var txt = ui.draggable.text();
        var newSpan = textWrapper(txt, [1, 0], 1);
        $(this).after(newSpan);
        makeBtn($(this).next("span.w"));
        makeDropText($(this).next("span.w"));
        $("span.w.ui-state-highlight").removeClass("ui-state-highlight");
      },
      over: function(e, ui) {
        $(this).add($(this).next("span.w")).addClass("ui-state-highlight");
      },
      out: function() {
        $(this).add($(this).next("span.w")).removeClass("ui-state-highlight");
      }
    });
  }

  $("p.given").html(chunkWords($("p.given").text()));

  $("p.given").on("click", ".b > .ui-icon", function() {
    $(this).parent().remove();
  });

  $("p.given").blur(function() {
    var w = unChunkWords($(this));
    console.log(w);
    $(this).html(chunkWords(w));
    makeDropText($("p.given span.w"));
  });

  $("span.given").draggable({
    helper: "clone",
    revert: "invalid"
  });

  makeDropText($("p.given span.w"));
});
p.given {
  display: flex;
  flex-wrap: wrap;
}

p.given span.w span.ui-icon {
  cursor: pointer;
}

div.blanks {
  display: inline-block;
  min-width: 50px;
  border-bottom: 2px solid #000000;
  color: #000000;
}

div.blanks.ui-droppable-active {
  min-height: 20px;
}

span.answers>b {
  border-bottom: 2px solid #000000;
}

span.given {
  margin: 5px;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div class="row">
  <p class="given" contenteditable="true">Lorem Ipsum is simply dummy text of the printing and typesetting industry. [Lorem] Ipsum has been the industry's standard dummy text ever since the 1500s, Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
</div>

<div class="divider"></div>
<div class="section">
  <section>
    <div class="card blue-grey ">
      <div class="card-content white-text">
        <div class="row">
          <div class="col s12">
            <span class="given btn-flat white-text red lighten-1" rel="1">the Santee, thDakota</span>
            <span class="given btn-flat white-text red lighten-1" rel="2">America</span>
            <span class="given btn-flat white-text red lighten-1" rel="3">Qatar</span>
            <span class="given btn-flat white-text red lighten-1" rel="4">Philippines</span>
          </div>
        </div>
      </div>
    </div>
  </section>
</div>


Solution

  • You could do the following:

    1. Determine whether to insert before the first element or not

    While the dragging is ongoing:

    • Monitor when the the draggable element is hovering over the first word in the text.
    • If so, check if it is hovering more over the left side or the right side of the word
    • If more on the left side, then remove the highlight of the second word
    • If more to the right, then restore the highlight of the second word

    This visual indication will thus indicate whether the word will be inserted after the first word (2 are highlighted) or before it (1 is highlighted).

    2. Perform the drop accordingly

    When the user drops the element:

    • Check whether the first word is highlighted, and not any other.
    • If so, insert the new element before the current word
    • If not, insert the new element after the current word (like it was already done)

    So, you need two changes in your code to make this work:

    Add the following code for implementing the first part:

    document.addEventListener("mousemove", function() {
        var $draggable = $(".ui-draggable-dragging");
        if (!$draggable.length) return; // nothing is being dragged
        var $highlighted = $(".ui-state-highlight");
        if (!$highlighted.length || $($highlighted).index() > 0) return; // first word is not highlighted
        // Get center x coordinate of the item that is being dragged
        var dragX = $draggable.offset().left + $draggable.width() / 2;
        // Get center x coordinate of the first word in the paragraph
        var firstX = $highlighted.offset().left + $highlighted.width() / 2;
        // If draggable is more on the left side of the first word, then only the first word should be highlighted
        if ((dragX < firstX) === ($highlighted.length < 2)) return; // Situation is as it should be
        // Toggle the highlight on the second word of the paragraph
        $highlighted.first().next("span.w").toggleClass("ui-state-highlight");
    });
    

    Modify the drop handler for implementing the second part:

    drop: function(e, ui) {
        var txt = ui.draggable.text();
        // Use proper jQuery to create a new span element
        var newSpan = $("<span>").addClass('w b').text(txt);
        // Determine if the element is being dropped on the first word, and only that one
        if (!$(".ui-state-highlight").last().index()) {
            $(this).before(newSpan, "&nbsp;"); // ...then prepend
        } else {
            $(this).after("&nbsp;", newSpan); // normal case
        }
        makeBtn(newSpan);
        makeDropText(newSpan);
        $("span.w.ui-state-highlight").removeClass("ui-state-highlight");
    },
    

    Fix in chunkWords

    For some reason you had hard-coded in chunkWords that the first word (at index 0) could not be a dropped item -- it is always interpreted as plain text (marked as bold). This is a bug in your current code also, and you can reproduce it as follows:

    • drop a word between the first two words
    • edit the text and remove the first word so that the dropped word becomes the first word
    • Exit the editable area
    • The first word now is no longer a dropped element, but a word with "button" markup (square brackets).

    You wrote that existing behaviour should not break, but from comments I understand you did not expect this behaviour.

    Another issue is the following condition in that function:

    if (words[0].indexOf(".")) {
    

    ...think about that: why would a point in the first word influence the spacing of another word in your phrase? Surely you intended to do:

    if (words[i].indexOf(".")) {
    

    Anyway, I would suggest to not use textWrapper at all, and add the spaces in your final join. Not before.

    Fixing those two issues, that chunkWords should finally look like this:

      function chunkWords(p) {
        var words = p.split(" "), b;
        for (var i = 0; i < words.length; i++) {
          if (/\[.+\]/.test(words[i])) {
            b = makeTextBox(words[i].slice(1, -1));
          } else {
            b = $("<span>").addClass("w").text(words[i]);
          }
          // do not pad the value with "&nbsp;" at this moment:
          words[i] = b.prop("outerHTML");
        }
        return words.join("&nbsp;"); // add the spaces here
      }
    

    As with these changes textWrapper is no longer used, you can remove that function.

    See the applied changes in this fiddle