How to avoid breaking block level HTML elements from partial selection? - javascript

The problem in a nutshell is: given a wysiwyg editor (CKEditor) you want to make a plugin doing text transformation - select a piece of text and manipulate the text in it (eg uppercase). Example:
this is paragraph one
this is paragraph two
If bold represents your selection the result would be
this is paragraph ONE
THIS is paragraph two
This issue here is the selection will be a complete HTML fragment, even when a selection is no containing the full tag. The selected HTML is:
<p>one</p> <p>this</p>
Notice the first and last <p> tags. When you do your dom traverse in the selection html, apply the text transformation and replace the html it will use those partial tags, so your result become:
this is paragraph
ONE
THIS
is paragraph two
I checked if it's possible to "merge" the first and last partial tags with their dom parents, however the selection object is isolated, it doesn't have siblings or parents from it's original context.
Also tried to find an option to retrieve the selection without these auto-fixed tags, but no luck.
On the CKEditor documentation they mention a walker object - however that automatically expands from the selection to the full enclosing tag, which means the selection is only used as a minimum boundary.
Also because the selection object is isolated, it's not possible to just change the dom node text values there - the original dom fragment needs to be replaced (at least in case of CKEditor).
I tried not to stick with the CKEditor api as much as possible, however at this point I don't see any alternatives either. Is this is really a hard problem or I'm missing something?

One solution is to use the browser engine to mark the selected area with a tag (afaik that's a native operation). This is the same as you make your selection bold or italic - however here it's gonna be a temporary wrapper. Then you walk the DOM and replace the content in the temporary tags - and finally remove the tag (keeping the content).
This makes sure you can apply your transformation on the exact selection and by removing the tag you won't break the original DOM. Steps in a nutshell:
apply tag on selection (use the browser or wysiwyg api)
find all temp tags:
recursively walk the children of the tag
if tag is text node then apply transformation
otherwise recursive walk
collect tag's left sibling text node + tag's html + right sibling text node
replace tag's parent html with the previous compilation (=remove temp tag)
Not too elegant however works. This is inspired by #Andrew_Mast's idea about wrapping in a span.

I would loop through all of the word(s) and for each set of words inside a different tag, surround it with <span style="text-transform: uppercase;"> and </span>

Related

Input text in draft.js

I'm doing automation test and I need to input text in a text field, but the problem is there is no 'input' to do this.
This one won't work:
document.querySelector([class*=modal-dialog] [class*=AddCitation_] [class*='DraftEditorPlaceholder']).value='Hello World'
Does any one know how to input text in draft.js?
You can use document.execCommand("insertHTML", "html string here", false)
As you can see draft js uses contenteditable property of div to write text
So if you want to automate tests with draftjs insert any sample html string with command
so you have a sample text in draft editor now you can perform tests
Read this
https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
Let's first understand the code you have written:
document.querySelector([class*=modal-dialog] [class*=AddCitation_] [class*='DraftEditorPlaceholder']).value='Hello World'
This code attempts to:
get the first element
which has a class containing 'DraftEditorPlaceholder' and
which has an ancestor having a class containing AddCitation_ which
has an ancestor having a class containing modal-dialog
So, you will first need to ensure that the element you try to edit is inside an element having a class containing modal-dialog. In your screenshot we cannot see such an ancestor element, the root having a class of modal-content. If the selector you have written does not find an element, then an error will be thrown when you try to assign a value to any of its attributes, like value in this case. You have an element having a class of styles_AddCitation__3_D5j, which is the child of the element of the class of modal-content. You have an element having a class of public-DraftEditorPlaceholder-root, which, assuming that you have a class containing the modal-dialog text being its ancestor, then the selector will find it. However, this is the sibling of the element which you seem to expect to find with your query. So, you will need to sort out where you intend to put your text into.
Now, assuming that you have sorted your selector out and there is a span in the element you are talking about, you will need to write span at the end of your selector, with a space before it. Also, in this case you will need to set innerText instead of value to your desired value. If the target element is an input, then setting a value should suffice.
Another possible problem is that your Javascript code might run before the structure is actually generated, hence not being able to set attributes of elements which do not exist yet. So you will also need to make sure that your structure is in place indeed when this Javascript code runs.
const screen = document.getElementById("screen");//get span ID
screen.innerHTML += `<span class="screen" >${
this.textContent
}</span>`);//add text content
<body>
<h1>CALCULATOR</h1>
<!-- <div class="container"> -->
<div class="container">
<div class="column2" id="screen">
<!-- <span class="screen"></span> -->
</div>
I used this to insert span tags into a div tag.
I'm only a beginner so it might not be optimal
I used this while experimenting with JS on a simple calculator, because I wanted to see if it could work without using an input tag.
please let me know if this helps as it will also help me to see if my understanding is good

Remove direct parent node of selection (<b>)

I would like to remove and add the <b> tag to a selected text just by clicking on it.
I can already enclose selected text in the <b> tag via:
if (window.getSelection)
{
selection = getSelection();
var newNode = document.createElement("b");
range.surroundContents(newNode);
}
Now that time I would like to have a method to remove the <b> tag I added beforehand, but the commonly shown method of using range.commonAncestorContainer would give me lowest parent of the <div> type.
As a part of debugging I simply modified the code I used to add the <b> tag (to make sure I had a tag, I also first added it) to remove the parent node.
if (window.getSelection) {
selection = getSelection();
var newNode = document.createElement("b");
range.surroundContents(newNode);
node = range.commonAncestorContainer;
node.remove();
}
This will remove the parent <div> of the newly added <b> tag, instead of only the <b> tag itself.
For reference, here is the HTML:
<div contenteditable="true" id="input" class="inputblock" onclick="getClick()" onkeypress="changeNodeType(event)" data-text="Please paste your text here."></div>
The <b> tag will be created within text pasted into the shown <div>.
The fact that it also removes the content can be ignored. I'll find a way to work with that.
I unfortunately don't have as much time as I'd like to spend to write out a full code solution, but this is how I've solved this in the past:
Normalize your range. This is the hardest part. IE, edge, and different browsers behave differently so you will need to handle a lot of special edge cases. For example if the range is right before an empty text node, IE will do wonky things
For your the start and endcontainer, if they are text nodes, do nothing
Otherwise, you need to use a NodeIterator to do an in-order traversal and walk to the next node for the start container, and the previous node for the end container. This solves the case where, when you select something like <b>hello</b>, the selection can either be |<b>hello</b>| or <b>|hello|</b>. By normalizing and walking down the tree, you will end up with a - mostly equivalent - selection of <b>|hello|</b> in both cases. It's hard to explain this without pictures, so I would encourage you to draw out the DOM tree and do a walk to the next innermost child of the start container and the previous innermost child of the end container.
Once you have nestled your way down, you can start to walk UP the tree to find a bold tag. This is simply node.contains('b');
Do that for both the start and end node, then see if you're pointing to the same bold tag, and that the text content is the same as your selection.
If so, you have found your bold tag to nuke.
It's a non-trivial amount of code, and it requires a bunch of testing due really wonky edge cases with setting the selection and range in browsers. I have fought contenteditable for a long time :-) and I wish you the best of luck

Using jQuery to get the content of div.class to replace content of title tag

I need to work around a limitation on my company's platform where pages can only be rendered with the filename as the title. I don't want to change my file-names to have values like, "This page title with spaces, and maybe illegal characters", because I don't want my URLs to have a bunch %20's and illegal characters in them, so I've been trying to figure out how to use the contents of another section of the page over which I do have control - the breadcrumb - as the "title".
I've been trying to use jQuery's .get and .replaceWith to replace the contents of the title tag with the contents of span.ms-pagetitle, which contains the part I can edit, but I'm a jQuery noob, and just haven't been able to suss it out.
This ought to do it:
$('title').text($('span.ms-pagetitle').text())
This should be run only once; usually inside a $(document).ready() function. The .ms-pagetitle element ought to have no children.
Two things are going on here:
$('span.ms-pagetitle').text() first selects any items matching span.ms-pagetitle. Hopefully there is just one, but it will grab them all. Use :first or another more specific selector to get the one you want. .text() will
Get the combined text contents of each element in the set of matched
elements, including their descendants, or set the text contents of the
matched elements.
...hence the idea to keep the span childless.
So that will result in a string of text.
$('title').text('string') will set the contents of a selected tag when passed a string (and get when used with no argument), so you are setting the selected title text as the contents of the <title> tag here.

:contains incorrectly selecting child elements

Currently I'm trying to select a link element with the jQuery :contains selector. This works when the contents of the link is just text. but it seems that when the element contains other HTML elements the :contains selector selects a child element instead. Example
HTML:
<b> two</b> this not bold <b>This</b> is a bold Link
from that html, I'm trying to select the link using this selector
jQuery:
var selector = "a:contains('<b> two</b> this not bold <b>This</b> is a bold Link')";
var returnObj = $(selector);
Instead of getting one returned object (the link), jQuery returns three objects:
the first bold element
the text this is not bold
the second bold element
the problem isn't the single quotes within the contains(), as I've tried with and without them.
This is just a simplified example of what I'm trying to do. In reality, I'm dynamically creating selectors based off of a link object a user clicks. I then store that selector in a database for use later (for my app to display content related to that link). Since I can get the contents of the link, I figured I'd just use a:contents() if the link doesn't have an id.
based off of these pages, I seem to have my syntax right:
http://docs.jquery.com/Tutorials:How_to_Get_Anything_You_Want_2
http://api.jquery.com/contains-selector/
Thoughts on how to get the link object returned? Thanks!
hope this isn't too stupid a question, I'm new to JS and jQuery.
As mentioned, :contains() is meant to select by text content only, not inner HTML.
If you must match the a element based on that text, strip out the <b> tags:
var selector = "a:contains(' two this not bold This is a bold Link')";
Otherwise, see if you can simplify this selection by using a contextual selector (e.g. select based on its surrounding elements, parents, siblings, etc), or assign it a class and select by that class instead.
On a side note, I'd consider this yet another jQuery bug (could be a parsing error in Sizzle). In your situation, :contains() is not supposed to return or create any elements; it's supposed to return no matches simply because the selector doesn't match your a element. I suspect what it's doing instead is treating the <b></b> tags as new elements, and creating them on the fly along with your a element, which is wrong because the tags are inside the argument string and meant to be taken literally...
First of all your selector text does not match the actual text in your html.
The selector includes the this not bold which is not present in the html.
Most importantly the :contains works with the text only.. so you should check for
$("a:contains('two this not bold This is a bold Link')");
It is a very inefficient way though, and you should better add a class to the elements you want to target and use that for targeting..

how to use js to do very basic syntax coloring?

here's a very simple js but i don't know where to begin.
in a html page, if some text is enclosed by angle brackets, like this:
〈some text〉
i want the text to be colored (but not the brackets).
in normal html, i'd code it like this
〈<span class="booktitle">some text</span>〉
So, my question is, how do i start to write such a js script that search the text and replace it with span tags?
some basic guide on how to would be sufficient. Thanks.
(i know i need to read the whole html, find the match perhaps using regex, then replace the page with the new one. But have no idea how that can be done with js/DOM. Do i need to traverse every element, get their inner text, do possible replacement? A short example would be greatly appreciated.)
It depends partially on how cautious you need to be not to disturb event handlers on the elements you're traversing. If it's your page and you're in control of the handlers, you may not need to worry; if you're doing a library or bookmarklet or similar, you need to be very careful.
For example, consider this markup:
<p>And <a href='foo.html'>the 〈foo〉 is 〈bar〉</a>.</p>
If you did this:
var p = /* ...get a reference to the `p` element... */;
p.innerHTML = p.innerHTML.replace(/〈([^〉]*)〉/g, function(whole, c0) {
return "〈<span class='booktitle'>" + c0 + "</span>〉";
});
(live example) (the example uses unicode escapes and HTML numeric entities for 〈 and 〉 rather than the literals above, because JSBin doesn't like them raw, presumably an encoding issue)
...that would work and be really easy (as you see), but if there were an event handler on the a, it would get blown away (because we're destroying the a and recreating it). But if your text is uncomplicated and you're in control of the event handlers on it, that kind of simple solution might be all you need.
To be (almost) completely minimal-impact will require walking the DOM tree and only processing text nodes. For that, you'd be using Node#childNodes (for walking through the DOM), Node#nodeType (to know what kind of node you're dealing with), Node#nodeValue (to get the text of a text node), Node#splitText (on the text nodes, to split them in two so you can move one of them into your span), and Node#appendChild (to rehome the text node that you need to put in your span; don't worry about removing them from their parent, appendChild handles that for you). The above are covered by the DOM specification (v2 here, v3 here; most browsers are somewhere between the two; the links in the text above are to the DOM2 spec).
You'll want to be careful about this sort of case:
<p>The 〈foo <em>and</em> bar〉.</p>
...where the 〈 and the 〉 are in different text nodes (both children of the p, on either side of an em element); there you'll have to move part of each text node and the whole of the em into your span, most likely.
Hopefully that's enough to get you started.
If the text could be anywhere in the page, you have to traverse through each DOM element, split the text when you found a match using a regex.
I have put my code up there on jsfiddle: http://jsfiddle.net/thai/RjHqe/
What it does: It looks at the node you put it in,
If it's an element, then it looks into every child nodes of it.
If it's a text node, it finds the text enclosed in 〈angle brackets〉. If there is a match (look at the first match only), then it splits the text node into 3 parts:
left (the opening bracket and also text before that)
middle (the text inside the angle bracket)
right (the closing bracket and text after it)
the middle part is wrapped inside the <span> and the right part is being looked for more angle brackets.

Categories