diff --git a/app/assets/stylesheets/content/work_packages/single_view/_attachments.sass b/app/assets/stylesheets/content/work_packages/single_view/_attachments.sass
index 4a159274c8..823abda58d 100644
--- a/app/assets/stylesheets/content/work_packages/single_view/_attachments.sass
+++ b/app/assets/stylesheets/content/work_packages/single_view/_attachments.sass
@@ -83,3 +83,6 @@
.add-another-file
.button
margin-bottom: 0
+
+.work-package--attachments--draggable-item
+ cursor: pointer
diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml
index 4260ff5077..91fdc6c6e2 100644
--- a/config/locales/js-en.yml
+++ b/config/locales/js-en.yml
@@ -31,6 +31,12 @@ en:
ajax:
hide: "Hide"
loading: "Loading ..."
+
+ attachments:
+ draggable_hint: |
+ Drag on editor field to inline image or reference attachment. Closed editor fields will be opened while you keep dragging.
+
+
autocomplete_select:
placeholder:
multi: "Add \"%{name}\""
diff --git a/frontend/src/app/components/attachments/attachment-list/attachment-list-item.component.ts b/frontend/src/app/components/attachments/attachment-list/attachment-list-item.component.ts
index 5d5e479a51..9f478f7039 100644
--- a/frontend/src/app/components/attachments/attachment-list/attachment-list-item.component.ts
+++ b/frontend/src/app/components/attachments/attachment-list/attachment-list-item.component.ts
@@ -43,7 +43,10 @@ export class AttachmentListItemComponent {
@Input() public index:any;
@Input() public selfDestroy?:boolean;
+ static imageFileExtensions:string[] = ['jpeg', 'jpg', 'gif', 'bmp', 'png'];
+
public text = {
+ dragHint: this.I18n.t('js.attachments.draggable_hint'),
destroyConfirmation: this.I18n.t('js.text_attachment_destroy_confirmation'),
removeFile: (arg:any) => this.I18n.t('js.label_remove_file', arg)
};
@@ -54,8 +57,48 @@ export class AttachmentListItemComponent {
readonly pathHelper:PathHelperService) {
}
+ /**
+ * Set the appropriate data for drag & drop of an attachment item.
+ * @param evt DragEvent
+ */
+ public setDragData(evt:DragEvent) {
+ const url = this.downloadPath;
+ const previewElement = this.draggableHTML(url);
+
+ evt.dataTransfer.setData("text/plain", url);
+ evt.dataTransfer.setData("text/html", previewElement.outerHTML);
+ evt.dataTransfer.setData("text/uri-list", url);
+ evt.dataTransfer.setDragImage(previewElement, 0, 0);
+ }
+
+ public draggableHTML(url:string) {
+ let el;
+
+ if (this.isImage) {
+ el = document.createElement('img')
+ el.src = url;
+ el.textContent = this.fileName;
+ } else {
+ el = document.createElement('a');
+ el.href = url;
+ el.textContent = this.fileName;
+ }
+
+ return el;
+ }
+
public get downloadPath() {
- return this.pathHelper.attachmentDownloadPath(this.attachment.id, this.attachment.name);
+ return this.pathHelper.attachmentDownloadPath(this.attachment.id, this.fileName);
+ }
+
+ public get isImage() {
+ const ext = this.fileName.split('.').pop() || '';
+ return AttachmentListItemComponent.imageFileExtensions.indexOf(ext.toLowerCase()) > -1;
+ }
+
+ public get fileName() {
+ const a = this.attachment;
+ return a.fileName || a.customName || a.name;
}
public confirmRemoveAttachment($event:JQueryEventObject) {
diff --git a/frontend/src/app/components/attachments/attachment-list/attachment-list-item.html b/frontend/src/app/components/attachments/attachment-list/attachment-list-item.html
index cb51512e54..92e24402c3 100644
--- a/frontend/src/app/components/attachments/attachment-list/attachment-list-item.html
+++ b/frontend/src/app/components/attachments/attachment-list/attachment-list-item.html
@@ -1,4 +1,9 @@
-
+
diff --git a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts b/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts
index 2ff4b182a4..99f4c8c597 100644
--- a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts
+++ b/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts
@@ -148,7 +148,7 @@ export class WorkPackageEditFieldGroupComponent implements OnInit, OnDestroy {
(this.editMode && !this.skipField(field) || this.form.activeFields[field.fieldName])
if (shouldActivate) {
- field.activateOnForm(this.form, true);
+ field.activateOnForm(true);
} else {
this.states.workPackages
.get(this.workPackage.id)
diff --git a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts b/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts
index a324105501..1f303bfe50 100644
--- a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts
+++ b/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts
@@ -30,13 +30,11 @@ import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-r
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {WorkPackageNotificationService} from '../wp-notification.service';
import {States} from '../../states.service';
-import {WorkPackageEditForm} from '../../wp-edit-form/work-package-edit-form';
import {
displayClassName,
DisplayFieldRenderer,
editFieldContainerClass
} from '../../wp-edit-form/display-field-renderer';
-import {WorkPackageEditFieldHandler} from '../../wp-edit-form/work-package-edit-field-handler';
import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service';
import {SelectionHelpers} from '../../../helpers/selection-helpers';
import {debugLog} from '../../../helpers/debug_output';
@@ -59,6 +57,7 @@ export class WorkPackageEditFieldComponent implements OnInit {
@Input('workPackageId') public workPackageId:string;
@Input('wrapperClasses') public wrapperClasses?:string;
@Input('displayPlaceholder') public displayPlaceholder?:string;
+ @Input('isDropTarget') public isDropTarget?:boolean = false;
@ViewChild('displayContainer') readonly displayContainer:ElementRef;
@ViewChild('editContainer') readonly editContainer:ElementRef;
@@ -89,6 +88,17 @@ export class WorkPackageEditFieldComponent implements OnInit {
this.wpEditFieldGroup.register(this);
}
+ // Open the field when its closed and relay drag & drop events to it.
+ public startDragOverActivation(event:JQueryEventObject) {
+ if (!this.isDropTarget || this.active) {
+ return true;
+ }
+
+ this.handleUserActivate(null);
+ event.preventDefault();
+ return false;
+ }
+
public render() {
const el = this.fieldRenderer.render(this.resource, this.fieldName, null, this.displayPlaceholder);
this.displayContainer.nativeElement.innerHTML = '';
@@ -140,10 +150,10 @@ export class WorkPackageEditFieldComponent implements OnInit {
return false;
}
- public activateOnForm(form:WorkPackageEditForm, noWarnings:boolean = false) {
+ public activateOnForm(noWarnings:boolean = false) {
// Activate the field
this.active = true;
- return form
+ return this.wpEditFieldGroup.form
.activate(this.fieldName, noWarnings)
.catch(() => this.deactivate(true));
}
@@ -156,7 +166,7 @@ export class WorkPackageEditFieldComponent implements OnInit {
positionOffset = ClickPositionMapper.getPosition(evt);
}
- this.activateOnForm(this.wpEditFieldGroup.form)
+ this.activateOnForm()
.then((handler) => {
handler.focus(positionOffset);
});
diff --git a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.html b/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.html
index 904e441cf7..d015619967 100644
--- a/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.html
+++ b/frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.html
@@ -3,7 +3,8 @@
fieldName,
active && '-active' || '',
wrapperClasses || '-small'
- ]">
+ ]"
+ (dragover)="startDragOverActivation($event)">
diff --git a/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html b/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html
index b317e36ce7..d9b98d7170 100644
--- a/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html
+++ b/frontend/src/app/components/wp-form-group/wp-attribute-group.template.html
@@ -24,6 +24,7 @@