Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions eclipse-scout-core/src/form/fields/clipboardfield/ClipboardField.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {arrays, BlobWithName, ClipboardFieldModel, Device, DragAndDropOptions, FatalMessageOptions, InputFieldKeyStrokeContext, keys, KeyStrokeContext, mimeTypes, scout, Session, strings, ValueField} from '../../../index';
import {arrays, BlobWithName, ClipboardFieldModel, Device, DragAndDropOptions, FatalMessageOptions, keys, mimeTypes, scout, Session, strings, ValueField} from '../../../index';
import $ from 'jquery';

export class ClipboardField extends ValueField<string> implements ClipboardFieldModel {
Expand All @@ -32,7 +32,6 @@ export class ClipboardField extends ValueField<string> implements ClipboardField
static NON_DESTRUCTIVE_KEYS = [
// Default form handling
keys.ESC,
keys.ENTER,
keys.TAB,
// Navigate and mark text
keys.PAGE_UP,
Expand All @@ -58,16 +57,12 @@ export class ClipboardField extends ValueField<string> implements ClipboardField
keys.F12
];

// Keys that always alter the content of a text field, independent from the modifier keys
// Keys that always alter the content of a text field, independent of the modifier keys
static ALWAYS_DESTRUCTIVE_KEYS = [
keys.BACKSPACE,
keys.DELETE
];

protected override _createKeyStrokeContext(): KeyStrokeContext {
return new InputFieldKeyStrokeContext();
}

protected override _render() {
// We don't use makeDiv() here intentionally because the DIV created must
// not have the 'unselectable' attribute.
Expand Down Expand Up @@ -137,19 +132,28 @@ export class ClipboardField extends ValueField<string> implements ClipboardField
return selection;
}

protected _onKeyDown(event: JQuery.KeyDownEvent): boolean {
protected _onKeyDown(event: JQuery.KeyDownEvent) {
if (this._isDestructiveKey(event)) {
// do not allow to enter something manually, but allow the event to bubble up to the form
event.preventDefault();
}
}

/**
* Returns true if the given keystroke would change the value of the field, false otherwise.
*/
protected _isDestructiveKey(event: JQuery.KeyboardEventBase): boolean {
if (scout.isOneOf(event.which, ClipboardField.ALWAYS_DESTRUCTIVE_KEYS)) {
return false; // never allowed
return true;
}
if (event.ctrlKey || event.altKey || event.metaKey || scout.isOneOf(event.which, ClipboardField.NON_DESTRUCTIVE_KEYS)) {
return; // allow bubble to other event handlers
return false;
}
// do not allow to enter something manually
return false;
return true;
}

protected _onInput(event: JQuery.TriggeredEvent): boolean {
// 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
// if the user somehow managed to fire to input something (e.g. "delete" action in context menu or touch bar actions in safari), just reset the value to the previous content
this._renderDisplayText();
return false;
}
Expand Down Expand Up @@ -433,4 +437,58 @@ export class ClipboardField extends ValueField<string> implements ClipboardField
arrays.remove(files, jpgImage);
}
}

/**
* Returns the plain text of the selected content. If the field is not rendered or the field contains
* no selection, `null` is returned. If the selection starts or ends outside the field, only the part
* contained within the field is returned.
*/
getSelectedText(): string {
let field = this.$field?.[0];
let document = field?.ownerDocument;
if (!field || !document) {
return null;
}

let selection = document.getSelection();
if (!selection?.rangeCount) {
return null;
}

let selectedRange = selection.getRangeAt(0);

// Ignore selection if it is completely outside the field
let fieldRange = document.createRange();
fieldRange.selectNodeContents(field);
if (selectedRange.compareBoundaryPoints(Range.END_TO_START, fieldRange) > 0) {
return null; // end of selectedRange is before start of fieldRange
}
if (selectedRange.compareBoundaryPoints(Range.START_TO_END, fieldRange) < 0) {
return null; // start of selectedRange is after end of fieldRange
}

// Build a new range consisting of only the selected part withing field
let selectedFieldRange = selectedRange.cloneRange();
if (selectedRange.compareBoundaryPoints(Range.START_TO_START, fieldRange) < 0) {
// start of selectedRange is before start of fieldRange -> truncate to fieldRange
selectedFieldRange.setStart(fieldRange.startContainer, fieldRange.startOffset);
}
if (selectedRange.compareBoundaryPoints(Range.END_TO_END, fieldRange) > 0) {
// end of selectedRange is after end of fieldRange -> truncate to fieldRange
selectedFieldRange.setEnd(fieldRange.endContainer, fieldRange.endOffset);
}

// Range#toString only returns the content of text nodes, therefore newlines represented by <br> are missing,
// see https://dom.spec.whatwg.org/#dom-range-stringifier. To retain the newline characters, we create a temporary
// element with the cloned content and use to 'innerText' property to convert it to plain text. Because <br> only
// has an effect when rendered, we have to insert the temporary element in the DOM tree. Hiding the element is
// not necessary, since we remove it again immediately.
let tmp = document.createElement('div');
tmp.appendChild(selectedFieldRange.cloneContents());
document.body.appendChild(tmp);
let text = tmp.innerText;
tmp.remove();

return text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {Button, clipboard, ClipboardField} from '../../../index';

/**
* Finds a field of type {@link ClipboardField} on the surrounding form and copies its content to the clipboard.
* Intended to be used as a model variant on the ClipboardForm.
*/
export class ClipboardFormCopyAndCloseButton extends Button {

protected override _doAction() {
this._copyTextToClipboard();
super._doAction();
}

protected _copyTextToClipboard() {
let clipboardField = this.getForm()?.findChild(ClipboardField);
let textToCopy = clipboardField?.getSelectedText() || clipboardField?.displayText;
if (textToCopy) {
clipboard.copyText({
parent: this,
text: textToCopy
});
}
}
}
1 change: 1 addition & 0 deletions eclipse-scout-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ export * from './form/fields/checkbox/CheckBoxToggleKeyStroke';
export * from './form/fields/clipboardfield/ClipboardField';
export * from './form/fields/clipboardfield/ClipboardFieldModel';
export * from './form/fields/clipboardfield/ClipboardFieldAdapter';
export * from './form/fields/clipboardfield/ClipboardFormCopyAndCloseButton';
export * from './form/fields/colorfield/ColorField';
export * from './form/fields/colorfield/ColorFieldAdapter';
export * from './form/fields/datefield/DatePredictionFailedStatus';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2026 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -15,16 +15,17 @@
import org.eclipse.scout.rt.client.dto.FormData;
import org.eclipse.scout.rt.client.dto.FormData.DefaultSubtypeSdkCommand;
import org.eclipse.scout.rt.client.dto.FormData.SdkCommand;
import org.eclipse.scout.rt.client.ui.action.keystroke.AbstractKeyStroke;
import org.eclipse.scout.rt.client.ui.action.keystroke.IKeyStroke;
import org.eclipse.scout.rt.client.ui.form.AbstractForm;
import org.eclipse.scout.rt.client.ui.form.AbstractFormHandler;
import org.eclipse.scout.rt.client.ui.form.clipboard.ClipboardForm.MainBox.CancelButton;
import org.eclipse.scout.rt.client.ui.form.clipboard.ClipboardForm.MainBox.ClipboardField;
import org.eclipse.scout.rt.client.ui.form.clipboard.ClipboardForm.MainBox.ClipboardLabel;
import org.eclipse.scout.rt.client.ui.form.clipboard.ClipboardForm.MainBox.CopyAndCloseButton;
import org.eclipse.scout.rt.client.ui.form.clipboard.ClipboardForm.MainBox.OkButton;
import org.eclipse.scout.rt.client.ui.form.fields.ModelVariant;
import org.eclipse.scout.rt.client.ui.form.fields.button.AbstractCancelButton;
import org.eclipse.scout.rt.client.ui.form.fields.button.AbstractOkButton;
import org.eclipse.scout.rt.client.ui.form.fields.button.IButton;
import org.eclipse.scout.rt.client.ui.form.fields.clipboardfield.AbstractClipboardField;
import org.eclipse.scout.rt.client.ui.form.fields.groupbox.AbstractGroupBox;
import org.eclipse.scout.rt.client.ui.form.fields.labelfield.AbstractLabelField;
Expand All @@ -47,6 +48,11 @@ protected ClipboardForm(boolean callInitializer) {
super(callInitializer);
}

@Override
protected boolean getConfiguredInheritAccessibility() {
return false;
}

public MimeType[] getMimeTypes() {
return m_mimeTypes;
}
Expand All @@ -65,7 +71,8 @@ public void setMimeTypes(MimeType... mimeTypes) {

protected void checkOkButtonEnabled() {
boolean okButtonEnabled = getHandler() instanceof CopyHandler || getClipboardField().getValue() != null && !getClipboardField().getValue().isEmpty();
getOkButton().setEnabled(okButtonEnabled, true);
IButton okButton = getCopyAndCloseButton().isVisible() ? getCopyAndCloseButton() : getOkButton();
okButton.setEnabled(okButtonEnabled);
}

@Override
Expand All @@ -83,6 +90,10 @@ public void startPaste() {
start();
}

public MainBox getMainBox() {
return getFieldByClass(MainBox.class);
}

public ClipboardLabel getClipboardLabel() {
return getFieldByClass(ClipboardLabel.class);
}
Expand All @@ -91,6 +102,10 @@ public ClipboardField getClipboardField() {
return getFieldByClass(ClipboardField.class);
}

public CopyAndCloseButton getCopyAndCloseButton() {
return getFieldByClass(CopyAndCloseButton.class);
}

public OkButton getOkButton() {
return getFieldByClass(OkButton.class);
}
Expand All @@ -112,6 +127,16 @@ protected String getConfiguredBorderDecoration() {
return BORDER_DECORATION_EMPTY;
}

@Override
protected int getConfiguredHeightInPixel() {
return 340;
}

@Override
protected int getConfiguredWidthInPixel() {
return 730;
}

@Order(10)
@ClassId("7f2f4401-7bc8-45aa-9cf5-e23129aafd44")
public class ClipboardLabel extends AbstractLabelField {
Expand Down Expand Up @@ -140,22 +165,17 @@ protected boolean getConfiguredLabelVisible() {
protected boolean getConfiguredWrapText() {
return true;
}

@Override
protected boolean getConfiguredSelectable() {
return false;
}
}

@Order(20)
@ClassId("5c81521d-f16d-411a-85f1-c349d8b71a28")
public class ClipboardField extends AbstractClipboardField {

@Override
protected int getConfiguredHeightInPixel() {
return 190;
}

@Override
protected int getConfiguredWidthInPixel() {
return 705;
}

@Override
protected double getConfiguredGridWeightX() {
return 1;
Expand Down Expand Up @@ -187,6 +207,22 @@ protected void execChangedValue() {
}
}

@Order(29)
@ClassId("f869f3c0-02cf-4eaf-9369-36f0b72b6d2a")
@ModelVariant("ClipboardFormCopyAndClose")
public class CopyAndCloseButton extends AbstractOkButton {

@Override
protected void execInitField() {
setVisibleGranted(false); // only visible in "copy" mode (instead of the OkButton)
}

@Override
protected String getConfiguredLabel() {
return TEXTS.get("Copy");
}
}

@Order(30)
@ClassId("818d7930-126e-481e-ac47-b7e8654e8060")
public class OkButton extends AbstractOkButton {
Expand All @@ -196,46 +232,29 @@ public class OkButton extends AbstractOkButton {
@ClassId("a54fc3fc-ee5a-4351-b82f-3168e96c2f34")
public class CancelButton extends AbstractCancelButton {
}

/**
* Escape keyStroke is required in CopyHandler case, when form has no cancel-button but user should be able to close
* form with ESC anyway.
*/
@Order(50)
@ClassId("18fc9914-495f-4523-b75c-41259de828db")
public class EscapeKeyStroke extends AbstractKeyStroke {

@Override
protected String getConfiguredKeyStroke() {
return IKeyStroke.ESCAPE;
}

@Override
protected void execAction() {
doClose();
}
}
}

public class CopyHandler extends AbstractFormHandler {

@Override
protected void execLoad() {
// use setVisibleGranted here because we don't want to send the cancel-button (incl. ESC keyStroke) to the UI
super.execLoad();
setTitle(TEXTS.get("CopyToClipboard"));
getClipboardLabel().setValue(TEXTS.get("CopyToClipboardFromFieldBelow"));
getCancelButton().setVisibleGranted(false);
getOkButton().setLabel(TEXTS.get("CloseButton"));
checkOkButtonEnabled();
getClipboardLabel().setVisibleGranted(false);
// Switch to "copy" mode
getClipboardField().setReadOnly(true);
// Show copy button (with model variant) instead of normal ok button
getOkButton().setVisibleGranted(false);
getCopyAndCloseButton().setVisibleGranted(true);
checkOkButtonEnabled();
}
}

public class PasteHandler extends AbstractFormHandler {

@Override
protected void execLoad() {
super.execLoad();
setTitle(TEXTS.get("InsertFromClipboard"));
getClipboardLabel().setValue(TEXTS.get("PasteClipboardContentsInFieldBelow"));
checkOkButtonEnabled();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Configurations=Configurations
Configure=Configuration
ConfirmApplyChanges=Would you like to apply the changes?
Copy=Copy
CopyToClipboard=Copy to clipboard
CopyToClipboardFromFieldBelow=Please use the context menu or shortcut to copy field contents below to clipboard.
CorrelationId=Correlation ID
Criteria=Criteria
Expand Down Expand Up @@ -150,6 +151,7 @@ InactiveState=inactive
InactiveStates=Inactive
Info=Info
Information=Information
InsertFromClipboard=Insert from clipboard
InternalProcessingErrorMsg=An internal processing error has occurred{0}.
Interrupted=Canceled
InterruptedErrorText=The process was canceled.
Expand Down Expand Up @@ -259,7 +261,7 @@ PasswordUsernameNotPartOfPass=The username must not be part of the password.
PasswordWillExpireHeaderX=Your password expires {0}.
PasswordWillExpireInfo=Would you like to change it now?
PasswordsDoNotMatch=The two passwords do not match.
PasteClipboardContentsInFieldBelow=Please use the context menu, shortcut or drag and drop to paste clipboard contents into the field below.\r\nDepending on your browser text, image and file contents can be pasted.
PasteClipboardContentsInFieldBelow=Please use the context menu, shortcut or drag and drop to paste clipboard contents into the field below.
Path=Path
Pending=Waiting...
PleaseTryAgainLater=Please try again later.
Expand Down
Loading