Google Scripts - keep track of element [duplicate] - javascript

Update: This is a better way of asking the following question.
Is there an Id like attribute for an Element in a Document which I can use to reach that element at a later time. Let's say I inserted a paragraph to a document as follows:
var myParagraph = 'This should be highlighted when user clicks a button';
body.insertParagraph(0, myParagraph);
Then the user inserts another one at the beginning manually (i.e. by typing or pasting). Now the childIndex of my paragraph changes to 1 from 0. I want to reach that paragraph at a later time and highlight it. But because of the insertion, the childIndex is not valid anymore. There is no Id like attribute for Element interface or any type implementing that. CahceService and PropertiesService only accepts String data, so I can't store myParagraphas an Object.
Do you guys have any idea to achieve what I want?
Thanks,
Old version of the same question (Optional Read):
Imagine that user selects a word and presses the highlight button of my add-on. Then she does the same thing for several more words. Then she edits the document in a way that the start end end indexes of those highlighted words change.
At this point she presses the remove highlighting button. My add-on should disable highlighting on all previously selected words. The problem is that I don't want to scan the entire document and find any highlighted text. I just want direct access to those that previously selected.
Is there a way to do that? I tried caching selected elements. But when I get them back from the cache, I get TypeError: Cannot find function insertText in object Text. error. It seems like the type of the object or something changes in between cache.put() and cache.get().
var elements = selection.getSelectedElements();
for (var i = 0; i < elements.length; ++i) {
if (elements[i].isPartial()) {
Logger.log('partial');
var element = elements[i].getElement().asText();
var cache = CacheService.getDocumentCache();
cache.put('element', element);
var startIndex = elements[i].getStartOffset();
var endIndex = elements[i].getEndOffsetInclusive();
}
// ...
}
When I get back the element I get TypeError: Cannot find function insertText in object Text. error.
var cache = CacheService.getDocumentCache();
cache.get('text').insertText(0, ':)');
I hope I can clearly explained what I want to achieve.

One direct way is to add a bookmark, which is not dependent on subsequent document changes. It has a disadvantage: a bookmark is visible for everyone...
More interesting way is to add a named range with a unique name. Sample code is below:
function setNamedParagraph() {
var doc = DocumentApp.getActiveDocument();
// Suppose you want to remember namely the third paragraph (currently)
var par = doc.getBody().getParagraphs()[2];
Logger.log(par.getText());
var rng = doc.newRange().addElement(par);
doc.addNamedRange("My Unique Paragraph", rng);
}
function getParagraphByName() {
var doc = DocumentApp.getActiveDocument();
var rng = doc.getNamedRanges("My Unique Paragraph")[0];
if (rng) {
var par = rng.getRange().getRangeElements()[0].getElement().asParagraph();
Logger.log(par.getText());
} else {
Logger.log("Deleted!");
}
}
The first function "marks" the third paragraph as named range. The second one takes this paragraph by the range name despite subsequent document changes. Really here we need to consider the exception, when our "unique paragraph" was deleted.

Not sure if cache is the best approach. Cache is volatile, so it might happen that the cached value doesn't exist anymore. Probably PropertiesService is a better choice.

Related

Hard Line Break in Google Apps Script (Google Docs with Google Form Field to create new bullet in bulleted list)

I'm using Google Forms to feed a Google Doc (via Google Sheets), and in this Google Doc I have a pre-existing bulleted list. I would like to enter code that will add another bullet to the list.
My approach has been to add a ###newLine### tag to the end of the last pre-filled bullet in the form. I then used replace.Text(###newLine###) in GAS, and then added '\n' for a new line.
The problem is that this '\n' inserts a soft line break (like a Shift+Enter), and it doesn't create a new bullet. It just creates a new line under the prior bullet. I have tested within the doc by adding/removing the bullet associated with the above paragraph, and it is clear that this new line is associated with the above paragraph. What I would like is a a hard line break (like simply pressing Enter) which will create a new bullet.
Here's what the code looks like:
body.replaceText('###newLine###', '\n' + 'Produces a soft (shift+enter) line break.');
Also tried:
body.appendParagraph().
This attached to the end of the body and didn't seem to replaceText.
var insertPar = body.insertParagraph(21, 'test insert paragraph');
body.replaceText('###newBullet###', insertPar);
This would put it in the right spot but not as part of the list.
var listItemTest = body.appendListItem('#listItemTest#');
body.replaceText('###newBullet###', listItemTest);
This appended a numbered list to the end of the body but would not replace text or add to the existing bulleted list.
08-03-19, I tried the following, per Jescanellas's assistance. It works perfectly in the original test doc I provided, but I can't port it over to other docs. I think that's because I'm somehow failing to get the right data to attach it to the list at the right level, but I'm not sure where I'm messing up.
var formDataEntered = functionName.values[11] || ''; //This var is retrieved from the sheet attached to a form. It's the submitted data.
var listItem = body.getListItems(); //We're getting the list.
for (var i = 0; i < listItem.length;i++){ //We're creating a loop here.
var item = body.getListItems()[i]; //This gets the list and applies the loop to it.
if ((item.findText('###bulletTestPlaceholder###')) && (formDataEntered != '')){ //The ###bulletTestPlaceholder### is just a placeholder in the doc where I want to insert the bullet. Your purpose with the item.findText is to identify the list level we're going for - NOT to use the text itself as a placeholder and replace the text.
var index = body.getChildIndex(item); //You're getting all the data about var item (where we got the list and applied the loop).
var level = item.getNestingLevel(); //This gets the nesting level of var item. I'm wondering if this might be the issue as it's not specific to the findText('###bulletTestPlaceholder###')?
var glyph = item.getGlyphType(); //This gets the bullet type.
body.insertListItem((index + 1), formDataEntered).setNestingLevel(level).setGlyphType(glyph); //This is the location in the list where teh bullet will be placed. It also sets the nesting level and glyph type. I've tried playing with the nesting level using integers, but that doesn't fix it.
item.replaceText('###bulletTestPlaceholder###',''); //removes '###bulletTestPlaceholder###' text after it's no longer needed.
break; //stops the loop from looping multiple times.
} else if ((item.findText('###bulletTestPlaceholder###')) && (formDataEntered == '')) {
item.replaceText('###bulletTestPlaceholder###',''); //removes '###bulletTestPlaceholder###' text and avoids new line if no formDataEntered
}
}
After some investigating, and thanks to your examples and links I think I got the solution of this. If there is any issue or I misunderstood something, please tell me and I will correct the post.
This searches for the word ###origApproach### in all the paragraphs (list items) and adds the ###formData### next to it. After this it removes the ###origApproach### paragraph. I commented some lines in case you don't want to remove it.
function myFunction() {
var body = DocumentApp.getActiveDocument().getBody();
var listItem = body.getListItems();
for (var i = 0; i < listItem.length;i++){
var item = body.getListItems()[i];
if (item.findText('###origApproach###')){
var index = body.getChildIndex(item);
var level = item.getNestingLevel();
var glyph = item.getGlyphType();
//In case the indentation is changed:
var indentFirst = item.getIndentFirstLine();
var indentStart = item.getIndentStart();
//Added both setIndents to fix the indentation issue.
body.insertListItem((index + 1), '###formData###').setNestingLevel(level).setGlyphType(glyph).setIndentFirstLine(indentFirst).setIndentStart(indent);
body.removeChild(item); //Comment this if you don't want to remove the ###origApproach### paragraph
//Uncomment this if you want to keep the paragraph and remove ###origApproach###
//item.replaceText('###origApproach###','');
break;
}
}
}
If you remove the paragraph with the word ###origApproach### change index + 1 to index
EDIT
If it's changing your glpyh style, you can force it by using the parameters BULLET, HOLLOW_BULLET, NUMBER, etc instead of the variable glyph.
You can read more about these functions here

Indesign Script: Get first paragraph in textframe in each group

Using Indesign CS5.5, I have a vast collection of groups - all with an image and a textframe. The textframe has 3 paragraphs by default.
I need to get the text from the first paragraph of each textframe.
So far I have this:
var textboxes = app.activeDocument.groups.everyItem().textFrames;
for (i = 0; i <= textboxes.length; i++) {
if(textboxes[i] != 'undefined') {
var product = textboxes[i].contents;
$.writeln(product);
}
}
This gives me ALL the text...I really need to get the first paragraph only OR filter it somehow by font size.
I've tried using textboxes[i].paragraphs[0], but this returns the rather vague Object Invalid. It might be a specific group, but it's too vague for me to tell.
Is there a way to skip and continue if an object is invalid. AND is there perhaps a way to only look for text with a certain font size?
Any help would be greatly appreciated. I find Indesign's scripting API documentation quite poor.
Suggest to use:
var m1stParas = app.activeDocument.groups.everyItem().textFrames.everyItem().paragraphs[0];
which should return an array of paragraphs (each element is a 1st para of each TF from each group)
So you will have a set of text objects. Each object.contents is a string.
In case of error "invalid object" - has your doc possibly empty textFrames in some groups?
Jarek

how to change color of an element with javascript

I am taking a beginners javascript class. My teacher wants me to change the color of an element in my page, but I can't figure out how to do it. Here is the page:
// JavaScript Document
function myPar() {
pge = new Array ()
//I would need to change the color of JUST pge[4]
pge[4] = "So have you reverse engineered this document?"
pge[3] = "This first instance a button creation will create multiple buttons with no purpose. They will not be linked to any site in this lecture video. The first need to become familiar with the process and with the required syntax for setting a series of attributes. When the button construction sequence is completed the process will then append the button to the body tag as it did for the paragraph tag."
pge[2] = "The same process that was used to create paragraphs dynamically will be used to create buttons dynamically. This first instance of button creation will be design to create a button each time the function trigger is pressed."
pge[5] = "So this build of page content can happen in any sequence that is seen fit and then modified by changing index vaues."
pge[0] = "It was the best of times, it was the worst of times"
pge[1]= "There's no way that we can work out a way to colonize Mars in the next 50 years. Think of the logistical obstacles to such a plan. You'd need food, water, medicine. You'd need engineers, doctors, nurses, endless oxygen, of which mars has none."
pge[6] = "So what is such a big deal here?"
for (i=0;i<=pge.length-1;i++) {
var pgp = document.createElement("p");
var txt = document.createTextNode(pge[i]);
pgp.appendChild(txt);
pgp.setAttribute("class","mine");
pgp.appendChild(document.createElement("br"));
pgp.setAttribute("style","color:#605;font-size:1.5em;");
document.body.appendChild(pgp);
txt = "";
}
}
In your loop, check the value of i. If it's 4 then it's the one you want to change. So you can do something like this:
var len = pge.length;
for (var i=0; i< len;i++) {
if (i==4) { pgp.classList.add('redBG'); }
}
And have a CSS class:
.redBG {background-color:red; }
The best way to update the class is not using setAttribute. Its by modifying the classList (for modern browsers). In older broswers, you have to manually manipulate className as a string.
Also, you should ALWAYS var your variables.

InDesign: Accessing document dictionary

In my script, I am copying a table of cells that have a lot of text in them. This text has a bunch of custom hyphenation rules that are saved in the document dictionary, NOT in the user dictionary. This is accessed in the UI by opening User dictionary and selecting the document under Target.
When copying the table to another document, these rules are unfortunately not copied with it, and the text is changed.
How can I access this custom document dictionary so that my hyphenations are retained in the target document?
It is possible to access the user dictionary with UserDictionary, but where is the document dictionary located?
Answering this myself since I finally found the proper class to use:
The document dictionary can be accessed using HyphenationExceptions. To get all custom hyphenations from my target document, I did the following:
var myHyphenations = app.activeDocument.hyphenationExceptions;
for (var i = 0; i < myHyphenations.length; i++) {
if (myHyphenations[i].name === "Danish") {
var mySourceDictionary = myHyphenations[i];
mySourceHyphenations = mySourceDictionary.addedExceptions;
break
}
}
For some reason, it seems that it is NOT possible to get a certain HyphenationException using its name.
In other words, the below code does not work (it actually gives me a Norwegian dictionary):
var mySourceDictionary = app.activeDocument.hyphenationExceptions.item("Danish");
For this reason, I had to loop the array until I found the one I needed: ("Danish").

Jquery/Javascript - Syntax highlighting as user types in contentEditable region

I'm developing a contentEditable region on my website, where users will be able to type messages to each other.
<div contentEditable="true" class="smartText">User types here...</div>
The thing is, we will have smart text inside, meaning that if a user type #usersame inside this div, the #username should be highlighted in blue if the username exist and green if he doesn't exist. And of course all of this should happen as the user types...
I have no idea where to start, right now I have this:
$("body").on("keyup",".smartText",function(){
var $this = $(this),
value = $this.html(),
regex = /[^>]#\S+[^ ]/gim;
value = value.replace(regex,"<span style='color:red'>$&</span>");
$this.html(value);
});
But the text keeps jumping (as well as the caret position) and doesn't feel like the right direction. I guess it's a little similar to JSFiddle which colors code as it finds it.
I basically want the same thing as Twitter has.
Here is a JSFiddle to play around with: http://jsfiddle.net/denislexic/bhu9N/4/
Thanks in advance for your help.
I liked this problem and I worked very hard to solve. I believe I have finally succeeded (with a little assistance).
= UPDATED =
Piece of Code:
[...]
// formatText
formatText: function (el) {
var savedSel = helper.saveSelection(el);
el.innerHTML = el.innerHTML.replace(/<span[\s\S]*?>([\s\S]*?)<\/span>/g,"$1");
el.innerHTML = el.innerHTML.replace(/(#[^\s<\.]+)/g, helper.highlight);
// Restore the original selection
helper.restoreSelection(el, savedSel);
}
[...]
// point
keyup: function(e){
// format if key is valid
if(helper.keyIsAvailable(e)){
helper.formatText($this[0]);
}
// delete blank html elements
if(helper.keyIsDelete && $this.text()=="") {
$this.html("");
}
}
Screenshot:
JSFiddle here: http://jsfiddle.net/hayatbiralem/9Z3Rg/11/
Needed External Resources:
http://dl.dropboxusercontent.com/u/14243582/jscalc/js/rangy-core.js
http://dl.dropboxusercontent.com/u/14243582/jscalc/js/rangy-selectionsaverestore.js
Helper Question (thanks): replace innerHTML in contenteditable div
Regex Test Tool (thanks): http://www.pagecolumn.com/tool/regtest.htm
Keep in mind that the HTML markup typed by the user could be quite surprising, e.g: <span>#use</span><span>rname</span>, which still looks like #username to the user.
To avoid the crazy caret behavior (and some other nasty side effects) inside a contentEditable element, you should use W3C DOM API and walk the DOM tree each time there is a change in HTML (you can sniff the change by polling body.innerHTML upon a timer).
I've recently answered a similar question for CKEditor and described the algorithm of how to build a text-to-node map of the DOM, for finding a text match. The CKEditor DOM API is quite similar to the W3C one, you can adapt the same algorithm.
Once the match has been found, you should use DOM Range API to manipulate the content of the DOM nodes. E.g., to wrap a run of plain text with a styled <SPAN>:
var range = document.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
var span = document.createElement("span");
span.style.backgroundColor = "blue"
range.surroundContents(span);
Overall, this task is quite non-trivial and certainly isn't something you can fit into a single page of JavaScript code, to be answered here.
This seems to be somewhat a solution to your problem.
DEMO here: http://jsfiddle.net/bhu9N/5/
$(document).ready(function() {
$("body").on("keyup", ".editable", function(e) {
var $this = $(this);
if(e.keyCode==32) {//space
var words = $this.text().split(' ');
var lastword = $.trim(words[words.length-1]);
var reg = /^#\S+/;
if(reg.test(lastword)) {
//make an AJAX call for checking this word for existence
//suppose data is returned and data==1 means green
var data = 1;
if(data==1) {
var orgtext = $this.html();
orgtext = orgtext.replace(lastword, '<span class="green">'+lastword+'</span>');
$this.html(orgtext);
}
}
}
});
});​
Once the text is highlighted, the cursor goes to the starting of the div. So this still needs to be fixed. I will be updating the solution if I am able to find it. Meanwhile, just play around with what I have provided now and see if it helps.
As Ezos pointed out in his answer, I would not recommend trying to do anything intensive (such as making Ajax requests to check if a username exists or not) each time the user releases a key. You might have a bad time. With that said, I would recommend waiting a set amount of time after the user has stopped typing to run through what they've typed and highlight words, for example:
var textarea = $(".smartText");
var highlightWords = function highlightWords() {
var original = textarea.text();
var replaced = original.replace(/#[a-zA-Z0-9]+/g, function (username) {
// Magic
return "<span class='exists'>" + username + "</span>";
});
textarea.html(replaced);
};
var timer;
textarea.keyup(function (e) {
clearTimeout(timer);
if ($(this).text()) {
timer = setTimeout(highlightWords, 1000);
}
});
Link to a fiddle: http://jsfiddle.net/neJLW/
I think the code above should get you started in the right direction. Like you said, the cursor will still jump around so you'll have to save it and reset it in its old position each time you edit the contents of the div. Also, you'll want to adjust the timeout according to how long you expect determining if a username exists to take. You'll need to replace // Magic with your username check and adjust the return value accordingly.
As an aside, you'll want to keep in mind the accessibility issues with wrapping certain things in spans (see this GitHub issue for Lettering.js for an example).
Edit: Also note that this is not a robust solution (it doesn't react to copy paste for example). YMMV.
The method you are using seems very browser intensive and may cause some issues if someone types very quickly and it's running multiple requests before the 'String' can be verified through ajax. You might be better off if you use a library such as http://aehlke.github.io/tag-it/ - You can depict a function to change font color, etc, the same way it recommends a tag.
If i get time, i will make fiddle demo.

Categories

Resources