Home | History | Annotate | Download | only in ui
      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  */
      8 WebInspector.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  */
     17 WebInspector.InplaceEditor.startEditing = function(element, config)
     18 {
     19     if (config.multiline)
     20         return WebInspector.moduleManager.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 
     27 WebInspector.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 (!isMultiline || !e || !e.relatedTarget || !e.relatedTarget.isSelfOrDescendant(element))
    114                 editingCommitted.call(element);
    115         }
    116 
    117         function cleanUpAfterEditing()
    118         {
    119             WebInspector.markBeingEdited(element, false);
    120 
    121             element.removeEventListener("blur", blurEventListener, isMultiline);
    122             element.removeEventListener("keydown", keyDownEventListener, true);
    123             if (pasteCallback)
    124                 element.removeEventListener("paste", pasteEventListener, true);
    125 
    126             WebInspector.restoreFocusFromElement(element);
    127             self.closeEditor(editingContext);
    128         }
    129 
    130         /** @this {Element} */
    131         function editingCancelled()
    132         {
    133             self.cancelEditing(editingContext);
    134             cleanUpAfterEditing();
    135             cancelledCallback(this, context);
    136         }
    137 
    138         /** @this {Element} */
    139         function editingCommitted()
    140         {
    141             cleanUpAfterEditing();
    142 
    143             committedCallback(this, self.editorContent(editingContext), editingContext.oldText, context, moveDirection);
    144         }
    145 
    146         function defaultFinishHandler(event)
    147         {
    148             var isMetaOrCtrl = WebInspector.isMac() ?
    149                 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
    150                 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
    151             if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !isMultiline || isMetaOrCtrl))
    152                 return "commit";
    153             else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
    154                 return "cancel";
    155             else if (!isMultiline && event.keyIdentifier === "U+0009") // Tab key
    156                 return "move-" + (event.shiftKey ? "backward" : "forward");
    157         }
    158 
    159         function handleEditingResult(result, event)
    160         {
    161             if (result === "commit") {
    162                 editingCommitted.call(element);
    163                 event.consume(true);
    164             } else if (result === "cancel") {
    165                 editingCancelled.call(element);
    166                 event.consume(true);
    167             } else if (result && result.startsWith("move-")) {
    168                 moveDirection = result.substring(5);
    169                 if (event.keyIdentifier !== "U+0009")
    170                     blurEventListener();
    171             }
    172         }
    173 
    174         function pasteEventListener(event)
    175         {
    176             var result = pasteCallback(event);
    177             handleEditingResult(result, event);
    178         }
    179 
    180         function keyDownEventListener(event)
    181         {
    182             var handler = config.customFinishHandler || defaultFinishHandler;
    183             var result = handler(event);
    184             handleEditingResult(result, event);
    185         }
    186 
    187         element.addEventListener("blur", blurEventListener, isMultiline);
    188         element.addEventListener("keydown", keyDownEventListener, true);
    189         if (pasteCallback)
    190             element.addEventListener("paste", pasteEventListener, true);
    191 
    192         var handle = {
    193             cancel: editingCancelled.bind(element),
    194             commit: editingCommitted.bind(element)
    195         };
    196         this.augmentEditingHandle(editingContext, handle);
    197         return handle;
    198     }
    199 }
    200 
    201 /**
    202  * @constructor
    203  * @param {function(!Element,string,string,T,string)} commitHandler
    204  * @param {function(!Element,T)} cancelHandler
    205  * @param {T=} context
    206  * @template T
    207  */
    208 WebInspector.InplaceEditor.Config = function(commitHandler, cancelHandler, context)
    209 {
    210     this.commitHandler = commitHandler;
    211     this.cancelHandler = cancelHandler
    212     this.context = context;
    213 
    214     /**
    215      * Handles the "paste" event, return values are the same as those for customFinishHandler
    216      * @type {function(!Element)|undefined}
    217      */
    218     this.pasteHandler;
    219 
    220     /**
    221      * Whether the edited element is multiline
    222      * @type {boolean|undefined}
    223      */
    224     this.multiline;
    225 
    226     /**
    227      * Custom finish handler for the editing session (invoked on keydown)
    228      * @type {function(!Element,*)|undefined}
    229      */
    230     this.customFinishHandler;
    231 }
    232 
    233 WebInspector.InplaceEditor.Config.prototype = {
    234     setPasteHandler: function(pasteHandler)
    235     {
    236         this.pasteHandler = pasteHandler;
    237     },
    238 
    239     /**
    240      * @param {string} initialValue
    241      * @param {!Object} mode
    242      * @param {string} theme
    243      * @param {boolean=} lineWrapping
    244      * @param {boolean=} smartIndent
    245      */
    246     setMultilineOptions: function(initialValue, mode, theme, lineWrapping, smartIndent)
    247     {
    248         this.multiline = true;
    249         this.initialValue = initialValue;
    250         this.mode = mode;
    251         this.theme = theme;
    252         this.lineWrapping = lineWrapping;
    253         this.smartIndent = smartIndent;
    254     },
    255 
    256     setCustomFinishHandler: function(customFinishHandler)
    257     {
    258         this.customFinishHandler = customFinishHandler;
    259     }
    260 }
    261