13

I have got contentEditable div which each letter have own element, like:

<div contenteditable="true">
    <p id="p1">
        <span id="s1">S</span>
        <span id="s2">o</span>
        <span id="s3">m</span>
        <span id="s4">e</span>
        <span id="s5">&nbsp;</span>
        <span id="s6">t</span>
        <span id="s7">e</span>
        <span id="s8">x</span>
        <span id="s9">t</span>
    </p>
</div>

And I am trying to justify some longer text using text-align: justify, but this don't work. That is strange, because text-align: center and text-align: right works.

After that I am trying to do that using a script which adds margin-right to each space, but when I am writing new text into paragraph it crashes.

How can I do that (and save id and other attributes in each element) using JavaScript and/or JQuery?

Paolo Forgia
  • 6,572
  • 8
  • 46
  • 58
Kacper G.
  • 662
  • 8
  • 30
  • What are you trying to achieve? Are the letters supposed to fill the entire width? – Brett DeWoody Sep 20 '17 at 11:00
  • 1
    Fiddle would be of a great help... please provide one so that we can help – George Sharvadze Sep 20 '17 at 11:35
  • I am guessing you want the text to behave as normal text even if you ahve all your span. So when you do justify with a long text you want the sentences to break between words not break the words apart as i does now? – JohanSellberg Sep 20 '17 at 11:40
  • 2
    The question needs to be clarified. As is it's hard to know what the question or goal is. – Brett DeWoody Sep 20 '17 at 16:34
  • Why has OP set a bounty and then ignored every response, there are multitudes of good answers and on top of that we don't even have all of the required information – Shardj Sep 25 '17 at 15:05
  • To keep the `` elements together those `textContent` is a letter, obviously you need to place them into another containing element in the first place. You may start by creating elements like `` and move the adjacent characters under it. – Redu Sep 26 '17 at 16:41

7 Answers7

4

Here is an example of a JS + jQuery way to simulate the justification for characters wrapped in spans inside a contenteditable element. It's just a quick proof of concept, tested in Chrome only, and not thoroughly. Functionalities like delete / undo / copy / paste, etc. were not added to this example.

var selectors = {
  'wrapper': '#editable',
  'paragraphs': '#editable > p',
  'spans': '#editable > p > span'
}

function get_cursor_position(element) {
  var caretOffset = 0;
  var doc = element.ownerDocument || element.document;
  var win = doc.defaultView || doc.parentWindow;
  var sel;
  if (typeof win.getSelection != "undefined") {
    sel = win.getSelection();
    if (sel.rangeCount > 0) {
      var range = win.getSelection().getRangeAt(0);
      var preCaretRange = range.cloneRange();
      preCaretRange.selectNodeContents(element);
      preCaretRange.setEnd(range.endContainer, range.endOffset);
      caretOffset = preCaretRange.toString().length;
    }
  } else if ((sel = doc.selection) && sel.type != "Control") {
    var textRange = sel.createRange();
    var preCaretTextRange = doc.body.createTextRange();
    preCaretTextRange.moveToElementText(element);
    preCaretTextRange.setEndPoint("EndToEnd", textRange);
    caretOffset = preCaretTextRange.text.length;
  }
  return caretOffset;
}


function set_cursor_position(pos) {
  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', pos);
    sel.select();
  } else {
    sel = window.getSelection();
    sel.collapse($(selectors['spans'])[pos].firstChild, 0);
  }
}

function set_cursor_position_to_element(el) {
  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', pos);
    sel.select();
  } else {
    sel = window.getSelection();
    sel.collapse(el, 0);
  }
}


function justify(wrapper_selector, children_selector) {
  var line_width = 0,
    first_line_char = 0,
    wrapper = $(wrapper_selector),
    wrapper_width = wrapper.width(),
    children = $(children_selector),
    position_of_last_space_found,
    filled_line_width_at_last_space_found;

  // refresh
  children.removeAttr("padding-right").removeClass("spaced first-line-char last-line-space");

  for (var space_positions = [], l = children.length, child_i = 0; child_i < l; child_i++) {
    child_e = children.eq(child_i);
    line_width += $(child_e).width();
    first_line_char += 1;
    if (/\s/g.test($(child_e).text())) {
      space_positions.push(child_i);
      position_of_last_space_found = child_i;
      filled_line_width_at_last_space_found = line_width - child_e.width();
    }
    if (line_width >= wrapper_width) {
      remaining_space = wrapper_width - filled_line_width_at_last_space_found;
      line_chars_extra_margin = remaining_space / (space_positions.length - 1);
      for (margin_i = 0; margin_i < space_positions.length; margin_i++) {
        children.eq(space_positions[margin_i]).addClass("spaced").css("padding-right", Math.floor(line_chars_extra_margin * 10) / 10);
      }
      children.eq(position_of_last_space_found + 1).addClass("first-line-char");
      children.eq(position_of_last_space_found).addClass("last-line-space");
      line_width = 0;
      child_i = position_of_last_space_found;
      first_line_char = 0;
      space_positions = [];
    }
  }
}

function insert_char(html) {
  var sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.getRangeAt && sel.rangeCount) {
      range = sel.getRangeAt(0);
      range.deleteContents();
      var el = document.createElement("div");
      el.innerHTML = html;
      var frag = document.createDocumentFragment(),
        node, lastNode;
      while ((node = el.firstChild)) {
        lastNode = frag.appendChild(node);
      }
      var firstNode = frag.firstChild;
      pos = $(range.commonAncestorContainer).parent('span').index() + range.startOffset;
      $(selectors['spans']).eq(pos).before(frag);
      set_cursor_position(pos + 1, $(selectors['paragraphs']));
    }
  }
}

function get_span_at(x, y) {
  var $elements = $(selectors['spans']).map(function() {
    var $this = $(this);
    var offset = $this.offset();
    var l = offset.left;
    var t = offset.top;
    var h = $this.outerHeight(true);
    var w = $this.outerWidth(true);

    var maxx = l + w;
    var maxy = t + h;

    return (y <= maxy && y >= t) && (x <= maxx && x >= l) ? $this : null;
  });

  return $elements;
}

function init_demo() {
  var next_pos;

  // Copy the text from div.reference inside div.editable 
  // (only for the purpose of this example)
  var characters = $('div.reference p').text().trim().replace(/ /g, '\u00a0');
  for (var x = 0; x < characters.length; x++) {
    var c = characters.charAt(x);
    // wrap each character in a span
    $(selectors['paragraphs']).append("<span>" + c + "</span");
  }

  // initial justification
  justify(selectors['wrapper'], selectors['spans']);

  // re-justify on window resize
  $(window).resize(function() {
    clearTimeout(window.resizedFinished);
    window.resizedFinished = setTimeout(function() {
      justify(selectors['wrapper'], selectors['spans']);
    }, 20);
  });

  // Improve navigation with arrow keys
  $(selectors['wrapper']).on('keydown', function(e) {
    switch (e.which) {
      case 37: // left
        next_pos = get_cursor_position($(selectors['spans'])[0]) - 1;
        set_cursor_position(next_pos, $(selectors['paragraphs']));
        break;

      case 38: // up
        curr_pos = get_cursor_position($(selectors['spans'])[0]);
        curr_span = $(selectors['spans']).eq(curr_pos);
        curr_span_y = curr_span.position().top;
        curr_span_x = curr_span.position().left;
        next_span_y = curr_span_y - 1;
        next_span_x = curr_span_x + 1;
        next_span = get_span_at(curr_span_x, next_span_y);
        if (next_span[0]) {
          set_cursor_position_to_element(next_span[0][0]);
        }
        break;

      case 39: // right
        next_pos = get_cursor_position($(selectors['spans'])[0]);
        set_cursor_position(next_pos, $(selectors['paragraphs']));
        break;

      case 40: // down
        curr_pos = get_cursor_position($(selectors['spans'])[0]);
        curr_span = $(selectors['spans']).eq(curr_pos);
        curr_span_y = curr_span.position().top;
        curr_span_x = curr_span.position().left;
        curr_span_h = curr_span.outerHeight(true);
        next_span_y = curr_span_y + curr_span_h + 1;
        next_span_x = curr_span_x + 1;
        next_span = get_span_at(curr_span_x, next_span_y);
        if (next_span[0]) {
          set_cursor_position_to_element(next_span[0][0]);
        }
        break;
    }
  });

  // re-justify on character insertion
  $(selectors['wrapper']).on('keypress', function(e) {
    new_char = String.fromCharCode(e.which).replace(/ /g, '\u00a0');
    // Wrap new characters in spans
    new_el = '<span>' + new_char + '</span>';
    insert_char(new_el);
    justify(selectors['wrapper'], selectors['spans']);
    e.preventDefault();
  });

}

init_demo();
div.col {
  width: 50%;
  overflow: hidden;
  font-size: 1.2em;
  float: left;
  box-sizing: border-box;
  padding: 20px;
}

p {
  overflow: hidden;
}

div.reference p {
  text-align: justify;
}

div span {
  display: block;
  float: left;
}

.first-line-char {
  content: ' ';
  display: block;
  clear: left;
}

.last-line-space {
  display: none;
}
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<div class="col col1">
  <h2>reference</h2>
  <h4>This is just a reference. Edit the right column.</h4>
  <div class="reference">
    <p>
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Excepturi provident, nemo incidunt voluptate officia, ipsa nulla itaque laudantium aperiam cupiditate vero, nesciunt consequuntur, facilis aliquam enim quis ad. Fugiat, magni.
    </p>
  </div>
</div>
<div class="col col2">
  <h2>editable</h2>
  <h4>Click the paragraph text below to start to edit.</h4>
  <div contenteditable="true" class="editable" id="editable">
    <p>
      <!-- Text will be copied from the reference div -->
    </p>
  </div>
</div>

Demo.

Ivan Chaer
  • 6,980
  • 1
  • 38
  • 48
  • 1
    This is extremely buggy when using the arrow keys, backspace, delete or when copy-pasting. The lack of that functionality renders this solution unusable as it is. – Hubert Grzeskowiak Sep 27 '17 at 09:58
  • The OP was struggling with justifying text where each character was wrapped in a span. I sought to cover this problem by following a similar approach as he suggested (adding margin-right to each space). Additionally, I sought to cover the problem exposed in the post: "when I am writing new text into paragraph it crashes". Hopefully the basic principle of combining the Range interface with HTML elements was covered in my minimalistic example, and will allow the OP to craft his way through solving his problem, and even perhaps adding delete, undo and formatting features if that's what he needs. – Ivan Chaer Sep 27 '17 at 12:50
0

You can add one fake word at the end and make its width:100% and display: inline-block. Like this: (change width of parent according to your requirement)

.justified {
  width: 300px;
  text-align: justify;
}

.justified .fake-word {
  width: 100%;
  display: inline-block;
  height: 0.1em;
}
<div contenteditable="true">
  <p id="p1" class="justified">
    <span id="s1">S</span>
    <span id="s2">o</span>
    <span id="s3">m</span>
    <span id="s4">e</span>
    <span id="s5">&nbsp;</span>
    <span id="s6">t</span>
    <span id="s7">e</span>
    <span id="s8">x</span>
    <span id="s9">t</span>
    <span class="fake-word"></span>
  </p>
</div>

EDIT 1:

You can remove all span tags if you don't want space in between characters:

<div contenteditable="true">
      <p id="p1" class="justified">
         Some Text
        <span class="fake-word"></span>
      </p>
    </div>

EDIT 2: Also, you don't have to add .fake-word, there is another way of doing this by using pseudo-class:

 .justified:after {
      content: "";
      display: inline-block;
      width: 100%;
 }
Navjot Ahuja
  • 1,141
  • 7
  • 10
  • Yes, Thanks. But in Mozilla Firefox each letter is separated. I want only spaces to be separated – Kacper G. Sep 20 '17 at 18:04
  • You can remove all the span tags if you don't want space in between the characters. – Navjot Ahuja Sep 20 '17 at 20:16
  • And if you don't want to remove all span tags help us understand what you use them for and we could maybe tackle that part of the problem. – JohanSellberg Sep 21 '17 at 06:41
  • When you have span tag, they try to take equal space in the whole line making the chars distanced. text-align justify will work if the text occupies the whole space in the width, that's why we added that fake line occupying the whole space. – Navjot Ahuja Sep 21 '17 at 07:59
  • Justify only works without all the spans otherwise multiline sentances will break between characters not between words as it should so no fake-word does not help the justify problem. – JohanSellberg Sep 22 '17 at 07:33
0

For this problem, I think the flexbox approach is the best to take. Add an id to your <div> or give it style so that it take the width you want it to take, for example :

<div contenteditable="true" id="container">
    <p id="p1">
        <span id="s1">S</span>
        <span id="s2">o</span>
        <span id="s3">m</span>
        <span id="s4">e</span>
        <span id="s5">&nbsp;</span>
        <span id="s6">t</span>
        <span id="s7">e</span>
        <span id="s8">x</span>
        <span id="s9">t</span>
    </p>
</div>

Then in your css, add the following :

#container {
    width: 400px;
}

#p1 {
    width: 100%;
    display: flex;
    justify-content: space-between;
}
M0nst3R
  • 5,186
  • 1
  • 23
  • 36
  • Looks like the best suggestion so far. But problem arise with linebreaks and so right? Does he not want normal linebreaks between words (that is justify alignment to me) – JohanSellberg Sep 20 '17 at 11:47
  • I think linebreaks can simply be achieved by providing one `

    ` per line. I think that is a fair compromise for the desired behavior.

    – M0nst3R Sep 20 '17 at 11:53
  • Ye I don't think you can have fully justified long multiline text with that setup of spans he wants. I think its easier to think about why all the spans are needed and if the span problem can be solved in another way. – JohanSellberg Sep 20 '17 at 11:57
  • Well it depends on his intent, maybe it's not about justifying contents a la Google Docs. – M0nst3R Sep 20 '17 at 12:03
0

The harsh answer is that it is not easy to achieve what you are looking for if you want normal justify behavior but keeping all the spans.

But if you want to attempt such a thing you need to make your own justify script. To do this you need to loop through all the spans and when you enter a new line (check the offset of the span from top) you need to go back to the last space and then try to justify all the characters previously iterated. This can be done by encapsulating them in a new span or div and justify inside that (encapsulating div needs to be 100% width). Either just using css or by changing with of the space spans to have only the spans expand.

SO question to answer how to check the offset of the span: How to select nth line of text (CSS/JS)

I am sorry but without adding more spans/divs or removing spans there is no way to justify this content correctly what I can see.

JohanSellberg
  • 2,423
  • 1
  • 21
  • 28
0

What you have IS working but its just not showing with the text you have for a couple of reasons.

First reason is that that text is too short. You need enough to make the text wrap to show the justification.

And second reason is that by putting the span tags in new lines is adding a space between each one. Try putting it all on the same line

Like this

<div contenteditable="true">
    <p id="p1">
      <span id="s1">S</span><span id="s2">o</span><span id="s3">m</span><span id="s4">e</span><span id="s5">&nbsp;</span><span id="s6">t</span><span id="s7">e</span><span id="s8">x</span><span id="s9">t</span>
    </p>
</div>

Here is jsfiddle example working

ZeWebDev
  • 194
  • 8
  • Your example is held together with the spaces in your html to give places for the content to wrap. For this solution to be viable he have to find the good breakpoints and add the spaces in his html. We need more information around his code for that. – JohanSellberg Sep 25 '17 at 14:23
  • If you're talking about the   that was just an error when copy pasting. It works with regular spaces. And ive updated the jsfiddle. As for finding good breakpoints im not sure what you mean. The div will break the words automatically. – ZeWebDev Sep 25 '17 at 14:36
  • I mean the spaces outside of the span tags that you had in first fiddle. But yes replacing the   with a normal " " is also working fine. But without any normal " " anywhere in it it wont work. – JohanSellberg Sep 25 '17 at 14:49
  • Well `text: justify` needs spaces to have any effect. So theres not really any use in having it if you don't have spaces in your text. Also of course there will be space between words. It doesnt matter if there are spans or just plain text. And the `contenteditable` will add this spaces automatically as you write. – ZeWebDev Sep 25 '17 at 14:53
0

You can achive this by wrapping the words in an span tag with: display: inline-block;. example: https://codepen.io/MaxViewUp/pen/XeMJmX?editors=1100

<div id="d1" contenteditable="true">
 <p id="p1">
  <span class="word">
    <span id="s1">S</span>
    <span id="s2">o</span>
    <span id="s3">m</span>
    <span id="s4">e</span>
  </span>
  <span id="s5">&nbsp;</span>
  <span class="word">
    <span id="s1">S</span>
    <span id="s2">o</span>
    <span id="s3">m</span>
    <span id="s4">e</span>
  </span>
 </p>
</div>
Maxwell s.c
  • 1,583
  • 15
  • 29
-3

use text-align: justify with display:flex(mandatory at times) this should work.

karthik
  • 1,100
  • 9
  • 21