I have an in-house built WYSIWYG editor that is able to select content and convert it to a list (it does other things as well) using the JavaScript range and selection objects. This is similar to moving the caret to the end of a line when a BR tag is inserted (I used Tim Down's example, provided here) to implement that, however I need to keep the cursor inside the initial node so that further nodes can be inserted. The initial node would be an OL | UL tag and I need to be able to insert list items with the text from the selection.
The code that I'm using to handle this:
OverrideListCreation: function (commandName, showDefaultUI, commandValue, context) {
var html;
var contextHtml = context.range.htmlText;
var insertSuccess;
var element;
console.log("ContextHtml: " + contextHtml);
if (commandName == "insertorderedlist") {
element = this.designModeDocument.createElement("ol");
} else {
element = this.designModeDocument.createElement("ul");
}
element.setAttribute("class", "content");
element.setAttribute("style", "margin: 1em;");
insertSuccess = this.InsertNodeAtCursor(context, element, false);
element = this.designModeDocument.createElement("li");
element.setAttribute("class", "content");
element.setAttribute("style", "margin: 0em;");
if (contextHtml.search(/<br>/gi) > -1) {
console.log("ContextHtml Length: " + contextHtml.length);
console.log("Multiline Check - Passed");
var lines = contextHtml.split(/<br>/gi);
for (var i = 0; i < lines.length; i++) {
element.innerHTML = lines[i];
insertSuccess = this.InsertNodeAtCursor(context, element, false);
if (insertSuccess === false) {
return;
}
}
} else if (contextHtml.length > 1 && contextHtml.search(/<br>/gi) === -1) {
console.log("Single Line Check w/o BR - Passed");
element.innerHTML = contextHtml;
insertSuccess = this.InsertNodeAtCursor(context, element, false);
if (insertSuccess === false) {
return;
}
} else {
console.log("ContextHtml is empty, insert an empty list element");
insertSuccess = this.InsertNodeAtCursor(context, element, false);
if (insertSuccess === false) {
return;
}
}
return;
For Reference: The Context Object Code:
GetDesignModeContext: function () {
var context = new Object();
try {
if (this.designModeDocument.selection) {
context.selection = this.designModeDocument.selection;
context.range = context.selection.createRange();
context.selectedText = context.range.text;
switch (context.selection.type) {
case "None":
case "Text":
context.parentElement = context.range.parentElement();
break;
case "Control":
context.parentElement = context.range.item(0);
break;
default:
context.parentElement = this.designModeDocument.body;
break;
}
}
else if (this.designModeDocument.getSelection || this.designEditor.getSelection) {
context.selection = this.designEditor.getSelection();
context.selectedText = context.selection.toString();
try {
context.range = context.selection.getRangeAt(0);
}
catch (e) {
context.range = this.designModeDocument.createRange();
}
function IsSelectedTextNode(container, offset, start) {
if (container.nodeType != 3) return false;
var startIndex = start ? offset : 0;
var endIndex = start ? container.nodeValue.length : offset + 1;
var text = container.nodeValue.substring(startIndex, endIndex);
return (context.selectedText == text);
}
var r = context.range;
var p = null;
if (r.startContainer == r.endContainer) {
if (r.collapsed) {
p = r.startContainer;
}
else if (r.startOffset - r.endOffset <= 1 &&
r.startContainer.hasChildNodes()) {
p = r.startContainer.childNodes[r.startOffset];
}
}
else if (IsSelectedTextNode(r.startContainer, r.startOffset, true)) {
p = r.startContainer;
}
else if (IsSelectedTextNode(r.endContainer, r.endOffset, false)) {
p = r.endContainer;
}
if (!p) p = r.commonAncestorContainer;
while (p.nodeType == 3) p = p.parentNode;
context.parentElement = p;
}
if (context.parentElement == null) return null;
if (context.parentElement.nodeType != 1) return null;
if (context.parentElement.ownerDocument != this.designModeDocument) return null;
}
catch (e) {
return null;
}
return context;
},
The Insert Node at Cursor Code:
InsertNodeAtCursor: function (context, domElement, positionEnd) {
var selection, range;
if (typeof this.designModeDocument.getSelection != "undefined" && context.selection) {
//console.log("getSelection is supported by this browser && Context.Selection was not undefined.");
selection = this.designModeDocument.getSelection() || context.selection;
if ((selection.getRangeAt && selection.rangeCount) && context.range) {
//console.log("getRangeAt & rangeCount are supported by this browser && Context.Range is not undefined");
range = selection.getRangeAt(0) || context.range;
range.deleteContents();
//console.log("Inserting BR Element Node");
range.insertNode(domElement);
//console.log("Setting Position of Cursor");
if (positionEnd) {
range.setEndAfter(domElement);
range.setStartAfter(domElement);
} else {
}
//console.log("Selection Clean-up");
selection.removeAllRanges();
selection.addRange(range);
return true;
}
} else if (typeof this.designModeDocument.selection != "undefined" && context.selection) {
//console.log("Using alternate selection method.");
if (typeof selection.createRange && context.range) {
//console.log("Using alternate range method.");
range = selection.createRange || context.range;
var tmpElement = this.designModeDocument.createElement("div");
tmpElement.appendChild(domElement);
this.PasteHtml(range, tmpElement.innerHTML);
range.select();
this.designModeDocument.removeChild(tmpElement);
return true;
}
} else {
return false;
}
},
My initial thought were to change the setStartAfter and setEndAfter functions to not move the cursor however that didn't work, which is why there is an empty else block following those functions.
EDIT: I did have an older version of the list creation code that would take the selected text and build list items around it by replacing the break lines with start and close list tags, after which it would paste them to the editable document (The original target was for IE. The original code was written in as an included script file, which is why it has a different structure).
function doCommandIERangeList(strCommand, bShowDefaultUI, strOptional) {
// Get a text range for the selection
var strHTML;
var strHTMLFromControl;
var tr = getIFrameDocument(_strIFrame).selection.createRange();
strHTMLFromControl = tr.htmlText;
//if there are break lines present create a new list
//otherwise we just want to switch between list types
if (strHTMLFromControl.search(/<BR>/gi) > -1) {
if (strCommand == "InsertOrderedList") {
strHTML = "<ol>";
} else {
strHTML = "<ul>";
}
strHTML += "<li>" + strHTMLFromControl.replace(/<BR>/gi, "</li><li>") + "</li>";
tr = getIFrameDocument(_strIFrame).selection.createRange();
if (strCommand == "InsertOrderedList") {
strHTML += "</ol>";
} else {
strHTML += "</ul>";
}
//IE Version Check to remove somewhat randomly placed <br> tags when trying to format a set of text instructions as a List Range
if (IsIE9()) {
strHTML = strHTML.replace(/[\r\n]/g, "");
}
if (IsIE10()) {
strHTML = strHTML.replace(/[\r\n]/g, "");
}
tr.pasteHTML(strHTML);
} else {
//needed to switch between ul and ol
tr.execCommand(strCommand, bShowDefaultUI, strOptional);
}
// Reselect and give the focus back to the editor
tr.select();
frames.editor.focus();
}
UPDATE: I figured out how to get the HTML to build (I will post the code below) and insert into the document, however the list items are not rendering correctly in the editor window. Any thoughts?
OverrideListCreation: function (commandName, showDefaultUI, commandValue, context) {
var html;
var contextHtml = context.range.htmlText;
var insertSuccess;
var parentElement,element;
console.log("ContextHtml: " + contextHtml);
if (commandName == "insertorderedlist") {
parentElement = this.designModeDocument.createElement("ol");
parentElement.setAttribute("type", "1");
} else {
parentElement = this.designModeDocument.createElement("ul");
parentElement.setAttribute("type", "disc");
}
parentElement.setAttribute("class", "content");
parentElement.setAttribute("style", "margin: 1em;");
if (contextHtml.search(/<br>/gi) > -1) {
console.log("ContextHtml Length: " + contextHtml.length);
console.log("Multiline Check - Passed");
var lines = contextHtml.split(/<br>/gi);
for (var i = 0; i < lines.length; i++) {
console.log("Lines @ " + i + ": " + lines[i]);
element = this.designModeDocument.createElement("li");
element.setAttribute("class", "content");
element.setAttribute("style", "margin: 0em;");
if (typeof element.innerHTML != "undefined") {
element.innerHTML = lines[i]
} else {
element.textContent = lines[i];
}
parentElement.appendChild(element);
}
} else if (contextHtml.length > 1 && contextHtml.search(/<br>/gi) === -1) {
console.log("Single Line Check w/o BR - Passed");
element = this.designModeDocument.createElement("li");
element.setAttribute("class", "content");
element.setAttribute("style", "margin: 0em;");
if (typeof element.innerHTML != "undefined") {
element.innerHTML = contextHtml
} else {
element.textContent = contextHtml
}
parentElement.appendChild(element);
} else {
console.log("ContextHtml is empty, insert an empty list element");
element = this.designModeDocument.createElement("li");
element.setAttribute("class", "content");
element.setAttribute("style", "margin: 0em;");
parentElement.appendChild(element);
}
insertSuccess = this.InsertNodeAtCursor(context, parentElement, false);
return;
Specifying the type attributes (not HTML 5 friendly) did not solve my issue, so they are unused.