From c0d187071a5d438d0c7f994f6ae1cf1e0c885cad Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 3 Feb 2012 00:59:40 +0100 Subject: [PATCH] ENHANCEMENT New UploadField class to replace FileIframeField, ImageField and SimpleImageField --- README.md | 6 +- admin/css/screen.css | 2 +- admin/javascript/LeftAndMain.EditForm.js | 4 +- admin/scss/themes/_default.scss | 2 +- css/AssetUploadField.css | 42 + css/UploadField.css | 58 ++ docs/en/changelogs/3.0.0.md | 5 + docs/en/reference/dataextension.md | 2 +- docs/en/reference/form-field-types.md | 6 +- docs/en/reference/image.md | 6 +- docs/en/tutorials/2-extending-a-basic-site.md | 4 +- filesystem/File.php | 23 + forms/FileField.php | 6 +- forms/UploadField.php | 746 ++++++++++++++++ images/progressbar_blue.gif | Bin 0 -> 10134 bytes images/sprites_16x16-scf81c570f3.png | Bin 0 -> 6076 bytes images/sprites_16x16/cross-circle.png | Bin 0 -> 729 bytes .../sprites_16x16/cross-circle_disabled.png | Bin 0 -> 680 bytes images/sprites_16x16/drive-upload.png | Bin 0 -> 746 bytes .../sprites_16x16/drive-upload_disabled.png | Bin 0 -> 680 bytes images/sprites_16x16/minus-circle.png | Bin 0 -> 655 bytes .../sprites_16x16/minus-circle_disabled.png | Bin 0 -> 641 bytes images/sprites_16x16/navigation.png | Bin 0 -> 776 bytes images/sprites_16x16/navigation_disabled.png | Bin 0 -> 731 bytes images/sprites_16x16/network-cloud.png | Bin 0 -> 657 bytes .../sprites_16x16/network-cloud_disabled.png | Bin 0 -> 606 bytes images/sprites_16x16/pencil.png | Bin 0 -> 1529 bytes images/sprites_16x16/pencil_disabled.png | Bin 0 -> 665 bytes .../plug-disconnect-prohibition.png | Bin 0 -> 1520 bytes .../plug-disconnect-prohibition_disabled.png | Bin 0 -> 633 bytes javascript/AssetUploadField.js | 14 + javascript/UploadField.js | 164 ++++ javascript/UploadField_downloadtemplate.js | 29 + javascript/UploadField_uploadtemplate.js | 28 + javascript/lang/en_US.js | 17 +- scss/AssetUploadField.scss | 215 +++++ scss/UploadField.scss | 215 +++++ scss/_sprites.scss | 80 ++ scss/newfile | 0 templates/AssetUploadField.ss | 22 + templates/Includes/UploadField_FileButtons.ss | 9 + templates/UploadField.ss | 58 ++ templates/UploadField_FileEdit.ss | 11 + tests/forms/uploadfield/UploadFieldTest.php | 551 ++++++++++++ tests/forms/uploadfield/UploadFieldTest.yml | 45 + thirdparty/javascript-loadimage/.piston.yml | 8 + thirdparty/javascript-loadimage/README.md | 112 +++ thirdparty/javascript-loadimage/load-image.js | 114 +++ .../javascript-loadimage/load-image.min.js | 3 + thirdparty/javascript-loadimage/package.json | 45 + thirdparty/javascript-templates/.piston.yml | 8 + thirdparty/javascript-templates/README.md | 309 +++++++ thirdparty/javascript-templates/package.json | 40 + thirdparty/javascript-templates/tmpl.js | 85 ++ thirdparty/javascript-templates/tmpl.min.js | 2 + thirdparty/jquery-fileupload/.piston.yml | 8 + thirdparty/jquery-fileupload/README.md | 68 ++ .../cors/jquery.postmessage-transport.js | 108 +++ .../cors/jquery.xdr-transport.js | 76 ++ .../jquery-fileupload/jquery.fileupload-ui.js | 538 ++++++++++++ .../jquery-fileupload/jquery.fileupload.js | 831 ++++++++++++++++++ .../jquery.iframe-transport.js | 156 ++++ thirdparty/jquery-fileupload/package.json | 60 ++ 63 files changed, 4924 insertions(+), 17 deletions(-) create mode 100644 css/AssetUploadField.css create mode 100644 css/UploadField.css create mode 100644 forms/UploadField.php create mode 100644 images/progressbar_blue.gif create mode 100644 images/sprites_16x16-scf81c570f3.png create mode 100644 images/sprites_16x16/cross-circle.png create mode 100644 images/sprites_16x16/cross-circle_disabled.png create mode 100644 images/sprites_16x16/drive-upload.png create mode 100644 images/sprites_16x16/drive-upload_disabled.png create mode 100644 images/sprites_16x16/minus-circle.png create mode 100644 images/sprites_16x16/minus-circle_disabled.png create mode 100644 images/sprites_16x16/navigation.png create mode 100644 images/sprites_16x16/navigation_disabled.png create mode 100644 images/sprites_16x16/network-cloud.png create mode 100644 images/sprites_16x16/network-cloud_disabled.png create mode 100644 images/sprites_16x16/pencil.png create mode 100644 images/sprites_16x16/pencil_disabled.png create mode 100644 images/sprites_16x16/plug-disconnect-prohibition.png create mode 100644 images/sprites_16x16/plug-disconnect-prohibition_disabled.png create mode 100644 javascript/AssetUploadField.js create mode 100644 javascript/UploadField.js create mode 100644 javascript/UploadField_downloadtemplate.js create mode 100644 javascript/UploadField_uploadtemplate.js create mode 100644 scss/AssetUploadField.scss create mode 100644 scss/UploadField.scss create mode 100644 scss/_sprites.scss delete mode 100644 scss/newfile create mode 100644 templates/AssetUploadField.ss create mode 100644 templates/Includes/UploadField_FileButtons.ss create mode 100644 templates/UploadField.ss create mode 100644 templates/UploadField_FileEdit.ss create mode 100644 tests/forms/uploadfield/UploadFieldTest.php create mode 100644 tests/forms/uploadfield/UploadFieldTest.yml create mode 100644 thirdparty/javascript-loadimage/.piston.yml create mode 100644 thirdparty/javascript-loadimage/README.md create mode 100644 thirdparty/javascript-loadimage/load-image.js create mode 100644 thirdparty/javascript-loadimage/load-image.min.js create mode 100644 thirdparty/javascript-loadimage/package.json create mode 100644 thirdparty/javascript-templates/.piston.yml create mode 100644 thirdparty/javascript-templates/README.md create mode 100644 thirdparty/javascript-templates/package.json create mode 100644 thirdparty/javascript-templates/tmpl.js create mode 100644 thirdparty/javascript-templates/tmpl.min.js create mode 100644 thirdparty/jquery-fileupload/.piston.yml create mode 100644 thirdparty/jquery-fileupload/README.md create mode 100644 thirdparty/jquery-fileupload/cors/jquery.postmessage-transport.js create mode 100644 thirdparty/jquery-fileupload/cors/jquery.xdr-transport.js create mode 100644 thirdparty/jquery-fileupload/jquery.fileupload-ui.js create mode 100644 thirdparty/jquery-fileupload/jquery.fileupload.js create mode 100644 thirdparty/jquery-fileupload/jquery.iframe-transport.js create mode 100644 thirdparty/jquery-fileupload/package.json diff --git a/README.md b/README.md index 70b6cbd92..8e97959e3 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,8 @@ and [installation from source](http://doc.silverstripe.org/sapphire/en/installat LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - OF SUCH DAMAGE. \ No newline at end of file + OF SUCH DAMAGE. + +## Attribution ## + + * Some icons by [Yusuke Kamiyamane](http://p.yusukekamiyamane.com/). All rights reserved. Licensed under a [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/). diff --git a/admin/css/screen.css b/admin/css/screen.css index 931d4f460..2275b95de 100755 --- a/admin/css/screen.css +++ b/admin/css/screen.css @@ -510,7 +510,7 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; } .cms-logo { border-bottom: 1px solid #03090c; height: 31px; overflow: hidden; padding: 0 0 0 4px; vertical-align: middle; font-size: 12px; } .cms-logo .version { display: none; } -.cms-logo a { display: inline-block; height: 24px; width: 24px; float: left; margin-right: 8px; background: url('../images/logo_small.png') no-repeat; text-indent: -9999em; padding-right: 7px; border-right: 1px solid #19435c; } +.cms-logo a { display: inline-block; height: 24px; width: 24px; float: left; margin-right: 8px; background: url("../images/logo_small.png") no-repeat; text-indent: -9999em; padding-right: 7px; border-right: 1px solid #19435c; } .cms-logo span { font-weight: bold; font-size: 14px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; o-text-overflow: ellipsis; } .cms-login-status { border-top: 1px solid #19435c; height: 24px; padding: 7px 4px 0 4px; overflow: hidden; line-height: 16px; font-size: 11px; } diff --git a/admin/javascript/LeftAndMain.EditForm.js b/admin/javascript/LeftAndMain.EditForm.js index 87944606b..66f255dcf 100644 --- a/admin/javascript/LeftAndMain.EditForm.js +++ b/admin/javascript/LeftAndMain.EditForm.js @@ -31,7 +31,9 @@ * Variable: ChangeTrackerOptions * (Object) */ - ChangeTrackerOptions: {}, + ChangeTrackerOptions: { + ignoreFieldSelector: '.ss-upload :input' + }, /** * Constructor: onmatch diff --git a/admin/scss/themes/_default.scss b/admin/scss/themes/_default.scss index 6693b0eec..a249a823e 100644 --- a/admin/scss/themes/_default.scss +++ b/admin/scss/themes/_default.scss @@ -84,4 +84,4 @@ $grid-horizontal: 8px; /** ----------------------------------------------- * Application Logo (CMS Logo) Must be 24px x 24px * ------------------------------------------------ */ -$application-logo-small: image-url("logo_small.png"); +$application-logo-small: url("../images/logo_small.png"); diff --git a/css/AssetUploadField.css b/css/AssetUploadField.css new file mode 100644 index 000000000..b16b53c1e --- /dev/null +++ b/css/AssetUploadField.css @@ -0,0 +1,42 @@ +/** This file contains the default theme definitions for the admin interface. Please put mostly SCSS variable definitions in here, and leave the actual styling to _style.scss and auxilliary files. */ +/** ----------------------------------------------- Colours ------------------------------------------------ */ +/** ----------------------------------------------- Textures ----------------------------------------------- */ +/** ----------------------------------------------- Typography. ------------------------------------------------ */ +/** ----------------------------------------------- Grid Units (px) We have a vertical rhythm that the grid is based off both vertical and horizontal. All internal padding and margins are scaled to this and accounting for paragraphs ------------------------------------------------ */ +/** ----------------------------------------------- Application Logo (CMS Logo) Must be 24px x 24px ------------------------------------------------ */ +/** Helper SCSS file for generating sprites for the interface. */ +/* CMS action button sprite mix-in for UploadField, copied from admin/scss/_forms.scss */ +#AssetUploadField { border-bottom: 0; -moz-box-shadow: none; -webkit-box-shadow: none; -o-box-shadow: none; box-shadow: none; } + +.ss-assetuploadfield h3 { border-bottom: 1px solid rgba(201, 205, 206, 0.8); -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -o-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); margin: 0 0 8px; padding: 0 0 7px; clear: both; } +.ss-assetuploadfield .ss-uploadfield-files { margin: 0; padding: 0; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item { border: 1px solid #b3b3b3; -moz-border-radius: 5px; -webkit-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; -khtml-border-radius: 5px; border-radius: 5px; -moz-background-clip: padding; -webkit-background-clip: padding; -o-background-clip: padding-box; -ms-background-clip: padding-box; -khtml-background-clip: padding-box; background-clip: padding-box; background: #E2E2E2; margin: 0 0 5px; padding: 0; overflow: hidden; position: relative; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-preview { position: absolute; height: 30px; width: 40px; overflow: hidden; z-index: 1; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-info { position: relative; height: 30px; overflow: hidden; background-color: #5db4df; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5db4df), color-stop(8%, #5db1dd), color-stop(50%, #439bcb), color-stop(54%, #3f99cd), color-stop(96%, #207db6), color-stop(100%, #1e7cba)); background-image: -webkit-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -moz-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -o-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: -ms-linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); background-image: linear-gradient(top, #5db4df 0%, #5db1dd 8%, #439bcb 50%, #3f99cd 54%, #207db6 96%, #1e7cba 100%); } +.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info { background-color: #c11f1d; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #c11f1d), color-stop(4%, #bf1d1b), color-stop(8%, #b71b1c), color-stop(15%, #b61e1d), color-stop(27%, #b11d1d), color-stop(31%, #ab1d1c), color-stop(42%, #a51b1b), color-stop(46%, #9f1b19), color-stop(50%, #9f1b19), color-stop(54%, #991c1a), color-stop(58%, #971a18), color-stop(62%, #911b1b), color-stop(65%, #911b1b), color-stop(88%, #7e1816), color-stop(92%, #771919), color-stop(100%, #731817)); background-image: -webkit-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -moz-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -o-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -ms-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name { position: relative; z-index: 1; margin: 3px 0 3px 50px; width: 40%; background: #fff; border: 1px solid #b3b3b3; -moz-border-radius: 5px; -webkit-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; -khtml-border-radius: 5px; border-radius: 5px; display: block; line-height: 24px; height: 22px; padding: 0 5px; text-align: left; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name b { font-weight: bold; padding: 0 5px 0 0; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name span { font-size: 11px; color: #848484; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-status { float: right; padding: 0 0 0 5px; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-status.ui-state-error-text { color: red; font-weight: bold; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { position: absolute; top: 0; right: 0; left: 0; z-index: 0; } +.ss-assetuploadfield .ss-uploadfield-files label.ss-ui-button { background: none; border: 0; -moz-box-shadow: none; -webkit-box-shadow: none; -o-box-shadow: none; box-shadow: none; text-shadow: none; color: white; float: right; } +.ss-assetuploadfield .ss-uploadfield-files label.ss-ui-button.ss-uploadfield-item-delete { display: none; } +.ss-assetuploadfield .ss-uploadfield-files label.ss-ui-button.ss-uploadfield-item-edit { padding-top: 0; padding-bottom: 0; line-height: 30px; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress { width: 100%; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress div { -moz-border-radius: 5px; -webkit-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; -khtml-border-radius: 5px; border-radius: 5px; height: 30px; padding: 0; margin: 0; overflow: hidden; width: 100%; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress .ss-uploadfield-item-progressbar { background-color: #92a6b3; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #92a6b3), color-stop(11%, #90aab8), color-stop(22%, #96b1bf), color-stop(33%, #9eb4c1), color-stop(44%, #a7bac7), color-stop(100%, #c1d5dc)); background-image: -webkit-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -moz-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -o-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -ms-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress .ss-uploadfield-item-progressbarvalue { width: 0; background: #60b3dd url(../images/progressbar_blue.gif) repeat left center; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel { position: absolute; top: 7px; right: 7px; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button { display: block; overflow: hidden; text-indent: -9999px; padding: 0; margin: 0; border: 0; width: 16px; height: 16px; cursor: pointer; -moz-box-shadow: none; -webkit-box-shadow: none; -o-box-shadow: none; box-shadow: none; background: url('../images/sprites_16x16-scf81c570f3.png') 0 0 no-repeat; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform { /* don't use display none, for it will break jQuery('iframe').contents().height() */ height: 0; overflow: hidden; clear: both; } +.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform iframe { width: 100%; } +.ss-assetuploadfield .ss-uploadfield-addfile { height: 70px; padding: 5px 15px 15px; } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { float: left; margin: 19px 0 0; } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer { position: relative; overflow: hidden; display: block; margin: 0 10px 0 0; background-image: none; padding-left: 25px; padding-right: 7px; font-weight: bold; margin-left: 8px; color: white; border-color: #78a127; border-bottom-color: #59781d; background: #669933 url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #80bf40), color-stop(100%, #59862d)); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -webkit-linear-gradient(#80bf40, #59862d); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -moz-linear-gradient(#80bf40, #59862d); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -o-linear-gradient(#80bf40, #59862d); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -ms-linear-gradient(#80bf40, #59862d); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, linear-gradient(#80bf40, #59862d); text-shadow: #4d7326 0 1px 1px; } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer.ui-state-hover { border-color: #59781d; background: #80bf40 url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #8cc653), color-stop(100%, #669933)); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -webkit-linear-gradient(#8cc653, #669933); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -moz-linear-gradient(#8cc653, #669933); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -o-linear-gradient(#8cc653, #669933); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, -ms-linear-gradient(#8cc653, #669933); background: url('../images/sprites_16x16-scf81c570f3.png') 6px -46px no-repeat, linear-gradient(#8cc653, #669933); } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer:active, .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer:focus { padding: 8px 8px 8px 26px; border: none; background: #619130 url('../images/sprites_16x16-scf81c570f3.png') 7px -45px no-repeat; -moz-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -webkit-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -o-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; display: none; } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { -moz-border-radius: 10px; -webkit-border-radius: 10px; -o-border-radius: 10px; -ms-border-radius: 10px; -khtml-border-radius: 10px; border-radius: 10px; -moz-box-shadow: #9a9a9a 0 0 3px 3px inset; -webkit-box-shadow: #9a9a9a 0 0 3px 3px inset; -o-box-shadow: #9a9a9a 0 0 3px 3px inset; box-shadow: #9a9a9a 0 0 3px 3px inset; border: 2px dashed gray; background: rgba(201, 205, 206, 0.8); display: none; height: 66px; width: 300px; float: left; } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { padding: 15px 0 0; line-height: 22px; font-size: 20px; font-weight: bold; text-align: center; } +.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { display: block; font-size: 12px; } diff --git a/css/UploadField.css b/css/UploadField.css new file mode 100644 index 000000000..9840bd9e2 --- /dev/null +++ b/css/UploadField.css @@ -0,0 +1,58 @@ +/** This file contains the default theme definitions for the admin interface. Please put mostly SCSS variable definitions in here, and leave the actual styling to _style.scss and auxilliary files. */ +/** ----------------------------------------------- Colours ------------------------------------------------ */ +/** ----------------------------------------------- Textures ----------------------------------------------- */ +/** ----------------------------------------------- Typography. ------------------------------------------------ */ +/** ----------------------------------------------- Grid Units (px) We have a vertical rhythm that the grid is based off both vertical and horizontal. All internal padding and margins are scaled to this and accounting for paragraphs ------------------------------------------------ */ +/** ----------------------------------------------- Application Logo (CMS Logo) Must be 24px x 24px ------------------------------------------------ */ +/** Helper SCSS file for generating sprites for the interface. */ +/* CMS action button sprite mix-in for UploadField, copied from admin/scss/_forms.scss */ +.ss-uploadfield .clear { clear: both; } +.ss-uploadfield .middleColumn { width: 526px; padding: 0; background: #fff; border: 1px solid #b3b3b3; -moz-border-radius: 4px; -webkit-border-radius: 4px; -o-border-radius: 4px; -ms-border-radius: 4px; -khtml-border-radius: 4px; border-radius: 4px; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #efefef), color-stop(10%, #ffffff), color-stop(90%, #ffffff), color-stop(100%, #efefef)); background-image: -webkit-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -moz-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -o-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -ms-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); } +.ss-uploadfield .ss-uploadfield-item { margin: 0; padding: 15px; overflow: auto; } +.ss-uploadfield .ss-uploadfield-item-preview { height: 60px; line-height: 60px; width: 80px; text-align: center; font-weight: bold; float: left; overflow: hidden; } +.ss-uploadfield .ss-uploadfield-item-preview.ss-uploadfield-dropzone { -moz-box-shadow: #9a9a9a 0 0 3px 3px inset; -webkit-box-shadow: #9a9a9a 0 0 3px 3px inset; -o-box-shadow: #9a9a9a 0 0 3px 3px inset; box-shadow: #9a9a9a 0 0 3px 3px inset; border: 2px dashed gray; background: rgba(201, 205, 206, 0.8); display: none; } +.ss-uploadfield .ss-uploadfield-item-info { margin: 0 0 0 100px; } +.ss-uploadfield .ss-uploadfield-item-name { display: block; line-height: 13px; height: 26px; margin: 0; text-align: left; } +.ss-uploadfield .ss-uploadfield-item-name b { font-weight: bold; padding: 0 5px 0 0; } +.ss-uploadfield .ss-uploadfield-item-name span { font-size: 11px; color: #848484; } +.ss-uploadfield .ss-uploadfield-item-status { float: right; padding: 0 0 0 5px; } +.ss-uploadfield .ss-uploadfield-item-status.ui-state-error-text { color: red; font-weight: bold; } +.ss-uploadfield label.ss-ui-button { display: block; float: left; margin: 0 10px 0 0; } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-fromcomputer { position: relative; overflow: hidden; padding-left: 24px; padding-right: 6px; background: #e6e6e6 url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -webkit-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -moz-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -o-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -ms-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, linear-gradient(#ffffff, #d9d9d9); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-fromcomputer.ui-state-hover, .ss-uploadfield label.ss-ui-button.ss-uploadfield-fromcomputer:hover { background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -46px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-fromcomputer.ui-state-disabled { background-image: none; color: #444444; background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -72px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -72px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -72px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -72px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -72px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -72px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -72px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-fromfiles { padding-left: 24px; padding-right: 6px; background: #e6e6e6 url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -webkit-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -moz-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -o-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -ms-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, linear-gradient(#ffffff, #d9d9d9); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-fromfiles.ui-state-hover, .ss-uploadfield label.ss-ui-button.ss-uploadfield-fromfiles:hover { background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -202px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-fromfiles.ui-state-disabled { background-image: none; color: #444444; background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -228px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -228px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -228px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -228px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -228px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -228px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -228px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-startall { padding-left: 24px; padding-right: 6px; background: #e6e6e6 url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -webkit-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -moz-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -o-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -ms-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, linear-gradient(#ffffff, #d9d9d9); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-startall.ui-state-hover, .ss-uploadfield label.ss-ui-button.ss-uploadfield-startall:hover { background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -150px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-startall.ui-state-disabled { background-image: none; color: #444444; background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -176px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -176px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -176px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -176px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -176px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -176px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -176px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-delete { color: red; padding-left: 24px; padding-right: 6px; background: #e6e6e6 url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -webkit-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -moz-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -o-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -ms-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, linear-gradient(#ffffff, #d9d9d9); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-delete.ui-state-hover, .ss-uploadfield label.ss-ui-button.ss-uploadfield-item-delete:hover { background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -98px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-delete.ui-state-disabled { background-image: none; color: #444444; background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -124px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -124px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -124px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -124px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -124px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -124px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -124px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-remove { padding-left: 24px; padding-right: 6px; background: #e6e6e6 url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -webkit-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -moz-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -o-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -ms-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, linear-gradient(#ffffff, #d9d9d9); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-remove.ui-state-hover, .ss-uploadfield label.ss-ui-button.ss-uploadfield-item-remove:hover { background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -306px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-remove.ui-state-disabled { background-image: none; color: #444444; background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -332px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -332px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -332px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -332px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -332px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -332px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -332px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-edit { padding-left: 24px; padding-right: 6px; background: #e6e6e6 url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #d9d9d9)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -webkit-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -moz-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -o-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -ms-linear-gradient(#ffffff, #d9d9d9); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, linear-gradient(#ffffff, #d9d9d9); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-edit.ui-state-hover, .ss-uploadfield label.ss-ui-button.ss-uploadfield-item-edit:hover { background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -254px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield label.ss-ui-button.ss-uploadfield-item-edit.ui-state-disabled { background-image: none; color: #444444; background: white url('../images/sprites_16x16-scf81c570f3.png') 5px -280px no-repeat; background: url('../images/sprites_16x16-scf81c570f3.png') 5px -280px no-repeat, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -280px no-repeat, -webkit-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -280px no-repeat, -moz-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -280px no-repeat, -o-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -280px no-repeat, -ms-linear-gradient(#ffffff, #e6e6e6); background: url('../images/sprites_16x16-scf81c570f3.png') 5px -280px no-repeat, linear-gradient(#ffffff, #e6e6e6); } +.ss-uploadfield .ss-uploadfield-files { margin: 0; padding: 0; max-height: 272px; overflow: auto; position: relative; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item.ui-state-error { border: 0; border-bottom: 1px solid #b3b3b3; background: none; color: #444444; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item:last-child, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item.ui-state-error:last-child { border-bottom: 0; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { height: 28px; margin: 6px 0 0; position: relative; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progress { position: absolute; left: 0; right: 42px; width: auto; margin: 11px 0 0; height: 15px; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progress div { -moz-border-radius: 25px; -webkit-border-radius: 25px; -o-border-radius: 25px; -ms-border-radius: 25px; -khtml-border-radius: 25px; border-radius: 25px; height: 13px; padding: 0; margin: 0; overflow: hidden; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progressbar { border: 1px solid gray; background-color: #92a6b3; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #92a6b3), color-stop(11%, #90aab8), color-stop(22%, #96b1bf), color-stop(33%, #9eb4c1), color-stop(44%, #a7bac7), color-stop(100%, #c1d5dc)); background-image: -webkit-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -moz-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -o-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -ms-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progressbarvalue { border: 0; width: 0%; background: #60b3dd url(../images/progressbar_blue.gif) repeat-x left center; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start { position: absolute; top: 10px; right: 0; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button { display: block; overflow: hidden; text-indent: -9999px; padding: 0; margin: 0; border: 0; width: 16px; height: 16px; cursor: pointer; -moz-box-shadow: none; -webkit-box-shadow: none; -o-box-shadow: none; box-shadow: none; background: url('../images/sprites_16x16-scf81c570f3.png') 0 0 no-repeat; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start { right: 20px; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button { background: url('../images/sprites_16x16-scf81c570f3.png') 0 -156px no-repeat; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-editform { /* don't use display none, for it will break jQuery('iframe').contents().height() */ height: 0; overflow: hidden; clear: both; margin-top: 8px; } +.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-editform iframe { width: 100%; } +.ss-uploadfield .ss-uploadfield-addfile.borderTop { border-top: 1px solid #b3b3b3; } + +body.ss-uploadfield-edit-iframe { padding: 16px 16px; } + +.ss-upload .clear { clear: both; } +.ss-upload .ss-uploadfield-fromcomputer input { /* since we can't really style the file input, we use this hack to make it as big as the button and hide it */ position: absolute; top: 0; right: 0; margin: 0; border: solid #000; border-width: 0 0 100px 200px; opacity: 0; filter: alpha(opacity=0); -o-transform: translate(250px, -50px) scale(1); -moz-transform: translate(-300px, 0) scale(4); direction: ltr; cursor: pointer; } diff --git a/docs/en/changelogs/3.0.0.md b/docs/en/changelogs/3.0.0.md index a70af9050..94181d362 100644 --- a/docs/en/changelogs/3.0.0.md +++ b/docs/en/changelogs/3.0.0.md @@ -136,6 +136,11 @@ unless these are explicitly noted. Alternatively, you can enforce database usage by setting `SapphireTest->usesDatabase` to `TRUE` in your test class. +### Deprecated Classes ### + + * `FileIframeField`: Use `UploadField` + * `ImageField`: Use `UploadField` with `$myField->allowedExtensions = array('jpg', 'gif', 'png')` + ### Renamed Classes ### * `DataObjectDecorator`: Use `DataExtension` instead (the class doesn't implement the [GOF "Decorator" pattern](http://en.wikipedia.org/wiki/Decorator_pattern)) diff --git a/docs/en/reference/dataextension.md b/docs/en/reference/dataextension.md index a0ffeadd1..70f47ec56 100644 --- a/docs/en/reference/dataextension.md +++ b/docs/en/reference/dataextension.md @@ -90,7 +90,7 @@ The $fields parameter is passed by reference, as it is an object. :::php public function updateCMSFields(FieldList $fields) { $fields->push(new TextField('Position', 'Position Title')); - $fields->push(new ImageField('Image', 'Profile Image')); + $fields->push(new UploadField('Image', 'Profile Image')); } diff --git a/docs/en/reference/form-field-types.md b/docs/en/reference/form-field-types.md index 6fd1a0d3f..23db760fb 100644 --- a/docs/en/reference/form-field-types.md +++ b/docs/en/reference/form-field-types.md @@ -47,11 +47,7 @@ set {inlcudeDefaultJS} to false and work with behaviour.js. ## Files * `[FileField](api:FileField)`: Simple file upload dialog. -* `[FileIFrameField](api:FileIFrameField)`: File uploads through an iframe -* `[api:ImageField]`: Image upload through an iframe, with thumbnails and file-selection from existing assets -* `[SimpleImageField](api:SimpleImageField)`: SimpleImageField provides an easy way of uploading images to Image has_one -relationships. Unlike ImageField, it doesn't use an iframe. - +* `[UploadField](api:FileIFrameField)`: File uploads through HTML5 features, including upload progress, preview and relationship management. ## Relations diff --git a/docs/en/reference/image.md b/docs/en/reference/image.md index 62a8fcab8..e7d741831 100644 --- a/docs/en/reference/image.md +++ b/docs/en/reference/image.md @@ -10,7 +10,7 @@ including resizing. ### Form Fields * `[api:Image]`. Designed to provide a complex image uploader for the CMS. -* `[api:SimpleImageField]`. A Simple Image Upload Form +* `[api:FileField]` and `[api:UploadField]`. Allows upload of images (through limiting file extensions with `setAllowedExtensions()`). ### Resizing Images in PHP @@ -100,8 +100,8 @@ For output of an image tag with the image automatically resized to 80px width, y ### Form Upload -For usage on a website form, see `[api:SimpleImageField]`. -If you want to upload images within the CMS, see `[api:ImageField]`. +For usage on a website form, see `[api:FileField]`. +If you want to upload images within the CMS, see `[api:UploadField]`. ### Clearing Thumbnail Cache diff --git a/docs/en/tutorials/2-extending-a-basic-site.md b/docs/en/tutorials/2-extending-a-basic-site.md index e4ba319ed..198d8f523 100644 --- a/docs/en/tutorials/2-extending-a-basic-site.md +++ b/docs/en/tutorials/2-extending-a-basic-site.md @@ -594,7 +594,7 @@ insert an image in the *$Content* field). public function getCMSFields() { $fields = parent::getCMSFields(); - $fields->addFieldToTab("Root.Content.Images", new ImageField('Photo')); + $fields->addFieldToTab("Root.Content.Images", new UploadField('Photo')); return $fields; } @@ -610,7 +610,7 @@ a simple database field like all the fields we have seen so far, but has its own array, we create a relationship between the *StaffPage* table and the *Image* table by storing the id of the respective *Image* in the *StaffPage* table. -We then add an *ImageField* in the *getCMSFields* function to the tab "Root.Content.Images". Since this tab doesn't exist, +We then add an `[api:UploadField]` in the *getCMSFields* function to the tab "Root.Content.Images". Since this tab doesn't exist, the *addFieldToTab* function will create it for us. The *ImageField* allows us to select an image or upload a new one in the CMS. diff --git a/filesystem/File.php b/filesystem/File.php index 9db62de2e..2fb4d081b 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -680,6 +680,11 @@ class File extends DataObject { return ($size) ? self::format_size($size) : false; } + /** + * Formats a file size (eg: (int)42 becomes string '42 bytes') + * @param int $size + * @return string + */ public static function format_size($size) { if($size < 1024) return $size . ' bytes'; if($size < 1024*10) return (round($size/1024*10)/10). ' KB'; @@ -688,6 +693,24 @@ class File extends DataObject { if($size < 1024*1024*1024) return round(($size/1024)/1024) . ' MB'; return round($size/(1024*1024*1024)*10)/10 . ' GB'; } + + /** + * Convert a php.ini value (eg: 512M) to bytes + * + * @param string $phpIniValue + * @return int + */ + public function ini2bytes($PHPiniValue) { + switch(strtolower(substr(trim($PHPiniValue), -1))) { + case 'g': + $PHPiniValue *= 1024; + case 'm': + $PHPiniValue *= 1024; + case 'k': + $PHPiniValue *= 1024; + } + return $PHPiniValue; + } /** * Return file size in bytes. diff --git a/forms/FileField.php b/forms/FileField.php index ce53ceab7..83027f2c2 100644 --- a/forms/FileField.php +++ b/forms/FileField.php @@ -7,7 +7,8 @@ * Please set a validator on the form-object to get feedback * about imposed filesize/extension restrictions. * - * CAUTION: Doesn't work in the CMS due to ajax submission, please use {@link FileIFrameField} instead. + * See {@link UploadField} For a more full-featured field + * (incl. ajax-friendly uploads, previews and relationship management). * * Usage

* @@ -146,6 +147,7 @@ class FileField extends FormField { // save to record $record->{$this->name . 'ID'} = $file->ID; } + return $this; } public function Value() { @@ -168,6 +170,7 @@ class FileField extends FormField { */ public function setValidator($validator) { $this->upload->setValidator($validator); + return $this; } /** @@ -175,6 +178,7 @@ class FileField extends FormField { */ public function setFolderName($folderName) { $this->folderName = $folderName; + return $this; } /** diff --git a/forms/UploadField.php b/forms/UploadField.php new file mode 100644 index 000000000..0c670e85a --- /dev/null +++ b/forms/UploadField.php @@ -0,0 +1,746 @@ +NOTE: this Field will call write() on the supplied record
+ * + * Features (some might not be avaliable to old browsers): + * + * - File Drag&Drop support + * - Progressbar + * - Image thumbnail/file icons even before upload finished + * - Saving into relations + * - Edit file + * - allowedExtensions is by default File::$allowed_extensions
  • maxFileSize the vaule of min(upload_max_filesize, post_max_size) from php.ini + * + * @example + * $UploadField = new UploadField('myFiles', 'Please upload some images (max. 5 files)'); + * $UploadField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif')); + * $UploadField->setConfig('allowedMaxFileNumber', 5); + * + * + * @author Zauberfisch + * @package sapphire + * @subpackage forms + */ +class UploadField extends FileField { + + /** + * @var array + */ + public static $allowed_actions = array( + 'upload', + 'handleItem' + ); + + /** + * @var array + */ + public static $url_handlers = array( + 'item/$ID' => 'handleItem', + '$Action!' => '$Action', + ); + + /** + * @var String + */ + protected $template = 'UploadField'; + + /** + * @var String + */ + protected $templateFileButtons = 'UploadField_FileButtons'; + + /** + * @var String + */ + protected $templateFileEdit = 'UploadField_FileEdit'; + + /** + * @var DataObject + */ + protected $record; + + /** + * @var SS_List + */ + protected $items; + + /** + * Config for this field used in both, php and javascript (will be merged into the config of the javascript file upload plugin) + * @var array + */ + protected $config = array( + /** + * @var boolean + */ + 'autoUpload' => true, + /** + * php validation of allowedMaxFileNumber only works when a db relation is avaliable, set to null to allow unlimited + * if record has a has_one and allowedMaxFileNumber is null, it will be set to 1 + * @var int + */ + 'allowedMaxFileNumber' => null, + /** + * @var int + */ + 'previewMaxWidth' => 80, + /** + * @var int + */ + 'previewMaxHeight' => 60, + /** + * javascript template used to display uploading files + * @see javascript/UploadField_uploadtemplate.js + * @var string + */ + 'uploadTemplateName' => 'ss-uploadfield-uploadtemplate', + /** + * javascript template used to display already uploaded files + * @see javascript/UploadField_downloadtemplate.js + * @var string + */ + 'downloadTemplateName' => 'ss-uploadfield-downloadtemplate', + /** + * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm + * @example 'getCMSFields' + * @var FieldList|string + */ + 'fileEditFields' => null, + /** + * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm + * @example 'getCMSActions' + * @var FieldList|string + */ + 'fileEditActions' => null, + /** + * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm + * @example 'getCMSValidator' + * @var string + */ + 'fileEditValidator' => null + ); + + /** + * @param string $name The internal field name, passed to forms. + * @param string $title The field label. + * @param SS_List $items If no items are defined, the field will try to auto-detect an existion relation on {@link $record}, + * with the same name as the field name. + * @param Form $form Reference to the container form + */ + public function __construct($name, $title = null, SS_List $items = null) { + // TODO thats the first thing that came to my head, feel free to change it + $this->addExtraClass('ss-upload'); // class, used by js + $this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only + + parent::__construct($name, $title); + + if($items) $this->setItems($items); + + $this->getValidator()->setAllowedExtensions(array_filter(File::$allowed_extensions)); // filter out '' since this would be a regex problem on JS end + $this->getValidator()->setAllowedMaxFileSize(min(File::ini2bytes(ini_get('upload_max_filesize')), File::ini2bytes(ini_get('post_max_size')))); // get the lower max size + } + + /** + * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension) + * + * @param String + */ + public function setTemplateFileButtons($template) { + $this->templateFileButtons = $template; + return $this; + } + + /** + * @return String + */ + public function getTemplateFileButtons() { + return $this->templateFileButtons; + } + + /** + * Set name of template used for the edit (inline & popup) of a file file (without path or extension) + * + * @param String + */ + public function setTemplateFileEdit($template) { + $this->templateFileEdit = $template; + return $this; + } + + /** + * @return String + */ + public function getTemplateFileEdit() { + return $this->templateFileEdit; + } + + /** + * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File) + * @param DataOjbect $record + */ + public function setRecord($record) { + $this->record = $record; + return $this; + } + /** + * Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will use Form->getRecord() or Form->Controller()->data() + * @return DataObject + */ + public function getRecord() { + if (!$this->record && $this->form) { + if ($this->form->getRecord() && is_a($this->form->getRecord(), 'DataObject')) { + $this->record = $this->form->getRecord(); + } elseif ($this->form->Controller() && $this->form->Controller()->hasMethod('data') + && $this->form->Controller()->data() && is_a($this->form->Controller()->data(), 'DataObject')) { + $this->record = $this->form->Controller()->data(); + } + } + return $this->record; + } + + /** + * @param SS_List $items + */ + public function setItems(SS_List $items) { + $this->items = $items; + return $this; + } + + /** + * @return SS_List + */ + public function getItems() { + $name = $this->getName(); + if (!$this->items || !$this->items->exists()) { + $record = $this->getRecord(); + $this->items = array(); + // Try to auto-detect relationship + if ($record && $record->exists()) { + if ($record->has_many($name) || $record->many_many($name)) { + // Ensure relationship is cast to an array, as we can't alter the items of a DataList/RelationList (see below) + $this->items = $record->{$name}()->toArray(); + } elseif($record->has_one($name)) { + $item = $record->{$name}(); + if ($item && $item->exists()) + $this->items = array($record->{$name}()); + } + } + $this->items = new ArrayList($this->items); + // hack to provide $UploadFieldThumbnailURL, $hasRelation and $UploadFieldEditLink in template for each file + if ($this->items->exists()) { + foreach ($this->items as $i=>$file) { + $this->items[$i] = $this->customiseFile($file); + if(!$file->canView()) unset($this->items[$i]); // Respect model permissions + } + } + } + return $this->items; + } + + /** + * Hack to add some Variables and a dynamic template to a File + * @param File $file + * @param bool [$hasRelation] has this file a relation to the record the file is on? + * @return ViewableData_Customised + */ + protected function customiseFile(File $file, $hasRelation = true) { + $file = $file->customise(array( + 'UploadFieldHasRelation' => $hasRelation, + 'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file), + 'UploadFieldRemoveLink' => $this->getItemHandler($file->ID)->RemoveLink(), + 'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(), + 'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink() + )); + // we do this in a second customise to have the access to the previous customisations + return $file->customise(array( + 'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons()) + )); + } + + /** + * @param string $key + * @param mixed $val + */ + public function setConfig($key, $val) { + $this->config[$key] = $val; + return $this; + } + + /** + * @param string $key + * @return mixed + */ + public function getConfig($key) { + return $this->config[$key]; + } + + /** + * @param File $file + * @return string + */ + protected function getThumbnailURLForFile(File $file) { + if ($file && $file->exists() && file_exists(Director::baseFolder() . '/' . $file->getFilename())) { + if ($file->hasMethod('getThumbnail')) { + return $file->getThumbnail($this->getConfig('previewMaxWidth'), $this->getConfig('previewMaxHeight'))->getURL(); + } elseif ($file->hasMethod('getThumbnailURL')) { + return $file->getThumbnailURL($this->getConfig('previewMaxWidth'), $this->getConfig('previewMaxHeight')); + } elseif ($file->hasMethod('SetRatioSize')) { + return $file->SetRatioSize($this->getConfig('previewMaxWidth'), $this->getConfig('previewMaxHeight'))->getURL(); + } else { + return $file->Icon(); + } + } + return false; + } + + public function Field() { + $record = $this->getRecord(); + $name = $this->getName(); + if ($record && $record->exists()) { + if (!$record->has_many($name) && !$record->many_many($name) && !$this->getConfig('allowedMaxFileNumber') && + ((substr($name, -2) === 'ID' && $record->has_one(substr($name, 0, -2))) || $record->has_one($name))) { + // if there is a has_one relation with that name on the record and allowedMaxFileNumber has not been set, its wanted to be 1 + $this->setConfig('allowedMaxFileNumber', 1); + } + } + Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); + Requirements::javascript(SAPPHIRE_DIR . '/javascript/jquery_improvements.js'); + Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js'); + Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); + Requirements::javascript(SAPPHIRE_DIR . '/javascript/i18n.js'); + Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/javascript/ssui.core.js'); + + Requirements::combine_files('uploadfield.js', array( + THIRDPARTY_DIR . '/javascript-templates/tmpl.js', + THIRDPARTY_DIR . '/javascript-loadimage/load-image.js', + THIRDPARTY_DIR . '/jquery-fileupload/jquery.iframe-transport.js', + THIRDPARTY_DIR . '/jquery-fileupload/cors/jquery.xdr-transport.js', + THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload.js', + THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload-ui.js', + SAPPHIRE_DIR . '/javascript/UploadField_uploadtemplate.js', + SAPPHIRE_DIR . '/javascript/UploadField_downloadtemplate.js', + SAPPHIRE_DIR . '/javascript/UploadField.js', + )); + Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); // TODO hmmm, remove it? + Requirements::css(SAPPHIRE_DIR . '/css/UploadField.css'); + + $config = array( + 'url' => $this->Link('upload'), + 'acceptFileTypes' => '.+$', + 'maxNumberOfFiles' => $this->getConfig('allowedMaxFileNumber') + ); + if (count($this->getValidator()->getAllowedExtensions())) { + $allowedExtensions = $this->getValidator()->getAllowedExtensions(); + $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; + $config['errorMessages']['acceptFileTypes'] = sprintf(_t( + 'File.INVALIDEXTENSION', + 'Extension is not allowed (valid: %s)' + ), wordwrap(implode(', ', $allowedExtensions))); + } + if ($this->getValidator()->getAllowedMaxFileSize()) { + $config['maxFileSize'] = $this->getValidator()->getAllowedMaxFileSize(); + $config['errorMessages']['maxFileSize'] = sprintf(_t( + 'File.TOOLARGE', + 'Filesize is too large, maximum %s allowed.' + ), File::format_size($config['maxFileSize'])); + } + if ($config['maxNumberOfFiles'] > 1) { + $config['errorMessages']['maxNumberOfFiles'] = sprintf(_t( + 'UploadField.MAXNUMBEROFFILES', + 'Max number of %s file(s) exceeded.' + ), $config['maxNumberOfFiles']); + } + $configOverwrite = array(); + if (is_numeric($config['maxNumberOfFiles']) && $this->getItems()->count()) { + $configOverwrite['maxNumberOfFiles'] = $config['maxNumberOfFiles'] - $this->getItems()->count(); + } + $config = array_merge($config, $this->config, $configOverwrite); + return $this->customise(array( + 'configString' => str_replace('"', "'", Convert::raw2json($config)), + 'config' => new ArrayData($config), + 'multiple' => $config['maxNumberOfFiles'] !== 1, + 'displayInput' => (!isset($configOverwrite['maxNumberOfFiles']) || $configOverwrite['maxNumberOfFiles']) + ))->renderWith($this->getTemplate()); + } + + /** + * Validation method for this field, called when the entire form is validated + * + * @param $validator + * @return Boolean + */ + public function validate($validator) { + return true; + } + + /** + * @param SS_HTTPRequest $request + * @return UploadField_ItemHandler + */ + public function handleItem(SS_HTTPRequest $request) { + return $this->getItemHandler($request->param('ID')); + } + + /** + * @param int $itemID + * @return UploadField_ItemHandler + */ + public function getItemHandler($itemID) { + return Object::create('UploadField_ItemHandler', $this, $itemID); + } + + /** + * Action to handle upload of a single file + * + * @param SS_HTTPRequest $request + * @return string json + */ + public function upload(SS_HTTPRequest $request) { + if($this->isDisabled() || $this->isReadonly()) return $this->httpError(403); + + // Protect against CSRF on destructive action + $token = $this->getForm()->getSecurityToken(); + if(!$token->checkRequest($request)) return $this->httpError(400); + + $name = $this->getName(); + $tmpfile = $request->postVar($name); + $record = $this->getRecord(); + + if (!$tmpfile) { + $return = array('error' => _t('UploadField.FIELDNOTSET', 'File information not found')); + } else { + $return = array( + 'name' => $tmpfile['name'], + 'size' => $tmpfile['size'], + 'type' => $tmpfile['type'], + 'error' => $tmpfile['error'] + ); + } + if (!$return['error'] && $record && $record->exists()) { + $tooManyFiles = false; + if ($this->getConfig('allowedMaxFileNumber') && ($record->has_many($name) || $record->many_many($name))) { + if(!$record->isInDB()) $record->write(); + $tooManyFiles = $record->{$name}()->count() >= $this->getConfig('allowedMaxFileNumber'); + } elseif(substr($name, -2) === 'ID' && $record->has_one(substr($name, 0, -2))) { + $tooManyFiles = $record->{substr($name, 0, -2)}() && $record->{substr($name, 0, -2)}()->exists(); + } elseif($record->has_one($name)) { + $tooManyFiles = $record->{$name}() && $record->{$name}()->exists(); + } + if ($tooManyFiles) { + if(!$this->getConfig('allowedMaxFileNumber')) $this->setConfig('allowedMaxFileNumber', 1); + $return['error'] = sprintf(_t( + 'UploadField.MAXNUMBEROFFILES', + 'Max number of %s file(s) exceeded.' + ), $this->getConfig('allowedMaxFileNumber')); + } + } + if (!$return['error']) { + try { + $this->upload->loadIntoFile($tmpfile, null, $this->folderName); + } catch (Exception $e) { + // we shouldn't get an error here, but just in case + $return['error'] = $e->getMessage(); + } + if (!$return['error']) { + if ($this->upload->isError()) { + $return['error'] = implode(' '.PHP_EOL, $this->upload->getErrors()); + } else { + $file = $this->upload->getFile(); + $file->OwnerID = (Member::currentUser() ? Member::currentUser()->ID : 0); + $file->write(); + $hasRelation = false; + if ($record && $record->exists()) { + if ($record->has_many($name) || $record->many_many($name)) { + if(!$record->isInDB()) $record->write(); + $record->{$name}()->add($file); + $hasRelation = true; + } elseif(substr($name, -2) === 'ID' && $record->has_one(substr($name, 0, -2))) { + $record->{$name} = $file->ID; + $record->write(); + $hasRelation = true; + } elseif($record->has_one($name)) { + $record->{$name . 'ID'} = $file->ID; + $record->write(); + $hasRelation = true; + } + } + $file = $this->customiseFile($file, $hasRelation); + $return = array_merge($return, array( + 'id' => $file->ID, + 'name' => $file->getTitle() . '.' . $file->getExtension(), + 'url' => $file->getURL(), + 'thumbnail_url' => $file->UploadFieldThumbnailURL, + 'edit_url' => $file->UploadFieldEditLink, + 'size' => $file->getAbsoluteSize(), + 'buttons' => $file->UploadFieldFileButtons + )); + } + } + } + $response = new SS_HTTPResponse(Convert::raw2json(array($return))); + $response->addHeader('Content-Type', 'text/plain'); + return $response; + } + + function performReadonlyTransformation() { + $clone = clone $this; + $clone->addExtraClass('readonly'); + $clone->setReadonly(true); + return $clone; + } +} + +/** + * RequestHandler for actions (edit, remove, delete) on a single item (File) of the UploadField + * + * @author Zauberfisch + * @package sapphire + * @subpackage forms + */ +class UploadField_ItemHandler extends RequestHandler { + + /** + * @var UploadFIeld + */ + protected $parent; + + /** + * @var int FileID + */ + protected $itemID; + + public static $url_handlers = array( + '$Action!' => '$Action', + '' => 'index', + ); + + /** + * @param UploadFIeld $parent + * @param int $item + */ + public function __construct($parent, $itemID) { + $this->parent = $parent; + $this->itemID = $itemID; + + parent::__construct(); + } + + /** + * @return File + */ + function getItem() { + return DataObject::get_by_id('File', $this->itemID); + } + + /** + * @param string $action + * @return string + */ + public function Link($action = null) { + return Controller::join_links($this->parent->Link(), '/item/', $this->itemID, $action); + } + + /** + * @return string + */ + public function RemoveLink() { + $token = $this->parent->getForm()->getSecurityToken(); + return $token->addToUrl($this->Link('remove')); + } + + /** + * @return string + */ + public function DeleteLink() { + $token = $this->parent->getForm()->getSecurityToken(); + return $token->addToUrl($this->Link('delete')); + } + + /** + * @return string + */ + public function EditLink() { + return $this->Link('edit'); + } + + /** + * Action to handle removeing a single file from the db relation + * + * @param SS_HTTPRequest $request + * @return SS_HTTPResponse + */ + public function remove(SS_HTTPRequest $request) { + // Check form field state + if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403); + + // Protect against CSRF on destructive action + $token = $this->parent->getForm()->getSecurityToken(); + if(!$token->checkRequest($request)) return $this->httpError(400); + + $response = new SS_HTTPResponse(); + $response->setStatusCode(500); + $fieldName = $this->parent->getName(); + $record = $this->parent->getRecord(); + $id = $this->getItem()->ID; + if ($id && $record && $record->exists()) { + if (($record->has_many($fieldName) || $record->many_many($fieldName)) && $file = $record->{$fieldName}()->byID($id)) { + $record->{$fieldName}()->remove($file); + $response->setStatusCode(200); + } elseif(substr($fieldName, -2) === 'ID' && $record->has_one(substr($fieldName, 0, -2)) && $record->{$fieldName} == $id) { + $record->{$fieldName} = 0; + $record->write(); + $response->setStatusCode(200); + } elseif($record->has_one($fieldName) && $record->{$fieldName . 'ID'} == $id) { + $record->{$fieldName . 'ID'} = 0; + $record->write(); + $response->setStatusCode(200); + } + } + if ($response->getStatusCode() != 200) + $response->setStatusDescription(_t('UploadField.REMOVEERROR', 'Error removing file')); + return $response; + } + + /** + * Action to handle deleting of a single file + * + * @param SS_HTTPRequest $request + * @return SS_HTTPResponse + */ + public function delete(SS_HTTPRequest $request) { + // Check form field state + if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403); + + // Protect against CSRF on destructive action + $token = $this->parent->getForm()->getSecurityToken(); + if(!$token->checkRequest($request)) return $this->httpError(400); + + // Check item permissions + $item = $this->getItem(); + if(!$item) return $this->httpError(404); + if(!$item->canDelete()) return $this->httpError(403); + + // Only allow actions on files in the managed relation (if one exists) + $items = $this->parent->getItems(); + if($this->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403); + + // First remove the file from the current relationship + $this->remove($request); + + // Then delete the file from the filesystem + $item->delete(); + } + + /** + * Action to handle editing of a single file + * + * @param SS_HTTPRequest $request + * @return ViewableData_Customised + */ + public function edit(SS_HTTPRequest $request) { + // Check form field state + if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403); + + // Check item permissions + $item = $this->getItem(); + if(!$item) return $this->httpError(404); + if(!$item->canEdit()) return $this->httpError(403); + + // Only allow actions on files in the managed relation (if one exists) + $items = $this->parent->getItems(); + if($this->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403); + + Requirements::css(SAPPHIRE_DIR . '/css/UploadField.css'); + + return $this->customise(array( + 'Form' => $this->EditForm() + ))->renderWith($this->parent->getTemplateFileEdit()); + } + + /** + * @return Form + */ + public function EditForm() { + $file = $this->getItem(); + if (is_a($this->parent->getConfig('fileEditFields'), 'FieldList')) { + $fields = $this->parent->getConfig('fileEditFields'); + } elseif ($file->hasMethod($this->parent->getConfig('fileEditFields'))) { + $fields = $file->{$this->parent->getConfig('fileEditFields')}(); + } else { + $fields = $file->getCMSFields(); + // Only display main tab, to avoid overly complex interface + if($fields->hasTabSet() && $mainTab = $fields->findOrMakeTab('Root.Main')) $fields = $mainTab->Fields(); + } + if (is_a($this->parent->getConfig('fileEditActions'), 'FieldList')) { + $actions = $this->parent->getConfig('fileEditActions'); + } elseif ($file->hasMethod($this->parent->getConfig('fileEditActions'))) { + $actions = $file->{$this->parent->getConfig('fileEditActions')}(); + } else { + $actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save'))); + $saveAction->addExtraClass('ss-ui-action-constructive'); + } + if (is_a($this->parent->getConfig('fileEditValidator'), 'Validator')) { + $validator = $this->parent->getConfig('fileEditValidator'); + } elseif ($file->hasMethod($this->parent->getConfig('fileEditValidator'))) { + $validator = $file->{$this->parent->getConfig('fileEditValidator')}(); + } else { + $validator = null; + } + $form = new Form( + $this, + __FUNCTION__, + $fields, + $actions, + $validator + ); + $form->loadDataFrom($file); + $form->addExtraClass('small'); + + return $form; + } + + /** + * @param array $data + * @param Form $form + * @param SS_HTTPRequest $request + */ + public function doEdit(array $data, Form $form, SS_HTTPRequest $request) { + // Check form field state + if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403); + + // Check item permissions + $item = $this->getItem(); + if(!$item) return $this->httpError(404); + if(!$item->canEdit()) return $this->httpError(403); + + // Only allow actions on files in the managed relation (if one exists) + $items = $this->parent->getItems(); + if($this->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403); + + $form->saveInto($item); + $item->write(); + + $form->sessionMessage(_t('UploadField.Saved', 'Saved'), 'good'); + + return $this->parent->getForm()->Controller()->redirectBack(); + } + + /** + * Determines if the underlying record (if any) has a relationship + * matching the field name. Important for permission control. + * + * @return boolean + */ + protected function managesRelation() { + $record = $this->parent->getRecord(); + $fieldName = $this->parent->getName(); + if(!$record) return false; + + return ( + (substr($fieldName, -2) === 'ID' && $record->has_one(substr($fieldName, 0, -2))) + || $record->has_many($fieldName) + || $record->many_many($fieldName) + ); + } +} \ No newline at end of file diff --git a/images/progressbar_blue.gif b/images/progressbar_blue.gif new file mode 100644 index 0000000000000000000000000000000000000000..5ccc3fee357d2a49f6b229763c654c6bcbcded63 GIT binary patch literal 10134 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

    L5y0JKm7Y#mu=b3a5xAl9Yi% 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 O00001&W>84^hl#VOYFJ#X2yELbG1 z^bY$=b1=4x;W@7N`a{Idj-!jj3j*Zs?iL^#&4A-_;ZzVjacz5%0W4id)AwQS6k(8r z;rq`aCHh-5o`)ETKoEos4dw1-hSbzh_rUt*;l&^b%D(S^_FR{EL3oBem`6uZGh^F! z?#|6&wNf!rE*qGh>{rvhiaCk`7%?r!G1>Nq}h-9WXvOWMbkbF#{Moen1Z>xU$>;B3m~Do+`) zpb7BD!I^`6n~X-+AP&(Bqh4oQTf3Yr?8*WIY|{H67LTLX{SL*fgh7UYzJrxk7(^cw zMI3Rbu!2dpRjlx_y}f7v+cdwi4b@yiPjCRw^Fo&=UV#-Gm<(31?5b;8AeMCj@NBy_ zB^A)a9d(X_%Vx7}2MT@vTeMX0?`qI}cufu(p@X&+`n^3nJNq;qkEbllglQTii|qRB zIV6b`fngY13kxsE7hZ*cYPE_)BEiNI5$0j*w8H|vk(4Ot!!N2x}} zy1Keeu#wTx3!w|!cK&i;zV)&8wYXic*Q2FUsgLEsIz6NTa&SqKq|5k^a$R?w9DJf> c^``&>09jXAhv@$a5&!@I07*qoM6N<$g4fw%X8-^I literal 0 HcmV?d00001 diff --git a/images/sprites_16x16/drive-upload_disabled.png b/images/sprites_16x16/drive-upload_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..ee8bc18772f1e2418ce21109330a8342c086da56 GIT binary patch literal 680 zcmV;Z0$2TsP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv0006PNkl`45BvZZ2ZO{-oE#HeZl5E0 z#Sln);lTCY-#K1*AEK91C=_J1S}7b3Q!EyvL?S`Ax3`}m^q64>2JERc8V#9DCi46J z6buGW0XH`{7s5D&&`SYez`_IX@rNWSAR3LrFr7>$JQxfZJ!ZH-3XnW+0z#n>z-hnV z=W4ae<#L&u%_d`p-MaySKmfqB+wF3@-R5Gk$mw*N^Z7jA-`_KKV9u{5pU-!a*X#Y| z@pz_^N~IE~QYk*4fjRfQ3J@T<-EML^o#b#h{uBy@r@OnmC-j(M2j*3PSOTWY%S&>( zTy%AHMMk3$`kl+=M&dJ$M4!hDJ1}4+p9GWHY$m(iP8N%W^m;uM%x1GcQRp$l4h-0r zKcP1P0Pu~!h{|L#rerc{LXR1C?*+(gHk(7}+dt$G7syT*7z_q_(RHuaTP+p~9*sua z>2$c!XmG7oJE>l;<6W!-e=Ye)tyYsxr-Oor!-1clpLxIE^LD%C&1S>v_4=gQY{skA ziowDI@3B-W6=}8F@4MZOE0qez<8h8eB6xi)Uf)sZan*P{X0Y%`^+7Zrl}hCgaqi>8 z!vimuOP1PhN4yd)R;B$LU$%H{I-{|GER@csp}k(t$h#}{1y O0000h;0oRM3S+oLoE#L?c8~J z&e%5f1q*NT&D_WNzVo_Q3d0coXQ=dfElHr@pCAEb5F0pMM$8TDz98PAt8IkoIpZqIq;wIypRiDmX8r-Oeu&49P5++*(>&D}<`b z9M1#i;$nDa%oYkpI-8xU?(S}Nf9^*j5_xzfNts)^{$_c7eTlm+3xeRU`AV3o8ixE} zZ*N~X&a)R`XhsoWJ{xnzV*a~rGskhREm|xVV>7v2{2%6!w?~Gq z>q4*FjUa>o4^xVAR#ljuoJaL=2Ljul&(A-BEOP@}Wl-b)0cPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv0005-NklRco?tO%lLdgf<<`b0a-soXR1+8ouv$A-wL=;C?vGoZE-%I zORLpNSuU5#Y&Mfxt(HhV6mTC2xOhCi#bJ4|Sip2T{R^s1CKEXpi)~TBz1@Hf27|(Q zJeEhJ5ex={cNq?cfPFa-2nZ-(wO4Sv-F&0b5c~Z;bUK|+>2|wNsZ>PDXabkZ_1Nq6 zq-L`TFVt!^C>D#5PNxCahHyBnLg#Y1EKx=iIGxTl-mcW^b+}%ya5|k}x7)$taDe0S z2HP7=(sZ@gfeh-_?=G`y}qR}XArBFr_*zNYmYPBjQ zlSy^pGZKje;MbBUqX}#_8;{{%xm*rXsnnO?R}d+q39MEtiz^BFd|u9Gv-FN1f)~kn zyFi)O0>)yoa46YgSS}O_fUxR>ujmHdg00000NkvXXu0mjfd~Xi6 literal 0 HcmV?d00001 diff --git a/images/sprites_16x16/navigation.png b/images/sprites_16x16/navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..d6c52613d5c6acc55e2ee47f94d623df316dcf52 GIT binary patch literal 776 zcmV+j1NZ!iP)d}oHSEDQhBIQ!8y0x&GVI>;9j1jEG1%UM#9 zy>+XkCSX})p_PVdkj{iY9WMzSXuTdcH_&-H5^Zj%trNUXFEyCU73BFvv1f8>txr+4 z=VaZ7Lk`Mt!0)J$~8m5p}ipZoyxLhtcolal#iGZ)6J`j5QKH=Z_qo6FpmRKv~ zaXAM2dQWvo*;3{_TyD;FC(+)j3oG8F;Sh_|L z$g%=eRYAl3JSE!GuO8OhV|1>dszP%^AWE5i0n5Lp|D^5_b2gjd>QF< z8qbEjn3iwb29I+$2l-s-7|L(?1L0Xz5&uYfBzv+A5wITp@A_B1E#7G;JdEzo|d6-OMBC? z##oKVA8?nzZFM7=*jk_a{!>%tow{p=TmTb}5yem`N-YIz zJpuFU7|f*&8Uv|K!s=xKnuTYtxPRPWaup#{V}v#T3NQe4SU_8cuSX#O0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv0006?Nkl zF=C2{4hk*_c2IOtF(HY9M&nmdk`M@fVZp)AISz&P56Ie3($Xw(a1JU$B0)jW!6nhb zK{-9=O}yCB(xDd~xc8p(d+vSj%L4%3|GvIz7~CwU;Om zGJ#U5^n1PD`*=JKCnqNykw^q?Z*Q4owtseZ20J@D_mp9R_4V~n7=AiDJcJp3zaQ*& zJ5;Mx==FNg>2$zoG=jxqfxW%GC(1B^LZSGE*PGx&!FW98nj4J?f=Z>r!VZUn)$n&eP=*O4 z5{VG6bcKIn6bgla-oov6voM`XdjyI{Yiny)lwks~Sj<~pT{Yv&R`C|2qoX4roK7dl zY`wp~Kf=eWQZHqgfQLgM5b&0lmn|ZZ=!d~z=+JGBkB_GW&1sKn)I+^A<^_CQzd_#d z`TX~bi;EwYmXXtGA}a`gUa){0uaMWs+kZd@QSSl?egYVi`Xt_*Mu`9b N002ovPDHLkV1lZQJ}m$M literal 0 HcmV?d00001 diff --git a/images/sprites_16x16/network-cloud.png b/images/sprites_16x16/network-cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..f58cf417b86fc205a83d139a804cfbfc08bb8315 GIT binary patch literal 657 zcmV;C0&e|@P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv00062NklLL$p^Rg%rb^vJ|f2 z-JdJ8o8I9@tr<7)bk1E$MoF*_ethSi^L^)@`)~mfp%D#T<`;w48-b3F0r2_mLQl`| zAt4r<5l2U#NQ`5u30w~b@7ub&hwMAY8UzAyZXirey_EBL!`k1suvDtz>T2GaoO~g3 z?ZgIt|2>FCr$gDSVI3SG9x4{UqN3!iWb)bTkr7#Z#n&d!(s9#2?EBqTYL$y>U{q0rKjYHe>N2bPDNB8 zb~7$DE-^4L^m3s900G)bL_t(IPoCoXS-~7ZI+1Fa!nvpbm;CI%(X&)A^e3QSpkv10T-$oqzY7 z-{mKWNL<22#rUwWSS-S3vk8a8A^d*-EmABN3#3x10)Wji32~p#cf## znM@YD-L5*D%@CyJa!H%b=8n;5ME!pMv=02x^?F?m27@womT{QR=Y&K>QK-}D5Z?iz z;b1VJX0v(nqseqSJ?TcHL6J!0qX>mUg-WGDwOZ}JsaC60E|cUNA3BCeu6K sUjeY;JM)~io_T^*E`LmyJZ04W0F${B0dSTRyZ`_I07*qoM6N<$f_}^mqyPW_ literal 0 HcmV?d00001 diff --git a/images/sprites_16x16/pencil.png b/images/sprites_16x16/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..3ef2fa63e268971a0bc18951278218e279429ac1 GIT binary patch literal 1529 zcmaJ>dr;GM9FHP`2n^XkM5kHQox9N{Y5I&Uu1Q)cv#6yLl#MgAfktf7CFxoiK2UGq zr01prhj%`vo6l3`L_Kxj1H`f0M4jAJ#+>Iib|z|m^5HNX2n3V?OvwvHFlsOu!Wt@-LPjXW5>BFn3Qn9ls6a8IOJIEx%X45@ zkuKo=U|IGk~k#Ji37=!(*_4=AB-vySm?)u;~Qkz?Js9|y+deGaxgEP z|6hI4S>j`02P5)+!6jF*aOyBqFEdrGw2d&%;Z+=;3`JnT>%tEa&C}Vz^GSS7ARHf&GYaTLs<=_NNSIhgtXgWz%L7 zwW=(73{$DilodtIWkW8Z89NZouLG7}hU?TXGYIr$gN@FNW?9!^!Cu8SuJZ!R!8s7p{SuT-d=v4K(%F@os1Qavz(M7VncLRk=4T}n9 z>^#!(b&dY}&=|WV;(TJ#rci5RNn@v}>p+sO_+Z48?y|DJ8-^Q!CNX+RY+i)<{9E9m ztNH)jef-QGdHQkOy|TzwR&noi)3)UWhm0NVn}58pQ>DvMwgTJ#=eqcu6K_{%p7E&9c3w@n+IYNV|?(r@ky|ue*=7ZAu>RW|>h3*4ybT;pv`K0IEj@WhQR^6O!ha=0M zbVt|K-O@W7DMgwMTCgBFVOtr>b*)}*xio!6n@5qGAbeL?o3QL@Z)559yt!=?4{YM1 zx`q7-Ytr&V@s_wrclMaZ^3^{rxp#e0>W1Feo73CZoLQM~9d|jiqoyYG@7VnOtra_7 zLwZ}Vy=RY2n0ku7k+OQmrz;9ir=+$RmD~Ki*RZTKY&n@|NZ0)FNt?U1IjU)W!>H_Uob*rTty zsxMD;*JgcIel513Epkq`<>>rz<1=eNs5?@8JQ41EWyEOUT72}!oP!Jh(gB+#i#kXw GUiBZ;`8Zkt literal 0 HcmV?d00001 diff --git a/images/sprites_16x16/pencil_disabled.png b/images/sprites_16x16/pencil_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3bfa84eed7453ea22c7236c0b4c575155b34de GIT binary patch literal 665 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 TtHBZ=00000NkvXXu0mjf1-%f; literal 0 HcmV?d00001 diff --git a/javascript/AssetUploadField.js b/javascript/AssetUploadField.js new file mode 100644 index 000000000..74fae32b8 --- /dev/null +++ b/javascript/AssetUploadField.js @@ -0,0 +1,14 @@ +(function($) { + $('.ss-assetuploadfield').entwine({ + onmatch: function() { + this._super(); + + // Hide the "second step" part until we're actually uploading + this.find('.ss-uploadfield-editandorganize').hide(); + }, + onfileuploadstart: function(e) { + this.find('.ss-uploadfield-editandorganize').show(); + } + + }); +}(jQuery)); \ No newline at end of file diff --git a/javascript/UploadField.js b/javascript/UploadField.js new file mode 100644 index 000000000..b0f0be89f --- /dev/null +++ b/javascript/UploadField.js @@ -0,0 +1,164 @@ +(function($) { + $.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, { + _initTemplates: function() { + this.options.templateContainer = document.createElement( + this._files.prop('nodeName') + ); + this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName); + this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName); + }, + _enableFileInputButton: function() { + $.blueimpUI.fileupload.prototype._enableFileInputButton.call(this); + this.element.find('.ss-uploadfield-addfile').show(); + }, + _disableFileInputButton: function() { + $.blueimpUI.fileupload.prototype._disableFileInputButton.call(this); + this.element.find('.ss-uploadfield-addfile').hide(); + }, + _onAdd: function(e, data) { + // use _onAdd instead of add since we only want it called once for a file set, not for each file + var result = $.blueimpUI.fileupload.prototype._onAdd.call(this, e, data); + var firstNewFile = this._files.find('.ss-uploadfield-item').slice(data.files.length*-1).first(); + var top = '+=' + (firstNewFile.position().top - parseInt(firstNewFile.css('marginTop')) || 0 - parseInt(firstNewFile.css('borderTopWidth')) || 0); + firstNewFile.offsetParent().animate({scrollTop: top}, 1000); + return result; + } + }); + $.entwine('ss', function($) { + $('div.ss-upload').entwine({ + onmatch: function() { + var fileInput = this.find('input'); + var dropZone = this.find('.ss-uploadfield-dropzone'); + var config = $.parseJSON(fileInput.data('config').replace(/'/g,'"')); + this.fileupload($.extend(true, + { + formData: function(form) { + return [{name: 'SecurityID', value: $(form).find(':input[name=SecurityID]').val()}]; + }, + errorMessages: { + // errorMessages for all error codes suggested from the plugin author, some will be overwritten by the config comming from php + 1: ss.i18n._t('UploadField.PHP_MAXFILESIZE'), + 2: ss.i18n._t('UploadField.HTML_MAXFILESIZE'), + 3: ss.i18n._t('UploadField.ONLYPARTIALUPLOADED'), + 4: ss.i18n._t('UploadField.NOFILEUPLOADED'), + 5: ss.i18n._t('UploadField.NOTMPFOLDER'), + 6: ss.i18n._t('UploadField.WRITEFAILED'), + 7: ss.i18n._t('UploadField.STOPEDBYEXTENSION'), + maxFileSize: ss.i18n._t('UploadField.TOOLARGE'), + minFileSize: ss.i18n._t('UploadField.TOOSMALL'), + acceptFileTypes: ss.i18n._t('UploadField.INVALIDEXTENSION'), + maxNumberOfFiles: ss.i18n._t('UploadField.MAXNUMBEROFFILESSIMPLE'), + uploadedBytes: ss.i18n._t('UploadField.UPLOADEDBYTES'), + emptyResult: ss.i18n._t('UploadField.EMPTYRESULT') + }, + send: function(e, data) { + if (data.context && data.dataType && data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.total = 1; + data.loaded = 1; + $(this).data('fileupload').options.progress(e, data); + } + }, + progress: function(e, data) { + if (data.context) { + var value = parseInt(data.loaded / data.total * 100, 10) + '%'; + data.context.find('.ss-uploadfield-item-status').html((data.total == 1)?ss.i18n._t('UploadField.LOADING'):value); + data.context.find('.ss-uploadfield-item-progressbarvalue').css('width', value); + } + } + }, + config, + { + fileInput: fileInput, + dropZone: dropZone, + previewAsCanvas: false, + acceptFileTypes: new RegExp(config.acceptFileTypes, 'i') + } + )); + if (this.data('fileupload')._isXHRUpload({multipart: true})) { + $('.ss-uploadfield-item-uploador').show(); + dropZone.show(); // drag&drop avaliable + } + this._super(); + } + }); + $('div.ss-upload .ss-uploadfield-files .ss-uploadfield-item').entwine({ + onmatch: function() { + this.closest('.ss-upload').find('.ss-uploadfield-addfile').addClass('borderTop'); + }, + onunmatch: function() { + $('.ss-uploadfield-files:not(:has(.ss-uploadfield-item))').closest('.ss-upload').find('.ss-uploadfield-addfile').removeClass('borderTop'); + } + }); + $('div.ss-upload .ss-uploadfield-startall').entwine({ + onclick: function(e) { + this.closest('.ss-upload').find('.ss-uploadfield-item-start button').click(); + return false; + } + }); + $('div.ss-upload .ss-uploadfield-item-cancelfailed').entwine({ + onclick: function(e) { + this.closest('.ss-uploadfield-item').remove(); + return false; + } + }); + $('div.ss-upload .ss-uploadfield-item-remove:not(.ui-state-disabled), .ss-uploadfield-item-delete:not(.ui-state-disabled)').entwine({ + onclick: function(e) { + var fileupload = this.closest('div.ss-upload').data('fileupload'), + item = this.closest('.ss-uploadfield-item'), msg = ''; + + if(this.is('.ss-uploadfield-item-delete')) msg = ss.i18n._t('UploadField.ConfirmDelete'); + if(!msg || confirm(msg)) { + fileupload._trigger('destroy', e, { + context: item, + url: this.data('href'), + type: 'get', + dataType: fileupload.options.dataType + }); + } + + return false; + } + }); + $('div.ss-upload .ss-uploadfield-item-edit').entwine({ + onclick: function(e) { + var editform = this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform'); + if (editform.hasClass('loading')) { + // TODO Display loading indication, and register an event to toggle edit form + } else { + this.siblings().toggleClass('ui-state-disabled'); + editform.toggleEditForm(); + } + } + }); + $('div.ss-upload .ss-uploadfield-item-editform').entwine({ + EditFormVisible: false, + fitHeight: function() { + var iframe = this.find('iframe'), + h = iframe.contents().height() + 'px'; + iframe.css('height', h); + return h; + }, + showEditForm: function() { + return this.stop().animate({height: this.fitHeight()}); + }, + hideEditFormShow: function() { + return this.stop().animate({height: 0}); + }, + toggleEditForm: function() { + if (this.getEditFormVisible()) this.hideEditFormShow(); + else this.showEditForm(); + this.setEditFormVisible(!this.getEditFormVisible()); + } + }); + $('div.ss-upload .ss-uploadfield-item-editform iframe').entwine({ + onmatch: function() { + this.load(function() { + $(this).parent().removeClass('loading'); + }); + } + }); + }); +}(jQuery)); \ No newline at end of file diff --git a/javascript/UploadField_downloadtemplate.js b/javascript/UploadField_downloadtemplate.js new file mode 100644 index 000000000..ff6716fc7 --- /dev/null +++ b/javascript/UploadField_downloadtemplate.js @@ -0,0 +1,29 @@ +window.tmpl.cache['ss-uploadfield-downloadtemplate'] = tmpl( + '{% for (var i=0, files=o.files, l=files.length, file=files[0]; i' + + '

    ' + + '' + + '
    ' + + '
    ' + + '' + + '{% if (file.error) { %}' + + '
    ' + + '
    ' + + '
    ' + + '{% } else { %}' + + '
    {% print(file.buttons, true); %}
    ' + + '{% } %}' + + '
    ' + + '{% if (!file.error) { %}' + + '
    ' + + '{% } %}' + + '
  • ' + + '{% } %}' +); \ No newline at end of file diff --git a/javascript/UploadField_uploadtemplate.js b/javascript/UploadField_uploadtemplate.js new file mode 100644 index 000000000..ded445ee5 --- /dev/null +++ b/javascript/UploadField_uploadtemplate.js @@ -0,0 +1,28 @@ +window.tmpl.cache['ss-uploadfield-uploadtemplate'] = tmpl( + '{% for (var i=0, files=o.files, l=files.length, file=files[0]; i' + + '
    ' + + '
    ' + + '' + + '
    ' + + '{% if (!file.error) { %}' + + '
    ' + + '{% if (!o.options.autoUpload) { %}' + + '
    ' + + '{% } %}' + + '{% } %}' + + '
    ' + + '
    ' + + '
    ' + + '' + + '{% } %}' +); \ No newline at end of file diff --git a/javascript/lang/en_US.js b/javascript/lang/en_US.js index 309e0e984..efabd2c6e 100644 --- a/javascript/lang/en_US.js +++ b/javascript/lang/en_US.js @@ -16,6 +16,21 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') { 'FILEIFRAMEFIELD.DELETEIMAGE': 'Delete Image', 'FILEIFRAMEFIELD.CONFIRMDELETE': 'Are you sure you want to delete this file?', 'LeftAndMain.IncompatBrowserWarning': 'Your browser is not compatible with the CMS interface. Please use Internet Explorer 7+, Google Chrome 10+ or Mozilla Firefox 3.5+.', - 'GRIDFIELD.ERRORINTRANSACTION': 'An error occured while fetching data from the server\n Please try again later.' + 'GRIDFIELD.ERRORINTRANSACTION': 'An error occured while fetching data from the server\n Please try again later.', + 'UploadField.ConfirmDelete': 'Are you sure you want to remove this file from the server filesystem?', + 'UploadField.PHP_MAXFILESIZE': 'File exceeds upload_max_filesize (php.ini directive)', + 'UploadField.HTML_MAXFILESIZE': 'File exceeds MAX_FILE_SIZE (HTML form directive)', + 'UploadField.ONLYPARTIALUPLOADED': 'File was only partially uploaded', + 'UploadField.NOFILEUPLOADED': 'No File was uploaded', + 'UploadField.NOTMPFOLDER': 'Missing a temporary folder', + 'UploadField.WRITEFAILED': 'Failed to write file to disk', + 'UploadField.STOPEDBYEXTENSION': 'File upload stopped by extension', + 'UploadField.TOOLARGE': 'Filesize is too large', + 'UploadField.TOOSMALL': 'Filesize is too small', + 'UploadField.INVALIDEXTENSION': 'Extension is not allowed', + 'UploadField.MAXNUMBEROFFILESSIMPLE': 'Max number of files exceeded', + 'UploadField.UPLOADEDBYTES': 'Uploaded bytes exceed file size', + 'UploadField.EMPTYRESULT': 'Empty file upload result', + 'UploadField.LOADING': 'Loading ...' }); } diff --git a/scss/AssetUploadField.scss b/scss/AssetUploadField.scss new file mode 100644 index 000000000..c6c9bda01 --- /dev/null +++ b/scss/AssetUploadField.scss @@ -0,0 +1,215 @@ +@import "compass/css3"; + +// TODO we need a seperated file for styles that are used in both cms and front end (such as buttons) +@import "../admin/scss/themes/default.scss"; +@import "sprites.scss"; + +#AssetUploadField { + border-bottom: 0; + @include box-shadow(none); +} + +.ss-assetuploadfield { + h3 { + border-bottom: 1px solid $color-shadow-light; + @include box-shadow(0 1px 0 lighten($color-shadow-light, 95%)); + margin: 0 0 8px; + padding: 0 0 7px; + clear: both; + } + + .ss-uploadfield-files { + margin: 0; + padding: 0; + + .ss-uploadfield-item { + border: 1px solid lighten($color-medium-separator, 20%); + @include border-radius(5px); + @include background-clip(padding-box); + background: #E2E2E2; + margin: 0 0 5px; + padding: 0; + overflow: hidden; + position: relative; + } + .ss-uploadfield-item-preview { + position: absolute; + height: 30px; + width: 40px; + overflow: hidden; + z-index: 1; + } + .ss-uploadfield-item-info { + position: relative; + height: 30px; + overflow: hidden; + background-color: #5db4df; + @include background-image(linear-gradient(top, #5db4df 0%,#5db1dd 8%,#439bcb 50%,#3f99cd 54%,#207db6 96%,#1e7cba 100%)); + } + .ui-state-error .ss-uploadfield-item-info { + background-color: #c11f1d; + @include background-image(linear-gradient(top, #c11f1d 0%,#bf1d1b 4%,#b71b1c 8%,#b61e1d 15%,#b11d1d 27%,#ab1d1c 31%,#a51b1b 42%,#9f1b19 46%,#9f1b19 50%,#991c1a 54%,#971a18 58%,#911b1b 62%,#911b1b 65%,#7e1816 88%,#771919 92%,#731817 100%)); + } + .ss-uploadfield-item-name { + position: relative; + z-index: 1; + margin: 3px 0 3px 50px; + width: 40%; + background: #fff; + border: 1px solid lighten($color-medium-separator, 20%); + @include border-radius(5px); + display: block; + line-height: 24px; + height: 22px; + padding: 0 5px; + text-align: left; + + b { + font-weight: bold; + padding: 0 5px 0 0; + } + span { + font-size: $font-base-size - 1; + color: lighten($color-text, 25%); + } + } + .ss-uploadfield-item-status { + float: right; + padding: 0 0 0 5px; + + &.ui-state-error-text { + color: $color-button-destructive; + font-weight: bold; + } + } + .ss-uploadfield-item-actions { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 0; + } + label.ss-ui-button { + background: none; + border: 0; + @include box-shadow(none); + @include text-shadow(none); + color: $color-text-light; + float: right; + + &.ss-uploadfield-item-delete { + // TODO tmp hack until we have permissions and can disable delete + display: none; + } + &.ss-uploadfield-fromfiles { + //@include actionButtonSprite('network-cloud'); + } + &.ss-uploadfield-item-delete { + //color: $color-button-destructive; + //@include actionButtonSprite('minus-circle'); + } + &.ss-uploadfield-item-edit { + padding-top: 0; + padding-bottom: 0; + line-height: 30px; + } + } + .ss-uploadfield-item-progress { + width: 100%; + + div { + @include border-radius(5px); + height: 30px; + padding: 0; + margin: 0; + overflow: hidden; + width: 100%; + } + .ss-uploadfield-item-progressbar { + background-color: #92a6b3; + @include background-image(linear-gradient(top, #92a6b3 0%,#90aab8 11%,#96b1bf 22%,#9eb4c1 33%,#a7bac7 44%,#c1d5dc 100%)); + } + .ss-uploadfield-item-progressbarvalue { + width: 0; + background: #60b3dd url(../images/progressbar_blue.gif) repeat left center; + } + } + + + .ss-uploadfield-item-cancel { + position: absolute; + top: 7px; + right: 7px; + + button { + display: block; + overflow: hidden; + text-indent: -9999px; + padding: 0; + margin: 0; + border: 0; + width: 16px; + height: 16px; + cursor: pointer; + @include single-box-shadow(none); + background: sprite($sprites16, cross-circle) no-repeat; + } + } + .ss-uploadfield-item-editform { + /* don't use display none, for it will break jQuery('iframe').contents().height() */ + height: 0; + overflow: hidden; + clear: both; + iframe { + width: 100%; + } + } + } + .ss-uploadfield-addfile { + height: 70px; + padding: 5px 15px 15px; + + .ss-uploadfield-item-info { + float: left; + margin: 19px 0 0; + } + .ss-uploadfield-fromcomputer { + position: relative; + overflow: hidden; + display: block; + margin: 0 10px 0 0; + @include actionButtonSpriteConstructive('drive-upload'); + } + .ss-uploadfield-item-uploador { + float: left; + font-weight: bold; + font-size: 22px; + padding: 0 20px; + line-height: 70px; + display: none; + } + .ss-uploadfield-dropzone { + @include border-radius(10px); + @include box-shadow(lighten($color-medium-separator, 10%) 0 0 3px 3px inset); + border: 2px dashed $color-medium-separator; + background: $color-light-separator; + display: none; + height: 66px; + width: 300px; + float: left; + + div { + padding: 15px 0 0; + line-height: 22px; + font-size: 20px; + font-weight: bold; + text-align: center; + + span { + display: block; + font-size: 12px; + } + } + } + } +} \ No newline at end of file diff --git a/scss/UploadField.scss b/scss/UploadField.scss new file mode 100644 index 000000000..958904bc3 --- /dev/null +++ b/scss/UploadField.scss @@ -0,0 +1,215 @@ +@import "compass/css3"; + +// TODO we need a seperated file for styles that are used in both cms and front end (such as buttons) +@import "../admin/scss/themes/default.scss"; +@import "sprites.scss"; + + +.ss-uploadfield { + .clear { + clear: both; + } + .middleColumn { + // TODO .middleColumn styling should probably be theme specific (eg cms ui will look different than blackcandy) + // so we should move this style into the cms and black candy files + width: 526px; + padding: 0; + background: #fff; + border: 1px solid lighten($color-medium-separator, 20%); + @include border-radius(4px); + @include background-image(linear-gradient(#efefef, #fff 10%, #fff 90%, #efefef)); + } + .ss-uploadfield-item { + margin: 0; + padding: 15px; + overflow: auto; + } + .ss-uploadfield-item-preview { + height: 60px; + line-height: 60px; + width: 80px; + text-align: center; + font-weight: bold; + float: left; + overflow: hidden; + &.ss-uploadfield-dropzone { + @include box-shadow(lighten($color-medium-separator, 10%) 0 0 3px 3px inset); + border: 2px dashed $color-medium-separator; + background: $color-light-separator; + display: none; + } + } + .ss-uploadfield-item-info { + margin: 0 0 0 100px; + } + .ss-uploadfield-item-name { + display: block; + line-height: 13px; + height: 26px; + margin: 0; + text-align: left; + + b { + font-weight: bold; + padding: 0 5px 0 0; + } + span { + font-size: $font-base-size - 1; + color: lighten($color-text, 25%); + } + } + .ss-uploadfield-item-status { + float: right; + padding: 0 0 0 5px; + + &.ui-state-error-text { + color: $color-button-destructive; + font-weight: bold; + } + } + label.ss-ui-button { + display: block; + float: left; + margin: 0 10px 0 0; + + &.ss-uploadfield-fromcomputer { + position: relative; + overflow: hidden; + @include actionButtonSprite('drive-upload'); + } + &.ss-uploadfield-fromfiles { + @include actionButtonSprite('network-cloud'); + } + &.ss-uploadfield-startall { + @include actionButtonSprite('navigation'); + } + &.ss-uploadfield-item-delete { + color: $color-button-destructive; + @include actionButtonSprite('minus-circle'); + } + &.ss-uploadfield-item-remove { + @include actionButtonSprite('plug-disconnect-prohibition'); + } + &.ss-uploadfield-item-edit { + @include actionButtonSprite('pencil'); + } + } + .ss-uploadfield-files { + margin: 0; + padding: 0; + max-height: 272px; + overflow: auto; + position: relative; + + .ss-uploadfield-item, + .ss-uploadfield-item.ui-state-error { + border: 0; + border-bottom: 1px solid lighten($color-medium-separator, 20%); + background: none; + color: $color-text; + + &:last-child { + border-bottom: 0; + } + } + .ss-uploadfield-item-actions { + height: 28px; + margin: 6px 0 0; + position: relative; + } + .ss-uploadfield-item-progress { + position: absolute; + left: 0; + right: 42px; + width: auto; + margin: 11px 0 0; + height: 15px; + + div { + @include border-radius(25px); + height: 13px; + padding: 0; + margin: 0; + overflow: hidden; + } + } + .ss-uploadfield-item-progressbar { + border: 1px solid $color-medium-separator; + background-color: #92a6b3; + @include background-image(linear-gradient(top, #92a6b3 0%,#90aab8 11%,#96b1bf 22%,#9eb4c1 33%,#a7bac7 44%,#c1d5dc 100%)); + } + .ss-uploadfield-item-progressbarvalue { + border: 0; + width: 0%; + background: #60b3dd url(../images/progressbar_blue.gif) repeat-x left center; + } + .ss-uploadfield-item-cancel, + .ss-uploadfield-item-start { + position: absolute; + top: 10px; + right: 0; + + button { + display: block; + overflow: hidden; + text-indent: -9999px; + padding: 0; + margin: 0; + border: 0; + width: 16px; + height: 16px; + cursor: pointer; + @include single-box-shadow(none); + background: sprite($sprites16, cross-circle) no-repeat; + } + } + .ss-uploadfield-item-start { + right: 20px; + button { + background: sprite($sprites16, navigation) no-repeat; + } + } + .ss-uploadfield-item-editform { + /* don't use display none, for it will break jQuery('iframe').contents().height() */ + height: 0; + overflow: hidden; + clear: both; + margin-top: $grid-vertical; + iframe { + width: 100%; + } + } + } + .ss-uploadfield-addfile { + &.borderTop { + border-top: 1px solid lighten($color-medium-separator, 20%); + } + } +} + +body.ss-uploadfield-edit-iframe { + padding: $grid-vertical*2 $grid-horizontal*2; +} + +.ss-upload { + .clear { + clear: both; + } + .ss-uploadfield-fromcomputer { + input { + /* since we can't really style the file input, we use this hack to make it as big as the button and hide it */ + position: absolute; + top: 0; + right: 0; + margin: 0; + border: solid #000; + border-width: 0 0 100px 200px; + opacity: 0; + filter: alpha(opacity=0); + -o-transform: translate(250px, -50px) scale(1); + -moz-transform: translate(-300px, 0) scale(4); + direction: ltr; + cursor: pointer; + } + } +} \ No newline at end of file diff --git a/scss/_sprites.scss b/scss/_sprites.scss new file mode 100644 index 000000000..c12870339 --- /dev/null +++ b/scss/_sprites.scss @@ -0,0 +1,80 @@ +/** + * Helper SCSS file for generating sprites for the interface. + */ +@import "compass/utilities/sprites/base"; + +//$sprites32: sprite-map("sprites_32x32/*.png", $spacing: 10px); + +$sprites16: sprite-map("sprites_16x16/*.png", $spacing: 10px); + +/* CMS action button sprite mix-in for UploadField, copied from admin/scss/_forms.scss */ +@mixin actionButtonSprite($name) { + padding-left: 24px; + padding-right: 6px; + + @include background($color-button-generic sprite($sprites16, $name, 5px, 6px) no-repeat); + @include background(sprite($sprites16, $name, 5px, 6px) no-repeat, + linear-gradient(color-stops( + lighten($color-button-generic, 10%), + darken($color-button-generic, 5%) + )) + ); + &.ui-state-hover, + &:hover { + @include background(lighten($color-button-generic, 10%) sprite($sprites16, $name, 5px, 6px) no-repeat); + @include background(sprite($sprites16, $name, 5px, 6px) no-repeat, + linear-gradient(color-stops( + lighten($color-button-generic, 20%), + $color-button-generic + )) + ); + } + &.ui-state-disabled { + background-image: none; + color: $color-text; + @include background(lighten($color-button-generic, 20%) sprite($sprites16, $name+_disabled, 5px, 6px) no-repeat); + @include background(sprite($sprites16, $name+_disabled, 5px, 6px) no-repeat, + linear-gradient(color-stops( + lighten($color-button-generic, 20%), + $color-button-generic + )) + ); + } +} +@mixin actionButtonSpriteConstructive($name) { + background-image: none; + padding-left: 25px; + padding-right: 7px; + font-weight: bold; + margin-left: $grid-horizontal; + color: $color-text-light; + border-color: $color-button-constructive-border; + border-bottom-color: darken($color-button-constructive-border, 10%); + @include background($color-button-constructive sprite($sprites16, $name, 6, 6) no-repeat); + @include background(sprite($sprites16, $name + , 6, 6) no-repeat, + linear-gradient(color-stops( + lighten($color-button-constructive, 10%), + darken($color-button-constructive, 5%) + )) + ); + + @include text-shadow(darken($color-button-constructive, 10%) 0 1px 1px); + + &.ui-state-hover { + border-color: darken($color-button-constructive-border, 10%); + @include background(lighten($color-button-constructive, 10%) sprite($sprites16, $name, 6, 6) no-repeat); + @include background(sprite($sprites16, $name, 6, 6) no-repeat, + linear-gradient(color-stops( + lighten($color-button-constructive, 15%), + $color-button-constructive + )) + ); + } + &:active, &:focus { + padding: $grid-vertical 8px $grid_vertical 26px; + border: none; + @include background(darken($color-button-constructive, 2%) sprite($sprites16, $name, 7, 7) no-repeat); + @include box-shadow(inset 0 1px 3px rgb(23, 24, 26), 0 1px 0 rgba(255, 255, 255, .6)); + } +} \ No newline at end of file diff --git a/scss/newfile b/scss/newfile deleted file mode 100644 index e69de29bb..000000000 diff --git a/templates/AssetUploadField.ss b/templates/AssetUploadField.ss new file mode 100644 index 000000000..4189dae87 --- /dev/null +++ b/templates/AssetUploadField.ss @@ -0,0 +1,22 @@ +

    <% _t('AssetUploadField.CHOOSEFILES', 'Choose files') %>

    +
    +
    + +
    +
    +
    + <% _t('AssetUploadField.UPLOADOR', 'OR') %> +
    +
    +
    + <% _t('AssetUploadField.DROPAREA', 'Drop Area') %> + <% _t('AssetUploadField.DRAGFILESHERE', 'Drag files here') %> +
    +
    +
    +
    +

    <% _t('AssetUploadField.EDITANDORGANIZE', 'Edit & organize') %>

    +
      \ No newline at end of file diff --git a/templates/Includes/UploadField_FileButtons.ss b/templates/Includes/UploadField_FileButtons.ss new file mode 100644 index 000000000..ced06f2bf --- /dev/null +++ b/templates/Includes/UploadField_FileButtons.ss @@ -0,0 +1,9 @@ +<% if canEdit %> + + <% if UploadFieldHasRelation %> + +<% end_if %> +<% end_if %> +<% if canDelete %> + +<% end_if %> \ No newline at end of file diff --git a/templates/UploadField.ss b/templates/UploadField.ss new file mode 100644 index 000000000..b4cb26266 --- /dev/null +++ b/templates/UploadField.ss @@ -0,0 +1,58 @@ +
        + <% if $Items %> + <% loop $Items %> +
      • +
        + $hasRelation +
        +
        + +
        + <% if Top.isDisabled || Top.isReadonly %> + <% else %> + $UploadFieldFileButtons + <% end_if %> +
        +
        +
        + +
        +
      • + <% end_loop %> + <% end_if %> +
      +<% if isDisabled || isReadonly %> +<% else %> +
      style="display: none;"<% end_if %>> +
      + <% if $multiple %> + <% _t('UploadField.DROPFILES', 'drop files') %> + <% else %> + <% _t('UploadField.DROPFILE', 'drop a file') %> + <% end_if %> +
      +
      + + + + <% if not $config.autoUpload %> + + <% end_if %> +
      +
      +
      +
      +<% end_if %> \ No newline at end of file diff --git a/templates/UploadField_FileEdit.ss b/templates/UploadField_FileEdit.ss new file mode 100644 index 000000000..2715288ee --- /dev/null +++ b/templates/UploadField_FileEdit.ss @@ -0,0 +1,11 @@ + + + + <% base_tag %> + + + + + $Form + + \ No newline at end of file diff --git a/tests/forms/uploadfield/UploadFieldTest.php b/tests/forms/uploadfield/UploadFieldTest.php new file mode 100644 index 000000000..62bc564ce --- /dev/null +++ b/tests/forms/uploadfield/UploadFieldTest.php @@ -0,0 +1,551 @@ + array('UploadFieldTest_FileExtension') + ); + + function testUploadNoRelation() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + + $tmpFileName = 'testUploadBasic.txt'; + $_FILES = array('NoRelationField' => $this->getUploadFile($tmpFileName)); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/NoRelationField/upload', + array('NoRelationField' => $this->getUploadFile($tmpFileName)) + ); + $this->assertFalse($response->isError()); + $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); + $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); + $this->assertNotNull($uploadedFile); + } + + function testUploadHasOneRelation() { + $this->loginWithPermission('ADMIN'); + + // Unset existing has_one relation before re-uploading + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $record->HasOneFileID = null; + $record->write(); + + $tmpFileName = 'testUploadHasOneRelation.txt'; + $_FILES = array('HasOneFile' => $this->getUploadFile($tmpFileName)); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasOneFile/upload', + array('HasOneFile' => $this->getUploadFile($tmpFileName)) + ); + $this->assertFalse($response->isError()); + $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); + $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); + $this->assertNotNull($uploadedFile); + + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertTrue($record->HasOneFile()->exists()); + $this->assertEquals($record->HasOneFile()->Name, $tmpFileName); + } + + function testUploadHasManyRelation() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + + $tmpFileName = 'testUploadHasManyRelation.txt'; + $_FILES = array('HasManyFiles' => $this->getUploadFile($tmpFileName)); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasManyFiles/upload', + array('HasManyFiles' => $this->getUploadFile($tmpFileName)) + ); + $this->assertFalse($response->isError()); + $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); + $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); + $this->assertNotNull($uploadedFile); + + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertEquals(3, $record->HasManyFiles()->Count()); + $this->assertEquals($record->HasManyFiles()->Last()->Name, $tmpFileName); + } + + function testUploadManyManyRelation() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $relationCount = $record->ManyManyFiles()->Count(); + + $tmpFileName = 'testUploadManyManyRelation.txt'; + $_FILES = array('ManyManyFiles' => $this->getUploadFile($tmpFileName)); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/ManyManyFiles/upload', + array('ManyManyFiles' => $this->getUploadFile($tmpFileName)) + ); + $this->assertFalse($response->isError()); + $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); + $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); + $this->assertNotNull($uploadedFile); + + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertEquals($relationCount+1, $record->ManyManyFiles()->Count()); + $this->assertEquals($record->ManyManyFiles()->Last()->Name, $tmpFileName); + } + + function testAllowedMaxFileNumber() { + $this->markTestIncomplete(); + } + + function testRemoveFromHasOne() { + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file1 = $this->objFromFixture('File', 'file1'); + + $this->assertTrue($record->HasOneFile()->exists()); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasOneFile/item/' . $file1->ID . '/remove', + array() + ); + $this->assertFalse($response->isError()); + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertFalse($record->HasOneFile()->exists()); + $this->assertFileExists($file1->FullPath, 'File is only detached, not deleted from filesystem'); + } + + function testRemoveFromHasMany() { + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file2 = $this->objFromFixture('File', 'file2'); + $file3 = $this->objFromFixture('File', 'file3'); + + $this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title')); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $file2->ID . '/remove', + array() + ); + $this->assertFalse($response->isError()); + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title')); + $this->assertFileExists($file3->FullPath, 'File is only detached, not deleted from filesystem'); + } + + function testRemoveFromManyMany() { + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file4 = $this->objFromFixture('File', 'file4'); + $file5 = $this->objFromFixture('File', 'file5'); + + $this->assertContains('File4', $record->ManyManyFiles()->column('Title')); + $this->assertContains('File5', $record->ManyManyFiles()->column('Title')); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $file4->ID . '/remove', + array() + ); + $this->assertFalse($response->isError()); + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertNotContains('File4', $record->ManyManyFiles()->column('Title')); + $this->assertContains('File5', $record->ManyManyFiles()->column('Title')); + $this->assertFileExists($file4->FullPath, 'File is only detached, not deleted from filesystem'); + } + + function testDeleteFromHasOne() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file1 = $this->objFromFixture('File', 'file1'); + + $this->assertTrue($record->HasOneFile()->exists()); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasOneFile/item/' . $file1->ID . '/delete', + array() + ); + $this->assertFalse($response->isError()); + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertFalse($record->HasOneFile()->exists()); + $this->assertFileNotExists($file1->FullPath, 'File is also removed from filesystem'); + } + + function testDeleteFromHasMany() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file2 = $this->objFromFixture('File', 'file2'); + $file3 = $this->objFromFixture('File', 'file3'); + + $this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title')); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $file2->ID . '/delete', + array() + ); + $this->assertFalse($response->isError()); + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title')); + $this->assertFileNotExists($file2->FullPath, 'File is also removed from filesystem'); + + $fileNotOnRelationship = $this->objFromFixture('File', 'file1'); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $fileNotOnRelationship->ID . '/delete', + array() + ); + $this->assertEquals(403, $response->getStatusCode(), "Denies deleting files if they're not on the current relationship"); + } + + function testDeleteFromManyMany() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file4 = $this->objFromFixture('File', 'file4'); + $file5 = $this->objFromFixture('File', 'file5'); + $fileNoDelete = $this->objFromFixture('File', 'file-nodelete'); + + $this->assertContains('File4', $record->ManyManyFiles()->column('Title')); + $this->assertContains('File5', $record->ManyManyFiles()->column('Title')); + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $file4->ID . '/delete', + array() + ); + $this->assertFalse($response->isError()); + $record = DataObject::get_by_id($record->class, $record->ID, false); + $this->assertNotContains('File4', $record->ManyManyFiles()->column('Title')); + $this->assertContains('File5', $record->ManyManyFiles()->column('Title')); + $this->assertFileNotExists($file4->FullPath, 'File is also removed from filesystem'); + + // Test record-based permissions + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $fileNoDelete->ID . '/delete', + array() + ); + $this->assertEquals(403, $response->getStatusCode()); + } + + function testView() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file4 = $this->objFromFixture('File', 'file4'); + $file5 = $this->objFromFixture('File', 'file5'); + $fileNoView = $this->objFromFixture('File', 'file-noview'); + $fileNoEdit = $this->objFromFixture('File', 'file-noedit'); + $fileNoDelete = $this->objFromFixture('File', 'file-nodelete'); + + $response = $this->get('UploadFieldTest_Controller'); + $this->assertFalse($response->isError()); + + $parser = new CSSContentParser($response->getBody()); + $items = $parser->getBySelector('#ManyManyFiles .ss-uploadfield-files .ss-uploadfield-item'); + $ids = array(); + foreach($items as $item) $ids[] = (int)$item['data-fileid']; + + $this->assertContains($file4->ID, $ids, 'Views related file'); + $this->assertContains($file5->ID, $ids, 'Views related file'); + $this->assertNotContains($fileNoView->ID, $ids, "Doesn't view files without view permissions"); + $this->assertContains($fileNoEdit->ID, $ids, "Views files without edit permissions"); + $this->assertContains($fileNoDelete->ID, $ids, "Views files without delete permissions"); + } + + function testEdit() { + $this->loginWithPermission('ADMIN'); + + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $file4 = $this->objFromFixture('File', 'file4'); + $file5 = $this->objFromFixture('File', 'file5'); + $fileNoEdit = $this->objFromFixture('File', 'file-noedit'); + $baseUrl = 'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $file4->ID; + + $response = $this->get($baseUrl . '/edit'); + $this->assertFalse($response->isError()); + + $response = $this->post($baseUrl . '/EditForm', array('Title' => 'File 4 modified')); + $this->assertFalse($response->isError()); + + $record = DataObject::get_by_id($record->class, $record->ID, false); + $file4 = DataObject::get_by_id($file4->class, $file4->ID, false); + $this->assertEquals('File 4 modified', $file4->Title); + + // Test record-based permissions + $response = $this->post( + 'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $fileNoEdit->ID . '/edit', + array() + ); + $this->assertEquals(403, $response->getStatusCode()); + } + + function testGetRecord() { + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $form = $this->getMockForm(); + + $field = new UploadField('MyField'); + $field->setForm($form); + $this->assertNull($field->getRecord(), 'Returns no record by default'); + + $field = new UploadField('MyField'); + $field->setForm($form); + $form->loadDataFrom($record); + $this->assertEquals($record, $field->getRecord(), 'Returns record from form if available'); + + $field = new UploadField('MyField'); + $field->setForm($form); + $field->setRecord($record); + $this->assertEquals($record, $field->getRecord(), 'Returns record when set explicitly'); + } + + function testSetItems() { + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $form = $this->getMockForm(); + $items = new ArrayList(array( + $this->objFromFixture('File', 'file1'), + $this->objFromFixture('File', 'file2') + )); + + // Anonymous field + $field = new UploadField('MyField'); + $field->setForm($form); + $field->setRecord($record); + $field->setItems($items); + $this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title')); + + // Field with has_one auto-detected + $field = new UploadField('HasOneFile'); + $field->setForm($form); + $field->setRecord($record); + $field->setItems($items); + $this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title'), + 'Allows overwriting of items even when relationship is detected' + ); + } + + function testGetItems() { + $record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); + $form = $this->getMockForm(); + + // Anonymous field + $field = new UploadField('MyField'); + $field->setForm($form); + $field->setRecord($record); + $this->assertEquals(array(), $field->getItems()->column('Title')); + + // Field with has_one auto-detected + $field = new UploadField('HasOneFile'); + $field->setForm($form); + $field->setRecord($record); + $this->assertEquals(array('File1'), $field->getItems()->column('Title')); + + // Field with has_many auto-detected + $field = new UploadField('HasManyFiles'); + $field->setForm($form); + $field->setRecord($record); + $this->assertEquals(array('File2', 'File3'), $field->getItems()->column('Title')); + + // Field with many_many auto-detected + $field = new UploadField('ManyManyFiles'); + $field->setForm($form); + $field->setRecord($record); + $this->assertNotContains('File1',$field->getItems()->column('Title')); + $this->assertNotContains('File2',$field->getItems()->column('Title')); + $this->assertNotContains('File3',$field->getItems()->column('Title')); + $this->assertContains('File4',$field->getItems()->column('Title')); + $this->assertContains('File5',$field->getItems()->column('Title')); + } + + function testReadonly() { + $this->loginWithPermission('ADMIN'); + + $response = $this->get('UploadFieldTest_Controller'); + $this->assertFalse($response->isError()); + + $parser = new CSSContentParser($response->getBody()); + $this->assertFalse((bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-files .ss-uploadfield-item .ss-ui-button'), 'Removes all buttons on items'); + $this->assertFalse((bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-dropzone'), 'Removes dropzone'); + $this->assertFalse((bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-addfile .ss-ui-button'), 'Removes all buttons from "add" area'); + } + + function testDisabled() { + $this->loginWithPermission('ADMIN'); + + $response = $this->get('UploadFieldTest_Controller'); + $this->assertFalse($response->isError()); + + $parser = new CSSContentParser($response->getBody()); + $this->assertFalse((bool)$parser->getBySelector('#DisabledField .ss-uploadfield-files .ss-uploadfield-item .ss-ui-button'), 'Removes all buttons on items'); + $this->assertFalse((bool)$parser->getBySelector('#DisabledField .ss-uploadfield-dropzone'), 'Removes dropzone'); + $this->assertFalse((bool)$parser->getBySelector('#DisabledField .ss-uploadfield-addfile .ss-ui-button'), 'Removes all buttons from "add" area'); + + } + + protected function getMockForm() { + return new Form(new Controller(), 'Form', new FieldList(), new FieldList()); + } + + /** + * @return Array Emulating an entry in the $_FILES superglobal + */ + protected function getUploadFile($tmpFileName = 'UploadFieldTest-testUpload.txt') { + $tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName; + $tmpFileContent = ''; + for($i=0; $i<10000; $i++) $tmpFileContent .= '0'; + file_put_contents($tmpFilePath, $tmpFileContent); + + // emulates the $_FILES array + return array( + 'name' => $tmpFileName, + 'type' => 'text/plaintext', + 'size' => filesize($tmpFilePath), + 'tmp_name' => $tmpFilePath, + 'extension' => 'txt', + 'error' => UPLOAD_ERR_OK, + ); + } + + function setUp() { + parent::setUp(); + + if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH); + + /* Create a test folders for each of the fixture references */ + $folderIDs = $this->allFixtureIDs('Folder'); + foreach($folderIDs as $folderID) { + $folder = DataObject::get_by_id('Folder', $folderID); + if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename"); + } + + /* Create a test files for each of the fixture references */ + $fileIDs = $this->allFixtureIDs('File'); + foreach($fileIDs as $fileID) { + $file = DataObject::get_by_id('File', $fileID); + $fh = fopen(BASE_PATH."/$file->Filename", "w"); + fwrite($fh, str_repeat('x',1000000)); + fclose($fh); + } + } + + function tearDown() { + parent::tearDown(); + + /* Remove the test files that we've created */ + $fileIDs = $this->allFixtureIDs('File'); + foreach($fileIDs as $fileID) { + $file = DataObject::get_by_id('File', $fileID); + if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename"); + } + + /* Remove the test folders that we've crated */ + $folderIDs = $this->allFixtureIDs('Folder'); + foreach($folderIDs as $folderID) { + $folder = DataObject::get_by_id('Folder', $folderID); + if($folder && file_exists(BASE_PATH."/$folder->Filename")) Filesystem::removeFolder(BASE_PATH."/$folder->Filename"); + } + + // Remove left over folders and any files that may exist + if(file_exists('../assets/UploadFieldTest')) Filesystem::removeFolder('../assets/FileTest'); + } + +} + +class UploadFieldTest_Record extends DataObject implements TestOnly { + +static $db = array( + 'Title' => 'Text', +); + +static $has_one = array( + 'HasOneFile' => 'File', +); + +static $has_many = array( + 'HasManyFiles' => 'File', +); + +static $many_many = array( + 'ManyManyFiles' => 'File', +); + +} + +class UploadFieldTest_FileExtension extends DataExtension implements TestOnly { + + function extraStatics() { + return array( + 'has_one' => array('Record' => 'UploadFieldTest_Record') + ); + } + + function canDelete() { + if($this->owner->Name == 'nodelete.txt') return false; + } + + function canEdit() { + if($this->owner->Name == 'noedit.txt') return false; + } + + function canView() { + if($this->owner->Name == 'noview.txt') return false; + } +} + +class UploadFieldTest_Controller extends Controller implements TestOnly { + + protected $template = 'BlankPage'; + + function Form() { + $record = DataObject::get_one('UploadFieldTest_Record', '"Title" = \'Record 1\''); + + $fieldNoRelation = new UploadField('NoRelationField'); + $fieldNoRelation->setFolderName('UploadFieldTest'); + $fieldNoRelation->setRecord($record); + + $fieldHasOne = new UploadField('HasOneFile'); + $fieldHasOne->setFolderName('UploadFieldTest'); + $fieldHasOne->setRecord($record); + + $fieldHasMany = new UploadField('HasManyFiles'); + $fieldHasMany->setFolderName('UploadFieldTest'); + $fieldHasMany->setRecord($record); + + $fieldManyMany = new UploadField('ManyManyFiles'); + $fieldManyMany->setFolderName('UploadFieldTest'); + $fieldManyMany->setRecord($record); + + $fieldReadonly = new UploadField('ReadonlyField'); + $fieldReadonly->setFolderName('UploadFieldTest'); + $fieldReadonly->setRecord($record); + $fieldReadonly = $fieldReadonly->performReadonlyTransformation(); + + $fieldDisabled = new UploadField('DisabledField'); + $fieldDisabled->setFolderName('UploadFieldTest'); + $fieldDisabled->setRecord($record); + $fieldDisabled = $fieldDisabled->performDisabledTransformation(); + + $form = new Form( + $this, + 'Form', + new FieldList( + $fieldNoRelation, + $fieldHasOne, + $fieldHasMany, + $fieldManyMany, + $fieldReadonly, + $fieldDisabled + ), + new FieldList( + new FormAction('submit') + ), + new RequiredFields( + 'NoRelationField', + 'HasOneFile', + 'HasManyFiles', + 'ManyManyFiles', + 'ReadonlyField', + 'DisabledField' + ) + ); + return $form; + } + + function submit($data, $form) { + + } + +} \ No newline at end of file diff --git a/tests/forms/uploadfield/UploadFieldTest.yml b/tests/forms/uploadfield/UploadFieldTest.yml new file mode 100644 index 000000000..3453bcbb6 --- /dev/null +++ b/tests/forms/uploadfield/UploadFieldTest.yml @@ -0,0 +1,45 @@ +Folder: + folder1: + Name: UploadFieldTest +File: + file1: + Title: File1 + Filename: assets/UploadFieldTest/file1.txt + ParentID: =>Folder.folder1 + file2: + Title: File2 + Filename: assets/UploadFieldTest/file2.txt + ParentID: =>Folder.folder1 + file3: + Title: File3 + Filename: assets/UploadFieldTest/file3.txt + ParentID: =>Folder.folder1 + file4: + Title: File4 + Filename: assets/UploadFieldTest/file4.txt + ParentID: =>Folder.folder1 + file5: + Title: File5 + Filename: assets/UploadFieldTest/file5.txt + ParentID: =>Folder.folder1 + file-noview: + Title: noview.txt + Name: noview.txt + Filename: assets/UploadFieldTest/noview.txt + ParentID: =>Folder.folder1 + file-noedit: + Title: noedit.txt + Name: noedit.txt + Filename: assets/UploadFieldTest/noedit.txt + ParentID: =>Folder.folder1 + file-nodelete: + Title: nodelete.txt + Name: nodelete.txt + Filename: assets/UploadFieldTest/nodelete.txt + ParentID: =>Folder.folder1 +UploadFieldTest_Record: + record1: + Title: Record 1 + HasOneFileID: =>File.file1 + HasManyFiles: =>File.file2,=>File.file3 + ManyManyFiles: =>File.file4,=>File.file5,=>File.file-noview,=>File.file-noedit,=>File.file-nodelete \ No newline at end of file diff --git a/thirdparty/javascript-loadimage/.piston.yml b/thirdparty/javascript-loadimage/.piston.yml new file mode 100644 index 000000000..785812235 --- /dev/null +++ b/thirdparty/javascript-loadimage/.piston.yml @@ -0,0 +1,8 @@ +--- +repository_url: git://github.com/blueimp/JavaScript-Load-Image.git +lock: false +repository_class: Piston::Git::Repository +format: 1 +handler: + commit: 19c8e8cf0d4e854238c78174e593b607f04a1521 + branch: master diff --git a/thirdparty/javascript-loadimage/README.md b/thirdparty/javascript-loadimage/README.md new file mode 100644 index 000000000..5f07f9d81 --- /dev/null +++ b/thirdparty/javascript-loadimage/README.md @@ -0,0 +1,112 @@ +# JavaScript Load Image + +## Demo +[JavaScript Load Image Demo](http://blueimp.github.com/JavaScript-Load-Image/) + +## Usage +Include the (minified) JavaScript Load Image script in your HTML markup: + +```html + +``` + +In your application code, use the **loadImage()** function like this: + +```js +document.getElementById('file-input').onchange = function (e) { + window.loadImage( + e.target.files[0], + function (img) { + document.body.appendChild(img); + }, + {maxWidth: 600} + ); +}; +``` + +## Requirements +The JavaScript Load Image function has zero dependencies. + +## API +The **loadImage()** function accepts a [File](https://developer.mozilla.org/en/DOM/File) or [Blob](https://developer.mozilla.org/en/DOM/Blob) object or a simple image URL (e.g. "http://example.org/image.png") as first argument. + +If a [File](https://developer.mozilla.org/en/DOM/File) or [Blob](https://developer.mozilla.org/en/DOM/Blob) is passed as parameter, it returns a HTML **img** element if the browser supports the [URL](https://developer.mozilla.org/en/DOM/window.URL) API or a [FileReader](https://developer.mozilla.org/en/DOM/FileReader) object if supported, or **false**. +It always returns a HTML **img** element when passing an image URL: + +```js +document.getElementById('file-input').onchange = function (e) { + var loadingImage = window.loadImage( + e.target.files[0], + function (img) { + document.body.appendChild(img); + }, + {maxWidth: 600} + ); + if (!loadingImage) { + // Alternative code ... + } +}; +``` + +The **img** element or [FileReader](https://developer.mozilla.org/en/DOM/FileReader) object returned by the **loadImage()** function allows to abort the loading process by setting the **onload** and **onerror** event handlers to null: + +```js +document.getElementById('file-input').onchange = function (e) { + var loadingImage = window.loadImage( + e.target.files[0], + function (img) { + document.body.appendChild(img); + }, + {maxWidth: 600} + ); + loadingImage.onload = loadingImage.onerror = null; +}; +``` + +The second argument must be a **callback** function, which is called when the image has been loaded or an error occurred while loading the image. The callback function is passed one argument, which is either a HTML **img** element, a [canvas](https://developer.mozilla.org/en/HTML/Canvas) element, or an [Event](https://developer.mozilla.org/en/DOM/event) object of type "**error**": + +```js +var imageUrl = "http://example.org/image.png"; +window.loadImage( + imageUrl, + function (img) { + if(img.type === "error") { + console.log("Error loading image " + imageUrl); + } else { + document.body.appendChild(img); + } + }, + {maxWidth: 600} +); +``` + +The optional third argument is a map of options: + +* **maxWidth**: Defines the maximum width of the img/canvas element. +* **maxHeight**: Defines the maximum height of the img/canvas element. +* **minWidth**: Defines the minimum width of the img/canvas element. +* **minHeight**: Defines the minimum height of the img/canvas element. +* **canvas**: Defines if the returned element should be a [canvas](https://developer.mozilla.org/en/HTML/Canvas) element. + +They can be used the following way: + +```js +window.loadImage( + fileOrBlobOrUrl, + function (img) { + document.body.appendChild(img); + }, + { + maxWidth: 600, + maxHeight: 300, + minWidth: 100, + minHeight: 50, + canvas: true + } +); +``` + +All options are optional. By default, the image is returned as HTML **img** element without any image size restrictions. + +## License +The JavaScript Load Image script is released under the [MIT license](http://www.opensource.org/licenses/MIT). diff --git a/thirdparty/javascript-loadimage/load-image.js b/thirdparty/javascript-loadimage/load-image.js new file mode 100644 index 000000000..ea873cda6 --- /dev/null +++ b/thirdparty/javascript-loadimage/load-image.js @@ -0,0 +1,114 @@ +/* + * JavaScript Load Image 1.1.3 + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true */ +/*global window, document, URL, webkitURL, Blob, FileReader, define */ + +(function ($) { + 'use strict'; + + // Loads an image for a given File object. + // Invokes the callback with an img or optional canvas + // element (if supported by the browser) as parameter: + var loadImage = function (file, callback, options) { + var img = document.createElement('img'), + url, + isFile; + if (window.Blob && file instanceof Blob) { + url = loadImage.createObjectURL(file); + isFile = true; + } else { + url = file; + } + img.onerror = callback; + img.onload = function () { + if (isFile) { + loadImage.revokeObjectURL(url); + } + callback(loadImage.scale(img, options)); + }; + if (url) { + img.src = url; + return img; + } else { + return loadImage.readFile(file, function (url) { + img.src = url; + }); + } + }, + urlAPI = (window.createObjectURL && window) || + (window.URL && URL) || (window.webkitURL && webkitURL); + + // Scales the given image (img HTML element) + // using the given options. + // Returns a canvas object if the canvas option is true + // and the browser supports canvas, else the scaled image: + loadImage.scale = function (img, options) { + options = options || {}; + var canvas = document.createElement('canvas'), + scale = Math.max( + (options.minWidth || img.width) / img.width, + (options.minHeight || img.height) / img.height + ); + if (scale > 1) { + img.width = parseInt(img.width * scale, 10); + img.height = parseInt(img.height * scale, 10); + } + scale = Math.min( + (options.maxWidth || img.width) / img.width, + (options.maxHeight || img.height) / img.height + ); + if (scale < 1) { + img.width = parseInt(img.width * scale, 10); + img.height = parseInt(img.height * scale, 10); + } + if (!options.canvas || !canvas.getContext) { + return img; + } + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d') + .drawImage(img, 0, 0, img.width, img.height); + return canvas; + }; + + loadImage.createObjectURL = function (file) { + return urlAPI ? urlAPI.createObjectURL(file) : false; + }; + + loadImage.revokeObjectURL = function (url) { + return urlAPI ? urlAPI.revokeObjectURL(url) : false; + }; + + // Loads a given File object via FileReader interface, + // invokes the callback with a data url: + loadImage.readFile = function (file, callback) { + if (window.FileReader && FileReader.prototype.readAsDataURL) { + var fileReader = new FileReader(); + fileReader.onload = function (e) { + callback(e.target.result); + }; + fileReader.readAsDataURL(file); + return fileReader; + } + return false; + }; + + if (typeof define !== 'undefined' && define.amd) { + // Register as an AMD module: + define('loadImage', function () { + return loadImage; + }); + } else { + // Bind to the global (window) object: + $.loadImage = loadImage; + } +}(this)); diff --git a/thirdparty/javascript-loadimage/load-image.min.js b/thirdparty/javascript-loadimage/load-image.min.js new file mode 100644 index 000000000..4ef11c453 --- /dev/null +++ b/thirdparty/javascript-loadimage/load-image.min.js @@ -0,0 +1,3 @@ +(function(h){var e=function(a,c,d){var b=document.createElement("img"),g,f;window.Blob&&a instanceof Blob?(g=e.createObjectURL(a),f=!0):g=a;b.onerror=c;b.onload=function(){f&&e.revokeObjectURL(g);c(e.scale(b,d))};return g?(b.src=g,b):e.readFile(a,function(a){b.src=a})},f=window.createObjectURL&&window||window.URL&&URL||window.webkitURL&&webkitURL;e.scale=function(a,c){var c=c||{},d=document.createElement("canvas"),b=Math.max((c.minWidth||a.width)/a.width,(c.minHeight||a.height)/a.height);if(1b)a.width=parseInt(a.width*b,10),a.height=parseInt(a.height*b,10);if(!c.canvas||!d.getContext)return a;d.width=a.width;d.height=a.height;d.getContext("2d").drawImage(a,0,0,a.width,a.height);return d};e.createObjectURL=function(a){return f?f.createObjectURL(a):!1};e.revokeObjectURL=function(a){return f?f.revokeObjectURL(a):!1};e.readFile=function(a,c){if(window.FileReader&& +FileReader.prototype.readAsDataURL){var d=new FileReader;d.onload=function(a){c(a.target.result)};d.readAsDataURL(a);return d}return!1};"undefined"!==typeof define&&define.amd?define("loadImage",function(){return e}):h.loadImage=e})(this); \ No newline at end of file diff --git a/thirdparty/javascript-loadimage/package.json b/thirdparty/javascript-loadimage/package.json new file mode 100644 index 000000000..30e0ff839 --- /dev/null +++ b/thirdparty/javascript-loadimage/package.json @@ -0,0 +1,45 @@ +{ + "name": "blueimp-load-image", + "version": "1.1.3", + "title": "JavaScript Load Image", + "description": "JavaScript Load Image is a function to load images provided as File or Blob objects or via URL. It returns an optionally scaled HTML img or canvas element.", + "keywords": [ + "javascript", + "load", + "loading", + "image", + "file", + "blob", + "url", + "scale", + "scaling", + "img", + "canvas" + ], + "homepage": "https://github.com/blueimp/JavaScript-Load-Image", + "author": { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + }, + "maintainers": [ + { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/blueimp/JavaScript-Load-Image.git" + }, + "bugs": "https://github.com/blueimp/JavaScript-Load-Image/issues", + "licenses": [ + { + "type": "MIT", + "url": "http://www.opensource.org/licenses/MIT" + } + ], + "files": [ + "load-image.js" + ], + "main": "load-image.js" +} diff --git a/thirdparty/javascript-templates/.piston.yml b/thirdparty/javascript-templates/.piston.yml new file mode 100644 index 000000000..c3ce85934 --- /dev/null +++ b/thirdparty/javascript-templates/.piston.yml @@ -0,0 +1,8 @@ +--- +repository_url: git://github.com/blueimp/JavaScript-Templates.git +lock: false +repository_class: Piston::Git::Repository +format: 1 +handler: + commit: ddb6a9c25c3a1089c22af6e8a431cefced39e31c + branch: master diff --git a/thirdparty/javascript-templates/README.md b/thirdparty/javascript-templates/README.md new file mode 100644 index 000000000..d92357fc3 --- /dev/null +++ b/thirdparty/javascript-templates/README.md @@ -0,0 +1,309 @@ +# JavaScript Templates + +## Demo +[JavaScript Templates Demo](http://blueimp.github.com/JavaScript-Templates/) + +## Usage + +### Client-side +Include the (minified) JavaScript Templates script in your HTML markup: + +```html + +``` + +Add a script section with type **"text/html"** and your template definition as content: + +```html + +``` + +**"o"** (the lowercase letter) is a reference to the data parameter of the template function (see the API section on how to modify this identifier). + +In your application code, create a JavaScript object to use as data for the template: + +```js +var data = { + "title": "JavaScript Templates", + "license": { + "name": "MIT license", + "url": "http://www.opensource.org/licenses/MIT" + }, + "features": [ + "lightweight & fast", + "powerful", + "zero dependencies" + ] +}; +``` + +In a real application, this data could be the result of retrieving a [JSON](http://json.org/) resource. + +Render the result by calling the **tmpl()** method with the id of the template and the data object as arguments: + +```js +document.getElementById("result").innerHTML = tmpl("tmpl-demo", data); +``` + +### Server-side + +The following is an example how to use the JavaScript Templates engine on the server-side with [node.js](http://nodejs.org/). + +Create a new directory and add the **tmpl.js** file. Or alternatively, install the **blueimp-tmpl** package with [npm](http://npmjs.org/): + +```sh +npm install blueimp-tmpl +``` + +Add a file **template.html** with the following content: + +```html + +{%=o.title%} +

      {%=o.title%}

      +

      Features

      + +``` + +Add a file **server.js** with the following content: + +```js +require("http").createServer(function (req, res) { + var fs = require("fs"), + // The tmpl module exports the tmpl() function: + tmpl = require("./tmpl").tmpl, + // Use the following version if you installed the package with npm: + // tmpl = require("blueimp-tmpl").tmpl, + // Sample data: + data = { + "title": "JavaScript Templates", + "url": "https://github.com/blueimp/JavaScript-Templates", + "features": [ + "lightweight & fast", + "powerful", + "zero dependencies" + ] + }; + // Override the template loading method: + tmpl.load = function (id) { + var filename = id + ".html"; + console.log("Loading " + filename); + return fs.readFileSync(filename, "utf8"); + }; + res.writeHead(200, {"Content-Type": "text/html"}); + // Render the content: + res.end(tmpl("template", data)); +}).listen(8080, "localhost"); +console.log("Server running at http://localhost:8080/"); +``` + +Run the application with the following command: + +```sh +node server.js +``` + +## Requirements +The JavaScript Templates script has zero dependencies. + +## API + +### tmpl() function +The **tmpl()** function is added to the global **window** object and can be called as global function: + +```js +var result = tmpl("tmpl-demo", data); +``` + +The **tmpl()** function can be called with the id of a template, or with a template string: + +```js +var result = tmpl("

      {%=o.title%}

      ", data); +``` + +If called without second argument, **tmpl()** returns a reusable template function: + +```js +var func = tmpl("

      {%=o.title%}

      "); +document.getElementById("result").innerHTML = func(data); +``` + +### Templates cache +Templates loaded by id are cached in the map **tmpl.cache**, which can be modified: + +```js +var func = tmpl("tmpl-demo"); +var cached = typeof tmpl.cache["tmpl-demo"] === "function"; // true + +tmpl.cache["tmpl-demo"] = tmpl("

      {%=o.title%}

      "); +var result = tmpl("tmpl-demo", {title: "JS"}); // Renders "

      JS

      " +``` + +### Output encoding +The method **tmpl.encode** is used to escape HTML special characters in template output: + +```js +var output = tmpl.encode("<>&\"\x00"); // Renders "<>&"" +``` + +**tmpl.encode** makes use of the regular expression **tmpl.encReg** and the encoding map **tmpl.encMap** to match and replace special characters, which can be modified to change the behavior of the output encoding: + +```js +// Add single quotes to the encoding rules: +tmpl.encReg = /[<>&"'\x00]/g; +tmpl.encMap["'"] = "'"; + +var output = tmpl.encode("<>&\"'\x00"); // Renders "<>&"'" +``` + +### Local helper variables +The local variables available inside the templates are the following: + +* **o**: The data object given as parameter to the template function (see the next section on how to modify the parameter name). +* **_s**: The string for the rendered result content. +* **_t**: A reference to the **tmpl** function object. +* **_e**: A reference to the **tmpl.encode** method. +* **print**: Function to add content to the rendered result string. +* **include**: Function to include the return value of a different template in the result. + +To introduce additional local helper variables, the string **tmpl.helper** can be extended. The following adds a convenience function for *console.log* and a streaming function, that streams the template rendering result back to the callback argument (note the comma at the beginning of each variable declaration): + +```js +tmpl.helper += ",log=function(){console.log.apply(console, arguments)}" + + ",st='',stream=function(cb){var l=st.length;st=_s;cb( _s.slice(l));}"; +``` + +Those new helper functions could be used to stream the template contents to the console output: + +```html + +``` + +### Template function argument +The generated template functions accept one argument, which is the data object given to the **tmpl(id, data)** function. This argument is available inside the template definitions as parameter **o** (the lowercase letter). + +The argument name can be modified by overriding **tmpl.arg**: + +```js +tmpl.arg = "p"; + +// Renders "

      JavaScript Templates

      ": +var result = tmpl("

      {%=p.title%}

      ", {title: "JavaScript Templates"}); +``` + +### Template parsing +The template contents are matched and replaced using the regular expression **tmpl.regexp** and the replacement function **tmpl.func**. The replacement function operates based on the [parenthesized submatch strings](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_function_as_a_parameter). + +To use different tags for the template syntax, override **tmpl.regexp** with a modified regular expression, by exchanging all occurrences of "**\\{%**" and "**%\\}**", e.g. with "**\\[%**" and "**%\\]**": + +```js +tmpl.regexp = /(\s+)|('|\\)(?![^%]*%\])|(?:\[%(=|#)(.+?)%\])|(\[%)|(%\])/g; +``` + +## Templates syntax + +### Interpolation +Print variable with HTML special characters escaped: + +```html +

      {%=o.title%}

      +``` + +Print variable without escaping: + +```html +

      {%#o.user_id%}

      +``` + +Print output of function calls: + +```html +Website +``` + +Use dot notation to print nested properties: + +```html +{%=o.author.name%} +``` + +Note that the JavaScript Templates engine prints **falsy** values as empty strings. +That is, **undefined**, **null**, **false**, **0** and **NaN** will all be converted to **''**. +To be able to print e.g. the number 0, convert it to a String before using it as an output variable: + +```html +

      {%=0+''%}

      +``` + +### Evaluation +Use **print(str)** to add escaped content to the output: + +```html +Year: {% var d=new Date(); print(d.getFullYear()); %} +``` + +Use **print(str, true)** to add unescaped content to the output: + +```html +{% print("Fast & powerful", true); %} +``` + +Use **include(str, obj)** to include content from a different template: + +```html +
      +{% include('tmpl-link', {name: "Website", url: "http://example.org"}); %} +
      +``` + +If else condition: + +```html +{% if (o.author.url) { %} + {%=o.author.name%} +{% } else { %} + No author url. +{% } %} +``` + +For loop: + +```html + +``` + +## License +The JavaScript Templates script is released under the [MIT license](http://www.opensource.org/licenses/MIT). diff --git a/thirdparty/javascript-templates/package.json b/thirdparty/javascript-templates/package.json new file mode 100644 index 000000000..3f6721bc9 --- /dev/null +++ b/thirdparty/javascript-templates/package.json @@ -0,0 +1,40 @@ +{ + "name": "blueimp-tmpl", + "version": "1.0.2", + "title": "JavaScript Templates", + "description": "< 1KB lightweight, fast & powerful JavaScript templating engine with zero dependencies. Compatible with server-side environments like node.js, module loaders like RequireJS and all web browsers.", + "keywords": [ + "javascript", + "templates", + "templating" + ], + "homepage": "https://github.com/blueimp/JavaScript-Templates", + "author": { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + }, + "maintainers": [ + { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/blueimp/JavaScript-Templates.git" + }, + "bugs": "https://github.com/blueimp/JavaScript-Templates/issues", + "licenses": [ + { + "type": "MIT", + "url": "http://www.opensource.org/licenses/MIT" + } + ], + "scripts": { + "test": "node ./test/test.js" + }, + "files": [ + "tmpl.js" + ], + "main": "tmpl.js" +} diff --git a/thirdparty/javascript-templates/tmpl.js b/thirdparty/javascript-templates/tmpl.js new file mode 100644 index 000000000..1783074be --- /dev/null +++ b/thirdparty/javascript-templates/tmpl.js @@ -0,0 +1,85 @@ +/* + * JavaScript Templates 1.0.2 + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Inspired by John Resig's JavaScript Micro-Templating: + * http://ejohn.org/blog/javascript-micro-templating/ + */ + +/*jslint evil: true, regexp: true */ +/*global document, define */ + +(function ($) { + "use strict"; + var tmpl = function (str, data) { + var f = !/[^\-\w]/.test(str) ? tmpl.cache[str] = tmpl.cache[str] || + tmpl(tmpl.load(str)) : + new Function( + tmpl.arg, + ("var _s=''" + tmpl.helper + ";_s+='" + + str.replace(tmpl.regexp, tmpl.func) + + "';return _s;").split("_s+='';").join("") + ); + f.tmpl = f.tmpl || tmpl; + return data ? f(data) : f; + }; + tmpl.cache = {}; + tmpl.load = function (id) { + return document.getElementById(id).innerHTML; + }; + tmpl.regexp = /(\s+)|('|\\)(?![^%]*%\})|(?:\{%(=|#)(.+?)%\})|(\{%)|(%\})/g; + tmpl.func = function (s, p1, p2, p3, p4, p5, p6, o, str) { + if (p1) { // whitespace + return o && o + s.length !== str.length ? " " : ""; + } + if (p2) { // single quote or backslash + return "\\" + s; + } + if (p3) { // interpolation: {%=prop%}, or unescaped: {%#prop%} + if (p3 === "=") { + return "'+_e(" + p4 + ")+'"; + } + return "'+(" + p4 + "||'')+'"; + } + if (p5) { // evaluation start tag: {% + return "';"; + } + if (p6) { // evaluation end tag: %} + return "_s+='"; + } + }; + tmpl.encReg = /[<>&"\x00]/g; + tmpl.encMap = { + "<": "<", + ">": ">", + "&": "&", + "\"": """, + "\x00": "" + }; + tmpl.encode = function (s) { + return String(s || "").replace( + tmpl.encReg, + function (c) { + return tmpl.encMap[c]; + } + ); + }; + tmpl.arg = "o"; + tmpl.helper = ",_t=arguments.callee.tmpl,_e=_t.encode" + + ",print=function(s,e){_s+=e&&(s||'')||_e(s);}" + + ",include=function(s,d){_s+=_t(s,d);}"; + if (typeof define === "function" && define.amd) { + // Register as an AMD module: + define("tmpl", function () { + return tmpl; + }); + } else { + $.tmpl = tmpl; + } +}(this)); diff --git a/thirdparty/javascript-templates/tmpl.min.js b/thirdparty/javascript-templates/tmpl.min.js new file mode 100644 index 000000000..53146973b --- /dev/null +++ b/thirdparty/javascript-templates/tmpl.min.js @@ -0,0 +1,2 @@ +(function(e){var a=function(b,d){var c=!/[^\-\w]/.test(b)?a.cache[b]=a.cache[b]||a(a.load(b)):new Function(a.arg,("var _s=''"+a.helper+";_s+='"+b.replace(a.regexp,a.func)+"';return _s;").split("_s+='';").join(""));c.tmpl=c.tmpl||a;return d?c(d):c};a.cache={};a.load=function(a){return document.getElementById(a).innerHTML};a.regexp=/(\s+)|('|\\)(?![^%]*%\})|(?:\{%(=|#)(.+?)%\})|(\{%)|(%\})/g;a.func=function(a,d,c,f,g,e,i,h,j){if(d)return h&&h+a.length!==j.length?" ":"";if(c)return"\\"+a;if(f)return"="=== +f?"'+_e("+g+")+'":"'+("+g+"||'')+'";if(e)return"';";if(i)return"_s+='"};a.encReg=/[<>&"\x00]/g;a.encMap={"<":"<",">":">","&":"&",'"':""","\x00":""};a.encode=function(b){return(""+(b||"")).replace(a.encReg,function(b){return a.encMap[b]})};a.arg="o";a.helper=",_t=arguments.callee.tmpl,_e=_t.encode,print=function(s,e){_s+=e&&(s||'')||_e(s);},include=function(s,d){_s+=_t(s,d);}";"function"===typeof define&&define.amd?define("tmpl",function(){return a}):e.tmpl=a})(this); \ No newline at end of file diff --git a/thirdparty/jquery-fileupload/.piston.yml b/thirdparty/jquery-fileupload/.piston.yml new file mode 100644 index 000000000..271894c78 --- /dev/null +++ b/thirdparty/jquery-fileupload/.piston.yml @@ -0,0 +1,8 @@ +--- +repository_url: git://github.com/blueimp/jQuery-File-Upload.git +lock: false +repository_class: Piston::Git::Repository +format: 1 +handler: + commit: 57f986dad16bb0da500ca2b025a595da0a2b8719 + branch: master diff --git a/thirdparty/jquery-fileupload/README.md b/thirdparty/jquery-fileupload/README.md new file mode 100644 index 000000000..bb7151a18 --- /dev/null +++ b/thirdparty/jquery-fileupload/README.md @@ -0,0 +1,68 @@ +# jQuery File Upload Plugin + +## Demo +[Demo File Upload](http://blueimp.github.com/jQuery-File-Upload/) + +## Setup instructions +* [How to setup the plugin on your website](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) +* [How to use only the basic plugin (minimal setup guide).](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin) + +## Features +* **Multiple file upload:** + Allows to select multiple files at once and upload them simultaneously. +* **Drag & Drop support:** + Allows to upload files by dragging them from your desktop or filemanager and dropping them on your browser window. +* **Upload progress bar:** + Shows a progress bar indicating the upload progress for individual files and for all uploads combined. +* **Cancelable uploads:** + Individual file uploads can be canceled to stop the upload progress. +* **Resumable uploads:** + Aborted uploads can be resumed with browsers supporting the Blob API. +* **Chunked uploads:** + Large files can be uploaded in smaller chunks with browsers supporting the Blob API. +* **Preview images:** + A preview of image files can be displayed before uploading with browsers supporting the required HTML5 APIs. +* **No browser plugins (e.g. Adobe Flash) required:** + The implementation is based on open standards like HTML5 and JavaScript and requires no additional browser plugins. +* **Graceful fallback for legacy browsers:** + Uploads files via XMLHttpRequests if supported and uses iframes as fallback for legacy browsers. +* **HTML file upload form fallback:** + Shows a standard HTML file upload form if JavaScript is disabled. +* **Cross-site file uploads:** + Supports uploading files to a different domain with Cross-site XMLHttpRequests. +* **Multiple plugin instances:** + Allows to use multiple plugin instances on the same webpage. +* **Customizable and extensible:** + Provides an API to set individual options and define callBack methods for various upload events. +* **Multipart and file contents stream uploads:** + Files can be uploaded as standard "multipart/form-data" or file contents stream (HTTP PUT file upload). +* **Compatible with any server-side application platform:** + Works with Google App Engine (Python, Java), Ruby on Rails, PHP and any other platform that supports HTTP file uploads. + +## Requirements +* [jQuery](http://jquery.com/) v. 1.6+ +* [jQuery UI widget factory](http://wiki.jqueryui.com/w/page/12138135/Widget%20factory) v. 1.8.16+ +* [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/jquery.iframe-transport.js) (included) +* [JavaScript Load Image function](http://blueimp.github.com/JavaScript-Load-Image) v. 1.1.3+ (optional) +* [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates) v. 1.0.2+ (optional) + +The jQuery UI widget factory is a requirement for the basic File Upload plugin, but very lightweight without any other dependencies. +The UI version of the File Upload plugin also requires the JavaScript Templates engine and the JavaScript Load Image function (for the upload image previews). These dependencies are marked as optional, as the basic File Upload plugin can be used without them and the UI version of the plugin can be extended to override these dependencies with alternative solutions. + +The repository also includes the [jQuery XDomainRequest Transport Plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/jquery.xdr-transport.js), which is required for Cross-domain AJAX requests in Microsoft Internet Explorer >= 8. It is only included for the [Demo](http://blueimp.github.com/jQuery-File-Upload/), which makes use of Cross-domain DELETE requests (GET requests for IE) to delete uploaded files from the Demo File Upload service. + +[Cross-domain File Uploads](https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads) using the [Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/jquery.iframe-transport.js) require a redirect back to the origin server to retrieve the upload results. The example implementation makes use of [result.html](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/result.html) as redirect page. See also the example code in [application.js](https://github.com/blueimp/jQuery-File-Upload/blob/master/application.js) as well as the explanation of all [files in the repository](https://github.com/blueimp/jQuery-File-Upload/wiki/Plugin-files). + +## Browser Support (tested versions) +* Google Chrome - 7.0+ +* Apple Safari - 4.0+ +* Mozilla Firefox - 3.0+ +* Opera - 10.0+ +* Microsoft Internet Explorer 6.0+ + +Drag & Drop is only supported on Google Chrome, Firefox 4.0+ and Safari 5.0+. +Microsoft Internet Explorer has no support for multiple file selection or upload progress. +[Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +## License +Released under the [MIT license](http://www.opensource.org/licenses/MIT). diff --git a/thirdparty/jquery-fileupload/cors/jquery.postmessage-transport.js b/thirdparty/jquery-fileupload/cors/jquery.postmessage-transport.js new file mode 100644 index 000000000..4c0c6a803 --- /dev/null +++ b/thirdparty/jquery-fileupload/cors/jquery.postmessage-transport.js @@ -0,0 +1,108 @@ +/* + * jQuery postMessage Transport Plugin 1.0 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint unparam: true, nomen: true */ +/*global jQuery, window, document */ + +(function ($) { + 'use strict'; + + var counter = 0, + names = [ + 'accepts', + 'cache', + 'contents', + 'contentType', + 'crossDomain', + 'data', + 'dataType', + 'headers', + 'ifModified', + 'mimeType', + 'password', + 'processData', + 'timeout', + 'traditional', + 'type', + 'url', + 'username' + ], + convert = function (p) { + return p; + }; + + $.ajaxSetup({ + converters: { + 'postmessage text': convert, + 'postmessage json': convert, + 'postmessage html': convert + } + }); + + $.ajaxTransport('postmessage', function (options) { + if (options.postMessage && window.postMessage) { + var iframe, + loc = $('').prop('href', options.postMessage)[0], + target = loc.protocol + '//' + loc.host, + xhrUpload = options.xhr().upload; + return { + send: function (_, completeCallback) { + var message = { + id: 'postmessage-transport-' + (counter += 1) + }, + eventName = 'message.' + message.id; + iframe = $( + '' + ).bind('load', function () { + $.each(names, function (i, name) { + message[name] = options[name]; + }); + message.dataType = message.dataType.replace('postmessage ', ''); + $(window).bind(eventName, function (e) { + e = e.originalEvent; + var data = e.data, + ev; + if (e.origin === target && data.id === message.id) { + if (data.type === 'progress') { + ev = document.createEvent('Event'); + ev.initEvent(data.type, false, true); + $.extend(ev, data); + xhrUpload.dispatchEvent(ev); + } else { + completeCallback( + data.status, + data.statusText, + {postmessage: data.result}, + data.headers + ); + iframe.remove(); + $(window).unbind(eventName); + } + } + }); + iframe[0].contentWindow.postMessage( + message, + target + ); + }).appendTo(document.body); + }, + abort: function () { + if (iframe) { + iframe.remove(); + } + } + }; + } + }); + +}(jQuery)); diff --git a/thirdparty/jquery-fileupload/cors/jquery.xdr-transport.js b/thirdparty/jquery-fileupload/cors/jquery.xdr-transport.js new file mode 100644 index 000000000..3d14353d6 --- /dev/null +++ b/thirdparty/jquery-fileupload/cors/jquery.xdr-transport.js @@ -0,0 +1,76 @@ +/* + * jQuery XDomainRequest Transport Plugin 1.0.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Based on Julian Aubourg's ajaxHooks xdr.js: + * https://github.com/jaubourg/ajaxHooks/ + */ + +/*jslint unparam: true */ +/*global jQuery, window, XDomainRequest */ + +(function ($) { + 'use strict'; + if (window.XDomainRequest) { + $.ajaxTransport(function (s) { + if (s.crossDomain && s.async) { + if (s.timeout) { + s.xdrTimeout = s.timeout; + delete s.timeout; + } + var xdr; + return { + send: function (headers, completeCallback) { + function callback(status, statusText, responses, responseHeaders) { + xdr.onload = xdr.onerror = xdr.ontimeout = jQuery.noop; + xdr = null; + completeCallback(status, statusText, responses, responseHeaders); + } + xdr = new XDomainRequest(); + // XDomainRequest only supports GET and POST: + if (s.type === 'DELETE') { + s.url = s.url + (/\?/.test(s.url) ? '&' : '?') + + '_method=DELETE'; + s.type = 'POST'; + } else if (s.type === 'PUT') { + s.url = s.url + (/\?/.test(s.url) ? '&' : '?') + + '_method=PUT'; + s.type = 'POST'; + } + xdr.open(s.type, s.url); + xdr.onload = function () { + callback( + 200, + 'OK', + {text: xdr.responseText}, + 'Content-Type: ' + xdr.contentType + ); + }; + xdr.onerror = function () { + callback(404, 'Not Found'); + }; + if (s.xdrTimeout) { + xdr.ontimeout = function () { + callback(0, 'timeout'); + }; + xdr.timeout = s.xdrTimeout; + } + xdr.send((s.hasContent && s.data) || null); + }, + abort: function () { + if (xdr) { + xdr.onerror = jQuery.noop(); + xdr.abort(); + } + } + }; + } + }); + } +}(jQuery)); diff --git a/thirdparty/jquery-fileupload/jquery.fileupload-ui.js b/thirdparty/jquery-fileupload/jquery.fileupload-ui.js new file mode 100644 index 000000000..5f4f71b68 --- /dev/null +++ b/thirdparty/jquery-fileupload/jquery.fileupload-ui.js @@ -0,0 +1,538 @@ +/* + * jQuery File Upload User Interface Plugin 6.0.3 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global window, document, URL, webkitURL, FileReader, jQuery */ + +(function ($) { + 'use strict'; + + // The UI version extends the basic fileupload widget and adds + // a complete user interface based on the given upload/download + // templates. + $.widget('blueimpUI.fileupload', $.blueimp.fileupload, { + + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The following option limits the number of files that are + // allowed to be uploaded using this widget: + maxNumberOfFiles: undefined, + // The maximum allowed file size: + maxFileSize: undefined, + // The minimum allowed file size: + minFileSize: 1, + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /.+$/i, + // The regular expression to define for which files a preview + // image is shown, matched against the file type: + previewFileTypes: /^image\/(gif|jpeg|png)$/, + // The maximum file size for preview images: + previewMaxFileSize: 5000000, // 5MB + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // By default, preview images are displayed as canvas elements + // if supported by the browser. Set the following option to false + // to always display preview images as img elements: + previewAsCanvas: true, + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + var that = $(this).data('fileupload'), + files = data.files; + that._adjustMaxNumberOfFiles(-files.length); + data.isAdjusted = true; + data.files.valid = data.isValidated = that._validate(files); + data.context = that._renderUpload(files) + .appendTo(that._files) + .data('data', data); + // Force reflow: + that._reflow = that._transition && data.context[0].offsetWidth; + data.context.addClass('in'); + if ((that.options.autoUpload || data.autoUpload) && + data.isValidated) { + data.submit(); + } + }, + // Callback for the start of each file upload request: + send: function (e, data) { + if (!data.isValidated) { + var that = $(this).data('fileupload'); + if (!data.isAdjusted) { + that._adjustMaxNumberOfFiles(-data.files.length); + } + if (!that._validate(data.files)) { + return false; + } + } + if (data.context && data.dataType && + data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context.find('.progressbar div').css( + 'width', + parseInt(100, 10) + '%' + ); + } + }, + // Callback for successful uploads: + done: function (e, data) { + var that = $(this).data('fileupload'), + template, + preview; + if (data.context) { + data.context.each(function (index) { + var file = ($.isArray(data.result) && + data.result[index]) || {error: 'emptyResult'}; + if (file.error) { + that._adjustMaxNumberOfFiles(1); + } + that._transitionCallback( + $(this).removeClass('in'), + function (node) { + template = that._renderDownload([file]); + preview = node + .find('.preview img, .preview canvas'); + if (preview.length) { + template.find('.preview img') + .prop('width', preview.prop('width')) + .prop('height', preview.prop('height')); + } + template + .replaceAll(node); + // Force reflow: + that._reflow = that._transition && + template[0].offsetWidth; + template.addClass('in'); + } + ); + }); + } else { + template = that._renderDownload(data.result) + .appendTo(that._files); + // Force reflow: + that._reflow = that._transition && template[0].offsetWidth; + template.addClass('in'); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + var that = $(this).data('fileupload'), + template; + that._adjustMaxNumberOfFiles(data.files.length); + if (data.context) { + data.context.each(function (index) { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = file.error || data.errorThrown || + true; + that._transitionCallback( + $(this).removeClass('in'), + function (node) { + template = that._renderDownload([file]) + .replaceAll(node); + // Force reflow: + that._reflow = that._transition && + template[0].offsetWidth; + template.addClass('in'); + } + ); + } else { + that._transitionCallback( + $(this).removeClass('in'), + function (node) { + node.remove(); + } + ); + } + }); + } else if (data.errorThrown !== 'abort') { + that._adjustMaxNumberOfFiles(-data.files.length); + data.context = that._renderUpload(data.files) + .appendTo(that._files) + .data('data', data); + // Force reflow: + that._reflow = that._transition && data.context[0].offsetWidth; + data.context.addClass('in'); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + if (data.context) { + data.context.find('.progressbar div').css( + 'width', + parseInt(data.loaded / data.total * 100, 10) + '%' + ); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + $(this).find('.fileupload-progressbar div').css( + 'width', + parseInt(data.loaded / data.total * 100, 10) + '%' + ); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function () { + $(this).find('.fileupload-progressbar') + .addClass('in').find('div').css('width', '0%'); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function () { + $(this).find('.fileupload-progressbar') + .removeClass('in').find('div').css('width', '0%'); + }, + // Callback for file deletion: + destroy: function (e, data) { + var that = $(this).data('fileupload'); + if (data.url) { + $.ajax(data); + } + that._adjustMaxNumberOfFiles(1); + that._transitionCallback( + data.context.removeClass('in'), + function (node) { + node.remove(); + } + ); + } + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = decodeURIComponent(url.split('/').pop()) + .replace(/:/g, '-'), + type = 'application/octet-stream'; + link.bind('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (err) {} + }); + }, + + _adjustMaxNumberOfFiles: function (operand) { + if (typeof this.options.maxNumberOfFiles === 'number') { + this.options.maxNumberOfFiles += operand; + if (this.options.maxNumberOfFiles < 1) { + this._disableFileInputButton(); + } else { + this._enableFileInputButton(); + } + } + }, + + _formatFileSize: function (bytes) { + if (typeof bytes !== 'number') { + return ''; + } + if (bytes >= 1000000000) { + return (bytes / 1000000000).toFixed(2) + ' GB'; + } + if (bytes >= 1000000) { + return (bytes / 1000000).toFixed(2) + ' MB'; + } + return (bytes / 1000).toFixed(2) + ' KB'; + }, + + _hasError: function (file) { + if (file.error) { + return file.error; + } + // The number of added files is subtracted from + // maxNumberOfFiles before validation, so we check if + // maxNumberOfFiles is below 0 (instead of below 1): + if (this.options.maxNumberOfFiles < 0) { + return 'maxNumberOfFiles'; + } + // Files are accepted if either the file type or the file name + // matches against the acceptFileTypes regular expression, as + // only browsers with support for the File API report the type: + if (!(this.options.acceptFileTypes.test(file.type) || + this.options.acceptFileTypes.test(file.name))) { + return 'acceptFileTypes'; + } + if (this.options.maxFileSize && + file.size > this.options.maxFileSize) { + return 'maxFileSize'; + } + if (typeof file.size === 'number' && + file.size < this.options.minFileSize) { + return 'minFileSize'; + } + return null; + }, + + _validate: function (files) { + var that = this, + valid = !!files.length; + $.each(files, function (index, file) { + file.error = that._hasError(file); + if (file.error) { + valid = false; + } + }); + return valid; + }, + + _renderTemplate: function (func, files) { + return $(this.options.templateContainer).html(func({ + files: files, + formatFileSize: this._formatFileSize, + options: this.options + })).children(); + }, + + _renderUpload: function (files) { + var that = this, + options = this.options, + nodes = this._renderTemplate(options.uploadTemplate, files); + nodes.find('.preview span').each(function (index, node) { + var file = files[index]; + if (options.previewFileTypes.test(file.type) && + (!options.previewMaxFileSize || + file.size < options.previewMaxFileSize)) { + window.loadImage( + files[index], + function (img) { + $(node).append(img); + // Force reflow: + that._reflow = that._transition && + node.offsetWidth; + $(node).addClass('in'); + }, + { + maxWidth: options.previewMaxWidth, + maxHeight: options.previewMaxHeight, + canvas: options.previewAsCanvas + } + ); + } + }); + return nodes; + }, + + _renderDownload: function (files) { + var nodes = this._renderTemplate( + this.options.downloadTemplate, + files + ); + nodes.find('a').each(this._enableDragToDesktop); + return nodes; + }, + + _startHandler: function (e) { + e.preventDefault(); + var button = $(this), + tmpl = button.closest('.template-upload'), + data = tmpl.data('data'); + if (data && data.submit && !data.jqXHR && data.submit()) { + button.prop('disabled', true); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var tmpl = $(this).closest('.template-upload'), + data = tmpl.data('data') || {}; + if (!data.jqXHR) { + data.errorThrown = 'abort'; + e.data.fileupload._trigger('fail', e, data); + } else { + data.jqXHR.abort(); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(this); + e.data.fileupload._trigger('destroy', e, { + context: button.closest('.template-download'), + url: button.attr('data-url'), + type: button.attr('data-type'), + dataType: e.data.fileupload.options.dataType + }); + }, + + _transitionCallback: function (node, callback) { + var that = this; + if (this._transition && node.hasClass('fade')) { + node.bind( + this._transitionEnd, + function (e) { + // Make sure we don't respond to other transitions events + // in the container element, e.g. from button elements: + if (e.target === node[0]) { + node.unbind(that._transitionEnd); + callback.call(that, node); + } + } + ); + } else { + callback.call(this, node); + } + }, + + _initTransitionSupport: function () { + var that = this, + style = (document.body || document.documentElement).style, + suffix = '.' + that.options.namespace; + that._transition = style.transition !== undefined || + style.WebkitTransition !== undefined || + style.MozTransition !== undefined || + style.MsTransition !== undefined || + style.OTransition !== undefined; + if (that._transition) { + that._transitionEnd = [ + 'MSTransitionEnd', + 'webkitTransitionEnd', + 'transitionend', + 'oTransitionEnd' + ].join(suffix + ' ') + suffix; + } + }, + + _initButtonBarEventHandlers: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this._files, + ns = this.options.namespace; + fileUploadButtonBar.find('.start') + .bind('click.' + ns, function (e) { + e.preventDefault(); + filesList.find('.start button').click(); + }); + fileUploadButtonBar.find('.cancel') + .bind('click.' + ns, function (e) { + e.preventDefault(); + filesList.find('.cancel button').click(); + }); + fileUploadButtonBar.find('.delete') + .bind('click.' + ns, function (e) { + e.preventDefault(); + filesList.find('.delete input:checked') + .siblings('button').click(); + }); + fileUploadButtonBar.find('.toggle') + .bind('change.' + ns, function (e) { + filesList.find('.delete input').prop( + 'checked', + $(this).is(':checked') + ); + }); + }, + + _destroyButtonBarEventHandlers: function () { + this.element.find('.fileupload-buttonbar button') + .unbind('click.' + this.options.namespace); + this.element.find('.fileupload-buttonbar .toggle') + .unbind('change.' + this.options.namespace); + }, + + _initEventHandlers: function () { + $.blueimp.fileupload.prototype._initEventHandlers.call(this); + var eventData = {fileupload: this}; + this._files + .delegate( + '.start button', + 'click.' + this.options.namespace, + eventData, + this._startHandler + ) + .delegate( + '.cancel button', + 'click.' + this.options.namespace, + eventData, + this._cancelHandler + ) + .delegate( + '.delete button', + 'click.' + this.options.namespace, + eventData, + this._deleteHandler + ); + this._initButtonBarEventHandlers(); + this._initTransitionSupport(); + }, + + _destroyEventHandlers: function () { + this._destroyButtonBarEventHandlers(); + this._files + .undelegate('.start button', 'click.' + this.options.namespace) + .undelegate('.cancel button', 'click.' + this.options.namespace) + .undelegate('.delete button', 'click.' + this.options.namespace); + $.blueimp.fileupload.prototype._destroyEventHandlers.call(this); + }, + + _enableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', false) + .parent().removeClass('disabled'); + }, + + _disableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', true) + .parent().addClass('disabled'); + }, + + _initTemplates: function () { + this.options.templateContainer = document.createElement( + this._files.prop('nodeName') + ); + this.options.uploadTemplate = window.tmpl('template-upload'); + this.options.downloadTemplate = window.tmpl('template-download'); + }, + + _initFiles: function () { + this._files = this.element.find('.files'); + }, + + _create: function () { + this._initFiles(); + $.blueimp.fileupload.prototype._create.call(this); + this._initTemplates(); + }, + + destroy: function () { + $.blueimp.fileupload.prototype.destroy.call(this); + }, + + enable: function () { + $.blueimp.fileupload.prototype.enable.call(this); + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + }, + + disable: function () { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + $.blueimp.fileupload.prototype.disable.call(this); + } + + }); + +}(jQuery)); diff --git a/thirdparty/jquery-fileupload/jquery.fileupload.js b/thirdparty/jquery-fileupload/jquery.fileupload.js new file mode 100644 index 000000000..729b91d75 --- /dev/null +++ b/thirdparty/jquery-fileupload/jquery.fileupload.js @@ -0,0 +1,831 @@ +/* + * jQuery File Upload Plugin 5.5.4 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint nomen: true, unparam: true, regexp: true */ +/*global document, XMLHttpRequestUpload, Blob, File, FormData, location, jQuery */ + +(function ($) { + 'use strict'; + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The namespace used for event handler binding on the dropZone and + // fileInput collections. + // If not set, the name of the widget ("fileupload") is used. + namespace: undefined, + // The drop target collection, by the default the complete document. + // Set to null or an empty collection to disable drag & drop support: + dropZone: $(document), + // The file input field collection, that is listened for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null or an empty collection to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uplaods, else + // once for each file selection. + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows to override plugin options as well as define ajax settings. + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + data.submit(); + }, + + // Other callbacks: + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + // Callback for change events of the fileInput collection: + // change: function (e, data) {}, // .bind('fileuploadchange', func); + // Callback for paste events to the dropZone collection: + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + // Callback for drop events of the dropZone collection: + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + // Callback for dragover events of the dropZone collection: + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false + }, + + // A list of options that require a refresh after assigning a new value: + _refreshOptionsList: ['namespace', 'dropZone', 'fileInput'], + + _isXHRUpload: function (options) { + var undef = 'undefined'; + return !options.forceIframeTransport && + typeof XMLHttpRequestUpload !== undef && typeof File !== undef && + (!options.multipart || typeof FormData !== undef); + }, + + _getFormData: function (options) { + var formData; + if (typeof options.formData === 'function') { + return options.formData(options.form); + } else if ($.isArray(options.formData)) { + return options.formData; + } else if (options.formData) { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var total = data.total || this._getTotal(data.files), + loaded = parseInt( + e.loaded / e.total * (data.chunkSize || total), + 10 + ) + (data.uploadedBytes || 0); + this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); + data.lengthComputable = true; + data.loaded = loaded; + data.total = total; + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger('progress', e, data); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger('progressall', e, { + lengthComputable: true, + loaded: this._loaded, + total: this._total + }); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _initXHRData: function (options) { + var formData, + file = options.files[0]; + if (!options.multipart || options.blob) { + // For non-multipart uploads and chunked uploads, + // file meta data is not part of the request body, + // so we transmit this data as part of the HTTP headers. + // For cross domain requests, these headers must be allowed + // via Access-Control-Allow-Headers or removed using + // the beforeSend callback: + options.headers = $.extend(options.headers, { + 'X-File-Name': file.name, + 'X-File-Type': file.type, + 'X-File-Size': file.size + }); + if (!options.blob) { + // Non-chunked non-multipart upload: + options.contentType = file.type; + options.data = file; + } else if (!options.multipart) { + // Chunked non-multipart upload: + options.contentType = 'application/octet-stream'; + options.data = options.blob; + } + } + if (options.multipart && typeof FormData !== 'undefined') { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: options.paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: options.paramName, + value: file + }); + }); + } + } else { + if (options.formData instanceof FormData) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(options.paramName, options.blob); + } else { + $.each(options.files, function (index, file) { + // File objects are also Blob instances. + // This check allows the tests to run with + // dummy objects: + if (file instanceof Blob) { + formData.append(options.paramName, file); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options, 'iframe'); + } + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + } + if (!options.paramName) { + options.paramName = options.fileInput.prop('name') || + 'files[]'; + } + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || options.form.prop('method') || '') + .toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT') { + options.type = 'POST'; + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes = options.uploadedBytes || 0, + mcs = options.maxChunkSize || fs, + // Use the Blob methods with the slice implementation + // according to the W3C Blob API specification: + slice = file.webkitSlice || file.mozSlice || file.slice, + upload, + n, + jqXHR, + pipe; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = 'uploadedBytes'; + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // n is the number of blobs to upload, + // calculated via filesize, uploaded bytes and max chunk size: + n = Math.ceil((fs - ub) / mcs); + // The chunk upload method accepting the chunk number as parameter: + upload = function (i) { + if (!i) { + return that._getXHRPromise(true, options.context); + } + // Upload the blobs in sequential order: + return upload(i -= 1).pipe(function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options); + o.blob = slice.call( + file, + ub + i * mcs, + ub + (i + 1) * mcs + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) + .done(function () { + // Create a progress event if upload is done and + // no progress event has been invoked for this chunk: + if (!o.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: o.chunkSize, + total: o.chunkSize + }), o); + } + options.uploadedBytes = o.uploadedBytes += + o.chunkSize; + }); + return jqXHR; + }); + }; + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe = upload(n); + pipe.abort = function () { + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + } + this._active += 1; + // Initialize the global progress values: + this._loaded += data.uploadedBytes || 0; + this._total += this._getTotal(data.files); + }, + + _onDone: function (result, textStatus, jqXHR, options) { + if (!this._isXHRUpload(options)) { + // Create a progress event for each iframe load: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: 1, + total: 1 + }), options); + } + options.result = result; + options.textStatus = textStatus; + options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + options.jqXHR = jqXHR; + options.textStatus = textStatus; + options.errorThrown = errorThrown; + this._trigger('fail', null, options); + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._loaded -= options.loaded || options.uploadedBytes || 0; + this._total -= options.total || this._getTotal(options.files); + } + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + this._active -= 1; + options.textStatus = textStatus; + if (jqXHRorError && jqXHRorError.always) { + options.jqXHR = jqXHRorError; + options.result = jqXHRorResult; + } else { + options.jqXHR = jqXHRorResult; + options.errorThrown = jqXHRorError; + } + this._trigger('always', null, options); + if (this._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + this._trigger('stop'); + // Reset the global progress values: + this._loaded = this._total = 0; + } + }, + + _onSend: function (e, data) { + var that = this, + jqXHR, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function (resolve, args) { + that._sending += 1; + jqXHR = jqXHR || ( + (resolve !== false && + that._trigger('send', e, options) !== false && + (that._chunkedUpload(options) || $.ajax(options))) || + that._getXHRPromise(false, options.context, args) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._sending -= 1; + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (!nextSlot.isRejected()) { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.pipe(send); + } else { + pipe = (this._sequence = this._sequence.pipe(send, send)); + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + var args = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(args); + } + return send(false, args); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + limit = options.limitMultiFileUploads, + fileSet, + i; + if (!(options.singleFileUploads || limit) || + !this._isXHRUpload(options)) { + fileSet = [data.files]; + } else if (!options.singleFileUploads && limit) { + fileSet = []; + for (i = 0; i < data.files.length; i += limit) { + fileSet.push(data.files.slice(i, i + limit)); + } + } + data.originalFiles = data.files; + $.each(fileSet || data.files, function (index, element) { + var files = fileSet ? element : [element], + newData = $.extend({}, data, {files: files}); + newData.submit = function () { + newData.jqXHR = this.jqXHR = + (that._trigger('submit', e, this) !== false) && + that._onSend(e, this); + return this.jqXHR; + }; + return (result = that._trigger('add', e, newData)); + }); + return result; + }, + + // File Normalization for Gecko 1.9.1 (Firefox 3.5) support: + _normalizeFile: function (index, file) { + if (file.name === undefined && file.size === undefined) { + file.name = file.fileName; + file.size = file.fileSize; + } + }, + + _replaceFileInput: function (input) { + var inputClone = input.clone(true); + $('
      ').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // collection with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _onChange: function (e) { + var that = e.data.fileupload, + data = { + files: $.each($.makeArray(e.target.files), that._normalizeFile), + fileInput: $(e.target), + form: $(e.target.form) + }; + if (!data.files.length) { + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + data.files = [{name: e.target.value.replace(/^.*\\/, '')}]; + } + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger('change', e, data) === false || + that._onAdd(e, data) === false) { + return false; + } + }, + + _onPaste: function (e) { + var that = e.data.fileupload, + cbd = e.originalEvent.clipboardData, + items = (cbd && cbd.items) || [], + data = {files: []}; + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (that._trigger('paste', e, data) === false || + that._onAdd(e, data) === false) { + return false; + } + }, + + _onDrop: function (e) { + var that = e.data.fileupload, + dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, + data = { + files: $.each( + $.makeArray(dataTransfer && dataTransfer.files), + that._normalizeFile + ) + }; + if (that._trigger('drop', e, data) === false || + that._onAdd(e, data) === false) { + return false; + } + e.preventDefault(); + }, + + _onDragOver: function (e) { + var that = e.data.fileupload, + dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; + if (that._trigger('dragover', e) === false) { + return false; + } + if (dataTransfer) { + dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy'; + } + e.preventDefault(); + }, + + _initEventHandlers: function () { + var ns = this.options.namespace; + this.options.dropZone + .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) + .bind('drop.' + ns, {fileupload: this}, this._onDrop) + .bind('paste.' + ns, {fileupload: this}, this._onPaste); + this.options.fileInput + .bind('change.' + ns, {fileupload: this}, this._onChange); + }, + + _destroyEventHandlers: function () { + var ns = this.options.namespace; + this.options.dropZone + .unbind('dragover.' + ns, this._onDragOver) + .unbind('drop.' + ns, this._onDrop) + .unbind('paste.' + ns, this._onPaste); + this.options.fileInput + .unbind('change.' + ns, this._onChange); + }, + + _beforeSetOption: function (key, value) { + this._destroyEventHandlers(); + }, + + _afterSetOption: function (key, value) { + var options = this.options; + if (!options.fileInput) { + options.fileInput = $(); + } + if (!options.dropZone) { + options.dropZone = $(); + } + this._initEventHandlers(); + }, + + _setOption: function (key, value) { + var refresh = $.inArray(key, this._refreshOptionsList) !== -1; + if (refresh) { + this._beforeSetOption(key, value); + } + $.Widget.prototype._setOption.call(this, key, value); + if (refresh) { + this._afterSetOption(key, value); + } + }, + + _create: function () { + var options = this.options; + options.namespace = options.namespace || this.widgetName; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input:file') ? + this.element : this.element.find('input:file'); + } else if (!options.fileInput) { + options.fileInput = $(); + } + if (!options.dropZone) { + options.dropZone = $(); + } + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = this._loaded = this._total = 0; + this._initEventHandlers(); + }, + + destroy: function () { + this._destroyEventHandlers(); + $.Widget.prototype.destroy.call(this); + }, + + enable: function () { + $.Widget.prototype.enable.call(this); + this._initEventHandlers(); + }, + + disable: function () { + this._destroyEventHandlers(); + $.Widget.prototype.disable.call(this); + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + if (!data || this.options.disabled) { + return; + } + data.files = $.each($.makeArray(data.files), this._normalizeFile); + this._onAdd(null, data); + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + data.files = $.each($.makeArray(data.files), this._normalizeFile); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +}(jQuery)); diff --git a/thirdparty/jquery-fileupload/jquery.iframe-transport.js b/thirdparty/jquery-fileupload/jquery.iframe-transport.js new file mode 100644 index 000000000..222ca84e4 --- /dev/null +++ b/thirdparty/jquery-fileupload/jquery.iframe-transport.js @@ -0,0 +1,156 @@ +/* + * jQuery Iframe Transport Plugin 1.2.5 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/*jslint unparam: true, nomen: true */ +/*global jQuery, document */ + +(function ($) { + 'use strict'; + + // Helper variable to create unique names for the transport iframes: + var counter = 0; + + // The iframe transport accepts three additional options: + // options.fileInput: a jQuery collection of file input fields + // options.paramName: the parameter name for the file form data, + // overrides the name property of the file input field(s) + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: 'a', value: 1}, {name: 'b', value: 2}] + $.ajaxTransport('iframe', function (options) { + if (options.async && (options.type === 'POST' || options.type === 'GET')) { + var form, + iframe; + return { + send: function (_, completeCallback) { + form = $('
      '); + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6. + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + iframe = $( + '' + ).bind('load', function () { + var fileInputClones; + iframe + .unbind('load') + .bind('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': response} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('') + .appendTo(form); + form.remove(); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function () { + $(this).prop('name', options.paramName); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + $(input).prop('name', clone.prop('name')); + clone.replaceWith(input); + }); + } + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', 'javascript'.concat(':false;')); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, and script: + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return $.parseJSON($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return $(iframe[0].body).html(); + }, + 'iframe script': function (iframe) { + return $.globalEval($(iframe[0].body).text()); + } + } + }); + +}(jQuery)); diff --git a/thirdparty/jquery-fileupload/package.json b/thirdparty/jquery-fileupload/package.json new file mode 100644 index 000000000..8f60ef446 --- /dev/null +++ b/thirdparty/jquery-fileupload/package.json @@ -0,0 +1,60 @@ +{ + "name": "blueimp-file-upload", + "version": "6.0.3", + "title": "jQuery File Upload", + "description": "File Upload widget with multiple file selection, drag&drop support, progress bar and preview images for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (Google App Engine, PHP, Python, Ruby on Rails, Java, etc.) that supports standard HTML form file uploads.", + "keywords": [ + "file", + "upload", + "widget", + "multiple", + "selection", + "drag", + "drop", + "progress", + "preview", + "cross-domain", + "cross-site", + "chunked", + "resume", + "gae", + "go", + "python", + "php" + ], + "homepage": "https://github.com/blueimp/jQuery-File-Upload", + "author": { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + }, + "maintainers": [ + { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/blueimp/jQuery-File-Upload.git" + }, + "bugs": "https://github.com/blueimp/jQuery-File-Upload/issues", + "licenses": [ + { + "type": "MIT", + "url": "http://www.opensource.org/licenses/MIT" + } + ], + "dependencies": { + "jquery": ">=1.6", + "jquery.ui.widget": ">=1.8", + "blueimp-load-image": ">=1.1.3", + "blueimp-tmpl": ">=1.0.2" + }, + "files": [ + "jquery.iframe-transport.js", + "jquery.fileupload.js", + "jquery.fileupload-ui.js", + "jquery.fileupload-ui.css", + "progressbar.gif" + ] +}