dijit
Version:
Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible u
1,452 lines (1,277 loc) • 99.8 kB
JavaScript
define([
"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/kernel", // kernel.deprecated, kernel.locale
"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/domReady",
"dojo/sniff", // has("ie") has("mozilla") has("opera") has("safari") has("webkit")
"dojo/string",
"dojo/topic", // topic.publish() (publish)
"dojo/_base/unload", // unload
"dojo/_base/url", // url
"dojo/window", // winUtils.get()
"../_Widget",
"../_CssStateMixin",
"../selection",
"./range",
"./html",
"../focus",
"../main" // dijit._scopeName
], function(array, config, declare, Deferred, dom, domAttr, domClass, domConstruct, domGeometry, domStyle,
kernel, keys, lang, on, query, domReady, has, string, topic, unload, _Url, winUtils,
_Widget, _CssStateMixin, selectionapi, rangeapi, htmlapi, focus, dijit){
// module:
// dijit/_editor/RichText
// 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/promise/Promise
// 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.trim, // avoid IE10 problem hitting ENTER on last line when there's a trailing \n.
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") || has("trident")){
// IE generates <strong> and <em> but we want to normalize to <b> and <i>
// Still happens in IE11, but doesn't happen with Edge.
this.contentPostFilters = [this._normalizeFontStyle].concat(this.contentPostFilters);
this.contentDomPostFilters = [lang.hitch(this, "_stripBreakerNodes")].concat(this.contentDomPostFilters);
}
this.contentDomPostFilters = [lang.hitch(this, "_stripTrailingEmptyNodes")].concat(this.contentDomPostFilters);
this.inherited(arguments);
topic.publish(dijit._scopeName + "._editor.RichText::init", this);
},
startup: function(){
this.inherited(arguments);
// Don't call open() until startup() because we need to be attached to the DOM, and also if we are the
// child of a StackContainer, let StackContainer._setupChild() do DOM manipulations before iframe is
// created, to avoid duplicate onload call.
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: ["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.selection.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;
// Compute initial value of 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;
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){
this.value = 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 method is called when the editor is first loaded and also if the Editor's
// dom node is repositioned. Unfortunately repositioning the Editor tends to
// clear the iframe's contents, so we can't just no-op in that case.
this.window = w;
this.document = w.document;
// instantiate class to access selected text in editor's iframe
this.selection = new selectionapi.SelectionManager(w);
if(has("ie")){
this._localizeEditorCommands();
}
// Do final setup and set contents of editor.
// Use get("value") rather than html in case _loadFunc() is being called for a second time
// because editor's DOMNode was repositioned.
this.onLoad(this.get("value"));
});
// Attach iframe to document, and set the initial (blank) content.
var src = this._getIframeDocTxt().replace(/\\/g, "\\\\").replace(/'/g, "\\'"),
s;
// IE10 and earlier will throw an "Access is denied" error when attempting to access the parent frame if
// document.domain has been set, unless the child frame also has the same document.domain set. In some
// cases, we can only set document.domain while the document is being constructed using open/write/close;
// attempting to set it later results in a different "This method can't be used in this context" error.
// However, in at least IE9-10, sometimes the parent.window check will succeed and the access failure will
// only happen later when trying to access frameElement, so there is an additional check and fix there
// as well. See #17529
if (has("ie") < 11) {
s = 'javascript:document.open();try{parent.window;}catch(e){document.domain="' + document.domain + '";}' +
'document.write(\'' + src + '\');document.close()';
}
else {
s = "javascript: '" + src + "'";
}
// Attach to document before setting the content, to avoid problem w/iframe running in
// wrong security context (IE9 and IE11), see #16633.
this.editingArea.appendChild(ifr);
ifr.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);
// Find any associated label element, aria-label, or aria-labelledby and get unescaped text.
var title;
if(this["aria-label"]){
title = this["aria-label"];
}else{
var labelNode = query('label[for="' + this.id + '"]', this.ownerDocument)[0] ||
dom.byId(this["aria-labelledby"], this.ownerDocument);
if(labelNode){
title = labelNode.textContent || labelNode.innerHTML || "";
}
}
// The contents inside of <body>. The real contents are set later via a call to setValue().
// In auto-expand mode, need a wrapper div for AlwaysShowToolbar plugin to correctly
// expand/contract the editor as the content changes.
var html = "<div id='dijitEditorBody' role='textbox' aria-multiline='true' " +
(title ? " aria-label='" + string.escape(title) + "'" : "") + "></div>";
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 + ';';
});
// Now that we have the title, also set it as the title attribute on the iframe
this.iframe.setAttribute("title", title);
// if this.lang is unset then use default value, to avoid invalid setting of lang=""
var language = this.lang || kernel.locale.replace(/-.*/, "");
return [
"<!DOCTYPE html>",
"<html lang='" + language + "'" + (this.isLeftToRight() ? "" : " dir='rtl'") + ">\n",
"<head>\n",
"<meta http-equiv='Content-Type' content='text/html'>\n",
title ? "<title>" + string.escape(title) + "</title>" : "",
"<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
"\t}\n",
"\tbody,html,#dijitEditorBody { outline: none; }",
// Set <body> to expand to full size of editor, so clicking anywhere will work.
// Except in auto-expand mode, in which case the editor expands to the size of <body>.
// Also 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.
// Scrollers go on <body> since it's been set to height: 100%.
"html { height: 100%; width: 100%; overflow: hidden; }\n", // scroll bar is on #dijitEditorBody, shouldn't be on <html>
this.height ? "\tbody,#dijitEditorBody { height: 100%; width: 100%; overflow: auto; }\n" :
"\tbody,#dijitEditorBody { min-height: " + this.minHeight + "; width: 100%; overflow-x: auto; overflow-y: hidden; }\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"),
"\t\tline-height:", lineHeight, ";\n",
"\t}\n",
"\tp{ margin: 1em 0; }\n",
"\tli > ul:-moz-first-node, li > ol:-moz-first-node{ padding-top: 1.2em; }\n",
// Can't set min-height in IE>=9, it puts layout on li, which puts move/resize handles.
// Also can't set it on Edge, as it leads to strange behavior where hitting the return key
// doesn't start a new list item.
(has("ie") || has("trident") || has("edge") ? "" : "\tli{ min-height:1.2em; }\n"),
"</style>\n",
this._applyEditingAreaStyleSheets(), "\n",
"</head>\n<body role='application'",
title ? " aria-label='" + string.escape(title) + "'" : "",
// Onload handler fills in real editor content.
// On IE9, sometimes onload is called twice, and the first time frameElement is null (test_FullScreen.html)
// On IE9-10, it is also possible that accessing window.parent in the initial creation of the
// iframe DOM will succeed, but trying to access window.frameElement will fail, in which case we
// *can* set the domain without a "This method can't be used in this context" error. See #17529
"onload='try{frameElement && frameElement._loadFunc(window,document)}catch(e){document.domain=\"" + document.domain + "\";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, ownerWindow = winUtils.get(this.ownerDocument);
while((url = files[i++])){
var abstring = (new _Url(ownerWindow.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(), ownerWindow = winUtils.get(this.ownerDocument);
//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(ownerWindow.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(), ownerWindow = winUtils.get(this.ownerDocument);
//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(ownerWindow.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
var preventIEfocus = has("ie") && (this.isLoaded || !this.focusOnLoad);
if(preventIEfocus){
this.editNode.unselectable = "on";
}
this.editNode.contentEditable = !value;
this.editNode.tabIndex = value ? "-1" : this.tabIndex;
if(preventIEfocus){
this.defer(function(){
if(this.editNode){ // guard in case widget destroyed before timeout
this.editNode.unselectable = "off";
}
});
}
if(has("mozilla") && !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._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
if(!this.window.__registeredWindow){
this.window.__registeredWindow = true;
this._iframeRegHandle = focus.registerIframe(this.iframe);
}
// there's a wrapper div around the content, see _getIframeDocTxt().
this.editNode = this.document.body.firstChild;
var _this = this;
// Helper code so IE and FF skip over focusing on the <iframe> and just focus on the inner <div>.
// See #4996 IE wants to focus the BODY tag.
this.beforeIframeNode = domConstruct.place("<div tabIndex=-1></div>", this.iframe, "before");
this.afterIframeNode = domConstruct.place("<div tabIndex=-1></div>", this.iframe, "after");
this.iframe.onfocus = this.document.onfocus = function(){
_this.editNode.focus();
};
this.focusNode = this.editNode; // for InlineEditBox
var events = this.events.concat(this.captureEvents);
var ap = this.iframe ? this.document : this.editNode;
this.own.apply(this,
array.map(events, function(item){
var type = item.toLowerCase().replace(/^on/, "");
return on(ap, type, lang.hitch(this, item));
}, this)
);
this.own(
// mouseup in the margin does not generate an onclick event
on(ap, "mouseup", lang.hitch(this, "onClick"))
);
if(has("ie")){ // IE contentEditable
this.own(on(this.document, "mousedown", lang.hitch(this, "_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;
}
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.own(on(this.document, "mouseup", lang.hitch(this, "onDisplayChanged")))[0];
this.own(on(this.document, "mousedown", lang.hitch(this, 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);
// Tell app that the Editor has finished loading. isFulfilled() check avoids spurious
// console warning when this function is called repeatedly because Editor DOMNode was moved.
if(this.onLoadDeferred && !this.onLoadDeferred.isFulfilled()){
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
domReady(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 keydown event
// tags:
// protected
// Modifier keys should not cause the onKeyPressed event because they do not cause any change to the
// display
if(e.keyCode === keys.SHIFT ||
e.keyCode === keys.ALT ||
e.keyCode === keys.META ||
e.keyCode === keys.CTRL){
return true;
}
if(e.keyCode === keys.TAB && this.isTabIndent){
//prevent tab from moving focus out of editor
e.stopPropagation();
e.preventDefault();
// 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"));
}
}
// Make tab and shift-tab skip over the <iframe>, going from the nested <div> to the toolbar
// or next element after the editor
if(e.keyCode == keys.TAB && !this.isTabIndent && !e.ctrlKey && !e.altKey){
if(e.shiftKey){
// focus the <iframe> so the browser will shift-tab away from it instead
this.beforeIframeNode.focus();
}else{
// focus node after the <iframe> so the browser will tab away from it instead
this.afterIframeNode.focus();
}
// Prevent onKeyPressed from firing in order to avoid triggering a display change event when the
// editor is tabbed away; this fixes toolbar controls being inappropriately disabled in IE9+
return true;
}
if(has("ie") < 9 && 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
e.stopPropagation();
e.preventDefault();
this.execCommand("delete");
}
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();
}
}
}
var handlers = this._keyHandlers[e.keyCode],
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
this.defer("onKeyPressed", 1);
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);
},
addKeyHandler: function(/*String|Number*/ key, /*Boolean*/ ctrl, /*Boolean*/ shift, /*Function*/ handler){
// summary:
// Add a handler for a keyboard shortcut
// tags:
// protected
if(typeof key == "string"){
// Something like Ctrl-B. Since using keydown event, we need to convert string to a number.
key = key.toUpperCase().charCodeAt(0);
}
if(!lang.isArray(this._keyHandlers[key])){
this._keyHandlers[key] = [];
}
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
// Workaround IE problem when you blur the browser windows while an editor is focused: IE hangs
// when you focus editor #1, blur the browser window, and then click editor #0. See #16939.
// Note: Edge doesn't seem to have this problem.
if(has("ie") || has("trident")){
this.defer(function(){
if(!focus.curNode){
this.ownerDocumentBody.focus();
}
});
}
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(has("ie") < 9){
//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/fireEvent only in IE < 11
}else{
// Firefox and chrome
this.editNode.focus();
}
},
// _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;
},
_implCommand: function(/*String*/ cmd){
// summary:
// Used as the function name where we might
// find an override for advice on support
// for this command by the target browser.
// tags:
// private
return "_" + this._normalizeCommand(cmd) + "EnabledImpl";
},
_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
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":
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":
case "cut":
case "copy":
case "paste":
return true;
// Note: This code path is apparently never called. Not sure if it should return true or false
// for Edge.
case "blockdirltr":
case "blockdirrtl":
case "dirltr":
case "dirrtl":
case "inlinedirltr":
case "inlinedirrtl":
return has("ie") || has("trident") || has("edge");
// Note: This code path is apparently never called, not even by the dojox/editor table plugins.
// There's also an _inserttableEnabledImpl() method that's also never called.
// Previously this code returned truthy for IE and mozilla, but false for chrome/safari, so
// leaving it that way just in case.
case "inserttable":
case "insertcell":
case "insertcol":
case "insertrow":
case "deletecells":
case "deletecols":
case "deleterows":
case "mergecells":
case "splitcell":
return !has("webkit");
default:
return false;
}
},
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
if(this.focused){
// put focus back in the iframe, unless focus has somehow been shifted out of the editor completely
this.focus();
}
command = this._normalizeCommand(command, argument);
if(argument !== undefined){
if(command === "heading"){
throw new Error("unimplemented");
}else if(command === "formatblock" && (has("ie") || has("trident"))){
// See http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie.
// Not necessary on Edge though.
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 = this._implCommand(command);
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"){
// This is to deal with IE bug when running in non-English. See _localizeEditorCommands().
// Apparently not needed on IE11 or Edge.
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:
// Deprecated, remove for 2.0. New code should access this.selection directly.
// Run the named method of dijit/selection over the
// current editor instance's window, with the passed args.
// tags:
// private deprecated
return this.selection[name].apply(this.selection, 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 i