JavaScript รับข้อมูลคลิปบอร์ดจากการวางเหตุการณ์ (เบราว์เซอร์ข้าม)


299

เว็บแอปพลิเคชันสามารถตรวจจับเหตุการณ์การวางและดึงข้อมูลที่จะวางได้อย่างไร

ฉันต้องการลบเนื้อหา HTML ก่อนที่ข้อความจะถูกวางลงในโปรแกรมแก้ไขข้อความที่มีรูปแบบ

การล้างข้อความหลังจากวางหลังจากนั้นใช้งานได้ แต่ปัญหาคือการฟอร์แมตก่อนหน้านี้ทั้งหมดจะหายไป ตัวอย่างเช่นฉันสามารถเขียนประโยคในเครื่องมือแก้ไขและทำให้เป็นตัวหนา แต่เมื่อฉันวางข้อความใหม่การจัดรูปแบบทั้งหมดจะหายไป ฉันต้องการทำความสะอาดเฉพาะข้อความที่วางแล้วปล่อยให้การฟอร์แมตก่อนหน้านี้ไม่มีการแตะต้อง

ในอุดมคติแล้วโซลูชันควรทำงานกับเบราว์เซอร์ที่ทันสมัยทั้งหมด (เช่น MSIE, Gecko, Chrome และ Safari)

โปรดทราบว่า MSIE มีclipboardData.getData()แต่ฉันไม่พบฟังก์ชันการทำงานที่คล้ายกันสำหรับเบราว์เซอร์อื่น


คำตอบทั้งหมดเหล่านี้อธิบายวิธีรับเนื้อหาข้อความ การรับเนื้อหาภาพหรือเนื้อหาไฟล์ต้องใช้งานมากกว่านี้ บางทีเราสามารถเปลี่ยนชื่อเป็น "JavaScript รับ sanitized data clipboard data ... "
1.21 gigawatts

1
เช่นนิโก้กล่าวว่า: event.clipboardData.getData('Text')ทำงานสำหรับฉัน
Andre Elrico

document.addEventListener('paste'...ใช้งานได้สำหรับฉัน แต่ก่อให้เกิดข้อขัดแย้งหากผู้ใช้ต้องการวางที่อื่นบนหน้าเว็บ จากนั้นฉันลองmyCanvasElement.addEventListener('paste'...แต่ก็ไม่ได้ผล ในที่สุดฉันก็พบว่าmyCanvasElement.parentElement.addEventListener('paste'...ทำงานได้
Ryan

คำตอบ:


149

สถานการณ์เปลี่ยนไปนับตั้งแต่เขียนคำตอบนี้: ขณะนี้ Firefox ได้เพิ่มการสนับสนุนในเวอร์ชัน 22 แล้วเบราว์เซอร์หลัก ๆ ทั้งหมดจึงสนับสนุนการเข้าถึงข้อมูลคลิปบอร์ดในเหตุการณ์การวาง ดูคำตอบของ Nico Burnsสำหรับตัวอย่าง

ในอดีตนี้เป็นไปไม่ได้ในทางข้ามเบราว์เซอร์ อุดมคติคือการสามารถรับเนื้อหาที่วางผ่านpasteเหตุการณ์ซึ่งเป็นไปได้ในเบราว์เซอร์ล่าสุดแต่ไม่ได้อยู่ในเบราว์เซอร์รุ่นเก่า (โดยเฉพาะ Firefox <22)

เมื่อคุณต้องการสนับสนุนเบราว์เซอร์รุ่นเก่าคุณสามารถทำอะไรได้บ้างมีส่วนเกี่ยวข้องและแฮ็คบิตที่จะทำงานใน Firefox 2+, IE 5.5+ และเบราว์เซอร์ WebKit เช่น Safari หรือ Chrome เวอร์ชันล่าสุดของทั้ง TinyMCE และ CKEditor ใช้เทคนิคนี้:

  1. ตรวจจับเหตุการณ์ ctrl-v / shift-ins โดยใช้ตัวจัดการเหตุการณ์ keypress
  2. ในตัวจัดการนั้นให้บันทึกการเลือกผู้ใช้ปัจจุบันเพิ่มองค์ประกอบ textarea นอกหน้าจอ (พูดที่ด้านซ้าย -1000px) ลงในเอกสารdesignModeปิดและเรียกfocus()ใช้ textarea ดังนั้นย้ายคาเร็ตและเปลี่ยนเส้นทางวางได้อย่างมีประสิทธิภาพ
  3. ตั้งค่าตัวจับเวลาสั้น ๆ (พูด 1 มิลลิวินาที) ในตัวจัดการเหตุการณ์เพื่อเรียกใช้ฟังก์ชันอื่นที่เก็บค่า textarea เอา textarea ออกจากเอกสารเปิดdesignModeกลับมาคืนค่าการเลือกผู้ใช้และวางข้อความไว้

โปรดทราบว่านี่จะใช้งานได้เฉพาะกับเหตุการณ์การวางคีย์บอร์ดและไม่วางจากบริบทหรือเมนูแก้ไข เมื่อถึงเวลาที่เหตุการณ์การวางไฟช้าเกินไปที่จะเปลี่ยนเส้นทางเครื่องหมายรูปหมวกลงใน textarea (อย่างน้อยในเบราว์เซอร์บางตัว)

ในเหตุการณ์ที่ไม่น่าเป็นไปได้ที่คุณต้องการรองรับ Firefox 2 โปรดทราบว่าคุณจะต้องวาง textarea ในเอกสารหลักแทนที่จะเป็นเอกสาร WYSIWYG editor iframe ของเบราว์เซอร์นั้น


1
ว้าวขอบคุณมาก! ดูเหมือนจะเป็นแฮ็คที่มีความซับซ้อนมาก ๆ ;-) คุณช่วยอธิบายการออกแบบโหมดและสิ่งที่เลือกเพิ่มเติมได้อีกเล็กน้อยโดยเฉพาะในขั้นตอนที่ 3? ขอบคุณมาก!
อเล็กซ์

5
ฉันมีความรู้สึกที่น่ากลัวที่คุณถาม อย่างที่ฉันบอกว่ามันค่อนข้างเกี่ยวข้อง: ฉันขอแนะนำให้ดูที่แหล่งที่มาของ TinyMCE หรือ CKEditor เนื่องจากฉันไม่มีเวลาที่จะสรุปประเด็นทั้งหมดที่เกี่ยวข้อง สั้น ๆ แต่designModeเป็นที่พักแบบบูลและทำให้สามารถแก้ไขได้ทั้งหน้าเมื่อdocument trueโปรแกรมแก้ไขแบบ WYSIWYG มักจะใช้ iframe ด้วยdesignModeon เป็นบานหน้าต่างที่สามารถแก้ไขได้ การบันทึกและการกู้คืนการเลือกของผู้ใช้นั้นทำได้หนึ่งวิธีใน IE และอีกวิธีในเบราว์เซอร์อื่นเช่นเดียวกับการวางเนื้อหาลงในโปรแกรมแก้ไข คุณต้องได้รับTextRangeใน IE และRangeเบราว์เซอร์อื่น ๆ
Tim Down

6
@Samuel: คุณสามารถตรวจพบได้โดยใช้pasteเหตุการณ์ แต่โดยทั่วไปแล้วมันจะสายเกินไปแล้วในการเปลี่ยนเส้นทางการวางลงในองค์ประกอบอื่นดังนั้นการแฮ็คนี้จะไม่ทำงาน ทางเลือกในการแก้ไขส่วนใหญ่คือการแสดงกล่องโต้ตอบสำหรับผู้ใช้ที่จะวาง
Tim Down

6
ข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้: Firefox จะไม่อนุญาตให้คุณย้ายโฟกัสไปที่องค์ประกอบอื่นในpasteเหตุการณ์อย่างไรก็ตามมันจะช่วยให้คุณสามารถล้างเนื้อหาขององค์ประกอบ (และบันทึกลงในตัวแปรเพื่อให้คุณสามารถกู้คืนได้ในภายหลัง) ถ้าภาชนะนี้เป็นdiv(มันอาจจะทำงานสำหรับiframeเกินไป) แล้วคุณสามารถแล้ววงจรผ่านเนื้อหาวางโดยใช้วิธีการ Dom innerHTMLปกติหรือได้รับมันเป็นสตริงใช้ จากนั้นคุณสามารถคืนค่าเนื้อหาก่อนหน้าของdivและแทรกเนื้อหาใดก็ได้ที่คุณต้องการ โอ้และคุณต้องใช้แฮ็คตัวจับเวลาเหมือนข้างต้น ฉันประหลาดใจ TinyMCE ไม่ทำเช่นนี้ ...
นิโก้เบิร์นส์

8
@ ResistDesign: ฉันไม่เห็นด้วย - เป็นวิธีที่ไม่ซับซ้อนและซับซ้อนในการชดเชย API ที่ไม่เหมาะสม มันจะเป็นการดีกว่าถ้าคุณสามารถรับเนื้อหาที่วางโดยตรงจากเหตุการณ์การวางซึ่งก็คือไปได้ในทาง จำกัด ในเบราว์เซอร์บาง
Tim Down

318

โซลูชัน # 1 (ข้อความล้วนเท่านั้นและต้องใช้ Firefox 22+)

ใช้งานได้กับ IE6 +, FF 22+, Chrome, Safari, Edge (ทดสอบเฉพาะใน IE9 + แต่ควรใช้ได้กับรุ่นที่ต่ำกว่า)

หากคุณต้องการการสนับสนุนสำหรับการวาง HTML หรือ Firefox <= 22 ให้ดูโซลูชัน # 2

HTML

<div id='editableDiv' contenteditable='true'>Paste</div>

JavaScript

function handlePaste (e) {
    var clipboardData, pastedData;

    // Stop data actually being pasted into div
    e.stopPropagation();
    e.preventDefault();

    // Get pasted data via clipboard API
    clipboardData = e.clipboardData || window.clipboardData;
    pastedData = clipboardData.getData('Text');

    // Do whatever with pasteddata
    alert(pastedData);
}

document.getElementById('editableDiv').addEventListener('paste', handlePaste);

JSFiddle: https://jsfiddle.net/swL8ftLs/12/

โปรดทราบว่าวิธีนี้ใช้พารามิเตอร์ 'ข้อความ' สำหรับgetDataฟังก์ชั่นซึ่งไม่ได้มาตรฐาน อย่างไรก็ตามมันสามารถใช้ได้กับทุกเบราว์เซอร์ในขณะที่เขียน


โซลูชัน # 2 (HTML และใช้ได้กับ Firefox <= 22)

ทดสอบใน IE6 +, FF 3.5+, Chrome, Safari, Edge

HTML

<div id='div' contenteditable='true'>Paste</div>

JavaScript

var editableDiv = document.getElementById('editableDiv');

function handlepaste (e) {
    var types, pastedData, savedContent;

    // Browsers that support the 'text/html' type in the Clipboard API (Chrome, Firefox 22+)
    if (e && e.clipboardData && e.clipboardData.types && e.clipboardData.getData) {

        // Check for 'text/html' in types list. See abligh's answer below for deatils on
        // why the DOMStringList bit is needed. We cannot fall back to 'text/plain' as
        // Safari/Edge don't advertise HTML data even if it is available
        types = e.clipboardData.types;
        if (((types instanceof DOMStringList) && types.contains("text/html")) || (types.indexOf && types.indexOf('text/html') !== -1)) {

            // Extract data and pass it to callback
            pastedData = e.clipboardData.getData('text/html');
            processPaste(editableDiv, pastedData);

            // Stop the data from actually being pasted
            e.stopPropagation();
            e.preventDefault();
            return false;
        }
    }

    // Everything else: Move existing element contents to a DocumentFragment for safekeeping
    savedContent = document.createDocumentFragment();
    while(editableDiv.childNodes.length > 0) {
        savedContent.appendChild(editableDiv.childNodes[0]);
    }

    // Then wait for browser to paste content into it and cleanup
    waitForPastedData(editableDiv, savedContent);
    return true;
}

function waitForPastedData (elem, savedContent) {

    // If data has been processes by browser, process it
    if (elem.childNodes && elem.childNodes.length > 0) {

        // Retrieve pasted content via innerHTML
        // (Alternatively loop through elem.childNodes or elem.getElementsByTagName here)
        var pastedData = elem.innerHTML;

        // Restore saved content
        elem.innerHTML = "";
        elem.appendChild(savedContent);

        // Call callback
        processPaste(elem, pastedData);
    }

    // Else wait 20ms and try again
    else {
        setTimeout(function () {
            waitForPastedData(elem, savedContent)
        }, 20);
    }
}

function processPaste (elem, pastedData) {
    // Do whatever with gathered data;
    alert(pastedData);
    elem.focus();
}

// Modern browsers. Note: 3rd argument is required for Firefox <= 6
if (editableDiv.addEventListener) {
    editableDiv.addEventListener('paste', handlepaste, false);
}
// IE <= 8
else {
    editableDiv.attachEvent('onpaste', handlepaste);
}

JSFiddle: https://jsfiddle.net/nicoburns/wrqmuabo/23/

คำอธิบาย

onpasteเหตุการณ์ของการdivมีhandlePasteฟังก์ชั่นที่ติดอยู่กับมันและผ่านอาร์กิวเมนต์เดียวที่: eventวัตถุสำหรับเหตุการณ์วาง สิ่งที่น่าสนใจเป็นพิเศษสำหรับเราคือclipboardDataทรัพย์สินของเหตุการณ์นี้ซึ่งทำให้สามารถเข้าถึงคลิปบอร์ดในเบราว์เซอร์ที่ไม่ใช่เช่น ใน IE ที่เทียบเท่าคือwindow.clipboardDataแม้ว่าจะมี API ที่แตกต่างกันเล็กน้อย

ดูส่วนทรัพยากรด้านล่าง


handlepasteฟังก์ชั่น:

ฟังก์ชั่นนี้มีสองสาขา

การตรวจสอบครั้งแรกสำหรับการมีอยู่ของevent.clipboardDataและการตรวจสอบว่ามันเป็นtypesคุณสมบัติที่มี 'text / html' ( typesอาจเป็นอย่างใดอย่างหนึ่งDOMStringListซึ่งมีการตรวจสอบโดยใช้containsวิธีการหรือสตริงที่มีการตรวจสอบโดยใช้indexOfวิธีการ) หากเงื่อนไขเหล่านี้เป็นจริงเราจะดำเนินการตามวิธีที่ # 1 ยกเว้นด้วย 'text / html' แทนที่จะเป็น 'text / plain' ปัจจุบันสามารถใช้งานได้ใน Chrome และ Firefox 22+

หากวิธีนี้ไม่ได้รับการสนับสนุน (เบราว์เซอร์อื่น ๆ ทั้งหมด) เราก็ทำได้

  1. บันทึกเนื้อหาขององค์ประกอบลงใน DocumentFragment
  2. ล้างองค์ประกอบ
  3. เรียกใช้waitForPastedDataฟังก์ชัน

waitforpastedataฟังก์ชั่น:

ฟังก์ชันนี้สำรวจความคิดเห็นแรกสำหรับข้อมูลที่วาง (หนึ่งครั้งต่อ 20ms) ซึ่งจำเป็นเพราะไม่ปรากฏขึ้นทันที เมื่อข้อมูลปรากฏขึ้น:

  1. บันทึก InnerHTML ของ div ที่แก้ไขได้ (ซึ่งตอนนี้เป็นข้อมูลที่วาง) ไปยังตัวแปร
  2. คืนค่าเนื้อหาที่บันทึกไว้ใน DocumentFragment
  3. เรียกใช้ฟังก์ชัน 'processPaste' พร้อมกับดึงข้อมูล

processpasteฟังก์ชั่น:

ทำสิ่งที่กำหนดเองโดยใช้ข้อมูลที่วาง ในกรณีนี้เราเพียงแค่แจ้งเตือนข้อมูลคุณสามารถทำสิ่งที่คุณต้องการ คุณอาจต้องการเรียกใช้ข้อมูลที่วางผ่านกระบวนการฆ่าเชื้อข้อมูลบางประเภท


การบันทึกและกู้คืนตำแหน่งเคอร์เซอร์

ในการซิงก์จริงคุณอาจต้องการบันทึกการเลือกก่อนหน้าและเรียกคืนหลังจากนั้น ( ตั้งค่าตำแหน่งเคอร์เซอร์บน contentEditable <div> ) จากนั้นคุณสามารถแทรกข้อมูลที่วางในตำแหน่งที่เคอร์เซอร์อยู่เมื่อผู้ใช้เริ่มต้นการดำเนินการวาง

แหล่งข้อมูล:

ขอบคุณ Tim Down เพื่อแนะนำการใช้ DocumentFragment และ abligh สำหรับการตรวจจับข้อผิดพลาดใน Firefox เนื่องจากการใช้ DOMStringList แทนสตริงสำหรับ clipboardData.types


4
น่าสนใจ ฉันคิดว่าฉันเคยลองสิ่งนี้ในอดีตและมันไม่ได้ทำงานในเบราว์เซอร์บางตัว แต่ฉันแน่ใจว่าคุณพูดถูก แน่นอนว่าฉันต้องการย้ายเนื้อหาที่มีอยู่ไปสู่DocumentFragmentแทนที่จะใช้innerHTMLด้วยเหตุผลหลายประการ: อันดับแรกคุณเก็บตัวจัดการเหตุการณ์ที่มีอยู่ ประการที่สองการบันทึกและกู้คืนinnerHTMLไม่รับประกันว่าจะสร้างสำเนาที่เหมือนกันของ DOM ก่อนหน้า; ประการที่สามคุณสามารถบันทึกสิ่งที่เลือกRangeแทนการไม่ต้องกังวลกับการเพิ่มองค์ประกอบเครื่องหมายหรือการคำนวณออฟเซ็ตข้อความ (ซึ่งเป็นสิ่งที่คุณต้องทำถ้าคุณใช้innerHTML)
Tim Down

3
แน่นอนว่าไม่มีเนื้อหา (FONC?) ซึ่งจะแย่ลงหากการประมวลผลของเนื้อหาที่วางใช้เวลา แต่ทำไมการสกัดถึงDocumentFragmentความเจ็บปวดใน IE? มันเหมือนกับในเบราว์เซอร์อื่น ๆ เว้นแต่ว่าคุณจะใช้ Range และextractContents()ทำมันซึ่งไม่ได้กระชับกว่าทางเลือกในทุกกรณี ผมเคยนำมาใช้ตัวอย่างของเทคนิคของคุณโดยใช้ภูเขามากเพื่อให้สิ่งที่ดีและสม่ำเสมอทั้งเบราว์เซอร์: jsfiddle.net/bQeWC/4
Tim Down

1
@ มาร์ติน: การสาธิต jsFiddle ฉันโพสต์ในความคิดเห็นอาจช่วย
Tim Down

1
ดูเหมือนว่าจะไม่ทำงานอีกต่อไปบน Firefox 28 (อย่างน้อย) สำหรับ Windows มันไม่เคยออกไปจากwaitforpastedataฟังก์ชั่น
Oliboy50

1
FYI: Edge รองรับการอ่านข้อมูลด้วย MIME-Type text/htmlโดยใช้ W3C Clipboard API ในอดีตความพยายามดังกล่าวจะทำให้เกิดข้อยกเว้น ดังนั้นจึงไม่ต้องการวิธีแก้ปัญหานี้ / แฮ็คสำหรับ Edge อีกต่อไป
Jenny O'Reilly

130

รุ่นง่าย:

document.querySelector('[contenteditable]').addEventListener('paste', (e) => {
    e.preventDefault();
    const text = (e.originalEvent || e).clipboardData.getData('text/plain');
    window.document.execCommand('insertText', false, text);
});

การใช้ clipboardData

การสาธิต: http://jsbin.com/nozifexasu/edit?js,output

ทดสอบ Edge, Firefox, Chrome, Safari, Opera

Document.execCommand () ล้าสมัยแล้ว


หมายเหตุ:อย่าลืมตรวจสอบอินพุต / เอาต์พุตที่ฝั่งเซิร์ฟเวอร์ด้วย (เช่นแท็กแถบ PHP )


4
วิธีนี้ใช้งานได้ดี แต่ไม่มี IE เวอร์ชันใดที่อนุญาตให้เข้าถึงคลิปบอร์ดข้อมูลจากเหตุการณ์ :( วิธีแก้ปัญหาที่ยอดเยี่ยม แต่ควรจะสูงกว่านี้!
Eric Wood

1
ดูเหมือนว่าคุณจะได้รับข้อมูลคลิปบอร์ดใน IE ในลักษณะที่แตกต่างกันดังนั้นหากคุณตรวจพบ IE คุณสามารถใช้ข้อมูลนั้นแทนการสำรองข้อมูลพรอมต์: msdn.microsoft.com/en-us/library/ie/ms535220(v = vs.85) .aspx
แอนดรูว์

4
พบคำตอบข้ามเบราว์เซอร์ที่ดีที่สุด เพียงเพิ่มรหัสสำหรับ IE และมันสมบูรณ์แบบ
อาร์ตูโร

6
สิ่งนี้ใช้ได้ใน IE (อา, หวาน, ตรงกันข้ามกับ IE)window.clipboardData.getData('Text');
Benjineer

9
e.preventDefault(); if (e.clipboardData) { content = (e.originalEvent || e).clipboardData.getData('text/plain'); document.execCommand('insertText', false, content); } else if (window.clipboardData) { content = window.clipboardData.getData('Text'); document.selection.createRange().pasteHTML(content); }
Yukulelix

26

การสาธิตสด

ทดสอบกับ Chrome / FF / IE11

มีการรบกวน Chrome / IE ซึ่งเบราว์เซอร์เหล่านี้เพิ่ม<div>องค์ประกอบสำหรับแต่ละบรรทัดใหม่ มีโพสต์เกี่ยวกับเรื่องนี้ที่นี่และสามารถแก้ไขได้โดยการตั้งค่าองค์ประกอบcontenteditableให้เป็นdisplay:inline-block

เลือก HTML ที่ไฮไลต์แล้ววางที่นี่:

function onPaste(e){
  var content;
  e.preventDefault();

  if( e.clipboardData ){
    content = e.clipboardData.getData('text/plain');
    document.execCommand('insertText', false, content);
    return false;
  }
  else if( window.clipboardData ){
    content = window.clipboardData.getData('Text');
    if (window.getSelection)
      window.getSelection().getRangeAt(0).insertNode( document.createTextNode(content) );
  }
}


/////// EVENT BINDING /////////
document.querySelector('[contenteditable]').addEventListener('paste', onPaste);
[contenteditable]{ 
  /* chroem bug: https://stackoverflow.com/a/24689420/104380 */
  display:inline-block;
  width: calc(100% - 40px);
  min-height:120px; 
  margin:10px;
  padding:10px;
  border:1px dashed green;
}

/* 
 mark HTML inside the "contenteditable"  
 (Shouldn't be any OFC!)'
*/
[contenteditable] *{
  background-color:red;
}
<div contenteditable></div>


1
ฉันต้องการวางเป็นคุณลักษณะข้อความธรรมดา ทดสอบกับ IE9 และ IE10 และใช้งานได้ดี จำเป็นต้องพูดถึงที่ทำงานบนเบราว์เซอร์ที่สำคัญเช่นกัน ... ขอบคุณ
Savas Vedova

2
รหัสของคุณมีข้อบกพร่อง: ถ้า (e.originalEvent.clipboardData) สามารถทำให้เกิด NPE ได้เนื่องจากคุณไม่รู้ว่า e.originalEvent มีอยู่ ณ จุดนั้น
Sebastian

15

ฉันได้เขียนหลักฐานแนวคิดเกี่ยวกับข้อเสนอของ Tim Downs ที่นี่ด้วย textarea นอกจอ และนี่คือรหัส:

<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> 
<script language="JavaScript">
 $(document).ready(function()
{

var ctrlDown = false;
var ctrlKey = 17, vKey = 86, cKey = 67;

$(document).keydown(function(e)
{
    if (e.keyCode == ctrlKey) ctrlDown = true;
}).keyup(function(e)
{
    if (e.keyCode == ctrlKey) ctrlDown = false;
});

$(".capture-paste").keydown(function(e)
{
    if (ctrlDown && (e.keyCode == vKey || e.keyCode == cKey)){
        $("#area").css("display","block");
        $("#area").focus();         
    }
});

$(".capture-paste").keyup(function(e)
{
    if (ctrlDown && (e.keyCode == vKey || e.keyCode == cKey)){                      
        $("#area").blur();
        //do your sanitation check or whatever stuff here
        $("#paste-output").text($("#area").val());
        $("#area").val("");
        $("#area").css("display","none");
    }
});

});
</script>

</head>
<body class="capture-paste">

<div id="paste-output"></div>


    <div>
    <textarea id="area" style="display: none; position: absolute; left: -99em;"></textarea>
    </div>

</body>
</html>

เพียงคัดลอกและวางรหัสทั้งหมดลงในไฟล์ html ไฟล์เดียวและลองวาง (โดยใช้ ctrl-v) ข้อความจากคลิปบอร์ดได้ทุกที่ในเอกสาร

ฉันทดสอบใน IE9 และ Firefox, Chrome และ Opera เวอร์ชันใหม่แล้ว ทำงานได้ค่อนข้างดี นอกจากนี้ยังเป็นเรื่องดีที่ผู้ใช้สามารถใช้ชุดค่าผสมใด ๆ ก็ตามที่เขาชอบที่จะใช้งานฟังก์ชั่นนี้ยิ่งขึ้น แน่นอนว่าอย่าลืมรวมแหล่ง jQuery

อย่าลังเลที่จะใช้รหัสนี้และหากคุณได้รับการปรับปรุงหรือแก้ไขปัญหากรุณาโพสต์กลับมา โปรดทราบว่าฉันไม่ใช่นักพัฒนา Javascript ดังนั้นฉันอาจพลาดบางสิ่งบางอย่าง (=> ทำ testign ของคุณเอง)


แม็คไม่วางด้วย ctrl-v พวกเขาใช้ cmd-v ดังนั้นให้ตั้งค่า ctrlKey = 91 แทนที่จะเป็น 17
Jeremy T

2
หรืออาจจะไม่ใช่ 91: stackoverflow.com/questions/3834175/… ไม่ว่าฉันจะค่อนข้างแน่ใจว่า jQuery จัดการทุกอย่างให้คุณเพียงแค่ตรวจสอบ e.ctrlKey หรือ e.metaKey ฉันคิดว่า
Jeremy T

3
e.ctrlKey หรือ e.metaKey เป็นส่วนหนึ่งของ DOM DOM ไม่ใช่ jQuery: developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
rvighne

2
ฉันไม่คิดว่ามันจะใช้งานได้กับการคลิกขวาและการวาง ผู้คนจำนวนมากใช้วิธีการนั้น
Eric Wood

10

ขึ้นอยู่กับl2aelba anwser สิ่งนี้ถูกทดสอบบน FF, Safari, Chrome, IE (8,9,10 และ 11)

    $("#editText").on("paste", function (e) {
        e.preventDefault();

        var text;
        var clp = (e.originalEvent || e).clipboardData;
        if (clp === undefined || clp === null) {
            text = window.clipboardData.getData("text") || "";
            if (text !== "") {
                if (window.getSelection) {
                    var newNode = document.createElement("span");
                    newNode.innerHTML = text;
                    window.getSelection().getRangeAt(0).insertNode(newNode);
                } else {
                    document.selection.createRange().pasteHTML(text);
                }
            }
        } else {
            text = clp.getData('text/plain') || "";
            if (text !== "") {
                document.execCommand('insertText', false, text);
            }
        }
    });

มีวิธีการรักษาบรรทัดใหม่เมื่อวางกับ IE หรือไม่?
Staysee

10

อันนี้ไม่ได้ใช้ setTimeout ใด ๆ ()

ฉันได้ใช้นี้บทความดีดีเพื่อให้บรรลุการสนับสนุนเบราว์เซอร์ข้าม

$(document).on("focus", "input[type=text],textarea", function (e) {
    var t = e.target;
    if (!$(t).data("EventListenerSet")) {
        //get length of field before paste
        var keyup = function () {
            $(this).data("lastLength", $(this).val().length);
        };
        $(t).data("lastLength", $(t).val().length);
        //catch paste event
        var paste = function () {
            $(this).data("paste", 1);//Opera 11.11+
        };
        //process modified data, if paste occured
        var func = function () {
            if ($(this).data("paste")) {
                alert(this.value.substr($(this).data("lastLength")));
                $(this).data("paste", 0);
                this.value = this.value.substr(0, $(this).data("lastLength"));
                $(t).data("lastLength", $(t).val().length);
            }
        };
        if (window.addEventListener) {
            t.addEventListener('keyup', keyup, false);
            t.addEventListener('paste', paste, false);
            t.addEventListener('input', func, false);
        }
        else {//IE
            t.attachEvent('onkeyup', function () {
                keyup.call(t);
            });
            t.attachEvent('onpaste', function () {
                paste.call(t);
            });
            t.attachEvent('onpropertychange', function () {
                func.call(t);
            });
        }
        $(t).data("EventListenerSet", 1);
    }
}); 

รหัสนี้ถูกขยายด้วยหมายเลขอ้างอิงการเลือกก่อนวาง: สาธิต


+1 ฉันชอบสิ่งนี้ดีกว่า Nico Burns แม้ว่าฉันคิดว่าแต่ละคนมีที่ของตัวเอง
n0nag0n

5

สำหรับการทำความสะอาดข้อความที่วางและแทนที่ข้อความที่เลือกในปัจจุบันด้วยข้อความที่วางเป็นเรื่องเล็กน้อย

<div id='div' contenteditable='true' onpaste='handlepaste(this, event)'>Paste</div>

JS:

function handlepaste(el, e) {
  document.execCommand('insertText', false, e.clipboardData.getData('text/plain'));
  e.preventDefault();
}

คุณสามารถให้หน้าตัวอย่างที่ใช้งานได้หรือไม่ ฉันได้ลองแล้ว แต่ใช้งานไม่ได้
vsync

5

สิ่งนี้ควรใช้กับเบราว์เซอร์ทั้งหมดที่รองรับกิจกรรม onpaste และผู้สังเกตการณ์การกลายพันธุ์

วิธีการแก้ปัญหานี้ก้าวไปไกลกว่าการรับข้อความเท่านั้นจริง ๆ แล้วมันช่วยให้คุณสามารถแก้ไขเนื้อหาที่วางก่อนที่จะถูกวางลงในองค์ประกอบ

ใช้งานได้โดยใช้เนื้อหาที่น่าสนใจเหตุการณ์ onpaste (สนับสนุนโดยเบราว์เซอร์หลักทั้งหมด) ผู้สังเกตการณ์การกลายพันธุ์ (รองรับโดย Chrome, Firefox และ IE11 +)

ขั้นตอนที่ 1

สร้างองค์ประกอบ HTML ที่มี contenteditable

<div contenteditable="true" id="target_paste_element"></div>

ขั้นตอนที่ 2

ในโค้ด Javascript ของคุณให้เพิ่มกิจกรรมต่อไปนี้

document.getElementById("target_paste_element").addEventListener("paste", pasteEventVerifierEditor.bind(window, pasteCallBack), false);

เราจำเป็นต้องผูก pasteCallBack เนื่องจากผู้สังเกตการณ์การกลายพันธุ์จะถูกเรียกแบบอะซิงโครนัส

ขั้นตอนที่ 3

เพิ่มฟังก์ชั่นต่อไปนี้ในรหัสของคุณ

function pasteEventVerifierEditor(callback, e)
{
   //is fired on a paste event. 
    //pastes content into another contenteditable div, mutation observer observes this, content get pasted, dom tree is copied and can be referenced through call back.
    //create temp div
    //save the caret position.
    savedCaret = saveSelection(document.getElementById("target_paste_element"));

    var tempDiv = document.createElement("div");
    tempDiv.id = "id_tempDiv_paste_editor";
    //tempDiv.style.display = "none";
    document.body.appendChild(tempDiv);
    tempDiv.contentEditable = "true";

    tempDiv.focus();

    //we have to wait for the change to occur.
    //attach a mutation observer
    if (window['MutationObserver'])
    {
        //this is new functionality
        //observer is present in firefox/chrome and IE11
        // select the target node
        // create an observer instance
        tempDiv.observer = new MutationObserver(pasteMutationObserver.bind(window, callback));
        // configuration of the observer:
        var config = { attributes: false, childList: true, characterData: true, subtree: true };

        // pass in the target node, as well as the observer options
        tempDiv.observer.observe(tempDiv, config);

    }   

}



function pasteMutationObserver(callback)
{

    document.getElementById("id_tempDiv_paste_editor").observer.disconnect();
    delete document.getElementById("id_tempDiv_paste_editor").observer;

    if (callback)
    {
        //return the copied dom tree to the supplied callback.
        //copy to avoid closures.
        callback.apply(document.getElementById("id_tempDiv_paste_editor").cloneNode(true));
    }
    document.body.removeChild(document.getElementById("id_tempDiv_paste_editor"));

}

function pasteCallBack()
{
    //paste the content into the element.
    restoreSelection(document.getElementById("target_paste_element"), savedCaret);
    delete savedCaret;

    pasteHtmlAtCaret(this.innerHTML, false, true);
}   


saveSelection = function(containerEl) {
if (containerEl == document.activeElement)
{
    var range = window.getSelection().getRangeAt(0);
    var preSelectionRange = range.cloneRange();
    preSelectionRange.selectNodeContents(containerEl);
    preSelectionRange.setEnd(range.startContainer, range.startOffset);
    var start = preSelectionRange.toString().length;

    return {
        start: start,
        end: start + range.toString().length
    };
}
};

restoreSelection = function(containerEl, savedSel) {
    containerEl.focus();
    var charIndex = 0, range = document.createRange();
    range.setStart(containerEl, 0);
    range.collapse(true);
    var nodeStack = [containerEl], node, foundStart = false, stop = false;

    while (!stop && (node = nodeStack.pop())) {
        if (node.nodeType == 3) {
            var nextCharIndex = charIndex + node.length;
            if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
                range.setStart(node, savedSel.start - charIndex);
                foundStart = true;
            }
            if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
                range.setEnd(node, savedSel.end - charIndex);
                stop = true;
            }
            charIndex = nextCharIndex;
        } else {
            var i = node.childNodes.length;
            while (i--) {
                nodeStack.push(node.childNodes[i]);
            }
        }
    }

    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
}

function pasteHtmlAtCaret(html, returnInNode, selectPastedContent) {
//function written by Tim Down

var sel, range;
if (window.getSelection) {
    // IE9 and non-IE
    sel = window.getSelection();
    if (sel.getRangeAt && sel.rangeCount) {
        range = sel.getRangeAt(0);
        range.deleteContents();

        // Range.createContextualFragment() would be useful here but is
        // only relatively recently standardized and is not supported in
        // some browsers (IE9, for one)
        var el = document.createElement("div");
        el.innerHTML = html;
        var frag = document.createDocumentFragment(), node, lastNode;
        while ( (node = el.firstChild) ) {
            lastNode = frag.appendChild(node);
        }
        var firstNode = frag.firstChild;
        range.insertNode(frag);

        // Preserve the selection
        if (lastNode) {
            range = range.cloneRange();
            if (returnInNode)
            {
                range.setStart(lastNode, 0); //this part is edited, set caret inside pasted node.
            }
            else
            {
                range.setStartAfter(lastNode); 
            }
            if (selectPastedContent) {
                range.setStartBefore(firstNode);
            } else {
                range.collapse(true);
            }
            sel.removeAllRanges();
            sel.addRange(range);
        }
    }
} else if ( (sel = document.selection) && sel.type != "Control") {
    // IE < 9
    var originalRange = sel.createRange();
    originalRange.collapse(true);
    sel.createRange().pasteHTML(html);
    if (selectPastedContent) {
        range = sel.createRange();
        range.setEndPoint("StartToStart", originalRange);
        range.select();
    }
}
}

รหัสทำอะไร:

  1. บางคนยิงเหตุการณ์วางโดยใช้ ctrl-v, contextmenu หรือวิธีการอื่น
  2. ในเหตุการณ์การวางองค์ประกอบใหม่ที่มี contenteditable ถูกสร้างขึ้น (องค์ประกอบที่มี contenteditable มีสิทธิ์ยกระดับ)
  3. บันทึกตำแหน่งเครื่องหมายรูปหมวกขององค์ประกอบเป้าหมาย
  4. โฟกัสถูกตั้งค่าเป็นองค์ประกอบใหม่
  5. เนื้อหาถูกวางลงในองค์ประกอบใหม่และแสดงผลใน DOM
  6. ผู้สังเกตการกลายพันธุ์จับสิ่งนี้ได้ (มันบันทึกการเปลี่ยนแปลงทั้งหมดในแผนผังต้นไม้และเนื้อหา) จากนั้นยิงเหตุการณ์การกลายพันธุ์
  7. dom ของเนื้อหาที่วางได้รับการโคลนเป็นตัวแปรและกลับไปที่การโทรกลับ องค์ประกอบชั่วคราวถูกทำลาย
  8. การเรียกกลับได้รับการโคลน เครื่องหมายรูปหมวกถูกคืนค่า คุณสามารถแก้ไขสิ่งนี้ก่อนที่จะผนวกเข้ากับเป้าหมายของคุณ ธาตุ. ในตัวอย่างนี้ฉันใช้ฟังก์ชัน Tim Downs สำหรับบันทึก / เรียกคืนเครื่องหมายรูปหมวกและวาง HTML ลงในองค์ประกอบ

ตัวอย่าง


ขอบคุณมากที่Tim Down ดูคำตอบนี้:

รับเนื้อหาที่วางบนเอกสารในเหตุการณ์การวาง


4

โซลูชันที่ใช้งานได้สำหรับฉันคือการเพิ่มฟังเหตุการณ์เพื่อวางเหตุการณ์หากคุณวางลงในการป้อนข้อความ เนื่องจากเหตุการณ์การวางเกิดขึ้นก่อนที่ข้อความในการเปลี่ยนแปลงการป้อนข้อมูลภายในตัวจัดการการวางของฉันฉันสร้างฟังก์ชั่นรอการตัดบัญชีที่ฉันตรวจสอบการเปลี่ยนแปลงในกล่องอินพุตของฉันที่เกิดขึ้นในการวาง:

onPaste: function() {
    var oThis = this;
    setTimeout(function() { // Defer until onPaste() is done
        console.log('paste', oThis.input.value);
        // Manipulate pasted input
    }, 1);
}

2
สยองขวัญโชคไม่ดีที่เป็นส่วนหนึ่งของรายละเอียดงานของเรา;) แต่ฉันเห็นด้วยว่านี่คือการแฮ็กและแฮ็กที่ควรใช้เมื่อตัวเลือกอื่น ๆ หมดลงเท่านั้น
Lex

4

นี่นานเกินไปสำหรับความคิดเห็นเกี่ยวกับคำตอบของ Nico ซึ่งฉันไม่คิดว่าจะใช้งานกับ Firefox ได้อีกต่อไป (ตามความคิดเห็น) และไม่ได้ทำงานกับฉันใน Safari เหมือนเดิม

ก่อนอื่นคุณสามารถอ่านได้โดยตรงจากคลิปบอร์ด แทนที่จะเป็นรหัสเช่น:

if (/text\/plain/.test(e.clipboardData.types)) {
    // shouldn't this be writing to elem.value for text/plain anyway?
    elem.innerHTML = e.clipboardData.getData('text/plain');
}

ใช้:

types = e.clipboardData.types;
if (((types instanceof DOMStringList) && types.contains("text/plain")) ||
    (/text\/plain/.test(types))) {
    // shouldn't this be writing to elem.value for text/plain anyway?
    elem.innerHTML = e.clipboardData.getData('text/plain');
}

เพราะ Firefox มีtypesข้อมูลซึ่งเป็นที่ไม่ได้ดำเนินการDOMStringListtest

ถัดไป Firefox จะไม่อนุญาตให้วางเว้นแต่โฟกัสอยู่ในcontenteditable=trueสนาม

ท้ายที่สุด Firefox จะไม่อนุญาตให้วางได้อย่างน่าเชื่อถือเว้นแต่จะอยู่ในโฟกัสtextarea(หรืออาจจะป้อนข้อมูล) ซึ่งไม่เพียงcontenteditable=trueแต่ยัง:

  • ไม่ display:none
  • ไม่ visibility:hidden
  • ไม่เป็นศูนย์ขนาด

ฉันพยายามซ่อนเขตข้อมูลข้อความเพื่อให้ฉันสามารถวางทำงานผ่านโปรแกรมจำลอง JS VNC (เช่นไปที่ไคลเอนต์ระยะไกลและไม่มีจริงtextareaๆ ที่จะวางลงใน) ฉันพบว่าพยายามซ่อนเขตข้อมูลข้อความด้านบนทำให้เกิดอาการในบางครั้ง แต่โดยทั่วไปแล้วจะล้มเหลวในการวางครั้งที่สอง (หรือเมื่อเขตข้อมูลถูกล้างเพื่อป้องกันการวางข้อมูลเดียวกันสองครั้ง) เนื่องจากเขตข้อมูลสูญเสียการโฟกัส focus()แม้จะมี วิธีการแก้ปัญหาที่ผมขึ้นมาด้วยก็จะนำมันที่z-order: -1000ทำให้มันdisplay:noneทำให้มันเป็น 1px โดย 1px และการตั้งค่าทุกสีเพื่อความโปร่งใส yuck

ใน Safari คุณใช้ส่วนที่สองของด้านบนกล่าวคือคุณต้องมีส่วนtextareaที่ไม่display:noneเกี่ยวข้อง


บางทีนักพัฒนาที่ทำงานกับเอ็นจิ้นการแสดงผลของเบราว์เซอร์ควรมีหน้าหรือที่ว่างบนไซต์เอกสารที่พวกเขาสามารถใช้ในการเขียนบันทึกเกี่ยวกับคุณสมบัติที่พวกเขาทำงาน ตัวอย่างเช่นหากพวกเขาทำงานในฟังก์ชั่นการวางพวกเขาจะเพิ่ม "การวางจะไม่ทำงานหากการแสดงผลไม่มีการแสดงผลถูกซ่อนหรือขนาดเป็นศูนย์"
1.21 gigawatts

3

สิ่งแรกที่ควรคำนึงถึงก็คือ pastehandler ของ lib ปิดของ google http://closure-library.googlecode.com/svn/trunk/closure/goog/demos/pastehandler.html


เหตุการณ์นี้ดูเหมือนจะตรวจจับเหตุการณ์การวางอย่างปลอดภัย แต่ดูเหมือนว่าจะไม่สามารถจับ / ส่งคืนเนื้อหาที่วางได้หรือไม่
อเล็กซ์

@Alex: ถูกต้องและใช้ได้กับ textareas เท่านั้นไม่ใช้ text editor
Tim Down


2

สิ่งนี้ใช้ได้กับฉัน:

function onPasteMe(currentData, maxLen) {
    // validate max length of pasted text
    var totalCharacterCount = window.clipboardData.getData('Text').length;
}

<input type="text" onPaste="return onPasteMe(this, 50);" />

2
function myFunct( e ){
    e.preventDefault();

    var pastedText = undefined;
    if( window.clipboardData && window.clipboardData.getData ){
    pastedText = window.clipboardData.getData('Text');
} 
else if( e.clipboardData && e.clipboardData.getData ){
    pastedText = e.clipboardData.getData('text/plain');
}

//work with text

}
document.onpaste = myFunct;

1

คุณสามารถทำได้ด้วยวิธีนี้:

ใช้ปลั๊กอิน jQuery นี้สำหรับก่อน & โพสต์เหตุการณ์แปะ:

$.fn.pasteEvents = function( delay ) {
    if (delay == undefined) delay = 20;
    return $(this).each(function() {
        var $el = $(this);
        $el.on("paste", function() {
            $el.trigger("prepaste");
            setTimeout(function() { $el.trigger("postpaste"); }, delay);
        });
    });
};

ตอนนี้คุณสามารถใช้ปลั๊กอินนี้ ::

$('#txt').on("prepaste", function() { 

    $(this).find("*").each(function(){

        var tmp=new Date.getTime();
        $(this).data("uid",tmp);
    });


}).pasteEvents();

$('#txt').on("postpaste", function() { 


  $(this).find("*").each(function(){

     if(!$(this).data("uid")){
        $(this).removeClass();
          $(this).removeAttr("style id");
      }
    });
}).pasteEvents();

ชี้แจง

ก่อนอื่นให้ตั้ง uid สำหรับองค์ประกอบที่มีอยู่ทั้งหมดเป็น data data

จากนั้นเปรียบเทียบโหนดทั้งหมด POST PASTE ดังนั้นโดยการเปรียบเทียบคุณสามารถระบุสิ่งที่แทรกใหม่เพราะพวกเขาจะมี uid จากนั้นเพียงแค่ลบสไตล์ / คลาส / แอตทริบิวต์ id จากองค์ประกอบที่สร้างขึ้นใหม่เพื่อให้คุณสามารถจัดรูปแบบเก่าของคุณ



1

เพียงแค่ปล่อยให้เบราว์เซอร์วางตามปกติในเนื้อหาที่สามารถแก้ไขได้ div แล้วหลังจากที่วางสลับองค์ประกอบการขยายใด ๆ ที่ใช้สำหรับรูปแบบข้อความที่กำหนดเองด้วยข้อความตัวเอง ดูเหมือนว่าจะทำงานได้ดีใน internet explorer และเบราว์เซอร์อื่นที่ฉันลอง ...

$('[contenteditable]').on('paste', function (e) {
    setTimeout(function () {
        $(e.target).children('span').each(function () {
            $(this).replaceWith($(this).text());
        });
    }, 0);
});

การแก้ปัญหานี้อนุมานว่าคุณกำลังทำงานjQueryและว่าคุณไม่ต้องการจัดรูปแบบข้อความใน div ใด ๆ ที่สามารถแก้ไขเนื้อหาได้divs

ด้านบวกคือมันง่ายมาก


ทำไมต้องspanติดแท็ก ฉันจะจินตนาการว่าคำถามเกี่ยวกับแท็กทั้งหมด
Alexis Wilke

1

โซลูชันนี้จะแทนที่แท็ก html ง่ายและข้ามเบราว์เซอร์ ตรวจสอบ jsfiddle นี้: http://jsfiddle.net/tomwan/cbp1u2cx/1/ , รหัสหลัก:

var $plainText = $("#plainText");
var $linkOnly = $("#linkOnly");
var $html = $("#html");

$plainText.on('paste', function (e) {
    window.setTimeout(function () {
        $plainText.html(removeAllTags(replaceStyleAttr($plainText.html())));
    }, 0);
});

$linkOnly.on('paste', function (e) {
    window.setTimeout(function () {
        $linkOnly.html(removeTagsExcludeA(replaceStyleAttr($linkOnly.html())));
    }, 0);
});

function replaceStyleAttr (str) {
    return str.replace(/(<[\w\W]*?)(style)([\w\W]*?>)/g, function (a, b, c, d) {
        return b + 'style_replace' + d;
    });
}

function removeTagsExcludeA (str) {
    return str.replace(/<\/?((?!a)(\w+))\s*[\w\W]*?>/g, '');
}

function removeAllTags (str) {
    return str.replace(/<\/?(\w+)\s*[\w\W]*?>/g, '');
}

แจ้งให้ทราบล่วงหน้า: คุณควรทำงานเกี่ยวกับตัวกรอง xss ที่ด้านหลังเพราะวิธีนี้ไม่สามารถกรองสตริงเช่น '<< >>'


XSS filering บนเซิร์ฟเวอร์ไม่เกี่ยวข้องกับว่าตัวกรอง JavaScript ของคุณทำงานได้ดีหรือไม่ แฮกเกอร์เลี่ยงการกรอง JS ของคุณ 100%
Alexis Wilke

อย่าใช้ Regex ในการแยกวิเคราะห์ / แปลง HTML!
SubliemeSiem

0

นี่เป็นรหัสที่มีอยู่แล้วที่โพสต์ไว้ด้านบน แต่ฉันได้อัปเดตสำหรับ IE แล้วข้อผิดพลาดคือเมื่อข้อความที่มีอยู่ถูกเลือกและวางจะไม่ลบเนื้อหาที่เลือก สิ่งนี้ได้รับการแก้ไขโดยรหัสด้านล่าง

selRange.deleteContents(); 

ดูรหัสที่สมบูรณ์ด้านล่าง

$('[contenteditable]').on('paste', function (e) {
    e.preventDefault();

    if (window.clipboardData) {
        content = window.clipboardData.getData('Text');        
        if (window.getSelection) {
            var selObj = window.getSelection();
            var selRange = selObj.getRangeAt(0);
            selRange.deleteContents();                
            selRange.insertNode(document.createTextNode(content));
        }
    } else if (e.originalEvent.clipboardData) {
        content = (e.originalEvent || e).clipboardData.getData('text/plain');
        document.execCommand('insertText', false, content);
    }        
});
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.