Reputation: 1691
In example below I have a contenteditable
block where I implemented multiple underline algorithm. Every line starts at first appearance of a letter and stops at last appearance. For example blue line starts at first letter "a" and stops at last:
User can enter new letters to update lines:
There are 3 problems with this examples:
On every input carets jumps to start of text. It happens because on every key press I update entire html inside contenteditable
. I tried to save and restore caret position as suggested in Saving and Restoring caret position for contentEditable div. But I am not sure this solution works across different browsers. And in general code looks dirty.
User can not enter new line. In contenteditable
instead in of \n
symbols <div><br/></div>
is added.
When I press Ctrl+Z undo does not happen.
I am not experiences in Javascript and Web-development in general. Could you please help me fix these problems?
It seems to me that there must be some good solution. There are a lot of WYSIWYG editors on internet. They must somehow solved these issues?
Maybe there are some standard libraries to solved these issues?
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();
#text {
border: 1px solid silver;
padding: 1em;
line-height: 2em;
}
<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>
Upvotes: 4
Views: 1981
Reputation: 17546
WYSIWYG Editors generally let the user edit on one contenteditable
div and show the output in another non-editable div showing one over other cleverly. Thus they overcome lot of complexities like tracking around the caret position and undo-redo sequences.
I added #textresult
to show the output and a .wrapper
to enclose both div and Problem 1 & 3 is solved just by that.
<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>
To solve Problem 2 you should not use jQuery.text
, use the native HTMLELement.innerText
to get the content with new line character and replace it with <br>
after span formatting.
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();
.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;
}
<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>
Upvotes: 4