├── package.json ├── index.html ├── cheval.min.js ├── README.md ├── cheval.js └── LICENSE /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cheval", 3 | "version": "2.0.0", 4 | "description": "Copy to the clipboard using JavaScript without writing JS.", 5 | "main": "cheval.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ryanpcmcquen/cheval.git" 12 | }, 13 | "keywords": [ 14 | "clipboard" 15 | ], 16 | "author": "Ryan McQuen", 17 | "license": "MPL-2.0", 18 | "bugs": { 19 | "url": "https://github.com/ryanpcmcquen/cheval/issues" 20 | }, 21 | "homepage": "https://github.com/ryanpcmcquen/cheval#readme" 22 | } 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cheval 8 | 9 | 10 | 11 | 12 |

Cheval

13 | 14 |
15 | 16 |

17 | 18 |

19 | 20 | 21 |

22 | 23 |

24 | 25 | 26 |

27 | 28 |

29 | 30 | 31 |

32 | 33 |

34 | 35 | 36 |

37 | 38 |

39 | 40 |
You do not need to use a textarea, for example, this is a div!
41 |

42 | 43 |

44 | 45 |
source
46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /cheval.min.js: -------------------------------------------------------------------------------- 1 | // @license magnet:?xt=urn:btih:3877d6d54b3accd4bc32f8a48bf32ebc0901502a&dn=mpl-2.0.txt MPL-v2 2 | /*! cheval v2.0.0 by ryanpcmcquen */ 3 | !function(){"use strict";var afterCopyText_desktop="Copy again",afterCopyText_iPad="Now tap the text, then 'Copy'",afterCopyText_iPhoneOriPod="Now tap 'Copy'",afterCopyText_oldSafari="Press Command + C to copy",afterCopyText_notSupported="Please copy manually",sets={},regexBuilder=function(prefix){return new RegExp(prefix+"\\S*")};window.addEventListener("DOMContentLoaded",function(){var texts=Array.prototype.slice.call(document.querySelectorAll("[class*=text-to-copy]")),buttons=Array.prototype.slice.call(document.querySelectorAll("[class*=js-copy-btn]")),classNameFinder=function(arr,regex,namePrefix){return arr.map(function(item){return!!item.className.match(regex)&&item.className.match(regex)[0].replace(namePrefix,"")}).sort()};sets.texts=classNameFinder(texts,regexBuilder("text-to-copy"),"text-to-copy"),sets.buttons=classNameFinder(buttons,regexBuilder("js-copy-btn"),"js-copy-btn");var matches=sets.texts.map(function(ignore,index){return sets.texts[index].match(sets.buttons[index])}),throwErr=function(err){throw new Error(err)},iPhoneORiPod=!1,iPad=!1,oldSafari=!1,navAgent=window.navigator.userAgent;/^((?!chrome).)*safari/i.test(navAgent)&&!/^((?!chrome).)*[0-9][0-9](\.[0-9][0-9]?)?\ssafari/i.test(navAgent)&&(oldSafari=!0),navAgent.match(/iPhone|iPod/i)?iPhoneORiPod=!0:navAgent.match(/iPad/i)&&(iPad=!0);var cheval=function(btn,text){var copyBtn=document.querySelector(btn),setCopyBtnText=function(textToSet){copyBtn.textContent=textToSet};(iPhoneORiPod||iPad)&&oldSafari&&setCopyBtnText("Select text"),copyBtn?copyBtn.addEventListener("click",function(){var oldPosX=window.scrollX,oldPosY=window.scrollY,originalCopyItem=document.querySelector(text),dollyTheSheep=originalCopyItem.cloneNode(!0),copyItem=document.createElement("textarea");copyItem.style.opacity=0,copyItem.style.position="absolute";var copyValue=dollyTheSheep.value||dollyTheSheep.textContent;if(copyItem.value=copyValue,document.body.appendChild(copyItem),copyItem){copyItem.focus(),copyItem.selectionStart=0,copyItem.selectionEnd=copyValue.length;try{document.execCommand("copy"),copyItem.setAttribute("disabled",!0),oldSafari?setCopyBtnText(iPhoneORiPod?afterCopyText_iPhoneOriPod:iPad?afterCopyText_iPad:afterCopyText_oldSafari):(document.activeElement.blur(),setCopyBtnText(afterCopyText_desktop))}catch(ignore){setCopyBtnText(afterCopyText_notSupported)}originalCopyItem.focus(),window.scrollTo(oldPosX,oldPosY),originalCopyItem.selectionStart=0,originalCopyItem.selectionEnd=copyValue.length,copyItem.remove()}else throwErr("You don't have an element with the class: 'text-to-copy'. Please check the cheval README.")}):throwErr("You don't have a 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | ``` 106 | 107 | The characters after the dash in `text-to-copy-` or `js-copy-btn-` can be anything, they just have to match between the button and element. 108 | 109 | For example: 110 | 111 | ```html 112 | 113 | 114 | ``` 115 | 116 | Positioning of the elements and buttons does not matter, they do not need to be near each other on the page. This allows you to write declarative markup and not be concerned with the inner workings of this library. Enjoy! 117 | 118 | --- 119 | 120 | #### But ... 121 | 122 | ###### I want to dynamically add elements to my page, `cheval` only runs on page load! 123 | 124 | You're right! That's why in version `1.3.0`, `cheval` also adds itself to global scope. 125 | 126 | Now you can invoke at will on dynamic elements! 127 | 128 | ```js 129 | cheval('.dynamic-js-copy-btn', '.dynamic-text-to-copy'); 130 | ``` 131 | 132 | --- 133 | 134 | What does `cheval` mean? 135 | 136 | The name comes from Cheval glass, a type of mirror. 137 | 138 | --- 139 | 140 | This project is LibreJS compliant! 141 | 142 | [![LibreJS](https://www.gnu.org/software/librejs/images/logo-medium.png)](https://www.gnu.org/software/librejs/) 143 | 144 | --- 145 | 146 | If you prefer using specific tags instead of the latest version, you may specify a tag in the `jsDelivr` URL: 147 | 148 | https://cdn.jsdelivr.net/gh/ryanpcmcquen/cheval@2.0.0/cheval.min.js 149 | 150 | --- 151 | 152 | ### Contributing: 153 | 154 | If you wish to contribute to this project, pull requests are always welcome! Please make sure that any changes pass through [![http://jslint.com/](http://jslint.com/image/jslintpill.gif)](http://jslint.com/) with the following options before submitting them: 155 | 156 | ```js 157 | /*global module, window*/ 158 | /*jslint browser: true*/ 159 | ``` 160 | 161 | --- 162 | 163 | Thanks to [Lea Verou](https://github.com/LeaVerou) for lots of support and help with the name. 164 | 165 | Thanks to Nikita Tcherednikov for the Cheval icon, provided under the [Creative Commons license](https://creativecommons.org/licenses/by/3.0/us/). 166 | 167 | Thanks to Charles Raymond Macauley for the drawing available [here](https://commons.wikimedia.org/wiki/File:Jekyll.and.Hyde.Ch10.Drawing2.jpg). (Public domain) 168 | 169 | Thanks to BrowserStack and Koding! 170 | 171 | [![https://browserstack.com](https://usercontent.irccloud-cdn.com/file/1egf6j3q/BrowserStack_logo.png)](https://browserstack.com) 172 | 173 | 174 | Made with Koding 178 | 179 | 180 | --- 181 | 182 | Full download stats available here: 183 | 184 | https://data.jsdelivr.com/v1/package/gh/ryanpcmcquen/cheval/stats?from=2014-01-01 185 | -------------------------------------------------------------------------------- /cheval.js: -------------------------------------------------------------------------------- 1 | // @license magnet:?xt=urn:btih:3877d6d54b3accd4bc32f8a48bf32ebc0901502a&dn=mpl-2.0.txt MPL-v2 2 | /*! cheval v2.0.0 by ryanpcmcquen */ 3 | // Ryan P. C. McQuen | Everett, WA 4 | 5 | /*global module, window*/ 6 | /*jslint browser: true*/ 7 | (function () { 8 | 'use strict'; 9 | 10 | var textClassName = 'text-to-copy'; 11 | var buttonClassName = 'js-copy-btn'; 12 | var allowingButtonTextToChange = true; 13 | var afterCopyText = { 14 | desktop: 'Copy again', 15 | iPad: "Now tap the text, then 'Copy'", 16 | iPhoneOriPod: "Now tap 'Copy'", 17 | oldSafari: 'Press Command + C to copy', 18 | notSupported: 'Please copy manually', 19 | }; 20 | var sets = {}; 21 | var regexBuilder = function (prefix) { 22 | return new RegExp(prefix + '\\S*'); 23 | }; 24 | 25 | window.addEventListener('DOMContentLoaded', function () { 26 | var texts = Array.prototype.slice.call( 27 | document.querySelectorAll('[class*=' + textClassName + ']') 28 | ); 29 | var buttons = Array.prototype.slice.call( 30 | document.querySelectorAll('[class*=' + buttonClassName + ']') 31 | ); 32 | 33 | var classNameFinder = function (arr, regex, namePrefix) { 34 | return arr 35 | .map(function (item) { 36 | return item.className.match(regex) 37 | ? item.className.match(regex)[0].replace(namePrefix, '') 38 | : false; 39 | }) 40 | .sort(); 41 | }; 42 | 43 | sets.texts = classNameFinder( 44 | texts, 45 | regexBuilder(textClassName), 46 | textClassName 47 | ); 48 | 49 | sets.buttons = classNameFinder( 50 | buttons, 51 | regexBuilder(buttonClassName), 52 | buttonClassName 53 | ); 54 | 55 | var matches = sets.texts.map(function (ignore, index) { 56 | return sets.texts[index].match(sets.buttons[index]); 57 | }); 58 | 59 | var throwErr = function (err) { 60 | throw new Error(err); 61 | }; 62 | var iPhoneORiPod = false; 63 | var iPad = false; 64 | var oldSafari = false; 65 | var navAgent = window.navigator.userAgent; 66 | if ( 67 | /^((?!chrome).)*safari/i.test(navAgent) && 68 | // ^ Fancy safari detection thanks to: https://stackoverflow.com/a/23522755 69 | !/^((?!chrome).)*[0-9][0-9](\.[0-9][0-9]?)?\ssafari/i.test(navAgent) 70 | // ^ Even fancier Safari < 10 detection thanks to regex. :^) 71 | ) { 72 | oldSafari = true; 73 | } 74 | // We need to test for older Safari and the device, 75 | // because of quirky awesomeness. 76 | if (navAgent.match(/iPhone|iPod/i)) { 77 | iPhoneORiPod = true; 78 | } else if (navAgent.match(/iPad/i)) { 79 | iPad = true; 80 | } 81 | var cheval = function (btn, text) { 82 | var copyBtn = document.querySelector(btn); 83 | 84 | var setCopyBtnText = function (textToSet) { 85 | copyBtn.textContent = textToSet; 86 | }; 87 | if (iPhoneORiPod || iPad) { 88 | if (oldSafari) { 89 | setCopyBtnText('Select text'); 90 | } 91 | } 92 | if (copyBtn) { 93 | copyBtn.addEventListener('click', function () { 94 | var oldPosX = window.scrollX; 95 | var oldPosY = window.scrollY; 96 | // Clone the text-to-copy node so that we can 97 | // create a hidden textarea, with its text value. 98 | // Thanks to @LeaVerou for the idea. 99 | var originalCopyItem = document.querySelector(text); 100 | var dollyTheSheep = originalCopyItem.cloneNode(true); 101 | var copyItem = document.createElement('textarea'); 102 | copyItem.style.opacity = 0; 103 | copyItem.style.position = 'absolute'; 104 | // If .value is undefined, .textContent will 105 | // get assigned to the textarea we made. 106 | var copyValue = 107 | dollyTheSheep.value || dollyTheSheep.textContent; 108 | copyItem.value = copyValue; 109 | document.body.appendChild(copyItem); 110 | if (copyItem) { 111 | // Select the text: 112 | copyItem.focus(); 113 | copyItem.selectionStart = 0; 114 | // For some reason the 'copyItem' does not get 115 | // the correct length, so we use the OG. 116 | copyItem.selectionEnd = copyValue.length; 117 | try { 118 | // Now that we've selected the text, execute the copy command: 119 | document.execCommand('copy'); 120 | // And disable the cloned area to prevent jumping. 121 | // This has to come after the `copy` command. 122 | copyItem.setAttribute('disabled', true); 123 | // Allow the button text to be changed. 124 | // Set `allowingButtonTextToChange = false` to leave it alone. 125 | // Default is `true`. 126 | // Thanks to @jasondavis. 127 | if (allowingButtonTextToChange) { 128 | if (oldSafari) { 129 | if (iPhoneORiPod) { 130 | setCopyBtnText( 131 | afterCopyText.iPhoneOriPod 132 | ); 133 | } else if (iPad) { 134 | // The iPad doesn't have the 'Copy' box pop up, 135 | // you have to tap the text first. 136 | setCopyBtnText(afterCopyText.iPad); 137 | } else { 138 | // Just old! 139 | setCopyBtnText(afterCopyText.oldSafari); 140 | } 141 | } else { 142 | // Hide the onscreen keyboard, which opens 143 | // on iOS devices, due to the target element 144 | // being focused on. 145 | document.activeElement.blur(); 146 | setCopyBtnText(afterCopyText.desktop); 147 | } 148 | } 149 | } catch (ignore) { 150 | if (allowingButtonTextToChange) { 151 | setCopyBtnText(afterCopyText.notSupported); 152 | } 153 | } 154 | originalCopyItem.focus(); 155 | // Restore the user's original position to avoid 156 | // 'jumping' when they click a copy button. 157 | window.scrollTo(oldPosX, oldPosY); 158 | originalCopyItem.selectionStart = 0; 159 | originalCopyItem.selectionEnd = copyValue.length; 160 | copyItem.remove(); 161 | } else { 162 | throwErr( 163 | "You don't have an element with the class: '" + 164 | textClassName + 165 | "'. Please check the cheval README." 166 | ); 167 | } 168 | }); 169 | } else { 170 | throwErr( 171 | "You don't have a