上一篇文章CKEditor系列(二)事件系統是怎么實現的中,我們了解了CKEditor中事件系統的運行流程,我們先簡單回顧下:
當插件希望對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 );,對應的abort和error也要觸發,避免因圖片失敗,導致其它回調都沒機會執行了。
該回調會在下一輪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增加dataTransfer、dataValue(如果已經被其它插件設置了就直接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 );
這個主要是根據不同的type、trueType來對數據進行一些過濾操作
粘貼的數據總得進入到編輯器吧,這就靠它了。
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回調函數執行。
{
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羞辱我。
前端頁面和數據處理雖然看起來簡單,但是一個優秀的前端工程師對于細節的把控也是至關重要的,如何合理處理業務也體現前端工程師對性能優化的功力
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
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
arr2=[1,1,1,2,2,2,3,4]
arr1=Array.from(new Set(arr2)) //arr1=[1,2,3,4]
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);
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);
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);
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
const users=[
{ name: "Jim", age: 23 },
{ name: "Lily", age: 17 },
{ name: "Will", age: 35 }
];
users.find(item=> item.age>30);
//{name: "Will", age: 35}
// 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 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' 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]*$/;
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")
}
<td onclick="clickTd($(this))">
<input type="checkbox" name="task" onclick="oncheckAll(event)">
</td>
function clickTd($t){
$t.children("input").click();
}
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+" 秒")
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去掉
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);
}
var original=28.453
// 保留小數點后兩位
var result=Math.round(original*100)/100; //returns 28.45
// 保留小數點后一位
var result=Math.round(original*10)/10; //returns 28.5
var original=28.453
// 保留小數點后兩位
var result=original.toFixed(2); //returns 28.45
// 保留小數點后一位
var result=original.toFixed(1); //returns 28.5
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);
}
}
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;
}
}
明節是我們追思故人的時節,
疫情給今年清明帶來了一絲不一樣的意味。
今天我們來聊聊清明節和國外緬懷故人的方式。
清明節的英文暗示了在這個節日我們本該做的事:
如果對方對中國文化比較了解,也可以直接說:
其實清明是二十四節氣之一:
清明節的日期是這么算的:
別的節日可以慶祝(celebrate),但是清明節卻有其特殊性,只能用 observe:
外國人雖然沒有在清明掃墓的習慣,也會在特定的日期去墓碑前緬懷故人:
雖然 graveyard 也有墓園的意思,但通常都和教堂掛鉤, cemetery 則指更普遍意義上的墓地。
不管去不去掃墓,重要的是要追思故人,通常是緬懷長輩,是傳達孝道的一種方式:
如果覺得這種說法太正式,可以說:
雖然英文中 die 也可以形容去世,但是我們也可以選擇婉轉一點說這件悲痛的事:
國外現在很多人不再稱葬禮為 funeral,而是用下面這個短語來換個角度看待死亡:
回到清明節,必不可少的中國傳統小吃就是:
很多來自中國的點心都被叫做 dumplings,對于青團你可以這么解釋:
當然每個地方的青團也可能會采用不同的原料。
你和故人曾有過哪些美好的回憶?
你想對故人說些什么?
歡迎大家留言。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。