Hi is there a way to call the function that they use in autoHeight of Column Definitions? I have a function that Expand and Collapse the row based on the content of one column which is remarks. On the initial load the row height is fix for 2 text lines only. And when I click the Expand the row will expand depending on the remarks column content. Their autoHeight is working on initial load to expand the row base on the content but I do not need it on initial load. When I try to update it upon clicking Expand button then calling the resetRowHeights nothing happens. I have a temporary solution which I compute the length of the text, etc. It works on normal scenario but the problem is if they change the width of the column into smaller then expand. I try to get the ratio of text to column to dynamically compute it but the computation failed.
Below is my current code for computing the rows.
expandView() {
this.isExpand = true;
this.gridApi.forEachNode((rowNode: RowNode) => {
let ulRemarksTotalRowHeight = 0;
let customerRemarksTotalRowHeight = 0;
let ulRemarksList = [];
let customerRemarksList = [];
const ulRemarks = rowNode.data.map_ul_remarks;
const customerRemarks = rowNode.data.map_cust_remarks;
if (ulRemarks) {
ulRemarksList = rowNode.data.map_ul_remarks.split('\n');
}
if (customerRemarks) {
customerRemarksList = rowNode.data.map_cust_remarks.split('\n');
}
if (ulRemarksList) {
for (let i = 0; i < ulRemarksList.length; i++) {
const display = ulRemarksList[i];
// 28 - Default height value(in px) of one row in ag-Grid;
// 19 - Maximum text length for one row in ag-Grid(Initial Load/Without Resizing the Column of UL and Customer Remarks);
const ulRemarksHeight = Math.ceil(display.length / 19) * 28;
ulRemarksTotalRowHeight += ulRemarksHeight;
}
}
if (customerRemarksList) {
for (let i = 0; i < customerRemarksList.length; i++) {
const display = customerRemarksList[i];
// 28 - Default height value(in px) of one row in ag-Grid;
// 19 - Maximum text length for one row in ag-Grid(Initial Load/Without Resizing the Column of UL and Customer Remarks);
const customerRemarksHeight = Math.ceil(display.length / 19) * 28;
customerRemarksTotalRowHeight += customerRemarksHeight;
}
}
const largestHeight = Math.max(ulRemarksTotalRowHeight, customerRemarksTotalRowHeight);
if (largestHeight !== 0 && largestHeight > 56) {
rowNode.setRowHeight(largestHeight);
} else {
// 2 rows = 56px;
rowNode.setRowHeight(56);
}
});
this.gridApi.onRowHeightChanged();
}
Related
Is there any max number for sections while adding to card in googlescript?
I have a card with 13 sections in it, but it displays only 12 sections, Is there any specific limit on adding sections to the card
I have multiple sections which are going to add dynamically to the card,Is this possible to add n number of sections to the card?.
Yes, as of now the limit of sections a Card can have is 100.
However, 13 items are not a problem. The problem you are facing lies somewhere else in your code. Without that information we are not able to help.
As proof, here is some code that successfully shows how to adds 15 widgets:
function buildAddOn(e) {
var tempsections = [];
for (var i =0 ; i<15; i++) {
var tempsection = CardService.newCardSection().setHeader('Texts '+ i + 'aaaa.');
tempsection.addWidget(CardService.newTextButton().setText("Sections num"+i).setOnClickAction(CardService.newAction().setFunctionName("test")));
tempsections.push(tempsection);
}
// Build the main card after adding the section.
var card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Quick Label')
.setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/label_googblue_48dp.png'))
.addSection(tempsections[0])
.addSection(tempsections[1])
.addSection(tempsections[2])
.addSection(tempsections[3])
.addSection(tempsections[4])
.addSection(tempsections[5])
.addSection(tempsections[6])
.addSection(tempsections[7])
.addSection(tempsections[8])
.addSection(tempsections[9])
.addSection(tempsections[10])
.addSection(tempsections[11])
.addSection(tempsections[12])
.addSection(tempsections[13])
.addSection(tempsections[14])
.build();
return [card];
}
function test() {
return 0;
}
Also, for proof, here is the change to add 101 sections:
function buildAddOn(e) {
var tempsections = [];
for (var i =0 ; i<101; i++) {
var tempsection = CardService.newCardSection().setHeader('Texts '+ i + 'aaaa.');
tempsection.addWidget(CardService.newTextButton().setText("Sections num"+i).setOnClickAction(CardService.newAction().setFunctionName("test")));
tempsections.push(tempsection);
}
// Build the main card after adding the section.
var card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Quick Label')
.setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/label_googblue_48dp.png'));
for (var i =0 ; i<101; i++) {
card = card.addSections(tempsections[i]);
}
card = card.build();
return [card];
}
Consider the following code, which adds a sticky header on scroll to a page where a certain ID "sticky-header" is present and window size is greater than 425
sticky : function() {
var stickyTools = $j('#sticky-header');
if (stickyTools.length > 0 && window.outerWidth > 425) {
var headerHeight = stickyTools.height();
var fixmeTop = stickyTools.offset().top + headerHeight - 50;
$j(window).scroll(function() {
var currentScroll = $j(window).scrollTop();
if (currentScroll >= fixmeTop) {
stickyTools.addClass('sticky');
} else {
stickyTools.removeClass('sticky');
}
});
}
}
};
Now this works fine on one template but on another template I want to re-use the same functionality but ONLY if a check box is selected. The checkboxes have the class of "item-checkbox". I have a unique ID on that page which is "template-list".
I am unsure of the best way to nest another IF statement within the current one (if (currentScroll >= fixmeTop)) or if there is a more clever way of doing this?
I'm trying to create a google apps script that will format certain parts of a paragraph. For example, text that is underlined will become bolded/italicized as well.
One docs add-on I have tried has a similar feature: https://imgur.com/a/5Cw6Irn (this is exactly what I'm trying to achieve)
How can I write a function that will select a certain type of text and format it?
**I managed to write a script that iterates through every single letter in a paragraph and checks if it's underlined, but it becomes extremely slow as the paragraph gets longer, so I'm looking for a faster solution.
function textUnderline() {
var selectedText = DocumentApp.getActiveDocument().getSelection();
if(selectedText) {
var elements = selectedText.getRangeElements();
for (var index = 0; index < elements.length; index++) {
var element = elements[index];
if(element.getElement().editAsText) {
var text = element.getElement().editAsText();
var textLength = text.getText().length;
//For every single character, check if it's underlined and then format it
for (var i = 0; i < textLength; i++) {
if(text.isUnderline(i)) {
text.setBold(i, i, true);
text.setBackgroundColor(i,i,'#ffff00');
} else {
text.setFontSize(i, i, 8);
}
}
}
}
}
}
Use getTextAttributeIndices:
There is no need to check each character in the selection. You can use getTextAttributeIndices() to get the indices in which the text formatting changes. This method:
Retrieves the set of text indices that correspond to the start of distinct text formatting runs.
You just need to iterate through these indices (that is, check the indices in which text formatting changes), which are a small fraction of all character indices. This will greatly increase efficiency.
Code sample:
function textUnderline() {
var selectedText = DocumentApp.getActiveDocument().getSelection();
if(selectedText) {
var elements = selectedText.getRangeElements();
for (var index = 0; index < elements.length; index++) {
var element = elements[index];
if(element.getElement().editAsText) {
var text = element.getElement().editAsText();
var textRunIndices = text.getTextAttributeIndices();
var textLength = text.getText().length;
for (let i = 0; i < textRunIndices.length; i++) {
const startOffset = textRunIndices[i];
const endOffset = i + 1 < textRunIndices.length ? textRunIndices[i + 1] - 1 : textLength - 1;
if (text.isUnderline(textRunIndices[i])) {
text.setBold(startOffset, endOffset, true);
text.setBackgroundColor(startOffset, endOffset,'#ffff00');
} else {
text.setFontSize(startOffset, endOffset, 8);
}
}
}
}
}
}
Reference:
getTextAttributeIndices()
Based on the example shown in the animated gif, it seems your procedure needs to
handle a selection
set properties if the selected region is of some format (e.g. underlined)
set properties if the selected region is NOT of some format (e.g. not underlined)
finish as fast as possible
and your example code achieves all these goals expect the last one.
The problem is that you are calling the text.set...() functions at each index position. Each call is synchronous and blocks the code until the document is updated, thus your run time grows linearly with each character in the selection.
My suggestion is to build up a collection of subranges from the selection range and then for each subrange use text.set...(subrange.start, subrange.end) to apply the formatting. Now the run time will be dependent on chunks of characters, rather than single characters. i.e., you will only update when the formatting switches back and forth from, in your example, underlined to not underlined.
Here is some example code that implements this subrange idea. I separated the specific predicate function (text.isUnderline) and specific formatting effects into their own functions so as to separate the general idea from the specific implementation.
// run this function with selection
function transformUnderlinedToBoldAndYellow() {
transformSelection("isUnderline", boldYellowOrSmall);
}
function transformSelection(stylePredicateKey, stylingFunction) {
const selectedText = DocumentApp.getActiveDocument().getSelection();
if (!selectedText) return;
const getStyledSubRanges = makeStyledSubRangeReducer(stylePredicateKey);
selectedText.getRangeElements()
.reduce(getStyledSubRanges, [])
.forEach(stylingFunction);
}
function makeStyledSubRangeReducer(stylePredicateKey) {
return function(ranges, rangeElement) {
const {text, start, end} = unwrapRangeElement(rangeElement);
if (start >= end) return ranges; // filter out empty selections
const range = {
text, start, end,
styled: [], notStyled: [] // we will extend our range with subranges
};
const getKey = (isStyled) => isStyled ? "styled" : "notStyled";
let currentKey = getKey(text[stylePredicateKey](start));
range[currentKey].unshift({start: start});
for (let index = start + 1; index <= end; ++index) {
const isStyled = text[stylePredicateKey](index);
if (getKey(isStyled) !== currentKey) { // we are switching styles
range[currentKey][0].end = index - 1; // note end of this style
currentKey = getKey(isStyled);
range[currentKey].unshift({start: index}); // start new style range
}
}
ranges.push(range);
return ranges;
}
}
// a helper function to unwrap a range selection, deals with isPartial,
// maps RangeElement => {text, start, end}
function unwrapRangeElement(rangeElement) {
const isPartial = rangeElement.isPartial();
const text = rangeElement.getElement().asText();
return {
text: text,
start: isPartial
? rangeElement.getStartOffset()
: 0,
end: isPartial
? rangeElement.getEndOffsetInclusive()
: text.getText().length - 1
};
}
// apply specific formatting to satisfy the example
function boldYellowOrSmall(range) {
const {text, start, end, styled, notStyled} = range;
styled.forEach(function setTextBoldAndYellow(range) {
text.setBold(range.start, range.end || end, true);
text.setBackgroundColor(range.start, range.end || end, '#ffff00');
});
notStyled.forEach(function setTextSmall(range) {
text.setFontSize(range.start, range.end || end, 8);
});
}
I have a set of blocks, each have some associated number. I need to make blocks height correspond to this number. For example
As you can see the bigger number - the higher is div.
But its not 1:1 related, right? Otherwise we would not even see the blocks with number "1" or "10". So I'm trying to figure out how to calculate this approximate heights. I know its more like mathematical task.
Update: Also values can be any fractions, including less than 1
Finally, we have a working function that solves this problem.
Suppose we have an array of blocks objects with id and relative number value.
function blockHeights(blocks) {
const MAX_HEIGHT_PX = 500;
let logs = [];
blocks.forEach((block) => {
if (block.value) {
logs.push(Math.log10(block.value));
}
});
const minLog = Math.ceil(Math.min(...logs));
const addition = minLog < 0 ? (1 - minLog) : 0;
const maxLog = Math.ceil(Math.max(...logs)) + addition;
const step = MAX_HEIGHT_PX / maxLog;
let blockHeights = {};
blocks.forEach((block) => {
blockHeights[block.id] = Math.round(step * (Math.log10(block.value) + addition));
});
return blockHeights;
}
----------------------------------------------------------
There is also another solution, which works better in my case: to use the formula from here: https://stats.stackexchange.com/a/281164/266299
and normalize all values to fit between your predefined min and max block height. This way the function is going to look like this:
function blockHeights(blocks) {
const MIN_BLOCK_HEIGHT_PX = 65;
const MAX_BLOCK_HEIGHT_PX = 300;
const maxMinDifference = MAX_BLOCK_HEIGHT_PX - MIN_BLOCK_HEIGHT_PX;
const min = Math.min(...Object.values(blocks));
const max = Math.max(...Object.values(blocks));
blocks.forEach(block=> {
result[block.id] = ((block.value - min) * maxMinDifference / (max - min)) + MIN_BLOCK_HEIGHT_PX;
});
return result;
}
Maybe can loop on each value and check if
if (largestValue > 100) {
percentage = (currentValue / largestValue) * 100
if ( percentage < 5 ) {
BlockXUIHeight = 5
BlockXUIWidth = 5
}
}
I'm trying to set a feature to update images on a Google Document, the same way Lucidchart Add-on does on its "Updated inserted diagram" feature. For this, I'm current doing the following:
Creating a Named Range and storing its id on document properties, together with the data to generate the image, for later retrieve.
On update, call body.getNamedRangeById() and replace the element with the new generated image.
This works, but I have the following problems that does not happen with Lucidchart:
Every update, a blank line is added after the image.
If the user drag and drop the image inside document for reposition it, the Named Range disappears and I'm not able to retrieve it later.
If the user centralize the image, after update the image comes back to left position, even copying its attributes
Does anybody knows a good strategy to replace/update a referenced image on Google Docs, the same way Lucidchart add-on update feature works?
Thanks
NamedRanges indeed get lost when the range is moved, so they're not very good for your scenario. But there's no other way of identifying elements (which is a great misfeature of Google Docs).
In the case of an image you could use its LINK_URL to identify it, which seems to be what Lucidchart uses. It does not get in the way of the user, so it may be a good solution.
About getting a blank line and losing attributes when inserting an image, I imagine (since you haven't shared any code) you're inserting the image directly in the document body instead of a paragraph. Then a paragraph gets created automatically to wrap your image resulting in the blank line and lost of attributes.
Here's some code example:
function initialInsert() {
var data = Charts.newDataTable().addColumn(
Charts.ColumnType.STRING, 'Fruits').addColumn(
Charts.ColumnType.NUMBER, 'Amount').addRow(
['Apple',15]).addRow(
['Orange',6]).addRow(
['Banana',14]).build();
var chart = Charts.newPieChart().setDataTable(data).build();
var body = DocumentApp.getActiveDocument().getBody()
body.appendImage(chart).setLinkUrl('http://mychart');
//here we're inserting directly in the body, a wrapping paragraph element will be created for us
}
function updateImage() {
var data = Charts.newDataTable().addColumn(
Charts.ColumnType.STRING, 'Fruits').addColumn(
Charts.ColumnType.NUMBER, 'Amount').addRow(
['Apple',Math.floor(Math.random()*31)]).addRow( //random int between 0 and 30
['Orange',Math.floor(Math.random()*31)]).addRow(
['Banana',Math.floor(Math.random()*31)]).build();
var chart = Charts.newPieChart().setDataTable(data).build();
var img = getMyImg(DocumentApp.getActiveDocument().getBody(), 'http://mychart');
//let's insert on the current parent instead of the body
var parent = img.getParent(); //probably a paragraph, but does not really matter
parent.insertInlineImage(parent.getChildIndex(img)+1, chart).setLinkUrl('http://mychart');
img.removeFromParent();
}
function getMyImg(docBody, linkUrl) {
var imgs = docBody.getImages();
for( var i = 0; i < imgs.length; ++i )
if( imgs[i].getLinkUrl() === linkUrl )
return imgs[i];
return null;
}
About the link_url, you could of course do like Lucidchart does and link back to your site. So it's not just broken for the user.
Take a look at my add-on called PlantUML Gizmo.
Here's the code to the insert image function, which deals with replacing images if there's already one selected:
function insertImage(imageDataUrl, imageUrl) {
/*
* For debugging cursor info
*/
// var cursor = DocumentApp.getActiveDocument().getCursor();
// Logger.log(cursor.getElement().getParent().getType());
// throw "cursor info: " + cursor.getElement().getType() + " offset = " + cursor.getOffset() + " surrounding text = '" + cursor.getSurroundingText().getText() + "' parent's type = " +
// cursor.getElement().getParent().getType();
/*
* end debug
*/
var doc = DocumentApp.getActiveDocument();
var selection = doc.getSelection();
var replaced = false;
if (selection) {
var elements = selection.getSelectedElements();
// delete the selected image (to be replaced)
if (elements.length == 1 &&
elements[0].getElement().getType() ==
DocumentApp.ElementType.INLINE_IMAGE) {
var parentElement = elements[0].getElement().getParent(); // so we can re-insert cursor
elements[0].getElement().removeFromParent();
replaced = true;
// move cursor to just before deleted image
doc.setCursor(DocumentApp.getActiveDocument().newPosition(parentElement, 0));
} else {
throw "Please select only one image (image replacement) or nothing (image insertion)"
}
}
var cursor = doc.getCursor();
var blob;
if (imageDataUrl != "") {
blob = getBlobFromBase64(imageDataUrl);
} else {
blob = getBlobViaFetch(imageUrl);
}
var image = cursor.insertInlineImage(blob);
image.setLinkUrl(imageUrl);
// move the cursor to after the image
var position = doc.newPosition(cursor.getElement(), cursor.getOffset()+1);
doc.setCursor(position);
if (cursor.getElement().getType() == DocumentApp.ElementType.PARAGRAPH) {
Logger.log("Resizing");
// resize if wider than current page
var currentParagraph = DocumentApp.getActiveDocument().getCursor().getElement().asParagraph();
var originalImageWidth = image.getWidth(); // pixels
var documentWidthPoints = DocumentApp.getActiveDocument().getBody().getPageWidth() - DocumentApp.getActiveDocument().getBody().getMarginLeft() - DocumentApp.getActiveDocument().getBody().getMarginRight();
var documentWidth = documentWidthPoints * 96 / 72; // convert to pixels (a guess)
var paragraphWidthPoints = documentWidthPoints - currentParagraph.getIndentStart() - currentParagraph.getIndentEnd();
var paragraphWidth = paragraphWidthPoints * 96 / 72; // convert to pixels (a guess)
if (originalImageWidth > paragraphWidth) {
image.setWidth(paragraphWidth);
// scale proportionally
image.setHeight(image.getHeight() * image.getWidth() / originalImageWidth);
}
}
}