JS實現表單多文件上傳樣式美化支持選中文件後刪除相關項

一、文件上傳基礎

1. 單文件上傳

最簡單的文件上傳,是單文件上傳,form標籤中加入enctype="multipart/form-data",form表單中有一個input[type="file"]項

?
1
2
3
4
5
<form name= "form1" method= "post" action= "/abc.php" enctype= "multipart/form-data" >
<input type= "text" name= "user" id= "user" placeholder= "請輸入暱稱" >
<input type= "file" name= "userImage" id= "userImage" >
<input type= "submit" name= "sub" value= "提交" >
</form>

2. 多文件上傳

  1)類似單文件上傳,簡單的多文件上傳其實就是多幾個input[type="file"]項

?
1
2
3
4
5
6
7
<form name= "form1" method= "post" action= "/abc.php" enctype= "multipart/form-data" >
<input type= "text" name= "user" id= "user" placeholder= "請輸入暱稱" >
<input type= "file" name= "userImage1" id= "userImage1" >
<input type= "file" name= "userImage2" id= "userImage2" >
<input type= "file" name= "userImage3" id= "userImage3" >
<input type= "submit" name= "sub" value= "提交" >
</form>

  2) HTML5爲表單文件項新增了一個multiple屬性,可以設置實現選擇多個文件,如

<form name="form1" method="post" action="/abc.php" enctype="multipart/form-data">
<input type="text" name="user" id="user" placeholder="請輸入暱稱">
<input type="file" name="userImage" id="userImage" multiple>
<input type="submit" name="sub" value="提交">
</form>

二、表單文件上傳的美化

看了上面幾個圖片,可以知道原生的文件選擇項樣式是最基本的,主要體現在三個點:

無邊框,與其他有邊框的元素不合拍
選擇文件的按鈕樣式太基礎
選擇多個文件後只顯示總數,未顯示詳細選擇的文件名
基於幾個問題,可以按需對其進行美化

第一點可以直接添加邊框的樣式

第二點需要增添其他元素,可以新增一個按鈕(自行按需美化),將原始文件框隱藏,用JS事件綁定,點擊按鈕後模擬文件框的點擊

?
1
2
<input type= "file" name= "userImage" id= "userImage" style= "display: none;" >
<input type= "button" id= "" value= "選擇文件" onclick= "document.getElementById('userImage').click()" >

第三點與第二點類似,也得添加新的元素,選擇文件後,通過JS獲取選擇的文件信息,並在新的元素中顯示出來

想着很簡單,但隨之而來的問題就是,如果選中的文件數量很多,新元素佔空間的多少就是個問題,可以默認顯示幾個文件,再通過「查看更多文件」查看到更多的信息

隨之另外的想法是,一次性選中的文件很多,想取消某個文件時,又得重新選擇。這未免太繁瑣,所以需要提供即時刪除某個選中文件的操作

三、選中文件後的刪除

要提供選中文件後可刪除的操作,就必然需要提供相關入口及腳本操作,下面圍繞這點來做些解析

1. 界面的處理

選擇文件後,我們可以通過刪除按鈕刪除選中的文件,因爲會出現多文件的情況,所以需要一個信息模版

?
1
2
3
4
5
6
7
<!-- 當前選擇的文件列表 文件信息模版 -->
<script type= "text/template" id= "file-temp-item-tpl" >
<span class= "file-temp-item" style= "{{style}}" >
<span class= "file-temp-name" >{{name}}</span>
<span class= "file-temp-btn" >×</span>
</span>
</script>

選中的文件一多,就得再增添一個下拉框做輔助,最多顯示5個文件信息,然後通過下拉按鈕展開下拉框(按鈕樣式自行設定)

這裏5個文件間的位置計算的不是很到位,主要是這段代碼,可以自行設定

?
1
2
3
// 計算每一項座標left、佔寬width
left = i === 0 ? 2 : 2 + i * (100 / fileTempLen);
width = 100 / fileTempLen - 2;

下拉列表裏面的每一項也是一個模版

?
1
2
3
4
5
6
7
  <!-- 查看更多文件 文件信息模版 -->
<script type= "text/template" id= "file-more-item-tpl" >
<li>
<span class= "file-item-more-name" >{{name}}</span>
<span class= "file-item-more-btn" >×</span>
</li>
</script>

以下爲初始的HTML結構

?
1
2
3
4
5
6
7
8
9
10
11
12
 <form name= "form" id= "form" method= "post" action= "fileTest.php" enctype= "multipart/form-data" >
<!-- <input type= "number" name= "numberTest" value= "100" > -->
<input type= "file" name= "fileTest[]" id= "fileTest" multiple>
<!-- 當前選擇的文件列表(最多顯示5條) -->
<span class= "file-temp" >
</span>
<!-- 查看更多文件 -->
<ul class= "item-more" >
</ul>
<input type= "button" class= "btn btn-success" id= "uploadBtn" value= "上傳" >
<p class= "upload-tip" >文件上傳成功</p>
</form>

以下爲全部CSS樣式

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<link rel= "stylesheet" type= "text/css" href= "bootstrap.min.css" >
   <style type= "text/css" >
     html {
       font-family: Arial;
     }
     form {
       margin: 50px auto;
       width: 400px;
     }
     input {
       width: 300px;
       padding: 4px;
     }
     #uploadBtn {
       margin-top: -3px;
       margin-left: 5px;
       width: 60px;
       height: 30px;
       font-weight: bold;
       font-size: 12px;
     }
     #fileTest {
       display: inline-block;
       border: 1px solid #ccc;
       border-radius: 3px;
     }
     .file-temp {
       position: relative;
       display: none;
       width: 300px;
       height: 31px;
     }
     .file-temp-item {
       position: absolute;
       top: 4px;
       height: 24px;
     }
     .item-more-btn {
       display: inline-block;
       position: absolute;
       top: 18px;
       right: 0.5%;
       width: 10px;
       height: 10px;
       color: #777;
       cursor: pointer;
     }
     .item-more-btn:hover {
       border-top-color: #aaa;
     }
     .file-temp-name {
       display: inline-block;
       overflow: hidden;
       width: 90%;
       height: 26px;
       padding: 2px 15px 2px 5px;
       border-radius: 2px;
       background-color: #eaeaf3;
       text-overflow: ellipsis;
       white-space: nowrap;
     }
     .file-temp-btn {
       position: absolute;
       display: inline-block;
       top: 4px;
       right: 11%;
       width: 18px;
       height: 18px;
       line-height: 18px;
       text-align: center;
       border: 1px solid #ddd;
       background-color: #ccc;
       border-radius: 50%;
       color: #fff;
       font-size: 18px;
       cursor: pointer;
     }
     .item-more {
       position: absolute;
       overflow-y: auto;
       display: none;
       padding-left: 0;
       width: 300px;
       max-height: 150px;
       list-style: none;
     }
     .item-more li {
       position: relative;
       padding: 5px;
       border: 1px solid #ccc;
       border-top: none;
     }
     .item-more li:hover {
       background-color: #f5f5f9;
     }
     .file-item-more-name {
       display: inline-block;
       width: 90%;
       overflow: hidden;
       text-overflow: ellipsis;
       white-space: nowrap;
     }
     .file-item-more-btn {
       position: absolute;
       display: inline-block;
       top: 8px;
       right: 2%;
       width: 18px;
       height: 18px;
       line-height: 18px;
       text-align: center;
       border: 1px solid #ddd;
       background-color: #ddd;
       border-radius: 50%;
       color: #fff;
       font-size: 18px;
       cursor: pointer;
     }
     .file-item-more-btn:hover {
       background-color: #ccc;
     }
     .upload-tip {
       display: none;
       margin: 50px auto;
       text-align: center;
       font-size: 12px;
     }
   </style>

2. 腳本的處理

下面,着重介紹JS腳本的處理

要獲取到選中文件的信息,自然想到用value屬性,但通過文件項的value只能獲取到一個文件路徑(第一個),無論有沒有multiple

無multiple

?
1
<input type= "file" onchange= "console.log(this.value);" >

有multiple

?
1
<input type= "file" multiple onchange= "console.log(this.value);" >

既然直接通過value獲取不到所有選中的文件信息,只能尋求其他途徑。

  1)FileList

獲取選中的文件信息,還可以用FileList對象,這是在HTML5中新增的,每個表單文件項都有個files屬性,裏邊存儲這選中的文件的一些信息

?
1
<input type= "file" multiple onchange= "console.log(this.files);" >

選中兩個文件後,查看文件信息

FileList對象看起來是個類數組,有length屬性。所以我們應該可以通過修改或刪除相關的項來自定義我們選擇的文件(注意其實這是不能修改的,且繼續看下去)

假如我選擇了兩個文件,想刪除第二項目,使用splice刪除,則

?
1
<input type= "file" multiple onchange= "console.log(Array.prototype.splice.call(this.files, 1, 1));" >

報錯,由此可知FileList的length屬性是隻讀的,那直接修改爲可寫可配置呢

?
1
2
3
4
Object.defineProperty(FileList.prototype, 'length' , {
writable: true ,
configurable: true
});

配置之後length能修改了,乍一看還以爲splice生效了,然而輸出一看,FileList對象內容不變,仍爲兩項

查閱了一些資料後,瞭解到瀏覽器爲了安全性的考慮,把FileList對象的內容設爲了不可更改,只可以手動置空,但不能修改內容

所以,解決辦法是,新增一個數組,初始複製FileList對象的文件內容,之後的修改操作則通過這個可更改的數組進行

?
1
2
3
4
5
6
7
8
9
// 存儲更新所選文件
var curFiles = [];
...
// 選中文件後
var files = this .files;
if (files && files.length) {
// 原始FileList對象不可更改,所以將其賦予curFiles提供接下來的修改
Array.prototype.push.apply(curFiles, files);
}

假如點擊了刪除叉叉,可以直接更新文件信息數組

?
1
2
3
4
5
var name = $( this ).prev().text();
// 去除該文件
curFiles = curFiles.filter( function (file) {
return file.name !== name;
});

這樣一來,更新文件信息的問題得到解決,然後就可以進行文件的上傳了

點擊文件上傳,如果直接調用$form.submit(); 則上傳的文件信息依然是初始的FileList對象,達不到我們自定義的要求,所以需要用Ajax提交

那麼,該怎麼想後臺提供一個文件對象呢?

  2)FormData

HTML5引入了表單的新對象FormData, 它可以生成一個表單對象,我們可以向其中獲取/設置鍵值對信息,再一併提交給後臺

引用MDN的FormData使用方法,我們可以添加各種類型的數據,使用ajax提交

?
1
2
3
4
5
6
7
8
9
10
11
var oMyForm = new FormData();
oMyForm.append( "username" , "Groucho" );
oMyForm.append( "accountnum" , 123456); // 數字123456被立即轉換成字符串"123456"
// fileInputElement中已經包含了用戶所選擇的文件
oMyForm.append( "userfile" , fileInputElement.files[0]);
var oFileBody = '<a id="a"><b id="b">hey!</b></a>' ; // Blob對象包含的文件內容
var oBlob = new Blob([oFileBody], { type: "text/xml" });
oMyForm.append( "webmasterfile" , oBlob);
var oReq = new XMLHttpRequest();
oReq.open( "POST" , "http://foo.com/submitform.php" );
oReq.send(oMyForm);

也可使用JQ的封裝的ajax,不過要注意設置processData和contentType屬性爲false,防止JQ胡亂解析文件格式

?
1
2
3
4
5
6
7
8
9
var fd = new FormData(document.getElementById( "fileinfo" )); // 使用某個表單作爲初始項
fd.append( "CustomField" , "This is some extra data" );
$.ajax({
url: "stash.php" ,
type: "POST" ,
data: fd,
processData: false , // 告訴jQuery不要去處理髮送的數據
contentType: false // 告訴jQuery不要去設置Content-Type請求頭
});

這裏有幾個要注意的點:

1)FormData中的屬性值接受的是單個文件信息,不能是複合性的對象。可能表意不明,且看

?
1
2
3
4
var fd = new FormData($( '#form' )[0]);
fd.append( 'myFileTest' , curFiles);
$files = $_REQUEST[ 'myFileTest' ];
var_dump($files);

用PHP接收傳過來的數據,數據卻被直接轉換成字符串了,非文件對象

curFiles是文件對象,那PHP端是不是應該用$_FILES來接收信息呢,試試換成$files = $_FILES['myFileTest'];

直接出問題了,說明不能這樣處理,需要將curFiles內容一項一項拆開,即單個文件信息

?
1
2
3
4
5
6
var fd = new FormData($( '#form' )[0]);
for ( var i = 0, j = curFiles.length; i < j; ++i) {
fd.append( 'myFileTest[]' , curFiles[i]);
}
$files = $_FILES[ 'myFileTest' ];
var_dump($files);

文件接收成功,接下來就可以按需進行文件的操作了

2)後端獲取文件信息的時候,是直接通過原始$_FILES獲取的,其他一般的信息才用$_REQUEST獲取

換成$files = $_REQUEST['myFileTest'];試試,直接就是出現找不到myFileTest的問題

試試添加一般的文件再提交

?
1
2
3
4
5
6
7
8
9
var fd = new FormData($( '#form' )[0]);
for ( var i = 0, j = curFiles.length; i < j; ++i) {
fd.append( 'myFileTest[]' , curFiles[i]);
}
fd.append( 'myTest' , [1, 2, 3]);
$files = $_FILES[ 'myFileTest' ];
$test = $_REQUEST[ 'myTest' ];
var_dump($test);
var_dump($files);

3)如果需要multiple的多文件上傳,則需要在文件項的文件後添加[]號,表示這是一個多文件的數組,以供後端處理解析

?
1
fd.append( 'myFileTest[]' , curFiles[i]);

如果沒有後面的[],則連續的append會直接覆蓋原來的,最後後端獲取到的只是最後append進去的項

4)不要直接在JQ的ajax中實例化出一個FormData對象,會出問題

直接在data屬性中生成FormData對象,會被JQ忽略,所以後端什麼信息也拿不到

混合表單項簡單的例子:

在表單處理中,很多時候我們會進行文件上傳和其他基礎項的提交,簡單地多加一個input項目,看看是否處理成功

?
1
<input type= "number" name= "numberTest" value= "100" >

?
1
2
3
4
5
6
7
8
<?php
$files = $_FILES [ 'myFileTest' ];
$test = $_REQUEST [ 'numberTest' ];
echo json_encode( array (
'len' => count ( $files [ 'name' ]),
'num' => $test
));
?>

以下爲全部的JS腳本:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<script type= "text/javascript" >
/**
* 向文件列表元素中添加相應的文件項
* @param {Array} files 當前的文件列表數組對象
*/
function addItem(files) {
var fileTempItemTpl = $( '#file-temp-item-tpl' ).html(),
fileMoreItemTpl = $( '#file-more-item-tpl' ).html()
htmlTemp = [],
htmlMoreTemp = [],
// 文件列表中各文件座標位置及所佔空間
left = 2,
width = 100,
// 最多取前5個文件
fileTempLen = files.length > 5 ? 5 : files.length;
for ( var i = 0, j = files.length; i < j; ++i) {
// 當i > 4,即第6個文件開始
if (i > 4) {
htmlMoreTemp.push(fileMoreItemTpl.replace( '{{name}}' , files[i].name));
continue ;
}
// 計算每一項座標left、佔寬width
left = i === 0 ? 2 : 2 + i * (100 / fileTempLen);
width = 100 / fileTempLen - 2;
htmlTemp.push(fileTempItemTpl
.replace( '{{style}}' , 'left: ' + left + '%;width: ' + width + '%;' )
.replace( '{{name}}' , files[i].name)
);
}
// 渲染相關元素內容
$( '.file-temp' ).html( ''
+ '<input type="text" style="background-color:#fff;" class="form-control" id="fileTemp" readonly>'
+ htmlTemp.join( '' )
+ (files.length > 5
? '<span class="item-more-btn" title="查看更多">=</span>'
相關文章
相關標籤/搜索