How can I replace an image in Google Documents? - image

I'm trying to insert images into Google Docs (other GSuite apps later) from an Add-On. I've succeeded in fetching the image and inserting it when getCursor() returns a valid Position. When there is a selection (instead of a Cursor), I can succeed if it's text that's selected by walking up to the Parent of the selected text and inserting the image at the start of the paragraph (not perfect, but OK).
UPDATE: It seems that I was using a deprecated method (getSelectedElements()), but that didn't fix the issue. It seems the issue is only with wrapped images as well (I didn't realize that the type of the object changed when you changed it to a wrapped text).
However, when an wrapped-text Image (presumably a PositionedImage) is highlighted (with the rotate and resize handles visible in blue), both getSelection() and getCursor() return null. This is a problem as I would like to be able to get that image and replace it with the one I'm inserting.
Here's my code... any help would be great.
var response = UrlFetchApp.fetch(imageTokenURL);
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection)
{
Logger.log("Got Selection");
var replaced = false;
var elements = selection.getRangeElements();
if (elements.length === 1
&& elements[0].getElement().getType() === DocumentApp.ElementType.INLINE_IMAGE)
{
//replace the URL -- this never happens
}
//otherwise, we take the first element and work from there:
var firstElem = elements[0].getElement();
Logger.log("First Element Type = " + firstElem.getType());
if (firstElem.getType() == DocumentApp.ElementType.PARAGRAPH)
{
var newImage = firstElem.asParagraph().insertInlineImage(0, response);
newImage.setHeight(200);
newImage.setWidth(200);
}
else if (firstElem.getType() == DocumentApp.ElementType.TEXT)
{
var p = firstElem.getParent();
if (p.getType() == DocumentApp.ElementType.PARAGRAPH)
{
var index = p.asParagraph().getChildIndex(firstElem);
var newImage = p.asParagraph().insertInlineImage(index, response);
newImage.setHeight(200);
newImage.setWidth(200);
}
}
} else {
Logger.log("Checking Cursor");
var cursor = DocumentApp.getActiveDocument().getCursor();
if (cursor)
{
Logger.log("Got Cursor: " + cursor);
var newImage = cursor.insertInlineImage(response);
var p = cursor.getElement();
var size=200;
newImage.setHeight(size);
newImage.setWidth(size);
}
}

You are using the deprecated 'getSelectedElements()' method of the Range class. You may notice it's crossed out in the autocomplete selection box.
Instead, use the 'getRangeElements()' method. After selecting the image in the doc, the code below worked for me:
var range = doc.getSelection();
var element = range.getRangeElements()[0].getElement();
Logger.log(element.getType() == DocumentApp.ElementType.INLINE_IMAGE); //logs 'true'

Related

How to create a stock event in amCharts v4?

Is it possible to indicate events along a series in amCharts v4 similar to the Stock Event in the v3 stock chart?
While I was brought on board specifically for v4 and am not familiar with v3, I'm confident you can simulate some of these features using Bullets.
A bullet is a Container (basically a placeholder parent for whatever visual object or additional Containers that you want), that will appear at every point of data. You can put a label there as well as a line and any other shape, e.g.:
var stockBullet = series.bullets.push(new am4charts.Bullet());
stockBullet.dy = -20;
var circle = stockBullet.createChild(am4core.Circle);
circle.stroke = "#000";
circle.strokeWidth = 1;
circle.radius = 10;
circle.fill = series.fill.brighten(-0.3);
circle.dy = -10;
var line = stockBullet.createChild(am4core.Line);
line.stroke = "#000";
line.strokeWidth = 1;
line.height = 20;
var label = stockBullet.createChild(am4core.Label);
label.fill = am4core.color("#000");
label.strokeWidth = 0;
label.dy = -20;
label.textAlign = "middle";
label.horizontalCenter = "middle"
Since we don't want a bullet to appear at every point of data, only at Stock Events, we can handle that once the bullets are ready on the chart by going through their data, disabling them if need be, otherwise providing text for our label (and maybe tooltipText if need be) (presume there is a property stockEvent in the data):
stockBullet.events.on("inited", function(event) {
if (event.target.dataItem && event.target.dataItem.dataContext && event.target.dataItem.dataContext.stockEvent) {
event.target.children.getIndex(2).text = event.target.dataItem.dataContext.stockEvent.text;
} else {
event.target.disabled = true;
}
});
Getting tooltips of different objects to play well with each other can be tricky depending on your chart, e.g. if it has Chart Cursor enabled there's a cursorTooltipEnabled property to prevent triggering a tooltip over bullets. To simplify things in this case what I did is make an invisible series per unique stock event bullet. For each stock event, use adapters to set its paired series' tooltipText to what's desired, and the base, visible series' tooltipText to "":
series.adapter.add("tooltipText", function(text, target) {
if (target.tooltipDataItem.dataContext.stockEvent) {
return "";
}
return text;
});
// ...
hiddenSeries.adapter.add("tooltipText", function(text, target) {
if (target.tooltipDataItem.dataContext.stockEvent) {
return target.tooltipDataItem.dataContext.stockEvent.description;
}
return "";
});
Here's a demo:
https://codepen.io/team/amcharts/pen/337984f18c6329ce904ef52a0c3eeaaa
Screenshot:

UPDATED: Javascript logic to fix in a small function (SVG, obtaining absolute coords)

NEW:
So here is the code at codepen:
http://codepen.io/cmer41k/pen/pRJNww/
Currently function UpdateCoords(draggable) - is commented out in the code.
What I wanted is to update on mouseup event the coordinates of the path (circle as path here) to the absolute ones and remove transform attribute.
But I am failing to do that;(( sorry only learning
OLD:
In my code I have an svg element (path) that gets dragged around the root svg obj (svg) via transform="translate(x,y)" property.
I wanted to update such path element's attribute "d" (the string that describes all coords) to use absolute coordinates and get rid of transformed\translate thing.
Basically:
was: d="M10,10 30,10 20,30" + transform="translate(20,0);
to be: d="M30,10 50,10 40,30" + transform="translate(0,0)" (or if we can delete the transform - even better)
So I did the code that does the thing for me, but there is a bug that prevents proper result.
I am sure I am doing something wrong in here:
var v = Object.keys(path.controlPoints).length
// controlPoints here is just a place in path object where I store the coords for the path.
var matrix = path.transform.baseVal.consolidate();
//I validated that the above line does give me proper transform matrix with proper x,y translated values. Now below I am trying to loop through and update all control points (coordinates) of the path
for (i=0; i<v; i++) {
var position = svg.createSVGPoint();
position.x = path.controlPoints["p"+i].x;
position.y = path.controlPoints["p"+i].y;
// so for each of path's control points I create intermediate svgpoint that can leverage matrix data (or so I think) to "convert" old coords into the new ones.
position = position.matrixTransform(matrix);
path.controlPoints["p"+i].x = position.x;
path.controlPoints["p"+i].y = position.y;
}
// I am sure I am doing something wrong here, maybe its because I am not "cleaning"/resetting this position thing in this loop or smth?
Sorry I am not a programmer, just learning stuff and the question is - in this code snipped provided the goal that I described - is something wrong with how I handle "position"?
Alright, the code snipped is now functioning properly!
So after I figured how to obtain properly the matrix I still had a weird displacement for any subsequent draggables.
I became clear that those displacements happen even before my function.
I debugged it a bit and realized that I was not clearing the ._x and ._y params that I use for dragging.
Now code works!
http://codepen.io/cmer41k/pen/XpbpQJ
var svgNS = "http://www.w3.org/2000/svg";
var draggable = null;
var canvas = {};
var inventory = {};
var elementToUpdate = {};
//debug
var focusedObj = {};
var focusedObj2 = {};
// to be deleted
window.onload = function() {
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
AddListeners();
}
function AddListeners() {
document.getElementById("svg").addEventListener("mousedown", Drag);
document.getElementById("svg").addEventListener("mousemove", Drag);
document.getElementById("svg").addEventListener("mouseup", Drag);
}
// Drag function //
function Drag(e) {
var t = e.target, id = t.id, et = e.type; m = MousePos(e); //MousePos to ensure we obtain proper mouse coordinates
if (!draggable && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable
copy = t.cloneNode(true);
copy.onmousedown = copy.onmouseup = copy.onmousemove = Drag;
copy.removeAttribute("id");
copy._x = 0;
copy._y = 0;
canvas.appendChild(copy);
draggable = copy;
dPoint = m;
}
else if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
draggable = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (draggable && (et == "mousemove")) {
draggable._x += m.x - dPoint.x;
draggable._y += m.y - dPoint.y;
dPoint = m;
draggable.setAttribute("transform", "translate(" +draggable._x+","+draggable._y+")");
}
// stop drag
if (draggable && (et == "mouseup")) {
draggable.className.baseVal="draggable";
UpdateCoords(draggable);
console.log(draggable);
draggable._x = 0;
draggable._y = 0;
draggable = null;
}
}

Appcelerator Store Local Searches

When a button is pressed, I would like the id and the name of the button saved locally.
I am not quite sure the best way to approach this problem. Should I use appcelerator properties (http://docs.appcelerator.com/titanium/3.0/#!/api/Titanium.App.Properties) or write to a file to storage? At the moment I am using the Ti.App.Properties.setList.
Example code:
searchStorageName = "searchHistory";
searchResultsArray = [];
var currentEntries = (Ti.App.Properties.getList(searchStorageName));
// Create search entry object.
var localSearchObject = {
company_name: resultNodeCompany,
company_id: resultNodeCompanyID,
variation_id: resultNodeCompanyVariationID
};
// Check if existing entries, if so push current search
// and previous searches to array.
if(currentEntries === null || currentEntries === undefined){
searchResultsArray.push(localSearchObject);
Ti.App.Properties.setList(searchStorageName, searchResultsArray);
// searchResultsArray.push(localSearchObject, currentEntries);
}
else {
searchResultsArray.push(localSearchObject, currentEntries);
Ti.App.Properties.setList(searchStorageName, searchResultsArray);
}
I am stuck at the moment as it is inserting duplicate searches into the array. When I loop over the values to create a list in the UI it shows duplicates.
var currentEntries = (Ti.App.Properties.getList(searchStorageName));
var currentEntriesLength = currentEntries.length;
var getPreviousHistorySearchesArray = [];
currentEntries.forEach(function(entry, index) {
var company_name = entry.company_name;
var company_id = entry.company_id;
var variation_id = entry.variation_id;
// Create View Entry.
createSearchHistoryViewEntry(index, company_name, company_id, variation_id);
}
Use SQLite_Database Better than local properties http://docs.appcelerator.com/titanium/3.0/#!/guide/Working_with_a_SQLite_Database

How to programmatically move a tab to another window in a firefox Addon-SDK extension?

While it looks like you can change the order of a tab within a window by updating the tab .index property, it doesn't look like the tabs api directly supports the move of a tab to another window.
Am I missing something? Is there a viable workaround?
It is possible through the low level module window/utils. The example below duplicates the active tab across every open window
const { getMostRecentBrowserWindow, windows: getWindows } = require("sdk/window/utils");
const { ActionButton } = require("sdk/ui/button/action");
var button = ActionButton({
id: "duplicatetab-button",
label: "Duplicate tab",
icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACUElEQVQ4jaWTTWtTURCGjzc33CCpbVKN4kexC9EUY1Hov+iqPyDrbgtuCrViKUERqsWVguBGQaW4UiKiaEVxoShFGgnuBMUqNW3zce49Z+ZxUWtwoRR8YXbzPswM7xj+JgVEiXGsYVknxgII4Ltt5p8AB8RArOAUVQfqQJNtAFA8QgvF6i9PR1Dt0KbVBTjncM4hIni/OZv3HsRB+wvefiP2LcQnJIkQe49FEJFNQLPZZHh4mEwmQyqVoqenh3K5TGvlK1dOlageH+HG4DFar1/S0A6Lr99xdN8QxWKRXC6HGR0dJZvNMjk5Sb1ep1gskk6nuTo/D+/ec7dvkBdhP9cKeX7UXxEZQ2/YRxRFLC8vY+bm5qhUKnjvsdYyPj5OFEWcnTnHujiS5TfcPDbAw50h9w7u5f7UadLZFLVaDRHBiGzuY61lbGyMXC5HoVBgrbGGWAW/TvvxHR7s7udFKs/1oyfZ+PSRTqeDqm7eoFqtEoYhmUyG2dlZVJU4iREfI/WP3Nt9iMUdu7jdf5Anly5i0oaVlRWazSZmYWGBIAiIoohyucz09DQTExPMnJli9dlT5vcM8Kh3gFsHDuNqb9mb7yXMRBhjWFpawpRKJVKpFMYYgiAgDEOCIOD81BkunBjh8pEhKqUhGkvP6bQ/U//wgUP5/YRhSDabxbTbbVQV5xyq2q0kgR8NdOM7JKuo/Y5qggqIdPvMlnkrQCKCquJFsOrxeHAJxA48eFU6Xv4EqOpv41YqnQirqliv4MEmQtN7RBSs7wL+/gvb038DfgJnyUabbHzUbQAAAABJRU5ErkJggg==",
onClick: function() {
var xulwindows = getWindows("navigator:browser");
var xulactivewindow = getMostRecentBrowserWindow();
var xulactivetab = xulactivewindow.gBrowser.selectedTab;
xulwindows.forEach(function(win){
if(win === xulactivewindow)
return;
var duplicatedtab = win.gBrowser.duplicateTab(xulactivetab);
win.gBrowser.moveTabTo(duplicatedtab, 0); // the second argument is the index
});
}
});
#paa's solution is nice but it doesn't move a tab. His is duplicating the tab. So flash movies will not retain their position etc. And its not a move its a duplicatio, like he explained.
I did a lot of research was real fun. The way they move tabs in Firefox is via docShell swapping. This will accomplish what you want. It's written for bootstrap though so needs touch up for addon sdk.
Pass second argument as string of tabbed or non-tabbed if you want to move it to a new window. Else pass second argument an existing window and it will be moved there. can copy paste and run this code from sratchpad.
this uses the gBrowser.swapBrowsersAndCloseOther function
function moveTabToWin(aTab, tDOMWin) {
//tDOMWin means target DOMWindow means the window you want the tab in
//if tDOMWin == 'tabbed' or == 'non-tabbed' it opens in a new window
//if aTopContWin is the last in its window, then its window is closed
if (tDOMWin == 'tabbed' || tDOMWin == 'non-tabbed') {
var sa = Cc["#mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
var wuri = Cc["#mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
wuri.data = 'about:blank';
sa.AppendElement(wuri);
let features = "chrome,dialog=no";
if (tDOMWin == 'tabbed') {
features += ',all';
}
var sDOMWin = aTab.ownerGlobal; //source DOMWindow
if (PrivateBrowsingUtils.permanentPrivateBrowsing || PrivateBrowsingUtils.isWindowPrivate(sDOMWin)) {
features += ",private";
} else {
features += ",non-private";
}
var XULWindow = Services.ww.openWindow(null, 'chrome://browser/content/browser.xul', null, features, sa);
XULWindow.addEventListener('load', function() {
var DOMWindow = XULWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
DOMWindow.gBrowser.selectedTab.linkedBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
DOMWindow.gBrowser.swapBrowsersAndCloseOther(DOMWindow.gBrowser.selectedTab, aTab);
//DOMWindow.gBrowser.selectedTab = newTab;
}, false);
} else if (tDOMWin) {
//existing dom window
var newTab = tDOMWin.gBrowser.addTab('about:blank');
newTab.linkedBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
tDOMWin.gBrowser.swapBrowsersAndCloseOther(newTab, aTab);
tDOMWin.gBrowser.selectedTab = newTab;
}
}
moveTabToWin(gBrowser.selectedTab, 'tabbed');
I'v got inspired by #Noitidart's answer and came up with my solution.
I'm adding setWindow(window, index) method to Tab's prototype, so that any SDK tab can be moved to another window from anywhere in the addon with a simple call like this:
browserWindows[0].activeTab.setWindow(browserWindows.activeWindow, 0);
This will move active tab of window 0 to the beginning of active window.
And here is the method:
Update:
I've put together a module to do exactly this: jetpack-tab-setwindow
Old solution (breaks in FF43)
var Tab = require("sdk/tabs/tab").Tab;
Tab.prototype.setWindow = function (window, index) {
var tab = this;
var oldWindow = tab.window;
if ( oldWindow !== window ) {
// We have to use lower-level API here
var Ci = require('chrome').Ci;
var viewFor = require("sdk/view/core").viewFor;
var aTab = viewFor(tab);
var aWin = viewFor(window);
var gBrowser = aWin.gBrowser;
// Get tab properties
var isSelected = oldWindow.activeTab == tab;
var isPinned = aTab.pinned;
// Log for debugging:
var tabId = tab.id;
console.log('setWindow', {index, isSelected, isPinned, tab, tabId});
// Create a placeholder-tab on destination windows
var newTab = gBrowser.addTab('about:newtab');
newTab.linkedBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); // we don't need this tab anyways
// If index specified, move placeholder-tab to desired index
if ( index != undefined ) {
var length = gBrowser.tabContainer.childElementCount;
if ( index < 0 ) index = length - index;
if( 0 <= index && index < length ) {
gBrowser.moveTabTo(newTab, index);
}
}
// Copy tab properties to placeholder-tab
if ( isPinned ) {
gBrowser.pinTab(newTab);
}
// For some reason this doesn't seem to work :-(
if ( isSelected ) {
gBrowser.selectedTab = newTab;
}
// Swap tabs and remove placeholder-tab
gBrowser.swapBrowsersAndCloseOther(newTab, aTab);
}
};

Store & retrieve the identifiers of a multipliable widget's instances

The aim is to remove only the last row at any time and only by the last remove button.
There is a user interface which building up as a multiplication of the same row. The number of rows are controlled by 'Add' & 'Remove' buttons which are also elements of the row. The problem is that the hidden widgets - that are applied for each row to distinguish the instances by storing their row numbers - are storing the very same number which is the last one. Except the first (0) hidden widget which stores the proper number (0). Where am I missing the point? How should this be resolved?
As per the remove buttons have two different purposes (not detailed here), we use a cacheService to distinguish the last row from all the others. Only the last row should be removed at any time.
var cache = CacheService.getPrivateCache();
we clear the cache and create the first instance
function doGet() {
var app = UiApp.createApplication();
app.add(app.createVerticalPanel().setId('mainContainer'));
cache.removeAll([]);
ui(0);
cache.put('numberOfInstances',0);
return app; }
each instance is held by a horizontal panel which contains the mentioned hidden widget, a label which informs about the instance number, and the Add & Remove buttons.
function ui(instance) {
var app = UiApp.getActiveApplication();
var eventContainer = app.createHorizontalPanel()
.setId('eventContainer' + instance);
var instanceContainer = app.createHidden('instanceContainer',instance);
var showInstance = app.createLabel(instance)
.setId('showInstance' + instance);
var addButton = app.createButton('Add')
.setId('add' + instance)
.addClickHandler(app.createClientHandler()
.forEventSource().setEnabled(false)) //avoiding multiple click during server response
.addClickHandler(app.createServerHandler('add')
.addCallbackElement(instanceContainer));
var removeButton = app.createButton('X')
.addClickHandler(app.createServerHandler('remove')
.addCallbackElement(instanceContainer));
app.getElementById('mainContainer')
.add(eventContainer
.add(instanceContainer)
.add(showInstance)
.add(addButton)
.add(removeButton));
return app; }
and the event handling...
function add(inst) {
var app = UiApp.getActiveApplication();
var instance = Number(inst.parameter.instanceContainer);
ui(instance+1);
cache.put('numberOfInstances',instance+1);
return app; }
function remove(inst) {
var app = UiApp.getActiveApplication();
var instance = Number(inst.parameter.instanceContainer);
var numberOfInstances = cache.get('numberOfInstances')
if( (instance != 0) && (instance = numberOfInstances) ) {
app.getElementById('mainContainer').remove(app.getElementById('eventContainer' + instance));
cache.put('numberOfInstances',instance-1);
app.getElementById('add' + (instance-1)).setEnabled(true); } //avoiding multiple click during server response
return app; }
The aim is to remove only the last row at any time and only by the last remove button.
Many Thanks.
Why don't you simply use a clientHandler just as you did on the 'add' button? You could target the preceding 'remove' button and disable it each time you create a new one and change /update each time you remove one row.
EDIT : I can suggest you something, feel free to have a look, I changed a bit the approach but it is working and I hope you'll find it at least interesting ;-)
Link to the online test
function doGet() {
var app = UiApp.createApplication();
var counter = app.createHidden().setName('counter').setId('counter').setValue('1');
var mainContainer = app.createVerticalPanel().setId('mainContainer')
app.add(mainContainer.add(counter));
var event1Container = app.createHorizontalPanel()
var showInstance = app.createLabel('1')
var addButton = app.createButton('Add')
.setId('add1')
.addClickHandler(app.createClientHandler()
.forEventSource().setEnabled(false)) //avoiding multiple click during server response
.addClickHandler(app.createServerHandler('add')
.addCallbackElement(mainContainer));
var removeButton = app.createButton('X')
.setId('remove1')
.addClickHandler(app.createServerHandler('remove')
.addCallbackElement(mainContainer));
mainContainer.add(event1Container
.add(showInstance)
.add(addButton)
.add(removeButton));
return app; }
function add(inst) {
var app = UiApp.getActiveApplication();
var hiddenVal =inst.parameter.counter;
var counterVal = Number(hiddenVal);
var mainContainer = app.getElementById('mainContainer')
var counter = app.getElementById('counter')
++ counterVal
counter.setValue(counterVal.toString())
var eventContainer = app.createHorizontalPanel().setId('eventContainer'+counterVal)
var showInstance = app.createLabel(counterVal.toString())
var addButton = app.createButton('Add')
.setId('add'+counterVal)
.addClickHandler(app.createClientHandler()
.forEventSource().setEnabled(false)) //avoiding multiple click during server response
.addClickHandler(app.createServerHandler('add')
.addCallbackElement(mainContainer));
var removeButton = app.createButton('X')
.setId('remove'+counterVal)
.addClickHandler(app.createServerHandler('remove')
.addCallbackElement(mainContainer));
app.add(eventContainer
.add(showInstance)
.add(addButton)
.add(removeButton));
if(counterVal>1){app.getElementById('remove'+(counterVal-1)).setEnabled(false)}
return app; }
function remove(inst) {
var app = UiApp.getActiveApplication();
var counterVal = Number(inst.parameter.counter);
var counter = app.getElementById('counter')
if(counterVal ==1) {return app}
var maincontainer = app.getElementById('mainContainer')
app.getElementById('eventContainer' + counterVal).setVisible(false)
--counterVal
counter.setValue(counterVal.toString())
app.getElementById('add'+counterVal).setEnabled(true)
app.getElementById('remove'+counterVal).setEnabled(true)
return app;
}
NOTE : I didn't make use of .remove(widget) since this is a fairly new method and I don't know exactly how it works... I'll test it later. Until then I used setVisible(false) instead, sorry about that :-)
Note 2 : I didn't use the cache since the hidden widget is sufficient to keep track of what is going on... if you needed it for something else then you could always add it back .

Resources