blob: 86dcdc5d70e3e3367b0e1f44f0aabaf1994b79be [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.
/**
* @unrestricted
*/
UI.InplaceEditor = class {
/**
* @param {!Element} element
* @param {!UI.InplaceEditor.Config=} config
* @return {?UI.InplaceEditor.Controller}
*/
static startEditing(element, config) {
if (!UI.InplaceEditor._defaultInstance)
UI.InplaceEditor._defaultInstance = new UI.InplaceEditor();
return UI.InplaceEditor._defaultInstance.startEditing(element, config);
}
/**
* @param {!Element} element
* @param {!UI.InplaceEditor.Config=} config
* @return {!Promise.<!UI.InplaceEditor.Controller>}
*/
static startMultilineEditing(element, config) {
return self.runtime.extension(UI.InplaceEditor).instance().then(startEditing);
/**
* @param {!Object} inplaceEditor
* @return {!UI.InplaceEditor.Controller|!Promise.<!UI.InplaceEditor.Controller>}
*/
function startEditing(inplaceEditor) {
var controller = /** @type {!UI.InplaceEditor} */ (inplaceEditor).startEditing(element, config);
if (!controller)
return Promise.reject(new Error('Editing is already in progress'));
return controller;
}
}
/**
* @return {string}
*/
editorContent(editingContext) {
var element = editingContext.element;
if (element.tagName === 'INPUT' && element.type === 'text')
return element.value;
return element.textContent;
}
setUpEditor(editingContext) {
var element = editingContext.element;
element.classList.add('editing');
var oldTabIndex = element.getAttribute('tabIndex');
if (typeof oldTabIndex !== 'number' || oldTabIndex < 0)
element.tabIndex = 0;
this._focusRestorer = new UI.ElementFocusRestorer(element);
editingContext.oldTabIndex = oldTabIndex;
}
closeEditor(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(editingContext) {
var element = editingContext.element;
if (element.tagName === 'INPUT' && element.type === 'text')
element.value = editingContext.oldText;
else
element.textContent = editingContext.oldText;
}
augmentEditingHandle(editingContext, handle) {
}
/**
* @param {!Element} element
* @param {!UI.InplaceEditor.Config=} config
* @return {?UI.InplaceEditor.Controller}
*/
startEditing(element, config) {
if (!UI.markBeingEdited(element, true))
return null;
config = config || new UI.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;
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() {
UI.markBeingEdited(element, false);
element.removeEventListener('blur', blurEventListener, isMultiline);
element.removeEventListener('keydown', keyDownEventListener, true);
if (pasteCallback)
element.removeEventListener('paste', pasteEventListener, true);
if (self._focusRestorer)
self._focusRestorer.restore();
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 = Host.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 === UI.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')
event.consume(true);
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() {}};
this.augmentEditingHandle(editingContext, handle);
return handle;
}
};
/**
* @typedef {{cancel: function(), commit: function(), setWidth: function(number)}}
*/
UI.InplaceEditor.Controller;
/**
* @template T
* @unrestricted
*/
UI.InplaceEditor.Config = class {
/**
* @param {function(!Element,string,string,T,string)} commitHandler
* @param {function(!Element,T)} cancelHandler
* @param {T=} context
* @param {function(!Element,!Event):boolean=} blurHandler
*/
constructor(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;
}
setPasteHandler(pasteHandler) {
this.pasteHandler = pasteHandler;
}
/**
* @param {string} initialValue
* @param {!Object} mode
* @param {string} theme
* @param {boolean=} lineWrapping
* @param {boolean=} smartIndent
*/
setMultilineOptions(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(postKeydownFinishHandler) {
this.postKeydownFinishHandler = postKeydownFinishHandler;
}
};