blob: bb6ad273df3d4e7ca39e81185dd9e3f2b422d8d5 [file] [log] [blame]
/*******************************************************************************
* 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;
}
};