| /******************************************************************************* |
| * Copyright (c) 2014-2017 BSI Business Systems Integration AG. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * BSI Business Systems Integration AG - initial API and implementation |
| ******************************************************************************/ |
| scout.ClipboardField = function() { |
| scout.ClipboardField.parent.call(this); |
| |
| this.dropType = 0; |
| this.dropMaximumSize = scout.dragAndDrop.DEFAULT_DROP_MAXIMUM_SIZE; |
| this._fileUploadWaitRetryCountTimeout = 99; |
| this._fullSelectionLength = 0; |
| }; |
| scout.inherits(scout.ClipboardField, scout.ValueField); |
| |
| // Keys that don't alter the content of a text field and are therefore always allowed in the clipboard field |
| scout.ClipboardField.NON_DESTRUCTIVE_KEYS = [ |
| // Default form handling |
| scout.keys.ESC, |
| scout.keys.ENTER, |
| scout.keys.TAB, |
| // Navigate and mark text |
| scout.keys.PAGE_UP, |
| scout.keys.PAGE_DOWN, |
| scout.keys.END, |
| scout.keys.HOME, |
| scout.keys.LEFT, |
| scout.keys.UP, |
| scout.keys.RIGHT, |
| scout.keys.DOWN, |
| // Browser hotkeys (e.g. developer tools) |
| scout.keys.F1, |
| scout.keys.F2, |
| scout.keys.F3, |
| scout.keys.F4, |
| scout.keys.F5, |
| scout.keys.F6, |
| scout.keys.F7, |
| scout.keys.F8, |
| scout.keys.F9, |
| scout.keys.F10, |
| scout.keys.F11, |
| scout.keys.F12 |
| ]; |
| |
| // Keys that always alter the content of a text field, independent from the modifier keys |
| scout.ClipboardField.ALWAYS_DESTRUCTIVE_KEYS = [ |
| scout.keys.BACKSPACE, |
| scout.keys.DELETE |
| ]; |
| |
| /** |
| * @override Widget.js |
| */ |
| scout.ClipboardField.prototype._createKeyStrokeContext = function() { |
| return new scout.InputFieldKeyStrokeContext(); |
| }; |
| |
| scout.ClipboardField.prototype._render = function() { |
| // We don't use makeDiv() here intentionally because the DIV created must |
| // not have the 'unselectable' attribute. Otherwise clipboard-field will |
| // not work in IE9. |
| this.addContainer(this.$parent, 'clipboard-field'); |
| this.addLabel(); |
| this.addFieldContainer(this.$parent.makeDiv()); |
| this.htmlField = scout.HtmlComponent.install(this.$fieldContainer, this.session); |
| this.htmlField.setLayout(new scout.ClipboardFieldLayout(this)); |
| this.addField(this.$fieldContainer.appendDiv().addClass('input-field')); |
| this.copyButton = scout.create('CopyButton', { |
| parent: this, |
| processButton: false, |
| $input: this.$field |
| }); |
| this.copyButton.render(this.$fieldContainer); |
| this.addStatus(); |
| |
| this.$field |
| .disableSpellcheck() |
| .attr('contenteditable', this._shouldBeContentEditable()) |
| .attr('tabindex', '0') |
| .on('keydown', this._onKeyDown.bind(this)) |
| .on('input', this._onInput.bind(this)) |
| .on('paste', this._onPaste.bind(this)) |
| .on('copy', this._onCopy.bind(this)) |
| .on('cut', this._onCopy.bind(this)); |
| |
| this.$parent.on('click', function(event) { |
| // this.focus(); // FIXME CGU what is this for |
| }.bind(this)); |
| }; |
| |
| scout.ClipboardField.prototype._remove = function() { |
| scout.ClipboardField.parent.prototype._remove.call(this); |
| this.copyButton.destroy(); |
| }; |
| |
| scout.ClipboardField.prototype._renderProperties = function() { |
| scout.ClipboardField.parent.prototype._renderProperties.call(this); |
| this._renderDropType(); |
| }; |
| |
| /** |
| * @override |
| */ |
| scout.ClipboardField.prototype.focus = function() { |
| if (!this.rendered) { |
| return; |
| } |
| // The copy button should be focused by default |
| this.copyButton.focus(); |
| }; |
| |
| scout.ClipboardField.prototype._shouldBeContentEditable = function() { |
| // On iOS the on screen keyboard pops up which is very annoying. |
| // Drawback is that 'Select all' button disappears and text selection is not limited to the field anymore. |
| // But since the user can use the copy button this should not be a real problem. |
| return !scout.device.isIos(); |
| }; |
| |
| scout.ClipboardField.prototype._createDragAndDropHandler = function() { |
| return scout.dragAndDrop.handler(this, { |
| supportedScoutTypes: scout.dragAndDrop.SCOUT_TYPES.FILE_TRANSFER, |
| dropType: function() { |
| return this.dropType; |
| }.bind(this), |
| dropMaximumSize: function() { |
| return this.maximumSize; |
| }.bind(this), |
| allowedTypes: function() { |
| return this.allowedMimeTypes; |
| }.bind(this) |
| }); |
| }; |
| |
| scout.ClipboardField.prototype._renderDisplayText = function() { |
| var displayText = this.displayText; |
| var img; |
| this.$field.children().each(function(idx, elem) { |
| if (!img && elem.nodeName === 'IMG') { |
| img = elem; |
| } |
| }); |
| |
| if (scout.strings.hasText(displayText)) { |
| this.$field.html(scout.strings.nl2br(displayText, true)); |
| scout.scrollbars.install(this.$field, { |
| parent: this |
| }); |
| |
| setTimeout(function() { |
| this.$field.selectAllText(); |
| // store length of full selection, in order to determine if the whole text is selected in "onCopy" |
| var selection = this._getSelection(); |
| this._fullSelectionLength = (selection) ? selection.toString().length : 0; |
| }.bind(this)); |
| } else { |
| this.$field.empty(); |
| } |
| // restore old img for firefox upload mechanism. |
| if (img) { |
| this.$field.prepend(img); |
| } |
| }; |
| |
| // Because a <div> is used as field, jQuery's val() used in ValueField.js is not working here, so |
| // the content of displayText variable is used instead. |
| // (For reading the displayText innerHmtl() _could_ be used on the div-field, but some browsers |
| // would collapse whitespaces which would also collapse multiple tabs when coping some table rows. |
| // So instead of reading the effective displayText from the field, the internal displayText value |
| // will be reused without actual reading. Parsing of pasted content is handled onPaste() and stored |
| // in this.displayText.) |
| scout.ClipboardField.prototype._readDisplayText = function() { |
| return this.displayText; |
| }; |
| |
| scout.ClipboardField.prototype._getSelection = function() { |
| var selection, myWindow = this.$container.window(true); |
| if (myWindow.getSelection) { |
| selection = myWindow.getSelection(); |
| } else if (document.getSelection) { |
| selection = document.getSelection(); |
| } |
| if (!selection || selection.toString().length === 0) { |
| return null; |
| } |
| return selection; |
| }; |
| |
| scout.ClipboardField.prototype._onKeyDown = function(event) { |
| if (scout.isOneOf(event.which, scout.ClipboardField.ALWAYS_DESTRUCTIVE_KEYS)) { |
| return false; // never allowed |
| } |
| if (event.ctrlKey || event.altKey || event.metaKey || scout.isOneOf(event.which, scout.ClipboardField.NON_DESTRUCTIVE_KEYS)) { |
| return; // allow bubble to other event handlers |
| } |
| // do not allow to enter something manually |
| return false; |
| }; |
| |
| scout.ClipboardField.prototype._onInput = function(event) { |
| // if the user somehow managed to fire to input something (e.g. "delete" menu in FF & IE), just reset the value to the previous content |
| this._renderDisplayText(); |
| return false; |
| }; |
| |
| scout.ClipboardField.prototype._onCopy = function(event) { |
| if (scout.device.isIos() && this._onIosCopy(event) === false) { |
| return; |
| } |
| |
| var selection, text, dataTransfer, myWindow = this.$container.window(true); |
| try { |
| if (event.originalEvent.clipboardData) { |
| dataTransfer = event.originalEvent.clipboardData; |
| } else if (myWindow.clipboardData) { |
| dataTransfer = myWindow.clipboardData; |
| } |
| } catch (e) { |
| // Because windows forbids concurrent access to the clipboard, a possible exception is thrown on 'myWindow.clipboardData' |
| // (see Remarks on https://msdn.microsoft.com/en-us/library/windows/desktop/ms649048(v=vs.85).aspx) |
| // Because of this behavior a failed access will just be logged but not presented to the user. |
| $.log.error('Error while reading "clipboardData"', e); |
| } |
| if (!dataTransfer) { |
| $.log.error('Unable to access clipboard data.'); |
| return false; |
| } |
| |
| // scroll bar must not be in field when copying |
| scout.scrollbars.uninstall(this.$field, this.session); |
| |
| selection = this._getSelection(); |
| if (!selection) { |
| return; |
| } |
| |
| // if the length of the selection is equals to the length of the (initial) full selection |
| // use the internal 'displayText' value because some browsers are collapsing white spaces |
| // which lead to problems when coping data form tables with empty cells ("\t\t"). |
| if (selection.toString().length === this._fullSelectionLength) { |
| text = this.displayText; |
| } else { |
| text = selection.toString(); |
| } |
| |
| try { |
| // Chrome, Firefox - causes an exception in IE |
| dataTransfer.setData('text/plain', text); |
| } catch (e) { |
| // IE, see https://www.lucidchart.com/techblog/2014/12/02/definitive-guide-copying-pasting-javascript/ |
| dataTransfer.setData('Text', text); |
| } |
| |
| // (re)install scroll bars |
| scout.scrollbars.install(this.$field, { |
| parent: this |
| }); |
| |
| return false; |
| }; |
| |
| scout.ClipboardField.prototype._onIosCopy = function(event) { |
| // Event.preventDefault() does not work with iOS, so setting a custom text is not possible |
| // The default behavior copies rich text. Since it is not expected to copy the style of the clipboard field, temporarily set color and background-color |
| // https://bugs.webkit.org/show_bug.cgi?id=176980 |
| var oldStyle = this.$field.attr('style'); |
| this.$field.css({ |
| 'color': '#000', |
| 'background-color': 'transparent' |
| }); |
| setTimeout(function() { |
| this.$field.attrOrRemove('style', oldStyle); |
| }.bind(this)); |
| return false; |
| }; |
| |
| scout.ClipboardField.prototype._onPaste = function(event) { |
| if (this.readOnly) { |
| // Prevent pasting in "copy" mode |
| return false; |
| } |
| |
| var startPasteTimestamp = Date.now(); |
| var dataTransfer, myWindow = this.$container.window(true); |
| this.$field.selectAllText(); |
| if (event.originalEvent.clipboardData) { |
| dataTransfer = event.originalEvent.clipboardData; |
| } else if (myWindow.clipboardData) { |
| dataTransfer = myWindow.clipboardData; |
| } else { |
| // unable to obtain data transfer object |
| throw new Error('Unable to access clipboard data.'); |
| } |
| |
| var filesArgument = [], // options to be uploaded, arguments for this.session.uploadFiles |
| additionalOptions = {}, |
| additionalOptionsCompatibilityIndex = 0, // counter for additional options |
| contentCount = 0; |
| |
| // some browsers (e.g. IE) specify text content simply as data of type 'Text', it is not listed in list of types |
| var textContent = dataTransfer.getData('Text'); |
| if (textContent) { |
| if (window.Blob) { |
| filesArgument.push(new Blob([textContent], { |
| type: scout.mimeTypes.TEXT_PLAIN |
| })); |
| contentCount++; |
| } else { |
| // compatibility workaround |
| additionalOptions['textTransferObject' + additionalOptionsCompatibilityIndex++] = textContent; |
| contentCount++; |
| } |
| } |
| |
| if (contentCount === 0 && dataTransfer.items) { |
| Array.prototype.forEach.call(dataTransfer.items, function(item) { |
| if (item.type === scout.mimeTypes.TEXT_PLAIN) { |
| item.getAsString(function(str) { |
| filesArgument.push(new Blob([str], { |
| type: scout.mimeTypes.TEXT_PLAIN |
| })); |
| contentCount++; |
| }); |
| } else if (scout.isOneOf(item.type, [scout.mimeTypes.IMAGE_PNG, scout.mimeTypes.IMAGE_JPG, scout.mimeTypes.IMAGE_JPEG, scout.mimeTypes.IMAGE_GIF])) { |
| var file = item.getAsFile(); |
| if (file) { |
| // When pasting an image from the clipboard, Chrome and Firefox create a File object with |
| // a generic name such as "image.png" or "grafik.png" (hardcoded in Chrome, locale-dependent |
| // in FF). It is therefore not possible to distinguish between a real file and a bitmap |
| // from the clipboard. The following code measures the time between the start of the paste |
| // event and the file's last modified timestamp. If it is "very small", the file is likely |
| // a bitmap from the clipbaord and not a real file. In that case, add a special "scoutName" |
| // attribute to the file object that is then used as a filename in session.uploadFiles(). |
| var lastModifiedDiff = startPasteTimestamp - file.lastModified; |
| if (lastModifiedDiff < 1000) { |
| file.scoutName = ''; |
| } |
| filesArgument.push(file); |
| contentCount++; |
| } |
| } |
| }); |
| } |
| |
| var waitForFileReaderEvents = 0; |
| if (contentCount === 0 && dataTransfer.files) { |
| Array.prototype.forEach.call(dataTransfer.files, function(item) { |
| var reader = new FileReader(); |
| // register functions for file reader |
| reader.onload = function(event) { |
| var f = new Blob([event.target.result], { |
| type: item.type |
| }); |
| f.name = item.name; |
| filesArgument.push(f); |
| waitForFileReaderEvents--; |
| }; |
| reader.onerror = function(event) { |
| waitForFileReaderEvents--; |
| $.log.error('Error while reading file ' + item.name + ' / ' + event.target.error.code); |
| }; |
| // start file reader |
| waitForFileReaderEvents++; |
| contentCount++; |
| reader.readAsArrayBuffer(item); |
| }); |
| } |
| |
| // upload function needs to be called asynchronously to support real files |
| var uploadFunctionTimeoutCount = 0; |
| var uploadFunction = function() { |
| if (waitForFileReaderEvents !== 0 && uploadFunctionTimeoutCount++ !== this._fileUploadWaitRetryCountTimeout) { |
| setTimeout(uploadFunction, 150); |
| return; |
| } |
| |
| if (uploadFunctionTimeoutCount >= this._fileUploadWaitRetryCountTimeout) { |
| var boxOptions = { |
| entryPoint: this.$container.entryPoint(), |
| header: this.session.text('ui.ClipboardTimeoutTitle'), |
| body: this.session.text('ui.ClipboardTimeout'), |
| yesButtonText: this.session.text('Ok') |
| }; |
| |
| this.session.showFatalMessage(boxOptions); |
| return; |
| } |
| |
| // upload paste event as files |
| if (filesArgument.length > 0 || Object.keys(additionalOptions).length > 0) { |
| this.session.uploadFiles(this, filesArgument, additionalOptions, this.maximumSize, this.allowedMimeTypes); |
| } |
| }.bind(this); |
| |
| // upload content function, if content can not be read from event |
| // (e.g. "Allow programmatic clipboard access" is disabled in IE) |
| var uploadContentFunction = function() { |
| // store old inner html (will be replaced) |
| scout.scrollbars.uninstall(this.$field, this.session); |
| var oldHtmlContent = this.$field.html(); |
| this.$field.html(''); |
| var restoreOldHtmlContent = function() { |
| this.$field.html(oldHtmlContent); |
| scout.scrollbars.install(this.$field, { |
| parent: this |
| }); |
| }.bind(this); |
| setTimeout(function() { |
| var imgElementsFound = false; |
| this.$field.children().each(function(idx, elem) { |
| if (elem.nodeName === 'IMG') { |
| var srcAttr = $(elem).attr('src'); |
| var srcDataMatch = /^data:(.*);base64,(.*)/.exec(srcAttr); |
| var mimeType = srcDataMatch && srcDataMatch[1]; |
| if (scout.isOneOf(mimeType, scout.mimeTypes.IMAGE_PNG, scout.mimeTypes.IMAGE_JPG, scout.mimeTypes.IMAGE_JPEG, scout.mimeTypes.IMAGE_GIF)) { |
| var encData = window.atob(srcDataMatch[2]); // base64 decode |
| var byteNumbers = []; |
| for (var i = 0; i < encData.length; i++) { |
| byteNumbers[i] = encData.charCodeAt(i); |
| } |
| var byteArray = new Uint8Array(byteNumbers); |
| var f = new Blob([byteArray], { |
| type: mimeType |
| }); |
| f.name = ''; |
| filesArgument.push(f); |
| imgElementsFound = true; |
| } |
| } |
| }); |
| if (imgElementsFound) { |
| restoreOldHtmlContent(); |
| } else { |
| // try to read nativly pasted text from field |
| var nativePasteContent = this.$field.text(); |
| if (scout.strings.hasText(nativePasteContent)) { |
| this.setDisplayText(nativePasteContent); |
| filesArgument.push(new Blob([nativePasteContent], { |
| type: scout.mimeTypes.TEXT_PLAIN |
| })); |
| } else { |
| restoreOldHtmlContent(); |
| } |
| } |
| uploadFunction(); |
| }.bind(this), 0); |
| }.bind(this); |
| |
| if (contentCount > 0) { |
| uploadFunction(); |
| |
| // do not trigger any other actions |
| return false; |
| } else { |
| uploadContentFunction(); |
| |
| // trigger other actions to catch content |
| return true; |
| } |
| }; |