├── .gitignore ├── img └── cover.png ├── .editorconfig ├── README.md ├── css ├── style.css └── prism.css ├── js ├── app.js ├── prism.js └── prism-highlight.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/dumb-codepen/HEAD/img/cover.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Simple Live Editor - [DEMO](https://willianjusten.com.br/dumb-codepen/) 2 | 3 | ![Preview do Site](/img/cover.png) 4 | 5 | > A simple live editor with very few lines of code. Created to study purposes. 6 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | iframe { 8 | width: 100%; 9 | height: 50vh; 10 | border: none; 11 | } 12 | 13 | 14 | .editor { 15 | display: flex; 16 | } 17 | 18 | .editor pre { 19 | background: #1E1F20; 20 | border: 1px solid rgb(211, 211, 211); 21 | color: rgb(211, 211, 211); 22 | flex: 1; 23 | font-size: 16px; 24 | height: 50vh; 25 | padding: 30px 10px; 26 | margin: 0; 27 | position: relative; 28 | } 29 | 30 | .editor pre:before { 31 | display: block; 32 | font-size: 12px; 33 | position: absolute; 34 | top: 10px; 35 | left: 10px; 36 | } 37 | 38 | #html:before { 39 | content: 'HTML'; 40 | } 41 | 42 | #css:before { 43 | content: 'CSS'; 44 | } 45 | 46 | #js:before { 47 | content: 'JS'; 48 | } 49 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | /** 3 | * Components to our editor 4 | */ 5 | const htmlField = document.getElementById("html"); 6 | const cssField = document.getElementById("css"); 7 | const jsField = document.getElementById("js"); 8 | const preview = document.getElementById("preview"); 9 | 10 | /** 11 | * Method that gets the values from the textareas 12 | * and insert to an iframe 13 | */ 14 | function render() { 15 | let iframeComponent = preview.contentWindow.document; 16 | 17 | iframeComponent.open(); 18 | iframeComponent.writeln(` 19 | ${htmlField.innerText} 20 | 21 | `); 22 | iframeComponent.close(); 23 | } 24 | 25 | /** 26 | * Create listener to call the render 27 | * always after a keypress. 28 | */ 29 | function compile() { 30 | document.addEventListener('keyup', function() { 31 | render(); 32 | }); 33 | }; 34 | 35 | /** 36 | * Create the listener 37 | * and render the first values 38 | */ 39 | compile(); 40 | render(); 41 | })(); 42 | -------------------------------------------------------------------------------- /css/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.14.0 2 | http://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */ 3 | /** 4 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML 5 | * Based on https://github.com/chriskempson/tomorrow-theme 6 | * @author Rose Pritchard 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"], 11 | textarea { 12 | color: #ccc; 13 | background: none; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | 31 | } 32 | 33 | /* Code blocks */ 34 | pre[class*="language-"], 35 | textarea { 36 | padding: 1em; 37 | margin: .5em 0; 38 | overflow: auto; 39 | } 40 | 41 | :not(pre) > code[class*="language-"], 42 | pre[class*="language-"], 43 | textarea { 44 | background: #2d2d2d; 45 | } 46 | 47 | /* Inline code */ 48 | :not(pre) > code[class*="language-"] { 49 | padding: .1em; 50 | border-radius: .3em; 51 | white-space: normal; 52 | } 53 | 54 | .token.comment, 55 | .token.block-comment, 56 | .token.prolog, 57 | .token.doctype, 58 | .token.cdata { 59 | color: #999; 60 | } 61 | 62 | .token.punctuation { 63 | color: #ccc; 64 | } 65 | 66 | .token.tag, 67 | .token.attr-name, 68 | .token.namespace, 69 | .token.deleted { 70 | color: #e2777a; 71 | } 72 | 73 | .token.function-name { 74 | color: #6196cc; 75 | } 76 | 77 | .token.boolean, 78 | .token.number, 79 | .token.function { 80 | color: #f08d49; 81 | } 82 | 83 | .token.property, 84 | .token.class-name, 85 | .token.constant, 86 | .token.symbol { 87 | color: #f8c555; 88 | } 89 | 90 | .token.selector, 91 | .token.important, 92 | .token.atrule, 93 | .token.keyword, 94 | .token.builtin { 95 | color: #cc99cd; 96 | } 97 | 98 | .token.string, 99 | .token.char, 100 | .token.attr-value, 101 | .token.regex, 102 | .token.variable { 103 | color: #7ec699; 104 | } 105 | 106 | .token.operator, 107 | .token.entity, 108 | .token.url { 109 | color: #67cdcc; 110 | } 111 | 112 | .token.important, 113 | .token.bold { 114 | font-weight: bold; 115 | } 116 | .token.italic { 117 | font-style: italic; 118 | } 119 | 120 | .token.entity { 121 | cursor: help; 122 | } 123 | 124 | .token.inserted { 125 | color: green; 126 | } 127 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Simple Live Editor 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 39 | 40 | 58 | 59 | 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /js/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.14.0 2 | http://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-([\w-]+)\b/i,t=0,n=_self.Prism={manual:_self.Prism&&_self.Prism.manual,disableWorkerMessageHandler:_self.Prism&&_self.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof r?new r(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(w instanceof s)){if(m&&b!=t.length-1){h.lastIndex=k;var _=h.exec(e);if(!_)break;for(var j=_.index+(d?_[1].length:0),P=_.index+_[0].length,A=b,x=k,O=t.length;O>A&&(P>x||!t[A].type&&!t[A-1].greedy);++A)x+=t[A].length,j>=x&&(++b,k=x);if(t[b]instanceof s)continue;I=A-b,w=e.slice(k,x),_.index-=k}else{h.lastIndex=0;var _=h.exec(w),I=1}if(_){d&&(p=_[1]?_[1].length:0);var j=_.index+p,_=_[0].slice(p),P=j+_.length,N=w.slice(0,j),S=w.slice(P),C=[b,I];N&&(++b,k+=N.length,C.push(N));var E=new s(u,f?n.tokenize(_,f):_,y,_,m);if(C.push(E),S&&C.push(S),Array.prototype.splice.apply(t,C),1!=I&&n.matchGrammar(e,t,r,b,k,!0,u),i)break}else if(i)break}}}}},tokenize:function(e,t){var r=[e],a=t.rest;if(a){for(var l in a)t[l]=a[l];delete t.rest}return n.matchGrammar(e,r,t,0,0,!1),r},hooks:{all:{},add:function(e,t){var r=n.hooks.all;r[e]=r[e]||[],r[e].push(t)},run:function(e,t){var r=n.hooks.all[e];if(r&&r.length)for(var a,l=0;a=r[l++];)a(t)}}},r=n.Token=function(e,t,n,r,a){this.type=e,this.content=t,this.alias=n,this.length=0|(r||"").length,this.greedy=!!a};if(r.stringify=function(e,t,a){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return r.stringify(n,t,e)}).join("");var l={type:e.type,content:r.stringify(e.content,t,a),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:a};if(e.alias){var i="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run("wrap",l);var o=Object.keys(l.attributes).map(function(e){return e+'="'+(l.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+l.tag+' class="'+l.classes.join(" ")+'"'+(o?" "+o:"")+">"+l.content+""},!_self.document)return _self.addEventListener?(n.disableWorkerMessageHandler||_self.addEventListener("message",function(e){var t=JSON.parse(e.data),r=t.language,a=t.code,l=t.immediateClose;_self.postMessage(n.highlight(a,n.languages[r],r)),l&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return a&&(n.filename=a.src,n.manual||a.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/(^|[^\\])["']/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup; 5 | Prism.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(?:;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^{}\s][^{};]*?(?=\s*\{)/,string:{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.languages.css,Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/()[\s\S]*?(?=<\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:"language-css",greedy:!0}}),Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(?:true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|NaN|Infinity)\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,"function":/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/i,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[[^\]\r\n]+]|\\.|[^\/\\\[\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=\s*(?:function\b|(?:\([^()]*\)|[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/i,alias:"function"},constant:/\b[A-Z][A-Z\d_]*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${[^}]+}|[^\\`])*`/,greedy:!0,inside:{interpolation:{pattern:/\${[^}]+}/,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}}}),Prism.languages.javascript["template-string"].inside.interpolation.inside.rest=Prism.languages.javascript,Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\s\S]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript",greedy:!0}}),Prism.languages.js=Prism.languages.javascript; 8 | -------------------------------------------------------------------------------- /js/prism-highlight.js: -------------------------------------------------------------------------------- 1 | // Cross-broswer implementation of text ranges and selections 2 | // documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/ 3 | // Version: 2.6 4 | // Copyright (c) 2013 Daniel Wachsstock 5 | // MIT license: 6 | // Permission is hereby granted, free of charge, to any person 7 | // obtaining a copy of this software and associated documentation 8 | // files (the "Software"), to deal in the Software without 9 | // restriction, including without limitation the rights to use, 10 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the 12 | // Software is furnished to do so, subject to the following 13 | // conditions: 14 | 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | // OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | (function() { 28 | 29 | // a bit of weirdness with IE11: using 'focus' is flaky, even if I'm not bubbling, as far as I can tell. 30 | var focusEvent = 'onfocusin' in document.createElement('input') ? 'focusin' : 'focus'; 31 | 32 | // IE11 normalize is buggy (http://connect.microsoft.com/IE/feedback/details/809424/node-normalize-removes-text-if-dashes-are-present) 33 | var n = document.createElement('div'); 34 | n.appendChild(document.createTextNode('x-')); 35 | n.appendChild(document.createTextNode('x')); 36 | n.normalize(); 37 | var canNormalize = n.firstChild.length == 3; 38 | 39 | 40 | bililiteRange = function(el, debug) { 41 | var ret; 42 | if (debug) { 43 | ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser 44 | } else if (window.getSelection && el.setSelectionRange) { 45 | // Standards. Element is an input or textarea 46 | // note that some input elements do not allow selections 47 | try { 48 | el.selectionStart; // even getting the selection in such an element will throw 49 | ret = new InputRange(); 50 | } catch (e) { 51 | ret = new NothingRange(); 52 | } 53 | } else if (window.getSelection) { 54 | // Standards, with any other kind of element 55 | ret = new W3CRange(); 56 | } else if (document.selection) { 57 | // Internet Explorer 58 | ret = new IERange(); 59 | } else { 60 | // doesn't support selection 61 | ret = new NothingRange(); 62 | } 63 | ret._el = el; 64 | // determine parent document, as implemented by John McLear 65 | ret._doc = el.ownerDocument; 66 | ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow; 67 | ret._textProp = textProp(el); 68 | ret._bounds = [0, ret.length()]; 69 | // There's no way to detect whether a focus event happened as a result of a click (which should change the selection) 70 | // or as a result of a keyboard event (a tab in) or a script action (el.focus()). So we track it globally, which is a hack, and is likely to fail 71 | // in edge cases (right-clicks, drag-n-drop), and is vulnerable to a lower-down handler preventing bubbling. 72 | // I just don't know a better way. 73 | // I'll hack my event-listening code below, rather than create an entire new bilililiteRange, potentially before the DOM has loaded 74 | if (!('bililiteRangeMouseDown' in ret._doc)) { 75 | var _doc = { 76 | _el: ret._doc 77 | }; 78 | ret._doc.bililiteRangeMouseDown = false; 79 | bililiteRange.fn.listen.call(_doc, 'mousedown', function() { 80 | ret._doc.bililiteRangeMouseDown = true; 81 | }); 82 | bililiteRange.fn.listen.call(_doc, 'mouseup', function() { 83 | ret._doc.bililiteRangeMouseDown = false; 84 | }); 85 | } 86 | // note that bililiteRangeSelection is an array, which means that copying it only copies the address, which points to the original. 87 | // make sure that we never let it (always do return [bililiteRangeSelection[0], bililiteRangeSelection[1]]), which means never returning 88 | // this._bounds directly 89 | if (!('bililiteRangeSelection' in el)) { 90 | // start tracking the selection 91 | function trackSelection(evt) { 92 | if (evt && evt.which == 9) { 93 | // do tabs my way, by restoring the selection 94 | // there's a flash of the browser's selection, but I don't see a way of avoiding that 95 | ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); 96 | } else { 97 | el.bililiteRangeSelection = ret._nativeSelection(); 98 | } 99 | } 100 | trackSelection(); 101 | // only IE does this right and allows us to grab the selection before blurring 102 | if ('onbeforedeactivate' in el) { 103 | ret.listen('beforedeactivate', trackSelection); 104 | } else { 105 | // with standards-based browsers, have to listen for every user interaction 106 | ret.listen('mouseup', trackSelection).listen('keyup', trackSelection); 107 | } 108 | ret.listen(focusEvent, function() { 109 | // restore the correct selection when the element comes into focus (mouse clicks change the position of the selection) 110 | // Note that Firefox will not fire the focus event until the window/tab is active even if el.focus() is called 111 | // https://bugzilla.mozilla.org/show_bug.cgi?id=566671 112 | if (!ret._doc.bililiteRangeMouseDown) { 113 | ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); 114 | } 115 | }); 116 | } 117 | if (!('oninput' in el)) { 118 | // give IE8 a chance. Note that this still fails in IE11, which has has oninput on contenteditable elements but does not 119 | // dispatch input events. See http://connect.microsoft.com/IE/feedback/details/794285/ie10-11-input-event-does-not-fire-on-div-with-contenteditable-set 120 | // TODO: revisit this when I have IE11 running on my development machine 121 | var inputhack = function() { 122 | ret.dispatch({ 123 | type: 'input' 124 | }) 125 | }; 126 | ret.listen('keyup', inputhack); 127 | ret.listen('cut', inputhack); 128 | ret.listen('paste', inputhack); 129 | ret.listen('drop', inputhack); 130 | el.oninput = 'patched'; 131 | } 132 | return ret; 133 | } 134 | 135 | function textProp(el) { 136 | // returns the property that contains the text of the element 137 | // note that for elements the text attribute represents the obsolete text color, not the textContent. 138 | // we document that these routines do not work for elements so that should not be relevant 139 | if (typeof el.value != 'undefined') return 'value'; 140 | if (typeof el.text != 'undefined') return 'text'; 141 | if (typeof el.textContent != 'undefined') return 'textContent'; 142 | return 'innerText'; 143 | } 144 | 145 | // base class 146 | function Range() {} 147 | Range.prototype = { 148 | length: function() { 149 | return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness 150 | }, 151 | bounds: function(s) { 152 | if (bililiteRange.bounds[s]) { 153 | this._bounds = bililiteRange.bounds[s].apply(this); 154 | } else if (s) { 155 | this._bounds = s; // don't do error checking now; things may change at a moment's notice 156 | } else { 157 | var b = [ 158 | Math.max(0, Math.min(this.length(), this._bounds[0])), 159 | Math.max(0, Math.min(this.length(), this._bounds[1])) 160 | ]; 161 | b[1] = Math.max(b[0], b[1]); 162 | return b; // need to constrain it to fit 163 | } 164 | return this; // allow for chaining 165 | }, 166 | select: function() { 167 | var b = this._el.bililiteRangeSelection = this.bounds(); 168 | if (this._el == document.activeElement) { 169 | // only actually select if this element is active! 170 | this._nativeSelect(this._nativeRange(b)); 171 | } 172 | this.dispatch({ 173 | type: 'select' 174 | }); 175 | return this; // allow for chaining 176 | }, 177 | text: function(text, select) { 178 | if (arguments.length) { 179 | var bounds = this.bounds(), 180 | el = this._el; 181 | // signal the input per DOM 3 input events, http://www.w3.org/TR/DOM-Level-3-Events/#h4_events-inputevents 182 | // we add another field, bounds, which are the bounds of the original text before being changed. 183 | this.dispatch({ 184 | type: 'beforeinput', 185 | data: text, 186 | bounds: bounds 187 | }); 188 | this._nativeSetText(text, this._nativeRange(bounds)); 189 | if (select == 'start') { 190 | this.bounds([bounds[0], bounds[0]]); 191 | } else if (select == 'end') { 192 | this.bounds([bounds[0] + text.length, bounds[0] + text.length]); 193 | } else if (select == 'all') { 194 | this.bounds([bounds[0], bounds[0] + text.length]); 195 | } 196 | this.dispatch({ 197 | type: 'input', 198 | data: text, 199 | bounds: bounds 200 | }); 201 | return this; // allow for chaining 202 | } else { 203 | return this._nativeGetText(this._nativeRange(this.bounds())).replace(/\r/g, ''); // need to correct for IE's CrLf weirdness 204 | } 205 | }, 206 | insertEOL: function() { 207 | this._nativeEOL(); 208 | this._bounds = [this._bounds[0] + 1, this._bounds[0] + 1]; // move past the EOL marker 209 | return this; 210 | }, 211 | sendkeys: function(text) { 212 | var self = this; 213 | this.data().sendkeysOriginalText = this.text(); 214 | this.data().sendkeysBounds = undefined; 215 | 216 | function simplechar(rng, c) { 217 | if (/^{[^}]*}$/.test(c)) c = c.slice(1, -1); // deal with unknown {key}s 218 | for (var i = 0; i < c.length; ++i) { 219 | var x = c.charCodeAt(i); 220 | rng.dispatch({ 221 | type: 'keypress', 222 | keyCode: x, 223 | which: x, 224 | charCode: x 225 | }); 226 | } 227 | rng.text(c, 'end'); 228 | } 229 | text.replace(/{[^}]*}|[^{]+|{/g, function(part) { 230 | (bililiteRange.sendkeys[part] || simplechar)(self, part, simplechar); 231 | }); 232 | this.bounds(this.data().sendkeysBounds); 233 | this.dispatch({ 234 | type: 'sendkeys', 235 | which: text 236 | }); 237 | return this; 238 | }, 239 | top: function() { 240 | return this._nativeTop(this._nativeRange(this.bounds())); 241 | }, 242 | scrollIntoView: function(scroller) { 243 | var top = this.top(); 244 | // scroll into position if necessary 245 | if (this._el.scrollTop > top || this._el.scrollTop + this._el.clientHeight < top) { 246 | if (scroller) { 247 | scroller.call(this._el, top); 248 | } else { 249 | this._el.scrollTop = top; 250 | } 251 | } 252 | return this; 253 | }, 254 | wrap: function(n) { 255 | this._nativeWrap(n, this._nativeRange(this.bounds())); 256 | return this; 257 | }, 258 | selection: function(text) { 259 | if (arguments.length) { 260 | return this.bounds('selection').text(text, 'end').select(); 261 | } else { 262 | return this.bounds('selection').text(); 263 | } 264 | }, 265 | clone: function() { 266 | return bililiteRange(this._el).bounds(this.bounds()); 267 | }, 268 | all: function(text) { 269 | if (arguments.length) { 270 | this.dispatch({ 271 | type: 'beforeinput', 272 | data: text 273 | }); 274 | this._el[this._textProp] = text; 275 | this.dispatch({ 276 | type: 'input', 277 | data: text 278 | }); 279 | return this; 280 | } else { 281 | return this._el[this._textProp].replace(/\r/g, ''); // need to correct for IE's CrLf weirdness 282 | } 283 | }, 284 | element: function() { 285 | return this._el 286 | }, 287 | // includes a quickie polyfill for CustomEvent for IE that isn't perfect but works for me 288 | // IE10 allows custom events but not "new CustomEvent"; have to do it the old-fashioned way 289 | dispatch: function(opts) { 290 | opts = opts || {}; 291 | var event = document.createEvent ? document.createEvent('CustomEvent') : this._doc.createEventObject(); 292 | event.initCustomEvent && event.initCustomEvent(opts.type, !!opts.bubbles, !!opts.cancelable, opts.detail); 293 | for (var key in opts) event[key] = opts[key]; 294 | // dispatch event asynchronously (in the sense of on the next turn of the event loop; still should be fired in order of dispatch 295 | var el = this._el; 296 | setTimeout(function() { 297 | try { 298 | el.dispatchEvent ? el.dispatchEvent(event) : el.fireEvent("on" + opts.type, document.createEventObject()); 299 | } catch (e) { 300 | // IE8 will not let me fire custom events at all. Call them directly 301 | var listeners = el['listen' + opts.type]; 302 | if (listeners) 303 | for (var i = 0; i < listeners.length; ++i) { 304 | listeners[i].call(el, event); 305 | } 306 | } 307 | }, 0); 308 | return this; 309 | }, 310 | listen: function(type, func) { 311 | var el = this._el; 312 | if (el.addEventListener) { 313 | el.addEventListener(type, func); 314 | } else { 315 | el.attachEvent("on" + type, func); 316 | // IE8 can't even handle custom events created with createEventObject (though it permits attachEvent), so we have to make our own 317 | var listeners = el['listen' + type] = el['listen' + type] || []; 318 | listeners.push(func); 319 | } 320 | return this; 321 | }, 322 | dontlisten: function(type, func) { 323 | var el = this._el; 324 | if (el.removeEventListener) { 325 | el.removeEventListener(type, func); 326 | } else try { 327 | el.detachEvent("on" + type, func); 328 | } catch (e) { 329 | var listeners = el['listen' + type]; 330 | if (listeners) 331 | for (var i = 0; i < listeners.length; ++i) { 332 | if (listeners[i] === func) listeners[i] = function() {}; // replace with a noop 333 | } 334 | } 335 | return this; 336 | } 337 | }; 338 | 339 | // allow extensions ala jQuery 340 | bililiteRange.fn = Range.prototype; // to allow monkey patching 341 | bililiteRange.extend = function(fns) { 342 | for (fn in fns) Range.prototype[fn] = fns[fn]; 343 | }; 344 | 345 | //bounds functions 346 | bililiteRange.bounds = { 347 | all: function() { 348 | return [0, this.length()] 349 | }, 350 | start: function() { 351 | return [0, 0] 352 | }, 353 | end: function() { 354 | return [this.length(), this.length()] 355 | }, 356 | selection: function() { 357 | if (this._el == document.activeElement) { 358 | this.bounds('all'); // first select the whole thing for constraining 359 | return this._nativeSelection(); 360 | } else { 361 | return this._el.bililiteRangeSelection; 362 | } 363 | } 364 | }; 365 | 366 | // sendkeys functions 367 | bililiteRange.sendkeys = { 368 | '{enter}': function(rng) { 369 | var x = '\n'.charCodeAt(0); 370 | rng.dispatch({ 371 | type: 'keypress', 372 | keyCode: x, 373 | which: x, 374 | charCode: x 375 | }); 376 | rng.insertEOL(); 377 | }, 378 | '{tab}': function(rng, c, simplechar) { 379 | simplechar(rng, '\t'); // useful for inserting what would be whitespace 380 | }, 381 | '{newline}': function(rng, c, simplechar) { 382 | simplechar(rng, '\n'); // useful for inserting what would be whitespace (and if I don't want to use insertEOL, which does some fancy things) 383 | }, 384 | '{backspace}': function(rng) { 385 | var b = rng.bounds(); 386 | if (b[0] == b[1]) rng.bounds([b[0] - 1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character 387 | rng.text('', 'end'); // delete the characters and update the selection 388 | }, 389 | '{del}': function(rng) { 390 | var b = rng.bounds(); 391 | if (b[0] == b[1]) rng.bounds([b[0], b[0] + 1]); // no characters selected; it's just an insertion point. Remove the next character 392 | rng.text('', 'end'); // delete the characters and update the selection 393 | }, 394 | '{rightarrow}': function(rng) { 395 | var b = rng.bounds(); 396 | if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right 397 | rng.bounds([b[1], b[1]]); 398 | }, 399 | '{leftarrow}': function(rng) { 400 | var b = rng.bounds(); 401 | if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left 402 | rng.bounds([b[0], b[0]]); 403 | }, 404 | '{selectall}': function(rng) { 405 | rng.bounds('all'); 406 | }, 407 | '{selection}': function(rng) { 408 | // insert the characters without the sendkeys processing 409 | var s = rng.data().sendkeysOriginalText; 410 | for (var i = 0; i < s.length; ++i) { 411 | var x = s.charCodeAt(i); 412 | rng.dispatch({ 413 | type: 'keypress', 414 | keyCode: x, 415 | which: x, 416 | charCode: x 417 | }); 418 | } 419 | rng.text(s, 'end'); 420 | }, 421 | '{mark}': function(rng) { 422 | rng.data().sendkeysBounds = rng.bounds(); 423 | } 424 | }; 425 | // Synonyms from the proposed DOM standard (http://www.w3.org/TR/DOM-Level-3-Events-key/) 426 | bililiteRange.sendkeys['{Enter}'] = bililiteRange.sendkeys['{enter}']; 427 | bililiteRange.sendkeys['{Tab}'] = bililiteRange.sendkeys['{tab}']; 428 | bililiteRange.sendkeys['{Backspace}'] = bililiteRange.sendkeys['{backspace}']; 429 | bililiteRange.sendkeys['{Delete}'] = bililiteRange.sendkeys['{del}']; 430 | bililiteRange.sendkeys['{ArrowRight}'] = bililiteRange.sendkeys['{rightarrow}']; 431 | bililiteRange.sendkeys['{ArrowLeft}'] = bililiteRange.sendkeys['{leftarrow}']; 432 | 433 | function IERange() {} 434 | IERange.prototype = new Range(); 435 | IERange.prototype._nativeRange = function(bounds) { 436 | var rng; 437 | if (this._el.tagName == 'INPUT') { 438 | // IE 8 is very inconsistent; textareas have createTextRange but it doesn't work 439 | rng = this._el.createTextRange(); 440 | } else { 441 | rng = this._doc.body.createTextRange(); 442 | rng.moveToElementText(this._el); 443 | } 444 | if (bounds) { 445 | if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds 446 | if (bounds[0] > this.length()) bounds[0] = this.length(); 447 | if (bounds[1] < rng.text.replace(/\r/g, '').length) { // correct for IE's CrLf weirdness 448 | // block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range 449 | rng.moveEnd('character', -1); 450 | rng.moveEnd('character', bounds[1] - rng.text.replace(/\r/g, '').length); 451 | } 452 | if (bounds[0] > 0) rng.moveStart('character', bounds[0]); 453 | } 454 | return rng; 455 | }; 456 | IERange.prototype._nativeSelect = function(rng) { 457 | rng.select(); 458 | }; 459 | IERange.prototype._nativeSelection = function() { 460 | // returns [start, end] for the selection constrained to be in element 461 | var rng = this._nativeRange(); // range of the element to constrain to 462 | var len = this.length(); 463 | var sel = this._doc.selection.createRange(); 464 | try { 465 | return [ 466 | iestart(sel, rng), 467 | ieend(sel, rng) 468 | ]; 469 | } catch (e) { 470 | // TODO: determine if this is still necessary, since we only call _nativeSelection if _el is active 471 | // IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess 472 | return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0, 0] : [len, len]; 473 | } 474 | }; 475 | IERange.prototype._nativeGetText = function(rng) { 476 | return rng.text; 477 | }; 478 | IERange.prototype._nativeSetText = function(text, rng) { 479 | rng.text = text; 480 | }; 481 | IERange.prototype._nativeEOL = function() { 482 | if ('value' in this._el) { 483 | this.text('\n'); // for input and textarea, insert it straight 484 | } else { 485 | this._nativeRange(this.bounds()).pasteHTML('\n
'); 486 | } 487 | }; 488 | IERange.prototype._nativeTop = function(rng) { 489 | var startrng = this._nativeRange([0, 0]); 490 | return rng.boundingTop - startrng.boundingTop; 491 | } 492 | IERange.prototype._nativeWrap = function(n, rng) { 493 | // hacky to use string manipulation but I don't see another way to do it. 494 | var div = document.createElement('div'); 495 | div.appendChild(n); 496 | // insert the existing range HTML after the first tag 497 | var html = div.innerHTML.replace('><', '>' + rng.htmlText + '<'); 498 | rng.pasteHTML(html); 499 | }; 500 | 501 | // IE internals 502 | function iestart(rng, constraint) { 503 | // returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after 504 | var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness 505 | if (rng.compareEndPoints('StartToStart', constraint) <= 0) return 0; // at or before the beginning 506 | if (rng.compareEndPoints('StartToEnd', constraint) >= 0) return len; 507 | for (var i = 0; rng.compareEndPoints('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1)); 508 | return i; 509 | } 510 | 511 | function ieend(rng, constraint) { 512 | // returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after 513 | var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness 514 | if (rng.compareEndPoints('EndToEnd', constraint) >= 0) return len; // at or after the end 515 | if (rng.compareEndPoints('EndToStart', constraint) <= 0) return 0; 516 | for (var i = 0; rng.compareEndPoints('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1)); 517 | return i; 518 | } 519 | 520 | // an input element in a standards document. "Native Range" is just the bounds array 521 | function InputRange() {} 522 | InputRange.prototype = new Range(); 523 | InputRange.prototype._nativeRange = function(bounds) { 524 | return bounds || [0, this.length()]; 525 | }; 526 | InputRange.prototype._nativeSelect = function(rng) { 527 | this._el.setSelectionRange(rng[0], rng[1]); 528 | }; 529 | InputRange.prototype._nativeSelection = function() { 530 | return [this._el.selectionStart, this._el.selectionEnd]; 531 | }; 532 | InputRange.prototype._nativeGetText = function(rng) { 533 | return this._el.value.substring(rng[0], rng[1]); 534 | }; 535 | InputRange.prototype._nativeSetText = function(text, rng) { 536 | var val = this._el.value; 537 | this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]); 538 | }; 539 | InputRange.prototype._nativeEOL = function() { 540 | this.text('\n'); 541 | }; 542 | InputRange.prototype._nativeTop = function(rng) { 543 | // I can't remember where I found this clever hack to find the location of text in a text area 544 | var clone = this._el.cloneNode(true); 545 | clone.style.visibility = 'hidden'; 546 | clone.style.position = 'absolute'; 547 | this._el.parentNode.insertBefore(clone, this._el); 548 | clone.style.height = '1px'; 549 | clone.value = this._el.value.slice(0, rng[0]); 550 | var top = clone.scrollHeight; 551 | // this gives the bottom of the text, so we have to subtract the height of a single line 552 | clone.value = 'X'; 553 | top -= clone.scrollHeight; 554 | clone.parentNode.removeChild(clone); 555 | return top; 556 | } 557 | InputRange.prototype._nativeWrap = function() { 558 | throw new Error("Cannot wrap in a text element") 559 | }; 560 | 561 | function W3CRange() {} 562 | W3CRange.prototype = new Range(); 563 | W3CRange.prototype._nativeRange = function(bounds) { 564 | var rng = this._doc.createRange(); 565 | rng.selectNodeContents(this._el); 566 | if (bounds) { 567 | w3cmoveBoundary(rng, bounds[0], true, this._el); 568 | rng.collapse(true); 569 | w3cmoveBoundary(rng, bounds[1] - bounds[0], false, this._el); 570 | } 571 | return rng; 572 | }; 573 | W3CRange.prototype._nativeSelect = function(rng) { 574 | this._win.getSelection().removeAllRanges(); 575 | this._win.getSelection().addRange(rng); 576 | }; 577 | W3CRange.prototype._nativeSelection = function() { 578 | // returns [start, end] for the selection constrained to be in element 579 | var rng = this._nativeRange(); // range of the element to constrain to 580 | if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end 581 | var sel = this._win.getSelection().getRangeAt(0); 582 | return [ 583 | w3cstart(sel, rng), 584 | w3cend(sel, rng) 585 | ]; 586 | } 587 | W3CRange.prototype._nativeGetText = function(rng) { 588 | return String.prototype.slice.apply(this._el.textContent, this.bounds()); 589 | // return rng.toString(); // this fails in IE11 since it insists on inserting \r's before \n's in Ranges. node.textContent works as expected 590 | }; 591 | W3CRange.prototype._nativeSetText = function(text, rng) { 592 | rng.deleteContents(); 593 | rng.insertNode(this._doc.createTextNode(text)); 594 | if (canNormalize) this._el.normalize(); // merge the text with the surrounding text 595 | }; 596 | W3CRange.prototype._nativeEOL = function() { 597 | var rng = this._nativeRange(this.bounds()); 598 | rng.deleteContents(); 599 | var br = this._doc.createElement('br'); 600 | br.setAttribute('_moz_dirty', ''); // for Firefox 601 | rng.insertNode(br); 602 | rng.insertNode(this._doc.createTextNode('\n')); 603 | rng.collapse(false); 604 | }; 605 | W3CRange.prototype._nativeTop = function(rng) { 606 | if (this.length == 0) return 0; // no text, no scrolling 607 | if (rng.toString() == '') { 608 | var textnode = this._doc.createTextNode('X'); 609 | rng.insertNode(textnode); 610 | } 611 | var startrng = this._nativeRange([0, 1]); 612 | var top = rng.getBoundingClientRect().top - startrng.getBoundingClientRect().top; 613 | if (textnode) textnode.parentNode.removeChild(textnode); 614 | return top; 615 | } 616 | W3CRange.prototype._nativeWrap = function(n, rng) { 617 | rng.surroundContents(n); 618 | }; 619 | 620 | // W3C internals 621 | function nextnode(node, root) { 622 | // in-order traversal 623 | // we've already visited node, so get kids then siblings 624 | if (node.firstChild) return node.firstChild; 625 | if (node.nextSibling) return node.nextSibling; 626 | if (node === root) return null; 627 | while (node.parentNode) { 628 | // get uncles 629 | node = node.parentNode; 630 | if (node == root) return null; 631 | if (node.nextSibling) return node.nextSibling; 632 | } 633 | return null; 634 | } 635 | 636 | function w3cmoveBoundary(rng, n, bStart, el) { 637 | // move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only! 638 | // if the start is moved after the end, then an exception is raised 639 | if (n <= 0) return; 640 | var node = rng[bStart ? 'startContainer' : 'endContainer']; 641 | if (node.nodeType == 3) { 642 | // we may be starting somewhere into the text 643 | n += rng[bStart ? 'startOffset' : 'endOffset']; 644 | } 645 | while (node) { 646 | if (node.nodeType == 3) { 647 | var length = node.nodeValue.length; 648 | if (n <= length) { 649 | rng[bStart ? 'setStart' : 'setEnd'](node, n); 650 | // special case: if we end next to a
, include that node. 651 | if (n == length) { 652 | // skip past zero-length text nodes 653 | for (var next = nextnode(node, el); next && next.nodeType == 3 && next.nodeValue.length == 0; next = nextnode(next, el)) { 654 | rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); 655 | } 656 | if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); 657 | } 658 | return; 659 | } else { 660 | rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one 661 | n -= length; // and eat these characters 662 | } 663 | } 664 | node = nextnode(node, el); 665 | } 666 | } 667 | var START_TO_START = 0; // from the w3c definitions 668 | var START_TO_END = 1; 669 | var END_TO_END = 2; 670 | var END_TO_START = 3; 671 | // from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange) 672 | // -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange. 673 | // * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range. 674 | // * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range. 675 | // * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range. 676 | // * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range. 677 | function w3cstart(rng, constraint) { 678 | if (rng.compareBoundaryPoints(START_TO_START, constraint) <= 0) return 0; // at or before the beginning 679 | if (rng.compareBoundaryPoints(END_TO_START, constraint) >= 0) return constraint.toString().length; 680 | rng = rng.cloneRange(); // don't change the original 681 | rng.setEnd(constraint.endContainer, constraint.endOffset); // they now end at the same place 682 | return constraint.toString().replace(/\r/g, '').length - rng.toString().replace(/\r/g, '').length; 683 | } 684 | 685 | function w3cend(rng, constraint) { 686 | if (rng.compareBoundaryPoints(END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end 687 | if (rng.compareBoundaryPoints(START_TO_END, constraint) <= 0) return 0; 688 | rng = rng.cloneRange(); // don't change the original 689 | rng.setStart(constraint.startContainer, constraint.startOffset); // they now start at the same place 690 | return rng.toString().replace(/\r/g, '').length; 691 | } 692 | 693 | function NothingRange() {} 694 | NothingRange.prototype = new Range(); 695 | NothingRange.prototype._nativeRange = function(bounds) { 696 | return bounds || [0, this.length()]; 697 | }; 698 | NothingRange.prototype._nativeSelect = function(rng) { // do nothing 699 | }; 700 | NothingRange.prototype._nativeSelection = function() { 701 | return [0, 0]; 702 | }; 703 | NothingRange.prototype._nativeGetText = function(rng) { 704 | return this._el[this._textProp].substring(rng[0], rng[1]); 705 | }; 706 | NothingRange.prototype._nativeSetText = function(text, rng) { 707 | var val = this._el[this._textProp]; 708 | this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]); 709 | }; 710 | NothingRange.prototype._nativeEOL = function() { 711 | this.text('\n'); 712 | }; 713 | NothingRange.prototype._nativeTop = function() { 714 | return 0; 715 | }; 716 | NothingRange.prototype._nativeWrap = function() { 717 | throw new Error("Wrapping not implemented") 718 | }; 719 | 720 | 721 | // data for elements, similar to jQuery data, but allows for monitoring with custom events 722 | var data = []; // to avoid attaching javascript objects to DOM elements, to avoid memory leaks 723 | bililiteRange.fn.data = function() { 724 | var index = this.element().bililiteRangeData; 725 | if (index == undefined) { 726 | index = this.element().bililiteRangeData = data.length; 727 | data[index] = new Data(this); 728 | } 729 | return data[index]; 730 | } 731 | try { 732 | Object.defineProperty({}, 'foo', {}); // IE8 will throw an error 733 | var Data = function(rng) { 734 | // we use JSON.stringify to display the data values. To make some of those non-enumerable, we have to use properties 735 | Object.defineProperty(this, 'values', { 736 | value: {} 737 | }); 738 | Object.defineProperty(this, 'sourceRange', { 739 | value: rng 740 | }); 741 | Object.defineProperty(this, 'toJSON', { 742 | value: function() { 743 | var ret = {}; 744 | for (var i in Data.prototype) 745 | if (i in this.values) ret[i] = this.values[i]; 746 | return ret; 747 | } 748 | }); 749 | // to display all the properties (not just those changed), use JSON.stringify(state.all) 750 | Object.defineProperty(this, 'all', { 751 | get: function() { 752 | var ret = {}; 753 | for (var i in Data.prototype) ret[i] = this[i]; 754 | return ret; 755 | } 756 | }); 757 | } 758 | 759 | Data.prototype = {}; 760 | Object.defineProperty(Data.prototype, 'values', { 761 | value: {} 762 | }); 763 | Object.defineProperty(Data.prototype, 'monitored', { 764 | value: {} 765 | }); 766 | 767 | bililiteRange.data = function(name, newdesc) { 768 | newdesc = newdesc || {}; 769 | var desc = Object.getOwnPropertyDescriptor(Data.prototype, name) || {}; 770 | if ('enumerable' in newdesc) desc.enumerable = !!newdesc.enumerable; 771 | if (!('enumerable' in desc)) desc.enumerable = true; // default 772 | if ('value' in newdesc) Data.prototype.values[name] = newdesc.value; 773 | if ('monitored' in newdesc) Data.prototype.monitored[name] = newdesc.monitored; 774 | desc.configurable = true; 775 | desc.get = function() { 776 | if (name in this.values) return this.values[name]; 777 | return Data.prototype.values[name]; 778 | }; 779 | desc.set = function(value) { 780 | this.values[name] = value; 781 | if (Data.prototype.monitored[name]) this.sourceRange.dispatch({ 782 | type: 'bililiteRangeData', 783 | bubbles: true, 784 | detail: { 785 | name: name, 786 | value: value 787 | } 788 | }); 789 | } 790 | Object.defineProperty(Data.prototype, name, desc); 791 | } 792 | } catch (err) { 793 | // if we can't set object property properties, just use old-fashioned properties 794 | Data = function(rng) { 795 | this.sourceRange = rng 796 | }; 797 | Data.prototype = {}; 798 | bililiteRange.data = function(name, newdesc) { 799 | if ('value' in newdesc) Data.prototype[name] = newdesc.value; 800 | } 801 | } 802 | 803 | })(); 804 | 805 | // Polyfill for forEach, per Mozilla documentation. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill 806 | if (!Array.prototype.forEach) { 807 | Array.prototype.forEach = function(fun /*, thisArg */ ) { 808 | "use strict"; 809 | 810 | if (this === void 0 || this === null) 811 | throw new TypeError(); 812 | 813 | var t = Object(this); 814 | var len = t.length >>> 0; 815 | if (typeof fun !== "function") 816 | throw new TypeError(); 817 | 818 | var thisArg = arguments.length >= 2 ? arguments[1] : void 0; 819 | for (var i = 0; i < len; i++) { 820 | if (i in t) 821 | fun.call(thisArg, t[i], i, t); 822 | } 823 | }; 824 | } 825 | 826 | 827 | bililiteRange.fancyText = function(editor, highlighter, threshold) { 828 | if (editor.tagName.toLowerCase() == 'textarea') { 829 | // turn the editor into an editable
, since that is what Prism works on
830 |     var replacement = document.createElement('pre');
831 |     replacement.setAttribute('contenteditable', true);
832 |     [].forEach.call(editor.attributes, function(attr) {
833 |       replacement.setAttribute(attr.name, attr.value);
834 |     });
835 |     replacement.textContent = editor.value;
836 |     editor.parentNode.replaceChild(replacement, editor);
837 |     editor = replacement;
838 |   }
839 |   // for large texts, it can be too slow to run the highlighter on every input event.
840 |   // use the code from http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
841 |   // to limit it to once every threshold milliseconds
842 |   function debounce(func, threshold) {
843 |     if (!threshold) return func; // no debouncing
844 |     var timeout;
845 |     return function() {
846 |       var self = this,
847 |         args = arguments;
848 |       clearTimeout(timeout);
849 |       timeout = setTimeout(function() {
850 |         func.apply(self, args);
851 |       }, threshold);
852 |     };
853 |   }
854 | 
855 |   var rng = bililiteRange(editor);
856 | 
857 |   function highlight() {
858 |     rng.bounds('selection');
859 |     // handle what Lea Verou calls "Dirty fix to #2"--seems to be Chrome issue with missing newlines
860 |     // from https://github.com/LeaVerou/dabblet/issues/2
861 |     if (!/\n$/.test(editor.textContent)) editor.textContent += '\n';
862 |     highlighter(editor);
863 |     rng.select();
864 |   }
865 |   if (highlighter) {
866 |     highlight();
867 |     rng.listen('input', debounce(highlight, threshold));
868 |   }
869 |   rng.listen('paste', function(evt) {
870 |     // Firefox changes newlines to br's on paste!
871 |     // Chrome pastes cr's! Nothing is easy.
872 |     rng.bounds('selection').
873 |     text(evt.clipboardData.getData("text/plain").replace(/\r/g, ''), 'end').
874 |     select();
875 |     evt.preventDefault();
876 |   });
877 |   rng.listen('keydown', function(evt) {
878 |     // avoid the fancy element-creation with newlines
879 |     if (evt.keyCode == 13) {
880 |       rng.bounds('selection').text('\n', 'end', rng.data().autoindent).select();
881 |       evt.preventDefault();
882 |     }
883 |   });
884 | 
885 |   return editor;
886 | };
887 | 
888 | Prism.hooks.add('before-insert', function(env){
889 | 	var el = env.element;
890 | 	if (!(el.hasAttribute('data-linenumber'))) return;
891 | 	var startNumber = parseInt(el.getAttribute('data-linenumber'))||0;
892 | 	el.style.counterReset = getComputedStyle(el).counterReset.replace(/-?\d+/, startNumber-1);
893 | 	var line = '', endline = '';
894 | 	// some highlighting puts newlines inside the span, which messes up the code below. Fix that. Newlines that are actually inside the span will still
895 | 	// cause problems.
896 | 	var code = env.highlightedCode.replace(/\n<\/span>/g, '\n');
897 | 	env.highlightedCode = line + code.split('\n').join(endline+'\n'+line) + endline;
898 | });
899 | 
900 | var editorHTML = document.querySelector('.language-html');
901 | var editorCSS = document.querySelector('.language-css');
902 | var editorJS = document.querySelector('.language-js');
903 | 
904 | editorHTML = bililiteRange.fancyText(editorHTML, Prism.highlightElement);
905 | editorCSS = bililiteRange.fancyText(editorCSS, Prism.highlightElement);
906 | editorJS = bililiteRange.fancyText(editorJS, Prism.highlightElement);
907 | 


--------------------------------------------------------------------------------