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?
Related
<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.
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.
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
I have a random image script, which displays random images on each page load. I want to add Next, Previous and Random buttons to this script, but don't know how to implement them.
Here's the Script
<script type="text/javascript">
var Statements = new Array(
'<img src="http://4.bp.blogspot.com/_UdzqQpb36Jo/R9kVS0h1BFI/AAAAAAAAD_o/SRGugAQSF0A/s1600/timming_pictures_37.jpg" height="650" width="625">',
'<img src="http://4.bp.blogspot.com/_UdzqQpb36Jo/SCxOksTrn4I/AAAAAAAAFDg/q3RilNGj9kc/s1600/loving_husbands_03.jpg" height="650" width="625">',
'<img src="http://3.bp.blogspot.com/_lCRnsgTQgRo/Se2KNaw6bpI/AAAAAAAAA5c/yV2PCN0Pmyo/s1600/pic22806.jpg" height="650" width="625">',
'<img src="http://1.bp.blogspot.com/_lCRnsgTQgRo/Se2J4mZjNEI/AAAAAAAAA4s/Q6Z8IlWLS-A/s1600/pic14006.jpg" height="650" width="625">'
);
function GetStatement(outputtype)
{
if(++Number > Statements.length - 1) Number = 0;
if (outputtype==0)
document.write(Statements[Number])
else if (document.getElementById)
document.getElementById("ponder").innerHTML=Statements[Number];
}
function GetRandomNumber(lbound, ubound)
{
return (Math.floor(Math.random() * (ubound - lbound)) + lbound);
}
var Number = GetRandomNumber(0, Statements.length - 1);
</script>
<script type="text/javascript">
GetStatement(0)
</script>
PS. I am using this script in blogger blog as blogpost.
Based on the request from first my answer, here the code:
HTML code:
<body onload="showPicNo(a)">
<div id="picture"><img name="picturegallery"></div>
<div id="navigation">
Backward |
Forward |
Share site
</div>
<body>
And the modifed Javascript code:
/*
The array 'pics' contains all adresses of picture you want to show. Scrope: global
*/
var pics=new Array (
"https://www.gravatar.com/avatar/9be5d328127c377109b295b5941733fb?s=32&d=identicon&r=PG",
"https://www.gravatar.com/avatar/1e81ddcda5d50a39c2beeaba3797702a?s=32&d=identicon&r=PG&f=1",
"https://www.gravatar.com/avatar/f47359966d28f2e603b6f759855970db?s=32&d=identicon&r=PG&f=1",
"https://www.gravatar.com/avatar/7d1a2026b0dca412b04ec548397b37f6?s=32&d=identicon&r=PG"
);
/*
The variable to used as the minimum number of elements and container for the current picture.
Because we work with an array, the index from first array element is zero.
Scope: global
*/
var a = 0;
/*
The variabe that used as the maximum number of showed pictures.
Here: The number of elements from array 'pics'. Scrope: global
*/
var b = pics.length;
/*
The current url that contains the adressbar from browser. Scope: global
*/
var currentUrl = document.URL;
/*
---------------------------------------------------------------------------------
Initial quicktest, if the parameter 'picnoprogram' does exits.
The parameter 'picnoprogram' is used parallel as an indicator and
as persistance layer.
Scope: global
*/
var existsPicParameter = currentUrl.match(/picnoparam\S*/i);
/*
Some helper variables. Scope: global
*/
var tmpPicParameter, tmpPicParameterOld;
/*
First requirement: If the page was loaded at the first time, it shall be show a
random picture from all available pictures.
The value of variable 'existsPicParamete' will be Null, if the parameter does not
exists in the adress bar; else a list of elements will be stored in there.
*/
if (existsPicParameter != null)
{
/*
So the page wasn't been loaded at first time.
We need the index from the last showed picture.
*/
tmpPicParameter = existsPicParameter[0].match(/picnoparam=\d+/i);
if (tmpPicParameter != null)
{
/*
Extracting the index from string
*/
a = parseInt(tmpPicParameter[0].match(/\d+/i));
tmpPicParameterOld = tmpPicParameter[0];
}
} else {
/*
So the page was loaded at the first time.
Generate a random number, within the declared range.
*/
a = Math.floor(Math.random() * (b - a)) + a;
}
/*
End of the initial quicktest
---------------------------------------------------------------------------------
*/
/*
The javascript function for forward scrolling.
It needs an explicit call.
*/
function fw()
{
if (a == (b - 1))
{
/*
The index of last array element is the array length minus 1.
Then reset the counter variable.
*/
a = 0;
} else {
a++;
}
navigate(a);
}
/*
The javascript function for backward scrolling.
It needs an explicit call.
*/
function bw()
{
if (a == 0)
{
a = b - 1;
} else {
a--
}
navigate(a);
}
/*
The javascript function that modified the page url
*/
function navigate(a)
{
if (existsPicParameter == null)
{
if (currentUrl.indexOf("?") == -1)
{
currentUrl += "?";
} else {
/*
In case there were already one or more url parameters,
the seporerator for another one is '&'
*/
currentUrl += "&";
}
/*
Extend the current url with parameter 'picnoprogram'.
*/
currentUrl += "picnoparam="+a;
} else {
/*
Update the current parameter value in the adressbar with the new one,
by replacing.
Only if the parameter 'picnoprogram' had been alerady exist
*/
tmpPicParameter[0] = tmpPicParameter[0].replace(/\d+/i,a);
currentUrl = currentUrl.replace(tmpPicParameterOld,tmpPicParameter[0]);
}
/* "Reload the site" */
window.location.replace(currentUrl);
}
/*
The javascript function for assigning the stored url to the HTML element 'img'.
It needs an explicit call.
*/
function showPicNo(picNumber)
{
window.document.images["picturegallery"].src=pics[picNumber];
}
/*
The javascript function that uses the share service from facebook.
See: http://stackoverflow.com/questions/13223951/facebook-share-button-how-to-implement .
It needs an explicit call.
*/
function shareButton()
{
/*
This ist the main part of the solution
*/
var facebooksUrlForSharingSites = 'https://www.facebook.com/sharer/sharer.php?u='
facebooksUrlForSharingSites += document.URL;
/*
An example way how to transfer the sharing data
*/
window.open(facebooksUrlForSharingSites,450,420);
}
Hopefully it meets your requirement.
I don't know your surrounding HTML code. In my eyes you should throw away your code.
Here a simple image gallery that should meet your requirements:
HTML code:
<html>
<head>
<meta http-equiv="expires" content="0">
</head>
<body onload="randomStartPic()">
<div id="picture"><img name="picturegallery"></div>
<div id="navigation">
Backward | Forward
</div>
<body>
</html>
And the Javascript code:
var pics=new Array ("http://4.bp.blogspot.com/_UdzqQpb36Jo/R9kVS0h1BFI/AAAAAAAAD_o/SRGugAQSF0A/s1600/timming_pictures_37.jpg", ...)
var a=0, b = pics.length;
function fw()
{
if (a == (b - 1))
{
a = 0;
} else {
a++;
}
showPicNo(a);
}
function bw()
{
if (a == 0)
{
a = b - 1;
} else {
a--;
}
showPicNo(a);
}
function randomStartPic()
{
a = Math.floor(Math.random() * (b - a)) + a;
showPicNo(a);
}
function showPicNo(picNumber)
{
window.document.images["picturegallery"].src=pics[picNumber];
}
At my local computer it did work perfectly...
Again, I am working with code from my predecessor and am at a loss for this one. It appears to be a sampled navigation script. It is receiving an error in IE stating Object doesn't support this property or method. Here is what I have narrowed the error down to.
The function:
/**
* hoverIntent r5 // 2007.03.27 // jQuery 1.1.2+
* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
*
* #param f onMouseOver function || An object with configuration options
* #param g onMouseOut function || Nothing (use configuration options object)
* #author Brian Cherne <brian#cherne.net>
*/
(function($){$.fn.hoverIntent=function(f,g){var cfg={sensitivity:7,interval:100,timeout:0};
cfg=$.extend(cfg,g?{over:f,out:g}:f);
var cX,cY,pX,pY;
var track=function(ev){cX=ev.pageX;
cY=ev.pageY;
};
var compare=function(ev,ob){ob.hoverIntent_t=clearTimeout(ob.hoverIntent_t);
if((Math.abs(pX-cX)+Math.abs(pY-cY))<cfg.sensitivity){$(ob).unbind("mousemove",track);
ob.hoverIntent_s=1;
return cfg.over.apply(ob,[ev]);
}else{pX=cX;
pY=cY;
ob.hoverIntent_t=setTimeout(function(){compare(ev,ob);
},cfg.interval);
}};
var delay=function(ev,ob){ob.hoverIntent_t=clearTimeout(ob.hoverIntent_t);
ob.hoverIntent_s=0;
return cfg.out.apply(ob,[ev]);
};
var handleHover=function(e){var p=(e.type=="mouseover"?e.fromElement:e.toElement)||e.relatedTarget;
while(p&&p!=this){try{p=p.parentNode;
}catch(e){p=this;
}}if(p==this){return false;
}var ev=jQuery.extend({},e);
var ob=this;
if(ob.hoverIntent_t){ob.hoverIntent_t=clearTimeout(ob.hoverIntent_t);
}if(e.type=="mouseover"){pX=ev.pageX;
pY=ev.pageY;
$(ob).bind("mousemove",track);
if(ob.hoverIntent_s!=1){ob.hoverIntent_t=setTimeout(function(){compare(ev,ob);
},cfg.interval);
}}else{$(ob).unbind("mousemove",track);
if(ob.hoverIntent_s==1){ob.hoverIntent_t=setTimeout(function(){delay(ev,ob);
},cfg.timeout);
}}};
return this.mouseover(handleHover).mouseout(handleHover);
};
})(jQuery);
The document.ready line triggering the error:
var config = {
sensitivity: 1, // number = sensitivity threshold (must be 1 or higher)
interval: 50, // number = milliseconds for onMouseOver polling interval
over: megaHoverOver, // function = onMouseOver callback (REQUIRED)
timeout: 200, // number = milliseconds delay before onMouseOut
out: megaHoverOut // function = onMouseOut callback (REQUIRED)
};
$("ul#topnav li .sub").css({'opacity':'0'});
$("ul#topnav li").hoverIntent(config);
I am at a loss as to how to resolve this and finally get this section fixed.
The two functions that are defined within document.ready.
function megaHoverOver(){
$(this).find(".sub").stop().fadeTo('fast', 1).show();
//Calculate width of all ul's
(function($) {
jQuery.fn.calcSubWidth = function() {
rowWidth = 0;
//Calculate row
$(this).find("ul").each(function() {
rowWidth += $(this).width();
});
};
})(jQuery);
if ( $(this).find(".row").length > 0 ) { //If row exists...
var biggestRow = 0;
//Calculate each row
$(this).find(".row").each(function() {
$(this).calcSubWidth();
//Find biggest row
if(rowWidth > biggestRow) {
biggestRow = rowWidth;
}
});
//Set width
$(this).find(".sub").css({'width' :biggestRow});
$(this).find(".row:last").css({'margin':'0'});
} else { //If row does not exist...
$(this).calcSubWidth();
//Set Width
$(this).find(".sub").css({'width' : rowWidth});
}
}
function megaHoverOut(){
$(this).find(".sub").stop().fadeTo('fast', 0, function() {
$(this).hide();
});
}
I'm able to run that code without errors (see http://jsfiddle.net/veHEY/). It looks like the issue might be in the megaHoverOver and megaHoverOut functions that you're passing in through the configuration object. Do you have the code for those functions?
Previously:
The problem is almost certainly
opacity, which is not supported in
IE. Check out this nice quirksmode
article on cross-browser opacity
issues.
Correction: As #patrick rightly points out, and backs up with a source code reference to boot, jQuery is smart enough to automatically deal with IE's own special brands of opacity handling. Whatever the OP's problem is, this is not the answer.