整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

CKEditor系列(三)粘贴操作是怎么完成的

CKEditor系列(三)粘贴操作是怎么完成的

上一篇文章CKEditor系列(二)事件系统是怎么实现的中,我们了解了CKEditor中事件系统的运行流程,我们先简单回顾下:

  • 用户注册回调函数时可以指定优先级,值越小的优先级越高,默认是10
  • 系统会根据用户的传参组装成系统规范的回调函数,供后续执行
  • 执行回调函数时可以将取消事件和阻止事件,不让其它监听该事件的回调函数执行。

当插件希望对paste事件进行响应,一般有两种方式可供选择。

直接监听'paste'事件

默认情况下,插件clipboard插件是监听paste事件最多的。 我们可以看到里面多次出现类似这样的代码

// plugins/clipboard/plugin.js
editor.on( 'paste', function( evt ) {

})

我们可以看到里面有几个优先级priority 为1回调

处理粘贴图片的场景

将png、jpg、gif图片的内容base64信息赋值给evt.data.dataValue

editor.on( 'paste', function( evt ) {
    var dataObj=evt.data,
        data=dataObj.dataValue,
        dataTransfer=dataObj.dataTransfer;

    // If data empty check for image content inside data transfer. https://dev.ckeditor.com/ticket/16705
    // Allow both dragging and dropping and pasting images as base64 (#4681).
    if ( !data && isFileData( evt, dataTransfer ) ) {
        var file=dataTransfer.getFile( 0 );
        if ( CKEDITOR.tools.indexOf( supportedImageTypes, file.type ) !=-1 ) {
            var fileReader=new FileReader();

            // Convert image file to img tag with base64 image.
            fileReader.addEventListener( 'load', function() {
                evt.data.dataValue='<img src="' + fileReader.result + '" />';
                editor.fire( 'paste', evt.data );
            }, false );

            // Proceed with normal flow if reading file was aborted.
            fileReader.addEventListener( 'abort', function() {
                // (#4681)
                setCustomIEEventAttribute( evt );
                editor.fire( 'paste', evt.data );
            }, false );

            // Proceed with normal flow if reading file failed.
            fileReader.addEventListener( 'error', function() {
                // (#4681)
                setCustomIEEventAttribute( evt );
                editor.fire( 'paste', evt.data );
            }, false );

            fileReader.readAsDataURL( file );

            latestId=dataObj.dataTransfer.id;

            evt.stop();
        }
    }
}, null, null, 1 );

因为base64信息需要通过fileReader来处理:在图片的load回调里面才能拿到,所以我们需要先执行evt.stop(),避免其它回调被执行了,然后在图片load的回调里面重新触发一直paste事件 editor.fire( 'paste', evt.data );,对应的aborterror也要触发,避免因图片失败,导致其它回调都没机会执行了。

该回调会在下一轮paste回调执行中再次执行吗?不会,因为该回调首次执行时evt.data.dataValue为空,下次执行时evt.data.dataValue已经被上次执行给赋值了,不会重复执行fileReader相关处理了。

数据准备

editor.on( 'paste', function( evt ) {
    // Init `dataTransfer` if `paste` event was fired without it, so it will be always available.
    if ( !evt.data.dataTransfer ) {
        evt.data.dataTransfer=new CKEDITOR.plugins.clipboard.dataTransfer();
    }

    // If dataValue is already set (manually or by paste bin), so do not override it.
    if ( evt.data.dataValue ) {
        return;
    }

    var dataTransfer=evt.data.dataTransfer,
        // IE support only text data and throws exception if we try to get html data.
        // This html data object may also be empty if we drag content of the textarea.
        value=dataTransfer.getData( 'text/html' );

    if ( value ) {
        evt.data.dataValue=value;
        evt.data.type='html';
    } else {
        // Try to get text data otherwise.
        value=dataTransfer.getData( 'text/plain' );

        if ( value ) {
            evt.data.dataValue=editor.editable().transformPlainTextToHtml( value );
            evt.data.type='text';
        }
    }
}, null, null, 1 );

可以看到这个回调函数是主要是给evt.data增加dataTransferdataValue(如果已经被其它插件设置了就直接return出去)和type的,是做准备工作的,所以这个回调函数自然需要最先执行,优先级设置为1。

看看第二个回调函数

解决兼容性

editor.on( 'paste', function( evt ) {
    var data=evt.data.dataValue,
        blockElements=CKEDITOR.dtd.$block;

    // Filter webkit garbage.
    if ( data.indexOf( 'Apple-' ) > -1 ) {
        // Replace special webkit's   with simple space, because webkit
        // produces them even for normal spaces.
        data=data.replace( /<span class="Apple-converted-space"> <\/span>/gi, ' ' );

        // Strip <span> around white-spaces when not in forced 'html' content type.
        // This spans are created only when pasting plain text into Webkit,
        // but for safety reasons remove them always.
        if ( evt.data.type !='html' ) {
            data=data.replace( /<span class="Apple-tab-span"[^>]*>([^<]*)<\/span>/gi, function( all, spaces ) {
                // Replace tabs with 4 spaces like Fx does.
                return spaces.replace( /\t/g, '    ' );
            } );
        }

        // This br is produced only when copying & pasting HTML content.
        if ( data.indexOf( '<br class="Apple-interchange-newline">' ) > -1 ) {
            evt.data.startsWithEOL=1;
            evt.data.preSniffing='html'; // Mark as not text.
            data=data.replace( /<br class="Apple-interchange-newline">/, '' );
        }

        // Remove all other classes.
        data=data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
    }

    // Strip editable that was copied from inside. (https://dev.ckeditor.com/ticket/9534)
    if ( data.match( /^<[^<]+cke_(editable|contents)/i ) ) {
        var tmp,
            editable_wrapper,
            wrapper=new CKEDITOR.dom.element( 'div' );

        wrapper.setHtml( data );
        // Verify for sure and check for nested editor UI parts. (https://dev.ckeditor.com/ticket/9675)
        while ( wrapper.getChildCount()==1 &&
                ( tmp=wrapper.getFirst() ) &&
                tmp.type==CKEDITOR.NODE_ELEMENT &&    // Make sure first-child is element.
                ( tmp.hasClass( 'cke_editable' ) || tmp.hasClass( 'cke_contents' ) ) ) {
            wrapper=editable_wrapper=tmp;
        }

        // If editable wrapper was found strip it and bogus <br> (added on FF).
        if ( editable_wrapper )
            data=editable_wrapper.getHtml().replace( /<br>$/i, '' );
    }

    if ( CKEDITOR.env.ie ) {
        //   <p> -> <p> (br.cke-pasted-remove will be removed later)
        data=data.replace( /^ (?: |\r\n)?<(\w+)/g, function( match, elementName ) {
            if ( elementName.toLowerCase() in blockElements ) {
                evt.data.preSniffing='html'; // Mark as not a text.
                return '<' + elementName;
            }
            return match;
        } );
    } else if ( CKEDITOR.env.webkit ) {
        // </p><div><br></div> -> </p><br>
        // We don't mark br, because this situation can happen for htmlified text too.
        data=data.replace( /<\/(\w+)><div><br><\/div>$/, function( match, elementName ) {
            if ( elementName in blockElements ) {
                evt.data.endsWithEOL=1;
                return '</' + elementName + '>';
            }
            return match;
        } );
    } else if ( CKEDITOR.env.gecko ) {
        // Firefox adds bogus <br> when user pasted text followed by space(s).
        data=data.replace( /(\s)<br>$/, '$1' );
    }

    evt.data.dataValue=data;
}, null, null, 3 );

从上面的代码很容易看出,主要是针对不同的浏览器做一下兼容性相关的处理,具体细节我们不用太关心

针对不同粘贴源进行数据过滤

editor.on( 'paste', function( evt ) {
    var dataObj=evt.data,
        type=editor._.nextPasteType || dataObj.type,
        data=dataObj.dataValue,
        trueType,
        // Default is 'html'.
        defaultType=editor.config.clipboard_defaultContentType || 'html',
        transferType=dataObj.dataTransfer.getTransferType( editor ),
        isExternalPaste=transferType==CKEDITOR.DATA_TRANSFER_EXTERNAL,
        isActiveForcePAPT=editor.config.forcePasteAsPlainText===true;

    // If forced type is 'html' we don't need to know true data type.
    if ( type=='html' || dataObj.preSniffing=='html' ) {
        trueType='html';
    } else {
        trueType=recogniseContentType( data );
    }

    delete editor._.nextPasteType;

    // Unify text markup.
    if ( trueType=='htmlifiedtext' ) {
        data=htmlifiedTextHtmlification( editor.config, data );
    }

    // Strip presentational markup & unify text markup.
    // Forced plain text (dialog or forcePAPT).
    // Note: we do not check dontFilter option in this case, because forcePAPT was implemented
    // before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so
    // forcePAPT should have priority as it had before 4.5.
    if ( type=='text' && trueType=='html' ) {
        data=filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
    }
    // External paste and pasteFilter exists and filtering isn't disabled.
    // Or force filtering even for internal and cross-editor paste, when forcePAPT is active (#620).
    else if ( isExternalPaste && editor.pasteFilter && !dataObj.dontFilter || isActiveForcePAPT ) {
        data=filterContent( editor, data, editor.pasteFilter );
    }

    if ( dataObj.startsWithEOL ) {
        data='<br data-cke-eol="1">' + data;
    }
    if ( dataObj.endsWithEOL ) {
        data +='<br data-cke-eol="1">';
    }

    if ( type=='auto' ) {
        type=( trueType=='html' || defaultType=='html' ) ? 'html' : 'text';
    }

    dataObj.type=type;
    dataObj.dataValue=data;
    delete dataObj.preSniffing;
    delete dataObj.startsWithEOL;
    delete dataObj.endsWithEOL;
}, null, null, 6 );

这个主要是根据不同的typetrueType来对数据进行一些过滤操作

插入粘贴数据

粘贴的数据总得进入到编辑器吧,这就靠它了。

editor.on( 'paste', function( evt ) {
    var data=evt.data;
    if ( data.dataValue ) {
        editor.insertHtml( data.dataValue, data.type, data.range );

        // Defer 'afterPaste' so all other listeners for 'paste' will be fired first.
        // Fire afterPaste only if paste inserted some HTML.
        setTimeout( function() {
            editor.fire( 'afterPaste' );
        }, 0 );
    }
}, null, null, 1000 );

这个就比较简单了,但是也很重要,等paste事件系统的回调函数和用户添加的回调函数执行完毕后,这个回调函数作为最后执行的(如果前面的回调函数没有执行evt.stop()或者evt.cancel()),将evt.data.dataValue的值插入到编辑器中。

我们可以再多看一下/plugins/clipboard/plugin.js文件,里面有个对工具栏增加粘贴按钮,加上pasteCommand的操作

{

    exec: function( editor, data ) {
    data=typeof data !=='undefined' && data !==null ? data : {};

    var cmd=this,
        notification=typeof data.notification !=='undefined' ? data.notification : true,
        forcedType=data.type,
        keystroke=CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard,
            editor.getCommandKeystroke( this ) ),
        msg=typeof notification==='string' ? notification : editor.lang.clipboard.pasteNotification
            .replace( /%1/, '<kbd aria-label="' + keystroke.aria + '">' + keystroke.display + '</kbd>' ),
        pastedContent=typeof data==='string' ? data : data.dataValue;

    function callback( data, withBeforePaste ) {
        withBeforePaste=typeof withBeforePaste !=='undefined' ? withBeforePaste : true;

        if ( data ) {
            data.method='paste';

            if ( !data.dataTransfer ) {
                data.dataTransfer=clipboard.initPasteDataTransfer();
            }

            firePasteEvents( editor, data, withBeforePaste );
        } else if ( notification && !editor._.forcePasteDialog ) {
            editor.showNotification( msg, 'info', editor.config.clipboard_notificationDuration );
        }

        // Reset dialog mode (#595).
        editor._.forcePasteDialog=false;

        editor.fire( 'afterCommandExec', {
            name: 'paste',
            command: cmd,
            returnValue: !!data
        } );
    }

    // Force type for the next paste. Do not force if `config.forcePasteAsPlainText` set to true or 'allow-word' (#1013).
    if ( forcedType && editor.config.forcePasteAsPlainText !==true && editor.config.forcePasteAsPlainText !=='allow-word' ) {
        editor._.nextPasteType=forcedType;
    } else {
        delete editor._.nextPasteType;
    }

    if ( typeof pastedContent==='string' ) {
        callback( {
            dataValue: pastedContent
        } );
    } else {
        editor.getClipboardData( callback );
    }
}

上面的callback会执行firePasteEvents,然后触发paste事件。 如果pastedContent不是字符串的话,会先执行 editor.getClipboardData,该方法中有一个目前看到的优先级最好的paste回调

editor.on( 'paste', onPaste, null, null, 0 );

function onPaste( evt ) {
    evt.removeListener();
    evt.cancel();
    callback( evt.data );
}

onPaste方法里面会移除当前的回调函数,并取消掉后面未执行的paste回调,然后执行callback,也就是说它会触发一轮新的paste回调函数执行。

通过pasteTools插件来注册paste回调

{
    register: function(definition) {
        if (typeof definition.priority !=='number')
        {
            definition.priority=10;
        }

        this.handlers.push(definition);
    },
    addPasteListener: function( editor ) {
        editor.on( 'paste', function( evt ) {
            var handlers=getMatchingHandlers( this.handlers, evt ),
                filters,
                isLoaded;

            if ( handlers.length===0 ) {
                return;
            }

            filters=getFilters( handlers );

            isLoaded=loadFilters( filters, function() {
                return editor.fire( 'paste', evt.data );
            } );

            if ( !isLoaded ) {
                return evt.cancel();
            }

            handlePaste( handlers, evt );
        }, this, null, 3 );
    }
}
...
function getMatchingHandlers( handlers, evt ) {
    return CKEDITOR.tools.array.filter( handlers, function( handler ) {
        return handler.canHandle( evt );
    } ).sort( function( handler1, handler2 ) {
        if ( handler1.priority===handler2.priority ) {
            return 0;
        }

        return handler1.priority - handler2.priority;
    } );
}

function handlePaste( handlers, evt ) {
    var handler=handlers.shift();

    if ( !handler ) {
        return;
    }

    handler.handle( evt, function() {
        handlePaste( handlers, evt );
    } );
}

这个会把通过它注册的回调函数放进自己的handlers里面,而不跟上面那些直接监听paste放在一起,只有该组件自身才监听paste事件,优先级为3。这等于是将通过pasteTools.register注册的这一组回调全部按照了优先级为3的顺序来执行了,当然,这一组的回调直接同样按照优先级高低来执行,并且会根据其canHandle方法返回的值来过滤该回调是否执行,通过其handle来执行回调逻辑。

通过对源码的搜索,发现CKEditor大部分官方提供的对粘贴进行干预的插件都是通过pasteTools.register注册的。

总结

通过对pasteTools插件的学习,我们可以对自己想做系统级事件和用户级事件的分离的方式多一点启发,我们假设editor.on('paste')这种模式是系统级别的,只允许系统级插件有这种操作,而用户级插件不行,用户级插件只能通过系统级插件pasteTools暴露出来的register来注册,我们可以根据用户级插件的canHandle方法来让该插件只处理自己希望处理的那一部分。 类似的这种分离方法,也能更好地降低用户级插件对整个系统的影响


向以活好著称的我给大家来点精华,不搞假大空,直接上实操,不用看懂,收藏就完了,以后你肯定会用到的。

今天带来的是前端开发中经常碰到的数字问题,解决方式有些过于粗暴,未来还会不断美化更新。

也欢迎大家关注我的Github(点击原文链接),共同学习,共同提高。编者不才,如有问题,欢迎雅正,若有收获,请尽情用star羞辱我。

充分利用JavaScript自带原生方法解决问题

前端页面和数据处理虽然看起来简单,但是一个优秀的前端工程师对于细节的把控也是至关重要的,如何合理处理业务也体现前端工程师对性能优化的功力

获取数组最大值和最小值

  • 利用sort排序方法 针对数组进行排序,数组第一个和最后一个就是最大值和最小值
let arr=[1,2,3,4,5,6,7];
arr.sort(function (a, b) {
	 return a-b;
}); 
//sort传入一个排序函数,a-b为升序排序,b-a为降序排序
let min=arr[0]; // 1
let max=arr[arr.length - 1]; //7
  • 使用Math中的max/min方法 可以使用apply来实现。apply传入的是一个数组
let arr=[1,2,3,4,5,6,7];
let max=Math.max.apply(null, arr);
let min=Math.min.apply(null, arr);
console.log(max, min) // 7,1

浮点数取整

  • 丢弃小数部分,保留整数部分 parseInt(7/2)
  • 向上取整,有小数就整数部分加1 Math.ceil(7/2)
  • 四舍五入 Math.round(7/2)
  • 向下取整 Math.floor(7/2)
  • 注:都是JS内置对象

数组去重

  • ES6
arr2=[1,1,1,2,2,2,3,4]
arr1=Array.from(new Set(arr2)) //arr1=[1,2,3,4]
  • push( )去重
let arr3=[];  
for(var i=0; i < arr.length; i++) {  
    (function(i) {  
        if(arr3.indexOf(arr[i])==-1) { //不包含该值则返回-1  
            arr3.push(arr[i]);  
        }  
    }(i))  
}  
console.log(arr3); 
//如果当前数组的第i项在当前数组中第一次出现的位置不是i,那么表示第i项是重复的,忽略掉。否则存入结果数组  
let arr4=[arr[0]];  
for(var i=1; i < arr.length; i++) {  
    (function(i) {  
        if(arr.indexOf(arr[i])==i) {  
            arr4.push(arr[i]);  
        }  
    }(i))  
}  
console.log(arr4);  
  • sort( )去重
let arrSort=arr.sort();  
let arr5=[];  
for(let i=0; i< arrSort.length; i++) {  
    if(arrSort[i] !=arrSort[i+1]) {  
        arr5.push(arrSort[i]);  
    }  
}  
console.log(arr5);  
  • splice( )去重
for(let i=0, len=arr6.length; i < len; i++) {  
    for(let j=i + 1; j < len; j++) {  
        if(arr6[i]===arr6[j]) {  
            arr6.splice(i,1);  
            len--;  
            j--;  
        }  
    }  
}  
console.log(arr6); 

用好 filter, map, every, find和其它 ES6 新增的高阶遍历函数

  • 将数组中的false值去掉
const array=[3, 4, 5, 2, 3, undefined, null, 0, ""];
const compact=arr=> arr.filter(Boolean);
compact(array);
//[3, 4, 5, 2, 3]
  • 判断用户是否全部是成年人
const users=[
  { name: "Jim", age: 23 },
  { name: "Lily", age: 17 },
  { name: "Will", age: 35 }
];
users.every(user=> user.age >=18);
//false
  • 判断用户中是否有30岁以上的人
const users=[
  { name: "Jim", age: 23 },
  { name: "Lily", age: 17 },
  { name: "Will", age: 35 }
];
users.find(item=> item.age>30);
//{name: "Will", age: 35}

JS判断点击区域

  • 适用场景:显示页面弹窗时,用户点击页面其他地方弹窗自动关闭.
// HTML exitContent.vue
<div id="facePanel">
	<div v-show="showFace">这是个弹窗</div>
</div>

// JavaScript
handleClick(e) {
	let facePanel=document.getElementById('facePanel')
	if(!facePanel.contains(e.target) && this.$refs.editContent.showFace){
	  this.$refs.editContent.showFace=false
	}
},

合理利用正则表达式解决问题

<input> 标签输入限制

  • 文本框只能输入数字代码(小数点也不能输入)
<input onkeyup="this.value=this.value.replace(/\D/g,'')"  
onafterpaste="this.value=this.value.replace(/\D/g,'')"> 
  • 只能输入数字,能输小数点.
<input onkeyup="if(isNaN(value))execCommand('undo')" onafterpaste="if(isNaN(value))execCommand('undo')"> 
<input name=txt1 onchange="
	if(/\D/.test(this.value)){
	alert('只能输入数字');
	this.value='';}"> 
  • 数字和小数点方法二
<input type=text tvalue="" ovalue="" onkeypress="
	if(!this.value.match(/^[\+\-]?\d*?\.?\d*?$/))this.value=this.t_value;
		else this.tvalue=this.value;
	if(this.value.match(/^(?:[\+\-]?\d+(?:\.\d+)?)?$/))this.ovalue=this.value" 
		onkeyup="if(!this.value.match(/^[\+\-]?\d*?\.?\d*?$/))this.value=this.t_value;
		else this.tvalue=this.value;
	if(this.value.match(/^(?:[\+\-]?\d+(?:\.\d+)?)?$/))this.ovalue=this.value" 
		onblur="if(!this.value.match(/^(?:[\+\-]?\d+(?:\.\d+)?|\.\d*?)?$/))this.value=this.o_value;
		else{if(this.value.match(/^\.\d+$/))this.value=0+this.value;
	if(this.value.match(/^\.$/))this.value=0;this.ovalue=this.value}">
  • 只能输入字母和汉字
<input onkeyup="value=value.replace(/[\d]/g,'') "
		onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[\d]/g,''))" 
		maxlength=10 name="Numbers"> 
<input onkeyup="value=value.replace(/[^\a-zA-Z\u4E00-\u9FA5]/g,'')" 		onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\a-zA-Z\u4E00-\u9FA5]/g,''))" 
		maxlength=10 name="Numbers"> 
  • 只能输入英文字母和数字,不能输入中文
<input onkeyup="value=value.replace(/[^\w\.\/]/ig,'')"> 
  • 只能输入数字和英文
<font color="Red">chun</font> 
<input onKeyUp="value=value.replace(/[^\d|chun]/g,'')"> 
  • 小数点后只能有最多两位(数字,中文都可输入),不能输入字母和运算符号:
<input onKeyPress="
	if((event.keyCode<48 || event.keyCode>57) && event.keyCode!=46 || /\.\d\d$/.test(value))
	event.returnValue=false"> 
  • 小数点后只能有最多两位(数字,字母,中文都可输入),可以输入运算符号:
<input onkeyup="this.value=this.value.replace(/^(\-)*(\d+)\.(\d\d).*$/,'$1$2.$3')"> 
  • input的type设置为number后可以输入e
<input type='number' onkeypress='return( /[\d]/.test(String.fromCharCode(event.keyCode) ) )' />
// 匹配邮箱
let reg=/^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$

// (新)匹配手机号
let reg=/^1[0-9]{10}$/;

// (旧)匹配手机号
let reg=/^1[3|4|5|7|8][0-9]{9}$/;

// 匹配8-16位数字和字母密码的正则表达式
let reg=/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$/;

// 匹配国内电话号码 0510-4305211
let reg=/\d{3}-\d{8}|\d{4}-\d{7}/;

// 匹配身份证号码
let reg=/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;

// 匹配腾讯QQ号
let reg=/[1-9][0-9]{4,}/;

// 匹配ip地址
let reg=/\d+\.\d+\.\d+\.\d+/;

// 匹配中文
let reg=/^[\u4e00-\u9fa5]*$/;

JS判断字符串是否全是空格

let test="   \n   ";
if(test.match(/^\s+$/)){
    console.log("all space or \\n")
}
if(test.match(/^[ ]+$/)){
    console.log("all space")
}
if(test.match(/^[ ]*$/)){
    console.log("all space or empty")
}
if(test.match(/^\s*$/)){
    console.log("all space or \\n or empty")
}

常用正则匹配

  • /^\d+$/  //非负整数(正整数 + 0)
  • /^[0-9]*[1-9][0-9]*$/  //正整数
  • /^((-\d+)|(0+))$/  //非正整数(负整数 + 0)
  • /^-[0-9]*[1-9][0-9]*$/  //负整数
  • /^-?\d+$/    //整数
  • let reg=/^([^0][0-9]+|0)$/; if (reg.test(params.xzd2BorrowRate)) { params.xzd2BorrowRate +='.0'; } //整数
  • /^\d+(\.\d+)?$/  //非负浮点数(正浮点数 + 0)
  • /^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$/  //正浮点数
  • /^((-\d+(\.\d+)?)|(0+(\.0+)?))$/  //非正浮点数(负浮点数 + 0)
  • /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/  //负浮点数
  • /^(-?\d+)(\.\d+)?$/  //浮点数
  • replace(/[^0-9]/ig, "") //只保留数字

JS扩大checkbox的点击区域

  • 试用场景:在表格中点击 td 也能选中 ,或者在父节点也能点击触发事件
  • 做法:为 td 元素添加点击事件,在点击事件里面触发checkbox的点击事件:
<td onclick="clickTd($(this))">
	<input type="checkbox" name="task"  onclick="oncheckAll(event)">
</td>

function clickTd($t){
    $t.children("input").click();
}
  • bug浮现:点击checkbox的时候,因为页面元素所在区域重叠,点击事件向上传递,会首先触发checkbox的点击事件,然后触发 td 的点击事件,然后 td 的点击事件会被再次触发,就相当于checkbox点击了两次,所以需要 checkbox 的点击事件执行完之后,让该点击事件停止传递。
  • 解决方案:checkbox的点击事件里面加入拦截即可:
function oncheckAll(event){
    event.stopPropagation();
}

计算时间戳间隔时间

let date1=new Date();  //开始时间  
let date2=new Date();    //结束时间  
let date3=date2.getTime()-date1.getTime()  //时间差的毫秒数  
------------------------------------------------------------
//计算出相差天数  
var days=Math.floor(date3/(24*3600*1000))   
//计算出小时数   
var leave1=date3%(24*3600*1000)    //计算天数后剩余的毫秒数  
var hours=Math.floor(leave1/(3600*1000))  
//计算相差分钟数  
var leave2=leave1%(3600*1000)        //计算小时数后剩余的毫秒数  
var minutes=Math.floor(leave2/(60*1000))  
//计算相差秒数  
var leave3=leave2%(60*1000)      //计算分钟数后剩余的毫秒数  
var seconds=Math.round(leave3/1000)  
alert(" 相差 "+days+"天 "+hours+"小时 "+minutes+" 分钟"+seconds+" 秒")

JS提取字符串中连续的数字

let str="013d1we22ewfa33rr4rwq0dsf00dsf9fas999";

let getNum=function (Str,isFilter) {
   //用来判断是否把连续的0去掉
    isFilter=isFilter || false;
    if (typeof Str==="string") {
        // var arr=Str.match(/(0\d{2,})|([1-9]\d+)/g);
        //"/[1-9]\d{1,}/g",表示匹配1到9,一位数以上的数字(不包括一位数).
        //"/\d{2,}/g",  表示匹配至少二个数字至多无穷位数字
        var arr=Str.match( isFilter ? /[1-9]\d{1,}/g : /\d{2,}/g);
        console.log(arr);
        return arr.map(function (item) {
            //转换为整数,
            //但是提取出来的数字,如果是连续的多个0会被改为一个0,如000---->0,
            //或者0开头的连续非零数字,比如015,会被改为15,这是一个坑
            // return parseInt(item);
            //字符串,连续的多个0也会存在,不会被去掉
            return item;
        });
    } else {
        return [];
    }
}
console.log(getNum(str));//默认不加1,即不会把提取出来的0去掉

JS获取项目根目录

function getRootPath(){
    //获取当前网址,如: http://localhost:8083/uimcardprj/share/meun.jsp
    var curWwwPath=window.document.location.href;
    //获取主机地址之后的目录,如: uimcardprj/share/meun.jsp
    var pathName=window.document.location.pathname;
    var pos=curWwwPath.indexOf(pathName);
    //获取主机地址,如: http://localhost:8083
    var localhostPaht=curWwwPath.substring(0,pos);
    //获取带"/"的项目名,如:/uimcardprj
    var projectName=pathName.substring(0,pathName.substr(1).indexOf('/')+1);
    return(localhostPaht+projectName);
}

JS判断小数点后几位

  • num.toString().split(".")[1].length

JS截取数字小数点后N位

  • 利用math.round
var original=28.453
// 保留小数点后两位
var result=Math.round(original*100)/100;  //returns 28.45
// 保留小数点后一位
var result=Math.round(original*10)/10;  //returns 28.5
  • 利用toFixed
var original=28.453
// 保留小数点后两位
var result=original.toFixed(2); //returns 28.45
// 保留小数点后一位
var result=original.toFixed(1); //returns 28.5

JS控制Input输入数字和小数点后两位

function clearNoNum(value) {
    value=value.replace(/[^\d.]/g, "");  //清除“数字”和“.”以外的字符   
    value=value.replace(/\.{2,}/g, "."); //只保留第一个. 清除多余的   
    value=value.replace(".", "$#$").replace(/\./g, "").replace("$#$", ".");
    value=value.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3'); //只能输入两个小数   
    if (value.indexOf(".") < 0 && value !="") { //以上已经过滤,此处控制的是如果没有小数点,首位不能为类似于 01、02的金额  
        value=parseFloat(value);
    }
}

JS校验手机号和座机号的方法

function $isPhoneAvailable(str) {
    let isMob=/^[1][3,4,5,6,7,8,9][0-9]{9}$/;
    let isPhone=/^([0-9]{3,4}-)?[0-9]{7,8}$/;
    if (isPhone.test(str) || isMob.test(str)) {
        return true;
    }else {
        return false;
    }
}

JS indexOf() 不区分大小写实现

  • 使用 str.toLowerCase() 或者 str.toUpperCase() 将对比字符串全都转换成 大 (小) 写

JS replace() 如何针对key进行替换

  • str.replace(new RegExp('要替换的keyword','g'), '替换后的内容')

明节是我们追思故人的时节,

疫情给今年清明带来了一丝不一样的意味。

今天我们来聊聊清明节和国外缅怀故人的方式。


Tomb Sweeping Day


清明节的英文暗示了在这个节日我们本该做的事:

  • Tomb Sweeping Day: 清明节
  • Tomb: /tu?m/ n. 坟墓
  • Sweep: /swi?p/ v. 清扫

如果对方对中国文化比较了解,也可以直接说:

  • Qingming Festival


Solar term


其实清明是二十四节气之一:

  • Solar term: 节气

清明节的日期是这么算的:

  • It is the 15th day after the Spring Equinox, either 4 or 5 April in a given year. 它是春分后的第15天,是4月4号或者5号。


To observe Qingming Festival


别的节日可以庆祝(celebrate),但是清明节却有其特殊性,只能用 observe:

  • To observe Qingming Festival
  • To observe Tomb Sweeping Day

外国人虽然没有在清明扫墓的习惯,也会在特定的日期去墓碑前缅怀故人:

  • To visit the cemetery: 去墓园

虽然 graveyard 也有墓园的意思,但通常都和教堂挂钩, cemetery 则指更普遍意义上的墓地。


Filial piety


不管去不去扫墓,重要的是要追思故人,通常是缅怀长辈,是传达孝道的一种方式:

  • Filial piety: /?f?li?l ?pa??ti/ n. 孝道

如果觉得这种说法太正式,可以说:

  • Respect/ honor your parents: 尊敬父母
  • Be big on family: 重视家庭
  • Family guys: 把家庭看得很重的人(一般指男性)


Pass away


虽然英文中 die 也可以形容去世,但是我们也可以选择婉转一点说这件悲痛的事:

  • Pass away: 去世
  • I lost someone. 我失去了某人。
  • Someone is in a better place. 某人去了更好的地方。
  • Rest in peace (RIP): 安息

国外现在很多人不再称葬礼为 funeral,而是用下面这个短语来换个角度看待死亡:

  • A celebration of life: 生命的仪式


Green dumplings


回到清明节,必不可少的中国传统小吃就是:

  • Green dumplings/ Green rice balls: 青团

很多来自中国的点心都被叫做 dumplings,对于青团你可以这么解释:

  • It's made of glutinous rice and barley grass with red bean paste. 青团是豆沙馅儿的糯米艾草团子。

当然每个地方的青团也可能会采用不同的原料。


你和故人曾有过哪些美好的回忆?

你想对故人说些什么?

欢迎大家留言。