root / dijit / trunk / InlineEditBox.js

Revision 21391, 17.4 kB (checked in by bill, 5 weeks ago)

Don't support auto-correction of malformed HTML, due to performance/code size reasons. In general Dijit doesn't try to compensate for developer errors like this. Refs #10653 !strict.

  • Property svn:eol-style set to native
Line 
1dojo.provide("dijit.InlineEditBox");
2
3dojo.require("dojo.i18n");
4
5dojo.require("dijit._Widget");
6dojo.require("dijit._Container");
7dojo.require("dijit.form.Button");
8dojo.require("dijit.form.TextBox");
9
10dojo.requireLocalization("dijit", "common");
11
12dojo.declare("dijit.InlineEditBox",
13        dijit._Widget,
14        {
15        // summary:
16        //              An element with in-line edit capabilitites
17        //
18        // description:
19        //              Behavior for an existing node (`<p>`, `<div>`, `<span>`, etc.) so that
20        //              when you click it, an editor shows up in place of the original
21        //              text.  Optionally, Save and Cancel button are displayed below the edit widget.
22        //              When Save is clicked, the text is pulled from the edit
23        //              widget and redisplayed and the edit widget is again hidden.
24        //              By default a plain Textarea widget is used as the editor (or for
25        //              inline values a TextBox), but you can specify an editor such as
26        //              dijit.Editor (for editing HTML) or a Slider (for adjusting a number).
27        //              An edit widget must support the following API to be used:
28        //                      - displayedValue or value as initialization parameter,
29        //                      and available through attr('displayedValue') / attr('value')
30        //                      - void focus()
31        //                      - DOM-node focusNode = node containing editable text
32
33        // editing: [readonly] Boolean
34        //              Is the node currently in edit mode?
35        editing: false,
36
37        // autoSave: Boolean
38        //              Changing the value automatically saves it; don't have to push save button
39        //              (and save button isn't even displayed)
40        autoSave: true,
41
42        // buttonSave: String
43        //              Save button label
44        buttonSave: "",
45
46        // buttonCancel: String
47        //              Cancel button label
48        buttonCancel: "",
49
50        // renderAsHtml: Boolean
51        //              Set this to true if the specified Editor's value should be interpreted as HTML
52        //              rather than plain text (ex: `dijit.Editor`)
53        renderAsHtml: false,
54
55        // editor: String
56        //              Class name for Editor widget
57        editor: "dijit.form.TextBox",
58
59        // editorWrapper: String
60        //              Class name for widget that wraps the editor widget, displaying save/cancel
61        //              buttons.
62        editorWrapper: "dijit._InlineEditor",
63
64        // editorParams: Object
65        //              Set of parameters for editor, like {required: true}
66        editorParams: {},
67
68        onChange: function(value){
69                // summary:
70                //              Set this handler to be notified of changes to value.
71                // tags:
72                //              callback
73        },
74
75        onCancel: function(){
76                // summary:
77                //              Set this handler to be notified when editing is cancelled.
78                // tags:
79                //              callback
80        },
81
82        // width: String
83        //              Width of editor.  By default it's width=100% (ie, block mode).
84        width: "100%",
85
86        // value: String
87        //              The display value of the widget in read-only mode
88        value: "",
89
90        // noValueIndicator: [const] String
91        //              The text that gets displayed when there is no value (so that the user has a place to click to edit)
92        noValueIndicator: "<span style='font-family: wingdings; text-decoration: underline;'>&nbsp;&nbsp;&nbsp;&nbsp;&#x270d;&nbsp;&nbsp;&nbsp;&nbsp;</span>",
93
94        constructor: function(){
95                // summary:
96                //              Sets up private arrays etc.
97                // tags:
98                //              private
99                this.editorParams = {};
100        },
101
102        postMixInProperties: function(){
103                this.inherited(arguments);
104
105                // save pointer to original source node, since Widget nulls-out srcNodeRef
106                this.displayNode = this.srcNodeRef;
107
108                // connect handlers to the display node
109                var events = {
110                        ondijitclick: "_onClick",
111                        onmouseover: "_onMouseOver",
112                        onmouseout: "_onMouseOut",
113                        onfocus: "_onMouseOver",
114                        onblur: "_onMouseOut"
115                };
116                for(var name in events){
117                        this.connect(this.displayNode, name, events[name]);
118                }
119                dijit.setWaiRole(this.displayNode, "button");
120                if(!this.displayNode.getAttribute("tabIndex")){
121                        this.displayNode.setAttribute("tabIndex", 0);
122                }
123
124                if(!this.value && !("value" in this.params)){ // "" is a good value if specified directly so check params){
125                   this.value = dojo.trim(this.renderAsHtml ? this.displayNode.innerHTML :
126                      (this.displayNode.innerText||this.displayNode.textContent||""));
127                }
128                if(!this.value){
129                    this.displayNode.innerHTML = this.noValueIndicator;
130                }
131
132                dojo.addClass(this.displayNode, 'dijitInlineEditBoxDisplayMode');
133        },
134
135        setDisabled: function(/*Boolean*/ disabled){
136                // summary:
137                //              Deprecated.   Use attr('disable', ...) instead.
138                // tags:
139                //              deprecated
140                dojo.deprecated("dijit.InlineEditBox.setDisabled() is deprecated.  Use attr('disabled', bool) instead.", "", "2.0");
141                this.attr('disabled', disabled);
142        },
143
144        _setDisabledAttr: function(/*Boolean*/ disabled){
145                // summary:
146                //              Hook to make attr("disabled", ...) work.
147                //              Set disabled state of widget.
148                this.disabled = disabled;
149                dijit.setWaiState(this.domNode, "disabled", disabled);
150                if(disabled){
151                        this.displayNode.removeAttribute("tabIndex");
152                }else{
153                        this.displayNode.setAttribute("tabIndex", 0);
154                }
155                dojo.toggleClass(this.displayNode, "dijitInlineEditBoxDisplayModeDisabled", disabled);
156        },
157
158        _onMouseOver: function(){
159                // summary:
160                //              Handler for onmouseover and onfocus event.
161                // tags:
162                //              private
163                if(!this.disabled){
164                        dojo.addClass(this.displayNode, "dijitInlineEditBoxDisplayModeHover");
165                }
166        },
167
168        _onMouseOut: function(){
169                // summary:
170                //              Handler for onmouseout and onblur event.
171                // tags:
172                //              private
173                dojo.removeClass(this.displayNode, "dijitInlineEditBoxDisplayModeHover");
174        },
175
176        _onClick: function(/*Event*/ e){
177                // summary:
178                //              Handler for onclick event.
179                // tags:
180                //              private
181                if(this.disabled){ return; }
182                if(e){ dojo.stopEvent(e); }
183                this._onMouseOut();
184
185                // Since FF gets upset if you move a node while in an event handler for that node...
186                setTimeout(dojo.hitch(this, "edit"), 0);
187        },
188
189        edit: function(){
190                // summary:
191                //              Display the editor widget in place of the original (read only) markup.
192                // tags:
193                //              private
194
195                if(this.disabled || this.editing){ return; }
196                this.editing = true;
197
198                // save some display node values that can be restored later
199                this._savedPosition = dojo.style(this.displayNode, "position") || "static";
200                this._savedOpacity = dojo.style(this.displayNode, "opacity") || "1";
201                this._savedTabIndex = dojo.attr(this.displayNode, "tabIndex") || "0";
202
203                if(this.wrapperWidget){
204                        this.wrapperWidget.editWidget.attr("displayedValue" in this.editorParams ? "displayedValue" : "value", this.value);
205                }else{
206                        // Placeholder for edit widget
207                        // Put place holder (and eventually editWidget) before the display node so that it's positioned correctly
208                        // when Calendar dropdown appears, which happens automatically on focus.
209                        var placeholder = dojo.create("span", null, this.domNode, "before");
210
211                        // Create the editor wrapper (the thing that holds the editor widget and the save/cancel buttons)
212                        var ewc = dojo.getObject(this.editorWrapper);
213                        this.wrapperWidget = new ewc({
214                                value: this.value,
215                                buttonSave: this.buttonSave,
216                                buttonCancel: this.buttonCancel,
217                                tabIndex: this._savedTabIndex,
218                                editor: this.editor,
219                                inlineEditBox: this,
220                                sourceStyle: dojo.getComputedStyle(this.displayNode),
221                                save: dojo.hitch(this, "save"),
222                                cancel: dojo.hitch(this, "cancel")
223                        }, placeholder);
224                }
225                var ww = this.wrapperWidget;
226
227                if(dojo.isIE){
228                        dijit.focus(dijit.getFocus()); // IE (at least 8) needs help with tab order changes
229                }
230                // to avoid screen jitter, we first create the editor with position:absolute, visibility:hidden,
231                // and then when it's finished rendering, we switch from display mode to editor
232                // position:absolute releases screen space allocated to the display node
233                // opacity:0 is the same as visibility:hidden but is still focusable
234                // visiblity:hidden removes focus outline
235
236                dojo.style(this.displayNode, { position: "absolute", opacity: "0", display: "none" }); // makes display node invisible, display style used for focus-ability
237                dojo.style(ww.domNode, { position: this._savedPosition, visibility: "visible", opacity: "1" });
238                dojo.attr(this.displayNode, "tabIndex", "-1"); // needed by WebKit for TAB from editor to skip displayNode
239
240                // Replace the display widget with edit widget, leaving them both displayed for a brief time so that
241                // focus can be shifted without incident.  (browser may needs some time to render the editor.)
242                setTimeout(dojo.hitch(this, function(){
243                        ww.focus(); // both nodes are showing, so we can switch focus safely
244                        ww._resetValue = ww.getValue();
245                }), 0);
246        },
247
248        _onBlur: function(){
249                // summary:
250                //              Called when focus moves outside the InlineEditBox.
251                //              Performs garbage collection.
252                // tags:
253                //              private
254
255                this.inherited(arguments);
256                if(!this.editing){
257                        setTimeout(dojo.hitch(this, function(){
258                                if(this.wrapperWidget){
259                                        this.wrapperWidget.destroy();
260                                        delete this.wrapperWidget;
261                                }
262                        }), 0);
263                }
264        },
265
266        _showText: function(/*Boolean*/ focus){
267                // summary:
268                //              Revert to display mode, and optionally focus on display node
269                // tags:
270                //              private
271
272                var ww = this.wrapperWidget;
273                dojo.style(ww.domNode, { position: "absolute", visibility: "hidden", opacity: "0" }); // hide the editor from mouse/keyboard events
274                dojo.style(this.displayNode, { position: this._savedPosition, opacity: this._savedOpacity, display: "" }); // make the original text visible
275                dojo.attr(this.displayNode, "tabIndex", this._savedTabIndex);
276                if(focus){
277                        dijit.focus(this.displayNode);
278                }
279        },
280
281        save: function(/*Boolean*/ focus){
282                // summary:
283                //              Save the contents of the editor and revert to display mode.
284                // focus: Boolean
285                //              Focus on the display mode text
286                // tags:
287                //              private
288
289                if(this.disabled || !this.editing){ return; }
290                this.editing = false;
291
292                var ww = this.wrapperWidget;
293                var value = ww.getValue();
294                this.attr('value', value); // display changed, formatted value
295
296                // tell the world that we have changed
297                setTimeout(dojo.hitch(this, "onChange", value), 0); // setTimeout prevents browser freeze for long-running event handlers
298
299                this._showText(focus); // set focus as needed
300        },
301
302        setValue: function(/*String*/ val){
303                // summary:
304                //              Deprecated.   Use attr('value', ...) instead.
305                // tags:
306                //              deprecated
307                dojo.deprecated("dijit.InlineEditBox.setValue() is deprecated.  Use attr('value', ...) instead.", "", "2.0");
308                return this.attr("value", val);
309        },
310
311        _setValueAttr: function(/*String*/ val){
312                // summary:
313                //              Hook to make attr("value", ...) work.
314                //              Inserts specified HTML value into this node, or an "input needed" character if node is blank.
315
316                this.value = val = dojo.trim(val);
317                if(!this.renderAsHtml){
318                        val = val.replace(/&/gm, "&amp;").replace(/</gm, "&lt;").replace(/>/gm, "&gt;").replace(/"/gm, "&quot;").replace(/\n/g, "<br>");
319                }
320                this.displayNode.innerHTML = val || this.noValueIndicator;
321        },
322
323        getValue: function(){
324                // summary:
325                //              Deprecated.   Use attr('value') instead.
326                // tags:
327                //              deprecated
328                dojo.deprecated("dijit.InlineEditBox.getValue() is deprecated.  Use attr('value') instead.", "", "2.0");
329                return this.attr("value");
330        },
331
332        cancel: function(/*Boolean*/ focus){
333                // summary:
334                //              Revert to display mode, discarding any changes made in the editor
335                // tags:
336                //              private
337
338                if(this.disabled || !this.editing){ return; }
339                this.editing = false;
340
341                // tell the world that we have no changes
342                setTimeout(dojo.hitch(this, "onCancel"), 0); // setTimeout prevents browser freeze for long-running event handlers
343
344                this._showText(focus);
345        }
346});
347
348dojo.declare(
349        "dijit._InlineEditor",
350         [dijit._Widget, dijit._Templated],
351{
352        // summary:
353        //              Internal widget used by InlineEditBox, displayed when in editing mode
354        //              to display the editor and maybe save/cancel buttons.  Calling code should
355        //              connect to save/cancel methods to detect when editing is finished
356        //
357        //              Has mainly the same parameters as InlineEditBox, plus these values:
358        //
359        // style: Object
360        //              Set of CSS attributes of display node, to replicate in editor
361        //
362        // value: String
363        //              Value as an HTML string or plain text string, depending on renderAsHTML flag
364
365        templateString: dojo.cache("dijit", "templates/InlineEditBox.html"),
366        widgetsInTemplate: true,
367
368        postMixInProperties: function(){
369                this.inherited(arguments);
370                this.messages = dojo.i18n.getLocalization("dijit", "common", this.lang);
371                dojo.forEach(["buttonSave", "buttonCancel"], function(prop){
372                        if(!this[prop]){ this[prop] = this.messages[prop]; }
373                }, this);
374        },
375
376        postCreate: function(){
377                // Create edit widget in place in the template
378                var cls = dojo.getObject(this.editor);
379
380                // Copy the style from the source
381                // Don't copy ALL properties though, just the necessary/applicable ones
382                var srcStyle = this.sourceStyle;
383                var editStyle = "line-height:" + srcStyle.lineHeight + ";";
384                dojo.forEach(["Weight","Family","Size","Style"], function(prop){
385                        editStyle += "font-"+prop+":"+srcStyle["font"+prop]+";";
386                }, this);
387                dojo.forEach(["marginTop","marginBottom","marginLeft", "marginRight"], function(prop){
388                        this.domNode.style[prop] = srcStyle[prop];
389                }, this);
390                var width = this.inlineEditBox.width;
391                if(width == "100%"){
392                        // block mode
393                        editStyle += "width:100%;";
394                        this.domNode.style.display = "block";
395                }else{
396                        // inline-block mode
397                        editStyle += "width:" + (width + (Number(width) == width ? "px" : "")) + ";";
398                }
399                var editorParams = this.inlineEditBox.editorParams;
400                editorParams.style = editStyle;
401                editorParams[ "displayedValue" in cls.prototype ? "displayedValue" : "value"] = this.value;
402                var ew = (this.editWidget = new cls(editorParams, this.editorPlaceholder));
403
404                if(this.inlineEditBox.autoSave){
405                        // Hide the save/cancel buttons since saving is done by simply tabbing away or
406                        // selecting a value from the drop down list
407                        this.buttonContainer.style.display="none";
408
409                        // Selecting a value from a drop down list causes an onChange event and then we save
410                        this.connect(ew, "onChange", "_onChange");
411
412                        // ESC and TAB should cancel and save.  Note that edit widgets do a stopEvent() on ESC key (to
413                        // prevent Dialog from closing when the user just wants to revert the value in the edit widget),
414                        // so this is the only way we can see the key press event.
415                        this.connect(ew, "onKeyPress", "_onKeyPress");
416                }else{
417                        // If possible, enable/disable save button based on whether the user has changed the value
418                        if("intermediateChanges" in cls.prototype){
419                                ew.attr("intermediateChanges", true);
420                                this.connect(ew, "onChange", "_onIntermediateChange");
421                                this.saveButton.attr("disabled", true);
422                        }
423                }
424        },
425
426        _onIntermediateChange: function(val){
427                // summary:
428                //              Called for editor widgets that support the intermediateChanges=true flag as a way
429                //              to detect when to enable/disabled the save button
430                this.saveButton.attr("disabled", (this.getValue() == this._resetValue) || !this.enableSave());
431        },
432
433        destroy: function(){
434                this.editWidget.destroy(true); // let the parent wrapper widget clean up the DOM
435                this.inherited(arguments);
436        },
437
438        getValue: function(){
439                // summary:
440                //              Return the [display] value of the edit widget
441                var ew = this.editWidget;
442                return String(ew.attr("displayedValue" in ew ? "displayedValue" : "value"));
443        },
444
445        _onKeyPress: function(e){
446                // summary:
447                //              Handler for keypress in the edit box in autoSave mode.
448                // description:
449                //              For autoSave widgets, if Esc/Enter, call cancel/save.
450                // tags:
451                //              private
452
453                if(this.inlineEditBox.autoSave && this.inlineEditBox.editing){
454                        if(e.altKey || e.ctrlKey){ return; }
455                        // If Enter/Esc pressed, treat as save/cancel.
456                        if(e.charOrCode == dojo.keys.ESCAPE){
457                                dojo.stopEvent(e);
458                                this.cancel(true); // sets editing=false which short-circuits _onBlur processing
459                        }else if(e.charOrCode == dojo.keys.ENTER && e.target.tagName == "INPUT"){
460                                dojo.stopEvent(e);
461                                this._onChange(); // fire _onBlur and then save
462                        }
463
464                        // _onBlur will handle TAB automatically by allowing
465                        // the TAB to change focus before we mess with the DOM: #6227
466                        // Expounding by request:
467                        //      The current focus is on the edit widget input field.
468                        //      save() will hide and destroy this widget.
469                        //      We want the focus to jump from the currently hidden
470                        //      displayNode, but since it's hidden, it's impossible to
471                        //      unhide it, focus it, and then have the browser focus
472                        //      away from it to the next focusable element since each
473                        //      of these events is asynchronous and the focus-to-next-element
474                        //      is already queued.
475                        //      So we allow the browser time to unqueue the move-focus event
476                        //      before we do all the hide/show stuff.
477                }
478        },
479
480        _onBlur: function(){
481                // summary:
482                //              Called when focus moves outside the editor
483                // tags:
484                //              private
485
486                this.inherited(arguments);
487                if(this.inlineEditBox.autoSave && this.inlineEditBox.editing){
488                        if(this.getValue() == this._resetValue){
489                                this.cancel(false);
490                        }else if(this.enableSave()){
491                                this.save(false);
492                        }
493                }
494        },
495
496        _onChange: function(){
497                // summary:
498                //              Called when the underlying widget fires an onChange event,
499                //              such as when the user selects a value from the drop down list of a ComboBox,
500                //              which means that the user has finished entering the value and we should save.
501                // tags:
502                //              private
503
504                if(this.inlineEditBox.autoSave && this.inlineEditBox.editing && this.enableSave()){
505                        dojo.style(this.inlineEditBox.displayNode, { display: "" });
506                        dijit.focus(this.inlineEditBox.displayNode); // fires _onBlur which will save the formatted value
507                }
508        },
509
510        enableSave: function(){
511                // summary:
512                //              User overridable function returning a Boolean to indicate
513                //              if the Save button should be enabled or not - usually due to invalid conditions
514                // tags:
515                //              extension
516                return (
517                        this.editWidget.isValid
518                        ? this.editWidget.isValid()
519                        : true
520                );
521        },
522
523        focus: function(){
524                // summary:
525                //              Focus the edit widget.
526                // tags:
527                //              protected
528
529                this.editWidget.focus();
530                setTimeout(dojo.hitch(this, function(){
531                        if(this.editWidget.focusNode && this.editWidget.focusNode.tagName == "INPUT"){
532                                dijit.selectInputText(this.editWidget.focusNode);
533                        }
534                }), 0);
535        }
536});
Note: See TracBrowser for help on using the browser.