https://community.openproject.org/work_packages/22511 This adds drag and drop functionality for wiki-textareas concerning wp-attachments (mainly the description field) in workpackages. based on the use of our new directives by @furinvader as proposed in https://github.com/opf/openproject/pull/4471 Features: handles drop events on description fields of work packages and can distinguish between: - local files: initiates file upload (single file or upload queue) - external urls: become inserted as default weblink - external image files: become inserted as inline image - attachments from current work package: become inserted as attachment link - attachments (imagefiles) from current work package: become inserted as inline imagepull/4540/head
parent
2d617bfd0c
commit
d9a38f8b89
@ -0,0 +1,131 @@ |
||||
import {WpAttachmentsService} from './../wp-attachments/wp-attachments.service'; |
||||
import {WorkPackageResource} from './../../api/api-v3/hal-resources/work-package-resource.service' |
||||
import {InsertMode, ViewMode} from './wp-attachments-formattable.enums' |
||||
import {DropModel, EditorModel, MarkupModel, FieldModel, SingleAttachmentModel} from './wp-attachments-formattable.models' |
||||
|
||||
export class WpAttachmentsFormattableController { |
||||
private viewMode: ViewMode = ViewMode.SHOW; |
||||
|
||||
constructor(protected $scope: ng.IScope, |
||||
protected $element: ng.IAugmentedJQuery, |
||||
protected $rootScope: ng.IRootScopeService, |
||||
protected $location: ng.ILocationService, |
||||
protected wpAttachments: WpAttachmentsService, |
||||
protected $timeout: ng.ITimeoutService) { |
||||
|
||||
$element.get(0).addEventListener('drop', this.handleDrop); |
||||
$element.bind('dragenter', this.prevDefault) |
||||
.bind('dragleave', this.prevDefault) |
||||
.bind('dragover', this.prevDefault); |
||||
|
||||
} |
||||
|
||||
public handleDrop = (evt: DragEvent): void => { |
||||
evt.preventDefault(); |
||||
evt.stopPropagation(); |
||||
|
||||
const textarea: ng.IAugmentedJQuery = this.$element.find('textarea'); |
||||
this.viewMode = (textarea.length > 0) ? ViewMode.EDIT : ViewMode.SHOW; |
||||
|
||||
const workPackage: WorkPackageResource = this.$scope.workPackage; |
||||
const dropData: DropModel = new DropModel(this.$location, evt.dataTransfer, workPackage); |
||||
|
||||
var description: any; |
||||
|
||||
if (this.viewMode === ViewMode.EDIT) { |
||||
description = new EditorModel(textarea, new MarkupModel()); |
||||
} |
||||
else { |
||||
description = new FieldModel(workPackage, new MarkupModel()); |
||||
} |
||||
|
||||
if (angular.isUndefined(dropData.webLinkUrl) && angular.isUndefined(dropData.files)) |
||||
return; |
||||
|
||||
if(dropData.isUpload){ |
||||
if(dropData.filesAreValidForUploading()) { |
||||
if(!dropData.isDelayedUpload) { |
||||
this.wpAttachments.upload(workPackage, dropData.files).then(() => { |
||||
this.wpAttachments.load(workPackage,true).then((updatedAttachments: any) => { |
||||
if (angular.isUndefined(updatedAttachments)) |
||||
return; |
||||
|
||||
updatedAttachments.sort(function(a, b){ |
||||
return a.id > b.id ? 1 : -1; |
||||
}); |
||||
|
||||
if (dropData.filesCount === 1) { |
||||
const currentFile: SingleAttachmentModel = |
||||
new SingleAttachmentModel(updatedAttachments[updatedAttachments.length - 1]); |
||||
description.insertAttachmentLink( |
||||
currentFile.url, |
||||
(currentFile.isAnImage) ? InsertMode.INLINE : InsertMode.ATTACHMENT); |
||||
} |
||||
else if (dropData.filesCount > 1) { |
||||
for (let i: number = updatedAttachments.length - 1; |
||||
i >= updatedAttachments.length - dropData.filesCount; |
||||
i--) { |
||||
description.insertAttachmentLink( |
||||
updatedAttachments[i]._links.downloadLocation.href, |
||||
InsertMode.ATTACHMENT, |
||||
true); |
||||
} |
||||
} |
||||
description.save(); |
||||
},(err)=>{ |
||||
console.log("error while reloading attachments",err) |
||||
}); |
||||
}, function(err) { |
||||
console.log(err); |
||||
}); |
||||
} |
||||
else { |
||||
dropData.files.forEach((file: File) => { |
||||
description.insertAttachmentLink(file.name.replace(/ /g , '_'), InsertMode.ATTACHMENT, true); |
||||
file['isPending'] = true; |
||||
this.wpAttachments.addPendingAttachments(file); |
||||
}); |
||||
description.save(); |
||||
} |
||||
} |
||||
} |
||||
else { |
||||
const insertUrl: string = dropData.isAttachmentOfCurrentWp() ? dropData.removeHostInformationFromUrl() : dropData.webLinkUrl; |
||||
const insertAlternative: InsertMode = dropData.isWebImage() ? InsertMode.INLINE : InsertMode.LINK; |
||||
const insertMode: InsertMode = dropData.isAttachmentOfCurrentWp() ? InsertMode.ATTACHMENT : insertAlternative; |
||||
|
||||
description.insertWebLink(insertUrl, insertMode); |
||||
description.save(); |
||||
} |
||||
}; |
||||
|
||||
protected prevDefault(evt: DragEvent): void { |
||||
evt.preventDefault(); |
||||
evt.stopPropagation(); |
||||
} |
||||
} |
||||
|
||||
function wpAttachmentsFormattable() { |
||||
return { |
||||
bindToController: true, |
||||
controller: WpAttachmentsFormattableController, |
||||
link: function(scope: ng.IScope,
|
||||
element: ng.IAugmentedJQuery,
|
||||
attrs: ng.IAttributes,
|
||||
controllers: Array<ng.IControllerService>){ |
||||
// right now the attachments directive will only work in combination with either
|
||||
// the wpSingleView or the wpEditForm directive
|
||||
// else the drop handler will fail because of a missing reference to the current wp
|
||||
if(angular.isUndefined(controllers[0] && angular.isUndefined(controllers[1]))) |
||||
return; |
||||
|
||||
scope.workPackage = (controllers[0] === null) ? controllers[1].workPackage : controllers[0].workPackage; |
||||
}, |
||||
require: ['?^wpSingleView', '?^wpEditForm'], |
||||
restrict: 'A' |
||||
}; |
||||
} |
||||
|
||||
angular |
||||
.module('openproject') |
||||
.directive('wpAttachmentsFormattable', wpAttachmentsFormattable); |
@ -0,0 +1,14 @@ |
||||
export enum InsertMode { |
||||
ATTACHMENT, |
||||
DELAYED_ATTACHMENT, |
||||
INLINE, |
||||
LINK |
||||
} |
||||
|
||||
export enum ViewMode { |
||||
EDIT, |
||||
SHOW, |
||||
CREATE |
||||
} |
||||
|
||||
|
@ -0,0 +1,9 @@ |
||||
import {InsertMode} from './wp-attachments-formattable.enums' |
||||
|
||||
export interface IApplyAttachmentMarkup{ |
||||
contentToInsert: string; |
||||
|
||||
insertAttachmentLink: (url: string,insertMode: InsertMode, addLineBreak?:boolean) => void; |
||||
insertWebLink: (url: string,insertMode: InsertMode) => void; |
||||
save: () => void; |
||||
} |
@ -0,0 +1,184 @@ |
||||
import {IApplyAttachmentMarkup} from './wp-attachments-formattable.interfaces' |
||||
import {InsertMode} from './wp-attachments-formattable.enums' |
||||
import {WorkPackageResource} from "../../api/api-v3/hal-resources/work-package-resource.service"; |
||||
import IAugmentedJQuery = angular.IAugmentedJQuery; |
||||
|
||||
export class EditorModel implements IApplyAttachmentMarkup{ |
||||
private currentCaretPosition: number; |
||||
public contentToInsert:string = ""; |
||||
|
||||
constructor(protected textarea: IAugmentedJQuery, protected markupModel: MarkupModel){ |
||||
this.setCaretPosition(); |
||||
} |
||||
|
||||
public insertWebLink(url: string, insertMode: InsertMode = InsertMode.LINK): void { |
||||
this.contentToInsert = this.markupModel.createMarkup(url, insertMode); |
||||
}; |
||||
|
||||
public insertAttachmentLink(url: string, insertMode: InsertMode = InsertMode.ATTACHMENT, addLineBreak?: boolean): void { |
||||
this.contentToInsert = (addLineBreak) ? |
||||
this.contentToInsert + this.markupModel.createMarkup(url, insertMode, addLineBreak) : |
||||
this.markupModel.createMarkup(url, insertMode, addLineBreak); |
||||
}; |
||||
|
||||
private setCaretPosition(): void { |
||||
this.currentCaretPosition = (this.textarea[0] as HTMLTextAreaElement).selectionStart; |
||||
}; |
||||
|
||||
public save(): void { |
||||
this.textarea.val(this.textarea.val().substring(0, this.currentCaretPosition) + |
||||
this.contentToInsert + |
||||
this.textarea.val().substring(this.currentCaretPosition, this.textarea.val().length)).change(); |
||||
} |
||||
} |
||||
|
||||
export class MarkupModel{ |
||||
|
||||
public createMarkup(insertUrl: string, insertMode: InsertMode, addLineBreak?: boolean = false): string { |
||||
if (angular.isUndefined((insertUrl))) return ""; |
||||
|
||||
var markup:string = ""; |
||||
|
||||
switch (insertMode) { |
||||
case InsertMode.ATTACHMENT: |
||||
markup = "attachment:" + insertUrl.split("/").pop(); |
||||
break; |
||||
case InsertMode.DELAYED_ATTACHMENT: |
||||
markup = "attachment:" + insertUrl; |
||||
break; |
||||
case InsertMode.INLINE: |
||||
markup = "!" + insertUrl + "!"; |
||||
break; |
||||
case InsertMode.LINK: |
||||
markup += insertUrl; |
||||
break; |
||||
} |
||||
|
||||
if(addLineBreak) markup += "\r\n"; |
||||
return markup; |
||||
} |
||||
|
||||
} |
||||
|
||||
export class DropModel{ |
||||
public files: FileList; |
||||
public filesCount: number; |
||||
public isUpload: boolean; |
||||
public isDelayedUpload: boolean; |
||||
public isWebLink: boolean; |
||||
public webLinkUrl: string; |
||||
|
||||
protected config: any = { |
||||
imageFileTypes : ["jpg","jpeg","gif","png"], |
||||
maximumAttachmentFileSize : 0, // initialized during init process from ConfigurationService
|
||||
}; |
||||
|
||||
constructor(protected $location: ng.ILocationService, protected dt: DataTransfer, protected workPackage: WorkPackageResource){ |
||||
this.files = dt.files; |
||||
this.filesCount = this.files.length; |
||||
this.isUpload = this._isUpload(dt); |
||||
this.isDelayedUpload = this.workPackage.isNew; |
||||
this.isWebLink = ! this.isUpload; |
||||
this.webLinkUrl = dt.getData("URL"); |
||||
} |
||||
|
||||
public isWebImage(): boolean { |
||||
if(angular.isDefined(this.webLinkUrl)){ |
||||
return (this.config.imageFileTypes.indexOf(this.webLinkUrl.split(".").pop().toLowerCase()) > -1); |
||||
} |
||||
}; |
||||
|
||||
public isAttachmentOfCurrentWp():boolean { |
||||
if(this.isWebLink){ |
||||
|
||||
// weblink does not point to our server, so it can't be an attachment
|
||||
if(!(this.webLinkUrl.indexOf(this.$location.host()) > -1) ) return false; |
||||
|
||||
var isAttachment:boolean = false; |
||||
|
||||
this.workPackage.attachments.elements.forEach((attachment)=>{ |
||||
if(this.webLinkUrl.indexOf(attachment.href) > -1) { |
||||
isAttachment = true; |
||||
return; |
||||
} |
||||
}); |
||||
return isAttachment; |
||||
} |
||||
}; |
||||
|
||||
public filesAreValidForUploading(): boolean { |
||||
// needs: clarifying if rejected filetypes are a wanted feature
|
||||
// no filetypes are getting rejected yet
|
||||
var allFilesAreValid = true; |
||||
/*this.files.forEach((file)=>{ |
||||
if(file.size > this.config.maximumAttachmentFileSize) { |
||||
allFilesAreValid = false; |
||||
return; |
||||
} |
||||
});*/ |
||||
return allFilesAreValid; |
||||
}; |
||||
|
||||
public removeHostInformationFromUrl(): string { |
||||
return this.webLinkUrl.replace(window.location.origin, ""); |
||||
}; |
||||
|
||||
protected _isUpload(dt: DataTransfer): boolean { |
||||
if (dt.types && this.filesCount > 0) { |
||||
for (let i=0; i < dt.types.length; i++) { |
||||
if (dt.types[i] == "Files") { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
export class SingleAttachmentModel { |
||||
protected imageFileExtensions: Array<string> = ['jpeg','jpg','gif','bmp','png']; |
||||
|
||||
public fileExtension: string; |
||||
public fileName: string; |
||||
public isAnImage: boolean; |
||||
public url: string; |
||||
|
||||
|
||||
constructor(protected attachment: any){ |
||||
if(angular.isDefined(attachment)){ |
||||
this.fileName = attachment.fileName || attachment.name; |
||||
this.fileExtension = this.fileName.split(".").pop().toLowerCase(); |
||||
this.isAnImage = this.imageFileExtensions.indexOf(this.fileExtension) > -1; |
||||
this.url = angular.isDefined(attachment._links) ? attachment._links.downloadLocation.href : ''; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
export class FieldModel implements IApplyAttachmentMarkup { |
||||
public contentToInsert: string; |
||||
|
||||
constructor(protected workPackage: WorkPackageResource, protected markupModel: MarkupModel){ |
||||
this.contentToInsert = workPackage.description.raw || ""; |
||||
} |
||||
|
||||
|
||||
private addInitialLineBreak(): string { |
||||
return (this.contentToInsert.length > 0) ? "\r\n" : ""; |
||||
}; |
||||
|
||||
public insertAttachmentLink(url: string, insertMode: InsertMode, addLineBreak?: boolean): void { |
||||
this.contentToInsert += this.addInitialLineBreak() + this.markupModel.createMarkup(url,insertMode,false); |
||||
}; |
||||
|
||||
public insertWebLink(url: string, insertMode: InsertMode): void { |
||||
this.contentToInsert += this.addInitialLineBreak() + this.markupModel.createMarkup(url,insertMode,false); |
||||
}; |
||||
|
||||
public save(): void { |
||||
this.workPackage.description.raw = this.contentToInsert; |
||||
this.workPackage.save(); |
||||
}; |
||||
|
||||
} |
||||
|
Loading…
Reference in new issue