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 @@