source: Dev/branches/rest-dojo-ui/client/dijit/InlineEditBox.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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