插入位置,新线和撤消在可疑块中

时间:2018-03-09 08:54:52

标签: javascript wysiwyg contenteditable

在下面的示例中,我有一个contenteditable块,我在其中实现了多个下划线算法。每行首先出现一个字母,并在最后一次出现时停止。例如,蓝线从第一个字母开始" a"并最后停止: enter image description here

用户可以输入新的字母来更新行: enter image description here enter image description here

这个例子有3个问题:

  1. 在每个输入插入符号跳转到文本开头。这是因为在每次按键时都会更新contenteditable内的整个html。我试图按Saving and Restoring caret position for contentEditable div中的建议保存和恢复插入位置。但我不确定这个解决方案适用于不同的浏览器。通常代码看起来很脏。

  2. 用户无法输入新行。在contenteditable代替\n符号<div><br/></div>已添加。

  3. 当我按 Ctrl + Z 撤消时不会发生。

  4. 我不是一般的Javascript和Web开发经验。你能帮我解决一下这些问题吗?

    在我看来,必须有一些好的解决方案。互联网上有很多WYSIWYG编辑器。他们必须以某种方式解决这些问题?

    也许有一些标准库可以解决这些问题?

    &#13;
    &#13;
    var TEXT = $('#text');
    
    var COLORS = [
      '#1f77b4',
      '#ff7f0e',
      '#2ca02c',
      '#d62728',
      '#9467bd',
      '#e377c2',
      '#bcbd22',
      '#17becf',
    ];
    
    
    function makeSpan(start, stop, type, level) {
      return {
        start: start,
        stop: stop,
        type: type,
        level: level
      }
    }
    
    
    function parse(text) {
      var mins = {};
      var maxes = {};
      for (var index = 0; index < text.length; index++) {
        var char = text[index];
        if (char.match(/\s/)) {
          continue;
        }
        var min = mins[char];
        if (min == undefined) {
          mins[char] = index;
        }
        var max = maxes[char];
        if ((max == undefined) || (index > max)) {
          maxes[char] = index;
        }
      }
      var spans = [];
      for (var char in mins) {
        var min = mins[char];
        var max = maxes[char];
        if (max > min) {
          var span = makeSpan(min, max + 1, char);
          spans.push(span);
        }
      }
      return spans;
    }
    
    
    function querySpans(spans, value) {
      var results = [];
      spans.forEach(function(span) {
        if ((span.start <= value) && (value < span.stop)) {
          results.push(span)
        }
      });
      return results;
    }
    
    
    function getMaxLevel(spans) {
      var level = -1;
      spans.forEach(function(span) {
        if (level < span.level) {
          level = span.level;
        }
      });
      return level;
    }
    
    
    function levelSpans(spans) {
      var results = [];
      spans.forEach(function(span) {
        var found = querySpans(results, span.start);
        var level = getMaxLevel(found);
        span.level = level + 1;
        results.push(span);
      });
      return results;
    }
    
    
    function sortSpans(spans) {
      spans.sort(function(a, b) {
        return ((a.start - b.start) ||
          (a.stop - b.stop) ||
          a.type.localeCompare(b.type));
      })
      return spans;
    }
    
    
    function getBoundValues(spans) {
      var values = [];
      spans.forEach(function(span) {
        values.push(span.start);
        values.push(span.stop);
      });
      return values;
    }
    
    
    function uniqueValues(values) {
      var set = {};
      values.forEach(function(value) {
        set[value] = value;
      });
      var values = [];
      for (var key in set) {
        values.push(set[key]);
      }
      values.sort(function(a, b) {
        return a - b;
      });
      return values;
    }
    
    
    function chunkSpan(span, bounds) {
      var results = [];
      var previous = span.start;
      bounds.forEach(function(bound) {
        if ((span.start < bound) && (bound < span.stop)) {
          results.push(makeSpan(
            previous, bound,
            span.type, span.level
          ));
          previous = bound
        }
      });
      results.push(makeSpan(
        previous, span.stop,
        span.type, span.level
      ));
      return results;
    }
    
    
    function chunkSpans(spans) {
      var bounds = getBoundValues(spans);
      bounds = uniqueValues(bounds);
    
      var results = [];
      spans.forEach(function(span) {
        var chunks = chunkSpan(span, bounds);
        chunks.forEach(function(chunk) {
          results.push(chunk);
        });
      });
      return results;
    }
    
    
    function makeGroup(start, stop) {
      return {
        start: start,
        stop: stop,
        items: []
      }
    }
    
    
    function groupSpans(spans) {
      var previous = undefined;
      var results = [];
      spans.forEach(function(span) {
        if (previous == undefined) {
          previous = makeGroup(span.start, span.stop);
        }
        if (previous.start == span.start) {
          previous.items.push(span);
        } else {
          results.push(previous)
          previous = makeGroup(span.start, span.stop);
          previous.items.push(span);
        }
      });
      if (previous != undefined) {
        results.push(previous)
      }
      return results;
    }
    
    
    function formatTag(span, types) {
      var size = 2;
      var padding = 1 + span.level * (size + 1);
      var index = types.indexOf(span.type);
      color = COLORS[index % COLORS.length];
      return {
        open: ('<span style="' +
          'border-bottom: ' + size + 'px solid; ' +
          'padding-bottom: ' + padding + 'px; ' +
          'border-color: ' + color + '">'),
        close: '</span>'
      }
    
    }
    
    function formatSpans(text, groups, types) {
      var html = '';
      var previous = 0;
      groups.forEach(function(group) {
        html += text.slice(previous, group.start);
        var tags = [];
        group.items.forEach(function(span) {
          tags.push(formatTag(span, types));
        });
        tags.forEach(function(tag) {
          html += tag.open;
        });
        html += text.slice(group.start, group.stop);
        tags.forEach(function(tag) {
          html += tag.close;
        });
        previous = group.stop;
      });
      html += text.slice(previous, text.length);
      return html;
    }
    
    
    function getSpanTypes(spans) {
      var results = [];
      spans.forEach(function(span) {
        if (span.type != undefined) {
          results.push(span.type)
        }
      });
      return results;
    }
    
    
    function updateSpans(text, spans) {
      types = getSpanTypes(spans);
      types = uniqueValues(types);
    
      spans = sortSpans(spans);
      spans = levelSpans(spans);
      spans = chunkSpans(spans);
      spans = sortSpans(spans);
      groups = groupSpans(spans);
    
      html = formatSpans(text, groups, types);
      TEXT.html(html);
    }
    
    
    function update() {
      var text = TEXT.text();
      var spans = parse(text);
      updateSpans(text, spans);
    }
    
    
    TEXT.on('input propertychange', update);
    TEXT.focus();
    update();
    &#13;
    #text {
      border: 1px solid silver;
      padding: 1em;
      line-height: 2em;
    }
    &#13;
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    
    <div contenteditable="true" id="text">
      a d a b a a a b c c c f d
    </div>
    &#13;
    &#13;
    &#13;

1 个答案:

答案 0 :(得分:1)

WYSIWYG编辑器通常让用户在一个contenteditable div上进行编辑,并在另一个不可编辑的div中显示输出,巧妙地显示一个。因此,它们克服了许多复杂性,例如跟踪插入符号位置和撤消重做序列。

我添加#textresult以显示输出,并添加.wrapper以包含div和问题1&amp;这就解决了这个问题。

<div class="wrapper">
  <div contenteditable="true" id="text">
    a d a b a a a b c c c f d
  </div>
  <div id="textresult"></div>
</div>

要解决问题2,您不应使用jQuery.text,请使用原生HTMLELement.innerText获取包含新换行符的内容,并在跨度格式化后将其替换为<br>

&#13;
&#13;
var TEXT = $('#text');
var TEXTRESULT = $('#textresult');

var COLORS = [
  '#1f77b4',
  '#ff7f0e',
  '#2ca02c',
  '#d62728',
  '#9467bd',
  '#e377c2',
  '#bcbd22',
  '#17becf',
];


function makeSpan(start, stop, type, level) {
  return {
    start: start,
    stop: stop,
    type: type,
    level: level
  }
}


function parse(text) {
  var mins = {};
  var maxes = {};
  for (var index = 0; index < text.length; index++) {
    var char = text[index];
    if (char.match(/\s/)) {
      continue;
    }
    var min = mins[char];
    if (min == undefined) {
      mins[char] = index;
    }
    var max = maxes[char];
    if ((max == undefined) || (index > max)) {
      maxes[char] = index;
    }
  }
  var spans = [];
  for (var char in mins) {
    var min = mins[char];
    var max = maxes[char];
    if (max > min) {
      var span = makeSpan(min, max + 1, char);
      spans.push(span);
    }
  }
  return spans;
}


function querySpans(spans, value) {
  var results = [];
  spans.forEach(function(span) {
    if ((span.start <= value) && (value < span.stop)) {
      results.push(span)
    }
  });
  return results;
}


function getMaxLevel(spans) {
  var level = -1;
  spans.forEach(function(span) {
    if (level < span.level) {
      level = span.level;
    }
  });
  return level;
}


function levelSpans(spans) {
  var results = [];
  spans.forEach(function(span) {
    var found = querySpans(results, span.start);
    var level = getMaxLevel(found);
    span.level = level + 1;
    results.push(span);
  });
  return results;
}


function sortSpans(spans) {
  spans.sort(function(a, b) {
    return ((a.start - b.start) ||
      (a.stop - b.stop) ||
      a.type.localeCompare(b.type));
  })
  return spans;
}


function getBoundValues(spans) {
  var values = [];
  spans.forEach(function(span) {
    values.push(span.start);
    values.push(span.stop);
  });
  return values;
}


function uniqueValues(values) {
  var set = {};
  values.forEach(function(value) {
    set[value] = value;
  });
  var values = [];
  for (var key in set) {
    values.push(set[key]);
  }
  values.sort(function(a, b) {
    return a - b;
  });
  return values;
}


function chunkSpan(span, bounds) {
  var results = [];
  var previous = span.start;
  bounds.forEach(function(bound) {
    if ((span.start < bound) && (bound < span.stop)) {
      results.push(makeSpan(
        previous, bound,
        span.type, span.level
      ));
      previous = bound
    }
  });
  results.push(makeSpan(
    previous, span.stop,
    span.type, span.level
  ));
  return results;
}


function chunkSpans(spans) {
  var bounds = getBoundValues(spans);
  bounds = uniqueValues(bounds);

  var results = [];
  spans.forEach(function(span) {
    var chunks = chunkSpan(span, bounds);
    chunks.forEach(function(chunk) {
      results.push(chunk);
    });
  });
  return results;
}


function makeGroup(start, stop) {
  return {
    start: start,
    stop: stop,
    items: []
  }
}


function groupSpans(spans) {
  var previous = undefined;
  var results = [];
  spans.forEach(function(span) {
    if (previous == undefined) {
      previous = makeGroup(span.start, span.stop);
    }
    if (previous.start == span.start) {
      previous.items.push(span);
    } else {
      results.push(previous)
      previous = makeGroup(span.start, span.stop);
      previous.items.push(span);
    }
  });
  if (previous != undefined) {
    results.push(previous)
  }
  return results;
}


function formatTag(span, types) {
  var size = 2;
  var padding = 1 + span.level * (size + 1);
  var index = types.indexOf(span.type);
  color = COLORS[index % COLORS.length];
  return {
    open: ('<span style="' +
      'border-bottom: ' + size + 'px solid; ' +
      'padding-bottom: ' + padding + 'px; ' +
      'border-color: ' + color + '">'),
    close: '</span>'
  }

}

function formatSpans(text, groups, types) {
  var html = '';
  var previous = 0;
  groups.forEach(function(group) {
    html += text.slice(previous, group.start);
    var tags = [];
    group.items.forEach(function(span) {
      tags.push(formatTag(span, types));
    });
    tags.forEach(function(tag) {
      html += tag.open;
    });
    html += text.slice(group.start, group.stop);
    tags.forEach(function(tag) {
      html += tag.close;
    });
    previous = group.stop;
  });
  html += text.slice(previous, text.length);
  return html;
}


function getSpanTypes(spans) {
  var results = [];
  spans.forEach(function(span) {
    if (span.type != undefined) {
      results.push(span.type)
    }
  });
  return results;
}


function updateSpans(text, spans) {
  types = getSpanTypes(spans);
  types = uniqueValues(types);

  spans = sortSpans(spans);
  spans = levelSpans(spans);
  spans = chunkSpans(spans);
  spans = sortSpans(spans);
  groups = groupSpans(spans);

  html = formatSpans(text, groups, types);
  TEXTRESULT.html(html.replace(/\n/g,'<br>'));
}


function update() {
  var text = TEXT[0].innerText;
  var spans = parse(text);
  updateSpans(text, spans);
}


TEXT.on('input propertychange', update);
TEXT.focus();
update();
&#13;
.wrapper{
  position: relative;
}
#text {
  border: 1px solid silver;
  padding: 1em;
  line-height: 2em;
}
#textresult {
  border: 1px solid transparent;
  padding: 1em;
  line-height: 2em;
  color: transparent;
  position: absolute;
  top: 0;
  z-index: -1;
}
&#13;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
  <div contenteditable="true" id="text">
    a d a b a a a b c c c f d
  </div>
  <div id="textresult"></div>
</div>
&#13;
&#13;
&#13;