I would like to change the ForegroundColor of a selected text in a Google Docs with a keyboard shortcut.
I could make the "change the ForegroundColor" part (with a menu item bound to the function setColor() ), but not the "keyboard shortcut part".
I found this code but I have trouble implementing it:
$(document).keydown(function(e){
//CTRL + Q keydown combo
if(e.ctrlKey && e.keyCode == 81){
$( '#output' ).html("I've been pressed!");
}
})
My difficulties :
1) I am not sure where to place this code in my script editor (I tried to place it in the onOpen() function as below, but also above it without success).
2) I am not sure what the $(document) should refer to.
3) I am not sure what they mean by "having to click on / activate the sidebar first for that to happen".
function onOpen() {
var ui = DocumentApp.getUi();
ui.createMenu('My Menu')
.addItem('Color', 'setColor')
.addToUi();
var document = DocumentApp.getActiveDocument() // should it be here?
$(document).keydown(function(e){
//CTRL + Q keydown combo
if(e.ctrlKey && e.keyCode == 81){
SpreadsheetApp.getUi().alert('Hello, world!');
}
})
}
function setColor1() {
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var elements = selection.getRangeElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
// Only modify elements that can be edited as text; skip images and other non-text elements.
if (element.getElement().editAsText) {
var text = element.getElement().editAsText();
// Edit the selected part of the element, or the full element if it's completely selected.
if (element.isPartial()) {
text.setForegroundColor(element.getStartOffset(), element.getEndOffsetInclusive(), "#00FFFF");
} else {
text.setForegroundColor("#00FFFF");
}
}
}
}
}
In this line, the variable 'document' is an instance of the 'Document' object as defined in Google Apps Script documentation.
var document = DocumentApp.getActiveDocument()
It's an abstraction over the Google Document sitting in your Drive that allows you to modify your Drive files using a set of methods described here https://developers.google.com/apps-script/reference/document/document
The line below is jQuery syntax.
$(document).keydown(function(e){}
jQuery is a JavaScript library for client-side development. It is used for navigating the HTML DOM tree of a webpage. $(document) is jQuery's representation of the DOM. More on jQuery http://jquery.com/
Because Google Apps Script runs on Google servers, not locally on your machine, there's no DOM tree for you to navigate. You are limited to GAS functions that are mapped to bits of code on Google servers. These two lines of code have nothing to do with each other and are not referring to the same document.
Same applies to events that you can listen to. While GAS does have some event handling capabilities, the list of events is quite short (see 'Simple Triggers' and Installable Triggers') https://developers.google.com/apps-script/guides/triggers/
UPDATE
You can achieve this by building your own custom UI using HtmlService. The caveat is that there seems to be no way to hand over control from Google Document to the UI element, so you'll have to manually set focus on the sidebar after each action.
Here's the example of how the .gs file in your project may look like. Treat this as your server app.
//creates html output from the template
function onOpen(){
var ui = DocumentApp.getUi();
var htmlOutput = HtmlService.createTemplateFromFile('sidebar').evaluate();
ui.showSidebar(htmlOutput);
}
function setColor1(){
//your code
}
Below is my client-side code for the sidebar template. You create the template in your Script Editor by clicking on File -> New -> Html file.
Call it sidebar or whatever you choose for the var htmlOutput in the .gsfile above. If you press 'Ctrl + Q', the sidebar will display a confirmation and call the setColor1() function in your .gs file.
More on calling server-side GAS functions from the client https://developers.google.com/apps-script/guides/html/reference/run
More on HtmlService
https://developers.google.com/apps-script/guides/html/
The obvious downside is that the sidebar must acquire focus first, so you always need to click on it after making a selection.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<p> Press Ctrl + Q to change the color of selected text </p>
<p id="log"> </p>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$(document).ready(function(){
$(document).keydown(function(e){
if(e.ctrlKey && e.keyCode == 81){
$('#log').html('you pressed Ctrl + Q');
google.script.run.setColor1();
}
});
});
</script>
</body>
</html>
Hope this helps!
Instead of using the sidebar as Anton Dementiev suggested, it's possible to use a modeless dialog box. It can be run from the function onOpen() or from a menu item.
Advantage of modeless dialog box over the sidebar: the dialog box is smaller and can be move anywhere on the screen (click on the title an drag it)
Inconvenient: it's easier to set the focus on the sidebar, because on the modeless dialog box, you have to click inside the content of the dialog (which can be quite small) to set the focus.
The .html is the same than the one Anton Dementiev gave, but the .gs is different:
function onOpen() {
var ui = DocumentApp.getUi();
ui.createMenu('My Menu')
.addItem('Open Color Dialog', 'dialogBox')
.addToUi();
var htmlOutput = HtmlService // alternative to the sidebar
.createHtmlOutputFromFile('sidebar')
.setWidth(50)
.setHeight(50);
DocumentApp.getUi().showModelessDialog(htmlOutput, 'Title');
}
function dialogBox() {
var htmlOutput = HtmlService
.createHtmlOutputFromFile('sidebar')
.setWidth(50)
.setHeight(50);
DocumentApp.getUi().showModelessDialog(htmlOutput, 'Title');
}
It's really to cumbersome to have to click in the side bar/modeless dialog box and it's sloe, so I am doing with Autohotkey.
Here is a .ahk script (source)
#IfWinActive ahk_exe chrome.exe ; the shortcut will only work on chrome
!^+p::
;PART 1: check if the docs is in full-screen (the script work with the mouse position)
; put the mouse on the right top corner of the screen, freeze the spy tool, copy past the relative position (1357, 6 ).
PixelGetColor ColorWin, 1357, 6 RGB ; get the color of this area
; In my case this part should be white in full screen
if (ColorWin!="0xFFFFFF") ; if it's white (= fullscreen is OFF)
send {f11}; then press f11 to activate fullscreen
#
PixelGetColor ColorText, 647, 86 RGB
;msgbox, 64, (%ColorText%) ; uncomment if needed for debug to get color to
; get the mouse position and the color of each one you want
if (ColorText="0x000000") { ; black
click,647, 86
click,712, 120
click, 786, 177 ; blue
}
else If (ColorText="0xFF0000") { ; blue
click,647, 86
click,712, 120
click, 767, 179 ; blue light
}
else IF (ColorText="0xE8864A") { ; blue light
click,647, 86
click,712, 120
click, 679, 176 ; red
}
else ;
{
click,647, 86
click,712, 120
click, 657, 151 ; black
}
return
For my first JavaScript application I am building a widget based designer, and use a variety of widgets based on SVG included in the main HTML body with object tags. I'm trying to associate a bootstrap tooltip when the user clicks on the widget object, however I'm getting strange errors that don't crop up when using tooltips on other non object HTML elements.
As an object tag swallows mouse events it's not possible to trigger a tooltip with a hover and the only combination I can get to work to show the tooltip is the following code (nothing else works, bootstrap complains about ):
document.getElementById(widgetName).setAttribute("title", param0);
setTimeout(function () {
$("#" + widgetName).tooltip("show");
}, 1); // attribute needs to be set after the next tick so the DOM is refreshed.
setTimeout(function () { $("#" + widgetName).tooltip("hide"); }, 2000); // 2 sec delay before hiding
This code shows the tooltip but errors out with Unable to get property 'originalTitle' of undefined or null reference in IE10 after the 2 second timeout to hide the tooltip. I can't use tooltip object options (eg. the delay option) as I get the same error. Same problem in Chrome although Chrome does not report the error (no hiding though).
I'm sure it has something to do with trying to use tooltips on object tags as tooltips work normally otherwise. However I'm fairly new to JavaScript so let me know if I'm doing something dumb, and the bootstrap/Jquery code is too complex for me to trace the error.
Using: HTML5, IE10, latest twitter bootstrap, visual studio 2012 web
Thanks for any help / pointers.
UPDATE: Added code that inserts the object into the DOM
var objWidget = document.createElement("object");
objWidget.type = "image/svg+xml";
objWidget.data = "widgets/" + widgetFile + "." + widgetExt // location of widget
objWidget.className = "widget"
var widgetName = "objWidget" + widgetNum;
targetDiv = "widgetContainer"
objWidget.id = widgetName;
document.getElementById(targetDiv).appendChild(objWidget); // Add to Div
var objID = document.getElementById(widgetName);
objID.addEventListener("load", function () { // access properties once the file is loaded
var objDoc = objID.contentDocument;
objDoc.addEventListener("dragstart", drag_start, false);
objDoc.addEventListener("dragover", drag_over, false)
objDoc.addEventListener("drop", drop, false)
objDoc.getElementById("widget").setAttribute("data-widgetName", widgetName); // set a data attribute on the SVG that points to the parent object as object that created the event isn't recorded in the event.currentTarget
objID.setAttribute("draggable", "true")
objID.style.setProperty("position", "absolute");
objID.style.setProperty("left", objLeft + "px");
objID.style.setProperty("top", objTop + "px");
objID.style.setProperty("z-index", widgets[widgetNum].zOrder);
objDoc.defaultView.loaded(widgetName); // run widget startup routine only if it isn't a new widget
objDoc.defaultView.addEventListener("click", widgetClick, false);
objDoc.defaultView.addEventListener("dblclick", widgetDblClick, false);
}, false);
i test your code (add the tooltips to the svg tag) see: http://bootply.com/64598 FF and chrome don't have a problem
The Setup
So, I have a contenteditable div -- I'm making a WYSIWYG editor: bold, italics, formatting, whatever, and most lately: inserting fancy images (in a fancy box, with a caption).
<a class="fancy" href="i.jpg" target="_blank">
<img alt="" src="i.jpg" />
Optional Caption goes Here!
</a>
The user adds these fancy images with a dialog I present them with: they fill out the details, upload the image, and then much like the other editor functions, I use document.execCommand('insertHTML',false,fancy_image_html); to plop it in at the user's selection.
Desired Functionality
So, now that my user can plop in a fancy image -- they need to be able to move it around.
The user needs to be able to click and drag the image (fancy box and all) to place it anywhere that they please within the contenteditable. They need to be able to move it between paragraphs, or even within paragraphs -- between two words if they want.
What gives me hope
Keep in mind -- in a contenteditable, plain old <img> tags are already blessed by the user-agent with this lovely drag-and-drop capability. By default, you can drag and drop <img> tags around wherever you please; the default drag-and-drop operation behaves as one would dream.
So, considering how this default behavior already works so smashingly on our <img> buddies -- and I only want to extend this behaviour a little bit to include a tad more HTML -- this seems like something that should be easily possible.
My Efforts Thus Far
First, I set up my fancy <a> tag with the draggable attribute, and disabled contenteditable (not sure if that's necessary, but it seems like it may as well be off):
<a class="fancy" [...] draggable="true" contenteditable="false">
Then, because the user could still drag the image out of the fancy <a> box, I had to do some CSS. I'm working in Chrome, so I'm only showing you the -webkit- prefixes, though I used the others too.
.fancy {
-webkit-user-select:none;
-webkit-user-drag:element; }
.fancy>img {
-webkit-user-drag:none; }
Now the user can drag the whole fancy box, and the little partially-faded click-drag representation image reflects this -- I can see that I'm picking up the entire box now :)
I've tried several combinations of different CSS properties, the above combo seems to make sense to me, and seems to work best.
I was hoping that this CSS alone would be enough for the browser to use the entire element as the draggable item, automagically granting the user the functionality I've been dreaming of... It does however, appear to be more complicated than that.
HTML5's JavaScript Drag and Drop API
This Drag and Drop stuff seems more complicated than it needs to be.
So, I started getting deep into DnD api docs, and now I'm stuck. So, here's what I've rigged up (yes, jQuery):
$('.fancy')
.bind('dragstart',function(event){
//console.log('dragstart');
var dt=event.originalEvent.dataTransfer;
dt.effectAllowed = 'all';
dt.setData('text/html',event.target.outerHTML);
});
$('.myContentEditable')
.bind('dragenter',function(event){
//console.log('dragenter');
event.preventDefault();
})
.bind('dragleave',function(event){
//console.log('dragleave');
})
.bind('dragover',function(event){
//console.log('dragover');
event.preventDefault();
})
.bind('drop',function(event){
//console.log('drop');
var dt = event.originalEvent.dataTransfer;
var content = dt.getData('text/html');
document.execCommand('insertHTML',false,content);
event.preventDefault();
})
.bind('dragend',function(event){
//console.log('dragend');
});
So here's where I'm stuck: This almost completely works. Almost completely. I have everything working, up until the very end. In the drop event, I now have access to the fancy box's HTML content that I'm trying to have inserted at the drop location. All I need to do now, is insert it at the correct location!
The problem is I can't find the correct drop location, or any way to insert to it. I've been hoping to find some kind of 'dropLocation' object to dump my fancy box into, something like dropEvent.dropLocation.content=myFancyBoxHTML;, or perhaps, at least, some kind of drop location values with which to find my own way to put the content there?
Am I given anything?
Am I doing it completely wrong? Am I completely missing something?
I tried to use document.execCommand('insertHTML',false,content); like I expected I should be able to, but it unfortunately fails me here, as the selection caret is not located at the precise drop location as I'd hope.
I discovered that if I comment out all of the event.preventDefault();'s, the selection caret becomes visible, and as one would hope, when the user prepares to drop, hovering their drag over the contenteditable, the little selection caret can be seen running along between characters following the user's cursor and drop operation -- indicating to the user that the selection caret represents the precise drop location. I need the location of this selection caret.
With some experiments, I tried execCommand-insertHTML'ing during the drop event, and the dragend event -- neither insert the HTML where the dropping-selection-caret was, instead it uses whatever location was selected prior to the drag operation.
Because the selection caret is visible during dragover, I hatched a plan.
For awhile, I was trying, in the dragover event, to insert a temporary marker, like <span class="selection-marker">|</span>, just after $('.selection-marker').remove();, in an attempt for the browser to constantly (during dragover) be deleting all selection markers and then adding one at the insertion point -- essentially leaving one marker wherever that insertion point is, at any moment. The plan of course, was to then replace this temporary marker with the dragged content which I have.
None of this worked, of course: I couldn't get the selection-marker to insert at the apparently visible selection caret as planned -- again, the execCommand-insertedHTML placed itself wherever the selection caret was, prior to the drag operation.
Huff. So what have I missed? How is it done?
How do I obtain, or insert into, the precise location of a drag-and-drop operation?
I feel like this is, obviously, a common operation among drag-and-drops -- surely I must have overlooked an important and blatant detail of some kind? Did I even have to get deep into JavaScript, or maybe there's a way to do this just with attributes like draggable, droppable, contenteditable, and some fancydancy CSS3?
I'm still on the hunt -- still tinkering around -- I'll post back as soon as I find out what I've been failing at :)
The Hunt Continues (edits after original post)
Farrukh posted a good suggestion -- use:
console.log( window.getSelection().getRangeAt(0) );
To see where the selection caret actually is. I plopped this into the dragover event, which is when I figure the selection caret is visibily hopping around between my editable content in the contenteditable.
Alas, the Range object that is returned, reports offset indices that belong to the selection caret prior to the drag-and-drop operation.
It was a valiant effort. Thanks Farrukh.
So what's going on here? I am getting the sensation that the little selection caret I see hopping around, isn't the selection caret at all! I think it's an imposter!
Upon Further Inspection!
Turns out, it is an imposter! The real selection caret remains in place during the entire drag operation! You can see the little bugger!
I was reading MDN Drag and Drop Docs, and found this:
Naturally, you may need to move the insertion marker around a dragover event as well. You can use the event's clientX and clientY properties as with other mouse events to determine the location of the mouse pointer.
Yikes, does this mean I'm supposed to figure it out for myself, based on clientX and clientY?? Using mouse coordinates to determine the location of the selection caret myself? Scary!!
I'll look into doing so tomorrow -- unless myself, or somebody else here reading this, can find a sane solution :)
Dragon Drop
I've done a ridiculous amount of fiddling. So, so much jsFiddling.
This is not a robust, or complete solution; I may never quite come up with one. If anyone has any better solutions, I'm all ears -- I didn't want to have to do it this way, but it's the only way I've been able to uncover so far. The following jsFiddle, and the information I am about to vomit up, worked for me in this particular instance with my particular versions of Firefox and Chrome on my particular WAMP setup and computer. Don't come crying to me when it doesn't work on your website. This drag-and-drop crap is clearly every man for himself.
jsFiddle: Chase Moskal's Dragon Drop
So, I was boring my girlfriend's brains out, and she thought I kept saying "dragon drop" when really, I was just saying "drag-and-drop". It stuck, so that's what I call my little JavaScript buddy I've created for handling these drag-and-drop situations.
Turns out -- it's a bit of a nightmare. The HTML5 Drag-and-Drop API even at first glance, is horrible. Then, you almost warm up to it, as you start to understand and accept the way it's supposed to work.. Then you realize what a terrifying nightmare it actually is, as you learn how Firefox and Chrome go about this specification in their own special way, and seem to completely ignore all of your needs. You find yourself asking questions like: "Wait, what element is even being dragged right now? How to do I get that information? How do I cancel this drag operation? How can I stop this particular browser's unique default handling of this situation?"... The answers to your questions: "You're on your own, LOSER! Keep hacking things in, until something works!".
So, here's how I accomplished Precise Drag and Drop of Arbitrary HTML Elements within, around, and between multiple contenteditable's. (note: I'm not going fully in-depth with every detail, you'll have to look at the jsFiddle for that -- I'm just rambling off seemingly relevant details that I remember from the experience, as I have limited time)
My Solution
First, I applied CSS to the draggables (fancybox) -- we needed user-select:none; user-drag:element; on the fancy box, and then specifically user-drag:none; on the image within the fancy box (and any other elements, why not?). Unfortunately, this was not quite enough for Firefox, which required attribute draggable="false" to be explicitly set on the image to prevent it from being draggable.
Next, I applied attributes draggable="true" and dropzone="copy" onto the contenteditables.
To the draggables (fancyboxes), I bind a handler for dragstart. We set the dataTransfer to copy a blank string of HTML ' ' -- because we need to trick it into thinking we are going to drag HTML, but we are cancelling out any default behavior. Sometimes default behavior slips in somehow, and it results in a duplicate (as we do the insertion ourselves), so now the worst glitch is a ' ' (space) being inserted when a drag fails. We couldn't rely on the default behavior, as it would fail to often, so I found this to be the most versatile solution.
DD.$draggables.off('dragstart').on('dragstart',function(event){
var e=event.originalEvent;
$(e.target).removeAttr('dragged');
var dt=e.dataTransfer,
content=e.target.outerHTML;
var is_draggable = DD.$draggables.is(e.target);
if (is_draggable) {
dt.effectAllowed = 'copy';
dt.setData('text/plain',' ');
DD.dropLoad=content;
$(e.target).attr('dragged','dragged');
}
});
To the dropzones, I bind a handler for dragleave and drop. The dragleave handler exists only for Firefox, as in Firefox, the drag-drop would work (Chrome denies you by default) when you tried to drag it outside the contenteditable, so it performs a quick check against the Firefox-only relatedTarget. Huff.
Chrome and Firefox have different ways of acquiring the Range object, so effort had to be put in to do it differently for each browser in the drop event. Chrome builds a range based on mouse-coordinates (yup that's right), but Firefox provides it in the event data. document.execCommand('insertHTML',false,blah) turns out to be how we handle the drop. OH, I forgot to mention -- we can't use dataTransfer.getData() on Chrome to get our dragstart set HTML -- it appears to be some kind of weird bug in the specification. Firefox calls the spec out on it's bullcrap and gives us the data anyways -- but Chrome doesn't, so we bend over backwards and to set the content to a global, and go through hell to kill all the default behavior...
DD.$dropzones.off('dragleave').on('dragleave',function(event){
var e=event.originalEvent;
var dt=e.dataTransfer;
var relatedTarget_is_dropzone = DD.$dropzones.is(e.relatedTarget);
var relatedTarget_within_dropzone = DD.$dropzones.has(e.relatedTarget).length>0;
var acceptable = relatedTarget_is_dropzone||relatedTarget_within_dropzone;
if (!acceptable) {
dt.dropEffect='none';
dt.effectAllowed='null';
}
});
DD.$dropzones.off('drop').on('drop',function(event){
var e=event.originalEvent;
if (!DD.dropLoad) return false;
var range=null;
if (document.caretRangeFromPoint) { // Chrome
range=document.caretRangeFromPoint(e.clientX,e.clientY);
}
else if (e.rangeParent) { // Firefox
range=document.createRange(); range.setStart(e.rangeParent,e.rangeOffset);
}
var sel = window.getSelection();
sel.removeAllRanges(); sel.addRange(range);
$(sel.anchorNode).closest(DD.$dropzones.selector).get(0).focus(); // essential
document.execCommand('insertHTML',false,'<param name="dragonDropMarker" />'+DD.dropLoad);
sel.removeAllRanges();
// verification with dragonDropMarker
var $DDM=$('param[name="dragonDropMarker"]');
var insertSuccess = $DDM.length>0;
if (insertSuccess) {
$(DD.$draggables.selector).filter('[dragged]').remove();
$DDM.remove();
}
DD.dropLoad=null;
DD.bindDraggables();
e.preventDefault();
});
Okay, I'm sick of this. I've wrote all I want to about this. I'm calling it a day, and might update this if I think of anything important.
Thanks everybody. //Chase.
Since I wanted to see this in a native JS solution I worked a bit to remove all jQuery dependencies. Hopefully it can help someone.
First the markup
<div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
WAITING FOR STUFF
</div>
<div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Block 1
</span>
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Second Blk
</span>
</div>
Then some helpers
function addClass( elem, className ){
var classNames = elem.className.split( " " )
if( classNames.indexOf( className ) === -1 ){
classNames.push( className )
}
elem.className = classNames.join( " " )
}
function selectElem( selector ){
return document.querySelector( selector )
}
function selectAllElems( selector ){
return document.querySelectorAll( selector )
}
function removeElem( elem ){
return elem ? elem.parentNode.removeChild( elem ) : false
}
Then the actual methods
function nativeBindDraggable( elems = false ){
elems = elems || selectAllElems( '.native_drag' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection already (as good as array)
for( let i = 0 ; i < elems.length ; i++ ){
// For every elem in list, attach or re-attach event handling
elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
elems[i].ondragstart = function(e){
if (!e.target.id){
e.target.id = (new Date()).getTime();
}
window.inTransferMarkup = e.target.outerHTML;
window.transferreference = elems[i].dataset.transferreference;
addClass( e.target, 'dragged');
};
};
}
function nativeBindWriteRegion( elems = false ){
elems = elems || selectAllElems( '.native_receiver' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection
for( let i = 0 ; i < elems.length ; i++ ){
elems[i].ondragover = function(e){
e.preventDefault();
return false;
};
elems[i].ondrop = function(e){
receiveBlock(e);
};
}
}
function receiveBlock(e){
e.preventDefault();
let content = window.inTransferMarkup;
window.inTransferMarkup = "";
let range = null;
if (document.caretRangeFromPoint) { // Chrome
range = document.caretRangeFromPoint(e.clientX, e.clientY);
}else if (e.rangeParent) { // Firefox
range = document.createRange();
range.setStart(e.rangeParent, e.rangeOffset);
}
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange( range );
e.target.focus();
document.execCommand('insertHTML',false, content);
sel.removeAllRanges();
// reset draggable on all blocks, esp the recently created
nativeBindDraggable(
document.querySelector(
`[data-transferreference='${window.transferreference}']`
)
);
removeElem( selectElem( '.dragged' ) );
return false;
}
And lastly instantiate
nativeBindDraggable();
nativeBindWriteRegion();
Below is the functioning snippet
function addClass( elem, className ){
var classNames = elem.className.split( " " )
if( classNames.indexOf( className ) === -1 ){
classNames.push( className )
}
elem.className = classNames.join( " " )
}
function selectElem( selector ){
return document.querySelector( selector )
}
function selectAllElems( selector ){
return document.querySelectorAll( selector )
}
function removeElem( elem ){
return elem ? elem.parentNode.removeChild( elem ) : false
}
function nativeBindDraggable( elems = false ){
elems = elems || selectAllElems( '.native_drag' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection already (as good as array)
for( let i = 0 ; i < elems.length ; i++ ){
// For every elem in list, attach or re-attach event handling
elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
elems[i].ondragstart = function(e){
if (!e.target.id){
e.target.id = (new Date()).getTime();
}
window.inTransferMarkup = e.target.outerHTML;
window.transferreference = elems[i].dataset.transferreference;
addClass( e.target, 'dragged');
};
};
}
function nativeBindWriteRegion( elems = false ){
elems = elems || selectAllElems( '.native_receiver' );
if( !elems ){
// No element exists, abort
return false;
}else if( elems.outerHTML ){
// if only a single element, put in array
elems = [ elems ];
}
// else it is html-collection
for( let i = 0 ; i < elems.length ; i++ ){
elems[i].ondragover = function(e){
e.preventDefault();
return false;
};
elems[i].ondrop = function(e){
receiveBlock(e);
};
}
}
function receiveBlock(e){
e.preventDefault();
let content = window.inTransferMarkup;
window.inTransferMarkup = "";
let range = null;
if (document.caretRangeFromPoint) { // Chrome
range = document.caretRangeFromPoint(e.clientX, e.clientY);
}else if (e.rangeParent) { // Firefox
range = document.createRange();
range.setStart(e.rangeParent, e.rangeOffset);
}
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange( range );
e.target.focus();
document.execCommand('insertHTML',false, content);
sel.removeAllRanges();
// reset draggable on all blocks, esp the recently created
nativeBindDraggable(
document.querySelector(
`[data-transferreference='${window.transferreference}']`
)
);
removeElem( selectElem( '.dragged' ) );
return false;
}
nativeBindDraggable();
nativeBindWriteRegion();
<div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
WAITING FOR STUFF
</div>
<div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Block 1
</span>
<span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
Second Blk
</span>
</div>
event dragstart; dataTransfer.setData("text/html", "<div class='whatever'></div>");
event drop:
var me = this; setTimeout(function () {
var el = me.element.getElementsByClassName("whatever")[0];
if (el) { //do stuff here, el is your location for the fancy img
}
}, 0);
To summarise above answers, the keys are event.parentNode and event.rangeOffset (firefox) and caretRangeFromPoint(event.clientX, event.clientY) (chrome).
Here is a minimal example:
span {
border: 1px solid red;
}
span:before {
content: "grab ";
background-color: #0f0;
}
<p contenteditable="true"
ondrop="sel = window.getSelection();
if (document.caretRangeFromPoint)
range = document.caretRangeFromPoint(event.clientX, event.clientY)
else {
sel.collapse(event.rangeParent,event.rangeOffset)
range = sel.getRangeAt(0)
}
range.insertNode(sp1)"
ondragover="
return false"
>This is a contenteditable paragraph. Grab the green field in the following span
<span draggable="True" id="sp1" ondragstart="
event.dataTransfer.setData('text/plain', this.innerText)">span</span>
and drag it inside this paragraph.
</p>
I am using zeroclipboard to add a "copy" link to each row in a fairly large list, within a user script. To accomplish that, I using a method similar to the one listed on this page, where the ZeroClipboard.Client() element for each row is created when the user mouses over the row. This is working great in FireFox, but not in Chrome.
Also as a note: I copied the contents of the ZeroClipboard.js file into the user script itself instead of including it in an external file.
Here is the markup that creates the copy button for each element
<span style="color:blue; text-decoration:underline; cursor:pointer" id="copy_'+id+'" class="CopyLink" link="'+url+'" onmouseover="clipboard.add(this)">Copy</span>
Here is the code segment that adds the clipboard's client object:
function main(){
window.clipboard = {
load: function (){
if(!clipboard.initialized){
ZeroClipboard.setMoviePath("http://www.swfcabin.com/swf-files/1343927328.swf");
clipboard.initialized=true;
console.log("Clipboard intialized");
}
},
add: function(element){
clipboard.load();
var clip = new ZeroClipboard.Client();
console.log('Clipboard client loaded: ' + element.id);
clip.glue(element, element.parentNode);
console.log('Clipboard glued: ' + element.id);
clip.setText(element.getAttribute('link'));
console.log('Clipboard text set: ' + element.getAttribute('link'));
clip.addEventListener('complete',function(client,text) {
console.log('Clipboard copied: ' + text);//doesn't fire in chrome
});
clip.addEventListener('load',function(client) {
console.log('Clipboard loaded: ' + element.getAttribute('link'));
});
}
}
//other code in user script including injecting above markup
//as well as contents of ZeroClipboard.js
window.ZeroClipboard = { ... }
}
var script = document.createElement("script");
script.appendChild(document.createTextNode('('+main+')()'));
(document.head || document.body || document.documentElement).appendChild(script);
In this block, every console.log fires in FireFox when I mouse over and click the copy span, but in chrome, all except the 'complete' listener fire. I was able to verify that ZeroClipboard is working in my Chrome by using the example on this page. I am also able to verify that the flash object is being added to the page in the correct location, but it is simply not responding to a click.
Since the zeroclipboard code is no longer being maintained according to the site, I'm hoping someone out there can help me out. I'm thinking there is possibly some issue with dynamically adding the embedded flash objects in chrome on mouseover, or perhaps some difference between user scripts in chrome vs firefox with greasemonkey? Any help would be greatly appreciated, thanks
I'm not sure the reason behind it but I have been running into this on Chrome as well. I had two zeroclipboard implementations, one that was visible on page load, and one that was only visible when the user opened a dialog. The one that was visible on page load worked as expected, but the other one didn't. In order to "solve" the issue, I had to render the zeroclipboard link, set its absolute position to be off the screen (-500 px), then add some javascript to move the link into place when the dialog opens. This is an ugly solution but I think is the only way to get it to work in Chrome. Your case is particularly hairy since you have lots of dynamic zeroclipboards on your page whereas I only had one, but it seems to me that there's no reason this won't work for you.
<!-- <script type="text/javascript" src="http://davidwalsh.name/demo/ZeroClipboard.js"></script> -->
function copyText(fieldName,buttonName){
var fieldNameTemp =fieldName;
var buttonNameTemp =buttonName;
var val = "";
try{
val = navigator.userAgent.toLowerCase();
}catch(e){}
var swfurl = "js/ZeroClipboard.swf";
setTimeout(function () {
ZeroClipboard.setMoviePath(swfurl);
var clip = new ZeroClipboard.Client();
clip.addEventListener('mousedown', function () {
clip.setText(document.getElementById(fieldNameTemp).value);
});
clip.addEventListener('complete', function (client, text) {
try{
if(val.indexOf("opera") > -1 || val.indexOf("msie") > -1 || val.indexOf("safari") > -1 || val.indexOf("chrome") > -1){
alert('Your text has been copied');
}
}catch(e){
alert('Please alert not use on fireFox');
}
});
clip.glue(buttonNameTemp);
}, 2000);
}
I have one asp.net application, in which i have to disable or make it as read only to the Paste option from the context menu. But i searched in internet. I didn't get one exact method. Please help me for resolve this issue. Thanks in advance
The short answer is that you can't do this.
The user's browser is their own, and as such they have the sovereign power to interact with your page however they want. You can catch right-click events to prevent the menu from coming up; you can catch keypress events to stop Ctrl-V (and Shift-Insert, which is often forgotten) from being registered. However, you can't remove the Edit -> Paste menu option, which works within the browser and subverts Javascript altogether.
Not to mention that the user could just disable Javascript temporarily, paste into the field, then reenable Javascript if they wanted.
Even if you're happy with the limitations, you cannot modify the actual context menu, only prevent the right click from registering at all.
I suppose isn't possible to disable a single item on context menu, so I think you should try to disable the whole context menu, e.g.
$(document).bind("contextmenu", function(e) {
e.preventDefault();
});
∎ Another way of solving this pesky problem,
using JavaScript's Object-Oriented characteristics
<div class="ro-editable">
<span.....</...
<pre....</...
</div>
<script>
function make_editable_readonly(element){
element.backup_innerHTML = element.innerHTML; /* backup before editable. */
element.contenteditable = "true"; /* editable. CTRL+A is usable (FUN!) */
element.onautocomplete /* what in the world could change the content? actually lot!.. */
= element.onchange
= element.oncontextmenu
= element.oncopy
= element.oncuechange
= element.oncut
= element.ondrag
= element.ondragend
= element.ondrop
= element.onemptied
= element.onkeyup
= element.onmouseup
= element.onpaste
= element.onreset = function restore(){
if (element.innerHTML !== element.backup_innerHTML)
element.innerHTML = element.backup_innerHTML;
};
}
make_editable_readonly(
document.querySelector('div.ro-editable')
);
</script>
∎ Easy! Right? (and with no 💩y key-event "filtering" ☺):
∎ And here is a JSFiddle to play⚽ with
∎ taken from iCompile - ContentEditable And "ReadOnly"? YES! ☕