| /* |
| * Copyright (C) 2013, 2015 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. |
| */ |
| |
| WI.enclosingCodeMirror = function(element) |
| { |
| while (element) { |
| if (element.CodeMirror) |
| return element.CodeMirror; |
| element = element.parentNode; |
| } |
| return null; |
| }; |
| |
| WI.isBeingEdited = function(element) |
| { |
| while (element) { |
| if (element.__editing) |
| return true; |
| element = element.parentNode; |
| } |
| return false; |
| }; |
| |
| WI.markBeingEdited = function(element, value) |
| { |
| if (value) { |
| if (element.__editing) |
| return false; |
| element.__editing = true; |
| WI.__editingCount = (WI.__editingCount || 0) + 1; |
| } else { |
| if (!element.__editing) |
| return false; |
| delete element.__editing; |
| --WI.__editingCount; |
| } |
| return true; |
| }; |
| |
| WI.isEditingAnyField = function() |
| { |
| return !!WI.__editingCount; |
| }; |
| |
| WI.isEventTargetAnEditableField = function(event) |
| { |
| if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) |
| return true; |
| |
| if (event.target.isContentEditable) |
| return true; |
| |
| if (WI.isBeingEdited(event.target)) |
| return true; |
| |
| let codeMirror = WI.enclosingCodeMirror(event.target); |
| if (codeMirror) |
| return !codeMirror.getOption("readOnly"); |
| |
| return false; |
| }; |
| |
| WI.EditingConfig = class EditingConfig |
| { |
| constructor(commitHandler, cancelHandler, context) |
| { |
| this.commitHandler = commitHandler; |
| this.cancelHandler = cancelHandler; |
| this.context = context; |
| this.spellcheck = false; |
| } |
| |
| setPasteHandler(pasteHandler) |
| { |
| this.pasteHandler = pasteHandler; |
| } |
| |
| setMultiline(multiline) |
| { |
| this.multiline = multiline; |
| } |
| |
| setCustomFinishHandler(customFinishHandler) |
| { |
| this.customFinishHandler = customFinishHandler; |
| } |
| |
| setNumberCommitHandler(numberCommitHandler) |
| { |
| this.numberCommitHandler = numberCommitHandler; |
| } |
| }; |
| |
| WI.startEditing = function(element, config) |
| { |
| if (!WI.markBeingEdited(element, true)) |
| return null; |
| |
| config = config || new WI.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"); |
| element.contentEditable = "plaintext-only"; |
| |
| 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; |
| } |
| |
| function cleanUpAfterEditing() |
| { |
| WI.markBeingEdited(element, false); |
| |
| this.classList.remove("editing"); |
| this.contentEditable = false; |
| |
| 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); |
| |
| WI.restoreFocusFromElement(element); |
| } |
| |
| function editingCancelled() |
| { |
| if (this.tagName === "INPUT" && this.type === "text") |
| this.value = oldText; |
| else |
| this.textContent = oldText; |
| |
| cleanUpAfterEditing.call(this); |
| |
| cancelledCallback(this, context); |
| } |
| |
| 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 === WI.KeyboardShortcut.Key.Escape.keyCode || event.keyIdentifier === "U+001B") |
| return "cancel"; |
| else if (event.keyIdentifier === "U+0009") // Tab key |
| return "move-" + (event.shiftKey ? "backward" : "forward"); |
| else if (event.altKey) { |
| if (event.keyIdentifier === "Up" || event.keyIdentifier === "Down") |
| return "modify-" + (event.keyIdentifier === "Up" ? "up" : "down"); |
| if (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown") |
| return "modify-" + (event.keyIdentifier === "PageUp" ? "up-big" : "down-big"); |
| } |
| } |
| |
| 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(); |
| } else if (result && result.startsWith("modify-")) { |
| let direction = result.substring(7); |
| let delta = direction.startsWith("up") ? 1 : -1; |
| if (direction.endsWith("big")) |
| delta *= 10; |
| |
| if (event.shiftKey) |
| delta *= 10; |
| else if (event.ctrlKey) |
| delta /= 10; |
| |
| let modified = WI.incrementElementValue(element, delta); |
| if (!modified) |
| return; |
| |
| if (typeof config.numberCommitHandler === "function") |
| config.numberCommitHandler(element, getContent(element), oldText, context, moveDirection); |
| |
| event.preventDefault(); |
| } |
| } |
| |
| 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) |
| }; |
| }; |
| |
| WI.incrementElementValue = function(element, delta) |
| { |
| let selection = element.ownerDocument.defaultView.getSelection(); |
| if (!selection.rangeCount) |
| return false; |
| |
| let range = selection.getRangeAt(0); |
| if (!element.contains(range.commonAncestorContainer)) |
| return false; |
| |
| let wordRange = range.startContainer.rangeOfWord(range.startOffset, WI.EditingSupport.StyleValueDelimiters, element); |
| let word = wordRange.toString(); |
| let wordPrefix = ""; |
| let wordSuffix = ""; |
| let nonNumberInWord = /[^\d-\.]+/.exec(word); |
| if (nonNumberInWord) { |
| let nonNumberEndOffset = nonNumberInWord.index + nonNumberInWord[0].length; |
| if (range.startOffset > wordRange.startOffset + nonNumberInWord.index && nonNumberEndOffset < word.length && range.startOffset !== wordRange.startOffset) { |
| wordPrefix = word.substring(0, nonNumberEndOffset); |
| word = word.substring(nonNumberEndOffset); |
| } else { |
| wordSuffix = word.substring(nonNumberInWord.index); |
| word = word.substring(0, nonNumberInWord.index); |
| } |
| } |
| |
| let matches = WI.EditingSupport.CSSNumberRegex.exec(word); |
| if (!matches || matches.length !== 4) |
| return false; |
| |
| let replacement = matches[1] + (Math.round((parseFloat(matches[2]) + delta) * 100) / 100) + matches[3]; |
| |
| selection.removeAllRanges(); |
| selection.addRange(wordRange); |
| document.execCommand("insertText", false, wordPrefix + replacement + wordSuffix); |
| |
| let container = range.commonAncestorContainer; |
| let startOffset = range.startOffset; |
| // This check is for the situation when the cursor is in the space between the |
| // opening quote of the attribute and the first character. In that spot, the |
| // commonAncestorContainer is actually the entire attribute node since `="` is |
| // added as a simple text node. Since the opening quote is immediately before |
| // the attribute, the node for that attribute must be the next sibling and the |
| // text of the attribute's value must be the first child of that sibling. |
| if (container.parentNode.classList.contains("editing") && container.nextSibling) { |
| container = container.nextSibling.firstChild; |
| startOffset = 0; |
| } |
| startOffset += wordPrefix.length; |
| |
| if (!container) |
| return false; |
| |
| let replacementSelectionRange = document.createRange(); |
| replacementSelectionRange.setStart(container, startOffset); |
| replacementSelectionRange.setEnd(container, startOffset + replacement.length); |
| |
| selection.removeAllRanges(); |
| selection.addRange(replacementSelectionRange); |
| |
| return true; |
| }; |
| |
| WI.EditingSupport = { |
| StyleValueDelimiters: " \xA0\t\n\"':;,/()", |
| CSSNumberRegex: /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/, |
| NumberRegex: /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/ |
| }; |