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 */
8WebInspector.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 */
17WebInspector.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
27WebInspector.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 */
211WebInspector.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
237WebInspector.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