blob: 1192999405f9a531af4473346b6d16e88c4b9522 [file] [log] [blame]
/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WebInspector.isBeingEdited = function(element)
{
while (element) {
if (element.__editing)
return true;
element = element.parentNode;
}
return false;
}
WebInspector.markBeingEdited = function(element, value)
{
if (value) {
if (element.__editing)
return false;
element.__editing = true;
WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1;
} else {
if (!element.__editing)
return false;
delete element.__editing;
--WebInspector.__editingCount;
}
return true;
}
WebInspector.isEditingAnyField = function()
{
return !!WebInspector.__editingCount;
}
WebInspector.isEventTargetAnEditableField = function(event)
{
const textInputTypes = {"text": true, "search": true, "tel": true, "url": true, "email": true, "password": true};
if (event.target instanceof HTMLInputElement)
return event.target.type in textInputTypes;
var codeMirrorEditorElement = event.target.enclosingNodeOrSelfWithClass("CodeMirror");
if (codeMirrorEditorElement && codeMirrorEditorElement.CodeMirror)
return !codeMirrorEditorElement.CodeMirror.getOption("readOnly");
if (event.target instanceof HTMLTextAreaElement)
return true;
if (event.target.enclosingNodeOrSelfWithClass("text-prompt"))
return true;
return false;
}
/**
* @constructor
* @param {function(Element,string,string,*,string)} commitHandler
* @param {function(Element,*)} cancelHandler
* @param {*=} context
*/
WebInspector.EditingConfig = function(commitHandler, cancelHandler, context)
{
this.commitHandler = commitHandler;
this.cancelHandler = cancelHandler;
this.context = context;
/**
* Handles the "paste" event, return values are the same as those for customFinishHandler
* @type {function(Element)|undefined}
*/
this.pasteHandler;
/**
* Whether the edited element is multiline
* @type {boolean|undefined}
*/
this.multiline;
/**
* Custom finish handler for the editing session (invoked on keydown)
* @type {function(Element,*)|undefined}
*/
this.customFinishHandler;
/**
* Whether or not spellcheck is enabled.
* @type {boolean}
*/
this.spellcheck = false;
}
WebInspector.EditingConfig.prototype = {
setPasteHandler: function(pasteHandler)
{
this.pasteHandler = pasteHandler;
},
setMultiline: function(multiline)
{
this.multiline = multiline;
},
setCustomFinishHandler: function(customFinishHandler)
{
this.customFinishHandler = customFinishHandler;
}
}
/**
* @param {Element} element
* @param {WebInspector.EditingConfig=} config
*/
WebInspector.startEditing = function(element, config)
{
if (!WebInspector.markBeingEdited(element, true))
return;
config = config || new WebInspector.EditingConfig(function() {}, function() {});
var committedCallback = config.commitHandler;
var cancelledCallback = config.cancelHandler;
var pasteCallback = config.pasteHandler;
var context = config.context;
var oldText = getContent(element);
var moveDirection = "";
element.classList.add("editing");
var oldSpellCheck = element.hasAttribute("spellcheck") ? element.spellcheck : undefined;
element.spellcheck = config.spellcheck;
if (config.multiline)
element.classList.add("multiline");
var oldTabIndex = element.tabIndex;
if (element.tabIndex < 0)
element.tabIndex = 0;
function blurEventListener() {
editingCommitted.call(element);
}
function getContent(element) {
if (element.tagName === "INPUT" && element.type === "text")
return element.value;
else
return element.textContent;
}
/** @this {Element} */
function cleanUpAfterEditing()
{
WebInspector.markBeingEdited(element, false);
this.classList.remove("editing");
this.scrollTop = 0;
this.scrollLeft = 0;
if (oldSpellCheck === undefined)
element.removeAttribute("spellcheck");
else
element.spellcheck = oldSpellCheck;
if (oldTabIndex === -1)
this.removeAttribute("tabindex");
else
this.tabIndex = oldTabIndex;
element.removeEventListener("blur", blurEventListener, false);
element.removeEventListener("keydown", keyDownEventListener, true);
if (pasteCallback)
element.removeEventListener("paste", pasteEventListener, true);
WebInspector.restoreFocusFromElement(element);
}
/** @this {Element} */
function editingCancelled()
{
if (this.tagName === "INPUT" && this.type === "text")
this.value = oldText;
else
this.textContent = oldText;
cleanUpAfterEditing.call(this);
cancelledCallback(this, context);
}
/** @this {Element} */
function editingCommitted()
{
cleanUpAfterEditing.call(this);
committedCallback(this, getContent(this), oldText, context, moveDirection);
}
function defaultFinishHandler(event)
{
var hasOnlyMetaModifierKey = event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey;
if (isEnterKey(event) && (!config.multiline || hasOnlyMetaModifierKey))
return "commit";
else if (event.keyCode === WebInspector.KeyboardShortcut.Key.Escape.keyCode || event.keyIdentifier === "U+001B")
return "cancel";
else if (event.keyIdentifier === "U+0009") // Tab key
return "move-" + (event.shiftKey ? "backward" : "forward");
}
function handleEditingResult(result, event)
{
if (result === "commit") {
editingCommitted.call(element);
event.preventDefault();
event.stopPropagation();
} else if (result === "cancel") {
editingCancelled.call(element);
event.preventDefault();
event.stopPropagation();
} else if (result && result.startsWith("move-")) {
moveDirection = result.substring(5);
if (event.keyIdentifier !== "U+0009")
blurEventListener();
}
}
function pasteEventListener(event)
{
var result = pasteCallback(event);
handleEditingResult(result, event);
}
function keyDownEventListener(event)
{
var handler = config.customFinishHandler || defaultFinishHandler;
var result = handler(event);
handleEditingResult(result, event);
}
element.addEventListener("blur", blurEventListener, false);
element.addEventListener("keydown", keyDownEventListener, true);
if (pasteCallback)
element.addEventListener("paste", pasteEventListener, true);
element.focus();
return {
cancel: editingCancelled.bind(element),
commit: editingCommitted.bind(element)
};
}