رفع انسداد دسترسی به کلیپ بورد

دسترسی ایمن تر و رفع انسداد به کلیپ بورد برای متن و تصاویر

روش سنتی دسترسی به کلیپ بورد سیستم از طریق document.execCommand() برای تعاملات کلیپ بورد بود. اگرچه این روش برش و چسباندن به طور گسترده پشتیبانی می‌شد، هزینه‌ای داشت: دسترسی به کلیپ‌بورد همزمان بود و فقط می‌توانست در DOM بخواند و بنویسد.

این برای متن های کوچک خوب است، اما موارد زیادی وجود دارد که مسدود کردن صفحه برای انتقال کلیپ بورد تجربه ضعیفی است. ممکن است قبل از چسباندن ایمن محتوا به پاکسازی یا رمزگشایی تصویر زمان بر نیاز باشد. ممکن است مرورگر نیاز داشته باشد که منابع پیوندی را از یک سند چسبانده شده بارگیری یا درون خطی کند. این باعث مسدود شدن صفحه در هنگام انتظار روی دیسک یا شبکه می شود. تصور کنید مجوزهایی را به ترکیب اضافه کنید، در حالی که درخواست دسترسی به کلیپ بورد، مرورگر باید صفحه را مسدود کند. در همان زمان، مجوزهایی که در اطراف document.execCommand() برای تعامل با کلیپ بورد قرار داده شده اند، به طور ضعیف تعریف شده اند و بین مرورگرها متفاوت هستند.

Async Clipboard API این مشکلات را برطرف می‌کند و یک مدل مجوزهای کاملاً تعریف شده ارائه می‌کند که صفحه را مسدود نمی‌کند. Async Clipboard API محدود به مدیریت متن و تصاویر در اکثر مرورگرها است، اما پشتیبانی متفاوت است. حتماً نمای کلی سازگاری مرورگر را برای هر یک از بخش‌های زیر به دقت مطالعه کنید.

کپی: نوشتن داده ها در کلیپ بورد

writeText()

برای کپی کردن متن در کلیپ بورد، با writeText() تماس بگیرید. از آنجایی که این API ناهمزمان است، تابع writeText() یک Promise برمی‌گرداند که بسته به اینکه متن ارسال شده با موفقیت کپی شده باشد، حل می‌شود یا رد می‌شود:

async function copyPageUrl() {
  try {
    await navigator.clipboard.writeText(location.href);
    console.log('Page URL copied to clipboard');
  } catch (err) {
    console.error('Failed to copy: ', err);
  }
}

پشتیبانی مرورگر

  • کروم: 66.
  • لبه: 79.
  • فایرفاکس: 63.
  • سافاری: 13.1.

منبع

نوشتن ()

در واقع، writeText() فقط یک روش راحت برای متد write() عمومی است که به شما امکان می دهد تصاویر را در کلیپ بورد کپی کنید. مانند writeText() ناهمزمان است و یک Promise برمی گرداند.

برای نوشتن یک تصویر در کلیپ بورد، به تصویر به صورت blob نیاز دارید. یکی از راه‌های انجام این کار، درخواست تصویر از سرور با استفاده از fetch() و سپس فراخوانی blob() در پاسخ است.

درخواست تصویر از سرور ممکن است به دلایل مختلف مطلوب یا ممکن نباشد. خوشبختانه، شما همچنین می توانید تصویر را روی یک بوم بکشید و متد toBlob() را فراخوانی کنید.

سپس، آرایه ای از اشیاء ClipboardItem را به عنوان پارامتر به متد write() ارسال کنید. در حال حاضر شما فقط می توانید یک تصویر را در یک زمان ارسال کنید، اما امیدواریم در آینده پشتیبانی از چندین تصویر را اضافه کنیم. ClipboardItem یک شی با نوع MIME تصویر به عنوان کلید و حباب به عنوان مقدار می گیرد. برای اشیاء blob که از fetch() یا canvas.toBlob() به دست می آیند، ویژگی blob.type به طور خودکار نوع MIME صحیح را برای یک تصویر در بر می گیرد.

try {
  const imgURL = '/images/generic/file.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  await navigator.clipboard.write([
    new ClipboardItem({
      // The key is determined dynamically based on the blob's type.
      [blob.type]: blob
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

همچنین، می توانید یک وعده برای شی ClipboardItem بنویسید. برای این الگو باید از قبل نوع MIME داده را بدانید.

try {
  const imgURL = '/images/generic/file.png';
  await navigator.clipboard.write([
    new ClipboardItem({
      // Set the key beforehand and write a promise as the value.
      'image/png': fetch(imgURL).then(response => response.blob()),
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

پشتیبانی مرورگر

  • کروم: 66.
  • لبه: 79.
  • فایرفاکس: 127.
  • سافاری: 13.1.

منبع

رویداد کپی

در موردی که کاربر یک کپی از کلیپ بورد را شروع می کند و preventDefault() فراخوانی نمی کند، رویداد copy شامل یک ویژگی clipboardData با موارد از قبل در قالب مناسب است. اگر می‌خواهید منطق خود را پیاده‌سازی کنید، باید preventDefault() را فراخوانی کنید تا از رفتار پیش‌فرض به نفع پیاده‌سازی خودتان جلوگیری کنید. در این صورت، clipboardData خالی خواهد بود. صفحه ای را با متن و تصویر در نظر بگیرید، و زمانی که کاربر همه را انتخاب می کند و یک کپی از کلیپ بورد را آغاز می کند، راه حل سفارشی شما باید متن را کنار بگذارد و فقط تصویر را کپی کند. همانطور که در نمونه کد زیر نشان داده شده است، می توانید این کار را انجام دهید. آنچه در این مثال پوشش داده نشده است، نحوه بازگشت به APIهای قبلی است، زمانی که API Clipboard پشتیبانی نمی شود.

<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
  // Prevent the default behavior.
  e.preventDefault();
  try {
    // Prepare an array for the clipboard items.
    let clipboardItems = [];
    // Assume `blob` is the blob representation of `kitten.webp`.
    clipboardItems.push(
      new ClipboardItem({
        [blob.type]: blob,
      })
    );
    await navigator.clipboard.write(clipboardItems);
    console.log("Image copied, text ignored.");
  } catch (err) {
    console.error(err.name, err.message);
  }
});

برای رویداد copy :

پشتیبانی مرورگر

  • کروم: 1.
  • لبه: 12.
  • فایرفاکس: 22.
  • سافاری: 3.

منبع

برای ClipboardItem :

پشتیبانی مرورگر

  • کروم: 76.
  • لبه: 79.
  • فایرفاکس: 127.
  • سافاری: 13.1.

منبع

چسباندن: خواندن داده ها از کلیپ بورد

readText()

برای خواندن متن از کلیپ بورد، navigator.clipboard.readText() را فراخوانی کنید و منتظر بمانید تا وعده برگشتی حل شود:

async function getClipboardContents() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted content: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
}

پشتیبانی مرورگر

  • کروم: 66.
  • لبه: 79.
  • فایرفاکس: 125.
  • سافاری: 13.1.

منبع

خواندن ()

متد navigator.clipboard.read() نیز ناهمزمان است و یک وعده را برمی گرداند. برای خواندن یک تصویر از کلیپ بورد، فهرستی از اشیاء ClipboardItem را به دست آورید، سپس روی آنها تکرار کنید.

هر ClipboardItem می‌تواند محتویات خود را در انواع مختلف نگهداری کند، بنابراین باید دوباره از یک حلقه for...of استفاده کنید. برای هر نوع، متد getType() را با نوع فعلی به عنوان آرگومان فراخوانی کنید تا حباب مربوطه به دست آید. مانند قبل، این کد به تصاویر گره خورده نیست و با سایر انواع فایل های آینده کار خواهد کرد.

async function getClipboardContents() {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        const blob = await clipboardItem.getType(type);
        console.log(URL.createObjectURL(blob));
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
}

پشتیبانی مرورگر

  • کروم: 66.
  • لبه: 79.
  • فایرفاکس: 127.
  • سافاری: 13.1.

منبع

کار با فایل های پیست شده

برای کاربران مفید است که بتوانند از میانبرهای صفحه کلید کلیپ بورد مانند ctrl + c و ctrl + v استفاده کنند. Chromium فایل‌های فقط خواندنی را همانطور که در زیر ذکر شده است در کلیپ بورد نمایش می‌دهد. وقتی کاربر میانبر پیست پیش‌فرض سیستم عامل را می‌زند یا وقتی کاربر روی Edit و سپس Paste در نوار منوی مرورگر کلیک می‌کند، فعال می‌شود. هیچ کد لوله کشی بیشتری مورد نیاز نیست.

document.addEventListener("paste", async e => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  // Read the file's contents, assuming it's a text file.
  // There is no way to write back to it.
  console.log(await file.text());
});

پشتیبانی مرورگر

  • کروم: 3.
  • لبه: 12.
  • فایرفاکس: 3.6.
  • سافاری: 4.

منبع

رویداد خمیر

همانطور که قبلا ذکر شد، برنامه‌هایی برای معرفی رویدادها برای کار با Clipboard API وجود دارد، اما در حال حاضر می‌توانید از رویداد paste موجود استفاده کنید. با روش های ناهمزمان جدید برای خواندن متن کلیپ بورد به خوبی کار می کند. مانند رویداد copy ، فراموش نکنید که preventDefault() فراخوانی کنید.

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

پشتیبانی مرورگر

  • کروم: 1.
  • لبه: 12.
  • فایرفاکس: 22.
  • سافاری: 3.

منبع

مدیریت چندین نوع MIME

اکثر پیاده سازی ها چندین فرمت داده را برای یک عملیات برش یا کپی روی کلیپ بورد قرار می دهند. دو دلیل برای این وجود دارد: به‌عنوان یک توسعه‌دهنده برنامه، هیچ راهی برای آگاهی از قابلیت‌های برنامه‌ای که کاربر می‌خواهد متن یا تصاویر را در آن کپی کند، ندارید، و بسیاری از برنامه‌ها از چسباندن داده‌های ساختاریافته به‌عنوان متن ساده پشتیبانی می‌کنند. این معمولاً با یک آیتم منوی ویرایش با نامی مانند چسباندن و مطابقت سبک یا جای‌گذاری بدون قالب‌بندی به کاربران ارائه می‌شود.

مثال زیر نحوه انجام این کار را نشان می دهد. این مثال از fetch() برای بدست آوردن داده های تصویر استفاده می کند، اما همچنین می تواند از یک <canvas> یا File System Access API باشد.

async function copy() {
  const image = await fetch('kitten.png').then(response => response.blob());
  const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
  const item = new ClipboardItem({
    'text/plain': text,
    'image/png': image
  });
  await navigator.clipboard.write([item]);
}

امنیت و مجوزها

دسترسی به کلیپ بورد همیشه یک نگرانی امنیتی برای مرورگرها ایجاد کرده است. بدون مجوزهای مناسب، یک صفحه می‌تواند بی‌صدا تمام محتوای مخرب را در کلیپ‌بورد کاربر کپی کند که در صورت چسباندن نتایج فاجعه‌باری ایجاد کند. یک صفحه وب را تصور کنید که بی‌صدا rm -rf / یا یک تصویر بمب رفع فشار را در کلیپ‌بورد شما کپی می‌کند.

درخواست مرورگر از کاربر برای مجوز کلیپ بورد.
درخواست مجوز برای Clipboard API.

دادن دسترسی خواندن بدون محدودیت به صفحات وب به کلیپ بورد حتی مشکل سازتر است. کاربران به طور معمول اطلاعات حساسی مانند گذرواژه‌ها و جزئیات شخصی را در کلیپ‌بورد کپی می‌کنند، که سپس می‌تواند توسط هر صفحه‌ای بدون اطلاع کاربر خوانده شود.

مانند بسیاری از APIهای جدید، Clipboard API فقط برای صفحاتی که از طریق HTTPS ارائه می شوند پشتیبانی می شود. برای کمک به جلوگیری از سوء استفاده، دسترسی به کلیپ بورد فقط زمانی مجاز است که یک صفحه برگه فعال باشد. صفحات موجود در برگه های فعال می توانند بدون درخواست مجوز در کلیپ بورد بنویسند، اما خواندن از کلیپ بورد همیشه به اجازه نیاز دارد.

مجوزهای کپی و چسباندن به Permissions API اضافه شده است. مجوز clipboard-write به طور خودکار به صفحات زمانی که برگه فعال هستند اعطا می شود. مجوز clipboard-read باید درخواست شود، که می توانید با تلاش برای خواندن داده ها از کلیپ بورد انجام دهید. کد زیر دومی را نشان می دهد:

const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

// Listen for changes to the permission state
permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

همچنین می‌توانید با استفاده از گزینه allowWithoutGesture کنترل کنید که آیا یک حرکت کاربر برای فراخوانی برش یا چسباندن لازم است یا خیر. پیش‌فرض این مقدار بسته به مرورگر متفاوت است، بنابراین همیشه باید آن را وارد کنید.

اینجاست که ماهیت ناهمزمان Clipboard API واقعاً مفید است: تلاش برای خواندن یا نوشتن داده‌های کلیپ‌بورد به‌طور خودکار از کاربر درخواست می‌کند در صورتی که قبلاً به آن اجازه داده نشده باشد. از آنجایی که API مبتنی بر وعده است، این کاملاً شفاف است و کاربری که مجوز کلیپ‌بورد را رد می‌کند باعث رد شدن قول می‌شود تا صفحه بتواند به درستی پاسخ دهد.

از آنجایی که مرورگرها فقط زمانی اجازه دسترسی به کلیپ بورد را می دهند که یک صفحه برگه فعال باشد، متوجه خواهید شد که برخی از نمونه های اینجا اگر مستقیماً در کنسول مرورگر جایگذاری شوند اجرا نمی شوند، زیرا ابزارهای توسعه دهنده خود برگه فعال هستند. یک ترفند وجود دارد: دسترسی به کلیپ بورد را با استفاده از setTimeout() به تعویق بیندازید، سپس به سرعت روی صفحه کلیک کنید تا قبل از فراخوانی توابع، آن را متمرکز کنید:

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

یکپارچه سازی خط مشی مجوزها

برای استفاده از API در iframes، باید آن را با Permissions Policy فعال کنید، که مکانیزمی را تعریف می‌کند که امکان فعال و غیرفعال کردن انتخابی ویژگی‌های مرورگر و API‌های مختلف را فراهم می‌کند. به طور مشخص، بسته به نیاز برنامه خود clipboard-write باید یکی از clipboard-read یا هر دو را بگذرانید.

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

تشخیص ویژگی

برای استفاده از Async Clipboard API در حین پشتیبانی از همه مرورگرها، navigator.clipboard را آزمایش کنید و به روش‌های قبلی بازگردید. به عنوان مثال، در اینجا نحوه اعمال چسباندن برای گنجاندن مرورگرهای دیگر آورده شده است.

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  let text;
  if (navigator.clipboard) {
    text = await navigator.clipboard.readText();
  }
  else {
    text = e.clipboardData.getData('text/plain');
  }
  console.log('Got pasted text: ', text);
});

این تمام ماجرا نیست. قبل از Async Clipboard API، ترکیبی از اجرای کپی و چسباندن مختلف در مرورگرهای وب وجود داشت. در اکثر مرورگرها، کپی و چسباندن خود مرورگر را می توان با استفاده از document.execCommand('copy') و document.execCommand('paste') فعال کرد. اگر متنی که باید کپی شود رشته ای است که در DOM وجود ندارد، باید به DOM تزریق شود و انتخاب شود:

button.addEventListener('click', (e) => {
  const input = document.createElement('input');
  input.style.display = 'none';
  document.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  const result = document.execCommand('copy');
  if (result === 'unsuccessful') {
    console.error('Failed to copy text.');
  }
  input.remove();
});

دموها

می‌توانید با Async Clipboard API در دموهای زیر بازی کنید. در Glitch می توانید نسخه ی نمایشی متن یا نسخه ی نمایشی تصویر را دوباره میکس کنید تا با آنها آزمایش کنید.

مثال اول حرکت متن را روی و خارج از کلیپ بورد نشان می دهد.

برای امتحان کردن API با تصاویر، از این نسخه نمایشی استفاده کنید. به یاد داشته باشید که فقط PNG ها و فقط در چند مرورگر پشتیبانی می شوند.

قدردانی

API Asynchronous Clipboard توسط داروین هوانگ و گری کاچمارچیک پیاده سازی شد. داروین نسخه ی نمایشی را نیز ارائه کرد. با تشکر از کیاریک و دوباره گری کاچمارچیک برای بررسی بخش‌هایی از این مقاله.

تصویر قهرمان توسط مارکوس وینکلر در Unsplash .