InlineEditBox.js 20 KB

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