/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla 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/MPL/ * * 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 BlueGriffon. * * The Initial Developer of the Original Code is * Disruptive Innovations SARL. * Portions created by the Initial Developer are Copyright (C) 2006 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Daniel Glazman , Original author * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ var EXPORTED_SYMBOLS = ["EditorUtils"]; Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://app/modules/urlHelper.jsm"); //Components.utils.import("resource://app/modules/cssHelper.jsm"); var EditorUtils = { nsIDOMNode: Components.interfaces.nsIDOMNode, mActiveViewActive: false, mAtomService: null, /********** PUBLIC **********/ getCurrentEditorWindow: function getCurrentEditorWindow() { try { var windowManager = Services.wm; return windowManager.getMostRecentWindow("bluegriffon") || windowManager.getMostRecentWindow("BlueGriffon:MacCmdLineFwd"); } catch(e){} return null; }, getCurrentTabEditor: function getCurrentTabEditor() { try { var tmpWindow = this.getCurrentEditorWindow(); if (tmpWindow) { var tabeditor = tmpWindow.document.getElementById("tabeditor"); if (tabeditor) return tabeditor; } } catch(e) { } return null; }, getCurrentEditorElement: function getCurrentEditorElement() { var tabeditor = this.getCurrentTabEditor(); if (tabeditor) return tabeditor.getCurrentEditorElement() ; return null; }, getCurrentSourceEditorElement: function() { var editorElement = this.getCurrentEditorElement(); if (editorElement) { return editorElement.previousSibling; } return null; }, getCurrentSourceEditor: function() { var editorElement = this.getCurrentEditorElement(); if (editorElement) { var bespinIframe = editorElement.previousSibling; var bespinEditor = bespinIframe.contentWindow.gEditor; return bespinEditor; } return null; }, getCurrentEditor: function getCurrentEditor() { // Get the active editor from the tag var editor = null; try { var editorElement = this.getCurrentEditorElement(); if (editorElement) { editor = editorElement.getEditor(editorElement.contentWindow); // Do QIs now so editor users won't have to figure out which interface to use // Using "instanceof" does the QI for us. editor instanceof Components.interfaces.nsIEditor; editor instanceof Components.interfaces.nsIPlaintextEditor; editor instanceof Components.interfaces.nsIHTMLEditor; } } catch (e) { } return editor; }, getCurrentDocument: function getCurrentDocument() { // Get the active editor from the tag var editor = this.getCurrentEditor(); if (editor) return editor.document; return null; }, getCurrentCommandManager: function getCurrentCommandManager() { try { return this.getCurrentEditorElement().commandManager; } catch (e) { } return null; }, newCommandParams: function newCommandParams() { try { const contractId = "@mozilla.org/embedcomp/command-params;1"; const nsICommandParams = Components.interfaces.nsICommandParams; return Components.classes[contractId].createInstance(Components.interfaces.nsICommandParams); } catch(e) { } return null; }, getCurrentEditingSession: function getCurrentEditingSession() { try { return this.getCurrentEditorElement().editingSession; } catch (e) { } return null; }, getCurrentEditorType: function getCurrentEditorType() { try { return this.getCurrentEditorElement().editortype; } catch (e) { } return ""; }, isAlreadyEdited: function isAlreadyEdited(aURL) { Components.utils.import("resource://app/modules/urlHelper.jsm"); // blank documents are never "already edited"... if (UrlUtils.isUrlOfBlankDocument(aURL)) return null; var url = UrlUtils.newURI(aURL).spec; var enumerator = Services.wm.getEnumerator( "bluegriffon" ); while ( enumerator.hasMoreElements() ) { var win = enumerator.getNext().QueryInterface(Components.interfaces.nsIDOMWindowInternal); try { var mixed = win.gDialog.tabeditor.isAlreadyEdited(url); if (mixed) return {window: win, editor: mixed.editor, index: mixed.index}; } catch(e) {} } return null; }, isDocumentEditable: function isDocumentEditable() { try { return this.getCurrentEditor().isDocumentEditable; } catch (e) { } return false; }, isDocumentModified: function isDocumentModified() { try { return this.getCurrentEditor().documentModified; } catch (e) { } return false; }, isDocumentEmpty: function isDocumentEmpty() { try { return this.getCurrentEditor().documentIsEmpty; } catch (e) { } return false; }, getDocumentTitle: function getDocumentTitle() { try { return this.getCurrentDocument().title; } catch (e) { } return ""; }, isHTMLEditor: function isHTMLEditor() { // We don't have an editorElement, just return false if (!this.getCurrentEditorElement()) return false; var editortype = this.getCurrentEditorType(); switch (editortype) { case "html": case "htmlmail": return true; case "text": case "textmail": return false default: break; } return false; }, isEditingRenderedHTML: function isEditingRenderedHTML() { return this.isHTMLEditor(); // && !this.isInHTMLSourceMode(); }, setDocumentTitle: function setDocumentTitle(title) { try { this.getCurrentEditor().setDocumentTitle(title); // Update window title (doesn't work if called from a dialog) if ("UpdateWindowTitle" in window) window.UpdateWindowTitle(); else if ("UpdateWindowTitle" in window.opener) window.opener.UpdateWindowTitle(); } catch (e) { } }, getSelectionContainer: function getSelectionContainer() { var editor = this.getCurrentEditor(); if (!editor) return null; try { var selection = editor.selection; if (!selection) return null; } catch (e) { return null; } var result = { oneElementSelected:false }; if (selection.isCollapsed) { result.node = selection.focusNode; } else { var rangeCount = selection.rangeCount; if (rangeCount == 1) { result.node = editor.getSelectedElement(""); var range = selection.getRangeAt(0); // check for a weird case : when we select a piece of text inside // a text node and apply an inline style to it, the selection starts // at the end of the text node preceding the style and ends after the // last char of the style. Assume the style element is selected for // user's pleasure if (!result.node && range.startContainer.nodeType == this.nsIDOMNode.TEXT_NODE && range.startOffset == range.startContainer.length && range.endContainer.nodeType == this.nsIDOMNode.TEXT_NODE && range.endOffset == range.endContainer.length && range.endContainer.nextSibling == null && range.startContainer.nextSibling == range.endContainer.parentNode) result.node = range.endContainer.parentNode; if (!result.node) { // let's rely on the common ancestor of the selection result.node = range.commonAncestorContainer; } else { result.oneElementSelected = true; } } else { // assume table cells ! var i, container = null; for (i = 0; i < rangeCount; i++) { range = selection.getRangeAt(i); if (!container) { container = range.startContainer; } else if (container != range.startContainer) { // all table cells don't belong to same row so let's // select the parent of all rows result.node = container.parentNode; break; } result.node = container; } } } // make sure we have an element here while (result.node.nodeType != this.nsIDOMNode.ELEMENT_NODE) result.node = result.node.parentNode; // and make sure the element is not a special editor node like // the
we insert in blank lines // and don't select anonymous content !!! (fix for bug 190279) editor instanceof Components.interfaces.nsIHTMLEditor; while (result.node.hasAttribute("_moz_editor_bogus_node") || editor.isAnonymousElement(result.node)) result.node = result.node.parentNode; return result; }, getMetaElement: function getMetaElement(aName) { if (aName) { var name = aName.toLowerCase(); try { var metanodes = this.getCurrentDocument() .getElementsByTagName("meta"); for (var i = 0; i < metanodes.length; i++) { var metanode = metanodes.item(i); if (metanode && metanode.getAttribute("name") == name) return metanode; } } catch(e) {} } return null; }, createMetaElement: function createMetaElement(aName) { var editor = this.getCurrentEditor(); try { var metanode = editor.createElementWithDefaults("meta"); metanode.setAttribute("name", aName); return metanode; } catch(e) {} return null; }, insertMetaElement: function insertMetaElement(aElt, aContent, aInsertNew, aPrepend) { if (aElt) { var editor = this.getCurrentEditor(); try { if (!aContent) { if (!insertNew) editor.deleteNode(aElt); } else { if (aInsertNew) { aElt.setAttribute("content", aContent); if (aPrepend) this.prependHeadElement(aElt); else this.appendHeadElement(aElt); } else { editor.setAttribute(aElt, "content", aContent); } } } catch(e) {} } }, getHeadElement: function getHeadElement() { try { var doc = EditorUtils.getCurrentDocument(); var heads = doc.getElementsByTagName("head"); return heads.item(0); } catch(e) {} return null; }, prependHeadElement: function prependHeadElement(aElt) { var head = this.getHeadElement(); if (head) try { var editor = EditorUtils.getCurrentEditor(); editor.insertNode(aElt, head, 0, true); } catch(e) {} }, appendHeadElement: function appendHeadElement(aElt) { var head = this.getHeadElement(); if (head) { var pos = 0; if (head.hasChildNodes()) pos = head.childNodes.length; try { var editor = EditorUtils.getCurrentEditor(); editor.insertNode(aElt, head, pos, true); } catch(e) {} } }, getAtomService: function() { if (!this.mAtomService) this.mAtomService = Components.classes["@mozilla.org/atom-service;1"] .getService(Components.interfaces.nsIAtomService); return this.mAtomService; }, setTextProperty: function(property, attribute, value) { try { var propAtom = this.getAtomService().getAtom(property); this.getCurrentEditor().setInlineProperty(propAtom, attribute, value); } catch(e) {} }, getTextProperty: function(property, attribute, value, firstHas, anyHas, allHas) { try { var propAtom = this.getAtomService().getAtom(property); this.getCurrentEditor().getInlineProperty(propAtom, attribute, value, firstHas, anyHas, allHas); } catch(e) {} }, getBlockContainer: function(aElt) { var e = aElt; var display = e.ownerDocument.defaultView.getComputedStyle(e, "").getPropertyValue("display"); while (e && display == "inline" && e.className == "") { e = e.parentNode; display = e.ownerDocument.defaultView.getComputedStyle(e, "").getPropertyValue("display"); } return e; }, getCurrentTableEditor: function() { var editor = this.getCurrentEditor(); return (editor && (editor instanceof Components.interfaces.nsITableEditor)) ? editor : null; }, isStrictDTD: function() { var doctype = this.getCurrentEditor().document.doctype; return (doctype && doctype.publicId.lastIndexOf("Strict") != -1); }, isCSSDisabledAndStrictDTD: function() { var prefs = GetPrefs(); var IsCSSPrefChecked = prefs.getBoolPref("editor.use_css"); return !IsCSSPrefChecked && this.isStrictDTD(); }, getDocumentUrl: function() { try { var aDOMHTMLDoc = this.getCurrentEditor().document.QueryInterface(Components.interfaces.nsIDOMHTMLDocument); return aDOMHTMLDoc.URL; } catch (e) {} return ""; }, isXHTMLDocument: function() { var mimetype = this.getCurrentDocumentMimeType(); return (mimetype == "application/xhtml+xml"); }, getCurrentDocumentMimeType: function() { var doc = this.getCurrentDocument(); var editorMimeType = doc.contentType; var doctype = doc.doctype; var systemId = doctype ? doc.doctype.systemId : null; var isXML = false; switch (systemId) { case "http://www.w3.org/TR/html4/strict.dtd": // HTML 4 case "http://www.w3.org/TR/html4/loose.dtd": case null: isXML = false; break; case "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd": // XHTML 1 case "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd": isXML = true; break; case "": isXML = (doc.documentElement.getAttribute("xmlns") == "http://www.w3.org/1999/xhtml"); break; default: break; // should never happen... } return (isXML ? "application/xhtml+xml" : "text/html"); }, getWrapColumn: function() { try { return this.getCurrentEditor().wrapWidth; } catch (e) {} return 0; }, setDocumentURI: function(uri) { try { // XXX WE'LL NEED TO GET "CURRENT" CONTENT FRAME ONCE MULTIPLE EDITORS ARE ALLOWED this.getCurrentEditorElement().docShell.setCurrentURI(uri); } catch (e) { } }, documentReloadListener: { NotifyDocumentCreated: function() {}, NotifyDocumentWillBeDestroyed: function() {}, NotifyDocumentStateChanged:function( isNowDirty ) { var editor = EditorUtils.getCurrentEditor(); try { // unregister the listener to prevent multiple callbacks editor.removeDocumentStateListener( EditorUtils.documentReloadListener ); var charset = editor.documentCharacterSet; // update the META charset with the current presentation charset editor.documentCharacterSet = charset; } catch (e) {} } }, setDocumentCharacterSet: function(aCharset) { try { var editor = this.getCurrentEditor(); var editorElement = this.getCurrentEditorElement(); editor.documentCharacterSet = aCharset; var docUrl = this.getDocumentUrl(); if( !UrlUtils.isUrlOfBlankDocument(docUrl)) { // reloading the document will reverse any changes to the META charset, // we need to put them back in, which is achieved by a dedicated listener editor.addDocumentStateListener( this.documentReloadListener ); EditorLoadUrl(editorElement, docUrl); } } catch (e) {} }, createAnonymousElement: function(tag, parentNode, anonClass, isCreatedHidden) { var a = EditorUtils.getCurrentEditor().createAnonymousElement(tag, parentNode ? parentNode : EditorUtils.getCurrentDocument().body, anonClass, isCreatedHidden); return a; }, deleteAnonymousElement: function(node, parent) { //var ps = EditorUtils. }, getObjectForProperties: function(aNodeNames, aRequiredAttribute) { var editor = this.getCurrentEditor(); if (!editor) return null; /*var element; try { element = editor.getSelectedElement(""); } catch (e) {} if (element) return element;*/ // Find nearest parent of selection anchor node // that is a link, list, table cell, or table var anchorNode var node; try { anchorNode = editor.selection.anchorNode; if (anchorNode.firstChild) { // Start at actual selected node var offset = editor.selection.anchorOffset; // Note: If collapsed, offset points to element AFTER caret, // thus node may be null node = anchorNode.childNodes.item(offset); } if (!node) node = anchorNode; } catch (e) {} while (node) { if (node.nodeName) { var nodeName = node.nodeName.toLowerCase(); // Done when we hit the body if (nodeName == "body") break; if (aNodeNames.indexOf(nodeName) != -1 && (!aRequiredAttribute || node.getAttribute(aRequiredAttribute))) return node; } node = node.parentNode; } return null; }, insertElementAroundSelection: function(element) { function nodeIsBreak(editor, node) { //return !node || node.localName == 'BR' || editor.nodeIsBlock(node); return true; } var editor = this.getCurrentEditor(); try { // First get the selection as a single range var range, start, end, offset; var count = editor.selection.rangeCount; if (count == 1) range = editor.selection.getRangeAt(0).cloneRange(); else { range = editor.document.createRange(); start = editor.selection.getRangeAt(0) range.setStart(start.startContainer, start.startOffset); end = editor.selection.getRangeAt(--count); range.setEnd(end.endContainer, end.endOffset); } // Flatten the selection to child nodes of the common ancestor while (range.startContainer != range.commonAncestorContainer) range.setStartBefore(range.startContainer); while (range.endContainer != range.commonAncestorContainer) range.setEndAfter(range.endContainer); if (editor.nodeIsBlock(element)) // Block element parent must be a valid block while (!(range.commonAncestorContainer.localName in IsBlockParent)) range.selectNode(range.commonAncestorContainer); else { // Fail if we're not inserting a block (use setInlineProperty instead) if (!nodeIsBreak(editor, range.commonAncestorContainer)) return false; else if (range.commonAncestorContainer.localName in NotAnInlineParent) // Inline element parent must not be an invalid block do range.selectNode(range.commonAncestorContainer); while (range.commonAncestorContainer.localName in NotAnInlineParent); else // Further insert block check for (var i = range.startOffset; ; i++) if (i == range.endOffset) return false; else if (nodeIsBreak(editor, range.commonAncestorContainer.childNodes[i])) break; } // The range may be contained by body text, which should all be selected. offset = range.startOffset; start = range.startContainer.childNodes[offset]; if (!nodeIsBreak(editor, start)) { while (!nodeIsBreak(editor, start.previousSibling)) { start = start.previousSibling; offset--; } } end = range.endContainer.childNodes[range.endOffset]; if (end && !nodeIsBreak(editor, end.previousSibling)) { while (!nodeIsBreak(editor, end)) end = end.nextSibling; } // Now insert the node editor.insertNode(element, range.commonAncestorContainer, offset, true); offset = element.childNodes.length; if (!editor.nodeIsBlock(element)) editor.setShouldTxnSetSelection(false); // Move all the old child nodes to the element var empty = true; while (start != end) { var next = start.nextSibling; editor.deleteNode(start); editor.insertNode(start, element, element.childNodes.length); empty = false; start = next; } if (!editor.nodeIsBlock(element)) editor.setShouldTxnSetSelection(true); else { // Also move a trailing
if (start && start.localName == 'BR') { editor.deleteNode(start); editor.insertNode(start, element, element.childNodes.length); empty = false; } // Still nothing? Insert a
so the node is not empty if (empty) editor.insertNode(editor.createElementWithDefaults("br"), element, element.childNodes.length); // Hack to set the selection just inside the element editor.insertNode(editor.document.createTextNode(""), element, offset); } } finally { } return true; }, get activeViewActive() { return this.mActiveViewActive; }, set activeViewActive(val) { this.mActiveViewActive = val; } };