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">&nbsp;&nbsp;</var><input class="text" type="text" id="width"/>px </span>
                        <span><var id="lang_input_height">&nbsp;&nbsp;</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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,972评论 4 60
  • 第一天的培训结束了,已经很久没试过这种学习模式了,感觉又回到学校那会。虽然入行已有10年,但是掌握的知识点都很零碎...
    胡文涛_bc54阅读 251评论 0 4
  • 君生我未生,我生君已老,君恨我生迟,我恨君生早。君生我未生,我生君已老,恨不生同时,日日与君好。不是我不爱你,也不...
    路小赖阅读 572评论 0 0
  • 2018年水性铝银浆发展如何 2017年各地区轰轰烈烈的环保法制定、修改,以及环保税的增加,使得油性涂料企业人心惶...
    山东银箭铝银浆阅读 93评论 0 0
  • 奇怪的逻辑运算 一般语言的逻辑运算结果都是 true或者false但是js就很个性,我就是其中一个操作数的值。对于...
    雨墨心阅读 188评论 1 2