I am trying to translate a few Applescript (AS) into Javascript (JXA) for BBedit.
It's been a fun little thing to knock some rust off, but I'm stumped.
With AS I can set an insertion point into a BBEdit document thusly;
tell application "BBEdit"
activate
tell text 1 of window 1
select insertion point before line 40
end tell
end tell
I'm totally stumped when it comes to JXA. I've been poking around in the line objects but I can't find a insertsionPoint property.
You can access selection properties like so;
currentLine = bbedit.selection().startline().
But it is read only. So I think you need access to that select method if you want to set a selection or insertion point. I have no clue how, or if you can with JXA.
Anyone know how to set a BBEdit insertion point and/or selection with JXA?
Thanks.
JXA doesn't implement insertion reference forms (before/after/beginning/end). Relative (previous…/next…) and range (from…to…) specifiers are also borked, and filter (whose…) clauses are horrible too. Lots of non-trivial stuff that works fine in AS breaks in JXA: like Apple's earlier Scripting Bridge API, JXA was half-baked when shipped and immediately abandoned to rot.
Crap like this is why I recommend sticking to AppleScript. The language may stink, but it's the only [marginally] supported option that actually implements Apple events right. Normally I'd recommend calling into AppleScript from other languages via the AppleScript-ObjC bridge as the least sucky solution, but Apple have managed to break that too in 10.13.
(If you enjoy living dangerously, NodeAutomation provides non-broken Apple event support for Node.js, but with Apple abandoning AppleScript automation I don't want to waste anyone's time promoting or supporting it, so caveat emptor.)
Below is an example of using Javascript (JXA) for BBedit insertion point object and select method. BBEdit does seem to work with JXA but there is absolutely no documentation for JXA from BBEdit and very little information on the web. Below is some code that I came up with after spending hours of trial and error. So I hope it helps.
(() => {
/* Example to show using insertionPoints with BBEdit
the script adds new text to the last line of a BBEdit text document
*/
const strPath = '/Users/bartsimpson/Library/Mobile Documents/com~apple~ScriptEditor2/Documents/insertionPointsExample.txt';
const BBEdit = Application('BBEdit');
const docs = BBEdit.textDocuments;
//use select in case there are multiple BBEdit documents open
BBEdit.open(Path(strPath)).select();
let doc = docs[0];
let insertionPoints = doc.characters.insertionPoints;
let lines = doc.characters.lines;
let indexLastChar = doc.characters.length;
let indexLastLine = lines.length-1;
//last line in doc is blank
if (lines[indexLastLine].length() == 0){
insertionPoints[indexLastChar].contents = 'some new text5';
indexLastChar = doc.characters.length; //update after adding text
insertionPoints[indexLastChar].select(); //puts cursor end of last line
}
//last line in doc has text so add a new line first
else {
insertionPoints[indexLastChar].contents = '\n';
insertionPoints[indexLastChar+1].contents = 'some new text6';
indexLastChar = doc.characters.length; //update after adding text
insertionPoints[indexLastChar].select(); //puts cursor end of last line
}
})()
Related
I am creating a Google Docs add-on, and one of the features I am trying to create is a feature that reduces the number of empty lines between two paragraphs.
So, for instance, if I have 2 paragraphs with 5 empty/blank lines between them, I want the feature to reduce the number of empty lines to 1.
Essentially, I need a way to detect an empty line. I've looked at the API, and think I need to use the replaceText() method which searches for a regex pattern. However, I've tried it, and it didn't work (perhaps I'm using an incorrect patter, i dont know).
Can anyone help me in finding a way to detect an empty line? Thanks.
EDIT:
I just found out that Google Docs don't support all patterns of regex. Here is that link: https://support.google.com/analytics/answer/1034324?hl=en . I'm not familiar with regex. Can anyone provide an alternative that works in Google Docs.
Written and tested the below function, here are the steps to make it work
Copy the text from here Lorem Ipsum Google Doc, to a new Google Doc
Add the below snippet to your Tools > Script editor > project
Comments inline
// Regular expressions with the following special characters are not supported,
// as they can cause delays in processing your email: * (asterisk), + (plus sign)
// So RegEx is not applicable since you can't use "\s*", we need to find another solution
// A row/line is a Paragraph, weird right? So now you iterate through the rows
// Trim paragraph (row), if it's empty, then you can delete it.
function removeEmptyLines() {
var doc = DocumentApp.getActiveDocument();
Logger.log("Before:\n", doc.getBody().getText());
var paragraphs = doc.getBody().getParagraphs();
// Iterating from the last paragraph to the first
for (var i=paragraphs.length-1; i>=0; i--){
var line = paragraphs[i];
if ( ! line.getText().trim() ) {
// Paragraph (line) is empty, remove it
line.removeFromParent()
Logger.log("Removed: ", i);
}
}
Logger.log("After:\n", doc.getBody().getText());
}
References
https://developers.google.com/apps-script/reference/document/text#replaceText(String,String)
https://support.google.com/a/answer/1371415?hl=en
https://stackoverflow.com/a/3012832/5285732
The use case
When I'm typing a query into a search engine, sometimes it's useful to quote
a part of the query, so the engine doesn't bother me with useless suggestions.
The task
This operation is so frequent, that I want to do this with a shortcut.
The shortcut part isn't the issue, there's a way to assign a shortcut to a bookmarklet.
What I don't know how to do is
Get the current text area. The only thing I know about it is that the cursor is there.
I cannot assume any ids etc. Also, I don't want to install any hooks.
Insert "", and go backwards one character.
I'm expecting a one/two liner that I can place in a bookmarklet.
The solution
Since no one wanted to answer, and I'm just a novice in JavaScript,
I decided to see if there's a plugin that does close to what I want.
The choice fell to Firemacs,
since I'm using it anyway.
The code to go backward one char is simplicity itself:
goDoCommand('cmd_charPrevious');
However, the command to insert text didn't work.
But the command to paste is simple again:
goDoCommand('cmd_paste');
Now it only remains to put '""' in the clipboard. This one isn't easy:
var str = Components.classes["#mozilla.org/supports-string;1"]
.createInstance(Components.interfaces.nsISupportsString);
str.data = '""';
var trans = Components.classes["#mozilla.org/widget/transferable;1"]
.createInstance(Components.interfaces.nsITransferable);
trans.addDataFlavor("text/unicode");
trans.setTransferData("text/unicode",str, str.data.length * 2);
var clipid = Components.interfaces.nsIClipboard;
var clip = Components.classes["#mozilla.org/widget/clipboard;1"]
.getService(clipid);
clip.setData(trans,null,clipid.kGlobalClipboard);
Then I just patched this code into the extension instead of the "Ctrl-h" binding,
which I don't use. Problem solved. Now I can insert a pair of quotes very fast in Firefox.
I'm writing a Dashboard widget in Dashcode, and on the back side, I've got a string for credits. I want to include the widget's version number in that string, but if possible, I want to programmatically grab it from the CFBundleVersion or CFBundleShortVersionString key in Info.plist to avoid having to change the number in multiple places if and when I update the widget.
Searches on Apple's developer documentation, Google and various forums have proven fruitless so far. What I'd like to know is whether there's a built-in way to do this that Apple included but forgot to mention (like var version = widget.version(); or something), or whether my script will have to pull in and parse the entire plist before plucking out the one value I actually want.
Thanks for any help you can provide!
I seem to have found the answer: use Dashcode's "data source" facility to read in Info.plist as an XML data source. From there, this blog post showed me how to traverse the plist's structure and get the correct string (in this case, the fifth <string> element in the file, corresponding to CFBundleShortVersionString.
The function I ended up with:
function getWidgetVersion() {
var dataSource = dashcode.getDataSource("infoPlist");
var version = dataSource.selection().valueForKey("dict").valueForKey("string")[4]; // This line and the previous could probably be combined for the sake of brevity
if (typeof(version) == 'string') {
document.getElementById("creditsLabel").innerHTML += version; //I'll change this to just pass the number on
}
}
Since the text of the creditsLabel div has already been started off with a localized string, I get a nice little label saying "Version 1.0".
at the moment I have some troubles writing an abstraction layer for Mozilla and Webkit based browsers for using the DOM-range object (getting and processing user selections).
I have also tried to have a look at frameworks like Rangy but this seems far to complex for my task (I have no idea where exactly in the code to find the information I need. If someone could give me a hint, I would be grateful!).
What I want is simply this:
get back the reference to the text node the selection starts in and its offset
get back the reference to the text node the selection ends in and its offset
So far my layer looks like this:
var SEL_ABSTR = {
get_selection: function(window_object) {
return window_object.getSelection();
},
get_range: function(selection) {
return (selection.getRangeAt) ? selection.getRangeAt(0) : selection.createRange();
},
get_range_info: function(range, div_ele) {
var first_node, start_offset;
var last_node, end_offset;
if (range.startContainer == div_ele) {
// selections affects the containing div
first_node = div_ele.childNodes[0];
last_node = first_node;
start_offset = 0;
end_offset = first_node.nodeValue.length;
} else if (range.startOffset == range.startContainer.nodeValue.length && range.endOffset == 0) {
// known bug in Firefox
alert('firefox bug');
first_node = range.startContainer.nextSibling.childNodes[0];
last_node = first_node;
start_offset = 0;
end_offset = first_node.nodeValue.length;
} else {
first_node = range.startContainer;
last_node = range.endContainer;
start_offset = range.startOffset;
end_offset = range.endOffset;
}
return {
first_node: first_node,
start_offset: start_offset,
last_node: last_node,
end_offset: end_offset,
orig_diff: end_offset - start_offset
};
},
};
I have identified two Mozilla bugs for now:
Sometimes when the whole (if its the only one) text node is selected within the containing div I get back a reference to this div instead of a reference to the text node. Now I can handle it and give back a reference to the child of the div which is the text node
Sometimes I get back a reference to the previous sibling with offset == prevSibling.length and and a reference to nextSibling with offset == 0. But the correct reference would be in the middle. I can also handle this.
So what more is there for Mozilla? Webkit works fine!
One should assume that the DOM-range object is standard (and I am not talking of IE, this is another task ...)
greets!
more specific details here:
It was't meant as a critique on Rangy. So I am sorry if it sounded like that. I can imagine that handling these different APIs is not easy per se.
You are right, I wasn't specific regarding the task I am trying to fulfill. My structure is rather simple: I have a div (with attribute contenteditable=true) and text within that div (one text node at the beginning).
Starting from this structure, it is now possible to select text with the mouse and add a property to it; this property is then expressed by a span embracing the selected text and a class assigned to that span representing the selected property. Now it is possible to select again some text and a property. If it is the same text and another property, another class will be assigned to that span or removed if the property already exists. If text is selected which embraces several spans, they will be split in order to express that structure (perhaps you remember my post of July).
<div contenteditable="true">
hello I am
<span class="red">text but I am also <span class="underline">underlined</span></span>
<span class="underline"> also without color</span>
</div>
The algorithm works fine now for "symmetrical" cases: I can build a complex structure and then undo it backwards. It works fine for Safari and Chrome. Now I have of course to develop the algorithm further.
But for now I have problems with Mozilla because I do not understand the system for DOM range objects: startContainer, endContainer, startOffset, endOffset
In my perception regarding my specific case with a div only containing textnodes and spans I assume:
that startContainer and endContainer always point to a textnode affected by the mouse selection (there are no empty spans, they always contain either other spans or a textnode), marking the beginning and the end of the whole selection
that startOffset and endOffset indicate the position of the selection within the textnode at the beginning and at the end
In the posted code above I have identified two cases in which Mozilla acts differently from webkit.
So if I knew the rules of Mozilla DOM-range I could inegrate that in my layer so that the behaviour would be the same for webkit and Mozilla.
Thank you very much for your answer.
There is no rule that says selection boundaries must be expressed in terms of text nodes. Consider a selection inside an element that contains only <img> elements, for example. So, what you're calling bugs in Mozilla are not bugs at all; in fact, WebKit's selection handling is much buggier than Mozilla's. However, your observation that browsers vary in precisely where they consider a selection boundary to lie when it is at the end of a text node is valid and does complicate things. The best way to deal with it really depends on what you're trying to do, which isn't clear from your question.
If you want selection boundaries purely in terms of character offsets within the text content of an element, you can do this (although I'd generally recommend against it for reasons laid out in the linked answer).
Finally, as author of Rangy, I'd like to point out that it's based on the same APIs (DOM Range and Selection) that browsers implement, so I'd say it's no more or less complicated than those APIs. References:
Selection (work in progress)
DOM 2 Range (implemented by current versions of all major browsers)
DOM4 Range (successor to DOM 2 Range, work in progress)
You are noticing a bug where Gecko/Firefox and Presto/Opera incorrectly select the node in which the mouse cursor clicked though did not select. Wait, what? What happens is that if the click registers less than HALF of a character (though greater than 0 pixels) it is not VISUALLY selected HOWEVER Firefox and Opera still select the node itself! Trident/IE and WebKit (Chrome/Safari) do not do this.
I have been battling against this bug for some time, filed a bug at Mozilla (can't remember if I did so with Opera since this was last year) and today finally wrote directly to the editors of the DOM4 specification asking them to implement an EXPLICIT clarification for browser vendors on how to define the startContainer.
It IS possible to adapt the code to handle this however I do not have the time to write out all the code for you.
Here is the list of methods we'll use...
window.getSelection().getRangeAt(0).startContainer;
window.getSelection().anchorNode
window.getSelection().focusNode
It's VERY important to remember that the anchorNode is not EXPLICITLY the left starting position though the INITIAL CLICK. If the user click on the right side of text and drags the mouse to the left then the anchor ends up on the RIGHT side of the range. If the user click on the left side of text and then drags the mouse to the right the anchor is then on the left side of the range.
Essentially what you can try to do is see if neither the anchorNode nor the focusNode EXPLICITLY match the startContainer.
var sc = window.getSelection().getRangeAt(0).startContainer;
var an = window.getSelection().anchorNode
var fn = window.getSelection().focusNode
if (sc!==an && sc!==fn) {alert('startContainer bug encountered!');}
Even if you don't need the startContainer in ALL situations you're still going to eventually reference it SO it's best to use an object to represent the startContainer be it if the browser gets it right the first time (Trident/WebKit) or you have to correct it (Gecko/Presto).
This is where it gets a bit tricky especially because different people will have different goals and approaches so I will try to keep the following as generic as possible.
Either you can determine the correct startContainer using anchorNode or focusNode methods OR you can use object detection and W3C compliant methods. Those other methods include....
window.getSelection().getRangeAt(0).startContainer
window.getSelection().getRangeAt(0).startContainer.parentNode
window.getSelection().getRangeAt(0).startContainer.previousSibling
window.getSelection().getRangeAt(0).startContainer.nextSibling
window.getSelection().getRangeAt(0).startContainer.childNodes[]
When dealing with style elements such as s (strike), strong, em (emphasis) and so on you may access the textNode using the firstChild unless you have multiple style elements enclosed around the text.
.nextSibling.firstChild
.nextSibling.firstChild.nodeValue
<em>textNode here</em>
If you're having difficulty with determining what methods are available at what parts I recommending using the in operator. In example...
for (i in window.getSelection())
{
document.getElementById('textarea_example').value = document.getElementById('textarea_example').value+'\n'+i;
}
...keep in mind that if you're inside of a loop that it may repeat the options in your textarea element so CTRL+f for the first method and erase from it's second instance down to retain only relevant methods.
Remember to use alert and I often use multiple lines to show multiple pieces of information simultaneously to help me determine what I have. In example...
var e1 = scp.nodeName;
if (scp.nextSibling) {var e2 = scp.nextSibling.nodeName;} else {var e2 = 'null';}
var e3 = sc.nodeName;
if (sc.nextSibling) {var e4 = sc.nextSibling.nodeName;} else {var e4 = 'null';}
alert(
'startContainer = '+window.getSelection().getRangeAt(0).startContainer.nodeName
+'\n\n'+
'startContainer = '+window.getSelection().getRangeAt(0).startContainer.nodeValue
+'\n\n'+
e1
+'\n\n'+
e2
+'\n\n'+
e3
+'\n\n'+
e4
+'\n\nanchorNode = '+
window.getSelection().anchorNode.nodeName
+'\n\n'+
window.getSelection().anchorNode.nodeValue
+'\n\nfocusNode = '+
window.getSelection().focusNode.nodeName
+'\n\n'+
window.getSelection().focusNode.nodeValue
);
if (e2=='#text') {alert('e2 = '+scp.nextSibling.nodeValue);}
if (e4=='#text') {alert('e4 = '+scp.nextSibling.nodeValue);}
I was just reading the Times online and I wanted to copy a bit of text from the article and IM it to a friend, but I noticed when I did so, it automatically appended the link back to the article in what I had copied.
This is not a feature of my IM client, so I assume this happened because of some javascript on Times website.
How would I accomplish this if I wanted to implement it on my site? Basically, I would have to hijack the copy operation and append the URL of the article to the end of the copied content, right? Thoughts?
Here's the article I was reading, for reference: http://www.time.com/time/health/article/0,8599,1914857,00.html
It's a breeze with jQuery (which your referenced site is using):
$("body").bind('copy', function(e) {
// The user is copying something
});
You can use the jQuery Search & Share Plugin which does this exact thing whenever somebody copies more than 40 chars from your site: http://www.latentmotion.com/search-and-share/
The site that you referenced is apparently using a service called Tynt Insight to accomplish this though.
They are using the free service Tynt. If you want to accomplish the same thing, just use the same service.
What browser are you using (and what version)?
In some newer browsers, the user is either asked if a website can access the clipboard, or its simply not allowed. In other browsers (IE 6, for example), it is allowed, and websites can easily read from and write to your copy clipboard.
Here is the code (IE only)
clipboardData.setData("Text", "I just put this in the clipboard using JavaScript");
The "Copy & Paste Hijacker" jQuery plugin does exactly what you want and seems better suited for your purposes than Tynt or Search & Share: http://plugins.jquery.com/project/copypaste
You can easily format the copied content, specify max or min characters, and easily include the title of the page or the URL in the copied content exactly where you want.
I recently noticed this on another website and wrote a blog post on how it works. The jQuery example doesn't seem to actually modify what the user copies and pastes, it just adds a new context menu.
In short:
var content = document.getElementById("content");
content.addEventListener("copy", oncopy);
function oncopy() {
var newEl = document.createElement("p");
document.body.appendChild(newEl);
newEl.innerHTML = "In your copy, messing with your text!";
var selection = document.getSelection();
var range = selection.getRangeAt(0);
selection.selectAllChildren(newEl);
setTimeout(function() {
newEl.parentNode.removeChild(newEl);
selection.removeAllRanges();
selection.addRange(range);
}, 0)
}
The setTimeout at the end is important as it doesn't seem to work if the last part is executed immediately.
This example replaces your selected text at the last minute with my chosen string. You can also grab the existing selection and append whatever you like to the end.