1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 /** 6 * @constructor 7 */ 8 WebInspector.InplaceEditor = function() 9 { 10 }; 11 12 /** 13 * @param {!Element} element 14 * @param {!WebInspector.InplaceEditor.Config=} config 15 * @return {?{cancel: function(), commit: function(), setWidth: function(number)}} 16 */ 17 WebInspector.InplaceEditor.startEditing = function(element, config) 18 { 19 if (config.multiline) 20 return self.runtime.instance(WebInspector.InplaceEditor).startEditing(element, config); 21 22 if (!WebInspector.InplaceEditor._defaultInstance) 23 WebInspector.InplaceEditor._defaultInstance = new WebInspector.InplaceEditor(); 24 return WebInspector.InplaceEditor._defaultInstance.startEditing(element, config); 25 } 26 27 WebInspector.InplaceEditor.prototype = { 28 /** 29 * @return {string} 30 */ 31 editorContent: function(editingContext) { 32 var element = editingContext.element; 33 if (element.tagName === "INPUT" && element.type === "text") 34 return element.value; 35 36 return element.textContent; 37 }, 38 39 setUpEditor: function(editingContext) 40 { 41 var element = editingContext.element; 42 element.classList.add("editing"); 43 44 var oldTabIndex = element.getAttribute("tabIndex"); 45 if (typeof oldTabIndex !== "number" || oldTabIndex < 0) 46 element.tabIndex = 0; 47 WebInspector.setCurrentFocusElement(element); 48 editingContext.oldTabIndex = oldTabIndex; 49 }, 50 51 closeEditor: function(editingContext) 52 { 53 var element = editingContext.element; 54 element.classList.remove("editing"); 55 56 if (typeof editingContext.oldTabIndex !== "number") 57 element.removeAttribute("tabIndex"); 58 else 59 element.tabIndex = editingContext.oldTabIndex; 60 element.scrollTop = 0; 61 element.scrollLeft = 0; 62 }, 63 64 cancelEditing: function(editingContext) 65 { 66 var element = editingContext.element; 67 if (element.tagName === "INPUT" && element.type === "text") 68 element.value = editingContext.oldText; 69 else 70 element.textContent = editingContext.oldText; 71 }, 72 73 augmentEditingHandle: function(editingContext, handle) 74 { 75 }, 76 77 /** 78 * @param {!Element} element 79 * @param {!WebInspector.InplaceEditor.Config=} config 80 * @return {?{cancel: function(), commit: function()}} 81 */ 82 startEditing: function(element, config) 83 { 84 if (!WebInspector.markBeingEdited(element, true)) 85 return null; 86 87 config = config || new WebInspector.InplaceEditor.Config(function() {}, function() {}); 88 var editingContext = { element: element, config: config }; 89 var committedCallback = config.commitHandler; 90 var cancelledCallback = config.cancelHandler; 91 var pasteCallback = config.pasteHandler; 92 var context = config.context; 93 var isMultiline = config.multiline || false; 94 var moveDirection = ""; 95 var self = this; 96 97 /** 98 * @param {!Event} e 99 */ 100 function consumeCopy(e) 101 { 102 e.consume(); 103 } 104 105 this.setUpEditor(editingContext); 106 107 editingContext.oldText = isMultiline ? config.initialValue : this.editorContent(editingContext); 108 109 /** 110 * @param {!Event=} e 111 */ 112 function blurEventListener(e) { 113 if (config.blurHandler && !config.blurHandler(element, e)) 114 return; 115 if (!isMultiline || !e || !e.relatedTarget || !e.relatedTarget.isSelfOrDescendant(element)) 116 editingCommitted.call(element); 117 } 118 119 function cleanUpAfterEditing() 120 { 121 WebInspector.markBeingEdited(element, false); 122 123 element.removeEventListener("blur", blurEventListener, isMultiline); 124 element.removeEventListener("keydown", keyDownEventListener, true); 125 if (pasteCallback) 126 element.removeEventListener("paste", pasteEventListener, true); 127 128 WebInspector.restoreFocusFromElement(element); 129 self.closeEditor(editingContext); 130 } 131 132 /** @this {Element} */ 133 function editingCancelled() 134 { 135 self.cancelEditing(editingContext); 136 cleanUpAfterEditing(); 137 cancelledCallback(this, context); 138 } 139 140 /** @this {Element} */ 141 function editingCommitted() 142 { 143 cleanUpAfterEditing(); 144 145 committedCallback(this, self.editorContent(editingContext), editingContext.oldText, context, moveDirection); 146 } 147 148 function defaultFinishHandler(event) 149 { 150 var isMetaOrCtrl = WebInspector.isMac() ? 151 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey : 152 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey; 153 if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !isMultiline || isMetaOrCtrl)) 154 return "commit"; 155 else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B") 156 return "cancel"; 157 else if (!isMultiline && event.keyIdentifier === "U+0009") // Tab key 158 return "move-" + (event.shiftKey ? "backward" : "forward"); 159 } 160 161 function handleEditingResult(result, event) 162 { 163 if (result === "commit") { 164 editingCommitted.call(element); 165 event.consume(true); 166 } else if (result === "cancel") { 167 editingCancelled.call(element); 168 event.consume(true); 169 } else if (result && result.startsWith("move-")) { 170 moveDirection = result.substring(5); 171 if (event.keyIdentifier !== "U+0009") 172 blurEventListener(); 173 } 174 } 175 176 function pasteEventListener(event) 177 { 178 var result = pasteCallback(event); 179 handleEditingResult(result, event); 180 } 181 182 function keyDownEventListener(event) 183 { 184 var handler = config.customFinishHandler || defaultFinishHandler; 185 var result = handler(event); 186 handleEditingResult(result, event); 187 } 188 189 element.addEventListener("blur", blurEventListener, isMultiline); 190 element.addEventListener("keydown", keyDownEventListener, true); 191 if (pasteCallback) 192 element.addEventListener("paste", pasteEventListener, true); 193 194 var handle = { 195 cancel: editingCancelled.bind(element), 196 commit: editingCommitted.bind(element) 197 }; 198 this.augmentEditingHandle(editingContext, handle); 199 return handle; 200 } 201 } 202 203 /** 204 * @constructor 205 * @param {function(!Element,string,string,T,string)} commitHandler 206 * @param {function(!Element,T)} cancelHandler 207 * @param {T=} context 208 * @param {function(!Element,!Event):boolean=} blurHandler 209 * @template T 210 */ 211 WebInspector.InplaceEditor.Config = function(commitHandler, cancelHandler, context, blurHandler) 212 { 213 this.commitHandler = commitHandler; 214 this.cancelHandler = cancelHandler 215 this.context = context; 216 this.blurHandler = blurHandler; 217 218 /** 219 * Handles the "paste" event, return values are the same as those for customFinishHandler 220 * @type {function(!Element)|undefined} 221 */ 222 this.pasteHandler; 223 224 /** 225 * Whether the edited element is multiline 226 * @type {boolean|undefined} 227 */ 228 this.multiline; 229 230 /** 231 * Custom finish handler for the editing session (invoked on keydown) 232 * @type {function(!Element,*)|undefined} 233 */ 234 this.customFinishHandler; 235 } 236 237 WebInspector.InplaceEditor.Config.prototype = { 238 setPasteHandler: function(pasteHandler) 239 { 240 this.pasteHandler = pasteHandler; 241 }, 242 243 /** 244 * @param {string} initialValue 245 * @param {!Object} mode 246 * @param {string} theme 247 * @param {boolean=} lineWrapping 248 * @param {boolean=} smartIndent 249 */ 250 setMultilineOptions: function(initialValue, mode, theme, lineWrapping, smartIndent) 251 { 252 this.multiline = true; 253 this.initialValue = initialValue; 254 this.mode = mode; 255 this.theme = theme; 256 this.lineWrapping = lineWrapping; 257 this.smartIndent = smartIndent; 258 }, 259 260 setCustomFinishHandler: function(customFinishHandler) 261 { 262 this.customFinishHandler = customFinishHandler; 263 } 264 } 265