Bagaimana Anda mengatasi batasan bersarang bentuk HTML?


199

Saya tahu bahwa XHTML tidak mendukung tag formulir bersarang dan saya sudah membaca jawaban lain di sini tentang Stack Overflow mengenai masalah ini, tapi saya masih belum menemukan solusi yang elegan untuk masalah ini.

Beberapa mengatakan Anda tidak membutuhkannya dan mereka tidak dapat memikirkan skenario jika ini diperlukan. Yah, secara pribadi saya tidak bisa memikirkan skenario yang saya tidak membutuhkannya.

Mari kita lihat contoh yang sangat sederhana:

Anda membuat aplikasi blog dan Anda memiliki formulir dengan beberapa bidang untuk membuat posting baru dan toolbar dengan "tindakan" seperti "Simpan", "Hapus", "Batalkan".

<form
 action="/post/dispatch/too_bad_the_action_url_is_in_the_form_tag_even_though_conceptually_every_submit_button_inside_it_may_need_to_post_to_a_diffent_distinct_url"
method="post">

    <input type="text" name="foo" /> <!-- several of those here -->
    <div id="toolbar">
        <input type="submit" name="save" value="Save" />
        <input type="submit" name="delete" value="Delete" />
        <a href="/home/index">Cancel</a>
    </div>
</form>

Tujuan kami adalah menulis formulir dengan cara yang tidak memerlukan JavaScript , cukup formulir HTML lama dan kirim tombol.

Karena URL tindakan didefinisikan dalam tag Formulir dan bukan di setiap tombol kirim individual, satu-satunya pilihan kami adalah memposting ke URL umum dan kemudian mulai "jika ... lalu ... lain" untuk menentukan nama tombol yang telah diajukan. Tidak terlalu elegan, tetapi satu-satunya pilihan kami, karena kami tidak ingin bergantung pada JavaScript.

Satu-satunya masalah adalah bahwa menekan "Hapus", akan menyerahkan SEMUA bidang formulir di server meskipun satu-satunya hal yang diperlukan untuk tindakan ini adalah input tersembunyi dengan post-id. Bukan masalah besar dalam contoh kecil ini, tetapi saya memiliki formulir dengan ratusan (sehingga untuk berbicara) bidang dan tab di aplikasi LOB saya yang (karena persyaratan) harus menyerahkan semuanya dalam sekali jalan dan dalam hal apapun ini tampaknya sangat tidak efisien dan sia-sia. Jika form nesting didukung, setidaknya saya dapat membungkus tombol "Delete" di dalam form itu sendiri dengan hanya kolom post-id.

Anda dapat mengatakan "Cukup terapkan" Hapus "sebagai tautan alih-alih mengirim". Ini akan salah dalam banyak tingkatan, tetapi yang paling penting karena tindakan efek samping seperti "Hapus" di sini, seharusnya tidak pernah menjadi permintaan GET.

Jadi pertanyaan saya (terutama bagi mereka yang mengatakan mereka tidak perlu bersarang) adalah apa yang ANDA lakukan? Apakah ada solusi elegan yang saya lewatkan atau intinya adalah "Membutuhkan JavaScript atau mengirimkan semuanya"?


17
Ini lucu, tetapi XHTML mengizinkan bentuk tidak langsung bersarang menurut catatan menarik anderwald.info/internet/nesting-form-tags-in-xhtml - (X) HTML melarang formulir bersarang seperti "form> form", tetapi memungkinkan "form> fieldset> form ", validator W3 mengatakan itu valid, tetapi browser memiliki bug dengan sarang seperti itu.
Nishi


linkrot fix untuk tautan komentar @ Nishi: web.archive.org/web/20170420110433/http://anderwald.info/…
albert

Jawaban:


53

Saya akan menerapkan ini persis seperti yang Anda jelaskan: kirimkan semuanya ke server dan lakukan sederhana jika / selain memeriksa tombol apa yang diklik.

Dan kemudian saya akan menerapkan panggilan Javascript yang mengikat ke acara onsub formulir yang akan memeriksa sebelum formulir dikirimkan, dan hanya mengirimkan data yang relevan ke server (mungkin melalui formulir kedua pada halaman dengan ID yang diperlukan untuk memproses hal itu sebagai input tersembunyi, atau me-refresh lokasi halaman dengan data yang Anda butuhkan diteruskan sebagai permintaan GET, atau melakukan posting Ajax ke server, atau ...).

Dengan cara ini orang-orang tanpa Javascript dapat menggunakan formulir dengan baik, tetapi beban server diimbangi karena orang-orang yang memiliki Javascript hanya mengirimkan satu bagian data yang diperlukan. Memfokuskan diri hanya dengan mendukung satu atau yang lain benar-benar membatasi pilihan Anda secara tidak perlu.

Atau, jika Anda bekerja di belakang firewall perusahaan atau sesuatu dan semua orang telah menonaktifkan Javascript, Anda mungkin ingin melakukan dua bentuk dan mengerjakan beberapa keajaiban CSS agar terlihat seperti tombol hapus adalah bagian dari formulir pertama.


15
OP asli menyatakan tidak ada javascript
Jason

26
Masalah dengan metode ini adalah bahwa itu adalah non-TETAP. Simpan dan hapus berhubungan dengan tindakan yang berbeda pada sumber daya: "Simpan" -> POST / my_resource (membuat sumber daya baru) "Simpan" -> PUT / my_resource (memodifikasi sumber daya yang ada) "Hapus" -> DELETE / my_resource (hancurkan sumber daya) Jujur berbicara, masalahnya adalah POST diharapkan untuk menciptakan sumber daya baru. Tentu saja tidak boleh menghapus sumber daya yang ada.
user132447

178

Saya tahu ini adalah pertanyaan lama, tetapi HTML5 menawarkan beberapa opsi baru.

Yang pertama adalah memisahkan formulir dari bilah alat di markup, menambahkan formulir lain untuk tindakan hapus, dan mengaitkan tombol di bilah alat dengan formulir masing-masing menggunakan formatribut.

<form id="saveForm" action="/post/dispatch/save" method="post">
    <input type="text" name="foo" /> <!-- several of those here -->  
</form>
<form id="deleteForm" action="/post/dispatch/delete" method="post">
    <input type="hidden" value="some_id" />
</form>
<div id="toolbar">
    <input type="submit" name="save" value="Save" form="saveForm" />
    <input type="submit" name="delete" value="Delete" form="deleteForm" />
    <a href="/home/index">Cancel</a>
</div>

Opsi ini cukup fleksibel, tetapi posting asli juga menyebutkan bahwa mungkin perlu melakukan tindakan yang berbeda dengan satu bentuk. HTML5 datang untuk menyelamatkan, lagi. Anda dapat menggunakan formactionatribut pada tombol kirim, sehingga tombol berbeda dalam bentuk yang sama dapat dikirimkan ke URL yang berbeda. Contoh ini hanya menambahkan metode klon ke bilah alat di luar formulir, tetapi akan berfungsi sama di dalam formulir.

<div id="toolbar">
    <input type="submit" name="clone" value="Clone" form="saveForm"
           formaction="/post/dispatch/clone" />
</div>

http://www.whatwg.org/specs/web-apps/current-work/#attributes-for-form-submission

Keuntungan dari fitur-fitur baru ini adalah bahwa mereka melakukan semua ini secara deklaratif tanpa JavaScript. Kerugiannya adalah mereka tidak didukung pada peramban yang lebih lama, jadi Anda harus melakukan beberapa pengisian untuk peramban yang lebih lama.


4
+1 - tetapi alangkah baiknya jika mengetahui untuk apa dukungan browser form=itu - CanIUse tidak mengatakannya. caniuse.com/#feat=forms
AKX

1
Benar ini luar biasa, tetapi sampai hari ini fitur yang tidak didukung oleh IE masih mendiskualifikasi untuk penggunaan 'produksi'
Jako

3
Ini adalah praktik buruk untuk digunakan <input>untuk mendeklarasikan tombol. yang <button>unsur diperkenalkan 15 tahun yang lalu untuk tujuan ini. Sungguh orang.
Nicholas Shanks

27
Poin Anda tidak perlu dipertentangkan dan tidak relevan dengan pertanyaan OP. OP bertanya tentang batasan form nesting tanpa menggunakan JavaScript dan jawabannya menawarkan solusi. Apakah Anda menggunakan <input>atau <button>elemen tidak relevan, meskipun tombol biasanya lebih sesuai untuk bilah alat, seperti yang Anda katakan. Omong-omong, elemen tombol belum didukung oleh semua browser selama 15 tahun.
shovavnik

6
@AKX Walaupun merupakan pernyataan lama, Anda akan dapat melihat dukungan untuk fitur ini di sini: html5test.com/compare/feature/form-association-form.html . IE adalah penghapusbukuan, sebagian besar peramban desktop lain sepertinya dapat ditoleransi.
lemak

16

Anda dapat memiliki formulir berdampingan di halaman, tetapi tidak bersarang. lalu gunakan CSS untuk membuat semua tombol berbaris cantik?

<form method="post" action="delete_processing_page">
   <input type="hidden" name="id" value="foo" />
   <input type="submit" value="delete" class="css_makes_me_pretty" />
</form>

<form method="post" action="add_edit_processing_page">
   <input type="text" name="foo1"  />
   <input type="text" name="foo2"  />
   <input type="text" name="foo3"  />
   ...
   <input type="submit" value="post/edit" class="css_makes_me_pretty" />
</form>

5
Ya dan rasanya terlalu hackish. Ditambah di halaman yang sangat besar (bayangkan tab dan sub-tab dan tombol kirim bersarang di beberapa tingkat cakupan) hampir mustahil untuk sepenuhnya memisahkan posisi layar elemen dari posisi mereka di dokumen.
gCoder

3
baik, selalu ada keseimbangan antara apa yang ingin Anda lakukan dan apa yang dapat Anda lakukan. sepertinya mereka adalah pilihan - salah satu bentuk ditangani di server, atau salah satu beberapa bentuk yang menjadi sulit untuk mempertahankan - memilih dengan bijak :)
Jason

3
Ya, sayangnya dengan semua pembatasan dalam html, saya hanya akan tetap pada apa yang didukung dan mengirim seluruh formulir ke server dan kemudian menggunakan javascript secara tidak mencolok untuk klien yang mendukungnya untuk mencegat pengiriman formulir dan hanya mengirim apa yang benar-benar diperlukan. Itu kompromi terbaik.
gCoder

13

HTML5 memiliki gagasan "pemilik formulir" - atribut "formulir" untuk elemen input. Memungkinkan untuk meniru bentuk bersarang dan akan menyelesaikan masalah.


1
Adakah referensi tentang tautan yang layak ini?
dakab

Lihat w3.org/TR/html5/forms.html#form-owner "Elemen terkait bentuk, secara default, terkait dengan elemen bentuk leluhur terdekat, tetapi, jika dapat ditinjau kembali, mungkin memiliki atribut formulir yang ditentukan untuk menimpa. ini."
Nishi

11

Agak topik lama, tapi yang ini mungkin berguna untuk seseorang:

Seperti seseorang yang disebutkan di atas - Anda dapat menggunakan formulir dummy. Saya harus mengatasi masalah ini beberapa waktu lalu. Pada awalnya saya benar-benar lupa tentang pembatasan HTML ini dan baru saja menambahkan formulir bersarang. Hasilnya menarik - saya kehilangan bentuk pertama saya dari sarang. Kemudian ternyata semacam "trik" untuk hanya menambahkan bentuk dummy (yang akan dihapus dari browser) sebelum bentuk bersarang yang sebenarnya.

Dalam kasus saya, tampilannya seperti ini:

<form id="Main">
  <form></form> <!--this is the dummy one-->
  <input...><form id="Nested 1> ... </form>
  <input...><form id="Nested 1> ... </form>
  <input...><form id="Nested 1> ... </form>
  <input...><form id="Nested 1> ... </form>
  ......
</form>

Berfungsi baik dengan Chrome, FireFox dan Safari. IE hingga 9 (tidak yakin sekitar 10) dan Opera tidak mendeteksi parameter dalam bentuk utama. Global $ _REQUEST kosong, terlepas dari input apa pun. Bentuk batin tampaknya bekerja dengan baik di mana-mana.

Belum menguji saran lain yang dijelaskan di sini - fieldset di sekitar formulir bersarang.

EDIT : Frameset tidak berfungsi! Saya cukup menambahkan formulir Utama setelah yang lain (tidak ada lagi formulir bersarang) dan menggunakan "clone" jQuery untuk menduplikasi input dalam formulir pada klik tombol. Menambahkan .hide () ke masing-masing input yang dikloning agar tata letak tidak berubah dan sekarang berfungsi seperti pesona.


Ini bekerja dengan baik untuk saya. Saya sedang mengerjakan halaman asp.net, yang memiliki formulir lengkap. Saya memiliki formulir bersarang bagian dalam untuk digunakan untuk plugin jQuery Validation ( github.com/jzaefferer/jquery-validation ), dan sementara itu berfungsi dengan baik di FireFox dan IE, gagal di Chrome dengan 'TypeError: Tidak bisa memanggil metode' elemen 'dari tidak terdefinisi'. Ternyata panggilan inisialisasi validasi () gagal karena tidak dapat menemukan formulir dalam, karena Chrome menghapusnya dari blok html yang ditambahkan menggunakan jQuery.html (). Menambahkan bentuk boneka kecil pertama menyebabkan Chrome meninggalkan yang asli.
John - Not A Number

Tampaknya ini tidak berfungsi lagi. Firefox menghapus tag <form> kedua dan kemudian menghentikan formulir saya dengan tag </form> pertama. Ini meninggalkan tag </form> tidak valid di bagian bawah halaman dan formulir yang tidak akan dikirimkan dengan benar. :(
Tarquin

Sangat berguna bagi saya. Saya menambahkan widget web yang berisi formulir ke halaman asp.net. Terkejut oleh WebForm bahwa ia selalu menghasilkan formulir yang melampirkan segala sesuatu di tubuh HTML. Formulir terlampir menjadi sakit kepala untuk widget web. Bentuk dummy tampaknya menjadi solusi cepat dan efektif untuk situasi ini. Ini berfungsi dengan baik di Chrome dan Safari sejauh ini, belum menguji Firefox. Terima kasih telah berbagi, menyelamatkan saya banyak waktu!
JM Yang

4

Saya pikir Jason benar. Jika tindakan "Hapus" Anda minimal, buatlah itu dalam bentuk dengan sendirinya, dan sejajarkan dengan tombol lain sehingga membuat antarmuka tampak seperti satu formulir yang disatukan, meskipun itu tidak.

Atau, tentu saja, mendesain ulang antarmuka Anda, dan biarkan orang menghapus tempat lain sama sekali yang tidak mengharuskan mereka untuk melihat bentuk luar biasa itu sama sekali.


2

Salah satu cara saya akan melakukan ini tanpa javascript adalah dengan menambahkan satu set tombol radio yang menentukan tindakan yang harus diambil:

  • Memperbarui
  • Menghapus
  • Masa bodo

Maka skrip tindakan akan mengambil tindakan berbeda tergantung pada nilai set tombol radio.

Cara lain adalah dengan meletakkan dua formulir pada halaman seperti yang Anda sarankan, hanya saja tidak bersarang. Tata letak mungkin sulit dikendalikan:

<form name = "editform" action = "the_action_url" method = "post">
   <input type = "hidden" name = "task" value = "update" />
   <input type = "text" name = "foo" />
   <input type = "submit" name = "save" value = "Save" />
</form>

<form name = "delform" action = "the_action_url" method = "post">
   <input type = "hidden" name = "task" value = "delete" />
   <input type = "hidden" name = "post_id" value = "5" />
   <input type = "submit" name = "delete" value = "Delete" />
</form>

Menggunakan bidang "tugas" tersembunyi di skrip penanganan untuk bercabang dengan tepat.


1

Diskusi ini masih menarik bagi saya. Di belakang pos asli adalah "persyaratan" yang OP bagikan - yaitu formulir dengan kompatibilitas ke belakang. Sebagai seseorang yang karyanya pada saat penulisan terkadang harus mendukung kembali ke IE6 (dan selama bertahun-tahun yang akan datang), saya menggali itu.

Tanpa mendorong kerangka kerja (semua organisasi akan ingin meyakinkan diri mereka tentang kompatibilitas / ketahanan, dan saya tidak menggunakan diskusi ini sebagai pembenaran untuk kerangka kerja), solusi Drupal untuk masalah ini menarik. Drupal juga relevan secara langsung karena kerangka kerja telah memiliki kebijakan lama "itu harus bekerja tanpa Javascript (hanya jika Anda mau)" yaitu masalah OP.

Drupal menggunakan fungsi form.inc yang agak luas untuk menemukan triggering_element (ya, itulah nama dalam kode). Lihat bagian bawah kode yang tercantum pada halaman API untuk form_builder (jika Anda ingin menggali lebih detail, sumber direkomendasikan - drupal-x.xx / include / form.inc ). Builder menggunakan pembuatan atribut HTML otomatis dan, melalui itu, dapat kembali mendeteksi tombol mana yang ditekan, dan bertindak sesuai (ini dapat diatur untuk menjalankan proses terpisah juga).

Di luar pembuat formulir, Drupal membagi tindakan 'hapus' data menjadi url / formulir terpisah, kemungkinan karena alasan yang disebutkan dalam pos asli. Ini membutuhkan semacam langkah pencarian / daftar ( erang bentuk lain!, Tetapi mudah digunakan) sebagai awal. Tetapi ini memiliki keuntungan menghilangkan masalah "serahkan semuanya". Bentuk besar dengan data digunakan untuk tujuan yang dimaksudkan, pembuatan / pembaruan data (atau bahkan tindakan 'penggabungan').

Dengan kata lain, salah satu cara mengatasi masalah adalah dengan membagi formulir menjadi dua, kemudian masalahnya hilang (dan metode HTML juga dapat diperbaiki melalui POST).


0

Gunakan iframeuntuk formulir bersarang. Jika mereka perlu berbagi bidang, maka ... itu tidak benar-benar bersarang.


1
Menggunakan iframe adalah ide yang bagus, hanya memalukan bahwa dalam kebanyakan kasus Anda akan memerlukan javascript untuk mengubah ukurannya (karena Anda tidak bisa tahu apa ukuran teks pengguna, maka, seberapa besar tombolnya).
Nicholas Shanks

@ Nicholas, bagaimana Anda bisa menggunakan Javascript untuk mengubah ukurannya? HTML berbingkai tidak dapat berbicara dengan iframe, dan sebaliknya, kecuali mereka berada di domain yang sama.
Dan Rosenstark

@Nicholas, terima kasih, hanya ingin memastikan bahwa mereka harus berada di domain yang sama
Dan Rosenstark

0

Solusi saya adalah memiliki tombol memanggil fungsi JS yang menulis dan kemudian mengirimkan formulir dengan formulir utama

<head>
<script>
function removeMe(A, B){
        document.write('<form name="removeForm" method="POST" action="Delete.php">');
        document.write('<input type="hidden" name="customerID" value="' + A + '">');
        document.write('<input type="hidden" name="productID" value="' + B + '">');
        document.write('</form>');
        document.removeForm.submit();
}
function insertMe(A, B){
        document.write('<form name="insertForm" method="POST" action="Insert.php">');
        document.write('<input type="hidden" name="customerID" value="' + A + '">');
        document.write('<input type="hidden" name="productID" value="' + B + '">');
        document.write('</form>');
        document.insertForm.submit();
}
</script>
</head>

<body>
<form method="POST" action="main_form_purpose_page.php">

<input type="button" name="remove" Value="Remove" onclick="removeMe('$customerID','$productID')">
<input type="button" name="insert" Value="Insert" onclick="insertMe('$customerID','$productID')">

<input type="submit" name="submit" value="Submit">
</form>
</body>

-1

Jika Anda benar-benar tidak ingin menggunakan banyak formulir (seperti saran Jason), maka gunakan tombol dan penangan onclick.

<form id='form' name='form' action='path/to/add/edit/blog' method='post'>
    <textarea name='message' id='message'>Blog message here</textarea>
    <input type='submit' id='save' value='Save'>
</form>
<button id='delete'>Delete</button>
<button id='cancel'>Cancel</button>

Dan kemudian di javascript (saya menggunakan jQuery di sini untuk kemudahan) (walaupun itu cukup berlebihan untuk menambahkan beberapa penangan onclick)

$('#delete').click( function() {
   document.location = 'path/to/delete/post/id'; 
});
$('#cancel').click( function() {
   document.location = '/home/index';
});

Saya juga tahu, ini akan membuat setengah halaman tidak berfungsi tanpa javascript.


1
Kecuali saya telah melewatkan sesuatu, ini tidak lebih baik daripada menautkan ke tindakan hapus: hasil akhirnya akan menjadi permintaan GET ke tindakan hapus (buruk). Sebenarnya ini sedikit lebih kotor, karena membutuhkan JavaScript untuk menyelesaikan sesuatu yang bisa dilakukan oleh tag tautan lama.
Kyle Fox

-1

Menanggapi pertanyaan yang diposting oleh Yar dalam komentar atas jawabannya sendiri, saya sajikan beberapa JavaScript yang akan mengubah ukuran iframe. Untuk kasus tombol formulir, aman untuk menganggap iframe akan berada di domain yang sama. Ini adalah kode yang saya gunakan. Anda harus mengubah matematika / konstanta untuk situs Anda sendiri:

function resizeIFrame(frame)
{
    try {
        innerDoc = ('contentDocument' in frame) ? frame.contentDocument : frame.contentWindow.document;
        if('style' in frame) {
            frame.style.width = Math.min(755, Math.ceil(innerDoc.body.scrollWidth)) + 'px';
            frame.style.height = Math.ceil(innerDoc.body.scrollHeight) + 'px';
        } else {
            frame.width = Math.ceil(innerDoc.body.scrollWidth);
            frame.height = Math.ceil(innerDoc.body.scrollHeight);
        }
    } catch(err) {
        window.status = err.message;
    }
}

Kemudian panggil seperti ini:

<iframe ... frameborder="0" onload="if(window.parent && window.parent.resizeIFrame){window.parent.resizeIFrame(this);}"></iframe>

-1

Saya baru saja datang dengan cara yang baik untuk melakukannya dengan jquery.

<form name="mainform">    
    <div id="placeholder">
    <div>
</form>

<form id="nested_form" style="position:absolute">
</form>

<script>
   $(document).ready(function(){
      pos = $('#placeholder').position();
      $('#nested_form')
         .css('left', pos.left.toFixed(0)+'px')
         .css('top', pos.top.toFixed(0)+'px');
   });
</script>

-1

Nah, jika Anda mengirimkan formulir, browser juga mengirimkan input nama dan nilai pengiriman. Jadi yang bisa Anda lakukan adalah

<form   
 action="/post/dispatch/too_bad_the_action_url_is_in_the_form_tag_even_though_conceptually_every_submit_button_inside_it_may_need_to_post_to_a_diffent_distinct_url"  
method="post">  

    <input type="text" name="foo" /> <!-- several of those here -->  
    <div id="toolbar">
        <input type="submit" name="action:save" value="Save" />
        <input type="submit" name="action:delete" value="Delete" />
        <input type="submit" name="action:cancel" value="Cancel" />
    </div>
</form>

jadi di sisi server Anda hanya mencari parameter yang memulai string lebar "action:" dan sisanya memberi tahu Anda tindakan apa yang harus diambil

jadi ketika Anda mengklik tombol Save browser mengirimi Anda sesuatu seperti foo = asd & action: save = Save


-1

Saya mengatasi masalah dengan memasukkan kotak centang tergantung pada bentuk apa yang ingin dilakukan orang tersebut. Kemudian digunakan 1 tombol untuk mengirimkan seluruh formulir.


2
Selamat datang di Stack Overflow . Lebih baik menggambarkan bagaimana ini memecahkan masalah, daripada hanya mengatakan apa yang Anda lakukan. Lihat Cara Menjawab untuk tips tentang cara membuat jawaban hebat di Stack Overflow. Terima kasih!
Qantas 94 Heavy

-2

Sebagai alternatif, Anda dapat menetapkan formulir actiob dengan cepat ... mungkin bukan solusi terbaik, tetapi tentu saja meringankan logika sisi server ...

<form name="frm" method="post">  
    <input type="submit" value="One" onclick="javascript:this.form.action='1.htm'" />  
    <input type="submit" value="Two" onclick="javascript:this.form.action='2.htm'" />  
</form>

Mengapa? Saya bertanya dengan serius, karena tidak ada cara mudah untuk mendapatkan tombol mana yang diklik. Apakah Anda mengatakan bahwa sesuatu seperti metode klik () jQuery harus digunakan untuk mengatur pengendali onclick tombol? Atau bahwa menangani acara klik sebelum formulir dikirimkan adalah masalahnya?
Zack Morris
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.