Skip to content

User Scripts

marklieberman edited this page Jul 12, 2018 · 13 revisions

User scripts are subject to all of the usual limitations of web extensions. Specifically, the user script is evaluated in the content script environment. Within the content script you will have limited access to web extension APIs.

To access privileged APIs, the user script must execute in the background script environment. Foxy Gestures provides a executeInBackground() function to allow user scripts to execute code in the background.

Available web extension APIs are restricted based on the permissions key in manifest.json so please create an issue if you require access to additional APIs.

User Script Scope

User scripts are evaluated in the content/commands.js module within the frame where the gesture began. Functions passed to executeInBackground() are evaluated in background/commands.js. You may invoke any of the built-in commands in each module in your user script.

The following useful variables are in scope in your user script. These may be changed or renamed as the extension evolves.

  • data: An object containing the serializable event information for the gesture. For example:
data = {
   "context":{
      "frameUrl":"https://www.reddit.com/"
   },
   "element":{
      "tag":"IMG",
      "mediaSource": "https://b.thumbs.redditmedia.com/NZkUwNgjHbgvcWULQwrU81FeUDQQMKDnK8mDMaparLc.jpg",
      "mediaType": "image/jpg"
   }
}
  • mouseDown: A reference to the first mousedown event in the gesture.

  • mouseDown.target: A reference to the DOM element under the mouse when the gesture started.

  • data.element.linkHref is the href attribute of any enclosing <a> tag, not just the href of the element under the gesture..

  • data.element.mediaSource is the src attribute of any media element (img,video,audio,etc.) under the mouse.

  • data.element.mediaType is the mime type of any media element under the mouse. Only populated if specified, such as in HTML5 audio and video elements. It may be used to infer a file type from extensionless source URLs.

Repeatable Wheel and Chord Gestures

By default, a wheel or chord gesture can only be performed once and then the gesture is complete. (This ensures that the gesture state is cleaned up if the active tab changes.) If you want to repeat the gesture without releasing all buttons, you can end your user script like so:

// Allow the wheel or chord gesture to repeat.
var result = { repeat: true };
result;

Reference

executeInBackground(func, args);

Executes a function in the privileged background context. func is a function to execute. args is an array of arguments to pass to func. This function works by serializing func using Function.prototype.toString() and sending it to the background context. You may not use any closures or references external to func. All values in args must be serializable as JSON, so you cannot use DOM references. This function returns a promise that is resolved with the return value of func.

Example:

var src = data.element.mediaInfo && data.element.mediaInfo.source;
var promise = executeInBackground(src => {
  return browser.downloads.download({ url: src });
}, [ src ]);

promise.then(downloadId => {
  console.log('download ID is', downloadID);
});

Useful Functions

getCurrentWindow()

Returns a promise that resolves to the currently active window.

getCurrentWindowTabs()

Returns a promise that resolves to an array of tabs in the currently active window.

getActiveTab(callback)

Invokes the callback passing the currently active tab as the only argument.

Examples

Open Image In New Tab

var src = data.element.mediaInfo && data.element.mediaInfo.source;
if (src) {
    executeInBackground(src => {
        getActiveTab(tab => browser.tabs.create({
            url: src,
            index: tab.index + 1,
            active: true
        }));
    }, [ src ]);
}

Pattern Matching Quick Save

Saves an image, video, or audio file to custom location in the downloads folder. Rules are evaluated in order to determine the target path and filename.

(function () {
    let src = data.element.mediaInfo && data.element.mediaInfo.source;
    if (!src) {
        setStatus('Quick Save: No media found');
        return;
    }
    
    let placeholders = [], rules = [];
    
    // -- Define placeholders --
    placeholders.push([ '%IMAGE_DIR%', 'Images/' ]);
    placeholders.push([ '%VIDEO_DIR%', 'Video/' ]);
    placeholders.push([ '%AUDIO_DIR%', 'Audio/' ]);
    
    // -- Define rules --
    // Capturing group 1 must be the filename.
    // Defaults
    rules.push({ name: 'Images', regex: /([^\/]+\.(gif|png|jpe?g))($|\?|#)/i, target: '%IMAGE_DIR%/%F%', erase: true });
    rules.push({ name: 'Video', regex: /([^\/]+\.(webm|mp4|mkv))($|\?|#)/i, target: '%VIDEO_DIR%/%F%' });
    rules.push({ name: 'Audio', regex: /([^\/]+\.(mp3|flac|m4a|aac))($|\?|#)/i, target: '%AUDIO_DIR%/%F%' });

    // Evaluate rules
    let match = null;
    for (let i = 0; i < rules.length; i++) {
        if ((match = src.match(rules[i].regex)) != null) {
            // Evaluate placeholders
            let filename = match[1];
            placeholders.push([ '%F%', filename ]);
            let target = placeholders.reduce((target, placeholder) => target.replace(placeholder[0], placeholder[1]), rules[i].target);
            
            // -- Privileged operation begin --
            return executeInBackground((rule, src, target) => { 
                // Start the download
                return browser.downloads.download({ url: src, filename: target }).then(id => {
                    if (rule.erase) {
                        // Remove the download from history on completion
                        let listener = (downloadDelta) => {
                            if ((downloadDelta.id === id) && (downloadDelta.state.current !== 'in_progress')) {
                                browser.downloads.onChanged.removeListener(listener);
                                browser.downloads.erase({ id: downloadDelta.id });
                            }
                        };
                        browser.downloads.onChanged.addListener(listener);
                    }
                });
            }, [ rules[i], src, target ])
            // -- Privileged operation end --
                .then(x => setStatus('Quick Save: Saved "' + filename + '" using ' + rules[i].name))
                .catch(err => setStatus('Quick Save: Failed to save "' + filename + ': ' + err));
        }
    }
    
    setStatus('Quick Save: No matching rule');
    return;
}());