2022-12-16 16:36:47 +01:00
import { ReactiveCache } from '/imports/reactiveCache' ;
2022-09-16 19:52:48 +02:00
import { ObjectID } from 'bson' ;
2023-02-20 16:48:02 -05:00
import DOMPurify from 'dompurify' ;
2025-10-11 01:08:39 +03:00
import { sanitizeHTML , sanitizeText } from '/imports/lib/secureDOMPurify' ;
2025-10-11 00:49:43 +03:00
import uploadProgressManager from '../../lib/uploadProgressManager' ;
2025-10-12 03:48:21 +03:00
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager' ;
2022-09-16 19:52:48 +02:00
2022-07-16 09:32:49 +02:00
const filesize = require ( 'filesize' ) ;
const prettyMilliseconds = require ( 'pretty-ms' ) ;
2023-07-06 20:35:49 +02:00
// We store current card ID and the ID of currently opened attachment in a
// global var. This is used so that we know what's the next attachment to open
// when the user clicks on the prev/next button in the attachment viewer.
let cardId = null ;
let openAttachmentId = null ;
2023-08-14 15:38:13 +02:00
// Used to store the start and end coordinates of a touch event for attachment swiping
let touchStartCoords = null ;
let touchEndCoords = null ;
2023-07-10 13:19:19 +02:00
// Stores link to the attachment for which attachment actions popup was opened
2023-07-10 15:04:46 +03:00
attachmentActionsLink = null ;
2023-07-10 13:19:19 +02:00
2023-07-06 20:29:36 +02:00
Template . attachmentGallery . events ( {
2023-07-06 20:35:49 +02:00
'click .open-preview' ( event ) {
openAttachmentId = $ ( event . currentTarget ) . attr ( "data-attachment-id" ) ;
cardId = $ ( event . currentTarget ) . attr ( "data-card-id" ) ;
openAttachmentViewer ( openAttachmentId ) ;
2023-06-30 12:35:45 +02:00
} ,
2020-12-21 18:32:00 +02:00
'click .js-add-attachment' : Popup . open ( 'cardAttachments' ) ,
// If we let this event bubble, FlowRouter will handle it and empty the page
// content, see #101.
'click .js-download' ( event ) {
event . stopPropagation ( ) ;
} ,
2022-03-27 23:58:41 +02:00
'click .js-open-attachment-menu' : Popup . open ( 'attachmentActions' ) ,
2023-07-10 13:19:19 +02:00
'mouseover .js-open-attachment-menu' ( event ) { // For some reason I cannot combine handlers for "click .js-open-attachment-menu" and "mouseover .js-open-attachment-menu" events so this is a quick workaround.
attachmentActionsLink = event . currentTarget . getAttribute ( "data-attachment-link" ) ;
} ,
2023-07-06 20:29:36 +02:00
'click .js-rename' : Popup . open ( 'attachmentRename' ) ,
'click .js-confirm-delete' : Popup . afterConfirm ( 'attachmentDelete' , function ( ) {
2023-08-03 18:57:12 +02:00
Attachments . remove ( this . _id ) ;
2023-07-18 21:16:44 +02:00
Popup . back ( ) ;
2023-07-06 20:29:36 +02:00
} ) ,
} ) ;
2023-08-03 18:57:12 +02:00
function getNextAttachmentId ( currentAttachmentId , offset = 0 ) {
2023-02-26 11:24:32 +01:00
const attachments = ReactiveCache . getAttachments ( { 'meta.cardId' : cardId } ) ;
2023-07-06 20:35:49 +02:00
2023-08-03 18:57:12 +02:00
let i = 0 ;
for ( ; i < attachments . length ; i ++ ) {
if ( attachments [ i ] . _id === currentAttachmentId ) {
break ;
2023-07-06 20:35:49 +02:00
}
2023-08-03 18:57:12 +02:00
}
return attachments [ ( i + offset + 1 + attachments . length ) % attachments . length ] . _id ;
2023-07-06 20:35:49 +02:00
}
2023-08-03 18:57:12 +02:00
function getPrevAttachmentId ( currentAttachmentId , offset = 0 ) {
2023-02-26 11:24:32 +01:00
const attachments = ReactiveCache . getAttachments ( { 'meta.cardId' : cardId } ) ;
2023-07-06 20:35:49 +02:00
let i = 0 ;
for ( ; i < attachments . length ; i ++ ) {
if ( attachments [ i ] . _id === currentAttachmentId ) {
break ;
}
}
2023-08-03 18:57:12 +02:00
return attachments [ ( i + offset - 1 + attachments . length ) % attachments . length ] . _id ;
2023-07-06 20:35:49 +02:00
}
2023-08-03 18:57:12 +02:00
function attachmentCanBeOpened ( attachment ) {
return (
attachment . isImage ||
attachment . isPDF ||
attachment . isText ||
attachment . isJSON ||
attachment . isVideo ||
attachment . isAudio
) ;
}
2023-07-06 20:35:49 +02:00
2023-08-03 18:57:12 +02:00
function openAttachmentViewer ( attachmentId ) {
const attachment = ReactiveCache . getAttachment ( attachmentId ) ;
2023-07-06 20:35:49 +02:00
2023-08-03 18:57:12 +02:00
// Check if we can open the attachment (if we have a viewer for it) and exit if not
if ( ! attachmentCanBeOpened ( attachment ) ) {
return ;
}
2023-07-06 20:35:49 +02:00
2023-08-03 18:57:12 +02:00
/ *
Instructions for adding a new viewer :
- add a new case to the switch statement below
- implement cleanup in the closeAttachmentViewer ( ) function , if necessary
- mark attachment type as openable by adding a new condition to the attachmentCanBeOpened function
* /
switch ( true ) {
case ( attachment . isImage ) :
$ ( "#image-viewer" ) . attr ( "src" , attachment . link ( ) ) ;
$ ( "#image-viewer" ) . removeClass ( "hidden" ) ;
break ;
case ( attachment . isPDF ) :
$ ( "#pdf-viewer" ) . attr ( "data" , attachment . link ( ) ) ;
$ ( "#pdf-viewer" ) . removeClass ( "hidden" ) ;
break ;
case ( attachment . isVideo ) :
// We have to create a new <source> DOM element and append it to the video
// element, otherwise the video won't load
let videoSource = document . createElement ( 'source' ) ;
videoSource . setAttribute ( 'src' , attachment . link ( ) ) ;
$ ( "#video-viewer" ) . append ( videoSource ) ;
$ ( "#video-viewer" ) . removeClass ( "hidden" ) ;
break ;
case ( attachment . isAudio ) :
// We have to create a new <source> DOM element and append it to the audio
// element, otherwise the audio won't load
let audioSource = document . createElement ( 'source' ) ;
audioSource . setAttribute ( 'src' , attachment . link ( ) ) ;
$ ( "#audio-viewer" ) . append ( audioSource ) ;
$ ( "#audio-viewer" ) . removeClass ( "hidden" ) ;
break ;
case ( attachment . isText ) :
case ( attachment . isJSON ) :
$ ( "#txt-viewer" ) . attr ( "data" , attachment . link ( ) ) ;
$ ( "#txt-viewer" ) . removeClass ( "hidden" ) ;
break ;
}
2023-07-06 20:35:49 +02:00
2023-08-03 18:57:12 +02:00
$ ( '#attachment-name' ) . text ( attachment . name ) ;
$ ( '#viewer-overlay' ) . removeClass ( 'hidden' ) ;
2023-07-06 20:35:49 +02:00
}
function closeAttachmentViewer ( ) {
$ ( "#viewer-overlay" ) . addClass ( "hidden" ) ;
// We need to reset the viewers to avoid showing previous attachments
$ ( "#image-viewer" ) . attr ( "src" , "" ) ;
$ ( "#image-viewer" ) . addClass ( "hidden" ) ;
$ ( "#pdf-viewer" ) . attr ( "data" , "" ) ;
$ ( "#pdf-viewer" ) . addClass ( "hidden" ) ;
$ ( "#txt-viewer" ) . attr ( "data" , "" ) ;
$ ( "#txt-viewer" ) . addClass ( "hidden" ) ;
$ ( "#video-viewer" ) . get ( 0 ) . pause ( ) ; // Stop playback
$ ( "#video-viewer" ) . get ( 0 ) . currentTime = 0 ;
$ ( "#video-viewer" ) . empty ( ) ;
$ ( "#video-viewer" ) . addClass ( "hidden" ) ;
$ ( "#audio-viewer" ) . get ( 0 ) . pause ( ) ; // Stop playback
$ ( "#audio-viewer" ) . get ( 0 ) . currentTime = 0 ;
$ ( "#audio-viewer" ) . empty ( ) ;
$ ( "#audio-viewer" ) . addClass ( "hidden" ) ;
}
2023-08-14 15:38:13 +02:00
function openNextAttachment ( ) {
closeAttachmentViewer ( ) ;
2023-08-03 18:57:12 +02:00
let i = 0 ;
// Find an attachment that can be opened
while ( true ) {
const id = getNextAttachmentId ( openAttachmentId , i ) ;
const attachment = ReactiveCache . getAttachment ( id ) ;
if ( attachmentCanBeOpened ( attachment ) ) {
openAttachmentId = id ;
openAttachmentViewer ( id ) ;
break ;
}
i ++ ;
}
2023-08-14 15:38:13 +02:00
}
function openPrevAttachment ( ) {
closeAttachmentViewer ( ) ;
2023-08-03 18:57:12 +02:00
let i = 0 ;
// Find an attachment that can be opened
while ( true ) {
const id = getPrevAttachmentId ( openAttachmentId , i ) ;
const attachment = ReactiveCache . getAttachment ( id ) ;
if ( attachmentCanBeOpened ( attachment ) ) {
openAttachmentId = id ;
openAttachmentViewer ( id ) ;
break ;
}
i -- ;
}
2023-08-14 15:38:13 +02:00
}
function processTouch ( ) {
xDist = touchEndCoords . x - touchStartCoords . x ;
yDist = touchEndCoords . y - touchStartCoords . y ;
console . log ( "xDist: " + xDist ) ;
// Left swipe
if ( Math . abs ( xDist ) > Math . abs ( yDist ) && xDist < 0 ) {
openNextAttachment ( ) ;
}
// Right swipe
if ( Math . abs ( xDist ) > Math . abs ( yDist ) && xDist > 0 ) {
openPrevAttachment ( ) ;
2023-07-06 20:35:49 +02:00
}
2023-08-14 15:38:13 +02:00
// Up swipe
if ( Math . abs ( yDist ) > Math . abs ( xDist ) && yDist < 0 ) {
closeAttachmentViewer ( ) ;
}
}
Template . attachmentViewer . events ( {
'touchstart #viewer-container' ( event ) {
console . log ( "touchstart" )
touchStartCoords = {
x : event . changedTouches [ 0 ] . screenX ,
y : event . changedTouches [ 0 ] . screenY
}
} ,
'touchend #viewer-container' ( event ) {
console . log ( "touchend" )
touchEndCoords = {
x : event . changedTouches [ 0 ] . screenX ,
y : event . changedTouches [ 0 ] . screenY
}
processTouch ( ) ;
} ,
'click #viewer-container' ( event ) {
// Make sure the click was on #viewer-container and not on any of its children
2023-08-17 21:21:56 +02:00
if ( event . target !== event . currentTarget ) {
event . stopPropagation ( ) ;
return ;
}
2023-08-14 15:38:13 +02:00
closeAttachmentViewer ( ) ;
} ,
'click #viewer-content' ( event ) {
// Make sure the click was on #viewer-content and not on any of its children
2023-08-17 21:21:56 +02:00
if ( event . target !== event . currentTarget ) {
event . stopPropagation ( ) ;
return ;
}
2023-08-14 15:38:13 +02:00
closeAttachmentViewer ( ) ;
} ,
'click #viewer-close' ( ) {
closeAttachmentViewer ( ) ;
} ,
'click #next-attachment' ( ) {
openNextAttachment ( ) ;
} ,
'click #prev-attachment' ( ) {
openPrevAttachment ( ) ;
} ,
2020-12-21 18:32:00 +02:00
} ) ;
2020-12-17 21:27:02 +02:00
2023-07-06 20:29:36 +02:00
Template . attachmentGallery . helpers ( {
2020-12-17 21:27:02 +02:00
isBoardAdmin ( ) {
2023-01-16 23:00:10 +01:00
return ReactiveCache . getCurrentUser ( ) . isBoardAdmin ( ) ;
2015-09-03 23:12:46 +02:00
} ,
2021-11-24 09:38:56 +01:00
fileSize ( size ) {
2022-04-29 11:47:46 +02:00
const ret = filesize ( size ) ;
return ret ;
2021-11-24 09:38:56 +01:00
} ,
2023-02-20 01:21:33 +02:00
sanitize ( value ) {
2025-10-10 23:14:06 +03:00
return sanitizeHTML ( value ) ;
2023-02-20 01:21:33 +02:00
} ,
2020-12-21 18:32:00 +02:00
} ) ;
2015-11-13 11:13:54 +08:00
2022-07-16 09:32:49 +02:00
Template . cardAttachmentsPopup . onCreated ( function ( ) {
2022-04-28 15:56:37 +02:00
this . uploads = new ReactiveVar ( [ ] ) ;
2022-07-16 09:32:49 +02:00
} ) ;
Template . cardAttachmentsPopup . helpers ( {
2022-04-28 15:56:37 +02:00
getEstimateTime ( upload ) {
const ret = prettyMilliseconds ( upload . estimateTime . get ( ) ) ;
2022-07-16 09:32:49 +02:00
return ret ;
} ,
2022-04-28 15:56:37 +02:00
getEstimateSpeed ( upload ) {
const ret = filesize ( upload . estimateSpeed . get ( ) , { round : 0 } ) + "/s" ;
2022-07-16 09:32:49 +02:00
return ret ;
} ,
2022-04-28 15:56:37 +02:00
uploads ( ) {
return Template . instance ( ) . uploads . get ( ) ;
2022-07-16 09:32:49 +02:00
}
} ) ;
Renaissance
_,,ad8888888888bba,_
,ad88888I888888888888888ba,
,88888888I88888888888888888888a,
,d888888888I8888888888888888888888b,
d88888PP"""" ""YY88888888888888888888b,
,d88"'__,,--------,,,,.;ZZZY8888888888888,
,8IIl'" ;;l"ZZZIII8888888888,
,I88l;' ;lZZZZZ888III8888888,
,II88Zl;. ;llZZZZZ888888I888888,
,II888Zl;. .;;;;;lllZZZ888888I8888b
,II8888Z;; `;;;;;''llZZ8888888I8888,
II88888Z;' .;lZZZ8888888I888b
II88888Z; _,aaa, .,aaaaa,__.l;llZZZ88888888I888
II88888IZZZZZZZZZ, .ZZZZZZZZZZZZZZ;llZZ88888888I888,
II88888IZZ<'(@@>Z| |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
,II88888; `""" ;| |ZZ; `""" ;;llZ8888888888I888
II888888l `;; .;llZZ8888888888I888,
,II888888Z; ;;; .;;llZZZ8888888888I888I
III888888Zl; .., `;; ,;;lllZZZ88888888888I888
II88888888Z;;...;(_ _) ,;;;llZZZZ88888888888I888,
II88888888Zl;;;;;' `--'Z;. .,;;;;llZZZZ88888888888I888b
]I888888888Z;;;;' ";llllll;..;;;lllZZZZ88888888888I8888,
II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
`II8888888888888Zl;. ,;;lllZZZZZZZZWMZ88888888888I88888
II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
`II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
`II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
`II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
,II8888888888888888b .;;lllllll;;;.;..88888888I88888888b,
II888888888888888PZI;. .`;;;.;;;..; ...88888888I8888888888,
II888888888888PZ;;';;. ;. .;. .;. .. Y8888888I88888888888b,
,II888888888PZ;;' `8888888I8888888888888b,
II888888888' 888888I8888888888888888
,II888888888 ,888888I8888888888888888
,d88888888888 d888888I8888888888ZZZZZZ
,ad888888888888I 8888888I8888ZZZZZZZZZZZZ
888888888888888' 888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P' Y888ZZZZZZZZZZZZZZZZZZZZ
888888888, " ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888, ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a, _ ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d' ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand 8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
2015-05-12 19:20:58 +02:00
Template . cardAttachmentsPopup . events ( {
2022-07-16 09:32:49 +02:00
'change .js-attach-file' ( event , templateInstance ) {
2015-09-03 23:12:46 +02:00
const card = this ;
2022-04-26 23:14:57 +02:00
const files = event . currentTarget . files ;
if ( files ) {
2022-04-28 15:56:37 +02:00
let uploads = [ ] ;
2025-10-10 18:52:30 +03:00
const uploaders = handleFileUpload ( card , files ) ;
2023-08-16 18:51:40 +02:00
2025-10-10 18:52:30 +03:00
uploaders . forEach ( uploader => {
2022-07-16 09:32:49 +02:00
uploader . on ( 'start' , function ( ) {
2022-04-28 15:56:37 +02:00
uploads . push ( this ) ;
templateInstance . uploads . set ( uploads ) ;
2022-07-16 09:32:49 +02:00
} ) ;
2022-04-26 23:14:57 +02:00
uploader . on ( 'end' , ( error , fileRef ) => {
2022-04-28 15:56:37 +02:00
uploads = uploads . filter ( _upload => _upload . config . fileId != fileRef . _id ) ;
templateInstance . uploads . set ( uploads ) ;
if ( uploads . length == 0 ) {
2022-07-16 09:32:49 +02:00
Popup . back ( ) ;
}
2022-04-26 23:14:57 +02:00
} ) ;
2025-10-10 18:52:30 +03:00
} ) ;
2020-09-13 22:17:58 -05:00
}
Renaissance
_,,ad8888888888bba,_
,ad88888I888888888888888ba,
,88888888I88888888888888888888a,
,d888888888I8888888888888888888888b,
d88888PP"""" ""YY88888888888888888888b,
,d88"'__,,--------,,,,.;ZZZY8888888888888,
,8IIl'" ;;l"ZZZIII8888888888,
,I88l;' ;lZZZZZ888III8888888,
,II88Zl;. ;llZZZZZ888888I888888,
,II888Zl;. .;;;;;lllZZZ888888I8888b
,II8888Z;; `;;;;;''llZZ8888888I8888,
II88888Z;' .;lZZZ8888888I888b
II88888Z; _,aaa, .,aaaaa,__.l;llZZZ88888888I888
II88888IZZZZZZZZZ, .ZZZZZZZZZZZZZZ;llZZ88888888I888,
II88888IZZ<'(@@>Z| |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
,II88888; `""" ;| |ZZ; `""" ;;llZ8888888888I888
II888888l `;; .;llZZ8888888888I888,
,II888888Z; ;;; .;;llZZZ8888888888I888I
III888888Zl; .., `;; ,;;lllZZZ88888888888I888
II88888888Z;;...;(_ _) ,;;;llZZZZ88888888888I888,
II88888888Zl;;;;;' `--'Z;. .,;;;;llZZZZ88888888888I888b
]I888888888Z;;;;' ";llllll;..;;;lllZZZZ88888888888I8888,
II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
`II8888888888888Zl;. ,;;lllZZZZZZZZWMZ88888888888I88888
II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
`II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
`II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
`II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
,II8888888888888888b .;;lllllll;;;.;..88888888I88888888b,
II888888888888888PZI;. .`;;;.;;;..; ...88888888I8888888888,
II888888888888PZ;;';;. ;. .;. .;. .. Y8888888I88888888888b,
,II888888888PZ;;' `8888888I8888888888888b,
II888888888' 888888I8888888888888888
,II888888888 ,888888I8888888888888888
,d88888888888 d888888I8888888888ZZZZZZ
,ad888888888888I 8888888I8888ZZZZZZZZZZZZ
888888888888888' 888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P' Y888ZZZZZZZZZZZZZZZZZZZZ
888888888, " ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888, ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a, _ ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d' ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand 8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
2015-05-12 19:20:58 +02:00
} ,
2019-06-28 12:52:09 -05:00
'click .js-computer-upload' ( event , templateInstance ) {
templateInstance . find ( '.js-attach-file' ) . click ( ) ;
event . preventDefault ( ) ;
2015-09-03 23:12:46 +02:00
} ,
2015-11-13 11:13:54 +08:00
'click .js-upload-clipboard-image' : Popup . open ( 'previewClipboardImage' ) ,
} ) ;
2019-08-07 23:44:45 -04:00
const MAX _IMAGE _PIXEL = Utils . MAX _IMAGE _PIXEL ;
const COMPRESS _RATIO = Utils . IMAGE _COMPRESS _RATIO ;
2015-11-13 11:13:54 +08:00
let pastedResults = null ;
2025-10-10 18:52:30 +03:00
// Shared upload logic for drag-and-drop functionality
export function handleFileUpload ( card , files ) {
if ( ! files || files . length === 0 ) {
return [ ] ;
}
// Check if board allows attachments
const board = card . board ( ) ;
if ( ! board || ! board . allowsAttachments ) {
2025-10-10 21:46:07 +03:00
if ( process . env . DEBUG === 'true' ) {
console . warn ( 'Attachments not allowed on this board' ) ;
}
2025-10-10 18:52:30 +03:00
return [ ] ;
}
// Check if user can modify the card
if ( ! card . canModifyCard ( ) ) {
2025-10-10 21:46:07 +03:00
if ( process . env . DEBUG === 'true' ) {
console . warn ( 'User does not have permission to modify this card' ) ;
}
2025-10-10 18:52:30 +03:00
return [ ] ;
}
const uploads = [ ] ;
for ( const file of files ) {
// Basic file validation
if ( ! file || ! file . name ) {
2025-10-10 21:46:07 +03:00
if ( process . env . DEBUG === 'true' ) {
console . warn ( 'Invalid file object' ) ;
}
2025-10-10 18:52:30 +03:00
continue ;
}
const fileId = new ObjectID ( ) . toString ( ) ;
2025-10-10 23:14:06 +03:00
let fileName = sanitizeText ( file . name ) ;
2025-10-10 18:52:30 +03:00
// If sanitized filename is not same as original filename,
// it could be XSS that is already fixed with sanitize,
// or just normal mistake, so it is not a problem.
// That is why here is no warning.
if ( fileName !== file . name ) {
// If filename is empty, only in that case add some filename
if ( fileName . length === 0 ) {
fileName = 'Empty-filename-after-sanitize.txt' ;
}
}
const config = {
file : file ,
fileId : fileId ,
fileName : fileName ,
meta : Utils . getCommonAttachmentMetaFrom ( card ) ,
chunkSize : 'dynamic' ,
} ;
config . meta . fileId = fileId ;
try {
const uploader = Attachments . insert (
config ,
false ,
) ;
2025-10-10 21:46:07 +03:00
// Add to progress manager for tracking
const uploadId = uploadProgressManager . addUpload ( card . _id , uploader , file ) ;
2025-10-10 18:52:30 +03:00
uploader . on ( 'uploaded' , ( error , fileRef ) => {
if ( ! error ) {
if ( fileRef . isImage ) {
card . setCover ( fileRef . _id ) ;
2025-10-10 21:46:07 +03:00
if ( process . env . DEBUG === 'true' ) {
console . log ( ` Set cover image for card ${ card . _id } : ${ fileRef . name } ` ) ;
}
2025-10-10 18:52:30 +03:00
}
} else {
2025-10-10 21:46:07 +03:00
if ( process . env . DEBUG === 'true' ) {
console . error ( 'Upload error:' , error ) ;
}
2025-10-10 18:52:30 +03:00
}
} ) ;
uploader . on ( 'error' , ( error ) => {
2025-10-10 21:46:07 +03:00
if ( process . env . DEBUG === 'true' ) {
console . error ( 'Upload error:' , error ) ;
}
2025-10-10 18:52:30 +03:00
} ) ;
uploads . push ( uploader ) ;
uploader . start ( ) ;
} catch ( error ) {
2025-10-10 21:46:07 +03:00
if ( process . env . DEBUG === 'true' ) {
console . error ( 'Failed to create uploader:' , error ) ;
}
2025-10-10 18:52:30 +03:00
}
}
return uploads ;
}
2015-11-13 11:13:54 +08:00
Template . previewClipboardImagePopup . onRendered ( ( ) => {
// we can paste image from clipboard
2019-07-15 10:25:16 -04:00
const handle = results => {
2015-11-13 11:13:54 +08:00
if ( results . dataURL . startsWith ( 'data:image/' ) ) {
2019-07-15 10:25:16 -04:00
const direct = results => {
$ ( 'img.preview-clipboard-image' ) . attr ( 'src' , results . dataURL ) ;
pastedResults = results ;
} ;
if ( MAX _IMAGE _PIXEL ) {
// if has size limitation on image we shrink it before uploading
2019-08-07 23:44:45 -04:00
Utils . shrinkImage ( {
2019-07-15 10:25:16 -04:00
dataurl : results . dataURL ,
maxSize : MAX _IMAGE _PIXEL ,
ratio : COMPRESS _RATIO ,
callback ( changed ) {
if ( changed !== false && ! ! changed ) {
results . dataURL = changed ;
}
direct ( results ) ;
} ,
} ) ;
2019-10-09 18:09:14 +03:00
} else {
2019-10-09 14:24:45 +02:00
direct ( results ) ;
2019-07-15 10:25:16 -04:00
}
2015-11-13 11:13:54 +08:00
}
2019-07-15 10:25:16 -04:00
} ;
$ ( document . body ) . pasteImageReader ( handle ) ;
2015-11-13 11:13:54 +08:00
// we can also drag & drop image file to it
2019-07-15 10:25:16 -04:00
$ ( document . body ) . dropImageReader ( handle ) ;
2015-11-13 11:13:54 +08:00
} ) ;
Template . previewClipboardImagePopup . events ( {
'click .js-upload-pasted-image' ( ) {
2020-09-14 01:07:17 -05:00
const card = this ;
if ( pastedResults && pastedResults . file ) {
const file = pastedResults . file ;
2019-07-15 10:25:16 -04:00
window . oPasted = pastedResults ;
2022-09-16 19:52:48 +02:00
const fileId = new ObjectID ( ) . toString ( ) ;
2022-04-01 23:09:59 +02:00
const config = {
file ,
fileId : fileId ,
meta : Utils . getCommonAttachmentMetaFrom ( card ) ,
fileName : file . name || file . type . replace ( 'image/' , 'clipboard.' ) ,
chunkSize : 'dynamic' ,
} ;
config . meta . fileId = fileId ;
2020-09-13 23:03:20 -05:00
const uploader = Attachments . insert (
2022-04-01 23:09:59 +02:00
config ,
2020-09-13 23:03:20 -05:00
false ,
) ;
2020-09-14 01:07:17 -05:00
uploader . on ( 'uploaded' , ( error , fileRef ) => {
2020-09-13 23:03:20 -05:00
if ( ! error ) {
2020-09-14 01:07:17 -05:00
if ( fileRef . isImage ) {
card . setCover ( fileRef . _id ) ;
2020-09-13 23:03:20 -05:00
}
2015-11-13 11:13:54 +08:00
}
2020-09-13 23:03:20 -05:00
} ) ;
2020-09-14 01:07:17 -05:00
uploader . on ( 'end' , ( error , fileRef ) => {
2020-09-13 23:03:20 -05:00
pastedResults = null ;
$ ( document . body ) . pasteImageReader ( ( ) => { } ) ;
Popup . back ( ) ;
} ) ;
uploader . start ( ) ;
2015-11-13 11:13:54 +08:00
}
} ,
Renaissance
_,,ad8888888888bba,_
,ad88888I888888888888888ba,
,88888888I88888888888888888888a,
,d888888888I8888888888888888888888b,
d88888PP"""" ""YY88888888888888888888b,
,d88"'__,,--------,,,,.;ZZZY8888888888888,
,8IIl'" ;;l"ZZZIII8888888888,
,I88l;' ;lZZZZZ888III8888888,
,II88Zl;. ;llZZZZZ888888I888888,
,II888Zl;. .;;;;;lllZZZ888888I8888b
,II8888Z;; `;;;;;''llZZ8888888I8888,
II88888Z;' .;lZZZ8888888I888b
II88888Z; _,aaa, .,aaaaa,__.l;llZZZ88888888I888
II88888IZZZZZZZZZ, .ZZZZZZZZZZZZZZ;llZZ88888888I888,
II88888IZZ<'(@@>Z| |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
,II88888; `""" ;| |ZZ; `""" ;;llZ8888888888I888
II888888l `;; .;llZZ8888888888I888,
,II888888Z; ;;; .;;llZZZ8888888888I888I
III888888Zl; .., `;; ,;;lllZZZ88888888888I888
II88888888Z;;...;(_ _) ,;;;llZZZZ88888888888I888,
II88888888Zl;;;;;' `--'Z;. .,;;;;llZZZZ88888888888I888b
]I888888888Z;;;;' ";llllll;..;;;lllZZZZ88888888888I8888,
II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
`II8888888888888Zl;. ,;;lllZZZZZZZZWMZ88888888888I88888
II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
`II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
`II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
`II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
,II8888888888888888b .;;lllllll;;;.;..88888888I88888888b,
II888888888888888PZI;. .`;;;.;;;..; ...88888888I8888888888,
II888888888888PZ;;';;. ;. .;. .;. .. Y8888888I88888888888b,
,II888888888PZ;;' `8888888I8888888888888b,
II888888888' 888888I8888888888888888
,II888888888 ,888888I8888888888888888
,d88888888888 d888888I8888888888ZZZZZZ
,ad888888888888I 8888888I8888ZZZZZZZZZZZZ
888888888888888' 888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P' Y888ZZZZZZZZZZZZZZZZZZZZ
888888888, " ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888, ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a, _ ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d' ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand 8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
2015-05-12 19:20:58 +02:00
} ) ;
2022-03-27 23:58:41 +02:00
BlazeComponent . extendComponent ( {
isCover ( ) {
2022-12-16 16:36:47 +01:00
const ret = ReactiveCache . getCard ( this . data ( ) . meta . cardId ) . coverId == this . data ( ) . _id ;
2022-03-27 23:58:41 +02:00
return ret ;
} ,
2023-06-03 12:43:10 -04:00
isBackgroundImage ( ) {
2022-12-15 22:26:08 +01:00
//const currentBoard = Utils.getCurrentBoard();
2023-06-03 17:27:00 -04:00
//return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src");
return false ;
2023-06-03 12:43:10 -04:00
} ,
2022-03-27 23:58:41 +02:00
events ( ) {
return [
{
'click .js-add-cover' ( ) {
2022-12-16 16:36:47 +01:00
ReactiveCache . getCard ( this . data ( ) . meta . cardId ) . setCover ( this . data ( ) . _id ) ;
2022-03-27 23:58:41 +02:00
Popup . back ( ) ;
} ,
'click .js-remove-cover' ( ) {
2022-12-16 16:36:47 +01:00
ReactiveCache . getCard ( this . data ( ) . meta . cardId ) . unsetCover ( ) ;
2022-03-27 23:58:41 +02:00
Popup . back ( ) ;
} ,
2023-06-03 12:43:10 -04:00
'click .js-add-background-image' ( ) {
2022-12-15 22:26:08 +01:00
const currentBoard = Utils . getCurrentBoard ( ) ;
2023-07-10 13:19:19 +02:00
currentBoard . setBackgroundImageURL ( attachmentActionsLink ) ;
Utils . setBackgroundImage ( attachmentActionsLink ) ;
2023-06-03 12:43:10 -04:00
Popup . back ( ) ;
event . preventDefault ( ) ;
} ,
'click .js-remove-background-image' ( ) {
2022-12-15 22:26:08 +01:00
const currentBoard = Utils . getCurrentBoard ( ) ;
2023-06-03 12:43:10 -04:00
currentBoard . setBackgroundImageURL ( "" ) ;
2023-06-03 17:53:56 -04:00
Utils . setBackgroundImage ( "" ) ;
2023-06-03 12:43:10 -04:00
Popup . back ( ) ;
Utils . reload ( ) ;
2023-06-03 17:27:00 -04:00
event . preventDefault ( ) ;
2023-06-03 12:43:10 -04:00
} ,
2022-03-25 14:08:37 +01:00
'click .js-move-storage-fs' ( ) {
2022-04-07 22:49:13 +02:00
Meteor . call ( 'moveAttachmentToStorage' , this . data ( ) . _id , "fs" ) ;
2022-03-25 14:08:37 +01:00
Popup . back ( ) ;
} ,
'click .js-move-storage-gridfs' ( ) {
2022-04-07 22:49:13 +02:00
Meteor . call ( 'moveAttachmentToStorage' , this . data ( ) . _id , "gridfs" ) ;
2022-03-25 14:08:37 +01:00
Popup . back ( ) ;
} ,
2022-12-26 05:45:32 +02:00
'click .js-move-storage-s3' ( ) {
Meteor . call ( 'moveAttachmentToStorage' , this . data ( ) . _id , "s3" ) ;
Popup . back ( ) ;
} ,
2022-03-27 23:58:41 +02:00
}
]
}
} ) . register ( 'attachmentActionsPopup' ) ;
2022-04-24 17:12:31 +02:00
BlazeComponent . extendComponent ( {
2022-04-26 19:27:22 +02:00
getNameWithoutExtension ( ) {
const ret = this . data ( ) . name . replace ( new RegExp ( "\." + this . data ( ) . extension + "$" ) , "" ) ;
return ret ;
} ,
2022-04-24 17:12:31 +02:00
events ( ) {
return [
{
'keydown input.js-edit-attachment-name' ( evt ) {
// enter = save
if ( evt . keyCode === 13 ) {
this . find ( 'button[type=submit]' ) . click ( ) ;
}
} ,
'click button.js-submit-edit-attachment-name' ( event ) {
// save button pressed
event . preventDefault ( ) ;
const name = this . $ ( '.js-edit-attachment-name' ) [ 0 ]
. value
2022-04-26 19:27:22 +02:00
. trim ( ) + this . data ( ) . extensionWithDot ;
2025-10-10 23:14:06 +03:00
if ( name === sanitizeText ( name ) ) {
2023-02-21 21:27:34 +02:00
Meteor . call ( 'renameAttachment' , this . data ( ) . _id , name ) ;
}
2023-07-18 21:16:44 +02:00
Popup . back ( ) ;
2022-04-24 17:12:31 +02:00
} ,
}
]
}
} ) . register ( 'attachmentRenamePopup' ) ;
2025-10-12 03:48:21 +03:00
// Template helpers for attachment migration status
Template . registerHelper ( 'attachmentMigrationStatus' , function ( attachmentId ) {
return attachmentMigrationManager . getAttachmentMigrationStatus ( attachmentId ) ;
} ) ;
Template . registerHelper ( 'isAttachmentMigrating' , function ( attachmentId ) {
return attachmentMigrationManager . isAttachmentBeingMigrated ( attachmentId ) ;
} ) ;
Template . registerHelper ( 'attachmentMigrationProgress' , function ( ) {
return attachmentMigrationManager . attachmentMigrationProgress . get ( ) ;
} ) ;
Template . registerHelper ( 'attachmentMigrationStatusText' , function ( ) {
return attachmentMigrationManager . attachmentMigrationStatus . get ( ) ;
} ) ;