blob: 9c222bd2c79a9e2bab7aa3f2e947b2500f56afd5 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @constructor
*/
WebInspector.InplaceEditor = function()
{
}
/**
* @typedef {{cancel: function(), commit: function(), setWidth: function(number)}}
*/
WebInspector.InplaceEditor.Controller;
/**
* @param {!Element} element
* @param {!WebInspector.InplaceEditor.Config=} config
* @return {?WebInspector.InplaceEditor.Controller}
*/
WebInspector.InplaceEditor.startEditing = function(element, config)
{
if (!WebInspector.InplaceEditor._defaultInstance)
WebInspector.InplaceEditor._defaultInstance = new WebInspector.InplaceEditor();
return WebInspector.InplaceEditor._defaultInstance.startEditing(element, config);
}
/**
* @param {!Element} element
* @param {!WebInspector.InplaceEditor.Config=} config
* @return {!Promise.<!WebInspector.InplaceEditor.Controller>}
*/
WebInspector.InplaceEditor.startMultilineEditing = function(element, config)
{
return self.runtime.instancePromise(WebInspector.InplaceEditor).then(startEditing);
/**
* @param {!Object} inplaceEditor
* @return {!WebInspector.InplaceEditor.Controller|!Promise.<!WebInspector.InplaceEditor.Controller>}
*/
function startEditing(inplaceEditor)
{
var controller = /** @type {!WebInspector.InplaceEditor} */ (inplaceEditor).startEditing(element, config);
if (!controller)
return Promise.reject(new Error("Editing is already in progress"));
return controller;
}
}
WebInspector.InplaceEditor.prototype = {
/**
* @return {string}
*/
editorContent: function(editingContext) {
var element = editingContext.element;
if (element.tagName === "INPUT" && element.type === "text")
return element.value;
return element.textContent;
},
setUpEditor: function(editingContext)
{
var element = editingContext.element;
element.classList.add("editing");
var oldTabIndex = element.getAttribute("tabIndex");
if (typeof oldTabIndex !== "number" || oldTabIndex < 0)
element.tabIndex = 0;
WebInspector.setCurrentFocusElement(element);
editingContext.oldTabIndex = oldTabIndex;
},
closeEditor: function(editingContext)
{
var element = editingContext.element;
element.classList.remove("editing");
if (typeof editingContext.oldTabIndex !== "number")
element.removeAttribute("tabIndex");
else
element.tabIndex = editingContext.oldTabIndex;
element.scrollTop = 0;
element.scrollLeft = 0;
},
cancelEditing: function(editingContext)
{
var element = editingContext.element;
if (element.tagName === "INPUT" && element.type === "text")
element.value = editingContext.oldText;
else
element.textContent = editingContext.oldText;
},
augmentEditingHandle: function(editingContext, handle)
{
},
/**
* @param {!Element} element
* @param {!WebInspector.InplaceEditor.Config=} config
* @return {?WebInspector.InplaceEditor.Controller}
*/
startEditing: function(element, config)
{
if (!WebInspector.markBeingEdited(element, true))
return null;
config = config || new WebInspector.InplaceEditor.Config(function() {}, function() {});
var editingContext = { element: element, config: config };
var committedCallback = config.commitHandler;
var cancelledCallback = config.cancelHandler;
var pasteCallback = config.pasteHandler;
var context = config.context;
var isMultiline = config.multiline || false;
var moveDirection = "";
var self = this;
/**
* @param {!Event} e
*/
function consumeCopy(e)
{
e.consume();
}
this.setUpEditor(editingContext);
editingContext.oldText = isMultiline ? config.initialValue : this.editorContent(editingContext);
/**
* @param {!Event=} e
*/
function blurEventListener(e) {
if (config.blurHandler && !config.blurHandler(element, e))
return;
if (!isMultiline || !e || !e.relatedTarget || !e.relatedTarget.isSelfOrDescendant(element))
editingCommitted.call(element);
}
function cleanUpAfterEditing()
{
WebInspector.markBeingEdited(element, false);
element.removeEventListener("blur", blurEventListener, isMultiline);
element.removeEventListener("keydown", keyDownEventListener, true);
if (pasteCallback)
element.removeEventListener("paste", pasteEventListener, true);
WebInspector.restoreFocusFromElement(element);
self.closeEditor(editingContext);
}
/** @this {Element} */
function editingCancelled()
{
self.cancelEditing(editingContext);
cleanUpAfterEditing();
cancelledCallback(this, context);
}
/** @this {Element} */
function editingCommitted()
{
cleanUpAfterEditing();
committedCallback(this, self.editorContent(editingContext), editingContext.oldText, context, moveDirection);
}
/**
* @param {!Event} event
* @return {string}
*/
function defaultFinishHandler(event)
{
var isMetaOrCtrl = WebInspector.isMac() ?
event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !isMultiline || isMetaOrCtrl))
return "commit";
else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.key === "Escape")
return "cancel";
else if (!isMultiline && event.key === "Tab")
return "move-" + (event.shiftKey ? "backward" : "forward");
return "";
}
function handleEditingResult(result, event)
{
if (result === "commit") {
editingCommitted.call(element);
event.consume(true);
} else if (result === "cancel") {
editingCancelled.call(element);
event.consume(true);
} else if (result && result.startsWith("move-")) {
moveDirection = result.substring(5);
if (event.key !== "Tab")
blurEventListener();
}
}
/**
* @param {!Event} event
*/
function pasteEventListener(event)
{
var result = pasteCallback(event);
handleEditingResult(result, event);
}
/**
* @param {!Event} event
*/
function keyDownEventListener(event)
{
var result = defaultFinishHandler(event);
if (!result && config.postKeydownFinishHandler)
result = config.postKeydownFinishHandler(event);
handleEditingResult(result, event);
}
element.addEventListener("blur", blurEventListener, isMultiline);
element.addEventListener("keydown", keyDownEventListener, true);
if (pasteCallback)
element.addEventListener("paste", pasteEventListener, true);
var handle = {
cancel: editingCancelled.bind(element),
commit: editingCommitted.bind(element),
setWidth: function() {}
};
this.augmentEditingHandle(editingContext, handle);
return handle;
}
}
/**
* @constructor
* @param {function(!Element,string,string,T,string)} commitHandler
* @param {function(!Element,T)} cancelHandler
* @param {T=} context
* @param {function(!Element,!Event):boolean=} blurHandler
* @template T
*/
WebInspector.InplaceEditor.Config = function(commitHandler, cancelHandler, context, blurHandler)
{
this.commitHandler = commitHandler;
this.cancelHandler = cancelHandler;
this.context = context;
this.blurHandler = blurHandler;
/**
* @type {function(!Event):string|undefined}
*/
this.pasteHandler;
/**
* @type {boolean|undefined}
*/
this.multiline;
/**
* @type {function(!Event):string|undefined}
*/
this.postKeydownFinishHandler;
}
WebInspector.InplaceEditor.Config.prototype = {
setPasteHandler: function(pasteHandler)
{
this.pasteHandler = pasteHandler;
},
/**
* @param {string} initialValue
* @param {!Object} mode
* @param {string} theme
* @param {boolean=} lineWrapping
* @param {boolean=} smartIndent
*/
setMultilineOptions: function(initialValue, mode, theme, lineWrapping, smartIndent)
{
this.multiline = true;
this.initialValue = initialValue;
this.mode = mode;
this.theme = theme;
this.lineWrapping = lineWrapping;
this.smartIndent = smartIndent;
},
/**
* @param {function(!Event):string} postKeydownFinishHandler
*/
setPostKeydownFinishHandler: function(postKeydownFinishHandler)
{
this.postKeydownFinishHandler = postKeydownFinishHandler;
}
}