Rangy SurroundContents Replacement - javascript

I'm looking at implementing a simple 2 button toggle on a webpage to switch some selected text between having a H1 heading and H2. The surroundContents method works great, however I'm encountering a problem when trying to replace an existing parent tag element node. I've played around with all sorts of ways trying to do this but not had much success.
The basic functions are below. Using the same selected text and running both of these functions one after the other will result on some output such as the following:
After selecting text of "test text" and then selecting the H1 option:
<h1>test text</h1>
If the same text is selected again and this time the H2 option pressed: <h1><h2>test text</h2></h1>
function surroundSelectedWithH1() {
var element = document.createElement("h1");
// removed code to setup range to save space
if (range) {
range.surroundContents(element);
}
}
function surroundSelectedWithH2() {
var element = document.createElement("h2");
// removed code to setup range to save space
if (range) {
range.surroundContents(element);
}
}
This is fine, and what would be expected, but I'm really looking for a way to remove the original parent heading element so that the heading elements do not become nested (for example - the text is surrounded by either h1 or h2, not both). I did experiment accessing the parentNode etc but did not manage to get this approach functional. I've tried looking at the following parentElement suggestion Getting the parent node for selected text with rangy library however I wasn't able to have rangy write the changed parent element back to the DOM or have a satisfactory way of determining where in the DOM the object was in order to replace it. It quickly became an unwieldy approach and there must be a better option.
I do know that the rangy CssApplier module can handle this situation but I need to work with actual elements and not css.
I also noticed that on the raptor editor which uses rangy for a text editor implementation suffers from the exact same problem when applying headings: http://www.raptor-editor.com/demo
This question was also relevant but this particular element problem can't be handled with execCommand as far as I'm aware - Javascript: how to un-surroundContents range
Any help or advice graciously received.

Try with:
highlighter.unhighlightSelection()

Related

How do I find the React DOM node that represents its actual html element?

Here's my problem, in case you're interested: I want to find the text selected by the user and insert a tag at the start and end of that selection (to highlight the text).
The best approach to find the selection, as quoted by the top minds of StackOverflow, is to use window.getSelection().
This, in combination with getRangeAt(0), turns up a list of things, where startContainer and endContainer look especially promising. startContainer.parentNode points directly to the p tag I started the selection in. Great!
However, how do I actually know which element this represents in the React DOM? How do I manipulate the right thing?
window.addEventListener("mousedown", function(e){console.log(e)});
using this event listener (or better use mouseup), you can find all the data about the target, example: if you copy/paste this piece of code into the console and see the log, you'll find all info about this event including the target, timestamp, shift key, control key, x,y, which mouse button, and all relevant information. if you combine this with what you described earlier you will get the results you need
EDIT:
As Mox had indirectly mentioned... When React re-renders, all the of the new nodes would be over-written and the highlighting would be lost.
This option as it is won't work well with React.
I'm going to leave this answer here since it is an option if the highlighted text is not being re-rendered often.
I believe the [surroundContents][1] method will be of great use to you.
According to the documentation, you can create a new node. Setup the class attribute on that new node. This class should enable the highlighting you want.
Then you can pass the new node into the surroundContents method to place the content selected by the range into your new node.
Here is an example from the documentation:
var range = document.createRange();
var newNode = document.createElement("p");
range.selectNode(document.getElementsByTagName("div").item(0));
range.surroundContents(newNode);
I believe you already have a range object, so you would just need to create a new node to wrap the highlighted text and use the surroundContents method.
I do see 1 major concern with this method.
If a user highlights text from inside a container and outside the same container, a situation like following may occur:
<p>
some text
<span class="highlighting">
some more text
</p>
some other text
</span>
I do not know how surroundContents will handle this, so this may be become a problem for you.

Can execCommand or a similar function be used to set, rather than to toggle, a simple style like bold?

If a contenteditable div's contents are already bold, then execCommand("bold") will remove the bold style.
That's great in normal circumstances, but I have a situation where I'd like to loop through a bunch of contenteditable divs and set any non-bold text to bold, sort of a way to style multiple rich text elements at once, using the proper or or 'font-weight' whatevers that execCommand uses.
I can do it a hard way by scrutinizing each node in each div in isolation, but I want to make sure there isn't a simpler way first.
It depends on how complex the editor you are making is. If you don't want to allow normal text inside a bold region, it may be enough to test only the deepest containers of editable texts, so one element for each of the editable divs.
Here's an example,
function bold(node) {
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
if (!isBold(getDeepestContainer(selection.anchorNode)))
document.execCommand("bold");
selection.removeAllRanges();
node.blur();
}
function getDeepestContainer(node) {
var result = node;
while (result.childNodes.length === 1) {
result = result.firstChild;
}
if (result instanceof Element)
return result;
return result.parentElement;
}
http://codepen.io/anon/pen/NqZQzR?editors=101
Okay, I couldn't find an easy way, so I did it the hard way.
I already had a function that copied the contents of a single element to a working space div with clean CSS, and then recursed through each node within that working space. For each text node, it would loop through each style I'm looking for and see if that style is set. If it's NOT, it would clear an "every node has this style" flag set before its run. So I ended up with a list of every style set for every node.
The first step for this current task was to identify which styles applied to all selected elements. So I took that function and created a modified version that first added each element's contents to the working space div. Then it ran the normal recursive walk through the working space, basically treating each separate element as if they're part of the same complex rich text string.
That gave me the status of each style, like whether everything was bold and thus the bold button should show as pressed.
I also added an array for each style. As I recursed through the elements, tracking their individual "every node has this style" value, I would come up with a list of elements where every node had the style, and thus execcommand("bold") would toggle that element.
So now, for each style (like for the bold button), I knew whether it would toggle any elements, or all elements. If it would toggle all elements, or no elements, then that's fine because the behavior's consistent. But if it toggled a subset of elements (if "elements_that_would_toggle" array length for a style was greater than zero but less than the total_elements count), then I would ignore those toggle-able elements on the first button click.
At the end of that click processing, I then blank out those arrays for each style, thus reverting the behavior across the board to toggling, because now every element has been set to the same status.
It's sort of a disgusting approach, but it's controlled and consistent, and it works really well.
And it only took a day to get working.

Mouseover event on colored with textrange object fragment of text

I want to ask question is it something possible with text range or
other approach. I have a text. For example like this:
<div id="test">abcdefghij</div>
I use text range to highlight fragment of text with yellow color.
(I use example posted into SO). It is possible to add event listener
for mouser over on highlighted text? I want to do something when user
mouseover on 'cdefg'. Here is working jsfiddle.
http://jsfiddle.net/8mdX4/673/
I would appreciate any ideas and recommendations.
Best regards.
Edit:
I want to share the approach that I have usied after the topic.
I'm using focusNode property of selection because commonAncestorContainer
get container of all nodes - not that node currently selected.
Here is demo:
http://jsfiddle.net/zono/Q3ptC/2/
Mouseover on yellow text displaing tooltip (content of title property).
You are using document.execCommand a lot in your code to style elements, this will most likely create a <span> but that's up to the browser. When doing so, each browser will create elements in your target text as it sees fit. To my knowledge, there is no event management command available when using execCommand, so using this technique will make specific event assignments difficult to these elements.
If I were to write your code from scratch, I would use an approach with window.getSelection and create the elements myself, so that I could add the events as I see fit.
Another alternative is just to add window.getSelection() once you know that execCommand has created an element, find the selection's parent, and add the event to the parent node. Like this:
/*At a point where I know a selection is made*/
var sel = window.getSelection();
if (sel.rangeCount) {
parent = sel.getRangeAt(0).commonAncestorContainer
if (parent.nodeType != 1) {
parent = parent.parentNode
}
parent.onclick = function() {
alert("lookie lookie");
}
}
Your code updated with my example:
http://jsfiddle.net/8mdX4/680/

Drag-n-Drop on contentEditable elements

There are numerous WYSIWYG editors available on the internet, but I'm yet to find one that implements some form of drag-n-drop implementation.
It is easy to create one's own editor, but I want to the user to be able to drag elements (ie. tokens) from outside the editable area and have them drop it at a location of their choice inside the editable area.
It is easy to inject html at a specific location of an editable element, but how do one determine where the caret should be when the user is dragging a DIV over some element in the editable area. To better illustrate what I'm trying to explain, see the following scenario.
The editable area (either an IFRAME in edit mode or a DIV with its contentEditable attribute set to true) already contains the following text:
"Dear , please take note of ...."
The user now drags an element representing some token from a list of elements, over the editable area, moving the cursor over the text until the caret appear just before the comma (,) in the text as shown above. When the user releases the mouse button at that location, HTML will be injected which could result in something like this:
"Dear {UserFirstName}, please take note of ...".
I do not know if anyone has ever done anything similar to this, or at least know of how one would go about doing this using JavaScript.
Any help will be greatly appreciated.
Here is my approach to solving the issue of custom drag elements on editable elements. The big issue is that one cannot determine the text offset of the mouse cursor when hovering over the editable element. I have tried faking a mouse click to set the caret at the desired position but that did not work. Even if it did, one would not visually see the placement of the caret while dragging, but only the resulting drop.
Since one can bind mouse-over events to elements and not text-nodes, one can set the editable element to be temporarily un-editable. Find all elements and wrap each text-node in a span as to not breaking the flow of the text. Each span should be given a classname so we can find them again.
After the wrapping, one should again find all the wrapped text-nodes and wrap each character with another span with a classname that one can find them again.
Using event delegation one can add an event to the main editable element that will apply a style to each character span that will display the caret, a blinking GIF image as a background.
Again, using event delegation, one should add an event for the mouse-up event (drop event) on each character. One can now determine the offset using the character span's position (offset) within its parent (wrapped text-node). One can now undo all the wrapping, keeping a reference to the calculated offset and while undoing the wrapping keeping a reference to the applicable text-node.
Using the range & selection objects of the browser, one can now set the selection using the calculated offset to the applicable text-node and inject the required HTML at the newly set selection (caret position), et viola!
Here follows a snippet using jQuery that will find textnodes, wrap them:
editElement.find("*:not(.text-node)").contents().filter(function(){
return this.nodeType != 1;
}).wrap("<span class=\"text-node\"/>");
To find each text-node and wrap each character, use:
editElement.find(".text-node").each(function()
{
var textnode = $(this), text = textnode.text(), result = [];
for (var i = 0; i < text.length; i++) result.push(text.substr(i, 1));
textnode.html("<span class=\"char\">"
+ result.join("</span><span class=\"char\">") + "</span>");
});
To undo the wrapping:
editElement.find(".text-node").each(function()
{
this.parentNode.replaceChild(document.createTextNode($(this).text()), this);
});
Hope this approach helps those having similar challenges
If I understand what you're saying, then this is just basic drag and drop. The solution below should be close to the best answer for FIREFOX. I'm sure there are different functions for IE. Go to http://www.html5rocks.com/en/tutorials/dnd/basics/ for more help.
Set the "draggable" attribute of the object you want to drag, and set the object's "ondragstart" method to "dragStartHandler" or whatever your function is called.
// You can set this to 'text/plain' if you don't want to insert HTML content
var internalDNDType = 'text/html';
function dragStartHandler(event) {
// This is whatever html data you want to insert.
var textContent = "<b>"+userFirstName+"</b>";
event.dataTransfer.setData(internalDNDType, textContent);
event.dataTransfer.effectAllowed = 'copy';
}
function dragEnterHandler(event)
{
event.preventDefault();
return false;
}
function dragOverHandler(event)
{
event.preventDefault();
return false;
}
function dropHandler(event) {
event.preventDefault();
event.stopPropogation();
return false;
}
Currently an HTML5 API is being developed to do this, but unfortunately IE doesn't support it. Edit: Apparently IE actually does support drag and drop, but I'm not very familiar with how it works. Try Googling "IE drag and drop".
Try looking at these sites:
https://developer.mozilla.org/en/DragDrop/Drag_and_Drop
http://html5doctor.com/native-drag-and-drop/
http://www.thebuzzmedia.com/html5-drag-and-drop-and-file-api-tutorial/
http://www.webreference.com/programming/javascript/dragdropie/ (Drag and Drop in IE)

Transforming highlighted text inside an element using javascript

My goal is to be able to get the highlighted text within a document, but only if that text is within a given section, and then apply a certain style to that selected text after clicking a div tag. I'll explain what I mean:
So, having looked at window.getSelection() and document.selection.createRange().text, I attempted to use elmnt.getSelection() or elmnt.selection.createRange().text for some HTML element, elmnt. However, it doesn't seem to work, so that idea seems pretty null. This means I can't use this idea to determine the text that is highlighted within a given location. In case this doesn't make sense, essentially, I want html code that looks like this:
<body>
<div id="content">Stuff here will not be effected</div>
<div id="highlightable">Stuff here can be effected when highlighted</div>
<div id="morecontent">Stuff here will also not be effected</div>
</body>
So that whenever I've highlighted text, clicking on a specified div will apply the proper CSS.
Now, on to the div tags. Basically, here's what I've got on that:
$('.colorpicker').click( function(e)
{
console.log(getSelectedText());
}
Eventually, all I want this to highlight the selected text and have the div tag change the color of the selected text to that of the respective div tag that I've selected. Neither of these seems to be working right now, and my only guess for the reason of the div tag is that it unhighlights whatever I've got selected whenever I click on the div tag.
Fallbacks:
If there is more than one time that 'abc' is found on the page and I highlight to color 'abc', I would like that only that copy of 'abc' be highlighted.
I know this is a lot in one question, but even if I could get a little head start on this idea, my next personal project would be going a lot more smoothly. Thanks. :)
The key in the solution to this will be working with the objects that represent text ranges in browsers, not the selected text itself. Look into methods available to you in both the FireFox Range and IE TextRange objects. Both of these contain means of replacing the selected text with your own markup (e.g. a span wrapping your selected text.)
For FF look into Range.getRangeAt(0).surroundContents(element)
For IE look into TextRange.pasteHTML()
I must warn you though... You'll probably end up down a scary path of browser quirks if you go through with this. Already from the get-go you're supporting two different objects for two of the major browsers.

Categories

Resources