Keeping code clean with a WYSIWYG rich text editor - javascript

I'm tinkering with making a WYSIWYG editor. And yes, I'm fully aware of execCommand(), but I don't like how messy, and inconsistent it can get.
Note: This is not a question of how to get it to work, as other questions are, but rather how to keep the end result clean.
The next best option seems to be window.getSelection(), which works great, until I hit a situation like this:
<p id="para" contenteditable="true">
A bit of <b>a</b> <span class="blackColor">lon</span><i>ger</i> sentence.
</p>
Which to the user gives
A bit of a longer sentence
So, this is what I've come up with, so far to bold something:
function bold(){
var box = document.getElementById("para");
var sel = window.getSelection();
if(sel.rangeCount < 1) return;
var range = sel.getRangeAt(0);
if(!validSelection(range.startContainer, box) || !validSelection(range.endContainer, box)){
return;
}
// Get what we need from the first element
var data = range.extractContents();
for(var i = 0; i < data.childNodes.length; ++i){
var b = document.createElement('b');
b.innerHTML = data.childNodes[0].textContent;
data.appendChild(b);
data.removeChild(data.firstChild);
}
range.extractContents();
range.insertNode(data);
}
It works, but it can get messy, very, very fast.
<p id="para" contenteditable="true">
A <b>b</b><b>it</b><b></b> of <b>a</b> <span class="blackColor">lon</span><i>ger</i> sentence.
</p>
That's after calling bold() for "it" and then "bit".
If I call it over multiple elements, such as, "of a long"
<p id="para" contenteditable="true">
A bit <b>of </b><b>a</b><b> </b><b>lon</b><b>g</b><i>er</i> sentence.
</p>
That's seems much worse than execCommand(), but I'd still like to do this in a clean manner. What is the proper way to do this to where I won't have extremely messy code once all is said and done? Also, I do realize that other formatting has been lost from the original. Something I'm working on, but my main thing is keeping it clean right now.
I've noticed in the inspector that if I hit [Ctrl] + B, and and then undo, things break up into multiple text nodes, but stay not-so-messy in the actual code. However, that only works for bold, italics, and underline, and I'd like to implement a few more things.
As an end note, I am open to just cleaning up execCommand(), but if I can instead code it myself... well... I like coding. :)
Thanks.

Related

How to use innertHTML, in this following situation

We were given an assignment to basically make a Madlib by having the user enter words into textfields and then replacing words in a hidden paragraph existing in the html page. We have to use JavaScript and CSS.
the paragraph in the html page:
<span id="story" display="hidden">
Rain was still lashing the windows, which were now <span id="adjs1">__adjective__</span>, but inside all looked bright and cheerful. The firelight glowed over
the countless <span id="adjs2">___adjective___</span> <span id="plnouns">___plural_noun___</span> where people sat <span id="verbs1">___verb_with_ing___</span>
, talking, doing homework or, in the case of Fred and George Weasley, trying to find out what would happen if you fed a <span id="foods">___food___</span> to a
<span id="monsters1">___monster___</span>. Fred had "rescued" the <span id="adjs3">___adjective___</span>, fire-dwelling <span id="monsters2">___monster___</
span> from a Care of Magical Creatures class and it was now<span id="verbs2">___verb_with_ing___</span> gently on a table surrounded by a knot of curious peopl.
</span>
Everything was going fine till I keep missing on getting the results I want.
function generateMadlib(){
// Display the story. The story is initially hidden.
document.getElementById("story").style.display = "inline";
// Get the words from the textboxes.
var formElements = $("#madlibForm :text");
// Find the word locations on the hidden paragraph.
var storyWords = $("#story span");
// Replace word loc values with with formElement values
for (var i = 0; i < formElements.length; i++)
{
storyWords.eq(i).innerHTML = formElements.eq(i).val();
}
}
This line
storyWords.eq(i).innerHTML = formElements.eq(i).val();
doesn't change the values inside the spans within the paragraph. (the code returns the proper input on the textfields)
I also tried using the browser console and manually changing document.getElementById("adjs1").innerHTML = "test"; it will return "test" but the value doesn't actually change. Can anyone clarify what .innerHTML actually does?
.eq(i) returns a jQuery object so it don't have the innerHTML property, so you can use .html() to set the html content
storyWords.eq(i).html(formElements.eq(i).val())
or you can use .get() which will return a dom element reference
storyWords.get(i).innerHTML = formElements.eq(i).val();
But you can simplify the overall implementation like
function generateMadlib() {
// Display the story. The story is initially hidden.
$("#story").css('display', "inline");
// Get the words from the textboxes.
var formElements = $("#madlibForm :text");
$("#story span").html(function (idx) {
return formElements.eq(idx).val();
})
}

QtWebkit select text by itself after pressing space

I tried to google this question, but google don't know about this issue.
I'm using Qt 4.8.1 QtWebkit 2.2.3+Javascript for implement some kind of Html-Editor.
It is hard to explain the problem by text so I maid some screenshots (link in the end of message).
1) So at first I insert just normal text.
2) After I add one more word, and wrap it in tag using this javascript function
function misspelledWord(id)
{
var sel = rangy.getSelection();
var highlightDiv = document.createElement('span');
highlightDiv.className = "misspelled";
highlightDiv.id = "misspelled_" + id;
range.surroundContents( highlightDiv );
var space = document.createTextNode("\xa0");
highlightDiv.parentNode.insertBefore(space, highlightDiv.nextSibling);
range.setStartAfter(space);
range.setEndAfter(space);
sel.removeAllRanges();
sel.setSingleRange(range);
}
3) After I press space one more time. Webkit select all text till "span" by itself and I can not delete this selection, only if select it by hand one more time.
http://i.stack.imgur.com/PprG0.png - screenshots
So maybe somebody know how to fix this behavior?
Best regards
Paul

how to get selected text, but can I get surrounding context in javascript?

I am able to grab the text that a user has selected on a web page,
using this code:
function getSelected() {
var userSelection;
if (window.getSelection) {
selection = window.getSelection();
} else if (document.selection) {
selection = document.selection.createRange();
}
}
is it posible for me to get the words around the
selected word.
Take these sentences for example: "If you need to
framglubble the zartbox, then you should buy the red widget.
Otherwise you can buy the blue widget and save some money."
My code will tell me if the person has selected the word "widget".
But I'd like to know if the selection is after "red" or "blue". Is
this possible? I've been scouring the Internet for some advice, and
I'm having trouble finding an answer.
thank you for your help
I have written quick script that can identify the part before selection and after selection inside the same DIV element.
However if the same DIV contains the same word more than one time and you select only that word, the current code I wrote can't identify if it's the first or second selected word so bottom line it will not answer your needs.
Anyway, you can see/copy/test the code here: http://jsfiddle.net/kvHxJ/ just select something and see the alert that appears.
If it's enough for your needs after all then great, accept this answer and move on... otherwise I need to know: can we assume the user will select whole words only, one word only? If the answer is yes I do have idea how to go around this.
The way to do this in non-IE browsers is to obtain a Range object from the selection. The range has a start and end boundary, and each boundary of the range is expressed as an offset within a node; if the boundary is within a text node, this offset will be a character offset.
For example, if the following was a text node and the selection is delimited by pipes:
"red |widget| blue widget"
... then the range you'd get from the selection would have a start offset of 4 within the text node.
The following will get you a Range representing the selection and alert the start boundary:
var sel = window.getSelection();
var selectedRange = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
alert("Offset " + selectedRange.startOffset
+ " in node " + selectedRange.startContainer.nodeName);
}
Ranges may be compared to other Ranges, so if you wanted to know, for example, if the current selection came after the word "blue" in the above text node, you could create a Range encompassing the word "blue" and compare it with the selected Range:
// Assume the text node is stored in a variable called textNode
var blueRange = document.createRange();
blueRange.setStart(textNode, 11);
blueRange.setEnd(textNode, 15);
var selectionIsAfterBlue =
(selectedRange.compareBoundaryPoints(Range.END_TO_START, blueRange) == 1);
In IE, none of this works and everything is done differently, generally with much more difficulty. To normalize this to single consistent interface, you could use my Rangy library.
IE has the move set of methods, which reduces this problem to just a couple of lines to expand the selection forward or backward any number of words (see http://www.webreference.com/js/column12/trmethods.html). From there, it's just a matter of comparing text against any arbitrary list of values. Other browsers don't have this feature AFAIK. Fate of the browser wars: one develops an awesome feature ignored or barred by patent from any other, so the feature is forever lost and avoided as burden of cross-browser support for all these innovations inevitably falls squarely on the website designers.
So, below is a generalized function to only get the ID of the parent element of the selected text. And, to work with this cross-browser solution, you have to wrap each word in it's own element complete with unique ID or other attribute. With this setup, it should then be a relatively painless jump to looking ahead and back at sibling or sequentially ID'd/named elements.
The catch here is that the client has to click/drag from the start of the word or phrase to the end, and absolutely no bordering spaces. Even double-clicking on a word will cause it to reference the next element (or in the case of IE, the parent DIV). Additionally, you should add code to restrict the selection boundary to a single parent DIV, as the below code may also expand the selection to surrounding elements. But hopefully you can take fixing that up from here. Otherwise, it's up to using vectors to pinpoint the coordinates of a text compared to all surrounding text.
<script type="text/javascript">
function get_selected_element_id() {
if (window.getSelection) {
// FF
var range = window.getSelection();
}
else if (document.selection) {
// IE
var range = document.selection.createRange();
}
if (range.focusNode) {
// FF
var test_value = range.focusNode.parentNode.id;
}
else {
// IE
var test_value = range.parentElement().id;
}
return test_value;
}
</script>
</head>
<body>
<div id="test_div">
<span id="test1">test</span> <span id="test2">asdf</span> <span id="test3">test2</span> <span id="test4">bla</span>
</div>
<button onclick="alert(get_selected_element_id());">go</button>

Div Editable and More... More

Well,
I need to replace a word, in a div contentEdible property on, by the same word but formatted...
Like this:
<div> My balls are big </div>
To this (replace the word: balls):
<div> My <font style="color:blue;">balls</font> are big </div>
In a contentEditable this happens dinamically, while the user type the text the replacements happens. I think that a simple event onkeydown, onkeyup, or onkey press, can solve this part.
But, the trouble is with the caret, that after all that i tryed, it stay before the word replaced, when should be stay after. I tryed write some js code, tryed find some jquery scripts, but all them failed in this situation...
Any one has some ideia, or trick ?
I think:
--> Record the length of the word unformatted.
--> Delete this word
--> Put new word formatted.
--> Walk with the caret, to position based this formatted word length.
--> Is it?
PS: I have to considerate a word in any place of this div.
I don't know how to write this code that do what i say above.
Correct me, if i'm wrong.
Since yet, thanks!
Edit[1]: I want that this works on Mozilla Firefox, specificlly;
I only have IE6/7 on this machine, but maybe you can apply the concept to other browser versions of Ranges (or maybe this is cross-browser?).
Basically we store the cursor position, make our search/replacement, then put the cursor back where it was:
html:
<div id="content" contentEditable="true" onkeyup="highlight(this)">This is some area to type.</div>
and the script:
function highlight(elem) {
// store cursor position
var cursorPos=document.selection.createRange().duplicate();
var clickx = cursorPos.getBoundingClientRect().left;
var clicky = cursorPos.getBoundingClientRect().top;
// copy contents of div
var content = elem.innerHTML;
var replaceStart = '<font style="color:blue">';
var replaceEnd = '</font>';
// only replace/move cursor if any matches
// note the spacebands - this prevents duplicates
if(content.match(/ test /)) {
elem.innerHTML = content.replace(/ test /g,' '+replaceStart+'test'+replaceEnd+' ');
// reset cursor and focus
cursorPos = document.body.createTextRange();
cursorPos.moveToPoint(clickx, clicky);
cursorPos.select();
}
}

How do I select arbitrary text on the page using javascript?

Let's say I have a contentEditable div, to the user can edit and change the text and elements inside it. How do I arbitrarily change the selection in this div with javascript? By "change" I don't mean "change the contents of whatever the user has selected", I mean actually change what is selected. The user should then be able to type over the selection, replacing it with something else.
This should take into account that I may want to select text across elements. For instance:
<p>Some text <span>goes</span> here.</p>
I may for instance want to select "Some text go", or everything inside the <p>.
This only needs to work in Safari/Webkit.
Thanks in advance. As a native code developer, I find the DOM and Javascript in general quite frustrating.
Just to answer my own question in detail so anyone searching for something similar doesn't have to go looking elsewhere...
The code I ended up using was something like this:
var range = document.createRange();
range.setStart( <get the node the selection starts in>, <char offset in that node> );
range.setEnd( <get the node the selection ends in>, <char offset in that node> );
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
Big thanks to James Black for pointing me in the right direction.
Unless you need to write your own, you may want to look at tinyMCE, http://tinymce.moxiecode.com/, as it is a nice WYSIWYG editor in javascript.
In order to do this you will probably want to look at something like this:
http://codingtricks.blogspot.com/2009/03/javascript-select-partial-text-in-div.html
These may also be helpful:
JavaScript ranging gone wrong
https://developer.mozilla.org/en/DOM/window.getSelection
What you are trying to do will be complex, as you will need to take the selected area, remove all the tags, then put in the tag that you want for the selected area.
You can use document.getElementById('your_text_id').setSelectionRange(start, end); and you can use Math.random() to generate random numbers for start and end
While #Lucas's answer is good, there is a lot missing that would allow you to successfully use this. The node the selection starts in has to be the exact node, not a parent. In our case we were trying to put some text into a TextAngular control, then select text that looked liked ____ so the user could "fill in the blank".
Our input was html of the order <p>Some text goes here: _____</p> or
<p>Some partial goes here
<ul>
<li>Option 1</li>
<li>_____</li>
</ul>
To get this to work, we had to write something to find the underscores in the right element
function find_(node) {
var i, n;
// return the node with the _'s
for(i=0; i < node.childNodes.length; ++i) {
n = node.childNodes[i];
console.debug(n);
if(n.textContent) {
console.debug(n, n.textContent);
if(n.textContent.search(/___+/) > 0) {
return n;
}
}
if(n.childNodes) {
console.debug(n, n.childNodes);
n = find_(n);
if(n) {
return n;
}
}
}
return null;
}
So in the end, finding the node to satisfy <get the node the selection starts in> was a lot more work than that simple sentence led me to believe.
In the <ul> case. the node that contains the ____ is firstChild of the li node.
I've put this here to help others that need to do this not wonder why they are getting the error message
IndexSizeError: Failed to execute 'setStart' on 'Range': There is no child at offset 65.
When the problem is they are just looking at the wrong node.

Categories