最近在研究做一个多图上传,大视频上传、支持断点续传的功能,发现plupload插件比较符合要求,于是进行了简单研究,有什么不对的地方还望大家指正(新手,第一次写博客。。。)。
主要使用plupload插件(plupload.full.min.js、jquery.plupload.queue.js、jquery.plupload.queue.css、zh_CN.js)、以及Sortable.js插件
效果❤:
1.
4.
1.页面部分
引入css js
<link rel="stylesheet" type="text/css" href="/js/plupload-2.3.6/js/jquery.plupload.queue/css/jquery.plupload.queue.css">
<script src="/js/jquery.min.js"></script>
<script src="/js/plupload/plupload.full.min.js"></script>
<script src="/js/plupload-2.3.6/js/jquery.plupload.queue/jquery.plupload.queue.js"></script>
<script src="/js/plupload-2.3.6/js/i18n/zh_CN.js"></script>
1.1上传成功后缩略图展示部分样式
<style>
#pic-list {
list-style-type: none;
/*position: absolute;*/
border: 2px solid #767997;
padding: 5px;
display: none;
width: 315px;
overflow: hidden;
/*margin-bottom: 100px;*/
}
#pic-list #triangle {
border: 10px solid #767997;
border-color: transparent transparent #767997 transparent;
position: absolute;
top: -20px;
left: 5px;
}
#pic-list #title {
padding-bottom: 5px;
}
#pic-list #title span {
font-weight: bold;
}
#pic-list #title b {
float: right;
}
#pic-list #title b:hover {
color: #434664;
}
#pic-list #title #hint {
color: #434664;
font-size: .7em;
padding-top: .7em;
}
#pic-list li {
position: relative;
display: inline-block;
float: left;
margin: 5px 5px 0px 0px;
font-size: 0px; /* 解决换行间隙问题 */
cursor: move;
}
#pic-list li#addBtn {
box-sizing: border-box;
width: 100px;
/*height: 100px;*/
height: 75px;
/*line-height: 90px;*/
line-height: 60px;
border: 2px dashed #CCC;
color: #CCC;
font-size: 80px;
text-align: center;
padding-bottom: 30px;
}
#pic-list li img {
box-sizing: border-box;
width: 100px;
/*height: 100px;*/
height: 75px;
border: 2px dashed #CCC;
}
#pic-list li:not(#addBtn):hover {
filter: alpha(Opacity=50);
-moz-opacity: 0.5;
opacity: 0.5;
}
#pic-list li#addBtn:hover {
border-color: #767997;
color: #767997;
}
#pic-list li b{
display: none;
position: absolute;
right: 0px;
top: 0px;
color: black;
font-size: 15px;
text-align: center;
cursor: pointer;
}
#title b{
cursor: pointer;
}
#pic-list li:not(#addBtn):hover b {
display: block;
width: 20px;
height: 20px;
}
#pic-list li:not(#addBtn):hover b:hover {
background-color: #808080;
}
#hit{
height: 18px;
}
</style>
1.2html页面核心部分
<div id="uploader">
<p>Your browser doesn't have Flash, Silverlight or HTML5 support.</p>
</div>
<button id="toStop">暂停</button>
<button id="toStart">续传</button>
<!--缩略图展示部分-->
<div>
<ul id="pic-list" style="margin-top: 10px;">
<span id="triangle"></span>
<div id="title">
<b onclick="delall()">X</b>
<div id="hit"></div>
</div>
</ul>
</div>
1.3 js部分
<script type="text/javascript">
$(function() {
// Initialize the widget when the DOM is ready
var uploader = $("#uploader").pluploadQueue({
// General settings
runtimes: 'html5,flash,silverlight,html4',
url: "/admin/chunkUpload",//这里替换为你的路径
// Maximum file size
max_file_size: '10000mb',
chunk_size: '20mb',
// Specify what files to browse for
filters: [
{title: "Image files", extensions: "jpg,gif,png"},
{title: "Vedio files", extensions: "mp4,mkv"},
{title: "Zip files", extensions: "zip,avi"}
],
// Rename files by clicking on their titles 通过点击文件标题 对文件进行重命名(文件上传前,可以点击文件名称重新编辑文件名)
rename: true,
// Sort files
sortable: true,
// Enable ability to drag'n'drop files onto the widget (currently only HTML5 supports that)
dragdrop: true,
// Views to activate
views: {
list: true,
thumbs: true, // Show thumbs
active: 'thumbs'
},
// Flash settings
flash_swf_url: 'js/Moxie.swf',
// Silverlight settings
silverlight_xap_url: 'js/Moxie.xap'
});
uploader.bind('FileUploaded', function(up, file, rt) {
var data = JSON.parse(rt.response);
if(data.error==0){
$('#pic-list').show();
var url = data.info.path.replace('./','');
//判断是图片还是视频
//获取最后一个/的位置
var site = url.lastIndexOf(".");
//截取最后一个/后的值
var tp = url.substring(site + 1, url.length).toLowerCase();
var imgarr=['jpg','gif','png'];
var videoarr=['mp4','mkv'];
if(imgarr.indexOf(tp) > -1){//则包含该元素}){
//图片
var onepic = '<li><img src="这里替换为你的域名/'+url+'"><b onclick="delimg(this,false)">x</b><input type="hidden" name="" value="这里替换为你的域名/'+url+'"></li>';
//$('#title').after(onepic);
$('#pic-list').append(onepic);
}
if(videoarr.indexOf(tp) > -1){
//视频
var oneVideo='<li><video id="video-div" controls="controls" style="margin-top: 10px;max-width: 400px;width: 100%;max-height:300px;">' +
' <source src="这里替换为你的域名'+url+'" type="video/mp4" id="video">\n' +
' </video><b onclick="delimg(this,false)">x</b></li>';
//$('#title').after(oneVideo);
$('#pic-list').append(oneVideo);
}
uploader.refresh(); // 重新渲染DOM,避免在添加按钮原位置仍会响应打开文件夹
}
else{
alert("出错了");
}
});
//当上传队列中某一个文件开始上传后触发。
uploader.bind('BeforeUpload', function(uploader, file){
console.log('文件开始上传了');
//console.dir(file);
});
//当使用文件小片上传功能时,每一个小片上传完成后触发
uploader.bind('ChunkUploaded', function(uploader,file,responseObject){
console.log('-----当文件上传分片的时候打印 开始----');
//console.dir(JSON.parse(responseObject['response']));
console.log('-----当文件上传分片的时候打印 结束---');
});
$("#toStop").on('click', function () {
uploader.stop();
});
$("#toStart").on('click', function () {
uploader.start();
});
});
//单张图片删除
function delimg(o,isConfirm) {
if(!isConfirm){
if(confirm("图片删除后不可恢复,确定删除?")) {
console.log("delete");
var tgname = $(o).prev()[0].tagName
console.log($(o).prev()[0].tagName);
var src = '';
if(tgname == 'IMG'){
src =$(o).prev().attr("src");
}else{
src =$(o).prev().children().attr('src');
}
delurl = src.substring(18);//注意:此处的imgurl需要根据实际情况修改,需要减去网址域名部分,剩余部门作为参数传递
//删除图片路径
//imgpath.splice($.inArray(delurl, imgpath), 1);
$.post('/admin/uploadImage?get=delimg&imgurl=' + delurl, function (data) {
/*optional stuff to do after success */
console.log(data);
if (data == 1) {
if ($("#pic-list").children("li").length == 10) {
$("#addBtn").css("display", "block");
}
$(o).parent().remove();
}
else {
console.log(data);
}
});
}
}else{
console.log("delete all");
// /var src = $(o).prev().attr("src");
var tgname = $(o).prev()[0].tagName
console.log($(o).prev()[0].tagName);
var src = '';
if(tgname == 'IMG'){
src =$(o).prev().attr("src");
}else{
src =$(o).prev().children().attr('src');
}
delurl = src.substring(18);//注意:此处的imgurl需要根据实际情况修改,需要减去网址域名部分,剩余部门作为参数传递
//删除图片路径
imgpath=[];
$.post('/admin/uploadImage?get=delimg&imgurl=' + delurl, function (data) {
/*optional stuff to do after success */
console.log(data);
if (data == 1) {
if ($("#pic-list").children("li").length == 10) {
$("#addBtn").css("display", "block");
}
$(o).parent().remove();
}
else {
console.log(data);
}
});
}
}
// 图片全部删除
function delall() {
if(confirm("图片删除后不可恢复,确定删除?")) {
$("#pic-list").hide();
$("#pic-list li").children("b").each(function (index, el) {
delimg(el,true);
});
}
//显示添加 上传按钮
$('.plupload_buttons').show();
$('.plupload_upload_status').hide();
$('.plupload_total_file_size').html('0 b');
$('.plupload_total_status').html('0%');
$('.plupload_upload_status').html('');
}
</script>
2.后台PHP代码
//图片视频上传
function chunkUpload()
{
//封装好的plupload文件上传类,直接使用即可
require_once __DIR__.'/PluploadHandler.php';
$ph = new PluploadHandler(array(
'target_dir' => './uploads/',//上传路径
'allow_extensions' => 'jpg,jpeg,png,gif,mp4,mov,mkv,zip,avi',//允许上传格式
));
$ph->sendNoCacheHeaders();
$ph->sendCORSHeaders();
if ($result = $ph->handleUpload()) {
//图片上传路径 需做简单修改,这个可以打印出来自己看就明白了
$result['path']=str_replace('\\','/',$result['path']);
die(json_encode(array(
'error' => 0,
'info' => $result
)));
} else {
die(json_encode(array(
'error' => 1,
'error' => array(
'code' => $ph->getErrorCode(),
'message' => $ph->getErrorMessage()
)
)));
}
}
//视频图片删除
function uploadImage()
{
if ($_GET['get'] == 'delimg') {
$imgsrc = $_GET['imgurl'];
unlink($imgsrc);
echo 1;
}
}
另:这里把封装好的PluploadHandler.php代码贴出来
<?php
define('PLUPLOAD_MOVE_ERR', 103);
define('PLUPLOAD_INPUT_ERR', 101);
define('PLUPLOAD_OUTPUT_ERR', 102);
define('PLUPLOAD_TMPDIR_ERR', 100);
define('PLUPLOAD_TYPE_ERR', 104);
define('PLUPLOAD_UNKNOWN_ERR', 111);
define('PLUPLOAD_SECURITY_ERR', 105);
define('DS', DIRECTORY_SEPARATOR);
///**
// * Public interface:
// * @method void handleUpload(array $conf)
// * @method string combineChunksFor(string $file_name)
// * @method int getFileSizeFor(string $file_name)
// * @method string getTargetPathFor(string $file_name)
// * @method void sendNoCacheHeaders()
// * @method void sendCorsHeaders()
// * @method int getErrorCode()
// * @method string getErrorMessage()
// *
// */
class PluploadHandler
{
/**
* @property array $conf
*/
private $conf;
/**
* Resource containing the reference to the file that we will write to.
* @property resource $out
*/
private $out;
/**
* In case of the error, will contain error code.
* @property int [$error=null]
*/
protected $error = null;
function __construct($conf = array())
{
$this->conf = array_merge(
array(
'file_data_name' => 'file',
'tmp_dir' => ini_get("upload_tmp_dir") . DS . "plupload",
'target_dir' => false,
'cleanup' => true,
'max_file_age' => 5 * 3600, // in hours
'max_execution_time' => 5 * 60, // in seconds (5 minutes by default)
'chunk' => isset($_REQUEST['chunk']) ? intval($_REQUEST['chunk']) : 0,
'chunks' => isset($_REQUEST['chunks']) ? intval($_REQUEST['chunks']) : 0,
'append_chunks_to_target' => true,
'combine_chunks_on_complete' => true,
'file_name' => isset($_REQUEST['name']) ? $_REQUEST['name'] : false,
'allow_extensions' => false,
'delay' => 0, // in seconds
'cb_sanitize_file_name' => array($this, 'sanitize_file_name'),
'cb_check_file' => false,
'cb_filesize' => array($this, 'filesize'),
'error_strings' => array(
PLUPLOAD_MOVE_ERR => "Failed to move uploaded file.",
PLUPLOAD_INPUT_ERR => "Failed to open input stream.",
PLUPLOAD_OUTPUT_ERR => "Failed to open output stream.",
PLUPLOAD_TMPDIR_ERR => "Failed to open temp directory.",
PLUPLOAD_TYPE_ERR => "File type not allowed.",
PLUPLOAD_UNKNOWN_ERR => "Failed due to unknown error.",
PLUPLOAD_SECURITY_ERR => "File didn't pass security check."
),
'debug' => false,
'log_path' => FCPATH."error.log"
),
$conf
);
}
function __destruct()
{
$this->reset();
}
function handleUpload()
{
$conf = $this->conf;
@set_time_limit($conf['max_execution_time']);
try {
// Start fresh
$this->reset();
// Cleanup outdated temp files and folders
if ($conf['cleanup']) {
$this->cleanup();
}
// Fake network congestion
if ($conf['delay']) {
sleep($conf['delay']);
}
if (!$conf['file_name']) {
if (!empty($_FILES)) {
$conf['file_name'] = $_FILES[$conf['file_data_name']]['name'];
} else {
throw new Exception('', PLUPLOAD_INPUT_ERR);
}
}
//add jxr 文件名 可能有中文 处理
// $fname = $conf['file_name'];
// $index = strripos($fname,".");//01.mp4 2
// $tp = substr($fname,$index+1);
// $name = substr($fname,0,$index);
// $conf['file_name'] = md5($name).".".$tp;
if (is_callable($conf['cb_sanitize_file_name'])) {
$file_name = call_user_func($conf['cb_sanitize_file_name'], $conf['file_name']);
} else {
$file_name = $conf['file_name'];
}
// Check if file type is allowed
if ($conf['allow_extensions']) {
if (is_string($conf['allow_extensions'])) {
$conf['allow_extensions'] = preg_split('{\s*,\s*}', $conf['allow_extensions']);
}
if (!in_array(strtolower(pathinfo($file_name, PATHINFO_EXTENSION)), $conf['allow_extensions'])) {
throw new Exception('', PLUPLOAD_TYPE_ERR);
}
}
$this->lockTheFile($file_name);
$this->log("$file_name received" . ($conf['chunks'] ? ", chunks enabled: {$conf['chunk']} of {$conf['chunks']}" : ''));
// Write file or chunk to appropriate temp location
if ($conf['chunks']) {
$result = $this->handleChunk($conf['chunk'], $file_name);
} else {
$result = $this->handleFile($file_name);
}
$this->unlockTheFile($file_name);
return $result;
} catch (Exception $ex) {
$this->error = $ex->getCode();
$this->log("ERROR: " . $this->getErrorMessage());
$this->unlockTheFile($file_name);
return false;
}
}
/**
* Retrieve the error code
*
* @return int Error code
*/
function getErrorCode()
{
if (!$this->error) {
return null;
}
if (!isset($this->conf['error_strings'][$this->error])) {
return PLUPLOAD_UNKNOWN_ERR;
}
return $this->error;
}
/**
* Retrieve the error message
*
* @return string Error message
*/
function getErrorMessage()
{
if ($code = $this->getErrorCode()) {
return $this->conf['error_strings'][$code];
} else {
return '';
}
}
/**
* Combine chunks for specified file name.
*
* @throws Exception In case of error generates exception with the corresponding code
*
* @param string $file_name
* @return string Path to the target file
*/
function combineChunksFor($file_name)
{
$file_path = $this->getTargetPathFor($file_name);
if (!$tmp_path = $this->writeChunksToFile("$file_path.dir.part", "$file_path.part")) {
return false;
}
return $this->rename($tmp_path, $file_path);
}
protected function handleChunk($chunk, $file_name)
{
$file_path = $this->getTargetPathFor($file_name);
$this->log($this->conf['append_chunks_to_target']
? "chunks being appended directly to the target $file_path.part"
: "standalone chunks being written to $file_path.dir.part"
);
if ($this->conf['append_chunks_to_target']) {
$chunk_path = $this->writeUploadTo("$file_path.part", false, 'ab');
if ($this->isLastChunk($file_name)) {
return $this->rename($chunk_path, $file_path);
}
} else {
$chunk_path = $this->writeUploadTo("$file_path.dir.part" . DS . "$chunk.part");
if ($this->conf['combine_chunks_on_complete'] && $this->isLastChunk($file_name)) {
return $this->combineChunksFor($file_name);
}
}
return array(
'name' => $file_name,
'path' => $chunk_path,
'chunk' => $chunk,
'size' => call_user_func($this->conf['cb_filesize'], $chunk_path)
);
}
protected function handleFile($file_name)
{
$file_path = $this->getTargetPathFor($file_name);
$tmp_path = $this->writeUploadTo($file_path . ".part");
return $this->rename($tmp_path, $file_path);
}
protected function rename($tmp_path, $file_path)
{
// Upload complete write a temp file to the final destination
if (!$this->fileIsOK($tmp_path)) {
if ($this->conf['cleanup']) {
@unlink($tmp_path);
}
throw new Exception('', PLUPLOAD_SECURITY_ERR);
}
if (rename($tmp_path, $file_path)) {
$this->log("$tmp_path successfully renamed to $file_path");
//add jxr
//$this->videoPress(str_replace('.\\','',$file_path), FCPATH . 'uploads/');// .\uploads\01.mp4
return array(
'name' => basename($file_path),
'path' => $file_path,
'size' => call_user_func($this->conf['cb_filesize'], $file_path)
);
} else {
return false;
}
}
/**
* Writes either a multipart/form-data message or a binary stream
* to the specified file.
*
* @throws Exception In case of error generates exception with the corresponding code
*
* @param string $file_path The path to write the file to
* @param string [$file_data_name='file'] The name of the multipart field
* @return string Path to the target file
*/
protected function writeUploadTo($file_path, $file_data_name = false, $mode = 'wb')
{
if (!$file_data_name) {
$file_data_name = $this->conf['file_data_name'];
}
$base_dir = dirname($file_path);
if (!file_exists($base_dir) && !@mkdir($base_dir, 0777, true)) {
throw new Exception('', PLUPLOAD_TMPDIR_ERR);
}
if (!empty($_FILES)) {
if (!isset($_FILES[$file_data_name]) || $_FILES[$file_data_name]["error"] || !is_uploaded_file($_FILES[$file_data_name]["tmp_name"])) {
throw new Exception('', PLUPLOAD_INPUT_ERR);
}
return $this->writeToFile($_FILES[$file_data_name]["tmp_name"], $file_path, $mode);
} else {
return $this->writeToFile("php://input", $file_path, $mode);
}
}
/**
* Write source or set of sources to the specified target. Depending on the mode
* sources will either overwrite the content in the target or will be appended to
* the target
* Create by: jxr
* Date: 2019/8/1
* Time: 15:13
* @param $source_paths array|string
* @param $target_path string
* @param string $mode string [$mode='wb'] Mode to use (to append use 'ab') a追加b以二进制形式写入文件末尾(追加) wb已二进制形式写覆盖
* @return mixed string Path to the written target file
* @throws Exception
*/
protected function writeToFile($source_paths, $target_path, $mode = 'wb')
{
if (!is_array($source_paths)) {
$source_paths = array($source_paths);
}
if (!$out = @fopen($target_path, $mode)) {
throw new Exception('', PLUPLOAD_OUTPUT_ERR);
}
foreach ($source_paths as $source_path) {
if (!$in = @fopen($source_path, "rb")) {
throw new Exception('', PLUPLOAD_INPUT_ERR);
}
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
@fclose($in);
$this->log("$source_path " . ($mode == 'wb' ? "written" : "appended") . " to $target_path");
}
fflush($out);
@fclose($out);
return $target_path;
}
/**
* Combine chunks from the specified folder into the single file.
*
* @throws Exception In case of error generates exception with the corresponding code
*
* @param string $chunk_dir Directory containing the chunks
* @param string $target_path The file to write the chunks to
* @return string File path containing combined chunks
*/
protected function writeChunksToFile($chunk_dir, $target_path)
{
$chunk_paths = array();
for ($i = 0; $i < $this->conf['chunks']; $i++) {
$chunk_path = $chunk_dir . DS . "$i.part";
if (!file_exists($chunk_path)) {
throw new Exception('', PLUPLOAD_MOVE_ERR);
}
$chunk_paths[] = $chunk_path;
}
$this->writeToFile($chunk_paths, $target_path, 'ab');
$this->log("$chunk_dir combined into $target_path");
// Cleanup
if ($this->conf['cleanup']) {
$this->rrmdir($chunk_dir);
}
return $target_path;
}
/**
* Checks if currently processed chunk for the given filename is the last one.
*
* @param string $file_name
* @return boolean
*/
protected function isLastChunk($file_name)
{
if ($this->conf['append_chunks_to_target']) {
if ($result = $this->conf['chunks'] && $this->conf['chunks'] == $this->conf['chunk'] + 1) {
$this->log("last chunk received: {$this->conf['chunks']} out of {$this->conf['chunks']}");
}
} else {
$file_path = $this->getTargetPathFor($file_name);
$chunks = sizeof(glob("$file_path.dir.part/*.part"));
if ($result = $chunks == $this->conf['chunks']) {
$this->log("seems like last chunk ({$this->conf['chunk']}), 'cause there are $chunks out of {$this->conf['chunks']} *.part files in $file_path.dir.part.");
}
}
return $result;
}
/**
* Runs cb_check_file filter on the file if defined in config.
*
* @param string $file_path Path to the file to check
* @return boolean
*/
protected function fileIsOK($path)
{
return !is_callable($this->conf['cb_check_file']) || call_user_func($this->conf['cb_check_file'], $path);
}
/**
* Returns the size of the file in bytes for the given filename. Filename will be resolved
* against target_dir value defined in the config.
*
* @param string $file_name
* @return number|false
*/
function getFileSizeFor($file_name)
{
return call_user_func($this->conf['cb_filesize'], getTargetPathFor($file_name));
}
/**
* Resolves given filename against target_dir value defined in the config.
* 根据配置中定义的target_dir值解析给定文件名
*
* @param string $file_name
* @return string Resolved file path
*/
function getTargetPathFor($file_name)
{
$target_dir = str_replace(array("/", "\/"), DS, rtrim($this->conf['target_dir'], "/\\"));
return $target_dir . DS . $file_name;
}
/**
* Sends out headers that prevent caching of the output that is going to follow.
*/
function sendNoCacheHeaders()
{
// Make sure this file is not cached (as it might happen on iOS devices, for example)
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
}
/**
* Handles CORS.
*
* @param array $headers Additional headers to send out
* @param string [$origin='*'] Allowed origin
*/
function sendCORSHeaders($headers = array(), $origin = '*')
{
$allow_origin_present = false;
if (!empty($headers)) {
foreach ($headers as $header => $value) {
if (strtolower($header) == 'access-control-allow-origin') {
$allow_origin_present = true;
}
header("$header: $value");
}
}
if ($origin && !$allow_origin_present) {
header("Access-Control-Allow-Origin: $origin");
}
// other CORS headers if any...
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
exit; // finish preflight CORS requests here
}
}
/**
* Cleans up outdated *.part files and directories inside target_dir.
* Files are considered outdated if they are older than max_file_age hours.
* (@see config options)
*/
private function cleanup()
{
// Remove old temp files
if (file_exists($this->conf['target_dir'])) {
foreach (glob($this->conf['target_dir'] . '/*.part') as $tmpFile) {
if (time() - filemtime($tmpFile) < $this->conf['max_file_age']) {
continue;
}
if (is_dir($tmpFile)) {
self::rrmdir($tmpFile);
} else {
@unlink($tmpFile);
}
}
}
}
/**
* Sanitizes a filename replacing whitespace with dashes
*
* Removes special characters that are illegal in filenames on certain
* operating systems and special characters requiring special escaping
* to manipulate at the command line. Replaces spaces and consecutive
* dashes with a single dash. Trim period, dash and underscore from beginning
* and end of filename.
*
* @author WordPress
*
* @param string $filename The filename to be sanitized
* @return string The sanitized filename
*/
protected function sanitizeFileName($filename)
{
$special_chars = array("?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}");
$filename = str_replace($special_chars, '', $filename);
$filename = preg_replace('/[\s-]+/', '-', $filename);
$filename = trim($filename, '.-_');
return $filename;
}
/**
* Concise way to recursively remove a directory
* @see http://www.php.net/manual/en/function.rmdir.php#108113
*
* @param string $dir Directory to remove
*/
private function rrmdir($dir)
{
foreach (glob($dir . '/*') as $file) {
if (is_dir($file))
$this->rrmdir($file);
else
unlink($file);
}
rmdir($dir);
}
/**
* PHPs filesize() fails to measure files larger than 2gb
* @see http://stackoverflow.com/a/5502328/189673
*
* @param string $file Path to the file to measure
* @return int
*/
protected function filesize($file)
{
if (!file_exists($file)) {
$this->log("cannot measure $file, 'cause it doesn't exist.");
return false;
}
static $iswin;
if (!isset($iswin)) {
$iswin = (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN');
}
static $exec_works;
if (!isset($exec_works)) {
$exec_works = (function_exists('exec') && !ini_get('safe_mode') && @exec('echo EXEC') == 'EXEC');
}
// try a shell command
if ($exec_works) {
$cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : "stat -c%s \"$file\"";
@exec($cmd, $output);
if (is_array($output) && is_numeric($size = trim(implode("\n", $output)))) {
$this->log("filesize obtained via exec.");
return $size;
}
}
// try the Windows COM interface
if ($iswin && class_exists("COM")) {
try {
$fsobj = new COM('Scripting.FileSystemObject');
$f = $fsobj->GetFile(realpath($file));
$size = $f->Size;
} catch (Exception $e) {
$size = null;
}
if (ctype_digit($size)) {
$this->log("filesize obtained via Scripting.FileSystemObject.");
return $size;
}
}
// if everything else fails
$this->log("filesize obtained via native filesize.");
return @filesize($file);
}
/**
* Obtain the blocking lock on the specified file. All processes looking to work with
* the same file will have to wait, until we release it (@see unlockTheFile).
*
* @param string $file_name File to lock
*/
private function lockTheFile($file_name)
{
$file_path = $this->getTargetPathFor($file_name);
$this->out = fopen("$file_path.lock", 'w');
flock($this->out, LOCK_EX); // obtain blocking lock
}
/**
* Release the blocking lock on the specified file.
*
* @param string $file_name File to lock
*/
private function unlockTheFile($file_name)
{
$file_path = $this->getTargetPathFor($file_name);
fclose($this->out);
@unlink("$file_path.lock");
}
/**
* Reset private variables to their initial values.
*/
private function reset()
{
$conf = $this->conf;
$this->error = null;
if (is_resource($this->out)) {
fclose($this->out);
}
}
/**
* Log the message to the log_path, but only if debug is set to true.
* Each message will get prepended with the current timestamp.
*
* @param string $msg
*/
protected function log($msg)
{
if (!$this->conf['debug']) {
return;
}
$msg = date("Y-m-d H:i:s") . ": $msg\n";
file_put_contents($this->conf['log_path'], $msg, FILE_APPEND);
}
}
3.想要实现上传的图片视频进行拖动排序的话,可以使用Sortable.js插件:
3.1引入js
<script src="/js/Sortable.js"></script>
sortale的使用很简单,直接把下卖弄代码写入js即可,替换下面包含图片或视频的ul的id即可,其他均不用修改。
(function () {
'use strict';
var byId = function (id) { return document.getElementById(id); },
loadScripts = function (desc, callback) {
var deps = [], key, idx = 0;
for (key in desc) {
deps.push(key);
}
(function _next() {
var pid,
name = deps[idx],
script = document.createElement('script');
script.type = 'text/javascript';
script.src = desc[deps[idx]];
pid = setInterval(function () {
if (window[name]) {
clearTimeout(pid);
deps[idx++] = window[name];
if (deps[idx]) {
_next();
} else {
callback.apply(null, deps);
}
}
}, 30);
document.getElementsByTagName('head')[0].appendChild(script);
})()
},
console = window.console;
if (!console.log) {
console.log = function () {
alert([].join.apply(arguments, ' '));
};
}
Sortable.create(byId('pic-list'), {//只需根据你的情况替换这里的id
group: "words",
animation: 150,
store: {
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group);
return order ? order.split('|') : [];
},
set: function (sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.group, order.join('|'));
}
},
onAdd: function (evt){ console.log('onAdd.foo:', [evt.item, evt.from]); },
onUpdate: function (evt){ console.log('onUpdate.foo:', [evt.item, evt.from]); },
onRemove: function (evt){ console.log('onRemove.foo:', [evt.item, evt.from]); },
onStart:function(evt){ console.log('onStart.foo:', [evt.item, evt.from]);},
onSort:function(evt){ console.log('onStart.foo:', [evt.item, evt.from]);},
onEnd: function(evt){ console.log('onEnd.foo:', [evt.item, evt.from]);}
});
Sortable.create(byId('bar'), {
group: "words",
animation: 150,
onAdd: function (evt){ console.log('onAdd.bar:', evt.item); },
onUpdate: function (evt){ console.log('onUpdate.bar:', evt.item); },
onRemove: function (evt){ console.log('onRemove.bar:', evt.item); },
onStart:function(evt){ console.log('onStart.foo:', evt.item);},
onEnd: function(evt){ console.log('onEnd.foo:', evt.item);}
});
// Multi groups
Sortable.create(byId('multi'), {
animation: 150,
draggable: '.tile',
handle: '.tile__name'
});
[].forEach.call(byId('multi').getElementsByClassName('tile__list'), function (el){
Sortable.create(el, {
group: 'photo',
animation: 150
});
});
// Editable list
var editableList = Sortable.create(byId('editable'), {
animation: 150,
filter: '.js-remove',
onFilter: function (evt) {
evt.item.parentNode.removeChild(evt.item);
}
});
byId('addUser').onclick = function () {
Ply.dialog('prompt', {
title: 'Add',
form: { name: 'name' }
}).done(function (ui) {
var el = document.createElement('li');
el.innerHTML = ui.data.name + '<i class="js-remove">✖</i>';
editableList.el.appendChild(el);
});
};
// Advanced groups
[{
name: 'advanced',
pull: true,
put: true
},
{
name: 'advanced',
pull: 'clone',
put: false
}, {
name: 'advanced',
pull: false,
put: true
}].forEach(function (groupOpts, i) {
Sortable.create(byId('advanced-' + (i + 1)), {
sort: (i != 1),
group: groupOpts,
animation: 150
});
});
// 'handle' option
Sortable.create(byId('handle-1'), {
handle: '.drag-handle',
animation: 150
});
// Angular example
angular.module('todoApp', ['ng-sortable'])
.constant('ngSortableConfig', {onEnd: function() {
console.log('default onEnd()');
}})
.controller('TodoController', ['$scope', function ($scope) {
$scope.todos = [
{text: 'learn angular', done: true},
{text: 'build an angular app', done: false}
];
$scope.addTodo = function () {
$scope.todos.push({text: $scope.todoText, done: false});
$scope.todoText = '';
};
$scope.remaining = function () {
var count = 0;
angular.forEach($scope.todos, function (todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.archive = function () {
var oldTodos = $scope.todos;
$scope.todos = [];
angular.forEach(oldTodos, function (todo) {
if (!todo.done) $scope.todos.push(todo);
});
};
}])
.controller('TodoControllerNext', ['$scope', function ($scope) {
$scope.todos = [
{text: 'learn Sortable', done: true},
{text: 'use ng-sortable', done: false},
{text: 'Enjoy', done: false}
];
$scope.remaining = function () {
var count = 0;
angular.forEach($scope.todos, function (todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.sortableConfig = { group: 'todo', animation: 150 };
'Start End Add Update Remove Sort'.split(' ').forEach(function (name) {
$scope.sortableConfig['on' + name] = console.log.bind(console, name);
});
}]);
})();
// Background
document.addEventListener("DOMContentLoaded", function () {
function setNoiseBackground(el, width, height, opacity) {
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
for (var i = 0; i < width; i++) {
for (var j = 0; j < height; j++) {
var val = Math.floor(Math.random() * 255);
context.fillStyle = "rgba(" + val + "," + val + "," + val + "," + opacity + ")";
context.fillRect(i, j, 1, 1);
}
}
el.style.background = "url(" + canvas.toDataURL("image/png") + ")";
}
setNoiseBackground(document.getElementsByTagName('body')[0], 50, 50, 0.02);
}, false);
**
另:Sortable.js也贴出来
**
/**!
* Sortable
* @author RubaXa trash@rubaxa.org
* @license MIT
*/
(function sortableModule(factory) {
"use strict";
if (typeof define === "function" && define.amd) {
define(factory);
}
else if (typeof module != "undefined" && typeof module.exports != "undefined") {
module.exports = factory();
}
else {
/* jshint sub:true */
window["Sortable"] = factory();
}
})(function sortableFactory() {
"use strict";
if (typeof window === "undefined" || !window.document) {
return function sortableError() {
throw new Error("Sortable.js requires a window with a document");
};
}
var dragEl,
parentEl,
ghostEl,
cloneEl,
rootEl,
nextEl,
lastDownEl,
scrollEl,
scrollParentEl,
scrollCustomFn,
lastEl,
lastCSS,
lastParentCSS,
oldIndex,
newIndex,
activeGroup,
putSortable,
autoScroll = {},
tapEvt,
touchEvt,
moved,
forRepaintDummy,
/** @const */
R_SPACE = /\s+/g,
R_FLOAT = /left|right|inline/,
expando = 'Sortable' + (new Date).getTime(),
win = window,
document = win.document,
parseInt = win.parseInt,
setTimeout = win.setTimeout,
$ = win.jQuery || win.Zepto,
Polymer = win.Polymer,
captureMode = false,
passiveMode = false,
supportDraggable = ('draggable' in document.createElement('div')),
supportCssPointerEvents = (function (el) {
// false when IE11
if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) {
return false;
}
el = document.createElement('x');
el.style.cssText = 'pointer-events:auto';
return el.style.pointerEvents === 'auto';
})(),
_silent = false,
abs = Math.abs,
min = Math.min,
savedInputChecked = [],
touchDragOverListeners = [],
alwaysFalse = function () { return false; },
_autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
// Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
if (rootEl && options.scroll) {
var _this = rootEl[expando],
el,
rect,
sens = options.scrollSensitivity,
speed = options.scrollSpeed,
x = evt.clientX,
y = evt.clientY,
winWidth = window.innerWidth,
winHeight = window.innerHeight,
vx,
vy,
scrollOffsetX,
scrollOffsetY
;
// Delect scrollEl
if (scrollParentEl !== rootEl) {
scrollEl = options.scroll;
scrollParentEl = rootEl;
scrollCustomFn = options.scrollFn;
if (scrollEl === true) {
scrollEl = rootEl;
do {
if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
(scrollEl.offsetHeight < scrollEl.scrollHeight)
) {
break;
}
/* jshint boss:true */
} while (scrollEl = scrollEl.parentNode);
}
}
if (scrollEl) {
el = scrollEl;
rect = scrollEl.getBoundingClientRect();
vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
}
if (!(vx || vy)) {
vx = (winWidth - x <= sens) - (x <= sens);
vy = (winHeight - y <= sens) - (y <= sens);
/* jshint expr:true */
(vx || vy) && (el = win);
}
if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
autoScroll.el = el;
autoScroll.vx = vx;
autoScroll.vy = vy;
clearInterval(autoScroll.pid);
if (el) {
autoScroll.pid = setInterval(function () {
scrollOffsetY = vy ? vy * speed : 0;
scrollOffsetX = vx ? vx * speed : 0;
if ('function' === typeof(scrollCustomFn)) {
if (scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt, touchEvt, el) !== 'continue') {
return;
}
}
if (el === win) {
win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY);
} else {
el.scrollTop += scrollOffsetY;
el.scrollLeft += scrollOffsetX;
}
}, 24);
}
}
}
}, 30),
_prepareGroup = function (options) {
function toFn(value, pull) {
if (value == null || value === true) {
value = group.name;
if (value == null) {
return alwaysFalse;
}
}
if (typeof value === 'function') {
return value;
} else {
return function (to, from) {
var fromGroup = from.options.group.name;
return pull
? value
: value && (value.join
? value.indexOf(fromGroup) > -1
: (fromGroup == value)
);
};
}
}
var group = {};
var originalGroup = options.group;
if (!originalGroup || typeof originalGroup != 'object') {
originalGroup = {name: originalGroup};
}
group.name = originalGroup.name;
group.checkPull = toFn(originalGroup.pull, true);
group.checkPut = toFn(originalGroup.put);
group.revertClone = originalGroup.revertClone;
options.group = group;
}
;
// Detect support a passive mode
try {
window.addEventListener('test', null, Object.defineProperty({}, 'passive', {
get: function () {
// `false`, because everything starts to work incorrectly and instead of d'n'd,
// begins the page has scrolled.
passiveMode = false;
captureMode = {
capture: false,
passive: passiveMode
};
}
}));
} catch (err) {}
/**
* @class Sortable
* @param {HTMLElement} el
* @param {Object} [options]
*/
function Sortable(el, options) {
if (!(el && el.nodeType && el.nodeType === 1)) {
throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el);
}
this.el = el; // root element
this.options = options = _extend({}, options);
// Export instance
el[expando] = this;
// Default options
var defaults = {
group: null,
sort: true,
disabled: false,
store: null,
handle: null,
scroll: true,
scrollSensitivity: 30,
scrollSpeed: 10,
draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
ignore: 'a, img',
filter: null,
preventOnFilter: true,
animation: 0,
setData: function (dataTransfer, dragEl) {
dataTransfer.setData('Text', dragEl.textContent);
},
dropBubble: false,
dragoverBubble: false,
dataIdAttr: 'data-id',
delay: 0,
touchStartThreshold: parseInt(window.devicePixelRatio, 10) || 1,
forceFallback: false,
fallbackClass: 'sortable-fallback',
fallbackOnBody: false,
fallbackTolerance: 0,
fallbackOffset: {x: 0, y: 0},
supportPointer: Sortable.supportPointer !== false
};
// Set default options
for (var name in defaults) {
!(name in options) && (options[name] = defaults[name]);
}
_prepareGroup(options);
// Bind all private methods
for (var fn in this) {
if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
this[fn] = this[fn].bind(this);
}
}
// Setup drag mode
this.nativeDraggable = options.forceFallback ? false : supportDraggable;
// Bind events
_on(el, 'mousedown', this._onTapStart);
_on(el, 'touchstart', this._onTapStart);
options.supportPointer && _on(el, 'pointerdown', this._onTapStart);
if (this.nativeDraggable) {
_on(el, 'dragover', this);
_on(el, 'dragenter', this);
}
touchDragOverListeners.push(this._onDragOver);
// Restore sorting
options.store && this.sort(options.store.get(this));
}
Sortable.prototype = /** @lends Sortable.prototype */ {
constructor: Sortable,
_onTapStart: function (/** Event|TouchEvent */evt) {
var _this = this,
el = this.el,
options = this.options,
preventOnFilter = options.preventOnFilter,
type = evt.type,
touch = evt.touches && evt.touches[0],
target = (touch || evt).target,
originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0]) || target,
filter = options.filter,
startIndex;
_saveInputCheckedState(el);
// Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
if (dragEl) {
return;
}
if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) {
return; // only left button or enabled
}
// cancel dnd if original target is content editable
if (originalTarget.isContentEditable) {
return;
}
target = _closest(target, options.draggable, el);
if (!target) {
return;
}
if (lastDownEl === target) {
// Ignoring duplicate `down`
return;
}
// Get the index of the dragged element within its parent
startIndex = _index(target, options.draggable);
// Check filter
if (typeof filter === 'function') {
if (filter.call(this, evt, target, this)) {
_dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex);
preventOnFilter && evt.preventDefault();
return; // cancel dnd
}
}
else if (filter) {
filter = filter.split(',').some(function (criteria) {
criteria = _closest(originalTarget, criteria.trim(), el);
if (criteria) {
_dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex);
return true;
}
});
if (filter) {
preventOnFilter && evt.preventDefault();
return; // cancel dnd
}
}
if (options.handle && !_closest(originalTarget, options.handle, el)) {
return;
}
// Prepare `dragstart`
this._prepareDragStart(evt, touch, target, startIndex);
},
_prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) {
var _this = this,
el = _this.el,
options = _this.options,
ownerDocument = el.ownerDocument,
dragStartFn;
if (target && !dragEl && (target.parentNode === el)) {
tapEvt = evt;
rootEl = el;
dragEl = target;
parentEl = dragEl.parentNode;
nextEl = dragEl.nextSibling;
lastDownEl = target;
activeGroup = options.group;
oldIndex = startIndex;
this._lastX = (touch || evt).clientX;
this._lastY = (touch || evt).clientY;
dragEl.style['will-change'] = 'all';
dragStartFn = function () {
// Delayed drag has been triggered
// we can re-enable the events: touchmove/mousemove
_this._disableDelayedDrag();
// Make the element draggable
dragEl.draggable = _this.nativeDraggable;
// Chosen item
_toggleClass(dragEl, options.chosenClass, true);
// Bind the events: dragstart/dragend
_this._triggerDragStart(evt, touch);
// Drag start event
_dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex);
};
// Disable "draggable"
options.ignore.split(',').forEach(function (criteria) {
_find(dragEl, criteria.trim(), _disableDraggable);
});
_on(ownerDocument, 'mouseup', _this._onDrop);
_on(ownerDocument, 'touchend', _this._onDrop);
_on(ownerDocument, 'touchcancel', _this._onDrop);
_on(ownerDocument, 'selectstart', _this);
options.supportPointer && _on(ownerDocument, 'pointercancel', _this._onDrop);
if (options.delay) {
// If the user moves the pointer or let go the click or touch
// before the delay has been reached:
// disable the delayed drag
_on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
_on(ownerDocument, 'touchend', _this._disableDelayedDrag);
_on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
_on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
_on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler);
options.supportPointer && _on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler);
_this._dragStartTimer = setTimeout(dragStartFn, options.delay);
} else {
dragStartFn();
}
}
},
_delayedDragTouchMoveHandler: function (/** TouchEvent|PointerEvent **/e) {
if (min(abs(e.clientX - this._lastX), abs(e.clientY - this._lastY)) >= this.options.touchStartThreshold) {
this._disableDelayedDrag();
}
},
_disableDelayedDrag: function () {
var ownerDocument = this.el.ownerDocument;
clearTimeout(this._dragStartTimer);
_off(ownerDocument, 'mouseup', this._disableDelayedDrag);
_off(ownerDocument, 'touchend', this._disableDelayedDrag);
_off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
_off(ownerDocument, 'mousemove', this._disableDelayedDrag);
_off(ownerDocument, 'touchmove', this._disableDelayedDrag);
_off(ownerDocument, 'pointermove', this._disableDelayedDrag);
},
_triggerDragStart: function (/** Event */evt, /** Touch */touch) {
touch = touch || (evt.pointerType == 'touch' ? evt : null);
if (touch) {
// Touch device support
tapEvt = {
target: dragEl,
clientX: touch.clientX,
clientY: touch.clientY
};
this._onDragStart(tapEvt, 'touch');
}
else if (!this.nativeDraggable) {
this._onDragStart(tapEvt, true);
}
else {
_on(dragEl, 'dragend', this);
_on(rootEl, 'dragstart', this._onDragStart);
}
try {
if (document.selection) {
// Timeout neccessary for IE9
_nextTick(function () {
document.selection.empty();
});
} else {
window.getSelection().removeAllRanges();
}
} catch (err) {
}
},
_dragStarted: function () {
if (rootEl && dragEl) {
var options = this.options;
// Apply effect
_toggleClass(dragEl, options.ghostClass, true);
_toggleClass(dragEl, options.dragClass, false);
Sortable.active = this;
// Drag start event
_dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex);
} else {
this._nulling();
}
},
_emulateDragOver: function () {
if (touchEvt) {
if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) {
return;
}
this._lastX = touchEvt.clientX;
this._lastY = touchEvt.clientY;
if (!supportCssPointerEvents) {
_css(ghostEl, 'display', 'none');
}
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
var parent = target;
var i = touchDragOverListeners.length;
while (target && target.shadowRoot) {
target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
parent = target;
}
if (parent) {
do {
if (parent[expando]) {
while (i--) {
touchDragOverListeners[i]({
clientX: touchEvt.clientX,
clientY: touchEvt.clientY,
target: target,
rootEl: parent
});
}
break;
}
target = parent; // store last element
}
/* jshint boss:true */
while (parent = parent.parentNode);
}
if (!supportCssPointerEvents) {
_css(ghostEl, 'display', '');
}
}
},
_onTouchMove: function (/**TouchEvent*/evt) {
if (tapEvt) {
var options = this.options,
fallbackTolerance = options.fallbackTolerance,
fallbackOffset = options.fallbackOffset,
touch = evt.touches ? evt.touches[0] : evt,
dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x,
dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y,
translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
// only set the status to dragging, when we are actually dragging
if (!Sortable.active) {
if (fallbackTolerance &&
min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance
) {
return;
}
this._dragStarted();
}
// as well as creating the ghost element on the document body
this._appendGhost();
moved = true;
touchEvt = touch;
_css(ghostEl, 'webkitTransform', translate3d);
_css(ghostEl, 'mozTransform', translate3d);
_css(ghostEl, 'msTransform', translate3d);
_css(ghostEl, 'transform', translate3d);
evt.preventDefault();
}
},
_appendGhost: function () {
if (!ghostEl) {
var rect = dragEl.getBoundingClientRect(),
css = _css(dragEl),
options = this.options,
ghostRect;
ghostEl = dragEl.cloneNode(true);
_toggleClass(ghostEl, options.ghostClass, false);
_toggleClass(ghostEl, options.fallbackClass, true);
_toggleClass(ghostEl, options.dragClass, true);
_css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
_css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
_css(ghostEl, 'width', rect.width);
_css(ghostEl, 'height', rect.height);
_css(ghostEl, 'opacity', '0.8');
_css(ghostEl, 'position', 'fixed');
_css(ghostEl, 'zIndex', '100000');
_css(ghostEl, 'pointerEvents', 'none');
options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);
// Fixing dimensions.
ghostRect = ghostEl.getBoundingClientRect();
_css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
_css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
}
},
_onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
var _this = this;
var dataTransfer = evt.dataTransfer;
var options = _this.options;
_this._offUpEvents();
if (activeGroup.checkPull(_this, _this, dragEl, evt)) {
cloneEl = _clone(dragEl);
cloneEl.draggable = false;
cloneEl.style['will-change'] = '';
_css(cloneEl, 'display', 'none');
_toggleClass(cloneEl, _this.options.chosenClass, false);
// #1143: IFrame support workaround
_this._cloneId = _nextTick(function () {
rootEl.insertBefore(cloneEl, dragEl);
_dispatchEvent(_this, rootEl, 'clone', dragEl);
});
}
_toggleClass(dragEl, options.dragClass, true);
if (useFallback) {
if (useFallback === 'touch') {
// Bind touch events
_on(document, 'touchmove', _this._onTouchMove);
_on(document, 'touchend', _this._onDrop);
_on(document, 'touchcancel', _this._onDrop);
if (options.supportPointer) {
_on(document, 'pointermove', _this._onTouchMove);
_on(document, 'pointerup', _this._onDrop);
}
} else {
// Old brwoser
_on(document, 'mousemove', _this._onTouchMove);
_on(document, 'mouseup', _this._onDrop);
}
_this._loopId = setInterval(_this._emulateDragOver, 50);
}
else {
if (dataTransfer) {
dataTransfer.effectAllowed = 'move';
options.setData && options.setData.call(_this, dataTransfer, dragEl);
}
_on(document, 'drop', _this);
// #1143: Бывает элемент с IFrame внутри блокирует `drop`,
// поэтому если вызвался `mouseover`, значит надо отменять весь d'n'd.
// Breaking Chrome 62+
// _on(document, 'mouseover', _this);
_this._dragStartId = _nextTick(_this._dragStarted);
}
},
_onDragOver: function (/**Event*/evt) {
var el = this.el,
target,
dragRect,
targetRect,
revert,
options = this.options,
group = options.group,
activeSortable = Sortable.active,
isOwner = (activeGroup === group),
isMovingBetweenSortable = false,
canSort = options.sort;
if (evt.preventDefault !== void 0) {
evt.preventDefault();
!options.dragoverBubble && evt.stopPropagation();
}
if (dragEl.animated) {
return;
}
moved = true;
if (activeSortable && !options.disabled &&
(isOwner
? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
: (
putSortable === this ||
(
(activeSortable.lastPullMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) &&
group.checkPut(this, activeSortable, dragEl, evt)
)
)
) &&
(evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
) {
// Smart auto-scrolling
_autoScroll(evt, options, this.el);
if (_silent) {
return;
}
target = _closest(evt.target, options.draggable, el);
dragRect = dragEl.getBoundingClientRect();
if (putSortable !== this) {
putSortable = this;
isMovingBetweenSortable = true;
}
if (revert) {
_cloneHide(activeSortable, true);
parentEl = rootEl; // actualization
if (cloneEl || nextEl) {
rootEl.insertBefore(dragEl, cloneEl || nextEl);
}
else if (!canSort) {
rootEl.appendChild(dragEl);
}
return;
}
if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
(el === evt.target) && (_ghostIsLast(el, evt))
) {
//assign target only if condition is true
if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) {
target = el.lastElementChild;
}
if (target) {
if (target.animated) {
return;
}
targetRect = target.getBoundingClientRect();
}
_cloneHide(activeSortable, isOwner);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) {
if (!dragEl.contains(el)) {
el.appendChild(dragEl);
parentEl = el; // actualization
}
this._animate(dragRect, dragEl);
target && this._animate(targetRect, target);
}
}
else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
if (lastEl !== target) {
lastEl = target;
lastCSS = _css(target);
lastParentCSS = _css(target.parentNode);
}
targetRect = target.getBoundingClientRect();
var width = targetRect.right - targetRect.left,
height = targetRect.bottom - targetRect.top,
floating = R_FLOAT.test(lastCSS.cssFloat + lastCSS.display)
|| (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
isWide = (target.offsetWidth > dragEl.offsetWidth),
isLong = (target.offsetHeight > dragEl.offsetHeight),
halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
nextSibling = target.nextElementSibling,
after = false
;
if (floating) {
var elTop = dragEl.offsetTop,
tgTop = target.offsetTop;
if (elTop === tgTop) {
after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
}
else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) {
after = (evt.clientY - targetRect.top) / height > 0.5;
} else {
after = tgTop > elTop;
}
} else if (!isMovingBetweenSortable) {
after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
}
var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after);
if (moveVector !== false) {
if (moveVector === 1 || moveVector === -1) {
after = (moveVector === 1);
}
_silent = true;
setTimeout(_unsilent, 30);
_cloneHide(activeSortable, isOwner);
if (!dragEl.contains(el)) {
if (after && !nextSibling) {
el.appendChild(dragEl);
} else {
target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
}
}
parentEl = dragEl.parentNode; // actualization
this._animate(dragRect, dragEl);
this._animate(targetRect, target);
}
}
}
},
_animate: function (prevRect, target) {
var ms = this.options.animation;
if (ms) {
var currentRect = target.getBoundingClientRect();
if (prevRect.nodeType === 1) {
prevRect = prevRect.getBoundingClientRect();
}
_css(target, 'transition', 'none');
_css(target, 'transform', 'translate3d('
+ (prevRect.left - currentRect.left) + 'px,'
+ (prevRect.top - currentRect.top) + 'px,0)'
);
forRepaintDummy = target.offsetWidth; // repaint
_css(target, 'transition', 'all ' + ms + 'ms');
_css(target, 'transform', 'translate3d(0,0,0)');
clearTimeout(target.animated);
target.animated = setTimeout(function () {
_css(target, 'transition', '');
_css(target, 'transform', '');
target.animated = false;
}, ms);
}
},
_offUpEvents: function () {
var ownerDocument = this.el.ownerDocument;
_off(document, 'touchmove', this._onTouchMove);
_off(document, 'pointermove', this._onTouchMove);
_off(ownerDocument, 'mouseup', this._onDrop);
_off(ownerDocument, 'touchend', this._onDrop);
_off(ownerDocument, 'pointerup', this._onDrop);
_off(ownerDocument, 'touchcancel', this._onDrop);
_off(ownerDocument, 'pointercancel', this._onDrop);
_off(ownerDocument, 'selectstart', this);
},
_onDrop: function (/**Event*/evt) {
var el = this.el,
options = this.options;
clearInterval(this._loopId);
clearInterval(autoScroll.pid);
clearTimeout(this._dragStartTimer);
_cancelNextTick(this._cloneId);
_cancelNextTick(this._dragStartId);
// Unbind events
_off(document, 'mouseover', this);
_off(document, 'mousemove', this._onTouchMove);
if (this.nativeDraggable) {
_off(document, 'drop', this);
_off(el, 'dragstart', this._onDragStart);
}
this._offUpEvents();
if (evt) {
if (moved) {
evt.preventDefault();
!options.dropBubble && evt.stopPropagation();
}
ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl);
if (rootEl === parentEl || Sortable.active.lastPullMode !== 'clone') {
// Remove clone
cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl);
}
if (dragEl) {
if (this.nativeDraggable) {
_off(dragEl, 'dragend', this);
}
_disableDraggable(dragEl);
dragEl.style['will-change'] = '';
// Remove class's
_toggleClass(dragEl, this.options.ghostClass, false);
_toggleClass(dragEl, this.options.chosenClass, false);
// Drag stop event
_dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex, null, evt);
if (rootEl !== parentEl) {
newIndex = _index(dragEl, options.draggable);
if (newIndex >= 0) {
// Add event
_dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
// Remove event
_dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
// drag from one list and drop into another
_dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
_dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
}
}
else {
if (dragEl.nextSibling !== nextEl) {
// Get the index of the dragged element within its parent
newIndex = _index(dragEl, options.draggable);
if (newIndex >= 0) {
// drag & drop within the same list
_dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
_dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
}
}
}
if (Sortable.active) {
/* jshint eqnull:true */
if (newIndex == null || newIndex === -1) {
newIndex = oldIndex;
}
_dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
// Save sorting
this.save();
}
}
}
this._nulling();
},
_nulling: function() {
rootEl =
dragEl =
parentEl =
ghostEl =
nextEl =
cloneEl =
lastDownEl =
scrollEl =
scrollParentEl =
tapEvt =
touchEvt =
moved =
newIndex =
lastEl =
lastCSS =
putSortable =
activeGroup =
Sortable.active = null;
savedInputChecked.forEach(function (el) {
el.checked = true;
});
savedInputChecked.length = 0;
},
handleEvent: function (/**Event*/evt) {
switch (evt.type) {
case 'drop':
case 'dragend':
this._onDrop(evt);
break;
case 'dragover':
case 'dragenter':
if (dragEl) {
this._onDragOver(evt);
_globalDragOver(evt);
}
break;
case 'mouseover':
this._onDrop(evt);
break;
case 'selectstart':
evt.preventDefault();
break;
}
},
/**
* Serializes the item into an array of string.
* @returns {String[]}
*/
toArray: function () {
var order = [],
el,
children = this.el.children,
i = 0,
n = children.length,
options = this.options;
for (; i < n; i++) {
el = children[i];
if (_closest(el, options.draggable, this.el)) {
order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
}
}
return order;
},
/**
* Sorts the elements according to the array.
* @param {String[]} order order of the items
*/
sort: function (order) {
var items = {}, rootEl = this.el;
this.toArray().forEach(function (id, i) {
var el = rootEl.children[i];
if (_closest(el, this.options.draggable, rootEl)) {
items[id] = el;
}
}, this);
order.forEach(function (id) {
if (items[id]) {
rootEl.removeChild(items[id]);
rootEl.appendChild(items[id]);
}
});
},
/**
* Save the current sorting
*/
save: function () {
var store = this.options.store;
store && store.set(this);
},
/**
* For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
* @param {HTMLElement} el
* @param {String} [selector] default: `options.draggable`
* @returns {HTMLElement|null}
*/
closest: function (el, selector) {
return _closest(el, selector || this.options.draggable, this.el);
},
/**
* Set/get option
* @param {string} name
* @param {*} [value]
* @returns {*}
*/
option: function (name, value) {
var options = this.options;
if (value === void 0) {
return options[name];
} else {
options[name] = value;
if (name === 'group') {
_prepareGroup(options);
}
}
},
/**
* Destroy
*/
destroy: function () {
var el = this.el;
el[expando] = null;
_off(el, 'mousedown', this._onTapStart);
_off(el, 'touchstart', this._onTapStart);
_off(el, 'pointerdown', this._onTapStart);
if (this.nativeDraggable) {
_off(el, 'dragover', this);
_off(el, 'dragenter', this);
}
// Remove draggable attributes
Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
el.removeAttribute('draggable');
});
touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
this._onDrop();
this.el = el = null;
}
};
function _cloneHide(sortable, state) {
if (sortable.lastPullMode !== 'clone') {
state = true;
}
if (cloneEl && (cloneEl.state !== state)) {
_css(cloneEl, 'display', state ? 'none' : '');
if (!state) {
if (cloneEl.state) {
if (sortable.options.group.revertClone) {
rootEl.insertBefore(cloneEl, nextEl);
sortable._animate(dragEl, cloneEl);
} else {
rootEl.insertBefore(cloneEl, dragEl);
}
}
}
cloneEl.state = state;
}
}
function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
if (el) {
ctx = ctx || document;
do {
if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) {
return el;
}
/* jshint boss:true */
} while (el = _getParentOrHost(el));
}
return null;
}
function _getParentOrHost(el) {
var parent = el.host;
return (parent && parent.nodeType) ? parent : el.parentNode;
}
function _globalDragOver(/**Event*/evt) {
if (evt.dataTransfer) {
evt.dataTransfer.dropEffect = 'move';
}
evt.preventDefault();
}
function _on(el, event, fn) {
el.addEventListener(event, fn, captureMode);
}
function _off(el, event, fn) {
el.removeEventListener(event, fn, captureMode);
}
function _toggleClass(el, name, state) {
if (el) {
if (el.classList) {
el.classList[state ? 'add' : 'remove'](name);
}
else {
var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' ');
el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' ');
}
}
}
function _css(el, prop, val) {
var style = el && el.style;
if (style) {
if (val === void 0) {
if (document.defaultView && document.defaultView.getComputedStyle) {
val = document.defaultView.getComputedStyle(el, '');
}
else if (el.currentStyle) {
val = el.currentStyle;
}
return prop === void 0 ? val : val[prop];
}
else {
if (!(prop in style)) {
prop = '-webkit-' + prop;
}
style[prop] = val + (typeof val === 'string' ? '' : 'px');
}
}
}
function _find(ctx, tagName, iterator) {
if (ctx) {
var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
if (iterator) {
for (; i < n; i++) {
iterator(list[i], i);
}
}
return list;
}
return [];
}
function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex, originalEvt) {
sortable = (sortable || rootEl[expando]);
var evt = document.createEvent('Event'),
options = sortable.options,
onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
evt.initEvent(name, true, true);
evt.to = toEl || rootEl;
evt.from = fromEl || rootEl;
evt.item = targetEl || rootEl;
evt.clone = cloneEl;
evt.oldIndex = startIndex;
evt.newIndex = newIndex;
evt.originalEvent = originalEvt;
rootEl.dispatchEvent(evt);
if (options[onName]) {
options[onName].call(sortable, evt);
}
}
function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) {
var evt,
sortable = fromEl[expando],
onMoveFn = sortable.options.onMove,
retVal;
evt = document.createEvent('Event');
evt.initEvent('move', true, true);
evt.to = toEl;
evt.from = fromEl;
evt.dragged = dragEl;
evt.draggedRect = dragRect;
evt.related = targetEl || toEl;
evt.relatedRect = targetRect || toEl.getBoundingClientRect();
evt.willInsertAfter = willInsertAfter;
evt.originalEvent = originalEvt;
fromEl.dispatchEvent(evt);
if (onMoveFn) {
retVal = onMoveFn.call(sortable, evt, originalEvt);
}
return retVal;
}
function _disableDraggable(el) {
el.draggable = false;
}
function _unsilent() {
_silent = false;
}
/** @returns {HTMLElement|false} */
function _ghostIsLast(el, evt) {
var lastEl = el.lastElementChild,
rect = lastEl.getBoundingClientRect();
// 5 — min delta
// abs — нельзя добавлять, а то глюки при наведении сверху
return (evt.clientY - (rect.top + rect.height) > 5) ||
(evt.clientX - (rect.left + rect.width) > 5);
}
/**
* Generate id
* @param {HTMLElement} el
* @returns {String}
* @private
*/
function _generateId(el) {
var str = el.tagName + el.className + el.src + el.href + el.textContent,
i = str.length,
sum = 0;
while (i--) {
sum += str.charCodeAt(i);
}
return sum.toString(36);
}
/**
* Returns the index of an element within its parent for a selected set of
* elements
* @param {HTMLElement} el
* @param {selector} selector
* @return {number}
*/
function _index(el, selector) {
var index = 0;
if (!el || !el.parentNode) {
return -1;
}
while (el && (el = el.previousElementSibling)) {
if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) {
index++;
}
}
return index;
}
function _matches(/**HTMLElement*/el, /**String*/selector) {
if (el) {
try {
if (el.matches) {
return el.matches(selector);
} else if (el.msMatchesSelector) {
return el.msMatchesSelector(selector);
}
} catch(_) {
return false;
}
}
return false;
}
function _throttle(callback, ms) {
var args, _this;
return function () {
if (args === void 0) {
args = arguments;
_this = this;
setTimeout(function () {
if (args.length === 1) {
callback.call(_this, args[0]);
} else {
callback.apply(_this, args);
}
args = void 0;
}, ms);
}
};
}
function _extend(dst, src) {
if (dst && src) {
for (var key in src) {
if (src.hasOwnProperty(key)) {
dst[key] = src[key];
}
}
}
return dst;
}
function _clone(el) {
if (Polymer && Polymer.dom) {
return Polymer.dom(el).cloneNode(true);
}
else if ($) {
return $(el).clone(true)[0];
}
else {
return el.cloneNode(true);
}
}
function _saveInputCheckedState(root) {
savedInputChecked.length = 0;
var inputs = root.getElementsByTagName('input');
var idx = inputs.length;
while (idx--) {
var el = inputs[idx];
el.checked && savedInputChecked.push(el);
}
}
function _nextTick(fn) {
return setTimeout(fn, 0);
}
function _cancelNextTick(id) {
return clearTimeout(id);
}
// Fixed #973:
_on(document, 'touchmove', function (evt) {
if (Sortable.active) {
evt.preventDefault();
}
});
// Export utils
Sortable.utils = {
on: _on,
off: _off,
css: _css,
find: _find,
is: function (el, selector) {
return !!_closest(el, selector, el);
},
extend: _extend,
throttle: _throttle,
closest: _closest,
toggleClass: _toggleClass,
clone: _clone,
index: _index,
nextTick: _nextTick,
cancelNextTick: _cancelNextTick
};
/**
* Create sortable instance
* @param {HTMLElement} el
* @param {Object} [options]
*/
Sortable.create = function (el, options) {
return new Sortable(el, options);
};
// Export
Sortable.version = '1.7.0';
return Sortable;
});
关于用到的plupload资源也一并上传,在文章开始可下载