/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* * The contents of this file are subject to the Netscape Public * License Version 1.1 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.mozilla.org/NPL/ * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * The Original Code is Mozilla Communicator client code, * released March 31, 1998. * * The Initial Developer of the Original Code is Netscape Communications * Corporation. Portions created by Netscape are * Copyright (C) 1998 Netscape Communications Corporation. All * Rights Reserved. * * Contributor(s): * William A. ("PowerGUI") Law */ /*------------------------------ nsContextMenu --------------------------------- | This JavaScript "class" is used to implement the browser's content-area | | context menu. | | | | For usage, see references to this class in navigator.xul. | | | | Currently, this code is relatively useless for any other purpose. In the | | longer term, this code will be restructured to make it more reusable. | ------------------------------------------------------------------------------*/ function nsContextMenu( xulMenu ) { this.target = null; this.menu = null; this.onImage = false; this.onLink = false; this.onSaveableLink = false; this.link = false; this.inFrame = false; this.hasBGImage = false; // Initialize new menu. this.initMenu( xulMenu ); } // Prototype for nsContextMenu "class." nsContextMenu.prototype = { // onDestroy is a no-op at this point. onDestroy : function () { }, // Initialize context menu. initMenu : function ( popup, event ) { // Save menu. this.menu = popup; // Get contextual info. this.setTarget( document.popupNode ); // Initialize (disable/remove) menu items. this.initItems(); }, initItems : function () { this.initOpenItems(); this.initNavigationItems(); this.initViewItems(); this.initMiscItems(); this.initSaveItems(); this.initClipboardItems(); }, initOpenItems : function () { // Remove open/edit link if not applicable. this.showItem( "context-openlink", this.onSaveableLink ); this.showItem( "context-editlink", this.onSaveableLink ); // Remove open frame if not applicable. this.showItem( "context-openframe", this.inFrame ); // Remove separator after open items if neither link nor frame. this.showItem( "context-sep-open", this.onSaveableLink || this.inFrame ); }, initNavigationItems : function () { // Back determined by canGoBack broadcaster. this.setItemAttrFromNode( "context-back", "disabled", "canGoBack" ); // Forward determined by canGoForward broadcaster. this.setItemAttrFromNode( "context-forward", "disabled", "canGoForward" ); // Reload is always OK. // Stop determined by canStop broadcaster. this.setItemAttrFromNode( "context-stop", "disabled", "canStop" ); }, initSaveItems : function () { // Save page is always OK. // Save frame as depends on whether we're in a frame. this.showItem( "context-saveframe", this.inFrame ); // Save link depends on whether we're in a link. this.showItem( "context-savelink", this.onSaveableLink ); // Save background image depends on whether there is one. this.showItem( "context-savebgimage", this.hasBGImage ); // Save image depends on whether there is one. this.showItem( "context-saveimage", this.onImage ); }, initViewItems : function () { // View source is always OK. // View frame source depends on whether we're in a frame. this.showItem( "context-viewframesource", this.inFrame ); // View Info don't work no way no how. this.showItem( "context-viewinfo", false ); // View Frame Info isn't working, either. this.showItem( "context-viewframeinfo", false ); // View Image depends on whether an image was clicked on. this.showItem( "context-viewimage", this.onImage ); // Block image depends on whether an image was clicked on, and, // whether the user pref is enabled. this.showItem( "context-blockimage", this.onImage && this.isBlockingImages() ); }, initMiscItems : function () { // Add bookmark always OK. // Send Page not working yet. this.showItem( "context-sendpage", false ); }, initClipboardItems : function () { // Select All is always OK. // Copy depends on whether there is selected text. // Enabling this context menu item is now done through the global // command updating system // this.setItemAttr( "context-copy", "disabled", this.isNoTextSelected() ); // Copy link location depends on whether we're on a link. this.showItem( "context-copylink", this.onLink ); // Copy image location depends on whether we're on an image. this.showItem( "context-copyimage", this.onImage ); }, // Set various context menu attributes based on the state of the world. setTarget : function ( node ) { // Initialize contextual info. this.onImage = false; this.imageURL = ""; this.onLink = false; this.inFrame = false; this.hasBGImage = false; // Remember the node that was clicked. this.target = node; // See if the user clicked on an image. if ( this.target.nodeType == 1 ) { if ( this.target.tagName.toUpperCase() == "IMG" ) { this.onImage = true; this.imageURL = this.target.src; // Look for image map. var mapName = this.target.getAttribute( "usemap" ); if ( mapName ) { // Find map. var map = this.target.ownerDocument.getElementById( mapName.substr(1) ); if ( map ) { // Search child s for a match. var areas = map.childNodes; //XXX Client side image maps are too hard for now! dump( "Client side image maps not supported yet, sorry!\n" ); areas.length = 0; for ( var i = 0; i < areas.length && !this.onLink; i++ ) { var area = areas[i]; if ( area.nodeType == 1 && area.tagName.toUpperCase() == "AREA" ) { // Get type (rect/circle/polygon/default). var type = area.getAttribute( "type" ); var coords = this.parseCoords( area ); switch ( type.toUpperCase() ) { case "RECT": case "RECTANGLE": break; case "CIRC": case "CIRCLE": break; case "POLY": case "POLYGON": break; case "DEFAULT": // Default matches entire image. this.onLink = true; this.link = area; this.onSaveableLink = this.isLinkSaveable( this.link ); break; } } } } } } else if (this.target.tagName.toUpperCase() == "INPUT") { if(this.target.getAttribute( "type" ).toUpperCase() == "IMAGE") { this.onImage = true; this.imageURL = this.target.src; } } else if (this.target.getAttribute( "background" )) { this.onImage = true; this.imageURL = this.target.getAttribute( "background" ); } else { var cssAttr = this.target.style.getPropertyValue( "list-style-image" ) || this.target.style.getPropertyValue( "list-style" ) || this.target.style.getPropertyValue( "background-image" ) || this.target.style.getPropertyValue( "background" ); if ( cssAttr ) { this.onImage = true; this.imageURL = cssAttr.toLowerCase().replace(/url\("*(.+)"*\)/, "$1"); } } } // See if the user clicked in a frame. if ( this.target.ownerDocument != window.content.document ) { this.inFrame = true; } // Bubble out, looking for link. var elem = this.target; while ( elem && !this.onLink ) { // Test for element types of interest. if ( elem.nodeType == 1 && ( elem.tagName.toUpperCase() == "A" || elem.tagName.toUpperCase() == "AREA" ) ) { // Clicked on a link. this.onLink = true; // Remember corresponding element. this.link = elem; // Remember if it is saveable. this.onSaveableLink = this.isLinkSaveable( this.link ); } elem = elem.parentNode; } }, // Returns true iff clicked on link is saveable. isLinkSaveable : function ( link ) { // Test for missing protocol property. if ( !link.protocol ) { // We must resort to testing the URL string :-(. dump( "Bug! Link.protocol is still undefined!\n" ); var protocol = link.href.substr( 0, 11 ); return protocol != "javascript:"; } else { // Presume all but javascript: urls are saveable. return link.protocol != "javascript:"; } }, // Open linked-to URL in a new window. openLink : function () { // Determine linked-to URL. openNewWindowWith( this.linkURL() ); }, // Edit linked-to URL in a new window. editLink : function () { BrowserEditPage( this.linkURL() ); }, // Open clicked-in frame in its own window. openFrame : function () { openNewWindowWith( this.target.ownerDocument.location.href ); }, viewInfo : function () { dump( "nsContextMenu.viewInfo not implemented yet\n" ); }, viewFrameInfo : function () { dump( "nsContextMenu.viewFrameInfo not implemented yet\n" ); }, // Open new window with the URL of the image. viewImage : function () { openNewWindowWith( this.imageURL ); }, // Save URL of clicked-on frame. saveFrame : function () { this.savePage( this.target.ownerDocument.location.href ); }, // Save URL of clicked-on link. saveLink : function () { this.savePage( this.linkURL() ); }, // Save URL of clicked-on image. saveImage : function () { this.savePage( this.imageURL ); }, // Save URL of background image. saveBGImage : function () { this.savePage( this.bgImageURL() ); }, // Generate link URL and put it on clibboard. copyLink : function () { this.copyToClipboard( this.linkURL() ); }, // Generate image URL and put it on the clipboard. copyImage : function () { this.copyToClipboard( this.imageURL ); }, // Determine if "Block Image" is to appear in the menu. // Return false unless "imageBlocker.enabled" pref is set. isBlockingImages: function () { var pref = this.getService( 'component://netscape/preferences', 'nsIPref' ); var result = false; try { result = pref.GetBoolPref( "imageBlocker.enabled" ); } catch(e) { } return result; }, // Block image from loading in the future. blockImage : function () { var cookieViewer = this.createInstance( "component://netscape/cookieviewer/cookieviewer-world", "nsICookieViewer" ); cookieViewer.BlockImage(this.imageURL); }, /////////////// // Utilities // /////////////// // Create instance of component given progId and iid (as string). createInstance : function ( progId, iidName ) { var iid = Components.interfaces[ iidName ]; return Components.classes[ progId ].createInstance( iid ); }, // Get service given progId and iid (as string). getService : function ( progId, iidName ) { var iid = Components.interfaces[ iidName ]; return Components.classes[ progId ].getService( iid ); }, // Show/hide one item (specified via name or the item element itself). showItem : function ( itemOrId, show ) { var item = null; if ( itemOrId.constructor == String ) { // Argument specifies item id. item = document.getElementById( itemOrId ); } else { // Argument is the item itself. item = itemOrId; } if ( item ) { var styleIn = item.getAttribute( "style" ); var styleOut = styleIn; if ( show ) { // Remove style="display:none;". styleOut = styleOut.replace( "display:none;", "" ); } else { // Set style="display:none;". if ( styleOut.indexOf( "display:none;" ) == -1 ) { // Add style the first time we need to. styleOut += "display:none;"; } } // Only set style if it's different. if ( styleIn != styleOut ) { item.setAttribute( "style", styleOut ); } } }, // Set given attribute of specified context-menu item. If the // value is null, then it removes the attribute (which works // nicely for the disabled attribute). setItemAttr : function ( id, attr, val ) { var elem = document.getElementById( id ); if ( elem ) { if ( val == null ) { // null indicates attr should be removed. elem.removeAttribute( attr ); } else { // Set attr=val. elem.setAttribute( attr, val ); } } }, // Set context menu attribute according to like attribute of another node // (such as a broadcaster). setItemAttrFromNode : function ( item_id, attr, other_id ) { var elem = document.getElementById( other_id ); if ( elem && elem.getAttribute( attr ) == "true" ) { this.setItemAttr( item_id, attr, "true" ); } else { this.setItemAttr( item_id, attr, null ); } }, // Temporary workaround for DOM api not yet implemented by XUL nodes. cloneNode : function ( item ) { // Create another element like the one we're cloning. var node = document.createElement( item.tagName ); // Copy attributes from argument item to the new one. var attrs = item.attributes; for ( var i = 0; i < attrs.length; i++ ) { var attr = attrs.item( i ); node.setAttribute( attr.nodeName, attr.nodeValue ); } // Voila! return node; }, // Generate fully-qualified URL for clicked-on link. linkURL : function () { return this.link.href; }, // Returns "true" if there's no text selected, null otherwise. isNoTextSelected : function ( event ) { // Not implemented so all text-selected-based options are disabled. return "true"; }, // Copy link/image url to clipboard. copyToClipboard : function ( text ) { // Get clipboard. var clipboard = this.getService( "component://netscape/widget/clipboard", "nsIClipboard" ); // Create tranferable that will transfer the text. var transferable = this.createInstance( "component://netscape/widget/transferable", "nsITransferable" ); if ( clipboard && transferable ) { transferable.addDataFlavor( "text/unicode" ); // Create wrapper for text. var data = this.createInstance( "component://netscape/supports-wstring", "nsISupportsWString" ); if ( data ) { data.data = text ; transferable.setTransferData( "text/unicode", data, text.length * 2 ); // Put on clipboard. clipboard.setData( transferable, null ); } } }, // Save specified URL in user-selected file. savePage : function ( url ) { // Default is to save current page. if ( !url ) { url = window.content.location.href; } // Use stream xfer component to prompt for destination and save. var xfer = this.getService( "component://netscape/appshell/component/xfer", "nsIStreamTransfer" ); try { // When Necko lands, we need to receive the real nsIChannel and // do SelectFileAndTransferLocation! // Use this for now... xfer.SelectFileAndTransferLocationSpec( url, window ); } catch( exception ) { // Failed (or cancelled), give them another chance. dump( "SelectFileAndTransferLocationSpec failed, rv=" + exception + "\n" ); } return; }, // Parse coords= attribute and return array. parseCoords : function ( area ) { return []; }, toString : function () { return "contextMenu.target = " + this.target + "\n" + "contextMenu.onImage = " + this.onImage + "\n" + "contextMenu.onLink = " + this.onLink + "\n" + "contextMenu.link = " + this.link + "\n" + "contextMenu.inFrame = " + this.inFrame + "\n" + "contextMenu.hasBGImage = " + this.hasBGImage + "\n"; } };