JAVASCRIPT   45

codeflask.js

Guest on 10th October 2021 02:14:32 PM

  1. import { editorCss } from './styles/editor'
  2. import { injectCss } from './styles/injector'
  3. import { defaultCssTheme } from './styles/theme-default'
  4. import { escapeHtml } from './utils/html-escape'
  5. import Prism from 'prismjs'
  6.  
  7. export default class CodeFlask {
  8.   constructor (selectorOrElement, opts) {
  9.     if (!selectorOrElement) {
  10.       // If no selector or element is passed to CodeFlask,
  11.       // stop execution and throw error.
  12.       throw Error('CodeFlask expects a parameter which is Element or a String selector')
  13.     }
  14.  
  15.     if (!opts) {
  16.       // If no selector or element is passed to CodeFlask,
  17.       // stop execution and throw error.
  18.       throw Error('CodeFlask expects an object containing options as second parameter')
  19.     }
  20.  
  21.     if (selectorOrElement.nodeType) {
  22.       // If it is an element, assign it directly
  23.       this.editorRoot = selectorOrElement
  24.     } else {
  25.       // If it is a selector, tries to find element
  26.       const editorRoot = document.querySelector(selectorOrElement)
  27.  
  28.       // If an element is found using this selector,
  29.       // assign this element as the root element
  30.       if (editorRoot) {
  31.         this.editorRoot = editorRoot
  32.       }
  33.     }
  34.  
  35.     this.opts = opts
  36.     this.startEditor()
  37.   }
  38.  
  39.   startEditor () {
  40.     const isCSSInjected = injectCss(editorCss, null, this.opts.styleParent)
  41.  
  42.     if (!isCSSInjected) {
  43.       throw Error('Failed to inject CodeFlask CSS.')
  44.     }
  45.  
  46.     // The order matters (pre > code). Don't change it
  47.     // or things are going to break.
  48.     this.createWrapper()
  49.     this.createTextarea()
  50.     this.createPre()
  51.     this.createCode()
  52.  
  53.     this.runOptions()
  54.     this.listenTextarea()
  55.     this.populateDefault()
  56.     this.updateCode(this.code)
  57.   }
  58.  
  59.   createWrapper () {
  60.     this.code = this.editorRoot.innerHTML
  61.     this.editorRoot.innerHTML = ''
  62.     this.elWrapper = this.createElement('div', this.editorRoot)
  63.     this.elWrapper.classList.add('codeflask')
  64.   }
  65.  
  66.   createTextarea () {
  67.     this.elTextarea = this.createElement('textarea', this.elWrapper)
  68.     this.elTextarea.classList.add('codeflask__textarea', 'codeflask__flatten')
  69.   }
  70.  
  71.   createPre () {
  72.     this.elPre = this.createElement('pre', this.elWrapper)
  73.     this.elPre.classList.add('codeflask__pre', 'codeflask__flatten')
  74.   }
  75.  
  76.   createCode () {
  77.     this.elCode = this.createElement('code', this.elPre)
  78.     this.elCode.classList.add('codeflask__code', `language-${this.opts.language || 'html'}`)
  79.   }
  80.  
  81.   createLineNumbers () {
  82.     this.elLineNumbers = this.createElement('div', this.elWrapper)
  83.     this.elLineNumbers.classList.add('codeflask__lines')
  84.     this.setLineNumber()
  85.   }
  86.  
  87.   createElement (elementTag, whereToAppend) {
  88.     const element = document.createElement(elementTag)
  89.     whereToAppend.appendChild(element)
  90.  
  91.     return element
  92.   }
  93.  
  94.   runOptions () {
  95.     this.opts.rtl = this.opts.rtl || false
  96.     this.opts.tabSize = this.opts.tabSize || 2
  97.     this.opts.enableAutocorrect = this.opts.enableAutocorrect || false
  98.     this.opts.lineNumbers = this.opts.lineNumbers || false
  99.     this.opts.defaultTheme = this.opts.defaultTheme !== false
  100.     this.opts.areaId = this.opts.areaId || null
  101.     this.opts.ariaLabelledby = this.opts.ariaLabelledby || null
  102.     this.opts.readonly = this.opts.readonly || null
  103.  
  104.     // if handleTabs is not either true or false, make it true by default
  105.     if (typeof this.opts.handleTabs !== 'boolean') {
  106.       this.opts.handleTabs = true
  107.     }
  108.     // if handleTabs is not either true or false, make it true by default
  109.     if (typeof this.opts.handleSelfClosingCharacters !== 'boolean') {
  110.       this.opts.handleSelfClosingCharacters = true
  111.     }
  112.     // if handleTabs is not either true or false, make it true by default
  113.     if (typeof this.opts.handleNewLineIndentation !== 'boolean') {
  114.       this.opts.handleNewLineIndentation = true
  115.     }
  116.  
  117.     if (this.opts.rtl === true) {
  118.       this.elTextarea.setAttribute('dir', 'rtl')
  119.       this.elPre.setAttribute('dir', 'rtl')
  120.     }
  121.  
  122.     if (this.opts.enableAutocorrect === false) {
  123.       this.elTextarea.setAttribute('spellcheck', 'false')
  124.       this.elTextarea.setAttribute('autocapitalize', 'off')
  125.       this.elTextarea.setAttribute('autocomplete', 'off')
  126.       this.elTextarea.setAttribute('autocorrect', 'off')
  127.     }
  128.  
  129.     if (this.opts.lineNumbers) {
  130.       this.elWrapper.classList.add('codeflask--has-line-numbers')
  131.       this.createLineNumbers()
  132.     }
  133.  
  134.     if (this.opts.defaultTheme) {
  135.       injectCss(defaultCssTheme, 'theme-default', this.opts.styleParent)
  136.     }
  137.  
  138.     if (this.opts.areaId) {
  139.       this.elTextarea.setAttribute('id', this.opts.areaId)
  140.     }
  141.  
  142.     if (this.opts.ariaLabelledby) {
  143.       this.elTextarea.setAttribute('aria-labelledby', this.opts.ariaLabelledby)
  144.     }
  145.  
  146.     if (this.opts.readonly) {
  147.       this.enableReadonlyMode()
  148.     }
  149.   }
  150.  
  151.   updateLineNumbersCount () {
  152.     let numberList = ''
  153.  
  154.     for (let i = 1; i <= this.lineNumber; i++) {
  155.       numberList = numberList + `<span class="codeflask__lines__line">${i}</span>`
  156.     }
  157.  
  158.     this.elLineNumbers.innerHTML = numberList
  159.   }
  160.  
  161.   listenTextarea () {
  162.     this.elTextarea.addEventListener('input', (e) => {
  163.       this.code = e.target.value
  164.       this.elCode.innerHTML = escapeHtml(e.target.value)
  165.       this.highlight()
  166.       setTimeout(() => {
  167.         this.runUpdate()
  168.         this.setLineNumber()
  169.       }, 1)
  170.     })
  171.  
  172.     this.elTextarea.addEventListener('keydown', (e) => {
  173.       this.handleTabs(e)
  174.       this.handleSelfClosingCharacters(e)
  175.       this.handleNewLineIndentation(e)
  176.     })
  177.  
  178.     this.elTextarea.addEventListener('scroll', (e) => {
  179.       this.elPre.style.transform = `translate3d(-${e.target.scrollLeft}px, -${e.target.scrollTop}px, 0)`
  180.       if (this.elLineNumbers) {
  181.         this.elLineNumbers.style.transform = `translate3d(0, -${e.target.scrollTop}px, 0)`
  182.       }
  183.     })
  184.   }
  185.  
  186.   handleTabs (e) {
  187.     if (this.opts.handleTabs) {
  188.       if (e.keyCode !== 9) {
  189.         return
  190.       }
  191.       e.preventDefault()
  192.  
  193.       var input = this.elTextarea
  194.       var selectionDir = input.selectionDirection
  195.       var selStartPos = input.selectionStart
  196.       var selEndPos = input.selectionEnd
  197.       var inputVal = input.value
  198.  
  199.       var beforeSelection = inputVal.substr(0, selStartPos)
  200.       var selectionVal = inputVal.substring(selStartPos, selEndPos)
  201.       var afterSelection = inputVal.substring(selEndPos)
  202.       const indent = ' '.repeat(this.opts.tabSize)
  203.  
  204.       if (selStartPos !== selEndPos && selectionVal.length >= indent.length) {
  205.         var currentLineStart = selStartPos - beforeSelection.split('\n').pop().length
  206.         var startIndentLen = indent.length
  207.         var endIndentLen = indent.length
  208.  
  209.         // Unindent
  210.         if (e.shiftKey) {
  211.           var currentLineStartStr = inputVal.substr(currentLineStart, indent.length)
  212.           // Line start whit indent
  213.           if (currentLineStartStr === indent) {
  214.             startIndentLen = -startIndentLen
  215.  
  216.             if (currentLineStart > selStartPos) {
  217.               // Indent is in selection
  218.               selectionVal = selectionVal.substring(0, currentLineStart) + selectionVal.substring(currentLineStart + indent.length)
  219.               endIndentLen = 0
  220.             } else if (currentLineStart === selStartPos) {
  221.               // Indent is in start of selection
  222.               startIndentLen = 0
  223.               endIndentLen = 0
  224.               selectionVal = selectionVal.substring(indent.length)
  225.             } else {
  226.               // Indent is before selection
  227.               endIndentLen = -endIndentLen
  228.               beforeSelection = beforeSelection.substring(0, currentLineStart) + beforeSelection.substring(currentLineStart + indent.length)
  229.             }
  230.           } else {
  231.             startIndentLen = 0
  232.             endIndentLen = 0
  233.           }
  234.  
  235.           selectionVal = selectionVal.replace(new RegExp('\n' + indent.split('').join('\\'), 'g'), '\n')
  236.         } else {
  237.           // Indent
  238.           beforeSelection = beforeSelection.substr(0, currentLineStart) + indent + beforeSelection.substring(currentLineStart, selStartPos)
  239.           selectionVal = selectionVal.replace(/\n/g, '\n' + indent)
  240.         }
  241.  
  242.         // Set new indented value
  243.         input.value = beforeSelection + selectionVal + afterSelection
  244.  
  245.         input.selectionStart = selStartPos + startIndentLen
  246.         input.selectionEnd = selStartPos + selectionVal.length + endIndentLen
  247.         input.selectionDirection = selectionDir
  248.       } else {
  249.         input.value = beforeSelection + indent + afterSelection
  250.         input.selectionStart = selStartPos + indent.length
  251.         input.selectionEnd = selStartPos + indent.length
  252.       }
  253.  
  254.       var newCode = input.value
  255.       this.updateCode(newCode)
  256.       this.elTextarea.selectionEnd = selEndPos + this.opts.tabSize
  257.     }
  258.   }
  259.  
  260.   handleSelfClosingCharacters (e) {
  261.     if (!this.opts.handleSelfClosingCharacters) return
  262.     const openChars = ['(', '[', '{', '<', '\'', '"']
  263.     const closeChars = [')', ']', '}', '>', '\'', '"']
  264.     const key = e.key
  265.  
  266.     if (!openChars.includes(key) && !closeChars.includes(key)) {
  267.       return
  268.     }
  269.  
  270.     switch (key) {
  271.       case '(':
  272.       case ')':
  273.         this.closeCharacter(key)
  274.         break
  275.  
  276.       case '[':
  277.       case ']':
  278.         this.closeCharacter(key)
  279.         break
  280.  
  281.       case '{':
  282.       case '}':
  283.         this.closeCharacter(key)
  284.         break
  285.  
  286.       case '<':
  287.       case '>':
  288.         this.closeCharacter(key)
  289.         break
  290.  
  291.       case '\'':
  292.         this.closeCharacter(key)
  293.         break
  294.  
  295.       case '"':
  296.         this.closeCharacter(key)
  297.         break
  298.     }
  299.   }
  300.  
  301.   setLineNumber () {
  302.     this.lineNumber = this.code.split('\n').length
  303.  
  304.     if (this.opts.lineNumbers) {
  305.       this.updateLineNumbersCount()
  306.     }
  307.   }
  308.  
  309.   handleNewLineIndentation (e) {
  310.     if (!this.opts.handleNewLineIndentation) return
  311.     if (e.keyCode !== 13) {
  312.       return
  313.     }
  314.  
  315.     e.preventDefault()
  316.     var input = this.elTextarea
  317.     var selStartPos = input.selectionStart
  318.     var selEndPos = input.selectionEnd
  319.     var inputVal = input.value
  320.  
  321.     var beforeSelection = inputVal.substr(0, selStartPos)
  322.     var afterSelection = inputVal.substring(selEndPos)
  323.  
  324.     var lineStart = inputVal.lastIndexOf('\n', selStartPos - 1)
  325.     var spaceLast = lineStart + inputVal.slice(lineStart + 1).search(/[^ ]|$/)
  326.     var indent = (spaceLast > lineStart) ? (spaceLast - lineStart) : 0
  327.     var newCode = beforeSelection + '\n' + ' '.repeat(indent) + afterSelection
  328.  
  329.     input.value = newCode
  330.     input.selectionStart = selStartPos + indent + 1
  331.     input.selectionEnd = selStartPos + indent + 1
  332.  
  333.     this.updateCode(input.value)
  334.   }
  335.  
  336.   closeCharacter (char) {
  337.     const selectionStart = this.elTextarea.selectionStart
  338.     const selectionEnd = this.elTextarea.selectionEnd
  339.  
  340.     if (!this.skipCloseChar(char)) {
  341.       let closeChar = char
  342.       switch (char) {
  343.         case '(':
  344.           closeChar = String.fromCharCode(char.charCodeAt() + 1)
  345.           break
  346.         case '<':
  347.         case '{':
  348.         case '[':
  349.           closeChar = String.fromCharCode(char.charCodeAt() + 2)
  350.           break
  351.       }
  352.       const selectionText = this.code.substring(selectionStart, selectionEnd)
  353.       const newCode = `${this.code.substring(0, selectionStart)}${selectionText}${closeChar}${this.code.substring(selectionEnd)}`
  354.       this.updateCode(newCode)
  355.     } else {
  356.       const skipChar = this.code.substr(selectionEnd, 1) === char
  357.       const newSelectionEnd = skipChar ? selectionEnd + 1 : selectionEnd
  358.       const closeChar = !skipChar && ['\'', '"'].includes(char) ? char : ''
  359.       const newCode = `${this.code.substring(0, selectionStart)}${closeChar}${this.code.substring(newSelectionEnd)}`
  360.       this.updateCode(newCode)
  361.       this.elTextarea.selectionEnd = ++this.elTextarea.selectionStart
  362.     }
  363.  
  364.     this.elTextarea.selectionEnd = selectionStart
  365.   }
  366.  
  367.   skipCloseChar (char) {
  368.     const selectionStart = this.elTextarea.selectionStart
  369.     const selectionEnd = this.elTextarea.selectionEnd
  370.     const hasSelection = Math.abs(selectionEnd - selectionStart) > 0
  371.     return [')', '}', ']', '>'].includes(char) || (['\'', '"'].includes(char) && !hasSelection)
  372.   }
  373.  
  374.   updateCode (newCode) {
  375.     this.code = newCode
  376.     this.elTextarea.value = newCode
  377.     this.elCode.innerHTML = escapeHtml(newCode)
  378.     this.highlight()
  379.     this.setLineNumber()
  380.     setTimeout(this.runUpdate.bind(this), 1)
  381.   }
  382.  
  383.   updateLanguage (newLanguage) {
  384.     const oldLanguage = this.opts.language
  385.     this.elCode.classList.remove(`language-${oldLanguage}`)
  386.     this.elCode.classList.add(`language-${newLanguage}`)
  387.     this.opts.language = newLanguage
  388.     this.highlight()
  389.   }
  390.  
  391.   addLanguage (name, options) {
  392.     Prism.languages[name] = options
  393.   }
  394.  
  395.   populateDefault () {
  396.     this.updateCode(this.code)
  397.   }
  398.  
  399.   highlight () {
  400.     Prism.highlightElement(this.elCode, false)
  401.   }
  402.  
  403.   onUpdate (callback) {
  404.     if (callback && {}.toString.call(callback) !== '[object Function]') {
  405.       throw Error('CodeFlask expects callback of type Function')
  406.     }
  407.  
  408.     this.updateCallBack = callback
  409.   }
  410.  
  411.   getCode () {
  412.     return this.code
  413.   }
  414.  
  415.   runUpdate () {
  416.     if (this.updateCallBack) {
  417.       this.updateCallBack(this.code)
  418.     }
  419.   }
  420.  
  421.   enableReadonlyMode () {
  422.     this.elTextarea.setAttribute('readonly', true)
  423.   }
  424.  
  425.   disableReadonlyMode () {
  426.     this.elTextarea.removeAttribute('readonly')
  427.   }
  428. }

Raw Paste


Login or Register to edit or fork this paste. It's free.