Tooltip triggered by text selection - javascript

I am looking to create a tooltip that is triggered by the selection of text (left-click dragging over text). Preferably by creating a JQuery plugin.
My ultimate goal is when a user selects/highlights a sentence, phrase, paragraph, it will trigger a tooltip. The tooltip will contain social sharing buttons that will allow a user to post the selection to their personal profile status.
So if you like a particular quote, you can select it, click share to twitter, it will call the twitter api to post the selection (if over the 140 characters it will add an ellipsis) with a shortened url back to the page of the selection.
Obviously this is going to take a bit of development, but as a front-end designer I just need to get the ball rolling. Thank you for any help you can provide.
An example of the functionality I desire is similar to how the apture extension functions:
http://www.apture.com/extension/

Here's a little demo: http://jsfiddle.net/sje397/fNt22/
It just monitors the selected text on a timer and tracks the mouse position, then creates a hovering div with a 'share' button at the mouse position whenever the selected text is not empty.
var mousePos;
$(document).mousemove(function (e) {
mousePos = {left: e.pageX + 20, top: e.pageY + 20};
});
var selectedText = '';
function getSelectedText(){
if(window.getSelection){
return window.getSelection().toString();
}
else if(document.getSelection){
return document.getSelection();
}
else if(document.selection){
return document.selection.createRange().text;
}
}
function checkSelectionChanged() {
var current = getSelectedText();
if(current != selectedText) {
selectedText = current;
if(selectedText != '') {
$('#quote #text').text(selectedText);
$('#quote').offset(mousePos);
$('#quote').show();
} else {
$('#quote').hide();
}
}
}
setInterval(checkSelectionChanged, 1000);

I already wrote this plugin :)
http://www.latentmotion.com/search-and-share
You're welcome to adapt upon it all you like, as long as you give credit.
I've also written a somewhat slicker one, but never fully vetted it (it's kinda in alpha):
http://seox.org/pro-beta.html

Related

Caret disappears in Firefox when saving its position with Rangy

This happens only in Firefox.
Important: I am saving the caret's position with rangy.saveSelection():
when click the content editable div
on keyup
when adding an external html element (as a node) to the content editable div
I need the position saved constantly through multiple means to be able to insert html elements on click (I have some tags).
When I click in the contentEditable div and the div is empty (first focus, let's say), I cannot see the caret unless I start typing. If the caret is at the end, I cannot see it either.
Another weird behaviour is that I cannot use the arrows to navigate between the text in the contentEditable div.
If I remove the functions which (constantly) saves the caret's position (on input, click etc.) the caret returns to normal (the caret is visible).
The problem appears when I start saving the position of the caret. Clearly I should be doing some sort of reset or a clear.. but from what I understand, those seem counterproductive (as from my understanding they destroy the saved caret location).
The content editable div
<div class="input__boolean input__boolean--no-focus">
<div
#keydown.enter.prevent
#blur="addPlaceholder"
#keyup="saveCursorLocation($event); fixDelete(); clearHtmlElem($event);"
#input="updateBooleanInput($event); clearHtmlElem($event);"
#paste="pasted"
v-on:click="clearPlaceholder(); saveCursorLocation($event);"
class="input__boolean-content"
ref="divInput"
contenteditable="true">Cuvinte cheie, cautare booleana..</div>
</div>
My methods/functions
inputLength($event){
this.input_length = $event.target.innerText.length;
if(this.input_length == 0)
this.typed = false;
},
addPlaceholder(){
if(this.input_length == 0 && this.typed == false){
this.$refs.divInput.innerHTML = 'Cuvinte cheie, cautare booleana..'
}
},
clearPlaceholder(){
if(this.input_length == 0 && this.typed == false){
this.$refs.divInput.innerHTML = '';
}
},
updateBooleanInput($event){
this.typed = true;
this.inputLength($event);
},
saveCursorLocation($event){
if($event.which != 8){
if(this.saved_sel)
rangy.removeMarkers(this.saved_sel)
this.saved_sel = rangy.saveSelection();
}
// if(this.input_length == 0 && this.typed == false){
// var div = this.$refs.divInput;
// var sel = rangy.getSelection();
// sel.collapse(div, 0);
// }
},
insertNode: function(node){
var selection = rangy.getSelection();
var range = selection.getRangeAt(0);
range.insertNode(node);
range.setStartAfter(node);
range.setEndAfter(node);
selection.removeAllRanges();
selection.addRange(range);
},
addBooleanTag($event){
// return this.$refs.ChatInput.insertEmoji($event.img);
this.$refs.divInput.focus();
console.log(this.input_length);
if(this.typed == false & this.input_length == 0){
this.$refs.divInput.innerHTML = ''
var space = '';
this.typed = true
this.saveCursorLocation($event);
}
rangy.restoreSelection(this.saved_sel);
var node = document.createElement('img');
node.src = $event.img;
node.className = "boolean-button--img boolean-button--no-margin";
node.addEventListener('click', (event) => {
// event.currentTarget.node.setAttribute('contenteditable','false');
this.$refs.divInput.removeChild(node);
})
this.insertNode(node);
this.saveCursorLocation($event);
},
clearHtmlElem($event){
var i = 0;
var temp = $event.target.querySelectorAll("span, br");
if(temp.length > 0){
for(i = 0; i < temp.length; i++){
if(!temp[i].classList.contains('rangySelectionBoundary')){
if (temp[i].tagName == "br"){
temp[i].parentNode.removeChild(temp[i]);
} else {
temp[i].outerHTML = temp[i].innerHTML;
}
}
}
}
},
pasted($event){
$event.preventDefault();
var text = $event.clipboardData.getData('text/plain');
this.insert(document.createTextNode(text));
this.inputLength($event);
this.typed == true;
},
insert(node){
this.$refs.divInput.focus();
this.insertNode(node);
this.saveCursorLocation($event);
},
As you can see in the saveCursorLocation(), I was trying to solve the scenario in which you click in the contentEditable div and there's no caret - which is confusing for the user.
// if(this.input_length == 0 && this.typed == false){
// var div = this.$refs.divInput;
// var sel = rangy.getSelection();
// sel.collapse(div, 0);
// }
It was a dead end - most likely because of my poor understanding of Rangy and how should I use those functions.
Expected behaviour vs actual results on Firefox
When I click on the contentEditable div I expect the caret to appear (while in the background to save my position). When typing, I expect the caret to appear after the last typed character while also on keyup to save my caret's position. Also I expect to be able to navigate the text via left/right arrows and see the caret when doing so.
All of these are generated by
v-on:click="..... saveCursorLocation($event);"
and
#keyup="saveCursorLocation($event);....."
If anybody believes that it would be helpful, I can record the content editable div and its behaviour in Firefox.
EDIT: I managed to isolate the problem and reproduce it into a JSFiddle - https://jsfiddle.net/Darkkz/6Landbj5/13.
What to look for?
Open the fiddle link in Firefox, then press one of the blue buttons (SI, SAU, NU) and then look at the input, the caret is not displayed.
Click the input, the caret is not displayed
While typing in the input,the caret is not displayed. Although, if you click in a word/in between content, the caret will be displayed
Apparently rangy's Selection Save and Restore module can't be used to keep track of the current selection while the user interacts with a contenteditable, like you want.
I digged into it a bit, and the problem is that rangy inserts hidden <span>s as markers, and updates the selection to be after the marker, instead of keeping it inside the #text node the user's editing:
<div contenteditable>
#text [This is something I typed <!-- selection is moved from here -->]
<span class="rangySelectionBoundary"/>
<!-- to here -->
</div>
Firefox has trouble displaying the caret in this scenario (I haven't found a bug about this specific issue, but here's a similar one where the caret is not displayed when the selection is between two <span>s).
Commenting this code out seems to fix the issue with the disappearing caret. It's unclear to me why that code is needed -- it was added before 1.0 in a large commit with its message saying: "Fixes for save/restore problems with control ranges and multiple range selections. Added demos for save/restore and CSS class applier modules." -- so I'm not comfortable to suggest fixing this in rangy (and since it's unmaintained for a few years, I don't have much hope in getting its author's input on this).
So I tried to figure out why you needed this in the first place, to suggest other solutions not involving rangy.saveSelection (for example, rangy's Text Range module provides getSelection().saveCharacterRanges(containerNode) that works without modifying the DOM.
It appears that you have a <div contenteditable> and some "buttons" (<span>s), clicking on which would insert some HTML at the caret position. The problem you were trying to solve was that when the "buttons" were clicked, the selection moved from the contenteditable into the button, and you were unable to detect the insert position.
Instead of storing and restoring the selection, you can instead make the buttons user-select: none - this will keep the caret in the contenteditable.
To test this, I commented out all references to rangy.saveSelection and rangy.restoreSelection and changed the this.$refs.divInput.focus(); call in the "button"'s onclick handler to run only when the contenteditable wasn't already focused by wrapping it in an if (!this.$refs.divInput.contains(document.activeElement)). See how this works in this updated fiddle:
https://jsfiddle.net/fjxsgvm2/

Replace text in textarea on his exact position [duplicate]

I'm trying to use Javascript to replace the selected text in an arbitrary selected TEXTAREA node in Chrome (! not a content editable div !) The code fragment I see repeated in lots of places to replace selected text basically does this:
var sel = window.getSelection();
var range = sel.getRangeAt(0);
range.insertNode( document.createTextNode("test "));
However, this does not work for input fields such as TEXTAREA or INPUT TYPE=TEXT. The text is inserted BEFORE the TEXTAREA instead of inside it.
There is an alternative method to modify the selection text inside a text area using textarea.selectionStart and textarea.selectionEnd. However, these require figuring out which textarea element is actually active/selected. Chrome/Webkit document.activeElement seems to be broken and has been broken for a long time. I can't figure out any workaround to find the "currently selected textarea". See the bug here...
http://code.google.com/p/chromium/issues/detail?id=14436
You can see a micro-demo of the problem I'm trying to solve here.
http://dj1.willowmail.com/~jeske/_drop/insertIssue/1.html
http://ajaxandxml.blogspot.com/2007/11/emulating-activeelement-property-with.html
Any thoughts on this?
Given a webpage with an arbitrary bit of text selected in an arbitrary TEXTAREA node, without knowing ahead of time what textarea the focus is in, how do I find the active textarea and replace the selected text with some other text?
(( FYI: I'm using this code in a Chrome extension. An in-page javascript content script is extending the page javascript, so I have no idea what the page structure is ahead of time. It needs to work for any webpage. ))
I think the problem you may be having is that the active element changes as a result of clicking the button before your code runs. If you instead use the mousedown event and prevent the default button action, it works fine in Chrome:
http://jsfiddle.net/b3Fk5/2/
It appears that as of 8/23/2012, Chrome does not properly support activeElement, as it is often set to "body" when it shouldn't be.
There may also be some challenges because in my chrome extension, right-clicking to get a context menu might be altering the activeElement.
The solution was to provide a focus handler to create a more reliable activeElement in Chrome, and then use direct interaction with the TEXTAREA to handle the selection replacement.
var dActiveElement = null;
function _dom_trackActiveElement(evt) {
if (evt && evt.target) {
dActiveElement = evt.target;
console.log("focus on: " + dActiveElement.nodeName +
" id: " + dActiveElement.id);
} else {
console.log("focus else..");
}
}
if (document.addEventListener) {
document.addEventListener("focus",_dom_trackActiveElement,true);
}
function insertTextAtCursor(text) {
console.log("insertTextAtCursor : " + text);
if (dActiveElement.nodeName.toUpperCase() == "TEXTAREA") {
console.log("selection in textarea! id: " + dActiveElement.id);
var ta = dActiveElement;
var saveSelectionStart = ta.selectionStart;
var newvalue = ta.value.slice(0,ta.selectionStart) +
text + ta.value.slice(ta.selectionEnd,ta.length);
console.log("output : " + newvalue + ", len : " + newvalue.length);
var newSelectionEnd = ta.selectionStart + text.length;
ta.value = newvalue;
ta.selectionStart = ta.selectionEnd = (newSelectionEnd);
}
}

Issue with IE when Inserting text into text area at cursor position

I am having a scenario where I need to put cursor on text area and then click on tree view node on the same page to have selected node's text into my textarea where I placed cursor just before clicking on tree node.
I got many answers on Stack overflow including below,
Inserting text in textarea at cursor position if cursor placed else text should append at last in IE
Inserting text after cursor position in text areа
Insert text into textarea with jQuery
How to insert text at the current caret position in a textarea
Inserting text at cursor in a textarea, with Javascript
How do I insert some text where the cursor is?
FF and Chrome works fine with above solutions but IE 8 or lower version fails (didn’t check with IE9) if focus is moved to some other control.
There is below or similar implementation for IE in almost all posts:
(function ($) {
$.fn.getCursorPosition = function () {
var el = $(this).get(0);
var pos = 0;
if ('selectionStart' in el) {
pos = el.selectionStart;
} else if ('selection' in document) {
el.focus();
var Sel = document.selection.createRange();
var SelLength = document.selection.createRange().text.length;
Sel.moveStart('character', -el.value.length);
pos = Sel.text.length - SelLength
}
return pos;
}
})(jQuery);
Note : We also can use if(el.selectionStart || el.selectionStart == '0') instead of if ('selectionStart' in el) and if(document.selection) instead of if ('selection' in document)
But this will fail when focus is moved to some other control first and then executing it. In my case focus will be moved to tree nodes when user will traverse through nodes.
Is there any solution for this scenario?
I am thinking to write onkeyup and onclick on text area and save its cursor position into hidden field so when focus is moved to some other control, i will have hidden field to get cursor position of text area. I will post that here later, meanwhile if anyone has some good idea then please share.
Thank you in advance
As i mentioned above i have below code to make it working in IE as well,
(Also thought about having only onblur instead of these 2 events but it didn’t work as focus already lost when execution comes into my code to set hidden variable)
Below implementation It is working fine in my case.
if ($("#myTextArea").get(0).selectionStart == undefined) {
$("#myTextArea").click(function (e) {
$("#hdnTextAreaPosition").val($("#myTextArea").getCursorPosition());
});
$("#myTextArea").keyup(function (e) {
$("#hdnTextAreaPosition").val($("#myTextArea").getCursorPosition());
});
}
Above events (keyup and click) are in global script and will be attached only in case of selectStart is undefined
function getTextAreaCursorPosition() {
if ($("#myTextArea").get(0).selectionStart == undefined) {
return $("#hdnTextAreaPosition").val();
}
else {
return $("#myTextArea").getCursorPosition();
}
}
function insertToMyTextArea(textToInsert) {
$("#myTextArea").focus();
var cursorPosition = getTextAreaCursorPosition();
var myContent = $("#myTextArea").val();
$("#myTextArea").val(myContent.substring(0, cursorPosition) + textToInsert + myContent.substring(cursorPosition));
}
insertToMyTextArea is the main function i am calling on click of tree node.
Please share your views if any alternative solution is available instead of having events.
I would suggest using my jQuery plug-in for this in conjunction with some extra stuff to save the textarea's selection or cursor position before the focus is lost.
I've covered this exact case in a previous answer:
https://stackoverflow.com/a/5890708/96100

How do I know when a new (Spry) accordion tab has been selected (similar to "on click")? I want to attach my own behaviors

I have an accordion set up to handle registration. I am wanting to validate the data entered on each panel when the user clicks on a different panel tab. I have a continue button on each panel, and am able to validate to my heart's content when the user uses that to go to the next panel.
My problem is that they can also click independently on the accordion tabs (and I want them to be able to skip around for editing purposes), but I would like to validate on those events too.
I've done a bunch of searching, but have not found a satisfactory answer. I am fairly new to Javascript and super-brand-new to jQuery, so please, if you have code snippets for me, be thorough in explaining them.
This should be a straightforward problem (similar to on-click, etc.). I'm quite surprised and frustrated that I haven't found an answer yet.
Edit:
Eric, I couldn't get this to work. Here is my version. I put it in the head section. I have some test code in there that has worked reliably for me in the past (changing the label on one of the tabs). I'm assuming this code has worked for you? Anyway, thanks for your help and I hope we've understood each other sufficiently.
// add capability to detect when accordion tab has been clicked
RegFormAccordion.addEventListener('click', function(e){
var btnElement;
(function findAccordionButton(el){
//e.target is the original element actually clicked on
//the event bubbles up to ancestor/parent nodes which is why you can listen at
//the container
if(!btnElement){ btnElement = e.target; }
else { btnElement = el; }
if(e.target.className !== 'accordionBtn')
{
findAccordionButton(btnElement.parentNode);
}
else
{
var curr_panel_index = RegFormAccordion.getCurrentPanelIndex();
document.getElementById("verify-reg-panel-label").innerHTML = "Index = " + curr_panel_index; // test code to see if it's even getting here
if (curr_panel_index == 1) // contact section
{
ValidateContact();
}
else if (curr_panel_index == 2) // payment section
{
ValidatePayment();
}
UpdateVerifyPanel(); // update contents of verification panel
}
})()
} );
Event delegation.
someAccordianContainer.addEventListener('click', function(e){
var btnElement;
(function findAccordionButton(el){
//e.target is the original element actually clicked on
//the event bubbles up to ancestor/parent nodes which is why you can listen at
//the container
if(!btnElement){ btnElement = e.target; }
else { btnElement = el; }
if(e.target.className !== 'accordionBtn'){
findAccordionButton(btnElement.parentNode);
}
else { doSomething(btnElement); }
})()
} );
You will have to normalize for IE<=8 however if you're supporting older browsers, since it uses a proprietary attachEvent method. Hit quirksmode.org for the details or just use something like jQuery or MooTools.
OK. I found the function that SpryAccordion.js uses to open a new panel and added my own code. Simple and elegant. It's not what I would normally do (usually I leave "libraries" alone). But if you make it editable without giving me another way to take needed control, then the hack is gonna happen.
If I need to use another accordion somewhere else on my website, I will have to double check that I have the correct accordion before invoking the hack. A trade-off I'm willing to make. It works perfectly now. Here is the code:
Spry.Widget.Accordion.prototype.openPanel = function(elementOrIndex)
{
var panelA = this.currentPanel;
var panelB;
if (typeof elementOrIndex == "number")
panelB = this.getPanels()[elementOrIndex];
else
panelB = this.getElement(elementOrIndex);
if (!panelB || panelA == panelB)
return null;
// Start Becca's code
var panelIndex = this.getPanelIndex(panelA);
if (panelIndex == 1) // contact info panel
{
if (ValidateContact())
UpdateVerifyPanel();
else
return null;
}
else if (panelIndex == 2) // payment info panel
{
if (ValidatePayment())
UpdateVerifyPanel();
else
return null;
}
// End Becca's code
var contentA = panelA ? this.getPanelContent(panelA) : null;
var contentB = this.getPanelContent(panelB);
...
...
...
};
Yes, all I wanted was the same control over the panel tabs as I have over my own user-defined buttons, to make sure I could both validate before moving on, and to update my verification screen after any edit the user makes, not just the ones where they happen to hit my continue button. I'm a happy camper. So glad I took a couple of days off.
I hope this helps someone get more control over their own accordions. So glad I don't have to do a crash-course on jQuery when all I want right now is to get my blasted website up.

How to get a range object that correspond to the position of a drop event?

I want to drag and drop images into an aloha editable field.
I am looking at the at.tapo.aloha.plugins.Image plugin which seems great.
However, i need to adapt this plugin in order to work with thumbnail. I drag the thumbnail and when I drop it into the aloha editable, the html code is modified on the fly in order to use the real image.
GENTICS.Aloha.EventRegistry.subscribe(GENTICS.Aloha, 'editableCreated', function(event, editable) {
var the_obj = editable.obj;
jQuery(editable.obj).bind('drop', function(event){
var e = event.originalEvent;
var files = e.dataTransfer.files;
var count = files.length;
if (count < 1) {
var node = e.dataTransfer.mozSourceNode;
if (node.tagName === 'IMG') {
var html = '<img ....>'; //build the real image html code
/// The current selection but I want the drop position
var range = GENTICS.Aloha.Selection.getRangeObject();
if (!jQuery.isEmptyObject(range)) {
GENTICS.Utils.Dom.insertIntoDOM(jQuery(html), range, the_obj);
}
return false;
}
return true;
}
}
It works ok when something is selected in the aloha field. I can get a range and insert the html into the DOM at the selection position.
However, I would like to get a range object that correspond to the place where my image is dropped. How to do that?
Thanks in advance for ideas.
There isn't an easy way that I know of to do this in general. You could obtain pixel coordinates for the drop point (possibly from a mousemove event) and then attempt to get a range for that point. For that task, the answer to the following question sums it up nicely:
Creating a collapsed range from a pixel position in FF/Webkit
Tim Down showed me that there is no easy way and I finally used a workaround:
GENTICS.Aloha.EventRegistry.subscribe(GENTICS.Aloha, 'editableCreated', function(event, editable) {
var the_obj = editable.obj;
jQuery(editable.obj).bind('drop', function(event){
setTimeout(function () {
//at this point the html is updated and can be postprocessed
//in order to turn thumbnails into the real image
//force the focus in order to make sure that the editable is activated
//this will cause the deactivated event to be triggered, and the content to be saved
the_obj.focus();
}, 0);
});
});

Categories