michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: var widgets = require('sdk/widget'); michael@0: var pageMod = require('sdk/page-mod'); michael@0: var data = require('sdk/self').data; michael@0: var panels = require('sdk/panel'); michael@0: var simpleStorage = require('sdk/simple-storage'); michael@0: var notifications = require("sdk/notifications"); michael@0: michael@0: /* michael@0: Global variables michael@0: * Boolean to indicate whether the add-on is switched on or not michael@0: * Array for all workers associated with the 'selector' page mod michael@0: * Array for all workers associated with the 'matcher' page mod michael@0: */ michael@0: var annotatorIsOn = false; michael@0: var selectors = []; michael@0: var matchers = []; michael@0: michael@0: if (!simpleStorage.storage.annotations) michael@0: simpleStorage.storage.annotations = []; michael@0: michael@0: /* michael@0: Update the matchers: call this whenever the set of annotations changes michael@0: */ michael@0: function updateMatchers() { michael@0: matchers.forEach(function (matcher) { michael@0: matcher.postMessage(simpleStorage.storage.annotations); michael@0: }); michael@0: } michael@0: michael@0: /* michael@0: Constructor for an Annotation object michael@0: */ michael@0: function Annotation(annotationText, anchor) { michael@0: this.annotationText = annotationText; michael@0: this.url = anchor[0]; michael@0: this.ancestorId = anchor[1]; michael@0: this.anchorText = anchor[2]; michael@0: } michael@0: michael@0: /* michael@0: Function to deal with a new annotation. michael@0: Create a new annotation object, store it, and michael@0: notify all the annotators of the change. michael@0: */ michael@0: function handleNewAnnotation(annotationText, anchor) { michael@0: var newAnnotation = new Annotation(annotationText, anchor); michael@0: simpleStorage.storage.annotations.push(newAnnotation); michael@0: updateMatchers(); michael@0: } michael@0: michael@0: /* michael@0: Function to tell the selector page mod that the add-on has become (in)active michael@0: */ michael@0: function activateSelectors() { michael@0: selectors.forEach( michael@0: function (selector) { michael@0: selector.postMessage(annotatorIsOn); michael@0: }); michael@0: } michael@0: michael@0: /* michael@0: Toggle activation: update the on/off state and notify the selectors. michael@0: */ michael@0: function toggleActivation() { michael@0: annotatorIsOn = !annotatorIsOn; michael@0: activateSelectors(); michael@0: return annotatorIsOn; michael@0: } michael@0: michael@0: function detachWorker(worker, workerArray) { michael@0: var index = workerArray.indexOf(worker); michael@0: if(index != -1) { michael@0: workerArray.splice(index, 1); michael@0: } michael@0: } michael@0: michael@0: exports.main = function() { michael@0: michael@0: /* michael@0: The widget provides a mechanism to switch the selector on or off, and to michael@0: view the list of annotations. michael@0: michael@0: The selector is switched on/off with a left-click, and the list of annotations michael@0: is displayed on a right-click. michael@0: */ michael@0: var widget = widgets.Widget({ michael@0: id: 'toggle-switch', michael@0: label: 'Annotator', michael@0: contentURL: data.url('widget/pencil-off.png'), michael@0: contentScriptWhen: 'ready', michael@0: contentScriptFile: data.url('widget/widget.js') michael@0: }); michael@0: michael@0: widget.port.on('left-click', function() { michael@0: console.log('activate/deactivate'); michael@0: widget.contentURL = toggleActivation() ? michael@0: data.url('widget/pencil-on.png') : michael@0: data.url('widget/pencil-off.png'); michael@0: }); michael@0: michael@0: widget.port.on('right-click', function() { michael@0: console.log('show annotation list'); michael@0: annotationList.show(); michael@0: }); michael@0: michael@0: /* michael@0: The selector page-mod enables the user to select page elements to annotate. michael@0: michael@0: It is attached to all pages but only operates if the add-on is active. michael@0: michael@0: The content script highlights any page elements which can be annotated. If the michael@0: user clicks a highlighted element it sends a message to the add-on containing michael@0: information about the element clicked, which is called the anchor of the michael@0: annotation. michael@0: michael@0: When we receive this message we assign the anchor to the annotationEditor and michael@0: display it. michael@0: */ michael@0: var selector = pageMod.PageMod({ michael@0: include: ['*'], michael@0: contentScriptWhen: 'ready', michael@0: contentScriptFile: [data.url('jquery-1.4.2.min.js'), michael@0: data.url('selector.js')], michael@0: onAttach: function(worker) { michael@0: worker.postMessage(annotatorIsOn); michael@0: selectors.push(worker); michael@0: worker.port.on('show', function(data) { michael@0: annotationEditor.annotationAnchor = data; michael@0: annotationEditor.show(); michael@0: }); michael@0: worker.on('detach', function () { michael@0: detachWorker(this, selectors); michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: /* michael@0: The annotationEditor panel is the UI component used for creating michael@0: new annotations. It contains a text area for the user to michael@0: enter the annotation. michael@0: michael@0: When we are ready to display the editor we assign its 'anchor' property michael@0: and call its show() method. michael@0: michael@0: Its content script sends the content of the text area to the add-on michael@0: when the user presses the return key. michael@0: michael@0: When we receives this message we create a new annotation using the anchor michael@0: and the text the user entered, store it, and hide the panel. michael@0: */ michael@0: var annotationEditor = panels.Panel({ michael@0: width: 220, michael@0: height: 220, michael@0: contentURL: data.url('editor/annotation-editor.html'), michael@0: contentScriptFile: data.url('editor/annotation-editor.js'), michael@0: onMessage: function(annotationText) { michael@0: if (annotationText) michael@0: handleNewAnnotation(annotationText, this.annotationAnchor); michael@0: annotationEditor.hide(); michael@0: }, michael@0: onShow: function() { michael@0: this.postMessage('focus'); michael@0: } michael@0: }); michael@0: michael@0: /* michael@0: The annotationList panel is the UI component that lists all the annotations michael@0: the user has entered. michael@0: michael@0: On its 'show' event we pass it the array of annotations. michael@0: michael@0: The content script creates the HTML elements for the annotations, and michael@0: intercepts clicks on the links, passing them back to the add-on to open them michael@0: in the browser. michael@0: */ michael@0: var annotationList = panels.Panel({ michael@0: width: 420, michael@0: height: 200, michael@0: contentURL: data.url('list/annotation-list.html'), michael@0: contentScriptFile: [data.url('jquery-1.4.2.min.js'), michael@0: data.url('list/annotation-list.js')], michael@0: contentScriptWhen: 'ready', michael@0: onShow: function() { michael@0: this.postMessage(simpleStorage.storage.annotations); michael@0: }, michael@0: onMessage: function(message) { michael@0: require('sdk/tabs').open(message); michael@0: } michael@0: }); michael@0: michael@0: /* michael@0: We listen for the OverQuota event from simple-storage. michael@0: If it fires we just notify the user and delete the most michael@0: recent annotations until we are back in quota. michael@0: */ michael@0: simpleStorage.on("OverQuota", function () { michael@0: notifications.notify({ michael@0: title: 'Storage space exceeded', michael@0: text: 'Removing recent annotations'}); michael@0: while (simpleStorage.quotaUsage > 1) michael@0: simpleStorage.storage.annotations.pop(); michael@0: }); michael@0: michael@0: /* michael@0: The matcher page-mod locates anchors on web pages and prepares for the michael@0: annotation to be displayed. michael@0: michael@0: It is attached to all pages, and when it is attached we pass it the complete michael@0: list of annotations. It looks for anchors in its page. If it finds one it michael@0: highlights the anchor and binds mouseenter/mouseout events to 'show' and 'hide' michael@0: messages to the add-on. michael@0: michael@0: When the add-on receives the 'show' message it assigns the annotation text to michael@0: the annotation panel and shows it. michael@0: michael@0: Note that the matcher is active whether or not the add-on is active: michael@0: 'inactive' only means that the user can't create new add-ons, they can still michael@0: see old ones. michael@0: */ michael@0: var matcher = pageMod.PageMod({ michael@0: include: ['*'], michael@0: contentScriptWhen: 'ready', michael@0: contentScriptFile: [data.url('jquery-1.4.2.min.js'), michael@0: data.url('matcher.js')], michael@0: onAttach: function(worker) { michael@0: if(simpleStorage.storage.annotations) { michael@0: worker.postMessage(simpleStorage.storage.annotations); michael@0: } michael@0: worker.port.on('show', function(data) { michael@0: annotation.content = data; michael@0: annotation.show(); michael@0: }); michael@0: worker.port.on('hide', function() { michael@0: annotation.content = null; michael@0: annotation.hide(); michael@0: }); michael@0: worker.on('detach', function () { michael@0: detachWorker(this, matchers); michael@0: }); michael@0: matchers.push(worker); michael@0: } michael@0: }); michael@0: michael@0: /* michael@0: The annotation panel is the UI component that displays an annotation. michael@0: michael@0: When we are ready to show it we assign its 'content' attribute to contain michael@0: the annotation text, and that gets sent to the content process in onShow(). michael@0: */ michael@0: var annotation = panels.Panel({ michael@0: width: 200, michael@0: height: 180, michael@0: contentURL: data.url('annotation/annotation.html'), michael@0: contentScriptFile: [data.url('jquery-1.4.2.min.js'), michael@0: data.url('annotation/annotation.js')], michael@0: contentScriptWhen: 'ready', michael@0: onShow: function() { michael@0: this.postMessage(this.content); michael@0: } michael@0: }); michael@0: michael@0: }