jQuery Sortable update output on input

Viewed 331

We're using the jQuery Sortable library for dynamic menu management similar to WordPress. And it has multi level management.

<ol id="my-nav">
    <li data-id="unique-id-here" data-label="">
        <span>My Label</span>
        <input type="text" class="label-change">
        <ol><!-- PLACEHOLDER FOR SUBMENU --></ol>
    </li>
</ol>

<textarea id="output"></textarea>

We want to update the Menu Label using the inherited input field. So we did something like below:

$('body').on('keyup change', '.label-change', function () {
    var this_menu_label_field = $(this);
    var this_field_val        = this_menu_label_field.val();
    var this_menu_nav         = this_menu_label_field.parents('li');

    // Update the text string inside the <li>
    this_menu_nav.find('span').html(this_field_val);

    // Update the data-label attribute
    this_menu_nav.attr('data-label', this_field_val).sortable('refresh');

    var serialized_data = menu_container.sortable('serialize').get();
    $('#output').val(JSON.stringify(serialized_data));
});

The code is updating the string inside the <span> inside the respective <li> and also changing the data-label. But unfortunately it's updating only the first keystroke into the #output textarea.

For example: if we type "Whatever", it might take "W" or "Wha". And no further keystrokes are updated into the #output. But at always the update inside the <span> and data-label is working fine.

We need this kind of feature critically. But how can we achieve this?

DEMO Fiddle

https://jsfiddle.net/mayeenulislam/Lsrgu0qy/38/

  • Do nothing, see the value in the <textarea>
  • Do nothing, just type in the textbox
  • Now see the value in the <textarea> again
2 Answers

I fiddled around with this and was able to make it work. Check my forked fiddle here: jsfiddle.net/qf7n89oe.

The only change I made was to replace

this_menu_nav.attr('data-label', this_field_val).sortable('refresh');

with

this_menu_nav.data('label', this_field_val).sortable('refresh');

It seems using attr() causes some caching issues so I replaced it with data() which doesn't seem to have this problem.

jQuery Sortable stores data in data object. So it does not process data-* attributes. You can use Kodos Johnson answer if you would like to use jQuery data object. The alternative is to have your own definition of serialization function. See this example:

/**
 * ---------------------------------------
 * OUR CODE STARTS HERE
 * ---------------------------------------
 */

jQuery(document).ready(function($) {
  var menu_container = $('#my-nav');
  var menu_data_field = $('#output');

  var sortable = menu_container.sortable({
    delay: 500,
    group: 'serialization',
    onDrop: function($item, container, _super) {
      var data = sortable.sortable('serialize').get();

      var jsonString = JSON.stringify(data);

      menu_data_field.val(jsonString);
      _super($item, container);
    },
    serialize: function($parent, $children, parentIsContainer) {
      let result = $parent.map(function(e) {
        let attr = {};
        $.each(this.attributes, function() {
          const name = this.name.replace("data-", "");
          attr[name] = this.value;
        });
        return attr;
      });

      if (parentIsContainer)
        return [$children]
      else if ($children[0]) {
        result[0].children = $children
      }

      delete result.subContainers
      delete result.sortable

      return result.get();
    },
  });

  $('body').on('keyup', '.label-change', function() {
    var this_menu_label_field = $(this);
    var this_field_val = this_menu_label_field.val();
    var this_menu_nav = this_menu_label_field.parent('li');

    // Update the text string inside the <li>
    this_menu_nav.find('> span').html(this_field_val);

    // Update the data-label attribute
    this_menu_nav.attr('data-label', this_field_val).sortable('refresh');

    var serialized_data = menu_container.sortable('serialize').get();
    console.log(serialized_data);
    $('#output').val(JSON.stringify(serialized_data));
  });
});
body.dragging,
body.dragging * {
  cursor: move !important;
}

input[type="text"] {
  border-color: red;
}

#my-nav {
  padding-left: 0;
  list-style-type: none;
  overflow: hidden;
}

#my-nav ol {
  padding-left: 0;
  margin-left: 20px;
  list-style-type: none;
}

#my-nav li {
  padding: 5px 10px;
  background-color: #f8f8f8;
  color: #333;
  border: 1px solid #999;
  border-radius: 3px;
  margin-bottom: 10px;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
  cursor: move;
}

#my-nav li.placeholder {
  position: relative;
  border-style: dashed;
  background-color: #ededed;
  min-height: 34px;
}

#my-nav li.placeholder:before {
  position: absolute;
}

#my-nav li.dragged {
  position: absolute;
  top: 0;
  opacity: .5;
  z-index: 2000;
  box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}

#my-nav li.dragged i.icon-move {
  color: var(--primary);
}

#my-nav li.highlight {
  background: gray;
  color: lightgray;
}

#my-nav li:first-of-type {
  margin-top: 10px;
}

#my-nav i.icon-move {
  cursor: pointer;
  color: #999;
  padding: 5px;
}

.dd {
  position: relative;
  display: block;
  margin: 0;
  padding: 0;
  max-width: 600px;
  list-style: none;
  font-size: 13px;
  line-height: 20px;
}

.dd-list {
  display: block;
  position: relative;
  margin: 0;
  padding: 0;
  list-style: none;
}

.dd-list .dd-list {
  padding-left: 30px;
}

.dd-item,
.dd-empty,
.dd-placeholder {
  display: block;
  position: relative;
  margin: 0;
  padding: 0;
  min-height: 20px;
  font-size: 13px;
  line-height: 20px;
}

.dd-handle {
  display: block;
  height: 30px;
  margin: 5px 0;
  padding: 5px 10px;
  color: #333;
  text-decoration: none;
  font-weight: bold;
  border: 1px solid #ccc;
  background: #fafafa;
  border-radius: 3px;
  box-sizing: border-box;
}

.dd-handle:hover {
  color: #2ea8e5;
  background: #fff;
}

.dd-item>button {
  position: relative;
  cursor: pointer;
  float: left;
  width: 25px;
  height: 20px;
  margin: 5px 0;
  padding: 0;
  text-indent: 100%;
  white-space: nowrap;
  overflow: hidden;
  border: 0;
  background: transparent;
  font-size: 12px;
  line-height: 1;
  text-align: center;
  font-weight: bold;
}

.dd-item>button:before {
  display: block;
  position: absolute;
  width: 100%;
  text-align: center;
  text-indent: 0;
}

.dd-item>button.dd-expand:before {
  content: '+';
}

.dd-item>button.dd-collapse:before {
  content: '-';
}

.dd-expand {
  display: none;
}

.dd-collapsed .dd-list,
.dd-collapsed .dd-collapse {
  display: none;
}

.dd-collapsed .dd-expand {
  display: block;
}

.dd-empty,
.dd-placeholder {
  margin: 5px 0;
  padding: 0;
  min-height: 30px;
  background: #f2fbff;
  border: 1px dashed #b6bcbf;
  box-sizing: border-box;
  -moz-box-sizing: border-box;
}

.dd-empty {
  border: 1px dashed #bbb;
  min-height: 100px;
  background-color: #e5e5e5;
  background-size: 60px 60px;
  background-position: 0 0, 30px 30px;
}

.dd-dragel {
  position: absolute;
  pointer-events: none;
  z-index: 9999;
}

.dd-dragel>.dd-item .dd-handle {
  margin-top: 0;
}

.dd-dragel .dd-handle {
  box-shadow: 2px 4px 6px 0 rgba(0, 0, 0, 0.1);
}

.dd-nochildren .dd-placeholder {
  display: none;
}
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-sortable/0.9.13/jquery-sortable-min.js"></script>

<ol id="my-nav">
  <li data-id="unique-id-here" data-label="My Label">
    <span>My Label</span>
    <input type="text" class="label-change form-control" placeholder="Type here to see the impact">
    <ol>
      <li data-id="unique-id-here" data-label="My Sub Label">
        <span>My Sub Label</span>
        <input type="text" class="label-change form-control" placeholder="Type here to see the impact">
        <ol>
          <!-- PLACEHOLDER FOR SUBMENU -->
        </ol>
      </li>
    </ol>
  </li>
</ol>

<h4>OUTPUT HERE</h4>
<textarea id="output" class="form-control" placeholder="">[[{"label":"My Label","id":"unique-id-here","children":[[{"label":"My Sub Label","id":"unique-id-here","children":[[]]}]]}]]</textarea>

Related