You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2875 lines
90 KiB
JavaScript
2875 lines
90 KiB
JavaScript
define("dijit/_editor/RichText", [
|
|
"dojo/_base/array", // array.forEach array.indexOf array.some
|
|
"dojo/_base/config", // config
|
|
"dojo/_base/declare", // declare
|
|
"dojo/_base/Deferred", // Deferred
|
|
"dojo/dom", // dom.byId
|
|
"dojo/dom-attr", // domAttr.set or get
|
|
"dojo/dom-class", // domClass.add domClass.remove
|
|
"dojo/dom-construct", // domConstruct.create domConstruct.destroy domConstruct.place
|
|
"dojo/dom-geometry", // domGeometry.position
|
|
"dojo/dom-style", // domStyle.getComputedStyle domStyle.set
|
|
"dojo/_base/event", // event.stop
|
|
"dojo/_base/kernel", // kernel.deprecated
|
|
"dojo/keys", // keys.BACKSPACE keys.TAB
|
|
"dojo/_base/lang", // lang.clone lang.hitch lang.isArray lang.isFunction lang.isString lang.trim
|
|
"dojo/on", // on()
|
|
"dojo/query", // query
|
|
"dojo/ready", // ready
|
|
"dojo/sniff", // has("ie") has("mozilla") has("opera") has("safari") has("webkit")
|
|
"dojo/topic", // topic.publish() (publish)
|
|
"dojo/_base/unload", // unload
|
|
"dojo/_base/url", // url
|
|
"dojo/_base/window", // win.global
|
|
"../_Widget",
|
|
"../_CssStateMixin",
|
|
"./selection",
|
|
"./range",
|
|
"./html",
|
|
"../focus",
|
|
"../main" // dijit._scopeName
|
|
], function(array, config, declare, Deferred, dom, domAttr, domClass, domConstruct, domGeometry, domStyle,
|
|
event, kernel, keys, lang, on, query, ready, has, topic, unload, _Url, win,
|
|
_Widget, _CssStateMixin, selectionapi, rangeapi, htmlapi, focus, dijit){
|
|
|
|
// module:
|
|
// dijit/_editor/RichText
|
|
// summary:
|
|
// dijit/_editor/RichText is the core of dijit/Editor, which provides basic
|
|
// WYSIWYG editing features.
|
|
|
|
// if you want to allow for rich text saving with back/forward actions, you must add a text area to your page with
|
|
// the id==dijit._scopeName + "._editor.RichText.value" (typically "dijit/_editor/RichText.value). For example,
|
|
// something like this will work:
|
|
//
|
|
// <textarea id="dijit._editor.RichText.value" style="display:none;position:absolute;top:-100px;left:-100px;height:3px;width:3px;overflow:hidden;"></textarea>
|
|
//
|
|
|
|
var RichText = declare("dijit._editor.RichText", [_Widget, _CssStateMixin], {
|
|
// summary:
|
|
// dijit/_editor/RichText is the core of dijit.Editor, which provides basic
|
|
// WYSIWYG editing features.
|
|
//
|
|
// description:
|
|
// dijit/_editor/RichText is the core of dijit.Editor, which provides basic
|
|
// WYSIWYG editing features. It also encapsulates the differences
|
|
// of different js engines for various browsers. Do not use this widget
|
|
// with an HTML <TEXTAREA> tag, since the browser unescapes XML escape characters,
|
|
// like <. This can have unexpected behavior and lead to security issues
|
|
// such as scripting attacks.
|
|
//
|
|
// tags:
|
|
// private
|
|
|
|
constructor: function(params /*===== , srcNodeRef =====*/){
|
|
// summary:
|
|
// Create the widget.
|
|
// params: Object|null
|
|
// Initial settings for any of the widget attributes, except readonly attributes.
|
|
// srcNodeRef: DOMNode
|
|
// The widget replaces the specified DOMNode.
|
|
|
|
// contentPreFilters: Function(String)[]
|
|
// Pre content filter function register array.
|
|
// these filters will be executed before the actual
|
|
// editing area gets the html content.
|
|
this.contentPreFilters = [];
|
|
|
|
// contentPostFilters: Function(String)[]
|
|
// post content filter function register array.
|
|
// These will be used on the resulting html
|
|
// from contentDomPostFilters. The resulting
|
|
// content is the final html (returned by getValue()).
|
|
this.contentPostFilters = [];
|
|
|
|
// contentDomPreFilters: Function(DomNode)[]
|
|
// Pre content dom filter function register array.
|
|
// These filters are applied after the result from
|
|
// contentPreFilters are set to the editing area.
|
|
this.contentDomPreFilters = [];
|
|
|
|
// contentDomPostFilters: Function(DomNode)[]
|
|
// Post content dom filter function register array.
|
|
// These filters are executed on the editing area dom.
|
|
// The result from these will be passed to contentPostFilters.
|
|
this.contentDomPostFilters = [];
|
|
|
|
// editingAreaStyleSheets: dojo._URL[]
|
|
// array to store all the stylesheets applied to the editing area
|
|
this.editingAreaStyleSheets = [];
|
|
|
|
// Make a copy of this.events before we start writing into it, otherwise we
|
|
// will modify the prototype which leads to bad things on pages w/multiple editors
|
|
this.events = [].concat(this.events);
|
|
|
|
this._keyHandlers = {};
|
|
|
|
if(params && lang.isString(params.value)){
|
|
this.value = params.value;
|
|
}
|
|
|
|
this.onLoadDeferred = new Deferred();
|
|
},
|
|
|
|
baseClass: "dijitEditor",
|
|
|
|
// inheritWidth: Boolean
|
|
// whether to inherit the parent's width or simply use 100%
|
|
inheritWidth: false,
|
|
|
|
// focusOnLoad: [deprecated] Boolean
|
|
// Focus into this widget when the page is loaded
|
|
focusOnLoad: false,
|
|
|
|
// name: String?
|
|
// Specifies the name of a (hidden) `<textarea>` node on the page that's used to save
|
|
// the editor content on page leave. Used to restore editor contents after navigating
|
|
// to a new page and then hitting the back button.
|
|
name: "",
|
|
|
|
// styleSheets: [const] String
|
|
// semicolon (";") separated list of css files for the editing area
|
|
styleSheets: "",
|
|
|
|
// height: String
|
|
// Set height to fix the editor at a specific height, with scrolling.
|
|
// By default, this is 300px. If you want to have the editor always
|
|
// resizes to accommodate the content, use AlwaysShowToolbar plugin
|
|
// and set height="". If this editor is used within a layout widget,
|
|
// set height="100%".
|
|
height: "300px",
|
|
|
|
// minHeight: String
|
|
// The minimum height that the editor should have.
|
|
minHeight: "1em",
|
|
|
|
// isClosed: [private] Boolean
|
|
isClosed: true,
|
|
|
|
// isLoaded: [private] Boolean
|
|
isLoaded: false,
|
|
|
|
// _SEPARATOR: [private] String
|
|
// Used to concat contents from multiple editors into a single string,
|
|
// so they can be saved into a single `<textarea>` node. See "name" attribute.
|
|
_SEPARATOR: "@@**%%__RICHTEXTBOUNDRY__%%**@@",
|
|
|
|
// _NAME_CONTENT_SEP: [private] String
|
|
// USed to separate name from content. Just a colon isn't safe.
|
|
_NAME_CONTENT_SEP: "@@**%%:%%**@@",
|
|
|
|
// onLoadDeferred: [readonly] dojo.Deferred
|
|
// Deferred which is fired when the editor finishes loading.
|
|
// Call myEditor.onLoadDeferred.then(callback) it to be informed
|
|
// when the rich-text area initialization is finalized.
|
|
onLoadDeferred: null,
|
|
|
|
// isTabIndent: Boolean
|
|
// Make tab key and shift-tab indent and outdent rather than navigating.
|
|
// Caution: sing this makes web pages inaccessible to users unable to use a mouse.
|
|
isTabIndent: false,
|
|
|
|
// disableSpellCheck: [const] Boolean
|
|
// When true, disables the browser's native spell checking, if supported.
|
|
// Works only in Firefox.
|
|
disableSpellCheck: false,
|
|
|
|
postCreate: function(){
|
|
if("textarea" === this.domNode.tagName.toLowerCase()){
|
|
console.warn("RichText should not be used with the TEXTAREA tag. See dijit._editor.RichText docs.");
|
|
}
|
|
|
|
// Push in the builtin filters now, making them the first executed, but not over-riding anything
|
|
// users passed in. See: #6062
|
|
this.contentPreFilters = [lang.hitch(this, "_preFixUrlAttributes")].concat(this.contentPreFilters);
|
|
if(has("mozilla")){
|
|
this.contentPreFilters = [this._normalizeFontStyle].concat(this.contentPreFilters);
|
|
this.contentPostFilters = [this._removeMozBogus].concat(this.contentPostFilters);
|
|
}
|
|
if(has("webkit")){
|
|
// Try to clean up WebKit bogus artifacts. The inserted classes
|
|
// made by WebKit sometimes messes things up.
|
|
this.contentPreFilters = [this._removeWebkitBogus].concat(this.contentPreFilters);
|
|
this.contentPostFilters = [this._removeWebkitBogus].concat(this.contentPostFilters);
|
|
}
|
|
if(has("ie")){
|
|
// IE generates <strong> and <em> but we want to normalize to <b> and <i>
|
|
this.contentPostFilters = [this._normalizeFontStyle].concat(this.contentPostFilters);
|
|
this.contentDomPostFilters = [lang.hitch(this, this._stripBreakerNodes)].concat(this.contentDomPostFilters);
|
|
}
|
|
this.inherited(arguments);
|
|
|
|
topic.publish(dijit._scopeName + "._editor.RichText::init", this);
|
|
this.open();
|
|
this.setupDefaultShortcuts();
|
|
},
|
|
|
|
setupDefaultShortcuts: function(){
|
|
// summary:
|
|
// Add some default key handlers
|
|
// description:
|
|
// Overwrite this to setup your own handlers. The default
|
|
// implementation does not use Editor commands, but directly
|
|
// executes the builtin commands within the underlying browser
|
|
// support.
|
|
// tags:
|
|
// protected
|
|
var exec = lang.hitch(this, function(cmd, arg){
|
|
return function(){
|
|
return !this.execCommand(cmd,arg);
|
|
};
|
|
});
|
|
|
|
var ctrlKeyHandlers = {
|
|
b: exec("bold"),
|
|
i: exec("italic"),
|
|
u: exec("underline"),
|
|
a: exec("selectall"),
|
|
s: function(){ this.save(true); },
|
|
m: function(){ this.isTabIndent = !this.isTabIndent; },
|
|
|
|
"1": exec("formatblock", "h1"),
|
|
"2": exec("formatblock", "h2"),
|
|
"3": exec("formatblock", "h3"),
|
|
"4": exec("formatblock", "h4"),
|
|
|
|
"\\": exec("insertunorderedlist")
|
|
};
|
|
|
|
if(!has("ie")){
|
|
ctrlKeyHandlers.Z = exec("redo"); //FIXME: undo?
|
|
}
|
|
|
|
var key;
|
|
for(key in ctrlKeyHandlers){
|
|
this.addKeyHandler(key, true, false, ctrlKeyHandlers[key]);
|
|
}
|
|
},
|
|
|
|
// events: [private] String[]
|
|
// events which should be connected to the underlying editing area
|
|
events: ["onKeyPress", "onKeyDown", "onKeyUp"], // onClick handled specially
|
|
|
|
// captureEvents: [deprecated] String[]
|
|
// Events which should be connected to the underlying editing
|
|
// area, events in this array will be addListener with
|
|
// capture=true.
|
|
// TODO: looking at the code I don't see any distinction between events and captureEvents,
|
|
// so get rid of this for 2.0 if not sooner
|
|
captureEvents: [],
|
|
|
|
_editorCommandsLocalized: false,
|
|
_localizeEditorCommands: function(){
|
|
// summary:
|
|
// When IE is running in a non-English locale, the API actually changes,
|
|
// so that we have to say (for example) danraku instead of p (for paragraph).
|
|
// Handle that here.
|
|
// tags:
|
|
// private
|
|
if(RichText._editorCommandsLocalized){
|
|
// Use the already generate cache of mappings.
|
|
this._local2NativeFormatNames = RichText._local2NativeFormatNames;
|
|
this._native2LocalFormatNames = RichText._native2LocalFormatNames;
|
|
return;
|
|
}
|
|
RichText._editorCommandsLocalized = true;
|
|
RichText._local2NativeFormatNames = {};
|
|
RichText._native2LocalFormatNames = {};
|
|
this._local2NativeFormatNames = RichText._local2NativeFormatNames;
|
|
this._native2LocalFormatNames = RichText._native2LocalFormatNames;
|
|
//in IE, names for blockformat is locale dependent, so we cache the values here
|
|
|
|
//put p after div, so if IE returns Normal, we show it as paragraph
|
|
//We can distinguish p and div if IE returns Normal, however, in order to detect that,
|
|
//we have to call this.document.selection.createRange().parentElement() or such, which
|
|
//could slow things down. Leave it as it is for now
|
|
var formats = ['div', 'p', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'address'];
|
|
var localhtml = "", format, i=0;
|
|
while((format=formats[i++])){
|
|
//append a <br> after each element to separate the elements more reliably
|
|
if(format.charAt(1) !== 'l'){
|
|
localhtml += "<"+format+"><span>content</span></"+format+"><br/>";
|
|
}else{
|
|
localhtml += "<"+format+"><li>content</li></"+format+"><br/>";
|
|
}
|
|
}
|
|
// queryCommandValue returns empty if we hide editNode, so move it out of screen temporary
|
|
// Also, IE9 does weird stuff unless we do it inside the editor iframe.
|
|
var style = { position: "absolute", top: "0px", zIndex: 10, opacity: 0.01 };
|
|
var div = domConstruct.create('div', {style: style, innerHTML: localhtml});
|
|
this.ownerDocumentBody.appendChild(div);
|
|
|
|
// IE9 has a timing issue with doing this right after setting
|
|
// the inner HTML, so put a delay in.
|
|
var inject = lang.hitch(this, function(){
|
|
var node = div.firstChild;
|
|
while(node){
|
|
try{
|
|
this._sCall("selectElement", [node.firstChild]);
|
|
var nativename = node.tagName.toLowerCase();
|
|
this._local2NativeFormatNames[nativename] = document.queryCommandValue("formatblock");
|
|
this._native2LocalFormatNames[this._local2NativeFormatNames[nativename]] = nativename;
|
|
node = node.nextSibling.nextSibling;
|
|
//console.log("Mapped: ", nativename, " to: ", this._local2NativeFormatNames[nativename]);
|
|
}catch(e){ /*Sqelch the occasional IE9 error */ }
|
|
}
|
|
domConstruct.destroy(div);
|
|
});
|
|
this.defer(inject);
|
|
},
|
|
|
|
open: function(/*DomNode?*/ element){
|
|
// summary:
|
|
// Transforms the node referenced in this.domNode into a rich text editing
|
|
// node.
|
|
// description:
|
|
// Sets up the editing area asynchronously. This will result in
|
|
// the creation and replacement with an iframe.
|
|
// tags:
|
|
// private
|
|
|
|
if(!this.onLoadDeferred || this.onLoadDeferred.fired >= 0){
|
|
this.onLoadDeferred = new Deferred();
|
|
}
|
|
|
|
if(!this.isClosed){ this.close(); }
|
|
topic.publish(dijit._scopeName + "._editor.RichText::open", this);
|
|
|
|
if(arguments.length === 1 && element.nodeName){ // else unchanged
|
|
this.domNode = element;
|
|
}
|
|
|
|
var dn = this.domNode;
|
|
|
|
// "html" will hold the innerHTML of the srcNodeRef and will be used to
|
|
// initialize the editor.
|
|
var html;
|
|
|
|
if(lang.isString(this.value)){
|
|
// Allow setting the editor content programmatically instead of
|
|
// relying on the initial content being contained within the target
|
|
// domNode.
|
|
html = this.value;
|
|
delete this.value;
|
|
dn.innerHTML = "";
|
|
}else if(dn.nodeName && dn.nodeName.toLowerCase() == "textarea"){
|
|
// if we were created from a textarea, then we need to create a
|
|
// new editing harness node.
|
|
var ta = (this.textarea = dn);
|
|
this.name = ta.name;
|
|
html = ta.value;
|
|
dn = this.domNode = this.ownerDocument.createElement("div");
|
|
dn.setAttribute('widgetId', this.id);
|
|
ta.removeAttribute('widgetId');
|
|
dn.cssText = ta.cssText;
|
|
dn.className += " " + ta.className;
|
|
domConstruct.place(dn, ta, "before");
|
|
var tmpFunc = lang.hitch(this, function(){
|
|
//some browsers refuse to submit display=none textarea, so
|
|
//move the textarea off screen instead
|
|
domStyle.set(ta, {
|
|
display: "block",
|
|
position: "absolute",
|
|
top: "-1000px"
|
|
});
|
|
|
|
if(has("ie")){ //nasty IE bug: abnormal formatting if overflow is not hidden
|
|
var s = ta.style;
|
|
this.__overflow = s.overflow;
|
|
s.overflow = "hidden";
|
|
}
|
|
});
|
|
if(has("ie")){
|
|
this.defer(tmpFunc, 10);
|
|
}else{
|
|
tmpFunc();
|
|
}
|
|
|
|
if(ta.form){
|
|
var resetValue = ta.value;
|
|
this.reset = function(){
|
|
var current = this.getValue();
|
|
if(current !== resetValue){
|
|
this.replaceValue(resetValue);
|
|
}
|
|
};
|
|
on(ta.form, "submit", lang.hitch(this, function(){
|
|
// Copy value to the <textarea> so it gets submitted along with form.
|
|
// FIXME: should we be calling close() here instead?
|
|
domAttr.set(ta, 'disabled', this.disabled); // don't submit the value if disabled
|
|
ta.value = this.getValue();
|
|
}));
|
|
}
|
|
}else{
|
|
html = htmlapi.getChildrenHtml(dn);
|
|
dn.innerHTML = "";
|
|
}
|
|
|
|
this.value = html;
|
|
|
|
// If we're a list item we have to put in a blank line to force the
|
|
// bullet to nicely align at the top of text
|
|
if(dn.nodeName && dn.nodeName === "LI"){
|
|
dn.innerHTML = " <br>";
|
|
}
|
|
|
|
// Construct the editor div structure.
|
|
this.header = dn.ownerDocument.createElement("div");
|
|
dn.appendChild(this.header);
|
|
this.editingArea = dn.ownerDocument.createElement("div");
|
|
dn.appendChild(this.editingArea);
|
|
this.footer = dn.ownerDocument.createElement("div");
|
|
dn.appendChild(this.footer);
|
|
|
|
if(!this.name){
|
|
this.name = this.id + "_AUTOGEN";
|
|
}
|
|
|
|
// User has pressed back/forward button so we lost the text in the editor, but it's saved
|
|
// in a hidden <textarea> (which contains the data for all the editors on this page),
|
|
// so get editor value from there
|
|
if(this.name !== "" && (!config["useXDomain"] || config["allowXdRichTextSave"])){
|
|
var saveTextarea = dom.byId(dijit._scopeName + "._editor.RichText.value");
|
|
if(saveTextarea && saveTextarea.value !== ""){
|
|
var datas = saveTextarea.value.split(this._SEPARATOR), i=0, dat;
|
|
while((dat=datas[i++])){
|
|
var data = dat.split(this._NAME_CONTENT_SEP);
|
|
if(data[0] === this.name){
|
|
html = data[1];
|
|
datas = datas.splice(i, 1);
|
|
saveTextarea.value = datas.join(this._SEPARATOR);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!RichText._globalSaveHandler){
|
|
RichText._globalSaveHandler = {};
|
|
unload.addOnUnload(function(){
|
|
var id;
|
|
for(id in RichText._globalSaveHandler){
|
|
var f = RichText._globalSaveHandler[id];
|
|
if(lang.isFunction(f)){
|
|
f();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
RichText._globalSaveHandler[this.id] = lang.hitch(this, "_saveContent");
|
|
}
|
|
|
|
this.isClosed = false;
|
|
|
|
var ifr = (this.editorObject = this.iframe = this.ownerDocument.createElement('iframe'));
|
|
ifr.id = this.id+"_iframe";
|
|
ifr.style.border = "none";
|
|
ifr.style.width = "100%";
|
|
if(this._layoutMode){
|
|
// iframe should be 100% height, thus getting it's height from surrounding
|
|
// <div> (which has the correct height set by Editor)
|
|
ifr.style.height = "100%";
|
|
}else{
|
|
if(has("ie") >= 7){
|
|
if(this.height){
|
|
ifr.style.height = this.height;
|
|
}
|
|
if(this.minHeight){
|
|
ifr.style.minHeight = this.minHeight;
|
|
}
|
|
}else{
|
|
ifr.style.height = this.height ? this.height : this.minHeight;
|
|
}
|
|
}
|
|
ifr.frameBorder = 0;
|
|
ifr._loadFunc = lang.hitch( this, function(w){
|
|
this.window = w;
|
|
this.document = this.window.document;
|
|
|
|
if(has("ie")){
|
|
this._localizeEditorCommands();
|
|
}
|
|
|
|
// Do final setup and set initial contents of editor
|
|
this.onLoad(html);
|
|
});
|
|
|
|
// Set the iframe's initial (blank) content.
|
|
var src = this._getIframeDocTxt(),
|
|
s = "javascript: '" + src.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'";
|
|
ifr.setAttribute('src', s);
|
|
this.editingArea.appendChild(ifr);
|
|
|
|
if(has("safari") <= 4){
|
|
src = ifr.getAttribute("src");
|
|
if(!src || src.indexOf("javascript") === -1){
|
|
// Safari 4 and earlier sometimes act oddly
|
|
// So we have to set it again.
|
|
this.defer(function(){ ifr.setAttribute('src', s); });
|
|
}
|
|
}
|
|
|
|
// TODO: this is a guess at the default line-height, kinda works
|
|
if(dn.nodeName === "LI"){
|
|
dn.lastChild.style.marginTop = "-1.2em";
|
|
}
|
|
|
|
domClass.add(this.domNode, this.baseClass);
|
|
},
|
|
|
|
//static cache variables shared among all instance of this class
|
|
_local2NativeFormatNames: {},
|
|
_native2LocalFormatNames: {},
|
|
|
|
_getIframeDocTxt: function(){
|
|
// summary:
|
|
// Generates the boilerplate text of the document inside the iframe (ie, `<html><head>...</head><body/></html>`).
|
|
// Editor content (if not blank) should be added afterwards.
|
|
// tags:
|
|
// private
|
|
var _cs = domStyle.getComputedStyle(this.domNode);
|
|
|
|
// The contents inside of <body>. The real contents are set later via a call to setValue().
|
|
var html = "";
|
|
var setBodyId = true;
|
|
if(has("ie") || has("webkit") || (!this.height && !has("mozilla"))){
|
|
// In auto-expand mode, need a wrapper div for AlwaysShowToolbar plugin to correctly
|
|
// expand/contract the editor as the content changes.
|
|
html = "<div id='dijitEditorBody'></div>";
|
|
setBodyId = false;
|
|
}else if(has("mozilla")){
|
|
// workaround bug where can't select then delete text (until user types something
|
|
// into the editor)... and/or issue where typing doesn't erase selected text
|
|
this._cursorToStart = true;
|
|
html = " "; //
|
|
}
|
|
|
|
var font = [ _cs.fontWeight, _cs.fontSize, _cs.fontFamily ].join(" ");
|
|
|
|
// line height is tricky - applying a units value will mess things up.
|
|
// if we can't get a non-units value, bail out.
|
|
var lineHeight = _cs.lineHeight;
|
|
if(lineHeight.indexOf("px") >= 0){
|
|
lineHeight = parseFloat(lineHeight)/parseFloat(_cs.fontSize);
|
|
// console.debug(lineHeight);
|
|
}else if(lineHeight.indexOf("em")>=0){
|
|
lineHeight = parseFloat(lineHeight);
|
|
}else{
|
|
// If we can't get a non-units value, just default
|
|
// it to the CSS spec default of 'normal'. Seems to
|
|
// work better, esp on IE, than '1.0'
|
|
lineHeight = "normal";
|
|
}
|
|
var userStyle = "";
|
|
var self = this;
|
|
this.style.replace(/(^|;)\s*(line-|font-?)[^;]+/ig, function(match){
|
|
match = match.replace(/^;/ig,"") + ';';
|
|
var s = match.split(":")[0];
|
|
if(s){
|
|
s = lang.trim(s);
|
|
s = s.toLowerCase();
|
|
var i;
|
|
var sC = "";
|
|
for(i = 0; i < s.length; i++){
|
|
var c = s.charAt(i);
|
|
switch(c){
|
|
case "-":
|
|
i++;
|
|
c = s.charAt(i).toUpperCase();
|
|
default:
|
|
sC += c;
|
|
}
|
|
}
|
|
domStyle.set(self.domNode, sC, "");
|
|
}
|
|
userStyle += match + ';';
|
|
});
|
|
|
|
|
|
// need to find any associated label element and update iframe document title
|
|
var label=query('label[for="'+this.id+'"]');
|
|
|
|
return [
|
|
this.isLeftToRight() ? "<html>\n<head>\n" : "<html dir='rtl'>\n<head>\n",
|
|
(has("mozilla") && label.length ? "<title>" + label[0].innerHTML + "</title>\n" : ""),
|
|
"<meta http-equiv='Content-Type' content='text/html'>\n",
|
|
"<style>\n",
|
|
"\tbody,html {\n",
|
|
"\t\tbackground:transparent;\n",
|
|
"\t\tpadding: 1px 0 0 0;\n",
|
|
"\t\tmargin: -1px 0 0 0;\n", // remove extraneous vertical scrollbar on safari and firefox
|
|
|
|
// Set the html/body sizing. Webkit always needs this, other browsers
|
|
// only set it when height is defined (not auto-expanding), otherwise
|
|
// scrollers do not appear.
|
|
((has("webkit"))?"\t\twidth: 100%;\n":""),
|
|
((has("webkit"))?"\t\theight: 100%;\n":""),
|
|
"\t}\n",
|
|
|
|
// TODO: left positioning will cause contents to disappear out of view
|
|
// if it gets too wide for the visible area
|
|
"\tbody{\n",
|
|
"\t\ttop:0px;\n",
|
|
"\t\tleft:0px;\n",
|
|
"\t\tright:0px;\n",
|
|
"\t\tfont:", font, ";\n",
|
|
((this.height||has("opera")) ? "" : "\t\tposition: fixed;\n"),
|
|
// FIXME: IE 6 won't understand min-height?
|
|
"\t\tmin-height:", this.minHeight, ";\n",
|
|
"\t\tline-height:", lineHeight,";\n",
|
|
"\t}\n",
|
|
"\tp{ margin: 1em 0; }\n",
|
|
|
|
// Determine how scrollers should be applied. In autoexpand mode (height = "") no scrollers on y at all.
|
|
// But in fixed height mode we want both x/y scrollers. Also, if it's using wrapping div and in auto-expand
|
|
// (Mainly IE) we need to kill the y scroller on body and html.
|
|
(!setBodyId && !this.height ? "\tbody,html {overflow-y: hidden;}\n" : ""),
|
|
"\t#dijitEditorBody{overflow-x: auto; overflow-y:" + (this.height ? "auto;" : "hidden;") + " outline: 0px;}\n",
|
|
"\tli > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; }\n",
|
|
// Can't set min-height in IE9, it puts layout on li, which puts move/resize handles.
|
|
(!has("ie") ? "\tli{ min-height:1.2em; }\n" : ""),
|
|
"</style>\n",
|
|
this._applyEditingAreaStyleSheets(),"\n",
|
|
"</head>\n<body ",
|
|
(setBodyId?"id='dijitEditorBody' ":""),
|
|
|
|
// Onload handler fills in real editor content.
|
|
// On IE9, sometimes onload is called twice, and the first time frameElement is null (test_FullScreen.html)
|
|
"onload='frameElement && frameElement._loadFunc(window,document)' ",
|
|
"style='"+userStyle+"'>", html, "</body>\n</html>"
|
|
].join(""); // String
|
|
},
|
|
|
|
_applyEditingAreaStyleSheets: function(){
|
|
// summary:
|
|
// apply the specified css files in styleSheets
|
|
// tags:
|
|
// private
|
|
var files = [];
|
|
if(this.styleSheets){
|
|
files = this.styleSheets.split(';');
|
|
this.styleSheets = '';
|
|
}
|
|
|
|
//empty this.editingAreaStyleSheets here, as it will be filled in addStyleSheet
|
|
files = files.concat(this.editingAreaStyleSheets);
|
|
this.editingAreaStyleSheets = [];
|
|
|
|
var text='', i=0, url;
|
|
while((url=files[i++])){
|
|
var abstring = (new _Url(win.global.location, url)).toString();
|
|
this.editingAreaStyleSheets.push(abstring);
|
|
text += '<link rel="stylesheet" type="text/css" href="'+abstring+'"/>';
|
|
}
|
|
return text;
|
|
},
|
|
|
|
addStyleSheet: function(/*dojo/_base/url*/ uri){
|
|
// summary:
|
|
// add an external stylesheet for the editing area
|
|
// uri:
|
|
// Url of the external css file
|
|
var url=uri.toString();
|
|
|
|
//if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
|
|
if(url.charAt(0) === '.' || (url.charAt(0) !== '/' && !uri.host)){
|
|
url = (new _Url(win.global.location, url)).toString();
|
|
}
|
|
|
|
if(array.indexOf(this.editingAreaStyleSheets, url) > -1){
|
|
// console.debug("dijit/_editor/RichText.addStyleSheet(): Style sheet "+url+" is already applied");
|
|
return;
|
|
}
|
|
|
|
this.editingAreaStyleSheets.push(url);
|
|
this.onLoadDeferred.then(lang.hitch(this, function(){
|
|
if(this.document.createStyleSheet){ //IE
|
|
this.document.createStyleSheet(url);
|
|
}else{ //other browser
|
|
var head = this.document.getElementsByTagName("head")[0];
|
|
var stylesheet = this.document.createElement("link");
|
|
stylesheet.rel="stylesheet";
|
|
stylesheet.type="text/css";
|
|
stylesheet.href=url;
|
|
head.appendChild(stylesheet);
|
|
}
|
|
}));
|
|
},
|
|
|
|
removeStyleSheet: function(/*dojo/_base/url*/ uri){
|
|
// summary:
|
|
// remove an external stylesheet for the editing area
|
|
var url=uri.toString();
|
|
//if uri is relative, then convert it to absolute so that it can be resolved correctly in iframe
|
|
if(url.charAt(0) === '.' || (url.charAt(0) !== '/' && !uri.host)){
|
|
url = (new _Url(win.global.location, url)).toString();
|
|
}
|
|
var index = array.indexOf(this.editingAreaStyleSheets, url);
|
|
if(index === -1){
|
|
// console.debug("dijit/_editor/RichText.removeStyleSheet(): Style sheet "+url+" has not been applied");
|
|
return;
|
|
}
|
|
delete this.editingAreaStyleSheets[index];
|
|
query('link:[href="'+url+'"]', this.window.document).orphan();
|
|
},
|
|
|
|
// disabled: Boolean
|
|
// The editor is disabled; the text cannot be changed.
|
|
disabled: false,
|
|
|
|
_mozSettingProps: {'styleWithCSS':false},
|
|
_setDisabledAttr: function(/*Boolean*/ value){
|
|
value = !!value;
|
|
this._set("disabled", value);
|
|
if(!this.isLoaded){ return; } // this method requires init to be complete
|
|
if(has("ie") || has("webkit") || has("opera")){
|
|
var preventIEfocus = has("ie") && (this.isLoaded || !this.focusOnLoad);
|
|
if(preventIEfocus){ this.editNode.unselectable = "on"; }
|
|
this.editNode.contentEditable = !value;
|
|
if(preventIEfocus){
|
|
this.defer(function(){
|
|
if(this.editNode){ // guard in case widget destroyed before timeout
|
|
this.editNode.unselectable = "off";
|
|
}
|
|
});
|
|
}
|
|
}else{ //moz
|
|
try{
|
|
this.document.designMode=(value?'off':'on');
|
|
}catch(e){ return; } // ! _disabledOK
|
|
if(!value && this._mozSettingProps){
|
|
var ps = this._mozSettingProps;
|
|
var n;
|
|
for(n in ps){
|
|
if(ps.hasOwnProperty(n)){
|
|
try{
|
|
this.document.execCommand(n,false,ps[n]);
|
|
}catch(e2){}
|
|
}
|
|
}
|
|
}
|
|
// this.document.execCommand('contentReadOnly', false, value);
|
|
// if(value){
|
|
// this.blur(); //to remove the blinking caret
|
|
// }
|
|
}
|
|
this._disabledOK = true;
|
|
},
|
|
|
|
/* Event handlers
|
|
*****************/
|
|
|
|
onLoad: function(/*String*/ html){
|
|
// summary:
|
|
// Handler after the iframe finishes loading.
|
|
// html: String
|
|
// Editor contents should be set to this value
|
|
// tags:
|
|
// protected
|
|
|
|
// TODO: rename this to _onLoad, make empty public onLoad() method, deprecate/make protected onLoadDeferred handler?
|
|
|
|
if(!this.window.__registeredWindow){
|
|
this.window.__registeredWindow = true;
|
|
this._iframeRegHandle = focus.registerIframe(this.iframe);
|
|
}
|
|
if(!has("ie") && !has("webkit") && (this.height || has("mozilla"))){
|
|
this.editNode=this.document.body;
|
|
}else{
|
|
// there's a wrapper div around the content, see _getIframeDocTxt().
|
|
this.editNode=this.document.body.firstChild;
|
|
var _this = this;
|
|
if(has("ie")){ // #4996 IE wants to focus the BODY tag
|
|
this.tabStop = domConstruct.create('div', { tabIndex: -1 }, this.editingArea);
|
|
this.iframe.onfocus = function(){ _this.editNode.setActive(); };
|
|
}
|
|
}
|
|
this.focusNode = this.editNode; // for InlineEditBox
|
|
|
|
|
|
var events = this.events.concat(this.captureEvents);
|
|
var ap = this.iframe ? this.document : this.editNode;
|
|
array.forEach(events, function(item){
|
|
this.connect(ap, item.toLowerCase(), item);
|
|
}, this);
|
|
|
|
this.connect(ap, "onmouseup", "onClick"); // mouseup in the margin does not generate an onclick event
|
|
|
|
if(has("ie")){ // IE contentEditable
|
|
this.connect(this.document, "onmousedown", "_onIEMouseDown"); // #4996 fix focus
|
|
|
|
// give the node Layout on IE
|
|
// TODO: this may no longer be needed, since we've reverted IE to using an iframe,
|
|
// not contentEditable. Removing it would also probably remove the need for creating
|
|
// the extra <div> in _getIframeDocTxt()
|
|
this.editNode.style.zoom = 1.0;
|
|
}else{
|
|
this.connect(this.document, "onmousedown", function(){
|
|
// Clear the moveToStart focus, as mouse
|
|
// down will set cursor point. Required to properly
|
|
// work with selection/position driven plugins and clicks in
|
|
// the window. refs: #10678
|
|
delete this._cursorToStart;
|
|
});
|
|
}
|
|
|
|
if(has("webkit")){
|
|
//WebKit sometimes doesn't fire right on selections, so the toolbar
|
|
//doesn't update right. Therefore, help it out a bit with an additional
|
|
//listener. A mouse up will typically indicate a display change, so fire this
|
|
//and get the toolbar to adapt. Reference: #9532
|
|
this._webkitListener = this.connect(this.document, "onmouseup", "onDisplayChanged");
|
|
this.connect(this.document, "onmousedown", function(e){
|
|
var t = e.target;
|
|
if(t && (t === this.document.body || t === this.document)){
|
|
// Since WebKit uses the inner DIV, we need to check and set position.
|
|
// See: #12024 as to why the change was made.
|
|
this.defer("placeCursorAtEnd");
|
|
}
|
|
});
|
|
}
|
|
|
|
if(has("ie")){
|
|
// Try to make sure 'hidden' elements aren't visible in edit mode (like browsers other than IE
|
|
// do). See #9103
|
|
try{
|
|
this.document.execCommand('RespectVisibilityInDesign', true, null);
|
|
}catch(e){/* squelch */}
|
|
}
|
|
|
|
this.isLoaded = true;
|
|
|
|
this.set('disabled', this.disabled); // initialize content to editable (or not)
|
|
|
|
// Note that setValue() call will only work after isLoaded is set to true (above)
|
|
|
|
// Set up a function to allow delaying the setValue until a callback is fired
|
|
// This ensures extensions like dijit.Editor have a way to hold the value set
|
|
// until plugins load (and do things like register filters).
|
|
var setContent = lang.hitch(this, function(){
|
|
this.setValue(html);
|
|
if(this.onLoadDeferred){
|
|
this.onLoadDeferred.resolve(true);
|
|
}
|
|
this.onDisplayChanged();
|
|
if(this.focusOnLoad){
|
|
// after the document loads, then set focus after updateInterval expires so that
|
|
// onNormalizedDisplayChanged has run to avoid input caret issues
|
|
ready(lang.hitch(this, "defer", "focus", this.updateInterval));
|
|
}
|
|
// Save off the initial content now
|
|
this.value = this.getValue(true);
|
|
});
|
|
if(this.setValueDeferred){
|
|
this.setValueDeferred.then(setContent);
|
|
}else{
|
|
setContent();
|
|
}
|
|
},
|
|
|
|
onKeyDown: function(/* Event */ e){
|
|
// summary:
|
|
// Handler for onkeydown event
|
|
// tags:
|
|
// protected
|
|
|
|
// we need this event at the moment to get the events from control keys
|
|
// such as the backspace. It might be possible to add this to Dojo, so that
|
|
// keyPress events can be emulated by the keyDown and keyUp detection.
|
|
|
|
if(e.keyCode === keys.TAB && this.isTabIndent ){
|
|
event.stop(e); //prevent tab from moving focus out of editor
|
|
|
|
// FIXME: this is a poor-man's indent/outdent. It would be
|
|
// better if it added 4 " " chars in an undoable way.
|
|
// Unfortunately pasteHTML does not prove to be undoable
|
|
if(this.queryCommandEnabled((e.shiftKey ? "outdent" : "indent"))){
|
|
this.execCommand((e.shiftKey ? "outdent" : "indent"));
|
|
}
|
|
}
|
|
if(has("ie")){
|
|
if(e.keyCode == keys.TAB && !this.isTabIndent){
|
|
if(e.shiftKey && !e.ctrlKey && !e.altKey){
|
|
// focus the BODY so the browser will tab away from it instead
|
|
this.iframe.focus();
|
|
}else if(!e.shiftKey && !e.ctrlKey && !e.altKey){
|
|
// focus the BODY so the browser will tab away from it instead
|
|
this.tabStop.focus();
|
|
}
|
|
}else if(e.keyCode === keys.BACKSPACE && this.document.selection.type === "Control"){
|
|
// IE has a bug where if a non-text object is selected in the editor,
|
|
// hitting backspace would act as if the browser's back button was
|
|
// clicked instead of deleting the object. see #1069
|
|
event.stop(e);
|
|
this.execCommand("delete");
|
|
}else if((65 <= e.keyCode && e.keyCode <= 90) ||
|
|
(e.keyCode>=37 && e.keyCode<=40) // FIXME: get this from connect() instead!
|
|
){ //arrow keys
|
|
e.charCode = e.keyCode;
|
|
this.onKeyPress(e);
|
|
}
|
|
}
|
|
if(has("ff")){
|
|
if(e.keyCode === keys.PAGE_UP || e.keyCode === keys.PAGE_DOWN ){
|
|
if(this.editNode.clientHeight >= this.editNode.scrollHeight){
|
|
// Stop the event to prevent firefox from trapping the cursor when there is no scroll bar.
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
onKeyUp: function(/*===== e =====*/){
|
|
// summary:
|
|
// Handler for onkeyup event
|
|
// tags:
|
|
// callback
|
|
},
|
|
|
|
setDisabled: function(/*Boolean*/ disabled){
|
|
// summary:
|
|
// Deprecated, use set('disabled', ...) instead.
|
|
// tags:
|
|
// deprecated
|
|
kernel.deprecated('dijit.Editor::setDisabled is deprecated','use dijit.Editor::attr("disabled",boolean) instead', 2.0);
|
|
this.set('disabled',disabled);
|
|
},
|
|
_setValueAttr: function(/*String*/ value){
|
|
// summary:
|
|
// Registers that attr("value", foo) should call setValue(foo)
|
|
this.setValue(value);
|
|
},
|
|
_setDisableSpellCheckAttr: function(/*Boolean*/ disabled){
|
|
if(this.document){
|
|
domAttr.set(this.document.body, "spellcheck", !disabled);
|
|
}else{
|
|
// try again after the editor is finished loading
|
|
this.onLoadDeferred.then(lang.hitch(this, function(){
|
|
domAttr.set(this.document.body, "spellcheck", !disabled);
|
|
}));
|
|
}
|
|
this._set("disableSpellCheck", disabled);
|
|
},
|
|
|
|
onKeyPress: function(e){
|
|
// summary:
|
|
// Handle the various key events
|
|
// tags:
|
|
// protected
|
|
|
|
var c = (e.keyChar && e.keyChar.toLowerCase()) || e.keyCode,
|
|
handlers = this._keyHandlers[c],
|
|
args = arguments;
|
|
|
|
if(handlers && !e.altKey){
|
|
array.some(handlers, function(h){
|
|
// treat meta- same as ctrl-, for benefit of mac users
|
|
if(!(h.shift ^ e.shiftKey) && !(h.ctrl ^ (e.ctrlKey||e.metaKey))){
|
|
if(!h.handler.apply(this, args)){
|
|
e.preventDefault();
|
|
}
|
|
return true;
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
// function call after the character has been inserted
|
|
if(!this._onKeyHitch){
|
|
this._onKeyHitch = lang.hitch(this, "onKeyPressed");
|
|
}
|
|
this.defer("_onKeyHitch", 1);
|
|
return true;
|
|
},
|
|
|
|
addKeyHandler: function(/*String*/ key, /*Boolean*/ ctrl, /*Boolean*/ shift, /*Function*/ handler){
|
|
// summary:
|
|
// Add a handler for a keyboard shortcut
|
|
// description:
|
|
// The key argument should be in lowercase if it is a letter character
|
|
// tags:
|
|
// protected
|
|
if(!lang.isArray(this._keyHandlers[key])){
|
|
this._keyHandlers[key] = [];
|
|
}
|
|
//TODO: would be nice to make this a hash instead of an array for quick lookups
|
|
this._keyHandlers[key].push({
|
|
shift: shift || false,
|
|
ctrl: ctrl || false,
|
|
handler: handler
|
|
});
|
|
},
|
|
|
|
onKeyPressed: function(){
|
|
// summary:
|
|
// Handler for after the user has pressed a key, and the display has been updated.
|
|
// (Runs on a timer so that it runs after the display is updated)
|
|
// tags:
|
|
// private
|
|
this.onDisplayChanged(/*e*/); // can't pass in e
|
|
},
|
|
|
|
onClick: function(/*Event*/ e){
|
|
// summary:
|
|
// Handler for when the user clicks.
|
|
// tags:
|
|
// private
|
|
|
|
// console.info('onClick',this._tryDesignModeOn);
|
|
this.onDisplayChanged(e);
|
|
},
|
|
|
|
_onIEMouseDown: function(){
|
|
// summary:
|
|
// IE only to prevent 2 clicks to focus
|
|
// tags:
|
|
// protected
|
|
|
|
if(!this.focused && !this.disabled){
|
|
this.focus();
|
|
}
|
|
},
|
|
|
|
_onBlur: function(e){
|
|
// summary:
|
|
// Called from focus manager when focus has moved away from this editor
|
|
// tags:
|
|
// protected
|
|
|
|
// console.info('_onBlur')
|
|
|
|
this.inherited(arguments);
|
|
|
|
var newValue = this.getValue(true);
|
|
if(newValue !== this.value){
|
|
this.onChange(newValue);
|
|
}
|
|
this._set("value", newValue);
|
|
},
|
|
|
|
_onFocus: function(/*Event*/ e){
|
|
// summary:
|
|
// Called from focus manager when focus has moved into this editor
|
|
// tags:
|
|
// protected
|
|
|
|
// console.info('_onFocus')
|
|
if(!this.disabled){
|
|
if(!this._disabledOK){
|
|
this.set('disabled', false);
|
|
}
|
|
this.inherited(arguments);
|
|
}
|
|
},
|
|
|
|
// TODO: remove in 2.0
|
|
blur: function(){
|
|
// summary:
|
|
// Remove focus from this instance.
|
|
// tags:
|
|
// deprecated
|
|
if(!has("ie") && this.window.document.documentElement && this.window.document.documentElement.focus){
|
|
this.window.document.documentElement.focus();
|
|
}else if(this.ownerDocumentBody.focus){
|
|
this.ownerDocumentBody.focus();
|
|
}
|
|
},
|
|
|
|
focus: function(){
|
|
// summary:
|
|
// Move focus to this editor
|
|
if(!this.isLoaded){
|
|
this.focusOnLoad = true;
|
|
return;
|
|
}
|
|
if(this._cursorToStart){
|
|
delete this._cursorToStart;
|
|
if(this.editNode.childNodes){
|
|
this.placeCursorAtStart(); // this calls focus() so return
|
|
return;
|
|
}
|
|
}
|
|
if(!has("ie")){
|
|
focus.focus(this.iframe);
|
|
}else if(this.editNode && this.editNode.focus){
|
|
// editNode may be hidden in display:none div, lets just punt in this case
|
|
//this.editNode.focus(); -> causes IE to scroll always (strict and quirks mode) to the top the Iframe
|
|
// if we fire the event manually and let the browser handle the focusing, the latest
|
|
// cursor position is focused like in FF
|
|
this.iframe.fireEvent('onfocus', document.createEventObject()); // createEventObject only in IE
|
|
// }else{
|
|
// TODO: should we throw here?
|
|
// console.debug("Have no idea how to focus into the editor!");
|
|
}
|
|
},
|
|
|
|
// _lastUpdate: 0,
|
|
updateInterval: 200,
|
|
_updateTimer: null,
|
|
onDisplayChanged: function(/*Event*/ /*===== e =====*/){
|
|
// summary:
|
|
// This event will be fired every time the display context
|
|
// changes and the result needs to be reflected in the UI.
|
|
// description:
|
|
// If you don't want to have update too often,
|
|
// onNormalizedDisplayChanged should be used instead
|
|
// tags:
|
|
// private
|
|
|
|
// var _t=new Date();
|
|
if(this._updateTimer){
|
|
this._updateTimer.remove();
|
|
}
|
|
this._updateTimer = this.defer("onNormalizedDisplayChanged", this.updateInterval);
|
|
|
|
// Technically this should trigger a call to watch("value", ...) registered handlers,
|
|
// but getValue() is too slow to call on every keystroke so we don't.
|
|
},
|
|
onNormalizedDisplayChanged: function(){
|
|
// summary:
|
|
// This event is fired every updateInterval ms or more
|
|
// description:
|
|
// If something needs to happen immediately after a
|
|
// user change, please use onDisplayChanged instead.
|
|
// tags:
|
|
// private
|
|
delete this._updateTimer;
|
|
},
|
|
onChange: function(/*===== newContent =====*/){
|
|
// summary:
|
|
// This is fired if and only if the editor loses focus and
|
|
// the content is changed.
|
|
},
|
|
_normalizeCommand: function(/*String*/ cmd, /*Anything?*/argument){
|
|
// summary:
|
|
// Used as the advice function to map our
|
|
// normalized set of commands to those supported by the target
|
|
// browser.
|
|
// tags:
|
|
// private
|
|
|
|
var command = cmd.toLowerCase();
|
|
if(command === "formatblock"){
|
|
if(has("safari") && argument === undefined){ command = "heading"; }
|
|
}else if(command === "hilitecolor" && !has("mozilla")){
|
|
command = "backcolor";
|
|
}
|
|
|
|
return command;
|
|
},
|
|
|
|
_qcaCache: {},
|
|
queryCommandAvailable: function(/*String*/ command){
|
|
// summary:
|
|
// Tests whether a command is supported by the host. Clients
|
|
// SHOULD check whether a command is supported before attempting
|
|
// to use it, behaviour for unsupported commands is undefined.
|
|
// command:
|
|
// The command to test for
|
|
// tags:
|
|
// private
|
|
|
|
// memoizing version. See _queryCommandAvailable for computing version
|
|
var ca = this._qcaCache[command];
|
|
if(ca !== undefined){ return ca; }
|
|
return (this._qcaCache[command] = this._queryCommandAvailable(command));
|
|
},
|
|
|
|
_queryCommandAvailable: function(/*String*/ command){
|
|
// summary:
|
|
// See queryCommandAvailable().
|
|
// tags:
|
|
// private
|
|
|
|
var ie = 1;
|
|
var mozilla = 1 << 1;
|
|
var webkit = 1 << 2;
|
|
var opera = 1 << 3;
|
|
|
|
function isSupportedBy(browsers){
|
|
return {
|
|
ie: Boolean(browsers & ie),
|
|
mozilla: Boolean(browsers & mozilla),
|
|
webkit: Boolean(browsers & webkit),
|
|
opera: Boolean(browsers & opera)
|
|
};
|
|
}
|
|
|
|
var supportedBy = null;
|
|
|
|
switch(command.toLowerCase()){
|
|
case "bold": case "italic": case "underline":
|
|
case "subscript": case "superscript":
|
|
case "fontname": case "fontsize":
|
|
case "forecolor": case "hilitecolor":
|
|
case "justifycenter": case "justifyfull": case "justifyleft":
|
|
case "justifyright": case "delete": case "selectall": case "toggledir":
|
|
supportedBy = isSupportedBy(mozilla | ie | webkit | opera);
|
|
break;
|
|
|
|
case "createlink": case "unlink": case "removeformat":
|
|
case "inserthorizontalrule": case "insertimage":
|
|
case "insertorderedlist": case "insertunorderedlist":
|
|
case "indent": case "outdent": case "formatblock":
|
|
case "inserthtml": case "undo": case "redo": case "strikethrough": case "tabindent":
|
|
supportedBy = isSupportedBy(mozilla | ie | opera | webkit);
|
|
break;
|
|
|
|
case "blockdirltr": case "blockdirrtl":
|
|
case "dirltr": case "dirrtl":
|
|
case "inlinedirltr": case "inlinedirrtl":
|
|
supportedBy = isSupportedBy(ie);
|
|
break;
|
|
case "cut": case "copy": case "paste":
|
|
supportedBy = isSupportedBy( ie | mozilla | webkit | opera);
|
|
break;
|
|
|
|
case "inserttable":
|
|
supportedBy = isSupportedBy(mozilla | ie);
|
|
break;
|
|
|
|
case "insertcell": case "insertcol": case "insertrow":
|
|
case "deletecells": case "deletecols": case "deleterows":
|
|
case "mergecells": case "splitcell":
|
|
supportedBy = isSupportedBy(ie | mozilla);
|
|
break;
|
|
|
|
default: return false;
|
|
}
|
|
|
|
return (has("ie") && supportedBy.ie) ||
|
|
(has("mozilla") && supportedBy.mozilla) ||
|
|
(has("webkit") && supportedBy.webkit) ||
|
|
(has("opera") && supportedBy.opera); // Boolean return true if the command is supported, false otherwise
|
|
},
|
|
|
|
execCommand: function(/*String*/ command, argument){
|
|
// summary:
|
|
// Executes a command in the Rich Text area
|
|
// command:
|
|
// The command to execute
|
|
// argument:
|
|
// An optional argument to the command
|
|
// tags:
|
|
// protected
|
|
var returnValue;
|
|
|
|
//focus() is required for IE to work
|
|
//In addition, focus() makes sure after the execution of
|
|
//the command, the editor receives the focus as expected
|
|
this.focus();
|
|
|
|
command = this._normalizeCommand(command, argument);
|
|
|
|
if(argument !== undefined){
|
|
if(command === "heading"){
|
|
throw new Error("unimplemented");
|
|
}else if((command === "formatblock") && has("ie")){
|
|
argument = '<'+argument+'>';
|
|
}
|
|
}
|
|
|
|
//Check to see if we have any over-rides for commands, they will be functions on this
|
|
//widget of the form _commandImpl. If we don't, fall through to the basic native
|
|
//exec command of the browser.
|
|
var implFunc = "_" + command + "Impl";
|
|
if(this[implFunc]){
|
|
returnValue = this[implFunc](argument);
|
|
}else{
|
|
argument = arguments.length > 1 ? argument : null;
|
|
if(argument || command !== "createlink"){
|
|
returnValue = this.document.execCommand(command, false, argument);
|
|
}
|
|
}
|
|
|
|
this.onDisplayChanged();
|
|
return returnValue;
|
|
},
|
|
|
|
queryCommandEnabled: function(/*String*/ command){
|
|
// summary:
|
|
// Check whether a command is enabled or not.
|
|
// command:
|
|
// The command to execute
|
|
// tags:
|
|
// protected
|
|
if(this.disabled || !this._disabledOK){ return false; }
|
|
|
|
command = this._normalizeCommand(command);
|
|
|
|
//Check to see if we have any over-rides for commands, they will be functions on this
|
|
//widget of the form _commandEnabledImpl. If we don't, fall through to the basic native
|
|
//command of the browser.
|
|
var implFunc = "_" + command + "EnabledImpl";
|
|
|
|
if(this[implFunc]){
|
|
return this[implFunc](command);
|
|
}else{
|
|
return this._browserQueryCommandEnabled(command);
|
|
}
|
|
},
|
|
|
|
queryCommandState: function(command){
|
|
// summary:
|
|
// Check the state of a given command and returns true or false.
|
|
// tags:
|
|
// protected
|
|
|
|
if(this.disabled || !this._disabledOK){ return false; }
|
|
command = this._normalizeCommand(command);
|
|
try{
|
|
return this.document.queryCommandState(command);
|
|
}catch(e){
|
|
//Squelch, occurs if editor is hidden on FF 3 (and maybe others.)
|
|
return false;
|
|
}
|
|
},
|
|
|
|
queryCommandValue: function(command){
|
|
// summary:
|
|
// Check the value of a given command. This matters most for
|
|
// custom selections and complex values like font value setting.
|
|
// tags:
|
|
// protected
|
|
|
|
if(this.disabled || !this._disabledOK){ return false; }
|
|
var r;
|
|
command = this._normalizeCommand(command);
|
|
if(has("ie") && command === "formatblock"){
|
|
r = this._native2LocalFormatNames[this.document.queryCommandValue(command)];
|
|
}else if(has("mozilla") && command === "hilitecolor"){
|
|
var oldValue;
|
|
try{
|
|
oldValue = this.document.queryCommandValue("styleWithCSS");
|
|
}catch(e){
|
|
oldValue = false;
|
|
}
|
|
this.document.execCommand("styleWithCSS", false, true);
|
|
r = this.document.queryCommandValue(command);
|
|
this.document.execCommand("styleWithCSS", false, oldValue);
|
|
}else{
|
|
r = this.document.queryCommandValue(command);
|
|
}
|
|
return r;
|
|
},
|
|
|
|
// Misc.
|
|
|
|
_sCall: function(name, args){
|
|
// summary:
|
|
// Run the named method of dijit/_editor/selection over the
|
|
// current editor instance's window, with the passed args.
|
|
// tags:
|
|
// private
|
|
return win.withGlobal(this.window, name, selectionapi, args);
|
|
},
|
|
|
|
// FIXME: this is a TON of code duplication. Why?
|
|
|
|
placeCursorAtStart: function(){
|
|
// summary:
|
|
// Place the cursor at the start of the editing area.
|
|
// tags:
|
|
// private
|
|
|
|
this.focus();
|
|
|
|
//see comments in placeCursorAtEnd
|
|
var isvalid=false;
|
|
if(has("mozilla")){
|
|
// TODO: Is this branch even necessary?
|
|
var first=this.editNode.firstChild;
|
|
while(first){
|
|
if(first.nodeType === 3){
|
|
if(first.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
|
|
isvalid=true;
|
|
this._sCall("selectElement", [ first ]);
|
|
break;
|
|
}
|
|
}else if(first.nodeType === 1){
|
|
isvalid=true;
|
|
var tg = first.tagName ? first.tagName.toLowerCase() : "";
|
|
// Collapse before childless tags.
|
|
if(/br|input|img|base|meta|area|basefont|hr|link/.test(tg)){
|
|
this._sCall("selectElement", [ first ]);
|
|
}else{
|
|
// Collapse inside tags with children.
|
|
this._sCall("selectElementChildren", [ first ]);
|
|
}
|
|
break;
|
|
}
|
|
first = first.nextSibling;
|
|
}
|
|
}else{
|
|
isvalid=true;
|
|
this._sCall("selectElementChildren", [ this.editNode ]);
|
|
}
|
|
if(isvalid){
|
|
this._sCall("collapse", [ true ]);
|
|
}
|
|
},
|
|
|
|
placeCursorAtEnd: function(){
|
|
// summary:
|
|
// Place the cursor at the end of the editing area.
|
|
// tags:
|
|
// private
|
|
|
|
this.focus();
|
|
|
|
//In mozilla, if last child is not a text node, we have to use
|
|
// selectElementChildren on this.editNode.lastChild otherwise the
|
|
// cursor would be placed at the end of the closing tag of
|
|
//this.editNode.lastChild
|
|
var isvalid=false;
|
|
if(has("mozilla")){
|
|
var last=this.editNode.lastChild;
|
|
while(last){
|
|
if(last.nodeType === 3){
|
|
if(last.nodeValue.replace(/^\s+|\s+$/g, "").length>0){
|
|
isvalid=true;
|
|
this._sCall("selectElement", [ last ]);
|
|
break;
|
|
}
|
|
}else if(last.nodeType === 1){
|
|
isvalid=true;
|
|
this._sCall("selectElement", [ last.lastChild || last]);
|
|
break;
|
|
}
|
|
last = last.previousSibling;
|
|
}
|
|
}else{
|
|
isvalid=true;
|
|
this._sCall("selectElementChildren", [ this.editNode ]);
|
|
}
|
|
if(isvalid){
|
|
this._sCall("collapse", [ false ]);
|
|
}
|
|
},
|
|
|
|
getValue: function(/*Boolean?*/ nonDestructive){
|
|
// summary:
|
|
// Return the current content of the editing area (post filters
|
|
// are applied). Users should call get('value') instead.
|
|
// nonDestructive:
|
|
// defaults to false. Should the post-filtering be run over a copy
|
|
// of the live DOM? Most users should pass "true" here unless they
|
|
// *really* know that none of the installed filters are going to
|
|
// mess up the editing session.
|
|
// tags:
|
|
// private
|
|
if(this.textarea){
|
|
if(this.isClosed || !this.isLoaded){
|
|
return this.textarea.value;
|
|
}
|
|
}
|
|
|
|
return this._postFilterContent(null, nonDestructive);
|
|
},
|
|
_getValueAttr: function(){
|
|
// summary:
|
|
// Hook to make attr("value") work
|
|
return this.getValue(true);
|
|
},
|
|
|
|
setValue: function(/*String*/ html){
|
|
// summary:
|
|
// This function sets the content. No undo history is preserved.
|
|
// Users should use set('value', ...) instead.
|
|
// tags:
|
|
// deprecated
|
|
|
|
// TODO: remove this and getValue() for 2.0, and move code to _setValueAttr()
|
|
|
|
if(!this.isLoaded){
|
|
// try again after the editor is finished loading
|
|
this.onLoadDeferred.then(lang.hitch(this, function(){
|
|
this.setValue(html);
|
|
}));
|
|
return;
|
|
}
|
|
this._cursorToStart = true;
|
|
if(this.textarea && (this.isClosed || !this.isLoaded)){
|
|
this.textarea.value=html;
|
|
}else{
|
|
html = this._preFilterContent(html);
|
|
var node = this.isClosed ? this.domNode : this.editNode;
|
|
if(html && has("mozilla") && html.toLowerCase() === "<p></p>"){
|
|
html = "<p> </p>"; //
|
|
}
|
|
|
|
// Use to avoid webkit problems where editor is disabled until the user clicks it
|
|
if(!html && has("webkit")){
|
|
html = " "; //
|
|
}
|
|
node.innerHTML = html;
|
|
this._preDomFilterContent(node);
|
|
}
|
|
|
|
this.onDisplayChanged();
|
|
this._set("value", this.getValue(true));
|
|
},
|
|
|
|
replaceValue: function(/*String*/ html){
|
|
// summary:
|
|
// This function set the content while trying to maintain the undo stack
|
|
// (now only works fine with Moz, this is identical to setValue in all
|
|
// other browsers)
|
|
// tags:
|
|
// protected
|
|
|
|
if(this.isClosed){
|
|
this.setValue(html);
|
|
}else if(this.window && this.window.getSelection && !has("mozilla")){ // Safari
|
|
// look ma! it's a totally f'd browser!
|
|
this.setValue(html);
|
|
}else if(this.window && this.window.getSelection){ // Moz
|
|
html = this._preFilterContent(html);
|
|
this.execCommand("selectall");
|
|
if(!html){
|
|
this._cursorToStart = true;
|
|
html = " "; //
|
|
}
|
|
this.execCommand("inserthtml", html);
|
|
this._preDomFilterContent(this.editNode);
|
|
}else if(this.document && this.document.selection){//IE
|
|
//In IE, when the first element is not a text node, say
|
|
//an <a> tag, when replacing the content of the editing
|
|
//area, the <a> tag will be around all the content
|
|
//so for now, use setValue for IE too
|
|
this.setValue(html);
|
|
}
|
|
|
|
this._set("value", this.getValue(true));
|
|
},
|
|
|
|
_preFilterContent: function(/*String*/ html){
|
|
// summary:
|
|
// Filter the input before setting the content of the editing
|
|
// area. DOM pre-filtering may happen after this
|
|
// string-based filtering takes place but as of 1.2, this is not
|
|
// guaranteed for operations such as the inserthtml command.
|
|
// tags:
|
|
// private
|
|
|
|
var ec = html;
|
|
array.forEach(this.contentPreFilters, function(ef){ if(ef){ ec = ef(ec); } });
|
|
return ec;
|
|
},
|
|
_preDomFilterContent: function(/*DomNode*/ dom){
|
|
// summary:
|
|
// filter the input's live DOM. All filter operations should be
|
|
// considered to be "live" and operating on the DOM that the user
|
|
// will be interacting with in their editing session.
|
|
// tags:
|
|
// private
|
|
dom = dom || this.editNode;
|
|
array.forEach(this.contentDomPreFilters, function(ef){
|
|
if(ef && lang.isFunction(ef)){
|
|
ef(dom);
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
_postFilterContent: function(
|
|
/*DomNode|DomNode[]|String?*/ dom,
|
|
/*Boolean?*/ nonDestructive){
|
|
// summary:
|
|
// filter the output after getting the content of the editing area
|
|
//
|
|
// description:
|
|
// post-filtering allows plug-ins and users to specify any number
|
|
// of transforms over the editor's content, enabling many common
|
|
// use-cases such as transforming absolute to relative URLs (and
|
|
// vice-versa), ensuring conformance with a particular DTD, etc.
|
|
// The filters are registered in the contentDomPostFilters and
|
|
// contentPostFilters arrays. Each item in the
|
|
// contentDomPostFilters array is a function which takes a DOM
|
|
// Node or array of nodes as its only argument and returns the
|
|
// same. It is then passed down the chain for further filtering.
|
|
// The contentPostFilters array behaves the same way, except each
|
|
// member operates on strings. Together, the DOM and string-based
|
|
// filtering allow the full range of post-processing that should
|
|
// be necessaray to enable even the most agressive of post-editing
|
|
// conversions to take place.
|
|
//
|
|
// If nonDestructive is set to "true", the nodes are cloned before
|
|
// filtering proceeds to avoid potentially destructive transforms
|
|
// to the content which may still needed to be edited further.
|
|
// Once DOM filtering has taken place, the serialized version of
|
|
// the DOM which is passed is run through each of the
|
|
// contentPostFilters functions.
|
|
//
|
|
// dom:
|
|
// a node, set of nodes, which to filter using each of the current
|
|
// members of the contentDomPostFilters and contentPostFilters arrays.
|
|
//
|
|
// nonDestructive:
|
|
// defaults to "false". If true, ensures that filtering happens on
|
|
// a clone of the passed-in content and not the actual node
|
|
// itself.
|
|
//
|
|
// tags:
|
|
// private
|
|
|
|
var ec;
|
|
if(!lang.isString(dom)){
|
|
dom = dom || this.editNode;
|
|
if(this.contentDomPostFilters.length){
|
|
if(nonDestructive){
|
|
dom = lang.clone(dom);
|
|
}
|
|
array.forEach(this.contentDomPostFilters, function(ef){
|
|
dom = ef(dom);
|
|
});
|
|
}
|
|
ec = htmlapi.getChildrenHtml(dom);
|
|
}else{
|
|
ec = dom;
|
|
}
|
|
|
|
if(!lang.trim(ec.replace(/^\xA0\xA0*/, '').replace(/\xA0\xA0*$/, '')).length){
|
|
ec = "";
|
|
}
|
|
|
|
// if(has("ie")){
|
|
// //removing appended <P> </P> for IE
|
|
// ec = ec.replace(/(?:<p> </p>[\n\r]*)+$/i,"");
|
|
// }
|
|
array.forEach(this.contentPostFilters, function(ef){
|
|
ec = ef(ec);
|
|
});
|
|
|
|
return ec;
|
|
},
|
|
|
|
_saveContent: function(){
|
|
// summary:
|
|
// Saves the content in an onunload event if the editor has not been closed
|
|
// tags:
|
|
// private
|
|
|
|
var saveTextarea = dom.byId(dijit._scopeName + "._editor.RichText.value");
|
|
if(saveTextarea){
|
|
if(saveTextarea.value){
|
|
saveTextarea.value += this._SEPARATOR;
|
|
}
|
|
saveTextarea.value += this.name + this._NAME_CONTENT_SEP + this.getValue(true);
|
|
}
|
|
},
|
|
|
|
|
|
escapeXml: function(/*String*/ str, /*Boolean*/ noSingleQuotes){
|
|
// summary:
|
|
// Adds escape sequences for special characters in XML.
|
|
// Optionally skips escapes for single quotes
|
|
// tags:
|
|
// private
|
|
|
|
str = str.replace(/&/gm, "&").replace(/</gm, "<").replace(/>/gm, ">").replace(/"/gm, """);
|
|
if(!noSingleQuotes){
|
|
str = str.replace(/'/gm, "'");
|
|
}
|
|
return str; // string
|
|
},
|
|
|
|
getNodeHtml: function(/* DomNode */ node){
|
|
// summary:
|
|
// Deprecated. Use dijit/_editor/html::_getNodeHtml() instead.
|
|
// tags:
|
|
// deprecated
|
|
kernel.deprecated('dijit.Editor::getNodeHtml is deprecated','use dijit/_editor/html::getNodeHtml instead', 2);
|
|
return htmlapi.getNodeHtml(node); // String
|
|
},
|
|
|
|
getNodeChildrenHtml: function(/* DomNode */ dom){
|
|
// summary:
|
|
// Deprecated. Use dijit/_editor/html::getChildrenHtml() instead.
|
|
// tags:
|
|
// deprecated
|
|
kernel.deprecated('dijit.Editor::getNodeChildrenHtml is deprecated','use dijit/_editor/html::getChildrenHtml instead', 2);
|
|
return htmlapi.getChildrenHtml(dom);
|
|
},
|
|
|
|
close: function(/*Boolean?*/ save){
|
|
// summary:
|
|
// Kills the editor and optionally writes back the modified contents to the
|
|
// element from which it originated.
|
|
// save:
|
|
// Whether or not to save the changes. If false, the changes are discarded.
|
|
// tags:
|
|
// private
|
|
|
|
if(this.isClosed){ return; }
|
|
|
|
if(!arguments.length){ save = true; }
|
|
if(save){
|
|
this._set("value", this.getValue(true));
|
|
}
|
|
|
|
// line height is squashed for iframes
|
|
// FIXME: why was this here? if(this.iframe){ this.domNode.style.lineHeight = null; }
|
|
|
|
if(this.interval){ clearInterval(this.interval); }
|
|
|
|
if(this._webkitListener){
|
|
//Cleaup of WebKit fix: #9532
|
|
this.disconnect(this._webkitListener);
|
|
delete this._webkitListener;
|
|
}
|
|
|
|
// Guard against memory leaks on IE (see #9268)
|
|
if(has("ie")){
|
|
this.iframe.onfocus = null;
|
|
}
|
|
this.iframe._loadFunc = null;
|
|
|
|
if(this._iframeRegHandle){
|
|
this._iframeRegHandle.remove();
|
|
delete this._iframeRegHandle;
|
|
}
|
|
|
|
if(this.textarea){
|
|
var s = this.textarea.style;
|
|
s.position = "";
|
|
s.left = s.top = "";
|
|
if(has("ie")){
|
|
s.overflow = this.__overflow;
|
|
this.__overflow = null;
|
|
}
|
|
this.textarea.value = this.value;
|
|
domConstruct.destroy(this.domNode);
|
|
this.domNode = this.textarea;
|
|
}else{
|
|
// Note that this destroys the iframe
|
|
this.domNode.innerHTML = this.value;
|
|
}
|
|
delete this.iframe;
|
|
|
|
domClass.remove(this.domNode, this.baseClass);
|
|
this.isClosed = true;
|
|
this.isLoaded = false;
|
|
|
|
delete this.editNode;
|
|
delete this.focusNode;
|
|
|
|
if(this.window && this.window._frameElement){
|
|
this.window._frameElement = null;
|
|
}
|
|
|
|
this.window = null;
|
|
this.document = null;
|
|
this.editingArea = null;
|
|
this.editorObject = null;
|
|
},
|
|
|
|
destroy: function(){
|
|
if(!this.isClosed){ this.close(false); }
|
|
if(this._updateTimer){
|
|
this._updateTimer.remove();
|
|
}
|
|
this.inherited(arguments);
|
|
if(RichText._globalSaveHandler){
|
|
delete RichText._globalSaveHandler[this.id];
|
|
}
|
|
},
|
|
|
|
_removeMozBogus: function(/* String */ html){
|
|
// summary:
|
|
// Post filter to remove unwanted HTML attributes generated by mozilla
|
|
// tags:
|
|
// private
|
|
return html.replace(/\stype="_moz"/gi, '').replace(/\s_moz_dirty=""/gi, '').replace(/_moz_resizing="(true|false)"/gi,''); // String
|
|
},
|
|
_removeWebkitBogus: function(/* String */ html){
|
|
// summary:
|
|
// Post filter to remove unwanted HTML attributes generated by webkit
|
|
// tags:
|
|
// private
|
|
html = html.replace(/\sclass="webkit-block-placeholder"/gi, '');
|
|
html = html.replace(/\sclass="apple-style-span"/gi, '');
|
|
// For some reason copy/paste sometime adds extra meta tags for charset on
|
|
// webkit (chrome) on mac.They need to be removed. See: #12007"
|
|
html = html.replace(/<meta charset=\"utf-8\" \/>/gi, '');
|
|
return html; // String
|
|
},
|
|
_normalizeFontStyle: function(/* String */ html){
|
|
// summary:
|
|
// Convert 'strong' and 'em' to 'b' and 'i'.
|
|
// description:
|
|
// Moz can not handle strong/em tags correctly, so to help
|
|
// mozilla and also to normalize output, convert them to 'b' and 'i'.
|
|
//
|
|
// Note the IE generates 'strong' and 'em' rather than 'b' and 'i'
|
|
// tags:
|
|
// private
|
|
return html.replace(/<(\/)?strong([ \>])/gi, '<$1b$2')
|
|
.replace(/<(\/)?em([ \>])/gi, '<$1i$2' ); // String
|
|
},
|
|
|
|
_preFixUrlAttributes: function(/* String */ html){
|
|
// summary:
|
|
// Pre-filter to do fixing to href attributes on `<a>` and `<img>` tags
|
|
// tags:
|
|
// private
|
|
return html.replace(/(?:(<a(?=\s).*?\shref=)("|')(.*?)\2)|(?:(<a\s.*?href=)([^"'][^ >]+))/gi,
|
|
'$1$4$2$3$5$2 _djrealurl=$2$3$5$2')
|
|
.replace(/(?:(<img(?=\s).*?\ssrc=)("|')(.*?)\2)|(?:(<img\s.*?src=)([^"'][^ >]+))/gi,
|
|
'$1$4$2$3$5$2 _djrealurl=$2$3$5$2'); // String
|
|
},
|
|
|
|
/*****************************************************************************
|
|
The following functions implement HTML manipulation commands for various
|
|
browser/contentEditable implementations. The goal of them is to enforce
|
|
standard behaviors of them.
|
|
******************************************************************************/
|
|
|
|
/*** queryCommandEnabled implementations ***/
|
|
|
|
_browserQueryCommandEnabled: function(command){
|
|
// summary:
|
|
// Implementation to call to the native queryCommandEnabled of the browser.
|
|
// command:
|
|
// The command to check.
|
|
// tags:
|
|
// protected
|
|
if(!command) { return false; }
|
|
var elem = has("ie") ? this.document.selection.createRange() : this.document;
|
|
try{
|
|
return elem.queryCommandEnabled(command);
|
|
}catch(e){
|
|
return false;
|
|
}
|
|
},
|
|
|
|
_createlinkEnabledImpl: function(/*===== argument =====*/){
|
|
// summary:
|
|
// This function implements the test for if the create link
|
|
// command should be enabled or not.
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var enabled = true;
|
|
if(has("opera")){
|
|
var sel = this.window.getSelection();
|
|
if(sel.isCollapsed){
|
|
enabled = true;
|
|
}else{
|
|
enabled = this.document.queryCommandEnabled("createlink");
|
|
}
|
|
}else{
|
|
enabled = this._browserQueryCommandEnabled("createlink");
|
|
}
|
|
return enabled;
|
|
},
|
|
|
|
_unlinkEnabledImpl: function(/*===== argument =====*/){
|
|
// summary:
|
|
// This function implements the test for if the unlink
|
|
// command should be enabled or not.
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var enabled = true;
|
|
if(has("mozilla") || has("webkit")){
|
|
enabled = this._sCall("hasAncestorElement", ["a"]);
|
|
}else{
|
|
enabled = this._browserQueryCommandEnabled("unlink");
|
|
}
|
|
return enabled;
|
|
},
|
|
|
|
_inserttableEnabledImpl: function(/*===== argument =====*/){
|
|
// summary:
|
|
// This function implements the test for if the inserttable
|
|
// command should be enabled or not.
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var enabled = true;
|
|
if(has("mozilla") || has("webkit")){
|
|
enabled = true;
|
|
}else{
|
|
enabled = this._browserQueryCommandEnabled("inserttable");
|
|
}
|
|
return enabled;
|
|
},
|
|
|
|
_cutEnabledImpl: function(/*===== argument =====*/){
|
|
// summary:
|
|
// This function implements the test for if the cut
|
|
// command should be enabled or not.
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var enabled = true;
|
|
if(has("webkit")){
|
|
// WebKit deems clipboard activity as a security threat and natively would return false
|
|
var sel = this.window.getSelection();
|
|
if(sel){ sel = sel.toString(); }
|
|
enabled = !!sel;
|
|
}else{
|
|
enabled = this._browserQueryCommandEnabled("cut");
|
|
}
|
|
return enabled;
|
|
},
|
|
|
|
_copyEnabledImpl: function(/*===== argument =====*/){
|
|
// summary:
|
|
// This function implements the test for if the copy
|
|
// command should be enabled or not.
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var enabled = true;
|
|
if(has("webkit")){
|
|
// WebKit deems clipboard activity as a security threat and natively would return false
|
|
var sel = this.window.getSelection();
|
|
if(sel){ sel = sel.toString(); }
|
|
enabled = !!sel;
|
|
}else{
|
|
enabled = this._browserQueryCommandEnabled("copy");
|
|
}
|
|
return enabled;
|
|
},
|
|
|
|
_pasteEnabledImpl: function(/*===== argument =====*/){
|
|
// summary:c
|
|
// This function implements the test for if the paste
|
|
// command should be enabled or not.
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var enabled = true;
|
|
if(has("webkit")){
|
|
return true;
|
|
}else{
|
|
enabled = this._browserQueryCommandEnabled("paste");
|
|
}
|
|
return enabled;
|
|
},
|
|
|
|
/*** execCommand implementations ***/
|
|
|
|
_inserthorizontalruleImpl: function(argument){
|
|
// summary:
|
|
// This function implements the insertion of HTML 'HR' tags.
|
|
// into a point on the page. IE doesn't to it right, so
|
|
// we have to use an alternate form
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
if(has("ie")){
|
|
return this._inserthtmlImpl("<hr>");
|
|
}
|
|
return this.document.execCommand("inserthorizontalrule", false, argument);
|
|
},
|
|
|
|
_unlinkImpl: function(argument){
|
|
// summary:
|
|
// This function implements the unlink of an 'a' tag.
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
if((this.queryCommandEnabled("unlink")) && (has("mozilla") || has("webkit"))){
|
|
var a = this._sCall("getAncestorElement", [ "a" ]);
|
|
this._sCall("selectElement", [ a ]);
|
|
return this.document.execCommand("unlink", false, null);
|
|
}
|
|
return this.document.execCommand("unlink", false, argument);
|
|
},
|
|
|
|
_hilitecolorImpl: function(argument){
|
|
// summary:
|
|
// This function implements the hilitecolor command
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var returnValue;
|
|
var isApplied = this._handleTextColorOrProperties("hilitecolor", argument);
|
|
if(!isApplied){
|
|
if(has("mozilla")){
|
|
// mozilla doesn't support hilitecolor properly when useCSS is
|
|
// set to false (bugzilla #279330)
|
|
this.document.execCommand("styleWithCSS", false, true);
|
|
console.log("Executing color command.");
|
|
returnValue = this.document.execCommand("hilitecolor", false, argument);
|
|
this.document.execCommand("styleWithCSS", false, false);
|
|
}else{
|
|
returnValue = this.document.execCommand("hilitecolor", false, argument);
|
|
}
|
|
}
|
|
return returnValue;
|
|
},
|
|
|
|
_backcolorImpl: function(argument){
|
|
// summary:
|
|
// This function implements the backcolor command
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
if(has("ie")){
|
|
// Tested under IE 6 XP2, no problem here, comment out
|
|
// IE weirdly collapses ranges when we exec these commands, so prevent it
|
|
// var tr = this.document.selection.createRange();
|
|
argument = argument ? argument : null;
|
|
}
|
|
var isApplied = this._handleTextColorOrProperties("backcolor", argument);
|
|
if(!isApplied){
|
|
isApplied = this.document.execCommand("backcolor", false, argument);
|
|
}
|
|
return isApplied;
|
|
},
|
|
|
|
_forecolorImpl: function(argument){
|
|
// summary:
|
|
// This function implements the forecolor command
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
if(has("ie")){
|
|
// Tested under IE 6 XP2, no problem here, comment out
|
|
// IE weirdly collapses ranges when we exec these commands, so prevent it
|
|
// var tr = this.document.selection.createRange();
|
|
argument = argument? argument : null;
|
|
}
|
|
var isApplied = false;
|
|
isApplied = this._handleTextColorOrProperties("forecolor", argument);
|
|
if(!isApplied){
|
|
isApplied = this.document.execCommand("forecolor", false, argument);
|
|
}
|
|
return isApplied;
|
|
},
|
|
|
|
_inserthtmlImpl: function(argument){
|
|
// summary:
|
|
// This function implements the insertion of HTML content into
|
|
// a point on the page.
|
|
// argument:
|
|
// The content to insert, if any.
|
|
// tags:
|
|
// protected
|
|
argument = this._preFilterContent(argument);
|
|
var rv = true;
|
|
if(has("ie")){
|
|
var insertRange = this.document.selection.createRange();
|
|
if(this.document.selection.type.toUpperCase() === 'CONTROL'){
|
|
var n=insertRange.item(0);
|
|
while(insertRange.length){
|
|
insertRange.remove(insertRange.item(0));
|
|
}
|
|
n.outerHTML=argument;
|
|
}else{
|
|
insertRange.pasteHTML(argument);
|
|
}
|
|
insertRange.select();
|
|
//insertRange.collapse(true);
|
|
}else if(has("mozilla") && !argument.length){
|
|
//mozilla can not inserthtml an empty html to delete current selection
|
|
//so we delete the selection instead in this case
|
|
this._sCall("remove"); // FIXME
|
|
}else{
|
|
rv = this.document.execCommand("inserthtml", false, argument);
|
|
}
|
|
return rv;
|
|
},
|
|
|
|
_boldImpl: function(argument){
|
|
// summary:
|
|
// This function implements an over-ride of the bold command.
|
|
// argument:
|
|
// Not used, operates by selection.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
this._adaptIESelection();
|
|
applied = this._adaptIEFormatAreaAndExec("bold");
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("bold", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
_italicImpl: function(argument){
|
|
// summary:
|
|
// This function implements an over-ride of the italic command.
|
|
// argument:
|
|
// Not used, operates by selection.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
this._adaptIESelection();
|
|
applied = this._adaptIEFormatAreaAndExec("italic");
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("italic", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
_underlineImpl: function(argument){
|
|
// summary:
|
|
// This function implements an over-ride of the underline command.
|
|
// argument:
|
|
// Not used, operates by selection.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
this._adaptIESelection();
|
|
applied = this._adaptIEFormatAreaAndExec("underline");
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("underline", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
_strikethroughImpl: function(argument){
|
|
// summary:
|
|
// This function implements an over-ride of the strikethrough command.
|
|
// argument:
|
|
// Not used, operates by selection.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
this._adaptIESelection();
|
|
applied = this._adaptIEFormatAreaAndExec("strikethrough");
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("strikethrough", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
_superscriptImpl: function(argument){
|
|
// summary:
|
|
// This function implements an over-ride of the superscript command.
|
|
// argument:
|
|
// Not used, operates by selection.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
this._adaptIESelection();
|
|
applied = this._adaptIEFormatAreaAndExec("superscript");
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("superscript", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
_subscriptImpl: function(argument){
|
|
// summary:
|
|
// This function implements an over-ride of the superscript command.
|
|
// argument:
|
|
// Not used, operates by selection.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
this._adaptIESelection();
|
|
applied = this._adaptIEFormatAreaAndExec("subscript");
|
|
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("subscript", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
_fontnameImpl: function(argument){
|
|
// summary:
|
|
// This function implements the fontname command
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var isApplied;
|
|
if(has("ie")){
|
|
isApplied = this._handleTextColorOrProperties("fontname", argument);
|
|
}
|
|
if(!isApplied){
|
|
isApplied = this.document.execCommand("fontname", false, argument);
|
|
}
|
|
return isApplied;
|
|
},
|
|
|
|
_fontsizeImpl: function(argument){
|
|
// summary:
|
|
// This function implements the fontsize command
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var isApplied;
|
|
if(has("ie")){
|
|
isApplied = this._handleTextColorOrProperties("fontsize", argument);
|
|
}
|
|
if(!isApplied){
|
|
isApplied = this.document.execCommand("fontsize", false, argument);
|
|
}
|
|
return isApplied;
|
|
},
|
|
|
|
_insertorderedlistImpl: function(argument){
|
|
// summary:
|
|
// This function implements the insertorderedlist command
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
applied = this._adaptIEList("insertorderedlist", argument);
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("insertorderedlist", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
_insertunorderedlistImpl: function(argument){
|
|
// summary:
|
|
// This function implements the insertunorderedlist command
|
|
// argument:
|
|
// arguments to the exec command, if any.
|
|
// tags:
|
|
// protected
|
|
var applied = false;
|
|
if(has("ie")){
|
|
applied = this._adaptIEList("insertunorderedlist", argument);
|
|
}
|
|
if(!applied){
|
|
applied = this.document.execCommand("insertunorderedlist", false, argument);
|
|
}
|
|
return applied;
|
|
},
|
|
|
|
getHeaderHeight: function(){
|
|
// summary:
|
|
// A function for obtaining the height of the header node
|
|
return this._getNodeChildrenHeight(this.header); // Number
|
|
},
|
|
|
|
getFooterHeight: function(){
|
|
// summary:
|
|
// A function for obtaining the height of the footer node
|
|
return this._getNodeChildrenHeight(this.footer); // Number
|
|
},
|
|
|
|
_getNodeChildrenHeight: function(node){
|
|
// summary:
|
|
// An internal function for computing the cumulative height of all child nodes of 'node'
|
|
// node:
|
|
// The node to process the children of;
|
|
var h = 0;
|
|
if(node && node.childNodes){
|
|
// IE didn't compute it right when position was obtained on the node directly is some cases,
|
|
// so we have to walk over all the children manually.
|
|
var i;
|
|
for(i = 0; i < node.childNodes.length; i++){
|
|
var size = domGeometry.position(node.childNodes[i]);
|
|
h += size.h;
|
|
}
|
|
}
|
|
return h; // Number
|
|
},
|
|
|
|
_isNodeEmpty: function(node, startOffset){
|
|
// summary:
|
|
// Function to test if a node is devoid of real content.
|
|
// node:
|
|
// The node to check.
|
|
// tags:
|
|
// private.
|
|
if(node.nodeType === 1/*element*/){
|
|
if(node.childNodes.length > 0){
|
|
return this._isNodeEmpty(node.childNodes[0], startOffset);
|
|
}
|
|
return true;
|
|
}else if(node.nodeType === 3/*text*/){
|
|
return (node.nodeValue.substring(startOffset) === "");
|
|
}
|
|
return false;
|
|
},
|
|
|
|
_removeStartingRangeFromRange: function(node, range){
|
|
// summary:
|
|
// Function to adjust selection range by removing the current
|
|
// start node.
|
|
// node:
|
|
// The node to remove from the starting range.
|
|
// range:
|
|
// The range to adapt.
|
|
// tags:
|
|
// private
|
|
if(node.nextSibling){
|
|
range.setStart(node.nextSibling,0);
|
|
}else{
|
|
var parent = node.parentNode;
|
|
while(parent && parent.nextSibling == null){
|
|
//move up the tree until we find a parent that has another node, that node will be the next node
|
|
parent = parent.parentNode;
|
|
}
|
|
if(parent){
|
|
range.setStart(parent.nextSibling,0);
|
|
}
|
|
}
|
|
return range;
|
|
},
|
|
|
|
_adaptIESelection: function(){
|
|
// summary:
|
|
// Function to adapt the IE range by removing leading 'newlines'
|
|
// Needed to fix issue with bold/italics/underline not working if
|
|
// range included leading 'newlines'.
|
|
// In IE, if a user starts a selection at the very end of a line,
|
|
// then the native browser commands will fail to execute correctly.
|
|
// To work around the issue, we can remove all empty nodes from
|
|
// the start of the range selection.
|
|
var selection = rangeapi.getSelection(this.window);
|
|
if(selection && selection.rangeCount && !selection.isCollapsed){
|
|
var range = selection.getRangeAt(0);
|
|
var firstNode = range.startContainer;
|
|
var startOffset = range.startOffset;
|
|
|
|
while(firstNode.nodeType === 3/*text*/ && startOffset >= firstNode.length && firstNode.nextSibling){
|
|
//traverse the text nodes until we get to the one that is actually highlighted
|
|
startOffset = startOffset - firstNode.length;
|
|
firstNode = firstNode.nextSibling;
|
|
}
|
|
|
|
//Remove the starting ranges until the range does not start with an empty node.
|
|
var lastNode=null;
|
|
while(this._isNodeEmpty(firstNode, startOffset) && firstNode !== lastNode){
|
|
lastNode =firstNode; //this will break the loop in case we can't find the next sibling
|
|
range = this._removeStartingRangeFromRange(firstNode, range); //move the start container to the next node in the range
|
|
firstNode = range.startContainer;
|
|
startOffset = 0; //start at the beginning of the new starting range
|
|
}
|
|
selection.removeAllRanges();// this will work as long as users cannot select multiple ranges. I have not been able to do that in the editor.
|
|
selection.addRange(range);
|
|
}
|
|
},
|
|
|
|
_adaptIEFormatAreaAndExec: function(command){
|
|
// summary:
|
|
// Function to handle IE's quirkiness regarding how it handles
|
|
// format commands on a word. This involves a lit of node splitting
|
|
// and format cloning.
|
|
// command:
|
|
// The format command, needed to check if the desired
|
|
// command is true or not.
|
|
var selection = rangeapi.getSelection(this.window);
|
|
var doc = this.document;
|
|
var rs, ret, range, txt, startNode, endNode, breaker, sNode;
|
|
if(command && selection && selection.isCollapsed){
|
|
var isApplied = this.queryCommandValue(command);
|
|
if(isApplied){
|
|
|
|
// We have to split backwards until we hit the format
|
|
var nNames = this._tagNamesForCommand(command);
|
|
range = selection.getRangeAt(0);
|
|
var fs = range.startContainer;
|
|
if(fs.nodeType === 3){
|
|
var offset = range.endOffset;
|
|
if(fs.length < offset){
|
|
//We are not looking from the right node, try to locate the correct one
|
|
ret = this._adjustNodeAndOffset(rs, offset);
|
|
fs = ret.node;
|
|
offset = ret.offset;
|
|
}
|
|
}
|
|
var topNode;
|
|
while(fs && fs !== this.editNode){
|
|
// We have to walk back and see if this is still a format or not.
|
|
// Hm, how do I do this?
|
|
var tName = fs.tagName? fs.tagName.toLowerCase() : "";
|
|
if(array.indexOf(nNames, tName) > -1){
|
|
topNode = fs;
|
|
break;
|
|
}
|
|
fs = fs.parentNode;
|
|
}
|
|
|
|
// Okay, we have a stopping place, time to split things apart.
|
|
if(topNode){
|
|
// Okay, we know how far we have to split backwards, so we have to split now.
|
|
rs = range.startContainer;
|
|
var newblock = doc.createElement(topNode.tagName);
|
|
domConstruct.place(newblock, topNode, "after");
|
|
if(rs && rs.nodeType === 3){
|
|
// Text node, we have to split it.
|
|
var nodeToMove, tNode;
|
|
var endOffset = range.endOffset;
|
|
if(rs.length < endOffset){
|
|
//We are not splitting the right node, try to locate the correct one
|
|
ret = this._adjustNodeAndOffset(rs, endOffset);
|
|
rs = ret.node;
|
|
endOffset = ret.offset;
|
|
}
|
|
|
|
txt = rs.nodeValue;
|
|
startNode = doc.createTextNode(txt.substring(0, endOffset));
|
|
var endText = txt.substring(endOffset, txt.length);
|
|
if(endText){
|
|
endNode = doc.createTextNode(endText);
|
|
}
|
|
// Place the split, then remove original nodes.
|
|
domConstruct.place(startNode, rs, "before");
|
|
if(endNode){
|
|
breaker = doc.createElement("span");
|
|
breaker.className = "ieFormatBreakerSpan";
|
|
domConstruct.place(breaker, rs, "after");
|
|
domConstruct.place(endNode, breaker, "after");
|
|
endNode = breaker;
|
|
}
|
|
domConstruct.destroy(rs);
|
|
|
|
// Okay, we split the text. Now we need to see if we're
|
|
// parented to the block element we're splitting and if
|
|
// not, we have to split all the way up. Ugh.
|
|
var parentC = startNode.parentNode;
|
|
var tagList = [];
|
|
var tagData;
|
|
while(parentC !== topNode){
|
|
var tg = parentC.tagName;
|
|
tagData = {tagName: tg};
|
|
tagList.push(tagData);
|
|
|
|
var newTg = doc.createElement(tg);
|
|
// Clone over any 'style' data.
|
|
if(parentC.style){
|
|
if(newTg.style){
|
|
if(parentC.style.cssText){
|
|
newTg.style.cssText = parentC.style.cssText;
|
|
tagData.cssText = parentC.style.cssText;
|
|
}
|
|
}
|
|
}
|
|
// If font also need to clone over any font data.
|
|
if(parentC.tagName === "FONT"){
|
|
if(parentC.color){
|
|
newTg.color = parentC.color;
|
|
tagData.color = parentC.color;
|
|
}
|
|
if(parentC.face){
|
|
newTg.face = parentC.face;
|
|
tagData.face = parentC.face;
|
|
}
|
|
if(parentC.size){ // this check was necessary on IE
|
|
newTg.size = parentC.size;
|
|
tagData.size = parentC.size;
|
|
}
|
|
}
|
|
if(parentC.className){
|
|
newTg.className = parentC.className;
|
|
tagData.className = parentC.className;
|
|
}
|
|
|
|
// Now move end node and every sibling
|
|
// after it over into the new tag.
|
|
if(endNode){
|
|
nodeToMove = endNode;
|
|
while(nodeToMove){
|
|
tNode = nodeToMove.nextSibling;
|
|
newTg.appendChild(nodeToMove);
|
|
nodeToMove = tNode;
|
|
}
|
|
}
|
|
if(newTg.tagName == parentC.tagName){
|
|
breaker = doc.createElement("span");
|
|
breaker.className = "ieFormatBreakerSpan";
|
|
domConstruct.place(breaker, parentC, "after");
|
|
domConstruct.place(newTg, breaker, "after");
|
|
}else{
|
|
domConstruct.place(newTg, parentC, "after");
|
|
}
|
|
startNode = parentC;
|
|
endNode = newTg;
|
|
parentC = parentC.parentNode;
|
|
}
|
|
|
|
// Lastly, move the split out all the split tags
|
|
// to the new block as they should now be split properly.
|
|
if(endNode){
|
|
nodeToMove = endNode;
|
|
if(nodeToMove.nodeType === 1 || (nodeToMove.nodeType === 3 && nodeToMove.nodeValue)){
|
|
// Non-blank text and non-text nodes need to clear out that blank space
|
|
// before moving the contents.
|
|
newblock.innerHTML = "";
|
|
}
|
|
while(nodeToMove){
|
|
tNode = nodeToMove.nextSibling;
|
|
newblock.appendChild(nodeToMove);
|
|
nodeToMove = tNode;
|
|
}
|
|
}
|
|
|
|
// We had intermediate tags, we have to now recreate them inbetween the split
|
|
// and restore what styles, classnames, etc, we can.
|
|
var newrange;
|
|
if(tagList.length){
|
|
tagData = tagList.pop();
|
|
var newContTag = doc.createElement(tagData.tagName);
|
|
if(tagData.cssText && newContTag.style){
|
|
newContTag.style.cssText = tagData.cssText;
|
|
}
|
|
if(tagData.className){
|
|
newContTag.className = tagData.className;
|
|
}
|
|
if(tagData.tagName === "FONT"){
|
|
if(tagData.color){
|
|
newContTag.color = tagData.color;
|
|
}
|
|
if(tagData.face){
|
|
newContTag.face = tagData.face;
|
|
}
|
|
if(tagData.size){
|
|
newContTag.size = tagData.size;
|
|
}
|
|
}
|
|
domConstruct.place(newContTag, newblock, "before");
|
|
while(tagList.length){
|
|
tagData = tagList.pop();
|
|
var newTgNode = doc.createElement(tagData.tagName);
|
|
if(tagData.cssText && newTgNode.style){
|
|
newTgNode.style.cssText = tagData.cssText;
|
|
}
|
|
if(tagData.className){
|
|
newTgNode.className = tagData.className;
|
|
}
|
|
if(tagData.tagName === "FONT"){
|
|
if(tagData.color){
|
|
newTgNode.color = tagData.color;
|
|
}
|
|
if(tagData.face){
|
|
newTgNode.face = tagData.face;
|
|
}
|
|
if(tagData.size){
|
|
newTgNode.size = tagData.size;
|
|
}
|
|
}
|
|
newContTag.appendChild(newTgNode);
|
|
newContTag = newTgNode;
|
|
}
|
|
|
|
// Okay, everything is theoretically split apart and removed from the content
|
|
// so insert the dummy text to select, select it, then
|
|
// clear to position cursor.
|
|
sNode = doc.createTextNode(".");
|
|
breaker.appendChild(sNode);
|
|
newContTag.appendChild(sNode);
|
|
newrange = rangeapi.create(this.window);
|
|
newrange.setStart(sNode, 0);
|
|
newrange.setEnd(sNode, sNode.length);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newrange);
|
|
this._sCall("collapse", [false]);
|
|
sNode.parentNode.innerHTML = "";
|
|
}else{
|
|
// No extra tags, so we have to insert a breaker point and rely
|
|
// on filters to remove it later.
|
|
breaker = doc.createElement("span");
|
|
breaker.className="ieFormatBreakerSpan";
|
|
sNode = doc.createTextNode(".");
|
|
breaker.appendChild(sNode);
|
|
domConstruct.place(breaker, newblock, "before");
|
|
newrange = rangeapi.create(this.window);
|
|
newrange.setStart(sNode, 0);
|
|
newrange.setEnd(sNode, sNode.length);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newrange);
|
|
this._sCall("collapse", [false]);
|
|
sNode.parentNode.innerHTML = "";
|
|
}
|
|
if(!newblock.firstChild){
|
|
// Empty, we don't need it. Split was at end or similar
|
|
// So, remove it.
|
|
domConstruct.destroy(newblock);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}else{
|
|
range = selection.getRangeAt(0);
|
|
rs = range.startContainer;
|
|
if(rs && rs.nodeType === 3){
|
|
// Text node, we have to split it.
|
|
var offset = range.startOffset;
|
|
if(rs.length < offset){
|
|
//We are not splitting the right node, try to locate the correct one
|
|
ret = this._adjustNodeAndOffset(rs, offset);
|
|
rs = ret.node;
|
|
offset = ret.offset;
|
|
}
|
|
txt = rs.nodeValue;
|
|
startNode = doc.createTextNode(txt.substring(0, offset));
|
|
var endText = txt.substring(offset);
|
|
if(endText !== ""){
|
|
endNode = doc.createTextNode(txt.substring(offset));
|
|
}
|
|
// Create a space, we'll select and bold it, so
|
|
// the whole word doesn't get bolded
|
|
breaker = doc.createElement("span");
|
|
sNode = doc.createTextNode(".");
|
|
breaker.appendChild(sNode);
|
|
if(startNode.length){
|
|
domConstruct.place(startNode, rs, "after");
|
|
}else{
|
|
startNode = rs;
|
|
}
|
|
domConstruct.place(breaker, startNode, "after");
|
|
if(endNode){
|
|
domConstruct.place(endNode, breaker, "after");
|
|
}
|
|
domConstruct.destroy(rs);
|
|
var newrange = rangeapi.create(this.window);
|
|
newrange.setStart(sNode, 0);
|
|
newrange.setEnd(sNode, sNode.length);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newrange);
|
|
doc.execCommand(command);
|
|
domConstruct.place(breaker.firstChild, breaker, "before");
|
|
domConstruct.destroy(breaker);
|
|
newrange.setStart(sNode, 0);
|
|
newrange.setEnd(sNode, sNode.length);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newrange);
|
|
this._sCall("collapse", [false]);
|
|
sNode.parentNode.innerHTML = "";
|
|
return true;
|
|
}
|
|
}
|
|
}else{
|
|
return false;
|
|
}
|
|
},
|
|
|
|
_adaptIEList: function(command /*===== , argument =====*/){
|
|
// summary:
|
|
// This function handles normalizing the IE list behavior as
|
|
// much as possible.
|
|
// command:
|
|
// The list command to execute.
|
|
// argument:
|
|
// Any additional argument.
|
|
// tags:
|
|
// private
|
|
var selection = rangeapi.getSelection(this.window);
|
|
if(selection.isCollapsed){
|
|
// In the case of no selection, lets commonize the behavior and
|
|
// make sure that it indents if needed.
|
|
if(selection.rangeCount && !this.queryCommandValue(command)){
|
|
var range = selection.getRangeAt(0);
|
|
var sc = range.startContainer;
|
|
if(sc && sc.nodeType == 3){
|
|
// text node. Lets see if there is a node before it that isn't
|
|
// some sort of breaker.
|
|
if(!range.startOffset){
|
|
// We're at the beginning of a text area. It may have been br split
|
|
// Who knows? In any event, we must create the list manually
|
|
// or IE may shove too much into the list element. It seems to
|
|
// grab content before the text node too if it's br split.
|
|
// Why can't IE work like everyone else?
|
|
|
|
// Create a space, we'll select and bold it, so
|
|
// the whole word doesn't get bolded
|
|
var lType = "ul";
|
|
if(command === "insertorderedlist"){
|
|
lType = "ol";
|
|
}
|
|
var list = this.document.createElement(lType);
|
|
var li = domConstruct.create("li", null, list);
|
|
domConstruct.place(list, sc, "before");
|
|
// Move in the text node as part of the li.
|
|
li.appendChild(sc);
|
|
// We need a br after it or the enter key handler
|
|
// sometimes throws errors.
|
|
domConstruct.create("br", null, list, "after");
|
|
// Okay, now lets move our cursor to the beginning.
|
|
var newrange = rangeapi.create(this.window);
|
|
newrange.setStart(sc, 0);
|
|
newrange.setEnd(sc, sc.length);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newrange);
|
|
this._sCall("collapse", [true]);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
_handleTextColorOrProperties: function(command, argument){
|
|
// summary:
|
|
// This function handles appplying text color as best it is
|
|
// able to do so when the selection is collapsed, making the
|
|
// behavior cross-browser consistent. It also handles the name
|
|
// and size for IE.
|
|
// command:
|
|
// The command.
|
|
// argument:
|
|
// Any additional arguments.
|
|
// tags:
|
|
// private
|
|
var selection = rangeapi.getSelection(this.window);
|
|
var doc = this.document;
|
|
var rs, ret, range, txt, startNode, endNode, breaker, sNode;
|
|
argument = argument || null;
|
|
if(command && selection && selection.isCollapsed){
|
|
if(selection.rangeCount){
|
|
range = selection.getRangeAt(0);
|
|
rs = range.startContainer;
|
|
if(rs && rs.nodeType === 3){
|
|
// Text node, we have to split it.
|
|
var offset = range.startOffset;
|
|
if(rs.length < offset){
|
|
//We are not splitting the right node, try to locate the correct one
|
|
ret = this._adjustNodeAndOffset(rs, offset);
|
|
rs = ret.node;
|
|
offset = ret.offset;
|
|
}
|
|
txt = rs.nodeValue;
|
|
startNode = doc.createTextNode(txt.substring(0, offset));
|
|
var endText = txt.substring(offset);
|
|
if(endText !== ""){
|
|
endNode = doc.createTextNode(txt.substring(offset));
|
|
}
|
|
// Create a space, we'll select and bold it, so
|
|
// the whole word doesn't get bolded
|
|
breaker = doc.createElement("span");
|
|
sNode = doc.createTextNode(".");
|
|
breaker.appendChild(sNode);
|
|
// Create a junk node to avoid it trying to style the breaker.
|
|
// This will get destroyed later.
|
|
var extraSpan = doc.createElement("span");
|
|
breaker.appendChild(extraSpan);
|
|
if(startNode.length){
|
|
domConstruct.place(startNode, rs, "after");
|
|
}else{
|
|
startNode = rs;
|
|
}
|
|
domConstruct.place(breaker, startNode, "after");
|
|
if(endNode){
|
|
domConstruct.place(endNode, breaker, "after");
|
|
}
|
|
domConstruct.destroy(rs);
|
|
var newrange = rangeapi.create(this.window);
|
|
newrange.setStart(sNode, 0);
|
|
newrange.setEnd(sNode, sNode.length);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newrange);
|
|
if(has("webkit")){
|
|
// WebKit is frustrating with positioning the cursor.
|
|
// It stinks to have a selected space, but there really
|
|
// isn't much choice here.
|
|
var style = "color";
|
|
if(command === "hilitecolor" || command === "backcolor"){
|
|
style = "backgroundColor";
|
|
}
|
|
domStyle.set(breaker, style, argument);
|
|
this._sCall("remove", []);
|
|
domConstruct.destroy(extraSpan);
|
|
breaker.innerHTML = " "; //
|
|
this._sCall("selectElement", [breaker]);
|
|
this.focus();
|
|
}else{
|
|
this.execCommand(command, argument);
|
|
domConstruct.place(breaker.firstChild, breaker, "before");
|
|
domConstruct.destroy(breaker);
|
|
newrange.setStart(sNode, 0);
|
|
newrange.setEnd(sNode, sNode.length);
|
|
selection.removeAllRanges();
|
|
selection.addRange(newrange);
|
|
this._sCall("collapse", [false]);
|
|
sNode.parentNode.removeChild(sNode);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
_adjustNodeAndOffset: function(/*DomNode*/node, /*Int*/offset){
|
|
// summary:
|
|
// In the case there are multiple text nodes in a row the offset may not be within the node.
|
|
// If the offset is larger than the node length, it will attempt to find
|
|
// the next text sibling until it locates the text node in which the offset refers to
|
|
// node:
|
|
// The node to check.
|
|
// offset:
|
|
// The position to find within the text node
|
|
// tags:
|
|
// private.
|
|
while(node.length < offset && node.nextSibling && node.nextSibling.nodeType === 3){
|
|
//Adjust the offset and node in the case of multiple text nodes in a row
|
|
offset = offset - node.length;
|
|
node = node.nextSibling;
|
|
}
|
|
return {"node": node, "offset": offset};
|
|
},
|
|
|
|
_tagNamesForCommand: function(command){
|
|
// summary:
|
|
// Function to return the tab names that are associated
|
|
// with a particular style.
|
|
// command: String
|
|
// The command to return tags for.
|
|
// tags:
|
|
// private
|
|
if(command === "bold"){
|
|
return ["b", "strong"];
|
|
}else if(command === "italic"){
|
|
return ["i","em"];
|
|
}else if(command === "strikethrough"){
|
|
return ["s", "strike"];
|
|
}else if(command === "superscript"){
|
|
return ["sup"];
|
|
}else if(command === "subscript"){
|
|
return ["sub"];
|
|
}else if(command === "underline"){
|
|
return ["u"];
|
|
}
|
|
return [];
|
|
},
|
|
|
|
_stripBreakerNodes: function(/*DOMNode*/ node){
|
|
// summary:
|
|
// Function for stripping out the breaker spans inserted by the formatting command.
|
|
// Registered as a filter for IE, handles the breaker spans needed to fix up
|
|
// How bold/italic/etc, work when selection is collapsed (single cursor).
|
|
if(!this.isLoaded){ return; } // this method requires init to be complete
|
|
query(".ieFormatBreakerSpan", node).forEach(function(b){
|
|
while(b.firstChild){
|
|
domConstruct.place(b.firstChild, b, "before");
|
|
}
|
|
domConstruct.destroy(b);
|
|
});
|
|
return node;
|
|
}
|
|
});
|
|
|
|
return RichText;
|
|
|
|
});
|