- Author:
- David Nickerson <david.nickerson@gmail.com>
- Date:
- 2021-09-17 15:50:49+12:00
- Desc:
- tweak html formatting
- Permanent Source URI:
- https://models.fieldml.org/workspace/a1/rawfile/1b3862589abf79ae9119ee0b5e99a8b785d762e1/dojo-presentation/js/dojo/dijit/Editor.js
dojo.provide("dijit.Editor");
dojo.require("dijit._editor.RichText");
dojo.require("dijit.Toolbar");
dojo.require("dijit._editor._Plugin");
dojo.require("dijit._editor.range");
dojo.require("dijit._Container");
dojo.require("dojo.i18n");
dojo.requireLocalization("dijit._editor", "commands");
dojo.declare(
"dijit.Editor",
dijit._editor.RichText,
{
// summary:
// A rich text Editing widget
//
// description:
// This widget provides basic WYSIWYG editing features, based on the browser's
// underlying rich text editing capability, accompanied by a toolbar (dijit.Toolbar).
// A plugin model is available to extend the editor's capabilities as well as the
// the options available in the toolbar. Content generation may vary across
// browsers, and clipboard operations may have different results, to name
// a few limitations. Note: this widget should not be used with the HTML
// <TEXTAREA> tag -- see dijit._editor.RichText for details.
// plugins: Array
// a list of plugin names (as strings) or instances (as objects)
// for this widget.
plugins: null,
// extraPlugins: Array
// a list of extra plugin names which will be appended to plugins array
extraPlugins: null,
constructor: function(){
if(!dojo.isArray(this.plugins)){
this.plugins=["undo","redo","|","cut","copy","paste","|","bold","italic","underline","strikethrough","|",
"insertOrderedList","insertUnorderedList","indent","outdent","|","justifyLeft","justifyRight","justifyCenter","justifyFull"/*"createLink"*/];
}
this._plugins=[];
this._editInterval = this.editActionInterval * 1000;
//IE will always lose focus when other element gets focus, while for FF and safari,
//when no iframe is used, focus will be lost whenever another element gets focus.
//For IE, we can connect to onBeforeDeactivate, which will be called right before
//the focus is lost, so we can obtain the selected range. For other browsers,
//no equivelent of onBeforeDeactivate, so we need to do two things to make sure
//selection is properly saved before focus is lost: 1) when user clicks another
//element in the page, in which case we listen to mousedown on the entire page and
//see whether user clicks out of a focus editor, if so, save selection (focus will
//only lost after onmousedown event is fired, so we can obtain correct caret pos.)
//2) when user tabs away from the editor, which is handled in onKeyDown below.
if(dojo.isIE){
this.events.push("onBeforeDeactivate");
}else if(!this.useIframe){
if(!document.__connectedglobalMouseDown){
document.__connectedglobalMouseDown=1;
//check whether user is clicking something outside of the currently focused
//editor instance
var onGlobalMouseDown = function(e){
var newfocused=e.target;
//only FF3 and IE supports document.activeElement, however, dijit
//maintains something similar for us for all browsers, so use it
var focused = dijit._curFocus;
if(focused && focused.nodeName !='BODY' && newfocused!=focused && focused.contentEditable){
if(!dojo.isDescendant(newfocused,focused)){
//console.log('newfocused.type',focused,newfocused.contentEditable);
var editor = dijit.getEnclosingWidget(focused);
if(editor && editor instanceof dijit.Editor){
editor._saveSelection();
}
}
}
//console.log(e.type,e,document.activeElement,dijit._curFocus);
}
//need to use capture=true otherwise some dijit may stopEvent which would
//prevent onGlobalMouseDown from called properly.
document.body.addEventListener('mousedown',onGlobalMouseDown,true);
}
}
},
postCreate: function(){
//for custom undo/redo
if(this.customUndo){
dojo['require']("dijit._editor.range");
this._steps=this._steps.slice(0);
this._undoedSteps=this._undoedSteps.slice(0);
// this.addKeyHandler('z',this.KEY_CTRL,this.undo);
// this.addKeyHandler('y',this.KEY_CTRL,this.redo);
}
if(dojo.isArray(this.extraPlugins)){
this.plugins=this.plugins.concat(this.extraPlugins);
}
// try{
this.inherited(arguments);
// dijit.Editor.superclass.postCreate.apply(this, arguments);
this.commands = dojo.i18n.getLocalization("dijit._editor", "commands", this.lang);
if(!this.toolbar){
// if we haven't been assigned a toolbar, create one
this.toolbar = new dijit.Toolbar({});
dojo.place(this.toolbar.domNode, this.editingArea, "before");
}
dojo.forEach(this.plugins, this.addPlugin, this);
this.onNormalizedDisplayChanged(); //update toolbar button status
// }catch(e){ console.debug(e); }
this.toolbar.startup();
},
destroy: function(){
dojo.forEach(this._plugins, function(p){
if(p && p.destroy){
p.destroy();
}
});
this._plugins=[];
this.toolbar.destroy(); delete this.toolbar;
this.inherited(arguments);
},
addPlugin: function(/*String||Object*/plugin, /*Integer?*/index){
// summary:
// takes a plugin name as a string or a plugin instance and
// adds it to the toolbar and associates it with this editor
// instance. The resulting plugin is added to the Editor's
// plugins array. If index is passed, it's placed in the plugins
// array at that index. No big magic, but a nice helper for
// passing in plugin names via markup.
//
// plugin: String, args object or plugin instance
//
// args: This object will be passed to the plugin constructor
//
// index: Integer
// Used when creating an instance from
// something already in this.plugins. Ensures that the new
// instance is assigned to this.plugins at that index.
var args=dojo.isString(plugin)?{name:plugin}:plugin;
if(!args.setEditor){
var o={"args":args,"plugin":null,"editor":this};
dojo.publish(dijit._scopeName + ".Editor.getPlugin",[o]);
if(!o.plugin){
var pc = dojo.getObject(args.name);
if(pc){
o.plugin=new pc(args);
}
}
if(!o.plugin){
console.warn('Cannot find plugin',plugin);
return;
}
plugin=o.plugin;
}
if(arguments.length > 1){
this._plugins[index] = plugin;
}else{
this._plugins.push(plugin);
}
plugin.setEditor(this);
if(dojo.isFunction(plugin.setToolbar)){
plugin.setToolbar(this.toolbar);
}
},
//the following 3 functions are required to make the editor play nice under a layout widget, see #4070
startup: function(){
//console.log('startup',arguments);
},
resize: function(){
dijit.layout._LayoutWidget.prototype.resize.apply(this,arguments);
},
layout: function(){
this.editingArea.style.height=(this._contentBox.h - dojo.marginBox(this.toolbar.domNode).h)+"px";
if(this.iframe){
this.iframe.style.height="100%";
}
},
onBeforeDeactivate: function(e){
if(this.customUndo){
this.endEditing(true);
}
//in IE, the selection will be lost when other elements get focus,
//let's save focus before the editor is deactivated
this._saveSelection();
//console.log('onBeforeDeactivate',this);
},
/* beginning of custom undo/redo support */
// customUndo: Boolean
// Whether we shall use custom undo/redo support instead of the native
// browser support. By default, we only enable customUndo for IE, as it
// has broken native undo/redo support. Note: the implementation does
// support other browsers which have W3C DOM2 Range API implemented.
customUndo: dojo.isIE,
// editActionInterval: Integer
// When using customUndo, not every keystroke will be saved as a step.
// Instead typing (including delete) will be grouped together: after
// a user stop typing for editActionInterval seconds, a step will be
// saved; if a user resume typing within editActionInterval seconds,
// the timeout will be restarted. By default, editActionInterval is 3
// seconds.
editActionInterval: 3,
beginEditing: function(cmd){
if(!this._inEditing){
this._inEditing=true;
this._beginEditing(cmd);
}
if(this.editActionInterval>0){
if(this._editTimer){
clearTimeout(this._editTimer);
}
this._editTimer = setTimeout(dojo.hitch(this, this.endEditing), this._editInterval);
}
},
_steps:[],
_undoedSteps:[],
execCommand: function(cmd){
if(this.customUndo && (cmd=='undo' || cmd=='redo')){
return this[cmd]();
}else{
if(this.customUndo){
this.endEditing();
this._beginEditing();
}
try{
var r = this.inherited('execCommand',arguments);
if(dojo.isSafari && cmd=='paste' && !r){ //see #4598: safari does not support invoking paste from js
var su = dojo.string.substitute, _isM = navigator.userAgent.indexOf("Macintosh") != -1;
alert(su(this.commands.pasteShortcutSafari,[su(this.commands[_isM ? 'appleKey' : 'ctrlKey'], ['V'])]));
}
}catch(e){
if(dojo.isMoz && /copy|cut|paste/.test(cmd)){
// Warn user of platform limitation. Cannot programmatically access keyboard. See ticket #4136
var sub = dojo.string.substitute,
accel = {cut:'X', copy:'C', paste:'V'},
isMac = navigator.userAgent.indexOf("Macintosh") != -1;
alert(sub(this.commands.systemShortcutFF,
[this.commands[cmd], sub(this.commands[isMac ? 'appleKey' : 'ctrlKey'], [accel[cmd]])]));
}
r = false;
}
if(this.customUndo){
this._endEditing();
}
return r;
}
},
queryCommandEnabled: function(cmd){
if(this.customUndo && (cmd=='undo' || cmd=='redo')){
return cmd=='undo'?(this._steps.length>1):(this._undoedSteps.length>0);
}else{
return this.inherited('queryCommandEnabled',arguments);
}
},
focus: function(){
var restore=0;
//console.log('focus',dijit._curFocus==this.editNode)
if(this._savedSelection && (dojo.isIE || !this.useIframe)){
restore = dijit._curFocus!=this.editNode;
}
this.inherited(arguments);
if(restore){
this._restoreSelection();
}
},
_moveToBookmark: function(b){
var bookmark=b;
if(dojo.isIE){
if(dojo.isArray(b)){//IE CONTROL
bookmark=[];
dojo.forEach(b,function(n){
bookmark.push(dijit.range.getNode(n,this.editNode));
},this);
}
}else{//w3c range
var r=dijit.range.create();
r.setStart(dijit.range.getNode(b.startContainer,this.editNode),b.startOffset);
r.setEnd(dijit.range.getNode(b.endContainer,this.editNode),b.endOffset);
bookmark=r;
}
dojo.withGlobal(this.window,'moveToBookmark',dijit,[bookmark]);
},
_changeToStep: function(from,to){
this.setValue(to.text);
var b=to.bookmark;
if(!b){ return; }
this._moveToBookmark(b);
},
undo: function(){
// console.log('undo');
this.endEditing(true);
var s=this._steps.pop();
if(this._steps.length>0){
this.focus();
this._changeToStep(s,this._steps[this._steps.length-1]);
this._undoedSteps.push(s);
this.onDisplayChanged();
return true;
}
return false;
},
redo: function(){
// console.log('redo');
this.endEditing(true);
var s=this._undoedSteps.pop();
if(s && this._steps.length>0){
this.focus();
this._changeToStep(this._steps[this._steps.length-1],s);
this._steps.push(s);
this.onDisplayChanged();
return true;
}
return false;
},
endEditing: function(ignore_caret){
if(this._editTimer){
clearTimeout(this._editTimer);
}
if(this._inEditing){
this._endEditing(ignore_caret);
this._inEditing=false;
}
},
_getBookmark: function(){
var b=dojo.withGlobal(this.window,dijit.getBookmark);
var tmp=[];
if(dojo.isIE){
if(dojo.isArray(b)){//CONTROL
dojo.forEach(b,function(n){
tmp.push(dijit.range.getIndex(n,this.editNode).o);
},this);
b=tmp;
}
}else{//w3c range
tmp=dijit.range.getIndex(b.startContainer,this.editNode).o;
b={startContainer:tmp,
startOffset:b.startOffset,
endContainer:b.endContainer===b.startContainer?tmp:dijit.range.getIndex(b.endContainer,this.editNode).o,
endOffset:b.endOffset};
}
return b;
},
_beginEditing: function(cmd){
if(this._steps.length===0){
this._steps.push({'text':this.savedContent,'bookmark':this._getBookmark()});
}
},
_endEditing: function(ignore_caret){
var v=this.getValue(true);
this._undoedSteps=[];//clear undoed steps
this._steps.push({text: v, bookmark: this._getBookmark()});
},
onKeyDown: function(e){
//We need to save selection if the user TAB away from this editor
//no need to call _saveSelection for IE, as that will be taken care of in onBeforeDeactivate
if(!dojo.isIE && !this.iframe && e.keyCode==dojo.keys.TAB){
this._saveSelection();
}
if (e.keyCode === dojo.keys.TAB && this.isTabIndent ){
dojo.stopEvent(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"));
}
}else if (dojo.isMoz && this.iframe && !this.isTabIndent){
if( e.keyCode == dojo.keys.TAB &&
!e.shiftKey &&
!e.ctrlKey &&
!e.altKey
){
// update iframe document title for screen reader
this.iframe.contentDocument.title = this._localizedIframeTitles.iframeFocusTitle;
// Place focus on the iframe. A subsequent tab or shift tab
// will put focus on the correct control.
this.iframe.focus(); // this.focus(); won't work
dojo.stopEvent(e);
}else if(e.keyCode == dojo.keys.TAB && e.shiftKey){
// if there is a toolbar, set focus to it, otherwise ignore
if(this.toolbar){ // FIXME: this is badly factored!!!
this.toolbar.focus();
}
dojo.stopEvent(e);
}
}
if(!this.customUndo){
this.inherited('onKeyDown',arguments);
return;
}
var k = e.keyCode, ks = dojo.keys;
if(e.ctrlKey && !e.altKey){//undo and redo only if the special right Alt + z/y are not pressed #5892
if(k == 90 || k == 122){ //z
dojo.stopEvent(e);
this.undo();
return;
}else if(k == 89 || k == 121){ //y
dojo.stopEvent(e);
this.redo();
return;
}
}
this.inherited('onKeyDown',arguments);
switch(k){
case ks.ENTER:
case ks.BACKSPACE:
case ks.DELETE:
this.beginEditing();
break;
case 88: //x
case 86: //v
if(e.ctrlKey && !e.altKey && !e.metaKey){
this.endEditing();//end current typing step if any
if(e.keyCode == 88){
this.beginEditing('cut');
//use timeout to trigger after the cut is complete
setTimeout(dojo.hitch(this, this.endEditing), 1);
}else{
this.beginEditing('paste');
//use timeout to trigger after the paste is complete
setTimeout(dojo.hitch(this, this.endEditing), 1);
}
break;
}
//pass through
default:
if(!e.ctrlKey && !e.altKey && !e.metaKey && (e.keyCode<dojo.keys.F1 || e.keyCode>dojo.keys.F15)){
this.beginEditing();
break;
}
//pass through
case ks.ALT:
this.endEditing();
break;
case ks.UP_ARROW:
case ks.DOWN_ARROW:
case ks.LEFT_ARROW:
case ks.RIGHT_ARROW:
case ks.HOME:
case ks.END:
case ks.PAGE_UP:
case ks.PAGE_DOWN:
this.endEditing(true);
break;
//maybe ctrl+backspace/delete, so don't endEditing when ctrl is pressed
case ks.CTRL:
case ks.SHIFT:
case ks.TAB:
break;
}
},
_onBlur: function(){
//this._saveSelection();
this.inherited('_onBlur',arguments);
this.endEditing(true);
},
_saveSelection: function(){
this._savedSelection=this._getBookmark();
//console.log('save selection',this._savedSelection,this);
},
_restoreSelection: function(){
if(this._savedSelection){
//only restore the selection if the current range is collapsed
//if not collapsed, then it means the editor does not lose
//selection and there is no need to restore it
//if(dojo.withGlobal(this.window,'isCollapsed',dijit)){
//console.log('_restoreSelection true')
this._moveToBookmark(this._savedSelection);
//}
delete this._savedSelection;
}
},
_onFocus: function(){
//console.log('_onFocus');
this._restoreSelection();
this.inherited(arguments);
},
onClick: function(){
this.endEditing(true);
this.inherited('onClick',arguments);
}
/* end of custom undo/redo support */
}
);
/* the following code is to registered a handler to get default plugins */
dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){
if(o.plugin){ return; }
var args = o.args, p;
var _p = dijit._editor._Plugin;
var name = args.name;
switch(name){
case "undo": case "redo": case "cut": case "copy": case "paste": case "insertOrderedList":
case "insertUnorderedList": case "indent": case "outdent": case "justifyCenter":
case "justifyFull": case "justifyLeft": case "justifyRight": case "delete":
case "selectAll": case "removeFormat": case "unlink":
case "insertHorizontalRule":
p = new _p({ command: name });
break;
case "bold": case "italic": case "underline": case "strikethrough":
case "subscript": case "superscript":
p = new _p({ buttonClass: dijit.form.ToggleButton, command: name });
break;
case "|":
p = new _p({ button: new dijit.ToolbarSeparator() });
}
// console.log('name',name,p);
o.plugin=p;
});