Insert inline image to automatic email response via google forms - javascript

I am trying to insert a company logo into an automatic email that is sent out to customers who fill in a form via our website (which is linked to google forms). I am using the script to generate the automatic email and I can't seem to find out how to insert an inline image. I'm currently linking to an online image we have and it doesn't work well because the aspect ratio seems to vary depending on which device you are using.
Could someone please amend my code below so that it works with an inline image. Many thanks.
/**
* #OnlyCurrentDoc Limits the script to only accessing the current form.
*/
var DIALOG_TITLE = 'Example Dialog';
var SIDEBAR_TITLE = 'Example Sidebar';
/**
* Adds a custom menu with items to show the sidebar and dialog.
*
* #param {Object} e The event parameter for a simple onOpen trigger.
*/
function onOpen(e) {
FormApp.getUi()
.createAddonMenu()
.addItem('Show sidebar', 'showSidebar')
.addItem('Show dialog', 'showDialog')
.addToUi();
}
/**
* Runs when the add-on is installed; calls onOpen() to ensure menu creation and
* any other initializion work is done immediately.
*
* #param {Object} e The event parameter for a simple onInstall trigger.
*/
function onInstall(e) {
onOpen(e);
}
/**
* Opens a sidebar. The sidebar structure is described in the Sidebar.html
* project file.
*/
function showSidebar() {
var ui = HtmlService.createTemplateFromFile('Sidebar')
.evaluate()
.setTitle(SIDEBAR_TITLE);
FormApp.getUi().showSidebar(ui);
}
/**
* Opens a dialog. The dialog structure is described in the Dialog.html
* project file.
*/
function showDialog() {
var ui = HtmlService.createTemplateFromFile('Dialog')
.evaluate()
.setWidth(350)
.setHeight(170);
FormApp.getUi().showModalDialog(ui, DIALOG_TITLE);
}
/**
* Appends a new form item to the current form.
*
* #param {Object} itemData a collection of String data used to
* determine the exact form item created.
*/
function addFormItem(itemData) {
// Use data collected from sidebar to manipulate the form.
var form = FormApp.getActiveForm();
switch (itemData.type) {
case 'Date':
form.addDateItem().setTitle(itemData.name);
break;
case 'Scale':
form.addScaleItem().setTitle(itemData.name);
break;
case 'Text':
form.addTextItem().setTitle(itemData.name);
break;
}
}
/**
* Queries the form DocumentProperties to determine whether the formResponse
* trigger is enabled or not.
*
* #return {Boolean} True if the form submit trigger is enabled; false
* otherwise.
*/
function getTriggerState() {
// Retrieve and return the information requested by the dialog.
var properties = PropertiesService.getDocumentProperties();
return properties.getProperty('triggerId') != null;
}
/**
* Turns the form submit trigger on or off based on the given argument.
*
* #param {Boolean} enableTrigger whether to turn on the form submit
* trigger or not
*/
function adjustFormSubmitTrigger(enableTrigger) {
// Use data collected from dialog to manipulate form.
// Determine existing state of trigger on the server.
var form = FormApp.getActiveForm();
var properties = PropertiesService.getDocumentProperties();
var triggerId = properties.getProperty('triggerId');
if (!enableTrigger && triggerId != null) {
// Delete the existing trigger.
var triggers = ScriptApp.getUserTriggers(form);
for (var i = 0; i < triggers.length; i++) {
if (triggers[i].getUniqueId() == triggerId) {
ScriptApp.deleteTrigger(triggers[i]);
break;
}
}
properties.deleteProperty('triggerId');
} else if (enableTrigger && triggerId == null) {
// Create a new trigger.
var trigger = ScriptApp.newTrigger('respondToFormSubmit')
.forForm(form)
.onFormSubmit()
.create();
properties.setProperty('triggerId', trigger.getUniqueId());
}
}
/**
* Responds to form submit events if a form summit trigger is enabled.
* Collects some form information and sends it as an email to the form creator.
*
* #param {Object} e The event parameter created by a form
* submission; see
* https://developers.google.com/apps-script/understanding_events
*/
function respondToFormSubmit(e) {
if (MailApp.getRemainingDailyQuota() > 0) {
var form = FormApp.getActiveForm();
var message = 'There have been ' + form.getResponses().length +
' response(s) so far. Latest Response:\n';
var itemResponses = e.response.getItemResponses();
for (var i = 0; i < itemResponses.length; i++) {
var itemTitle = itemResponses[i].getItem().getTitle();
var itemResponse = JSON.stringify(itemResponses[i].getResponse());
message += itemTitle + ': ' + itemResponse + '\n';
}
MailApp.sendEmail(
Session.getEffectiveUser().getEmail(),
'Form response received for form ' + form.getTitle(),
message,
{name: 'Forms Add-on Template'});
}
}
/* Send Confirmation Email with Google Forms */
function Initialize() {
var triggers = ScriptApp.getProjectTriggers();
for (var i in triggers) {
ScriptApp.deleteTrigger(triggers[i]);
}
ScriptApp.newTrigger("SendConfirmationMail")
.forSpreadsheet(SpreadsheetApp.getActiveSpreadsheet())
.onFormSubmit()
.create();
}
function SendConfirmationMail(e) {
try {
var ss, bcc, sendername, subject, columns, submitter;
var message, value, textbody, sender, aliases;
// Log the aliases for this Gmail account and send an email as the first one.
var aliases = GmailApp.getAliases();
// This is your email address and you will be in the CC
bcc = aliases[0];
// This will show up as the sender's name
sendername = "The Flatser Team";
// This is the submitter's name
submitter = e.namedValues["Full Name"].toString();
// This is the submitter's email address
sender = e.namedValues["Email Address"].toString();
// Optional but change the following variable
// to have a custom subject for Google Docs emails
subject = "Please select a call time";
// This is the body of the auto-reply
message = "Dear "+ submitter + ", <br/><br/>Thank you for completing our form. Please fill in your availability for a telephone or skype call using the link below.<br/><br/>https://calendly.com/bcwolf/flatser-skype-call/06-23-2015<br/><br/><br/>Kind regards,<br/><br/>--<br/><br/><b>The Flatser Team</b><br/><a href='www.flatser.com'>www.flatser.com</a><br/><br/><img width='10%' height='10%' src='http://flatser.com/img/logo1.png'/>";
ss = SpreadsheetApp.getActiveSheet();
columns = ss.getRange(1, 1, 1, ss.getLastColumn()).getValues()[0];
textbody = message.replace("<br>", "\n");
GmailApp.sendEmail(sender, subject, textbody,
{bcc: bcc, name: sendername, htmlBody: message, from: aliases[0] });
} catch (e) {
Logger.log(e.toString());
}
}

You can set different widths according to different devices in your email using media queries.
In your case, i would prepend the <style> in your message variable for function SendConfirmationMail and add class to img tag inside the message variable which will then look as follows:
BODY OF MESSAGE ( assuming you need 100px width logo on mobile and 200px width on bigger screens. Also note do not set the height as it will ruin the aspect ratio if you calculate it incorrectly)
// This is the body of the auto-reply
// This is what i would add
message = "<style> #media (max-width:499px) { .logo__flaster { width: 100px; } #media (min-width:500px) { .logo__flaster { width: 200px; } </style>"
// This is same, i have only removed width from img and added class logo__flaster which i used in the style above.
message += "Dear "+ submitter + ", <br/><br/>Thank you for completing our form. Please fill in your availability for a telephone or skype call using the link below.<br/><br/>https://calendly.com/bcwolf/flatser-skype-call/06-23-2015<br/><br/><br/>Kind regards,<br/><br/>--<br/><br/><b>The Flatser Team</b><br/><a href='www.flatser.com'>www.flatser.com</a><br/><br/><img class='logo__flaster' src='http://flatser.com/img/logo1.png'/>";
Please read Media Queries in HTML Email article for more information, Also see more media queries templates if you want to customise more to a specific screen.

Related

How to autofill Google form with random answears using JS

I want to randomly fill a 100 google forms using JS. Is any way to do it?
There is example google form.
I couldn't find anything on stackoverflow or web, only python or java solutions. But I want to do it in javascript if it is possible of course.
Here is a dirty script, which could be a starting point. It only works with the specific form you provided as an example. It uses document.querySelector to target the form elements.
As soon as you'll open the form, it will fill it, submit it, go back to it, submit it, over and over.
To use it:
Install the TamperMonkey extension in Google Chrome
Click on the icon that appeared in your browser, select "Dashboard"
Create a new script, and replace all the content with the code below
Ctrl + S to save
Open the form in a tab and watch it do the work
Code:
// ==UserScript==
// #name GoogleForm Spammer
// #namespace http://tampermonkey.net/
// #version 0.1
// #description Spam a Google Form
// #author You
// #match https://docs.google.com/forms/*
// #grant unsafeWindow
// ==/UserScript==
(function() {
window.addEventListener('load', function() {
if (window.location.pathname.indexOf('/forms/d') === 0) { // If we're on the form page
submitRandomForm();
} else if (window.location.pathname.indexOf('/forms/u') === 0) { // If we're on the "submitted" page
goBackToForm();
}
function submitRandomForm() {
// Size
var radios = document.querySelectorAll(".appsMaterialWizToggleRadiogroupRadioButtonContainer"),
radioIndex = Math.floor(Math.random() * radios.length);
radios[radioIndex].click();
// Print
var checkboxes = document.querySelectorAll(".appsMaterialWizTogglePapercheckboxCheckbox"),
checkboxIndex = Math.floor(Math.random() * checkboxes.length);
checkboxes[checkboxIndex].click();
// Age (between 16 and 45)
var age = Math.floor(Math.random() * 30) + 16;
document.querySelector(".quantumWizTextinputPaperinputInput").value = age;
// Submit
document.querySelector(".freebirdFormviewerViewCenteredContent .appsMaterialWizButtonPaperbuttonLabel").click();
}
function goBackToForm() {
window.location.href = 'https://docs.google.com/forms/d/e/1FAIpQLSd7GueJGytOiQpkhQzo_dCU0oWwbk3L1htKblBO1m14VHSpHw/viewform';
}
});
})();
And here is a little cleaner way. You declare the form URL at the top, the form fields, and for some of them, a function which will return a random value according to your needs.
To try this one out, save that script, and try accessing this form:
// ==UserScript==
// #name GoogleForm Spammer
// #namespace http://tampermonkey.net/
// #version 0.1
// #description Spam a Google Form
// #author You
// #match https://docs.google.com/forms/*
// #grant none
// ==/UserScript==
var formUrl = 'https://docs.google.com/forms/d/e/1FAIpQLSdQ9iT7isDU8IIbyg-wowB-9HGzyq-xu2NyzsOeG0j8fhytmA/viewform';
var formSchema = [
{type: 'radio'}, // A
{type: 'radio'}, // B
{type: 'checkbox'}, // C
{type: 'checkbox'}, // D
{type: 'short_text', func: generateAnswerE }, // E
{type: 'paragraph', func: generateParagraph }, // F
];
function generateAnswerE() {
// Let's say we want a random number
return Math.floor(Math.random() * 30) + 16;
}
function generateParagraph() {
// Just for the example
return "Hello world";
}
(function() {
window.addEventListener('load', function() {
if (window.location.pathname.indexOf('/forms/d') === 0) { // If we're on the form page
submitRandomForm();
} else if (window.location.pathname.indexOf('/forms/u') === 0) { // If we're on the "submitted" page
window.location.href = formUrl;
}
function submitRandomForm() {
var formItems = document.querySelectorAll('.freebirdFormviewerViewItemsItemItem');
for (var i = 0; i < formSchema.length; i++) {
var field = formSchema[i],
item = formItems[i];
switch(field.type) {
case 'radio':
var radios = item.querySelectorAll(".appsMaterialWizToggleRadiogroupRadioButtonContainer"),
radioIndex = Math.floor(Math.random() * radios.length);
radios[radioIndex].click();
break;
case 'checkbox':
var checkboxes = item.querySelectorAll(".appsMaterialWizTogglePapercheckboxCheckbox"),
checkboxIndex = Math.floor(Math.random() * checkboxes.length);
checkboxes[checkboxIndex].click();
break;
case 'short_text':
item.querySelector(".quantumWizTextinputPaperinputInput").value = field.func();
break;
case 'paragraph':
item.querySelector(".quantumWizTextinputPapertextareaInput").value = field.func();
break;
}
}
// Submit
document.querySelector(".freebirdFormviewerViewCenteredContent .appsMaterialWizButtonPaperbuttonLabel").click();
}
});
})();

How to get the first revision without having a delete button?

<div class="right modal-footer">
<a class="modal-action waves-effect btn-flat left" ng-switch-when="true" ng-click="delete()">Delete</a>
<a class="modal-action waves-effect btn-flat" ng-click="close()">Cancel</a>
<a class="modal-action waves-effect btn-flat" ng-click="save()">Save</a>
</div>
</div>
The first revision or revision A should not have a delete button. Every other revision needs to have one, so that's revision B, C, etc. Does anyone have any ideas on how i can do this?
The above part is the footer for the summary revision dialog. It includes the action buttons of save, cancel, and delete. Delete button is the main focus here.
The below part is the JavaScript controller code that tells what the button to do. When the revision is opened it will display the original revisions details along with which revision this is. the close function is simple, just simply closing the dialog without saving the entered and changed information. The save function is also pretty simple, saves the entered data and will show the changes when the save button is clicked on. The Delete function will delete the current revision and move back the the revision before it, so for example, deleting revision C will display revision B. But what I am trying to do is hide the delete button on Revision A(the first revision) so it will not delete the initial revision and keep the delete button display for any other revision.
Thank you developers for your help.
angular.module('Comet').controller('RevisionEditController', function ($scope, $rootScope, $objectExtensions, $odataModel, $validation, $toast, ProposalsService, ErrorsService) {
const DIALOG_SEL = '#revisionEditDialog';
$scope.originalRevision = null;
$scope.revision = null;
/**
* Opens the dialog.
* #param {number} proposal - The proposal that the revision is for.
* #param {object} [revision] - The existing revision to edit. Null when creating a new revision.
*/
$scope.open = function (proposal, revision) {
$scope.originalRevision = new $odataModel.ProposalDetail(revision);
$scope.revision = new $odataModel.ProposalDetail(revision);
$scope.revision.rev = revision ? revision.rev : getNextRevision(proposal.proposalDetails);
$scope.revision.proposal = {
id: proposal.id
};
$(DIALOG_SEL).modal('open');
};
/**
* Closes the dialog.
*/
$scope.close = function () {
$(DIALOG_SEL).modal('close');
};
/**
* Saves the revision.
*/
$scope.save = function () {
if ($validation.validate($scope.revisionEditForm)) {
$rootScope.dataReady = false;
if ($scope.revision.id) {
ProposalsService.editProposalDetail($scope.revision.proposal.id, $scope.revision.id, $scope.originalRevision, $scope.revision)
.then(onSaveSuccess, onSaveFailure);
} else {
ProposalsService.addProposalDetail($scope.revision.proposal.id, $scope.revision)
.then(onSaveSuccess, onSaveFailure);
}
}
};
/**
* Deletes the revision.
*/
$scope.delete = function () {
ProposalsService.deleteProposalDetail($scope.revision.proposal.id, $scope.revision.id)
.then(onDeleteSuccess, onDeleteFailure);
};
/**
* Calls the revision updated callback and closes the dialog.
* #param {object} updatedRevision - The updated revision.
*/
function onSaveSuccess(updatedRevision) {
$scope.$ctrl.onRevisionUpdated({ revision: updatedRevision });
$scope.close();
$rootScope.dataReady = true;
}
/**
* Displays an error message and logs the exception.
* #param {object} ex - The exception to log.
*/
function onSaveFailure(ex) {
$toast.error('There was an error saving the revision. Please try again.');
ErrorsService.log(ex);
$rootScope.dataReady = true;
}
/**
* Calls the revision deleted callback and closes the dialog.
* #param {number} id - The ID of the revision that was deleted.
*/
function onDeleteSuccess(id) {
$scope.$ctrl.onRevisionDeleted({ id: id });
$scope.close();
$rootScope.dataReady = true;
}
/**
* Displays an error message and logs the exception.
* #param {object} ex - The exception to log.
*/
function onDeleteFailure(ex) {
$toast.error('There was an error deleting the revision. Please try again.');
ErrorsService.log(ex);
$rootScope.dataReady = true;
}
/**
* Gets the next revision number.
* #param {object[]} revisions - The previous revisions.
* #returns {string} The next revision number.
*/
function getNextRevision(revisions) {
var nextRevision = '';
var latestRevision = revisions
.sort(function (a, b) {
var order = 0;
if (a.id > b.id)
order = -1;
else if (a.id < b.id)
order = 1;
return order;
})[0];
if (latestRevision) {
nextRevision = latestRevision.rev ? '' : 'A';
var increment = true;
for (var idx = latestRevision.rev.length - 1; idx >= 0 && increment; idx--) {
var currLetter = latestRevision.rev[idx].charCodeAt(0);
if (currLetter == 90) {
nextRevision = nextRevision + 'A';
} else {
increment = false;
nextRevision = String.fromCharCode(currLetter + 1) + nextRevision;
}
}
if (nextRevision.length < latestRevision.rev.length) {
nextRevision = latestRevision.substring(0, nextRevision.length - 1) + nextRevision;
}
}
return nextRevision;
}
I assume that there is a loop where you iterate the revisions, use the loop's index to check with ngIf if the button should be shown.

How to poll a Google Doc from an add-on

A documented restriction with document and sheet add-ons is that Apps Script cannot tell what a user does outside of the add-on. This tantalizing tip is given:
It is possible to poll for changes in a file's contents from a
sidebar's client-side code, although you'll always have a slight
delay. That technique can also alert your script to changes in the
user's selected cells (in Sheets) and cursor or selection (in Docs).
Sadly, this isn't shown in any of the demo code. How can I do it?
The polling is done from the html code in your add-on's User Interface, calling across to server-side Apps Script functions using google.script.run.
Using jQuery simplifies this, and we can even start with the answers from jQuery, simple polling example.
function doPoll(){
$.post('ajax/test.html', function(data) {
alert(data); // process results here
setTimeout(doPoll,5000);
});
}
The basic idea can work for Google Apps Script, if we replace the ajax calls with the GAS equivalents.
Here's the skeleton of the poll function that you would use in your html file:
/**
* On document load, assign click handlers to button(s), add
* elements that should start hidden (avoids "flashing"), and
* start polling for document updates.
*/
$(function() {
// assign click handler(s)
// Add elements that should start hidden
// Start polling for updates
poll();
});
/**
* Poll a server-side function 'serverFunction' at the given interval
* and update DOM elements with results.
*
* #param {Number} interval (optional) Time in ms between polls.
* Default is 2s (2000ms)
*/
function poll(interval){
interval = interval || 2000;
setTimeout(function(){
google.script.run
.withSuccessHandler(
function(results) {
$('#some-element').updateWith(results);
//Setup the next poll recursively
poll(interval);
})
.withFailureHandler(
function(msg, element) {
showError(msg, $('#button-bar'));
element.disabled = false;
})
.serverFunction();
}, interval);
};
Add-on Example, Document Poller
This is a demonstration of the jQuery polling technique calling server-side Google Apps Script functions to detect user behavior in a Google Document. It does nothing useful, but it showcases a few things that would typically require knowledge of the user's activity and state of the document, for instance context-sensitve control over a button.
The same principle could apply to a spreadsheet, or a stand-alone GAS Web Application.
Like the UI App example in this question, this technique could be used to get around execution time limits, for operations with a User Interface at least.
The code builds upon the example add-on from Google's 5-minute quickstart. Follow the instructions from that guide, using the code below instead of that in the quickstart.
Code.gs
/**
* Creates a menu entry in the Google Docs UI when the document is opened.
*
* #param {object} e The event parameter for a simple onOpen trigger. To
* determine which authorization mode (ScriptApp.AuthMode) the trigger is
* running in, inspect e.authMode.
*/
function onOpen(e) {
DocumentApp.getUi().createAddonMenu()
.addItem('Start', 'showSidebar')
.addToUi();
}
/**
* Runs when the add-on is installed.
*
* #param {object} e The event parameter for a simple onInstall trigger. To
* determine which authorization mode (ScriptApp.AuthMode) the trigger is
* running in, inspect e.authMode. (In practice, onInstall triggers always
* run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or
* AuthMode.NONE.)
*/
function onInstall(e) {
onOpen(e);
}
/**
* Opens a sidebar in the document containing the add-on's user interface.
*/
function showSidebar() {
var ui = HtmlService.createHtmlOutputFromFile('Sidebar')
.setTitle('Document Poller');
DocumentApp.getUi().showSidebar(ui);
}
/**
* Check if there is a current text selection.
*
* #return {boolean} 'true' if any document text is selected
*/
function checkSelection() {
return {isSelection : !!(DocumentApp.getActiveDocument().getSelection()),
cursorWord : getCursorWord()};
}
/**
* Gets the text the user has selected. If there is no selection,
* this function displays an error message.
*
* #return {Array.<string>} The selected text.
*/
function getSelectedText() {
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection) {
var text = [];
var elements = selection.getSelectedElements();
for (var i = 0; i < elements.length; i++) {
if (elements[i].isPartial()) {
var element = elements[i].getElement().asText();
var startIndex = elements[i].getStartOffset();
var endIndex = elements[i].getEndOffsetInclusive();
text.push(element.getText().substring(startIndex, endIndex + 1));
} else {
var element = elements[i].getElement();
// Only translate elements that can be edited as text; skip images and
// other non-text elements.
if (element.editAsText) {
var elementText = element.asText().getText();
// This check is necessary to exclude images, which return a blank
// text element.
if (elementText != '') {
text.push(elementText);
}
}
}
}
if (text.length == 0) {
throw 'Please select some text.';
}
return text;
} else {
throw 'Please select some text.';
}
}
/**
* Returns the word at the current cursor location in the document.
*
* #return {string} The word at cursor location.
*/
function getCursorWord() {
var cursor = DocumentApp.getActiveDocument().getCursor();
var word = "<selection>";
if (cursor) {
var offset = cursor.getSurroundingTextOffset();
var text = cursor.getSurroundingText().getText();
word = getWordAt(text,offset);
if (word == "") word = "<whitespace>";
}
return word;
}
/**
* Returns the word at the index 'pos' in 'str'.
* From https://stackoverflow.com/questions/5173316/finding-the-word-at-a-position-in-javascript/5174867#5174867
*/
function getWordAt(str, pos) {
// Perform type conversions.
str = String(str);
pos = Number(pos) >>> 0;
// Search for the word's beginning and end.
var left = str.slice(0, pos + 1).search(/\S+$/),
right = str.slice(pos).search(/\s/);
// The last word in the string is a special case.
if (right < 0) {
return str.slice(left);
}
// Return the word, using the located bounds to extract it from the string.
return str.slice(left, right + pos);
}
Sidebar.html
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<!-- The CSS package above applies Google styling to buttons and other elements. -->
<div class="sidebar branding-below">
<form>
<div class="block" id="button-bar">
<button class="blue" id="get-selection" disabled="disable">Get selection</button>
</div>
</form>
</div>
<div class="sidebar bottom">
<img alt="Add-on logo" class="logo" height="27"
id="logo"
src="https://www.gravatar.com/avatar/adad1d8ad010a76a83574b1fff4caa46?s=128&d=identicon&r=PG">
<span class="gray branding-text">by Mogsdad, D.Bingham</span>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script>
/**
* On document load, assign click handlers to button(s), add
* elements that should start hidden (avoids "flashing"), and
* start polling for document selections.
*/
$(function() {
// assign click handler(s)
$('#get-selection').click(getSelection);
// Add elements that should start hidden
var newdiv1 = $( "<div class='block' id='cursor-word'/>" ).hide(),
newdiv2 = $( "<div class='block' id='selected-text'/>" ).hide();
$('#button-bar').after( newdiv1, newdiv2 );
$('#cursor-word').html('<H2>Word at cursor:</H2><p id="cursor-word-content"></p>');
$('#selected-text').html('<H2>Selected text:</H2><p id="selected-text-content"></p>');
// Start polling for updates
poll();
});
/**
* Poll the server-side 'checkSelection' function at the given
* interval for document selection, and enable or disable the
* '#get-selection' button.
*
* #param {Number} interval (optional) Time in ms between polls.
* Default is 2s (2000ms)
*/
function poll(interval){
interval = interval || 2000;
setTimeout(function(){
google.script.run
.withSuccessHandler(
function(cursor) {
if (cursor.isSelection) {
// Text has been selected: enable button, hide cursor word.
$('#get-selection').attr('disabled', false);
$('#cursor-word').hide();
// $('#selected-text').show(); // Not so fast - wait until button is clicked.
}
else {
$('#get-selection').attr('disabled', true);
$('#cursor-word').show();
$('#selected-text').hide();
}
$('#cursor-word-content').text(cursor.cursorWord);
//Setup the next poll recursively
poll(interval);
})
.withFailureHandler(
function(msg, element) {
showError(msg, $('#button-bar'));
element.disabled = false;
})
.checkSelection();
}, interval);
};
/**
* Runs a server-side function to retrieve the currently
* selected text.
*/
function getSelection() {
this.disabled = true;
$('#error').remove();
google.script.run
.withSuccessHandler(
function(selectedText, element) {
// Show selected text
$('#selected-text-content').text(selectedText);
$('#selected-text').show();
element.disabled = false;
})
.withFailureHandler(
function(msg, element) {
showError(msg, $('#button-bar'));
element.disabled = false;
})
.withUserObject(this)
.getSelectedText();
}
/**
* Inserts a div that contains an error message after a given element.
*
* #param msg The error message to display.
* #param element The element after which to display the error.
*/
function showError(msg, element) {
var div = $('<div id="error" class="error">' + msg + '</div>');
$(element).after(div);
}
</script>
Polling Interval
The setTimeout() function accepts a time interval expressed in milliseconds, but I found through experimentation that a two-second response was the best that could be expected. Therefore, the skeleton poll() has a 2000ms interval as its default. If your situation can tolerate a longer delay between poll cycles, then provide a larger value with the onLoad call to poll(), e.g. poll(10000) for a 10-second poll cycle.
Sheets
For a sheet example see How do I make a Sidebar display values from cells?

How does Firefox Inspector's Walker work?

In Firefox, at the start of modules/devtools/inspector/inspector-panel.js you see some references to a "walker", shown at the end of this snippet:
...
/**
* Represents an open instance of the Inspector for a tab.
* The inspector controls the highlighter, the breadcrumbs,
* the markup view, and the sidebar (computed view, rule view
* and layout view).
*
* Events:
* - ready
* Fired when the inspector panel is opened for the first time and ready to
* use
* - new-root
* Fired after a new root (navigation to a new page) event was fired by
* the walker, and taken into account by the inspector (after the markup
* view has been reloaded)
* - markuploaded
* Fired when the markup-view frame has loaded
* - layout-change
* Fired when the layout of the inspector changes
* - breadcrumbs-updated
* Fired when the breadcrumb widget updates to a new node
* - layoutview-updated
* Fired when the layoutview (box model) updates to a new node
* - markupmutation
* Fired after markup mutations have been processed by the markup-view
* - computed-view-refreshed
* Fired when the computed rules view updates to a new node
* - computed-view-property-expanded
* Fired when a property is expanded in the computed rules view
* - computed-view-property-collapsed
* Fired when a property is collapsed in the computed rules view
* - rule-view-refreshed
* Fired when the rule view updates to a new node
*/
function InspectorPanel(iframeWindow, toolbox) {
this._toolbox = toolbox;
this._target = toolbox._target;
this.panelDoc = iframeWindow.document;
this.panelWin = iframeWindow;
this.panelWin.inspector = this;
this._inspector = null;
this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
this._target.on("will-navigate", this._onBeforeNavigate);
EventEmitter.decorate(this);
}
exports.InspectorPanel = InspectorPanel;
InspectorPanel.prototype = {
/**
* open is effectively an asynchronous constructor
*/
open: function InspectorPanel_open() {
return this.target.makeRemote().then(() => {
return this._getWalker();
}).then(() => {
return this._getDefaultNodeForSelection();
}).then(defaultSelection => {
return this._deferredOpen(defaultSelection);
}).then(null, console.error);
},
get inspector() {
if (!this._target.form) {
throw new Error("Target.inspector requires an initialized remote actor.");
}
if (!this._inspector) {
this._inspector = InspectorFront(this._target.client, this._target.form);
}
return this._inspector;
},
_deferredOpen: function(defaultSelection) {
let deferred = promise.defer();
this.outerHTMLEditable = this._target.client.traits.editOuterHTML;
this.onNewRoot = this.onNewRoot.bind(this);
this.walker.on("new-root", this.onNewRoot);
this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
this.lastNodemenuItem = this.nodemenu.lastChild;
this._setupNodeMenu = this._setupNodeMenu.bind(this);
this._resetNodeMenu = this._resetNodeMenu.bind(this);
this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
// Create an empty selection
this._selection = new Selection(this.walker);
this.onNewSelection = this.onNewSelection.bind(this);
this.selection.on("new-node-front", this.onNewSelection);
this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
this.selection.on("before-new-node-front", this.onBeforeNewSelection);
this.onDetached = this.onDetached.bind(this);
this.selection.on("detached-front", this.onDetached);
this.breadcrumbs = new HTMLBreadcrumbs(this);
if (this.target.isLocalTab) {
this.browser = this.target.tab.linkedBrowser;
this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
// Show a warning when the debugger is paused.
// We show the warning only when the inspector
// is selected.
this.updateDebuggerPausedWarning = function() {
let notificationBox = this._toolbox.getNotificationBox();
let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
if (!notification && this._toolbox.currentToolId == "inspector" &&
this.target.isThreadPaused) {
let message = this.strings.GetStringFromName("debuggerPausedWarning.message");
notificationBox.appendNotification(message,
"inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH);
}
if (notification && this._toolbox.currentToolId != "inspector") {
notificationBox.removeNotification(notification);
}
if (notification && !this.target.isThreadPaused) {
notificationBox.removeNotification(notification);
}
}.bind(this);
this.target.on("thread-paused", this.updateDebuggerPausedWarning);
this.target.on("thread-resumed", this.updateDebuggerPausedWarning);
this._toolbox.on("select", this.updateDebuggerPausedWarning);
this.updateDebuggerPausedWarning();
}
this.highlighter = new Highlighter(this.target, this, this._toolbox);
let button = this.panelDoc.getElementById("inspector-inspect-toolbutton");
this.onLockStateChanged = function() {
if (this.highlighter.locked) {
button.removeAttribute("checked");
this._toolbox.raise();
} else {
button.setAttribute("checked", "true");
}
}.bind(this);
this.highlighter.on("locked", this.onLockStateChanged);
this.highlighter.on("unlocked", this.onLockStateChanged);
this._initMarkup();
this.isReady = false;
this.once("markuploaded", function() {
this.isReady = true;
// All the components are initialized. Let's select a node.
this._selection.setNodeFront(defaultSelection);
this.markup.expandNode(this.selection.nodeFront);
this.emit("ready");
deferred.resolve(this);
}.bind(this));
this.setupSearchBox();
this.setupSidebar();
return deferred.promise;
},
_onBeforeNavigate: function() {
this._defaultNode = null;
this.selection.setNodeFront(null);
this._destroyMarkup();
this.isDirty = false;
},
_getWalker: function() {
return this.inspector.getWalker().then(walker => {
this.walker = walker;
return this.inspector.getPageStyle();
}).then(pageStyle => {
this.pageStyle = pageStyle;
});
},
...
I didn't see this Promise documented anywhere in the Addon APIs, is there any documentation (or even source comments) on what this is, and how it is used?
Could it be used to add special styling or append some icons to certain elements in the DOM tree view of the Firefox DevTools Inspector?
Whenever "walker" is mentioned in the devtools code, it usually refers to the WalkerActor class in toolkit/devtools/server/actors/inspector.js.
Actors are javascript classes that are specifically made to get information from the currently inspected page and context or manipulate them.
The UI part of the tools you see as a user don't do that directly. Indeed, the devtools use a client-server protocol to communicate between the toolbox (that hosts all of the panels you use) and the actors that run as part of the page being inspected. This is what allows to use the tools to inspect remote devices.
When it comes to the WalkerActor in particular, its role is to traverse the DOM tree and give information about nodes to the inspector-panel so that it can be displayed in the tools.
What you see when you open the devtools inspector on a page is a part of the DOM tree (a part only because it's not entirely expanded and collapsed nodes haven't been retrieved yet) that has been retrieved by the WalkerActor and sent (via the protocol) to the inspector panel.
The actual UI rendering of the panel is done on the client-side (which means, the toolbox side, in comparison with the actors/page side), in browser/devtools/markupview/markup-view.js
In this file, MarkupView and MarkupContainer classes in particular are responsible for displaying nodes. They're not specifically part of the Addons API, but it should be relatively easy to get hold of them since privileged code has access to the gDevTools global variable:
Cu.import("resource://gre/modules/devtools/Loader.jsm");
let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = gDevTools.getToolbox(target);
let inspectorPanel = toolbox.getPanel("inspector");
inspector.markup // Returns the instance of MarkupView that is currently loaded in the inspector

How do I toggle my website title when my users have unread messages from their friend?

Like in facebook, when we're chatting with someone and during that time while we're browsing another site in another tab, Facebook tab's title toggles, once it's 'Facebook', then it's 'Rehan messaged you'. How do I achieve this?
document.title = 'Rehan messaged you';
The above javascript code will change my site's title. But how do I make it toggle with the original Title which is the name of my website 'MySite.Com'?
Here is a relatively robust solution where you can add/remove any number of messages, each of which will be iterated over while displaying the base message in between each at your specified toggle interval.
/***Framework Code***/
//Configs
var BASE_MESSAGE = "MySite.Com";//Message to display every other time
var toggleInterval = 1000;//One second between each toggle
//Private Variables
var otherMessages = [];
var currentMessageIndex = 0;
/**
* Updates the title, switching between the base text and iterating through
* the other messages
*/
var updateTitle = function(){
//Toggle between messages
if((currentMessage !== BASE_MESSAGE) || !otherMessages.length){
//Display base message every other time
document.title = BASE_MESSAGE;
} else {
//Display the next message in the list
currentMessageIndex = currentMessageIndex++ %otherMessages.length;
document.title = otherMessages[currentMessageIndex];
}
//Call again only if there are messages to show
if(otherMessages.length){
setTimeout(toggleInterval, updateTitle);
}
};
/**
* Adds a title message to display
*/
var addTitleMessage = function(message){
otherMessages.push(message);
//Avoid too many title updates
if(otherMessages.length == 1){
updateTitle();
}
};
/**
* Removes the specified title message from being displayed
*/
var removeTitleMessage = function(message){
var removeIndex = otherMessages.indexOf(message);
if(removeIndex != -1){
otherMessages.splice(removeIndex, 1);
}
};
.
/*** Your Code ***/
var newMessage = function(messageInfo){
..//Your normal new message code
addTitleMessage(getMyDisplayMessage(messageInfo));
};
var readMessage = function(messageInfo){
..//Your normal read code
removeTitleMessage(getMyDisplayMessage(messageInfo));
};
var getMyDisplayMessage = function(messageInfo){
return messageInfo.name + " has messaged you.";
}
You can do it using the setInterval function:
var cont = 0;
setInterval(function () {
if (cont % 2) {
var myText = 'Text 1';
} else {
var myText = 'Another text';
}
document.title = myText;
cont++;
},1000);
http://jsfiddle.net/7bMjN/
I made use of jQuery but it is not needed.
Use setInterval() or setTimeout(). In your case, code may be like that:
function toggleTitle() {
if (document.title == 'MySite.Com') document.title = 'Rehan messsaged you';
else document.title = 'MySite.Com'
}
var timer = setInterval(toggleTitle, 500);
this code will make your title change every 0.5 seconds. To stop that, use
clearInterval(timer);

Categories