editable.js

From disqus.com, 1 Month ago, written in JavaScript, viewed 3 times. This paste is a reply to Re: Re: time.js from disqus.com - view diff
URL https://pastebin.freepbx.org/view/dd524687 Embed
Download Paste or View Raw
  1. /* eslint-disable valid-jsdoc */
  2. /**
  3.  * editable.js - a xbrowser library for contentEditable
  4.  *
  5.  * https://github.com/benvinegar/editable.js
  6.  *
  7.  * MIT License
  8.  */
  9. define('core/editable',[],function () {
  10.     'use strict';
  11.  
  12.     var document = window.document;
  13.     var character = 'character';
  14.     var nbspRe = new RegExp(String.fromCharCode(160), 'gi');
  15.     var blockElemList = 'h1 h2 h3 h4 h5 h6 p pre blockquote address ul ol dir menu li dl div form'.split(' ');
  16.     var blockElems = {};
  17.     var i = 0;
  18.  
  19.     // build a list of block level elements
  20.     for (i = 0; i < blockElemList.length; i++)
  21.         blockElems[blockElemList[i]] = true;
  22.  
  23.     function normalizeSpace(str) {
  24.         return str.replace(nbspRe, ' ');
  25.     }
  26.  
  27.     /**
  28.      * Helper function to recursively aggregate all text from a set
  29.      * of HTML nodes
  30.      */
  31.     function getTextHelper(nodes, nodeCallback, blockJoin) {
  32.         var text = '';
  33.         var blocks = [];
  34.         var name, node, tmp, i;
  35.  
  36.         if (typeof blockJoin !== 'string')
  37.             blockJoin = '\n\n';
  38.  
  39.         for (i = 0; i < nodes.length; ++i) {
  40.             node = nodes[i];
  41.             name = node.nodeName.toLowerCase();
  42.  
  43.             if (node.nodeType === 1) { // html node
  44.                 tmp = nodeCallback && nodeCallback(node);
  45.  
  46.                 if (tmp) {
  47.                     text += tmp;
  48.                 } else if (blockElems.hasOwnProperty(name)) {
  49.                     // Inside a block-level element, ignore any subsequent block-level
  50.                     // elements (i.e. nested divs or paragraphs)
  51.                     if (text)
  52.                         blocks.push(text);
  53.                     text = getTextHelper(node.childNodes, nodeCallback, blockJoin);
  54.                 } else if (name === 'br') {
  55.                     // Always convert breaking tags to newlines
  56.                     text += '\n';
  57.                 } else {
  58.                     // Some other (inline) element; recur inside to extract text
  59.                     text += getTextHelper(node.childNodes, nodeCallback, blockJoin);
  60.                 }
  61.             }
  62.             else if (node.nodeType === 3) { // text node
  63.                 text += normalizeSpace(node.nodeValue);
  64.             }
  65.         }
  66.         // Block elements should get two new lines normally
  67.         blocks.push(text);
  68.         return blocks.join(blockJoin);
  69.     }
  70.  
  71.     var Editable = function (elem, emulate, overrides) {
  72.         var self = this;
  73.  
  74.         if (!elem || !elem.contentEditable)
  75.             throw new Error('First argument must be contentEditable');
  76.  
  77.         this.elem = elem;
  78.         this.emulateTextarea = elem.getAttribute('plaintext-only') || emulate;
  79.  
  80.         if (this.emulateTextarea) {
  81.             this.pasteHandler = function (evt) {
  82.                 var pasted = evt && evt.clipboardData || window.clipboardData;
  83.                 // check that the pasted content is text
  84.                 // TODO: Support directly pasted images
  85.                 if (pasted && !pasted.getData('text')) {
  86.                     evt.preventDefault();
  87.                     evt.stopPropagation();
  88.                 }
  89.                 var func = self.plainTextReformat;
  90.                 var later = function () {
  91.                     func.timeout = null;
  92.                     func.call(self);
  93.                 };
  94.                 if (func.timeout)
  95.                     clearTimeout(func.timeout);
  96.                 func.timeout = setTimeout(later, 0);
  97.             };
  98.  
  99.             elem.addEventListener('paste', this.pasteHandler);
  100.         }
  101.         for (var prop in overrides) {
  102.             if (overrides.hasOwnProperty(prop))
  103.                 this[prop] = overrides[prop];
  104.         }
  105.     };
  106.  
  107.     Editable.prototype = {
  108.  
  109.         /**
  110.          * helper function for inserting random html
  111.          */
  112.         insertHTML: function (html) {
  113.             if (document.all) {
  114.                 var range = document.selection.createRange();
  115.                 range.pasteHTML(html);
  116.                 range.collapse(false);
  117.                 return range.select();
  118.             }
  119.  
  120.             return document.execCommand('insertHTML', false, html);
  121.         },
  122.  
  123.         insertNode: function (node) {
  124.             var selection, range, html;
  125.             if (window.getSelection) {
  126.                 selection = window.getSelection();
  127.                 if (selection.getRangeAt && selection.rangeCount) {
  128.                     range = selection.getRangeAt(0);
  129.                     range.deleteContents();
  130.                     range.insertNode(node);
  131.                     range.collapse(false);
  132.                     selection.removeAllRanges();
  133.                     selection.addRange(range);
  134.                 }
  135.             } else if (document.selection && document.selection.createRange) {
  136.                 range = document.selection.createRange();
  137.                 html = node.nodeType === 3 ? node.data : node.outerHTML;
  138.                 range.pasteHTML(html);
  139.                 range.collapse(false);
  140.             }
  141.         },
  142.  
  143.         /**
  144.          * Grab all text nodes relative to their parents
  145.          */
  146.         // TODO make this function private and have the public version
  147.         // only return textNodes for the wrapped textArea
  148.         getTextNodes: function (nodeList) {
  149.             var elem = this.elem;
  150.  
  151.             // special case a single node by massaging into a list of nodes
  152.             if (nodeList && nodeList.nodeType)
  153.                 nodeList = [nodeList];
  154.             // if nothing is passed in, get text nodes for elem
  155.             else if (!nodeList)
  156.                 nodeList = elem.childNodes;
  157.  
  158.             var textNodes = [];
  159.  
  160.             for (var i = 0, node; i < nodeList.length; ++i) {
  161.                 node = nodeList[i];
  162.  
  163.                 if (!node)
  164.                     continue; // HACK to avoid erroring on whitespace nodes
  165.  
  166.                 switch (node.nodeType) {
  167.                 case 1:
  168.                     textNodes = textNodes.concat(this.getTextNodes(node.childNodes));
  169.                     break;
  170.                 case 3:
  171.                     // HACK don't count garbage FF nodes
  172.                     if (!/^\n\s+/.test(node.nodeValue))
  173.                         textNodes.push(node);
  174.                     break;
  175.                 }
  176.             }
  177.             return textNodes;
  178.         },
  179.  
  180.         /**
  181.          * Get unformatted text
  182.          */
  183.         text: function (nodeCallback) {
  184.             var elem = this.elem;
  185.             var out, nodes, i;
  186.  
  187.             // massage the NodeList into an Array of HTMLElements
  188.             try {
  189.                 nodes = Array.prototype.slice.call(elem.childNodes);
  190.             } catch (err) {
  191.                 nodes = [];
  192.                 for (i = 0; i < elem.childNodes.length; ++i)
  193.                     nodes.push(elem.childNodes[i]);
  194.             }
  195.             out = getTextHelper(nodes, nodeCallback, this.emulateTextarea && '\n');
  196.             return out.replace(/^\s+|\s+$/g, ''); // trim the text
  197.         },
  198.  
  199.         setText: function (str) {
  200.             str = str || '';  // ensure we have a string
  201.             var el = document.createDocumentFragment();
  202.             // 2 consequent line breaks -> we have a paragraph unless we emulate
  203.             var paragraphs = [str]; // we fix the input in TextareaView's fixInputStructure now
  204.  
  205.             var lenP = paragraphs && paragraphs.length;
  206.             var i, par, paragraph;
  207.  
  208.             for (i = 0; i < lenP; i++) {
  209.                 par = paragraphs[i];
  210.                 // parse string and convert it into dom tree wrapped in paragraph
  211.                 paragraph = this.createParagraph(par);
  212.                 // add paragraph to dom
  213.                 el.appendChild(paragraph);
  214.             }
  215.             // We'll keep only the last paragraphs excess <br> since it is
  216.             // necessary for editing.
  217.             el.lastChild.appendChild(document.createElement('br'));
  218.  
  219.             this.elem.innerHTML = '';  // clear first
  220.             this.elem.appendChild(el);
  221.  
  222.             // Fix Chrome bug where cursor is sometimes placed before the first element.
  223.             // Selection.modify() causes the page to scroll to the bottom in Firefox
  224.             // and isn't supported in IE.
  225.             if ('WebkitAppearance' in document.documentElement.style && window.navigator.userAgent.indexOf('Firefox') === -1 && window.navigator.userAgent.indexOf('MSIE') === -1) {
  226.                 // NOTE: These methods of browser-detection are not future-proof and
  227.                 // Selection.modify() is considered a non-standard feature, so we
  228.                 // might want to find an alternative solution.
  229.                 // https://developer.mozilla.org/en-US/docs/Web/API/Selection/modify
  230.                 var sel = window.getSelection && window.getSelection();
  231.                 if (sel && sel.anchorNode === this.elem && sel.modify)
  232.                     sel.modify('move', 'forward', 'line');
  233.             }
  234.         },
  235.  
  236.         createParagraph: function (text) {
  237.             var paragraph = document.createElement('p');
  238.             var i, j, lines, line, lenL, lenC, children;
  239.  
  240.             // line break inside "paragraph" -> Text + <br>
  241.             lines = text.split(/\r\n|\r|\n/);
  242.             for (j = 0, lenL = lines.length; j < lenL; j++) {
  243.                 line = lines[j];
  244.                 // Note the line below escapes HTML tags!
  245.                 // i.e. <b>Yo</b> becomes &lt;b&gt;Yo&lt;/b&gt;
  246.                 children = this.getHtmlElements(line);
  247.                 for (i = 0, lenC = children.length; i < lenC; i++)
  248.                     paragraph.appendChild(children[i]);
  249.  
  250.                 paragraph.appendChild(document.createElement('br'));
  251.             }
  252.  
  253.             // If we do have a last child in the paragraph, it is a line
  254.             // break which we don't want. "ali".split("\n") would give you
  255.             // ["ali"] where we add a <br> at the end of EVERY item.
  256.             if (paragraph.lastChild)
  257.                 paragraph.removeChild(paragraph.lastChild);
  258.  
  259.             return paragraph;
  260.         },
  261.  
  262.         // Accepts a string and return an array of HTMLElements created from the string
  263.         getHtmlElements: function (line) {
  264.             return [document.createTextNode(line)];
  265.         },
  266.  
  267.         plainTextReformat: function () {
  268.             if (this.elem.getElementsByTagName('p').length <= 1)
  269.                 return;
  270.  
  271.             this.emulateTextarea = false;
  272.             var rawText = this.text();
  273.             this.emulateTextarea = true;
  274.             this.setText(rawText);
  275.         },
  276.  
  277.         removeNode: function (node) {
  278.             var prev, sel, range;
  279.  
  280.             // Webkit/FF
  281.             if (window.getSelection) {
  282.                 // HACK in webkit, deleting the node that contains the current
  283.                 // range means that there are no more ranges in the Selection
  284.                 // object. This means that we have to programatically set the
  285.                 // range on the node previous to the mention in order to still
  286.                 // maintain document focus
  287.                 prev = node.previousSibling;
  288.                 node.parentNode.removeChild(node);
  289.                 sel = window.getSelection();
  290.                 range = document.createRange();
  291.                 if (prev) {
  292.                     range.setStart(prev, prev.length);
  293.                     range.setEnd(prev, prev.length);
  294.                 }
  295.                 sel.addRange(range);
  296.             }
  297.             // IE
  298.             else {
  299.                 // TODO port this over to using the selection
  300.                 // code, it's complicated so maybe not the best
  301.                 // idea to do right now
  302.                 node.parentNode.removeChild(node);
  303.             }
  304.         },
  305.         /**
  306.          * Get currently selected text node in
  307.          * the contentEditable element.
  308.          */
  309.         selectedTextNode: function () {
  310.             var elem = this.elem;
  311.             var sel, range, node, textNode, textNodes, prevNode, snippet, i, j;
  312.  
  313.             // Webkit/Firefox
  314.             if (window.getSelection) {
  315.                 sel = window.getSelection();
  316.                 return sel.anchorNode;
  317.             }
  318.             // Internet Explorer
  319.             else if (document.selection.createRange) {
  320.                 // I wish that you never have to touch this code. You're probably sitting here
  321.                 // looking at this function saying "wtf" to yourself becuase it's so hiddeous.
  322.                 // If Internet Explorer 9 every becomes the lowest rung on the Microsoft browser family,
  323.                 // simple delete this conditional and never look back. If you have the misfortune of
  324.                 // modifying the following snippet, I wish you the best of luck in your endeavor.
  325.                 range = document.selection.createRange().duplicate();
  326.  
  327.                 // Microsoft thinks about the entire contentEditable container like a single
  328.                 // line of text. Because of this, you won't be able to get the anchorNode of
  329.                 // the current selection. What we're doing here is simply moving the caret to
  330.                 // the front of the entire block of text.
  331.                 while (range.moveStart(character, -1000) === -1000)
  332.                     continue;
  333.  
  334.                 var text = range.text;
  335.                 // The trick is that we know where the end of our caret is
  336.                 // so all we have to do is loop over all text nodes and find
  337.                 // out where the two strings differ. Notice how we are truncating
  338.                 // the copied string for each iteration of the inner loop. This
  339.                 // is done so that when the two strings differ, we can simply
  340.                 // compare the remaining piece of the copied string with the
  341.                 // current node, this saves us an extra couple of loops.
  342.                 for (i = 0; i < elem.childNodes.length; ++i) {
  343.  
  344.                     node = elem.childNodes[i];
  345.                     textNodes = this.getTextNodes(node);
  346.  
  347.                     for (j = 0; j < textNodes.length; ++j) {
  348.                         textNode = textNodes[j];
  349.                         snippet = normalizeSpace(textNode.nodeValue);
  350.  
  351.                         if (text.indexOf(snippet) > -1) {
  352.                             prevNode = textNode;
  353.                             text = text.replace(snippet, '');
  354.                         }
  355.                         // special case where textNode content is longer
  356.                         // than the selected portion of the textNode
  357.                         else if (snippet.indexOf(text) > -1) {
  358.                             return textNode;
  359.                         }
  360.                     }
  361.                 }
  362.                 return prevNode;
  363.             }
  364.         },
  365.  
  366.         /**
  367.          * Get relative offset in the currently active text node
  368.          */
  369.         selectedTextNodeOffset: function (node) {
  370.             var range, offset, newOffset;
  371.  
  372.             // Webkit/Firefox
  373.             if (window.getSelection) {
  374.                 var sel = window.getSelection();
  375.                 // wondering if this should really
  376.                 // be the focus offset, does it even
  377.                 // really matter? probably not me thinks
  378.                 if (sel && sel.anchorOffset)
  379.                     newOffset = sel.anchorOffset;
  380.             }
  381.  
  382.             // Internet Explorer
  383.             else if (node && document.selection.createRange) {
  384.                 var textNodeText = normalizeSpace(node.nodeValue);
  385.  
  386.                 range = document.selection.createRange();
  387.                 var r2 = range.duplicate();
  388.                 var prevParent = r2.parentElement();
  389.  
  390.                 // move backwards over the range and compare the selected text
  391.                 // node text with the range. break if either we've found a match
  392.                 // or the previous range we create each iteration has a different
  393.                 // parent element
  394.                 for (offset = 0; range.moveStart(character, -1) !== 0; offset++) {
  395.                     if (
  396.                         textNodeText.indexOf(normalizeSpace(range.text)) === 0 ||
  397.                         prevParent !== range.parentElement()
  398.                     ) break;
  399.  
  400.                     r2 = range.duplicate();
  401.                     prevParent = r2.parentElement();
  402.                 }
  403.                 newOffset = offset;
  404.             }
  405.  
  406.  
  407.             return isNaN(newOffset) ? 0 : newOffset;
  408.         },
  409.  
  410.         /**
  411.          * Get cursor offset relative to results returned by .text().
  412.          */
  413.         offset: function () {
  414.             var sel = window.getSelection();
  415.             if (!(sel && sel.anchorNode && sel.anchorNode.nodeType === 3))  // This method only supports text nodes
  416.                 return 0;
  417.  
  418.             var elem = this.elem;
  419.             var nodes;
  420.  
  421.             // massage the NodeList into an Array of HTMLElements
  422.             try {
  423.                 nodes = Array.prototype.slice.call(elem.childNodes);
  424.             } catch (error) {
  425.                 nodes = [];
  426.                 for (var i = 0; i < elem.childNodes.length; ++i)
  427.                     nodes.push(elem.childNodes[i]);
  428.             }
  429.  
  430.             function getTextAroundCursor(nodes, blockJoin) {
  431.                 if (typeof blockJoin !== 'string')
  432.                     blockJoin = '\n\n';
  433.  
  434.                 var sections = [];
  435.                 var text = '';
  436.  
  437.                 function extendBySections(nextSections) {
  438.                     text += nextSections[0];
  439.                     for (var i = 1; i < nextSections.length; ++i) {
  440.                         sections.push(text);
  441.                         text = nextSections[i];
  442.                     }
  443.                 }
  444.  
  445.                 for (var i = 0; i < nodes.length; ++i) {
  446.                     var node = nodes[i];
  447.                     var name = node.nodeName.toLowerCase();
  448.  
  449.                     if (node.nodeType === 1) { // html node
  450.                         if (blockElems.hasOwnProperty(name)) {
  451.                             if (text)
  452.                                 text += blockJoin;
  453.                             extendBySections(getTextAroundCursor(node.childNodes, blockJoin));
  454.                         } else if (name === 'br') {
  455.                             text += '\n';
  456.                         } else {
  457.                             extendBySections(getTextAroundCursor(node.childNodes, blockJoin));
  458.                         }
  459.                     } else if (node.nodeType === 3) { // text node
  460.                         if (node === sel.anchorNode) {
  461.                             text += normalizeSpace(node.nodeValue.slice(0, sel.anchorOffset));
  462.                             sections.push(text);
  463.                             text = normalizeSpace(node.nodeValue.slice(sel.anchorOffset));
  464.                         } else {
  465.                             text += normalizeSpace(node.nodeValue);
  466.                         }
  467.                     }
  468.                 }
  469.  
  470.                 // Block elements should get two new lines normally
  471.                 sections.push(text);
  472.                 return sections;
  473.             }
  474.  
  475.             var sections = getTextAroundCursor(nodes, this.emulateTextarea && '\n');
  476.             if (sections.length === 1)  // Cursor was not found
  477.                 return 0;
  478.  
  479.             var offset = sections[0].length;
  480.             var out = sections.join('');
  481.  
  482.             // Correct for trailing whitespace
  483.             var trailingSpaces = out.match(/\s+$/);
  484.             if (trailingSpaces) {
  485.                 var trailingSpaceCount = trailingSpaces[0].length;
  486.                 offset = Math.min(offset, out.length - trailingSpaceCount);
  487.             }
  488.  
  489.             // Correct for leading whitespace
  490.             var leadingSpaces = out.match(/^\s+/);
  491.             if (leadingSpaces) {
  492.                 var leadingSpaceCount = leadingSpaces[0].length;
  493.                 offset -= leadingSpaceCount;
  494.             }
  495.  
  496.             return offset;
  497.         },
  498.  
  499.         /**
  500.          * Select some text in the contentEditable div
  501.          */
  502.         selectNodeText: function (node, start, end) {
  503.             var elem = this.elem;
  504.             var sel, range;
  505.  
  506.             // Webkit/Firefox
  507.             if (window.getSelection) {
  508.                 // clear all ranges
  509.                 sel = window.getSelection();
  510.                 sel.removeAllRanges();
  511.                 // select the new one
  512.                 range = document.createRange();
  513.                 range.setStart(node, start);
  514.                 range.setEnd(node, end);
  515.                 sel.addRange(range);
  516.                 return sel;
  517.             }
  518.  
  519.             // Internet Explorer
  520.             else if (document.selection.createRange) {
  521.                 range = document.selection.createRange();
  522.  
  523.                 // KLUDGE
  524.                 // if there is a substring match before we hit the start of the text node
  525.                 // then this code will break. Might want to think about a more robust way
  526.                 // about doing this.
  527.                 var text = normalizeSpace(node.nodeValue);
  528.  
  529.                 // MASSIVE HACK for ie < 9, clicks change the focus, so we need to
  530.                 // focus back on the contentEditable div and refind out
  531.                 // start position. The rest of the function can continue as
  532.                 // normal afterwards.
  533.                 if (range.parentElement().nodeName.toLowerCase() === 'body') {
  534.                     elem.focus();
  535.                     range = document.selection.createRange();
  536.                     // expand over the entire extArea
  537.                     while (range.moveStart(character, -1000) === -1000)
  538.                         continue;
  539.                     while (range.moveEnd(character, 1000) === 1000)
  540.                         continue;
  541.                     var rangeText = normalizeSpace(range.text);
  542.                     var index = rangeText.indexOf(text);
  543.                     if (index > 0)
  544.                         range.moveStart(character, index + 2); // put us inside the range
  545.                     range.collapse();
  546.                 }
  547.  
  548.                 // move start cursor to start of text node
  549.                 while (range.moveStart(character, -1) === -1 && text.indexOf(normalizeSpace(range.text)) !== 0)
  550.                     continue;
  551.  
  552.                 // move end cursor to end of text node
  553.                 while (range.moveEnd(character, 1) === 1 && text !== normalizeSpace(range.text))
  554.                     continue;
  555.  
  556.                 // move the start and end indicies of the Range and select
  557.                 range.moveStart(character, start);
  558.                 range.moveEnd(character, -1 * (end - start - range.text.length));
  559.                 range.select();
  560.  
  561.                 return range;
  562.             }
  563.         },
  564.     };
  565.  
  566.     Editable.normalizeSpace = normalizeSpace;
  567.  
  568.     // assign to the current window
  569.     return Editable;
  570. });
  571.  
  572. // https://c.disquscdn.com/next/next-core/core/editable.js

Reply to "editable.js"

Here you can reply to the paste above