I need to show a toolbar ABOVE a text selection in an editor. That's easy.
Unfortunately Safari on iOS seems to prefer opening its copy/paste/formatting context menu above text selections as well. But Facebook seems to have figured out a way to avoid this:
But after spending almost two hours with a remote debugger trying to figure out how the heck they achieved this, I'm giving up. I just can't figure it out.
I have built a very barebone prototype which displays a toolbar above a text selection (attached below). This works fine on my MacBook (using Chrome in this case):
But as the next screenshot demonstrates, Safari's context menu on an iPad conflicts with my toolbar. They open in almost the same position, making it impossible to interact with my toolbar.
I'm really keen to know how Facebook solved it. Any help would be greatly appreciated. Thank you :-)
Barebone prototype:
var toolbar = document.querySelector("div.Toolbar");
toolbar.style.display = "none";
document.onselectionchange = function()
{
var sel = getSelection();
if (sel.rangeCount > 0)
{
var ran = sel.getRangeAt(0);
if (ran.collapsed === false)
{
var rect = ran.getBoundingClientRect();
var rectTop = rect.top;
var rectCenter = rect.left + (rect.width / 2);
toolbar.style.display = "";
toolbar.style.left = (rectCenter - (toolbar.offsetWidth / 2)) + "px";
toolbar.style.top = (rectTop - toolbar.offsetHeight) + "px";
if (parseFloat(toolbar.style.top) < 0)
{
toolbar.style.top = "0px";
}
if (parseFloat(toolbar.style.left) < 0)
{
toolbar.style.left = "0px";
}
}
else
{
toolbar.style.display = "none";
}
}
else
{
toolbar.style.display = "none";
}
}
body
{
padding: 2em;
}
div.Toolbar
{
display: inline-block;
position: fixed;
background: orange;
box-shadow: 0px 0px 0.2em #333;
padding: 0.3em;
}
div[contenteditable="true"]
{
width: 600px;
height: 300px;
border: 1px solid gray;
padding: 0.5em;
}
<div class="Toolbar">
Buttons go here
</div>
<div contenteditable="true">
Try selecting some of this text in the editor. The toolbar shows up above the text selection as intended. But on an iPad we also get Safari's context menu which opens on top of our custom toolbar. Facebook has successfully solved this somehow - but how the heck did they do it?
</div>
I found the solution.
Quick answer: Add a "button" with an onclick or onmousedown handler.
Expanding on the solution:
I decided to go ahead with my project and simply attach the toolbar at the top of the viewport instead. But as the project progressed, I wanted to take a second shot at this problem - and this time the Context Menu did not overlap with my custom toolbar. It turned out that the mere presence of an element (<div> element representing a button in this case) with an onclick or onmousedown handler registered, was enough to make Safari realize that this is probably something where UI collisions should be avoided. Quite frankly I'm not a fan of this kind of "A.I", but at least there is an explaination now.
var button = document.createElement("div");
button.className = "ToolbarButton ToolbarButtonBold";
button.onmousedown = function() { /* ... */ }; // This saves the day
toolbar.appendChild(button);
I have a page with 60 different inputs. They are all numbers and need to be checked against one of the inputs. If that input is 3 greater than the parent div changes to red. Everything works beautifully except if I try to print the style I give through my javascript function (document.getElementById(classid).style.backgroundColor = "red";) does not display print. How do I get the page to print with the style given by the function?
<script type="text/javascript">
function CheckThisNumber(val, id){
var x = document.getElementById("a6").value;
var y = Number(x) +3;
var classid = "p" + id;
if((val)>=y) {
document.getElementById(classid).style.backgroundColor = "red"; }
else { document.getElementById(classid).style.backgroundColor = "white"; }
}
</script>
One of the many inputs:
<div class="a1" id="pa1">
<strong>A1</strong><br><input type="number" name="a1" id="a1" style="width:95%" onKeyUp="CheckThisNumber(this.value,this.id)">
</div>
As I said in the comments, it is based on the browser's print settings. You can enable it in the browser's settings and it would just print them normally, but by default it is disabled to save ink.
Some browser support a non-standard CSS to force the BG to print
-webkit-print-color-adjust: exact
info: https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-print-color-adjust
And another fall back would be to use box shadows
.redBG { box-shadow: inset 0 0 0 100px #FF0000; }
.whiteBG { box-shadow: inset 0 0 0 100px #FFFFFF; }
If you are using it on a large textarea, you might need to set the 100px to a much larger value.
I'm trying to determine if an element has a background explicitly set. I figured I could just check to see if .css('background')* was set, however, it's inconsistent between browsers. For example, chrome shows an element without a background set as
background: rgba(0, 0, 0, 0) none repeat scroll 0% 0% / auto padding-box border-box
background-color: rgba(0, 0, 0, 0)
background-image: none
whereas IE8 shows
background: undefined
background-color: transparent
background-image: none
(test case here)
*(shorthand properties of CSS aren't supported for getting rendered styles in jQuery)
Short of handling each separate case is there a better way to detect this?
temporary element approach
It's not ideal, but you could create a temporary element when your js initiates, insert it somewhere hidden in the document (because if you don't you get empty styles for webkit browsers) and then read the default background style set for that element. This would give you your baseline values. Then when you compare against your real element, if they differ you know that the background has been set. Obviously the downside to this method is it can not detect if you specifically set the background to the baseline state.
var baseline = $('<div />').hide().appendTo('body').css('background');
var isBackgroundSet = ( element.css('background') != baseline );
If you wanted to avoid possible global styles on elements, that would break the system i.e:
div { background: red; }
... you could use the following instead, but I doubt if it would work so well with older browsers:
var baseline = $('<fake />').hide().appendTo('body').css('background');
background
I spent some time with a similar issue - attempting to get the original width value from an element when set to a percentage. Which was much trickier than I had assumed, in the end I used a similar temporary element solution. I also expected, as Rene Koch does above, that the getComputedStyle method would work... really annoyingly it doesn't. Trying to detect the difference between the source CSS world and the runtime CSS world is a difficult thing.
This should work:
function isBGDefined(ele){
var img = $(ele).css('backgroundImage'),
col = $(ele).css('backgroundColor');
return img != 'none' || (col != 'rgba(0, 0, 0, 0)' && col != 'transparent');
};
DEMO
I didn't bother to test against the background property because in the end, it will change the computed styles of either backgroundImage and/or backgroundColor.
Here's the code run against your test case (with another added): http://jsfiddle.net/WG9MC/4/
this article explains how:
http://robertnyman.com/2006/04/24/get-the-rendered-style-of-an-element/
function getStyle(oElm, strCssRule){
var strValue = "";
if(document.defaultView && document.defaultView.getComputedStyle){
strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
}
else if(oElm.currentStyle){
strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
return p1.toUpperCase();
});
strValue = oElm.currentStyle[strCssRule];
}
return strValue;
}
Using the approach suggested by #pebbl I wrote a small jQuery function, hasBack(), to determine if an element has its background set.
$.fn.hasBack = function()
{
var me = $.fn.hasBack;
if(!me.cache)
{
// get the background color and image transparent/none values
// create a temporary element
var $tmpElem = $('<div />').hide().appendTo('body');
$.fn.hasBack.cache = {
color: $tmpElem.css('background-color'),
image: $tmpElem.css('background-image')
};
$tmpElem.remove();
}
var elem = this.eq(0);
return !(elem.css('background-color') === me.cache.color && elem.css('background-image') === me.cache.image);
}
This was tested in Chrome v22, Firefox v15, Opera 12.1, IE9, IE9 set to browser modes 9 compat, 9, 8, 7 and quirks mode.
Test case here.
So I've got 7 to 12 divs all of the same style which are floated left. I am looking for a css selector for all the ones that flow to a second row. I am pretty sure this is not possible with standard css, but I am wondering if anyone knows any jQuery or other tricks that could get this done. Thanks a bunch!
As you say your self, there is no way to do that with CSS (that I know of). However, it can be done quite easily with jQuery.
One way to do it would be to use a combination of filter and offset to only keep the elements with higher top offset than the others (those who doesn't fit on the first row).
var $elm = $(".yourSelector"); // Use your selector here
var $secondRowElms = $elm.filter(function () {
// Compare each item with the first item, to see if it has higher offset
return ($elm.first().offset().top < $(this).offset().top);
});
Here is a demo as well: http://jsfiddle.net/8ppJP/1/
try this:
$('.divs:not(:first)').filter(function(){
return $(this).position().top - $(this).height() == 0
}).nextAll().andSelf().addClass('next')
DEMO
var $divs = $('.container .sub');
var arrOffsetTops = [];
$divs.each(function(index,element){
arrOffsetTops[index]=element.position().top;
arrOffsetTops[index].newLine = (index==0 ? true : false);
if(index > 0) {
if(arrOffsetTops[index] > arrOffsetTops[index-1]) {
// it's on another line
arrOffsetTops[index].newLine = true;
}
}
});
You can then loop through your array, with the index and check for .newLine == true to do whatever you need to do with the div.
UPDATE:
An example of how you could use this:
var divCount = $divs.length;
for(var i=0; i<divCount; i++) {
if(true == arrOffsetTops[ i ].newLine) {
$divs.eq( i ).addClass('newline-marker');
}
}
.newline-marker {
-webkit-box-shadow:0 0 10px black;
-khtml-box-shadow:0 0 10px black;
-moz-box-shadow:0 0 10px black;
-o-box-shadow:0 0 10px black;
-ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=2, Direction=0, Color='#000000')";
filter: progid:DXImageTransform.Microsoft.Shadow(Strength=2, Direction=0, Color='#000000');
box-shadow:0 0 10px black;
zoom:1;
}
I am looking for a Javascript autocomplete implementation which includes the following:
Can be used in a HTML textarea
Allows for typing regular text without invoking autocomplete
Detects the # character and starts autocomplete when it is typed
Loads list of options through AJAX
I believe that this is similar to what Twitter is doing when tagging in a tweet, but I can't find a nice, reusable implementation.
A solution with jQuery would be perfect.
Thanks.
Another great library which solves this problem At.js (deprecated)
Source
Demo
They are now suggesting the Tribute library
https://github.com/zurb/tribute
Example
I'm sure your problem is long since solved, but jquery-textcomplete looks like it would do the job.
Have you tried this
GITHUB: https://github.com/podio/jquery-mentions-input
DEMO/CONFIG: http://podio.github.io/jquery-mentions-input/
It is pretty simple to implement.
I've created a Meteor package for this purpose. Meteor's data model allows for fast multi-rule searching with custom rendered lists. If you're not using Meteor for your web app, (I believe) you unfortunately won't find anything this awesome for autocompletion.
Autocompleting users with #, where online users are shown in green:
In the same line, autocompleting something else with metadata and bootstrap icons:
Fork, pull, and improve:
https://github.com/mizzao/meteor-autocomplete
Try this:
(function($){
$.widget("ui.tagging", {
// default options
options: {
source: [],
maxItemDisplay: 3,
autosize: true,
animateResize: false,
animateDuration: 50
},
_create: function() {
var self = this;
this.activeSearch = false;
this.searchTerm = "";
this.beginFrom = 0;
this.wrapper = $("<div>")
.addClass("ui-tagging-wrap");
this.highlight = $("<div></div>");
this.highlightWrapper = $("<span></span>")
.addClass("ui-corner-all");
this.highlightContainer = $("<div>")
.addClass("ui-tagging-highlight")
.append(this.highlight);
this.meta = $("<input>")
.attr("type", "hidden")
.addClass("ui-tagging-meta");
this.container = $("<div></div>")
.width(this.element.width())
.insertBefore(this.element)
.addClass("ui-tagging")
.append(
this.highlightContainer,
this.element.wrap(this.wrapper).parent(),
this.meta
);
var initialHeight = this.element.height();
this.element.height(this.element.css('lineHeight'));
this.element.keypress(function(e) {
// activate on #
if (e.which == 64 && !self.activeSearch) {
self.activeSearch = true;
self.beginFrom = e.target.selectionStart + 1;
}
// deactivate on space
if (e.which == 32 && self.activeSearch) {
self.activeSearch = false;
}
}).bind("expand keyup keydown change", function(e) {
var cur = self.highlight.find("span"),
val = self.element.val(),
prevHeight = self.element.height(),
rowHeight = self.element.css('lineHeight'),
newHeight = 0;
cur.each(function(i) {
var s = $(this);
val = val.replace(s.text(), $("<div>").append(s).html());
});
self.highlight.html(val);
newHeight = self.element.height(rowHeight)[0].scrollHeight;
self.element.height(prevHeight);
if (newHeight < initialHeight) {
newHeight = initialHeight;
}
if (!$.browser.mozilla) {
if (self.element.css('paddingBottom') || self.element.css('paddingTop')) {
var padInt =
parseInt(self.element.css('paddingBottom').replace('px', '')) +
parseInt(self.element.css('paddingTop').replace('px', ''));
newHeight -= padInt;
}
}
self.options.animateResize ?
self.element.stop(true, true).animate({
height: newHeight
}, self.options.animateDuration) :
self.element.height(newHeight);
var widget = self.element.autocomplete("widget");
widget.position({
my: "left top",
at: "left bottom",
of: self.container
}).width(self.container.width()-4);
}).autocomplete({
minLength: 0,
delay: 0,
maxDisplay: this.options.maxItemDisplay,
open: function(event, ui) {
var widget = $(this).autocomplete("widget");
widget.position({
my: "left top",
at: "left bottom",
of: self.container
}).width(self.container.width()-4);
},
source: function(request, response) {
if (self.activeSearch) {
self.searchTerm = request.term.substring(self.beginFrom);
if (request.term.substring(self.beginFrom - 1, self.beginFrom) != "#") {
self.activeSearch = false;
self.beginFrom = 0;
self.searchTerm = "";
}
if (self.searchTerm != "") {
if ($.type(self.options.source) == "function") {
self.options.source(request, response);
} else {
var re = new RegExp("^" + escape(self.searchTerm) + ".+", "i");
var matches = [];
$.each(self.options.source, function() {
if (this.label.match(re)) {
matches.push(this);
}
});
response(matches);
}
}
}
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
self.activeSearch = false;
//console.log("#"+searchTerm, ui.item.label);
this.value = this.value.replace("#" + self.searchTerm, ui.item.label) + ' ';
self.highlight.html(
self.highlight.html()
.replace("#" + self.searchTerm,
$("<div>").append(
self.highlightWrapper
.text(ui.item.label)
.clone()
).html()+' ')
);
self.meta.val((self.meta.val() + " #[" + ui.item.value + ":]").trim());
return false;
}
});
}
});
body, html {
font-family: "lucida grande",tahoma,verdana,arial,sans-serif;
}
.ui-tagging {
position: relative;
border: 1px solid #B4BBCD;
height: auto;
}
.ui-tagging .ui-tagging-highlight {
position: absolute;
padding: 5px;
overflow: hidden;
}
.ui-tagging .ui-tagging-highlight div {
color: transparent;
font-size: 13px;
line-height: 18px;
white-space: pre-wrap;
}
.ui-tagging .ui-tagging-wrap {
position: relative;
padding: 5px;
overflow: hidden;
zoom: 1;
border: 0;
}
.ui-tagging div > span {
background-color: #D8DFEA;
font-weight: normal !important;
}
.ui-tagging textarea {
display: block;
font-family: "lucida grande",tahoma,verdana,arial,sans-serif;
background: transparent;
border-width: 0;
font-size: 13px;
height: 18px;
outline: none;
resize: none;
vertical-align: top;
width: 100%;
line-height: 18px;
overflow: hidden;
}
.ui-autocomplete {
font-size: 13px;
background-color: white;
border: 1px solid black;
margin-bottom: -5px;
width: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea></textarea>
http://jsfiddle.net/mekwall/mcWnL/52/
This link will help you
I could not find any solution that matched my requirements perfectly, so I ended up with the following:
I use the jQuery keypress() event to check for the user pressing the # character.
If this is the case, a modal dialog is shown using jQuery UI. This dialog contains an autocomplete text field (many options can be used here, but I recommmend jQuery Tokeninput)
When the user selects an option in the dialog, a tag is added to the text field and the dialog is closed.
This is not the most elegant solution, but it works and it does not require extra keypresses compared to my original design.
Edit
So basically, we have our large text box where the user can enter text. He should be able to "tag" a user (this just means inserting #<userid> in the text). I attach to the jQuery keyup event and detect the # character using (e.which == 64) to show a modal with a text field for selecting the users to tag.
The meat of the solution is simply this modal dialog with a jQuery Tokeninput text box. As the user types here, the list of users is loaded through AJAX. See the examples on the website for how to use it properly. When the user closes the dialog, I insert the selected IDs into the large text box.
Recently i had to face this problem and this is how i nailed down...
Get the string index at the cursor position in the textarea by using selectionStart
slice the string from index 0 to the cursor position
Insert it into a span (since span has multiple border boxes)
Get the dimensions of the border box using element.getClientRects() relative to the view port. (here is the MDN Reference)
Calculate the top and left and feed it to the dropdown
This works in all latest browsers. haven't tested at old ones
Here is Working bin
Another plugin which provides similar functionality:
AutoSuggest
You can use it with custom triggers or you can use it without any triggers. Works with input fields, textareas and contenteditables. And jQuery is not a dependency.
I would recommend the textcomplete plugin. No jQuery dependency. You may need bootstrap.css to refer, but I recommend to write your own CSS, lighter and simple.
Follow the below steps to give it a try
npm install #textcomplete/core #textcomplete/textarea
Bind it to your input element
const editor = new TextareaEditor(inputEl);
const textcomplete = new Textcomplete(editor, strategy, options);
Set strategy(how to fetch suggestion list) and options(settings to configure the suggestions) according to your need.
JS version
Angular Version
This small extension seems to be the closest at least in presentation to what was asked. Since it's small, it can be easily understood and modified. http://lookapanda.github.io/jquery-hashtags/
THIS should work. With regards to the # kicking off the search, just add (dynamically or not) the symbol to the beginning of each possible search term.