赞
踩
最近在研究做一个多图上传,大视频上传、支持断点续传的功能,发现plupload插件比较符合要求,于是进行了简单研究,有什么不对的地方还望大家指正(新手,第一次写博客。。。)。
主要使用plupload插件(plupload.full.min.js、jquery.plupload.queue.js、jquery.plupload.queue.css、zh_CN.js)、以及Sortable.js插件
效果❤:
1.
4.
引入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>
<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>
<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>
<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>
//图片视频上传 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.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
* @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资源也一并上传,在文章开始可下载
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。