1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.isBeingEdited = function(element)
27{
28    while (element) {
29        if (element.__editing)
30            return true;
31        element = element.parentNode;
32    }
33
34    return false;
35}
36
37WebInspector.markBeingEdited = function(element, value)
38{
39    if (value) {
40        if (element.__editing)
41            return false;
42        element.__editing = true;
43        WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1;
44    } else {
45        if (!element.__editing)
46            return false;
47        delete element.__editing;
48        --WebInspector.__editingCount;
49    }
50    return true;
51}
52
53WebInspector.isEditingAnyField = function()
54{
55    return !!WebInspector.__editingCount;
56}
57
58WebInspector.isEventTargetAnEditableField = function(event)
59{
60    const textInputTypes = {"text": true, "search": true, "tel": true, "url": true, "email": true, "password": true};
61    if (event.target instanceof HTMLInputElement)
62        return event.target.type in textInputTypes;
63
64    var codeMirrorEditorElement = event.target.enclosingNodeOrSelfWithClass("CodeMirror");
65    if (codeMirrorEditorElement && codeMirrorEditorElement.CodeMirror)
66        return !codeMirrorEditorElement.CodeMirror.getOption("readOnly");
67
68    if (event.target instanceof HTMLTextAreaElement)
69        return true;
70
71    if (event.target.enclosingNodeOrSelfWithClass("text-prompt"))
72        return true;
73
74    return false;
75}
76
77WebInspector.EditingConfig = function(commitHandler, cancelHandler, context)
78{
79    this.commitHandler = commitHandler;
80    this.cancelHandler = cancelHandler;
81    this.context = context;
82    this.pasteHandler;
83    this.multiline;
84    this.customFinishHandler;
85    this.spellcheck = false;
86}
87
88WebInspector.EditingConfig.prototype = {
89    setPasteHandler: function(pasteHandler)
90    {
91        this.pasteHandler = pasteHandler;
92    },
93
94    setMultiline: function(multiline)
95    {
96        this.multiline = multiline;
97    },
98
99    setCustomFinishHandler: function(customFinishHandler)
100    {
101        this.customFinishHandler = customFinishHandler;
102    }
103}
104
105WebInspector.startEditing = function(element, config)
106{
107    if (!WebInspector.markBeingEdited(element, true))
108        return;
109
110    config = config || new WebInspector.EditingConfig(function() {}, function() {});
111    var committedCallback = config.commitHandler;
112    var cancelledCallback = config.cancelHandler;
113    var pasteCallback = config.pasteHandler;
114    var context = config.context;
115    var oldText = getContent(element);
116    var moveDirection = "";
117
118    element.classList.add("editing");
119
120    var oldSpellCheck = element.hasAttribute("spellcheck") ? element.spellcheck : undefined;
121    element.spellcheck = config.spellcheck;
122
123    if (config.multiline)
124        element.classList.add("multiline");
125
126    var oldTabIndex = element.tabIndex;
127    if (element.tabIndex < 0)
128        element.tabIndex = 0;
129
130    function blurEventListener() {
131        editingCommitted.call(element);
132    }
133
134    function getContent(element) {
135        if (element.tagName === "INPUT" && element.type === "text")
136            return element.value;
137        else
138            return element.textContent;
139    }
140
141    function cleanUpAfterEditing()
142    {
143        WebInspector.markBeingEdited(element, false);
144
145        this.classList.remove("editing");
146        this.scrollTop = 0;
147        this.scrollLeft = 0;
148
149        if (oldSpellCheck === undefined)
150            element.removeAttribute("spellcheck");
151        else
152            element.spellcheck = oldSpellCheck;
153
154        if (oldTabIndex === -1)
155            this.removeAttribute("tabindex");
156        else
157            this.tabIndex = oldTabIndex;
158
159        element.removeEventListener("blur", blurEventListener, false);
160        element.removeEventListener("keydown", keyDownEventListener, true);
161        if (pasteCallback)
162            element.removeEventListener("paste", pasteEventListener, true);
163
164        WebInspector.restoreFocusFromElement(element);
165    }
166
167    function editingCancelled()
168    {
169        if (this.tagName === "INPUT" && this.type === "text")
170            this.value = oldText;
171        else
172            this.textContent = oldText;
173
174        cleanUpAfterEditing.call(this);
175
176        cancelledCallback(this, context);
177    }
178
179    function editingCommitted()
180    {
181        cleanUpAfterEditing.call(this);
182
183        committedCallback(this, getContent(this), oldText, context, moveDirection);
184    }
185
186    function defaultFinishHandler(event)
187    {
188        var hasOnlyMetaModifierKey = event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey;
189        if (isEnterKey(event) && (!config.multiline || hasOnlyMetaModifierKey))
190            return "commit";
191        else if (event.keyCode === WebInspector.KeyboardShortcut.Key.Escape.keyCode || event.keyIdentifier === "U+001B")
192            return "cancel";
193        else if (event.keyIdentifier === "U+0009") // Tab key
194            return "move-" + (event.shiftKey ? "backward" : "forward");
195    }
196
197    function handleEditingResult(result, event)
198    {
199        if (result === "commit") {
200            editingCommitted.call(element);
201            event.preventDefault();
202            event.stopPropagation();
203        } else if (result === "cancel") {
204            editingCancelled.call(element);
205            event.preventDefault();
206            event.stopPropagation();
207        } else if (result && result.startsWith("move-")) {
208            moveDirection = result.substring(5);
209            if (event.keyIdentifier !== "U+0009")
210                blurEventListener();
211        }
212    }
213
214    function pasteEventListener(event)
215    {
216        var result = pasteCallback(event);
217        handleEditingResult(result, event);
218    }
219
220    function keyDownEventListener(event)
221    {
222        var handler = config.customFinishHandler || defaultFinishHandler;
223        var result = handler(event);
224        handleEditingResult(result, event);
225    }
226
227    element.addEventListener("blur", blurEventListener, false);
228    element.addEventListener("keydown", keyDownEventListener, true);
229    if (pasteCallback)
230        element.addEventListener("paste", pasteEventListener, true);
231
232    element.focus();
233
234    return {
235        cancel: editingCancelled.bind(element),
236        commit: editingCommitted.bind(element)
237    };
238}
239