Ueditor中支持音频
已有不少小伙伴给出了Ueditor中拓展支持插入音频功能的方法,但还是存在一些特例性的问题。踩完数个坑后总算把音频功能基本搞顺畅,这里整理汇总留个记录,以防年长健忘。
1. 修改ueditor.config.js文件,增加插入音频功能入口:
(1)在toolbars中增加'insertaudio'
toolbars: [[
'source', '|', 'undo', 'redo', '|',
...
'simpleupload', 'insertimage', 'emotion', 'insertaudio', 'insertvideo',
'|',
'horizontal', 'date', 'time', 'spechars', '|', 'wordimage'
]]
(2)在labelMap中增加'insertaudio'对应的提示文字
labelMap: {
'anchor' : '',
'vaecolor' : '自定义字体颜色',
'insertaudio' : '音频',
}
2. 修改ueditor.all.js文件,增加插入音频页面和命令入口:
(1)在iframeUrlMap中增加插入音频页面的路径
var iframeUrlMap = {
...
'insertaudio':'~/dialogs/audio/audio.html',
'insertvideo':'~/dialogs/video/video.html',
...
};
(2)在btnCmds中增加点击插入音频触发的命令
var btnCmds = ['undo', 'redo', 'formatmatch',
...
'insertaudio'];
(3)在dialogBtns的ok属性中增加插入音频对话框
var dialogBtns = {
noOk: ['searchreplace', 'help', 'spechars', 'webapp','preview'],
ok: ['attachment', 'anchor', 'link', 'insertimage', 'map', 'gmap',
...
'insertaudio', 'vaecolor']
};
3. 增加audio.html:
(1)在ueditor/dialogs目录下增加audio目录,并增加audio.html,实现点击插入音频按钮时弹出的插入音频页面(此处可根据自身业务需求参照video.html或image.html来实现)
注:此页面中提供了插入音频和上传音频两种方式,现只实现了上传音频的功能。插入音频相对比较简单,不详述
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>音频对话框</title>
<script type="text/javascript" src="../internal.js"></script>
<!-- jquery -->
<script type="text/javascript" src="../../third-party/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="../../../mains/public/public.js"></script>
<!-- webuploader -->
<script src="../../third-party/webuploader/webuploader.min.js"></script>
<link rel="stylesheet" type="text/css" href="../../third-party/webuploader/webuploader.css">
<!-- image dialog -->
<link rel="stylesheet" href="audio.css" type="text/css" />
</head>
<body>
<div class="wrapper">
<div id="tabhead" class="tabhead">
<span class="tab focus" data-content-id="upload"><var id="lang_tab_upload"></var></span>
</div>
<div id="tabbody" class="tabbody">
<!-- 插入音频 -->
<div id="remote" class="panel">
<div class="top">
<div class="row">
<label for="url"><var id="lang_input_url"></var></label>
<span><input class="text" id="url" type="text"/></span>
</div>
</div>
<div class="left">
<div class="row">
<label><var id="lang_input_size"></var></label>
<span><var id="lang_input_width"> </var><input class="text" type="text" id="width"/>px </span>
<span><var id="lang_input_height"> </var><input class="text" type="text" id="height"/>px </span>
<span><input id="lock" type="checkbox" disabled="disabled"><span id="lockicon"></span></span>
</div>
<div class="row">
<label><var id="lang_input_border"></var></label>
<span><input class="text" type="text" id="border"/>px </span>
</div>
<div class="row">
<label><var id="lang_input_vhspace"></var></label>
<span><input class="text" type="text" id="vhSpace"/>px </span>
</div>
<div class="row">
<label><var id="lang_input_title"></var></label>
<span><input class="text" type="text" id="title"/></span>
</div>
</div>
<div class="right"><div id="preview"></div></div>
</div>
<!-- 上传音频 -->
<div id="upload" class="panel focus">
<div class="titleBar">
<label>标题</label>
<input type="text" class="uploadAudioTitle"/>
</div>
<div id="queueList" class="queueList">
<div class="statusBar element-invisible">
<div class="progress">
<span class="text">0%</span>
<span class="percentage"></span>
</div><div class="info"></div>
<div class="btns">
<div id="filePickerBtn"></div>
<div class="uploadBtn"><var id="lang_start_upload"></var></div>
</div>
</div>
<div id="dndArea" class="placeholder">
<div class="filePickerContainer">
<div id="filePickerReady"></div>
</div>
</div>
<ul class="filelist element-invisible">
<li id="filePickerBlock" class="filePickerBlock"></li>
</ul>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="audio.js"></script>
</body>
</html>
(2)增加audio.css用以实现audio.html中的相关样式(此处可照搬video.css或image.css后对应修改下)
/* 上传音频 */
.tabbody #upload.panel {
width: 0;
height: 0;
overflow: hidden;
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px);
background: #fff;
display: block;
}
.tabbody #upload.panel.focus {
width: 100%;
height: 300px;
display: block;
clip: auto;
}
#upload .titleBar {
width: 100%;
margin: 10px;
font-size: 14px;
}
#upload .titleBar .uploadAudioTitle {
margin-left: 5px;
width: 88%;
}
// 后面省略,照搬后修改即可
...
(3)增加audio.js用以实现audio.html中的上传音频、插入音频代码等操作(同样的,照搬vedio.js或image.js后对应修改即可)
注:涉及到几个特例化的地方:①音频上传至服务器或云存储,改造过图片上传的应该驾轻就熟了;②因要求给插入的音频配上标题,此处上传为单个音频文件上传,故要禁用多选批量上传;③开始上传、暂停上传、取消上传的相应功能;④最终insertList的结构,要传递给ueditor.all.js中的insertaudio命令以向编辑器内插入音频控件代码
(function () {
var insertaudio,
uploadaudio;
// 音频文件key前缀
var keyPrefix = editor.getOpt('keyPrefix') + '/audio';
window.onload = function () {
initTabs();
initButtons();
};
/* 初始化tab标签 */
function initTabs() {
var tabs = $G('tabhead').children;
var audio = editor.selection.getRange().getClosedNode();
var id = tabs[0].getAttribute('data-content-id');
for (var i = 0; i < tabs.length; i++) {
domUtils.on(tabs[i], "click", function (e) {
var j, bodyId, target = e.target || e.srcElement;
id = target.getAttribute('data-content-id');
for (j = 0; j < tabs.length; j++) {
bodyId = tabs[j].getAttribute('data-content-id');
if(tabs[j] == target){
domUtils.addClass(tabs[j], 'focus');
domUtils.addClass($G(bodyId), 'focus');
}else {
domUtils.removeClasses(tabs[j], 'focus');
domUtils.removeClasses($G(bodyId), 'focus');
}
}
});
}
switch (id) {
case 'remote': // 插入音频/远程音频(预留)
insertaudio = insertaudio || new RemoteAudio();
break;
case 'upload': // 上传音频(主要)
uploadaudio = uploadaudio || new UploadAudio('queueList');
break;
}
}
/* 初始化onok事件 */
function initButtons() {
dialog.onok = function () {
var remote = false, list = [], id, tabs = $G('tabhead').children;
for (var i = 0; i < tabs.length; i++) {
if (domUtils.hasClass(tabs[i], 'focus')) {
id = tabs[i].getAttribute('data-content-id');
break;
}
}
switch (id) {
case 'remote':
list = insertaudio.getInsertList();
break;
case 'upload':
list = uploadaudio.getInsertList();
var count = uploadaudio.getQueueCount();
if (count) {
$('.info', '#queueList').html('<span style="color:red;">' + '还有2个未上传文件'.replace(/[\d]/, count) + '</span>');
return false;
}
// 配上标题
var title = $('.uploadAudioTitle').val();
if (!title || $.trim(title) == '') {
alert('请填写标题');
$('.uploadAudioTitle').focus();
return false;
}
if(list) {
for(var i = 0; i < list.length; i++) {
var f = list[i];
f['title'] = title;
list[i] = f;
}
}
break;
}
if(list) {
editor.execCommand('insertaudio', list);
remote && editor.fireEvent("catchRemoteAudio");
}
};
}
/* 上传音频 */
function UploadAudio(target) {
this.$wrap = target.constructor == String ? $('#' + target) : $(target);
this.init();
}
UploadAudio.prototype = {
init: function () {
this.audioList = [];
this.initContainer();
this.initUploader();
},
initContainer: function () {
this.$queue = this.$wrap.find('.filelist');
},
/* 初始化容器 */
initUploader: function () {
var _this = this,
$ = jQuery, // just in case. Make sure it's not an other libaray.
$wrap = _this.$wrap,
// 文件容器
$queue = $wrap.find('.filelist'),
// 状态栏,包括进度和控制按钮
$statusBar = $wrap.find('.statusBar'),
// 文件总体选择信息。
$info = $statusBar.find('.info'),
// 上传按钮
$upload = $wrap.find('.uploadBtn'),
// 上传按钮
$filePickerBtn = $wrap.find('.filePickerBtn'),
// 上传按钮
$filePickerBlock = $wrap.find('.filePickerBlock'),
// 没选择文件之前的内容。
$placeHolder = $wrap.find('.placeholder'),
// 总体进度条
$progress = $statusBar.find('.progress').hide(),
// 添加的文件数量
fileCount = 0,
// 添加的文件总大小
fileSize = 0,
// 优化retina, 在retina下这个值是2
ratio = window.devicePixelRatio || 1,
// 缩略图大小
thumbnailWidth = 550 * ratio,
thumbnailHeight = 113 * ratio,
// 可能有pedding, ready, uploading, confirm, done.
state = '',
// 所有文件的进度信息,key为file id
percentages = {},
supportTransition = (function () {
var s = document.createElement('p').style,
r = 'transition' in s ||
'WebkitTransition' in s ||
'MozTransition' in s ||
'msTransition' in s ||
'OTransition' in s;
s = null;
return r;
})(),
// WebUploader实例
uploader,
actionUrl = editor.getActionUrl(editor.getOpt('audioActionName')),
acceptExtensions = (editor.getOpt('audioAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, ''),
audioMaxSize = editor.getOpt('audioMaxSize'),
imageCompressBorder = editor.getOpt('imageCompressBorder');
if (!WebUploader.Uploader.support()) {
$('#filePickerReady').after($('<div>').html(lang.errorNotSupport)).hide();
return;
} else if (!editor.getOpt('audioActionName')) {
$('#filePickerReady').after($('<div>').html(lang.errorLoadConfig)).hide();
return;
}
uploader = _this.uploader = WebUploader.create({
pick: {
id: '#filePickerReady',
label: lang.uploadSelectFile,
multiple: false // 限制为单选
},
accept: {
title: 'Audios',
extensions: acceptExtensions,
mimeTypes: 'audio/mp3,audio/amr,audio/wma,audio/wav'
},
swf: '../../third-party/webuploader/Uploader.swf',
server: actionUrl,
fileVal: editor.getOpt('audioFieldName'),
duplicate: false,
fileNumLimit: 1, // 限制为单个文件
fileSingleSizeLimit: audioMaxSize // 默认 30 M
});
uploader.addButton({
id: '#filePickerBlock'
});
// uploader.addButton({
// id: '#filePickerBtn',
// label: lang.uploadAddFile
// });
setState('pedding');
// 当有文件添加进来时执行,负责view的创建
function addFile(file) {
var $li = $('<li id="' + file.id + '">' +
'<p class="title">' + file.name + '</p>' +
'<p class="progress"><span></span></p>' +
'</li>'),
$btns = $('<div class="file-panel">' +
'<span class="cancel">' + lang.uploadDelete + '</span>' +
'<span class="rotateRight">' + lang.uploadTurnRight + '</span>' +
'<span class="rotateLeft">' + lang.uploadTurnLeft + '</span></div>').appendTo($li),
$prgress = $li.find('p.progress span'),
$wrap = $li.find('p.imgWrap'),
$info = $('<p class="error"></p>').hide().appendTo($li),
showError = function (code) {
switch (code) {
case 'exceed_size':
text = lang.errorExceedSize;
break;
case 'interrupt':
text = lang.errorInterrupt;
break;
case 'http':
text = lang.errorHttp;
break;
case 'not_allow_type':
text = lang.errorFileType;
break;
default:
text = lang.errorUploadRetry;
break;
}
$info.text(text).show();
};
if (file.getStatus() === 'invalid') {
showError(file.statusText);
} else {
percentages[ file.id ] = [ file.size, 0 ];
file.rotation = 0;
/* 检查文件格式 */
if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) {
showError('not_allow_type');
uploader.removeFile(file);
}
}
file.on('statuschange', function (cur, prev) {
if (prev === 'progress') {
$prgress.hide().width(0);
} else if (prev === 'queued') {
$li.off('mouseenter mouseleave');
$btns.remove();
}
// 成功
if (cur === 'error' || cur === 'invalid') {
showError(file.statusText);
percentages[ file.id ][ 1 ] = 1;
} else if (cur === 'interrupt') {
showError('interrupt');
} else if (cur === 'queued') {
percentages[ file.id ][ 1 ] = 0;
} else if (cur === 'progress') {
$info.hide();
$prgress.css('display', 'block');
} else if (cur === 'complete') {
}
$li.removeClass('state-' + prev).addClass('state-' + cur);
});
$li.on('mouseenter', function () {
$btns.stop().animate({height: 30});
});
$li.on('mouseleave', function () {
$btns.stop().animate({height: 0});
});
$btns.on('click', 'span', function () {
var index = $(this).index(),
deg;
switch (index) {
case 0:
uploader.removeFile(file);
return;
case 1:
file.rotation += 90;
break;
case 2:
file.rotation -= 90;
break;
}
if (supportTransition) {
deg = 'rotate(' + file.rotation + 'deg)';
$wrap.css({
'-webkit-transform': deg,
'-mos-transform': deg,
'-o-transform': deg,
'transform': deg
});
} else {
$wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')');
}
});
$li.insertBefore($filePickerBlock);
// 隐藏继续添加控件,设置为单个文件上传
$filePickerBlock.hide();
}
// 取消上传
function cancelFile(file) {
var $li = $('#' + file.id);
var spans = $progress.children();
spans.eq(0).text('0%');
spans.eq(1).css('width', '0%');
$progress.css('display', 'none');
$('.statusBar').children('.info').css('display', 'inline-block');
$('.error').remove();
var upBtn = $('.uploadBtn');
upBtn.removeClass('state-paused disabled');
upBtn.addClass('state-ready');
upBtn.html(lang.uploadStart);
}
// 负责view的销毁
function removeFile(file) {
var $li = $('#' + file.id);
delete percentages[ file.id ];
updateTotalProgress();
$li.off().find('.file-panel').off().end().remove();
// 显示继续添加控件
$filePickerBlock.show();
}
function updateTotalProgress() {
var loaded = 0,
total = 0,
spans = $progress.children(),
percent;
$.each(percentages, function (k, v) {
total += v[ 0 ];
loaded += v[ 0 ] * v[ 1 ];
});
percent = total ? loaded / total : 0;
spans.eq(0).text(Math.round(percent * 100) + '%');
spans.eq(1).css('width', Math.round(percent * 100) + '%');
updateStatus();
}
function setState(val, files) {
if (val != state) {
var stats = uploader.getStats();
$upload.removeClass('state-' + state);
$upload.addClass('state-' + val);
switch (val) {
/* 未选择文件 */
case 'pedding':
$queue.addClass('element-invisible');
$statusBar.addClass('element-invisible');
$placeHolder.removeClass('element-invisible');
$progress.hide(); $info.hide();
uploader.refresh();
break;
/* 可以开始上传 */
case 'ready':
$placeHolder.addClass('element-invisible');
$queue.removeClass('element-invisible');
$statusBar.removeClass('element-invisible');
$progress.hide(); $info.show();
$upload.text(lang.uploadStart);
uploader.refresh();
break;
/* 上传中 */
case 'uploading':
$progress.show(); $info.hide();
// $upload.text(lang.uploadPause);
$upload.text(lang.uploadCancel);
break;
/* 暂停上传 */
case 'paused':
$progress.show(); $info.hide();
$upload.text(lang.uploadContinue);
break;
/* 取消上传 */
case 'cancel':
$placeHolder.addClass('element-invisible');
$queue.removeClass('element-invisible');
$statusBar.removeClass('element-invisible');
$progress.hide(); $info.show();
$upload.text(lang.uploadStart);
uploader.refresh();
break;
case 'confirm':
$progress.show(); $info.hide();
$upload.text(lang.uploadStart);
stats = uploader.getStats();
if (stats.successNum && !stats.uploadFailNum) {
setState('finish');
return;
}
break;
case 'finish':
$progress.hide(); $info.show();
if (stats.uploadFailNum) {
$upload.text(lang.uploadRetry);
} else {
$upload.text(lang.uploadStart);
}
break;
}
state = val;
updateStatus();
}
if (!_this.getQueueCount()) {
$upload.addClass('disabled')
} else {
$upload.removeClass('disabled')
}
}
function updateStatus() {
var text = '', stats;
if (state === 'ready') {
text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize));
} else if (state === 'confirm') {
stats = uploader.getStats();
if (stats.uploadFailNum) {
text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum);
}
} else {
stats = uploader.getStats();
text = lang.updateStatusFinish.replace('_', fileCount).
replace('_KB', WebUploader.formatSize(fileSize)).
replace('_', stats.successNum);
if (stats.uploadFailNum) {
text += lang.updateStatusError.replace('_', stats.uploadFailNum);
}
}
$info.html(text);
}
uploader.on('fileQueued', function (file) {
fileCount++;
fileSize += file.size;
if (fileCount === 1) {
$placeHolder.addClass('element-invisible');
$statusBar.show();
}
addFile(file);
});
uploader.on('fileDequeued', function (file) {
fileCount--;
fileSize -= file.size;
removeFile(file);
updateTotalProgress();
});
uploader.on('filesQueued', function (file) {
if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) {
setState('ready');
}
updateTotalProgress();
});
uploader.on('all', function (type, files) {
switch (type) {
case 'uploadFinished':
setState('confirm', files);
break;
case 'startUpload':
/* 添加额外的GET参数 */
var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '';
//url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params);
uploader.option('server', editor.getOpt('imageUrl'));
setState('uploading', files);
break;
case 'stopUpload':
setState('paused', files);
break;
}
});
uploader.on('uploadBeforeSend', function (file, data, header) {
//这里可以通过data对象添加POST参数
header['X_Requested_With'] = 'XMLHttpRequest';
// 上传token
var token = getUploadToken4UE();
if (token == null) {
alert('获取上传token异常,请稍后再试~');
return false;
}
data['token'] = token;
// 文件key
data['key'] = keyPrefix + '/' + uuid();
});
uploader.on('uploadProgress', function (file, percentage) {
var $li = $('#' + file.id),
$percent = $li.find('.progress span');
$percent.css('width', percentage * 100 + '%');
percentages[ file.id ][ 1 ] = percentage;
updateTotalProgress();
});
uploader.on('uploadSuccess', function (file, ret) {
var $file = $('#' + file.id);
try {
var responseText = (ret._raw || ret),
json = utils.str2json(responseText);
if (json.state == 'SUCCESS') {
//_this.audioList.push(json);
_this.audioList[$file.index()] = json; //按选择好的文件列表顺序存储
$file.append('<span class="success"></span>');
} else {
$file.find('.error').text(json.state).show();
}
} catch (e) {
$file.find('.error').text(lang.errorServerUpload).show();
}
});
uploader.on('uploadError', function (file, code) {
});
uploader.on('error', function (code, file) {
if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') {
addFile(file);
}
});
uploader.on('uploadComplete', function (file, ret) {
});
$upload.on('click', function () {
if ($(this).hasClass('disabled')) {
return false;
}
if (state === 'ready') {
uploader.upload();
} else if (state === 'paused') {
uploader.upload();
} else if (state === 'cancel') {
uploader.upload();
} else if (state === 'uploading') {
// uploader.stop();
// 调整为取消上传
var file = uploader.getFiles()[0];
uploader.stop(file);
// removeFile(file);
cancelFile(file);
// setState('cancel');
}
});
$upload.addClass('state-' + state);
updateTotalProgress();
},
getQueueCount: function () {
var file, i, status, readyFile = 0, files = this.uploader.getFiles();
for (i = 0; file = files[i++]; ) {
status = file.getStatus();
if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++;
}
return readyFile;
},
destroy: function () {
this.$wrap.remove();
},
getInsertList: function () {
var i, data, list = [],
prefix = editor.getOpt('audioUrlPrefix');
for (i = 0; i < this.audioList.length; i++) {
data = this.audioList[i];
if(data == undefined){
continue;
}
//修改END
list.push({
src: prefix + data.key,
key: + new Date() // 以时间戳作为音频控件父div的id
});
}
return list;
}
};
})();
(4)修改ueditor.css文件,增加插入音频按钮图标及页面窗口的相关样式:
.edui-default .edui-for-insertaudio .edui-icon {
background-position: -320px -20px;
}
/*audio-dialog*/
.edui-default .edui-for-insertaudio .edui-dialog-content {
width: 590px;
height: 390px;
}
/* audio*/
.edui-default .edui-for-insertaudio .edui-icon {
background-image: url(../images/audio.png) !important;
}
音频按钮图标放在ueditor目录的themes/default/images下。
(5)修改zh-cn.js文件,增加insertaudio的相关配置(类同insertvideo):
'insertaudio' : {
'static' : {
'lang_tab_remote' : "插入音频",
'lang_tab_upload' : "上传音频",
},
'uploadSelectFile' : '点击选择音频文件',
'uploadAddFile' : '继续添加',
'uploadStart' : '开始上传',
'uploadPause' : '暂停上传',
'uploadContinue' : '继续上传',
'uploadCancel' : '取消上传',
'uploadRetry' : '重试上传',
'uploadDelete' : '删除',
'uploadTurnLeft' : '向左旋转',
'uploadTurnRight' : '向右旋转',
'uploadPreview' : '预览中',
'uploadNoPreview' : '不能预览',
'updateStatusReady' : '选中_个音频文件,共_KB。',
'updateStatusConfirm' : '已成功上传_个音频文件,_个音频文件上传失败',
'updateStatusFinish' : '共_个(_KB),_个上传成功',
'updateStatusError' : ',_个音频文件上传失败。',
'errorNotSupport' : 'WebUploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级 flash 播放器。',
'errorLoadConfig' : '后端配置项没有正常加载,上传插件不能正常使用!',
'errorExceedSize' : '文件大小超出',
'errorFileType' : '文件格式不允许',
'errorInterrupt' : '文件传输中断',
'errorUploadRetry' : '上传失败,请重试',
'errorHttp' : 'http请求错误',
'errorServerUpload' : '服务器返回出错',
'remoteLockError' : "宽高不正确,不能所定比例",
'numError' : "请输入正确的长度或者宽度值!例如:123,400",
'audioUrlError' : "不允许的音频格式或者图片域!",
'audioLoadError' : "音频加载失败!请检查链接地址或网络状态!",
'searchRemind' : "请输入搜索关键词",
'searchLoading' : "音频加载中,请稍后……",
'searchRetry' : " :( ,抱歉,没有找到音频!请重试一次!"
},
(6)修改config.json文件,增加音频上传的相关配置
/* 上传音频配置项 */
"audioActionName": "uploadaudio", /* 执行上传音频的action名称 */
"audioFieldName": "file", /* 提交的音频表单名称 */
"audioMaxSize": 30720000, /* 上传大小限制,单位B */
"audioAllowFiles": [".mp3", ".wma", ".wav", ".amr"], /* 上传音频格式限制 */
"audioUrlPrefix": "http://audio.ushallnotpass.com/" /* 音频访问路径前缀 */
4. 修改ueditor.all.js文件,增加audio插件
<audio>标签自带样式不太好看,此处audio插件对插入的音频控件样式进行了改造,并实现了相关的播放控制事件。感谢Dandelion_drq
分享的解决方案:H5 <audio> 音频标签自定义样式修改以及添加播放控制事件
audio插件相关代码:
/**
* audio插件,为UEditor提供音频插入支持
*/
UE.plugins['audio'] = function (){
var me = this;
// 从publis.js中获取的静态文件路径,需自行修改设置
var playicon = staticpath + 'audio/play.png';
var pauseicon = staticpath + 'audio/pause.png';
// 内容填入后初始化音频控件
me.addListener("afterSetContent", function() {
var audioArr = me.document.getElementsByTagName('audio');
if(audioArr) {
$.each(audioArr, function(i, a) {
var aDiv = domUtils.findParent(a, function(node) {
return node.className === 'audio-wrapper';
});
if(aDiv) {
initAudioEvent(aDiv);
}
});
}
});
/**
* 插入音频
* @command insertaudio
* @method execCommand
* @param { String } cmd 命令字符串
* @param { Object } audioObjs 键值对对象, 描述一个音频的所有属性
* @example
* ```javascript
*
* var audioObjs = {
* // 音频地址
* src: 'http://www.xxx.com/yyy',
* // 音频标题
* title: 'this is a title'
* };
*
* //editor 是编辑器实例
* //向编辑器插入单个音频
* editor.execCommand( 'insertaudio', audioObjs );
* ```
*/
UE.commands["insertaudio"] = {
execCommand: function (cmd, audioObjs) {
audioObjs = utils.isArray(audioObjs) ? audioObjs : [audioObjs];
if (!audioObjs) {
return false;
}
var html = [];
for (var i = 0; i < audioObjs.length; i++) {
var src = createAudioHtml(audioObjs[i].key, audioObjs[i].src, audioObjs[i].title);
html.push(src);
}
me.execCommand("inserthtml", html.join(""));
// 初始化音频控件
initAudio(audioObjs);
me.focus();
}
};
/**
* 构造音频控件html
*
* @param {string} audioDivId - 音频控件父div的id
* @param {string} audioSrc - 音频控件地址
* @param {string} audioTitle - 音频标题
*/
function createAudioHtml(audioDivId, audioSrc, audioTitle) {
var src = '<div class="audio-wrapper" id="' + audioDivId + '" style="background-color: #fcfcfc;margin: 10px auto;max-width: 670px;height: 90px;border: 1px solid #e0e0e0;">'
+'<audio><source src="' + audioSrc + '"></audio>'
+'<div class="audio-left" style="float: left;text-align: center;width: 22%;height: 100%;">'
+'<img src="' + playicon + '" class="playicon" style="width: 40px;position: relative;top: 25px;margin: 0;display: initial;cursor: pointer;"/>'
+'<img src="' + pauseicon + '" class="pauseicon" style="width: 40px;position: relative;top: 25px;margin: 0;display: none;cursor: pointer;"/>'
+'</div>'
+'<div class="audio-right" style="margin-right: 5%;float: right;width: 73%;height: 100%;">'
+'<p class="audio-title" style="max-width: 536px;font-size: 15px;height: 35%;margin: 8px 0;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;">' + audioTitle + '</p>'
+'<div class="progress-bar-bg" style="background-color: #d9d9d9;position: relative;height: 2px;cursor: pointer;">'
+'<span class="progressDot" style="content: \' \';width: 12px;height: 12px;border-radius: 50%;-moz-border-radius: 50%;-webkit-border-radius: 50%;background-color: #3e87e8;position: absolute;left: 0;top: -5px;margin-left: 0px;cursor: pointer;"></span>'
+'<div class="progressBar" style="background-color: #649fec;width: 0;height: 2px;"></div>'
+'</div>'
+'<div class="audio-time" style="overflow: hidden;margin-top: -1px;">'
+'<span class="audioCurTime" style="float: left;margin-top:10px;font-size: 12px;color: #969696">00:00</span><span class="audioTotalTime" style="float: right;margin-top:10px;font-size: 12px;color: #969696">00:00</span>'
+'</div>'
+'</div>'
+'</div><br/>';
return src;
}
/**
* 初始化音频控件
*
* @param {array} audioObjs - 音频父div数组
*/
function initAudio(audioObjs) {
if(audioObjs) {
for(var i = 0; i < audioObjs.length; i++) {
var audioDiv = me.document.getElementById(audioObjs[i].key);
initAudioEvent(audioDiv);
}
}
}
/**
* 初始化音频控制事件
*
* @param {object} audioDiv - 音频父div
*/
function initAudioEvent(audioDiv) {
// div子节点
var divArr = domUtils.getElementsByTagName(audioDiv, 'div');
// audio控件
var audio = domUtils.getElementsByTagName(audioDiv, 'audio')[0];
// 控制音频文件名显示宽度
var audioRight = domUtils.filterNodeList(divArr, function(node) {
return node.className === 'audio-right';
});
var title = domUtils.getElementsByTagName(audioRight, 'p')[0];
domUtils.setStyle(title, 'max-width', domUtils.getComputedStyle(audioRight, 'width'));
// 右侧div组
var rightDivArr = domUtils.getElementsByTagName(audioRight, 'div');
// 进度条div
var progressDiv = domUtils.filterNodeList(rightDivArr, function(node) {
return node.className === 'progress-bar-bg';
});
// 已播放进度条
var progressBar = domUtils.getElementsByTagName(progressDiv, 'div')[0];
domUtils.setStyle(progressBar, 'width', 0); // 初始化为未播放状态
// 进度条上控制点
var progressDot = domUtils.getElementsByTagName(progressDiv, 'span')[0];
domUtils.setStyle(progressDot, 'left', 0); // 初始化为未播放状态
// 时间div
var timeDiv = domUtils.filterNodeList(rightDivArr, function(node) {
return node.className === 'audio-time';
});
// 时间span组
var timeSpanArr = domUtils.getElementsByTagName(timeDiv, 'span');
// 已播放时间
var audioCurTime = domUtils.filterNodeList(timeSpanArr, function(node) {
return node.className === 'audioCurTime';
});
audioCurTime.innerHTML = '00:00'; // 初始化为0
// 总时间
var audioTotalTime = domUtils.filterNodeList(timeSpanArr, function(node) {
return node.className === 'audioTotalTime';
});
// 点击播放/暂停图片时,控制音乐的播放与暂停
var playerDiv = domUtils.filterNodeList(divArr, function(node) {
return node.className === 'audio-left';
});
// 播放图标、暂停图标
var playerImgs = domUtils.getElementsByTagName(playerDiv, 'img');
var playImg = domUtils.filterNodeList(playerImgs, function(node) {
return node.className === 'playicon';
});
var pauseImg = domUtils.filterNodeList(playerImgs, function(node) {
return node.className === 'pauseicon';
});
// 初始化播放图标
domUtils.setStyle(playImg, 'display', 'initial');
domUtils.setStyle(pauseImg, 'display', 'none');
// 音频准备就绪后执行
audio.addEventListener('canplay', function() {
audioTotalTime.innerHTML = transTime(audio.duration); // 初始化为音频总时长
});
// 播放事件
domUtils.on(playImg, 'click', function(e) {
// 禁止事件冒泡
if (e && e.stopPropagation) {
e.stopPropagation();
} else {
window.event.cancelBubble = true;
}
// 监听音频播放时间并更新进度条
audio.addEventListener('timeupdate', function () {
updateProgress(audio, progressBar, progressDot, audioCurTime);
}, false);
// 监听播放完成事件
audio.addEventListener('ended', function () {
audioEnded(progressBar, progressDot,
audioCurTime, playImg, pauseImg)
}, false);
// 播放
audio.play();
// 切换播放暂停图标
domUtils.setStyle(playImg, 'display', 'none');
domUtils.setStyle(pauseImg, 'display', 'initial');
// 暂停其他正在播放的音频
var audios = me.document.getElementsByTagName('audio');
for (var i = 0; i < audios.length; i++) {
var parentDiv = domUtils.findParent(audios[i], function(node) {
return node.className === 'audio-wrapper';
});
if (parentDiv.id != audioDiv.id && !audios[i].paused) {
audios[i].pause();
var playerDiv = domUtils.getNextDomNode(audios[i]);
var players = domUtils.getElementsByTagName(playerDiv, 'img');
var play = domUtils.filterNodeList(players, function(node) {
return node.className === 'playicon';
});
var pause = domUtils.filterNodeList(players, function(node) {
return node.className === 'pauseicon';
});
domUtils.setStyle(play, 'display', 'initial');
domUtils.setStyle(pause, 'display', 'none');
}
}
});
// 暂停事件
domUtils.on(pauseImg, 'click', function(e) {
// 禁止事件冒泡
if (e && e.stopPropagation) {
e.stopPropagation();
} else {
window.event.cancelBubble = true;
}
// 暂停
audio.pause();
// 切换播放暂停图标
domUtils.setStyle(playImg, 'display', 'initial');
domUtils.setStyle(pauseImg, 'display', 'none');
});
// 点击进度条跳到指定点播放
domUtils.on(progressDiv, 'mousedown', function(e) {
// 只有音乐开始播放后才可以调节,已经播放过但暂停了的也可以
if (!audio.paused || audio.currentTime != 0) {
var pgsWidth = parseInt(domUtils.getComputedStyle(progressDiv, 'width'));
var rate = e.offsetX / pgsWidth;
audio.currentTime = audio.duration * rate;
updateProgress(audio, progressBar, progressDot, audioCurTime);
}
});
// 鼠标拖动进度点时可以调节进度
// 只有音乐开始播放后才可以调节,已经播放过但暂停了的也可以
// 鼠标按下时
domUtils.on(progressDot, 'mousedown', function(e) {
if (!audio.paused || audio.currentTime != 0) {
var oriLeft = progressDot.offsetLeft;
var mouseX = e.clientX;
var maxLeft = oriLeft; // 向左最大可拖动距离
var maxRight = progressDiv.offsetWidth - oriLeft; // 向右最大可拖动距离
// 禁止默认的选中事件(避免鼠标拖拽进度点的时候选中文字)
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
// 禁止事件冒泡
if (e && e.stopPropagation) {
e.stopPropagation();
} else {
window.event.cancelBubble = true;
}
// 开始拖动
me.document.onmousemove = function (e) {
var length = e.clientX - mouseX;
if (length > maxRight) {
length = maxRight;
} else if (length < -maxLeft) {
length = -maxLeft;
}
var pgsWidth = parseInt(domUtils.getComputedStyle(progressDiv, 'width'));
var rate = (oriLeft + length) / pgsWidth;
audio.currentTime = audio.duration * rate;
updateProgress(audio, progressBar, progressDot, audioCurTime);
};
// 拖动结束
me.document.onmouseup = function () {
me.document.onmousemove = null;
me.document.onmouseup = null;
};
}
});
}
/**
* 更新进度条与当前播放时间
*
* @param {object} audio - audio对象
* @param {object} progressBar - 进度条对象
* @param {object} progressDot - 进度条控制点对象
* @param {object} audioCurTime - 当前播放时间对象
*/
function updateProgress(audio, progressBar, progressDot, audioCurTime) {
var value = audio.currentTime / audio.duration;
domUtils.setStyle(progressBar, 'width', value * 100 + '%');
domUtils.setStyle(progressDot, 'left', value * 100 + '%');
audioCurTime.innerHTML = transTime(audio.currentTime);
}
/**
* 播放完成时把进度调回开始的位置
*
* @param {object} progressBar - 进度条对象
* @param {object} progressDot - 进度条控制点对象
* @param {object} audioCurTime - 当前播放时间对象
* @param {object} playImg- 播放按钮图标
* @param {object} pauseImg- 暂停按钮图标
*/
function audioEnded(progressBar, progressDot, audioCurTime, playImg, pauseImg) {
domUtils.setStyle(progressBar, 'width', 0);
domUtils.setStyle(progressDot, 'left', 0);
domUtils.setStyle(playImg, 'display', 'initial');
domUtils.setStyle(pauseImg, 'display', 'none');
audioCurTime.innerHTML = '00:00';
}
/**
* 音频播放时间换算
*
* @param {number} value - 音频当前播放时间,单位秒
*/
function transTime(value) {
var time = "";
var h = parseInt(value / 3600);
value %= 3600;
var m = parseInt(value / 60);
var s = parseInt(value % 60);
if (h > 0) {
time = formatTime(h + ":" + m + ":" + s);
} else {
time = formatTime(m + ":" + s);
}
return time;
}
/**
* 格式化时间显示,补零对齐
*
* eg:2:4 --> 02:04
* @param {string} value - 形如 h:m:s 的字符串
*/
function formatTime(value) {
var time = "";
var s = value.split(':');
var i = 0;
for (; i < s.length - 1; i++) {
time += s[i].length == 1 ? ("0" + s[i]) : s[i];
time += ":";
}
time += s[i].length == 1 ? ("0" + s[i]) : s[i];
return time;
}
};
5. 解决ueditor复制粘贴冲突
上述几个步骤完成后,插入音频功能就已实现了。但在编辑器中插入音频后再粘贴其他内容时,会发现音频控件的进度条样式出了问题。这是因为ueditor在粘贴后会对空内容div进行删除,而audio插件所插入的音频控件中有空内容div。可修改相应方法将音频控件中的div排除。
在ueditor.all.js中找到filter(div)方法,再找到以下这段代码:
if (browser.webkit) {
var br = root.lastChild();
if (br && br.type == 'element' && br.tagName == 'br') {
root.removeChild(br)
}
utils.each(me.body.querySelectorAll('div'), function (node) {
if (domUtils.isEmptyBlock(node)) {
domUtils.remove(node,true)
}
})
}
对删除div的判定条件进行修改,排除音频控件的相关div:
if (domUtils.isEmptyBlock(node) && node.className != 'progress-bar-bg' && node.className != 'progressBar') {
domUtils.remove(node,true)
}
6. 文章页面音频控件初始化
在ueditor中编辑完文章内容之后,在展示文章时也需要对文章内容中的音频控件进行初始化,故需要提供相应的初始化方法供页面调用。实现方式比较简单,和audio插件中的方法基本一致,不详述。
另若需支持移动端的访问,则需要对点击、滑动等事件进行区分处理,对PC端访问要响应mouse事件,对移动端访问要响应touch事件:
// 定义不同端事件调用
var hasTouch = 'ontouchstart' in window,
startEvent = hasTouch ? 'touchstart' : 'mousedown',
moveEvent = hasTouch ? 'touchmove' : 'mousemove',
endEvent = hasTouch ? 'touchend' : 'mouseup',
cancelEvent = hasTouch ? 'touchcancel' : 'mouseup';
7. 效果图
(1)Ueditor工具栏上音频图标
1.png(2)上传音频
2.png(3)Ueditor内传入音频后效果
3.png(4)移动端文章内音频效果
4.png