// Get any elem by ID const get = function(elem) { return document.getElementById(elem); }; // Main ICEcoder object var ICEcoder = { // ==== // INIT // ==== // URLs we're viewing ICEcoder and its assets from iceLoc: window.location.origin + window.location.pathname.replace(/\/$/, ""), assetsLoc: get('icecoderJSFile').dataset.assetsRoot, // Define settings filesW: 250, // Width of files pane minFilesW: 14, // Min width of files pane maxFilesW: 250, // Max width of files pane selectedTab: 0, // The tab that's currently selected savedPoints: [], // Ints array to indicate save points for docs savedContents: [], // Array of last known saved contents openFiles: [], // Array of open file URLs openFileMDTs: [], // Array of open file modification datetimes openFileVersions: [], // Array of open file version counts cMInstances: [], // List of CodeMirror instance no's nextcMInstance: 1, // Next available CodeMirror instance no selectedFiles: [], // Array of selected files thisFileFolderType: '', // The type of current item - file or folder thisFileFolderLink: '', // The id value of the current item results: [], // Array of find coords (line & char) resultsLines: [], // Array of lines containing results (simpler version of results) findResult: 0, // Array position of current find in results findRegex: false, // If find attempts are done using regex findUpdateMultiInfoID: [], // ID of multiple results modal elem to update & text, when rename/replace is successful scrollbarVisible: false, // Indicates if the main pane has a scrollbar mouseDown: false, // If the mouse is down mouseDownInCM: false, // If the mouse is down within CodeMirror instance (can be false, 'editor' or 'gutter') draggingFilesW: false, // If we're dragging the file manager width draggingTab: false, // If we're dragging a tab draggingWithKey: false, // The key that's down while dragging, false if no key tabLeftPos: [], // Array of left positions of tabs inside content area overCloseLink: false, // If mouse is over close link on tab or not colorCurrentBG: '#1d1d1b', // Current tab/file background color colorCurrentText: '#fff', // Current tab/file text color colorOpenBG: '#c3c3c3', // Open tab/file background color colorOpenTextFile: '#000', // Open file text color colorOpenTextTab: '#888', // Open tab text color colorSelectedBG: '#49d', // Selected tab/file background color colorSelectedText: '#fff', // Selected tab/file text color colorDropTgtBGFile: '#f80', // Drop dir target background color prevTab: 0, // Previous tab to current serverQueueItems: [], // Array of URLs to call in order origCursorPos: false, // Original cursor position before jump to definition origSelectionPos: false, // Original selection position before jump to definition previewWindow: false, // Target variable for the preview window previewWindowLoading: false, // Loading state of preview window pluginIntervalRefs: [], // Array of plugin interval refs overPopup: false, // Indicates if we're over a popup or not cmdKey: false, // Tracking Apple Command key up/down state codeZoomedOut: false, // If true, code on non declaration lines is zoomed out showingTool: false, // Which tool is showing right now (terminal, output, database, git etc) oppTagReplaceData: [], // Will contain data for automatic opposite tag replacement to sync them fmReady: false, // Indicates if the file manager is ready for action bugReportStatus: "off", // Values of: off, error, ok, bugs bugReportPath: "", // Bug report file path bugFilesSizesSeen: [], // Array of last seen sizes of bug files bugFilesSizesActual: [], // Array of actual sizes of bug files splitPane: false, // Single or split pane editing splitPaneLeftPerc: 100, // Width of left pane as a percentage renderLineStyle: [], // Array of styles to apply on renderLine event renderPaneShiftAmount: 0, // Shift comparison main (negative) vs diff pane (positive) editorFocusInstance: "", // Name of editor instance that has focus openSeconds: 0, // Number of seconds ICEcoder has been open for indexing: false, // Indicates if ICEcoder is currently indexing ready: false, // Indicates if ICEcoder is ready for action // Set our aliases initAliases: function() { const aliasArray = ["header", "files", "fileOptions", "optionsFile", "optionsEdit", "optionsSettings", "optionsHelp", "filesFrame", "editor", "tabsBar", "findBar", "terminal", "output", "database", "git", "content", "tools", "footer", "versionsDisplay", "splitPaneControls", "splitPaneNamesMain", "splitPaneNamesDiff", "charDisplay", "byteDisplay"]; // Create our ID aliases for (let i = 0; i < aliasArray.length; i++) { this[aliasArray[i]] = get(aliasArray[i]); } }, // On load, set the layout init: function() { // Contract the file manager if the user has set to have it hidden if (false === this.lockedNav) { this.filesW = this.minFilesW; } // Set layout this.setLayout(); // State we've over the root dir, enact a selection of it, then state // we're not over it it anymore this.overFileFolder('folder', '|'); this.selectFileFolder('init'); this.overFileFolder('folder', ''); this.filesFrame.contentWindow.focus(); // Hide the loading screen & auto open last files? this.showHide('hide', get('loadingMask')); this.autoOpenInt = setInterval(function(ic) { if (ic.fmReady) { if (ic.openLastFiles) {ic.autoOpenFiles();} clearInterval(ic.autoOpenInt); } }, 4, this); // Start bug checking this.startBugChecking(); // Set the time since last user interaction this.autoLogoutTimer = 0; // Start our interval timer, runs every second this.oneSecondInt = setInterval(function(ic) { ic.autoLogoutTimer++; let unsavedFiles = false; // Check if we have any unsaved files for(let i = 1; i <= ic.savedPoints.length; i++) { if (ic.savedPoints[i - 1] !== ic.getcMInstance(i).changeGeneration()) { unsavedFiles = true; } } // Show an auto-logout warning 60 secs before a logout if(false === unsavedFiles && ic.autoLogoutMins > 1 && ic.autoLogoutTimer == (ic.autoLogoutMins * 60) - 60) { ic.autoLogoutWarningScreen(); } if (get('autoLogoutIFrame') && get('autoLogoutIFrame').contentWindow.document.getElementById('timeRemaning')) { get('autoLogoutIFrame').contentWindow.document.getElementById('timeRemaning').innerHTML = ic.autoLogoutTimer > 0 ? (ic.autoLogoutMins * 60) - ic.autoLogoutTimer : 0; } // If there aren't any unsaved files, we have a timeout period > 0 and the time is up, we can logout if (false === unsavedFiles && ic.autoLogoutMins > 0 && ic.autoLogoutTimer >= ic.autoLogoutMins * 60) { ic.logout('autoLogout'); } // Increase number of seconds ICEcoder has been open for by 1 ic.openSeconds++; // Every 5 mins, ping our file to keep the session alive if (0 === ic.openSeconds % 300) { ic.filesFrame.contentWindow.frames['pingActive'].location.href = ic.iceLoc + "/lib/session-active-ping.php"; } // Every 3 seconds, re-index if we're not already busy if (false === ic.indexing && false === ic.loadingFile && 0 === ic.serverQueueItems.length && 0 === ic.openSeconds % 3) { ic.indexing = true; // Get new data let timestampExtra = ic.indexData ? "?timestamp=" + ic.indexData.timestamps.indexed + "&csrf=" + ic.csrf : ""; fetch(ic.iceLoc + '/lib/indexer.php' + timestampExtra) .then(function(response) { // Convert to JSON return response.json(); }).then(function(data) { if (data.timestamps.changed) { ic.indexData = data; // If we have git diff data if (data.gitDiff) { ic.updateGitDiffPane(); } // If we have git content data if (data.gitContent) { ic.highlightGitDiffs(); } } ic.indexing = false; }); } }, 1000, this); // ICEcoder is ready to start using this.ready = true; }, // ====== // LAYOUT // ====== // Set our layout according to the browser size setLayout: function(setEditor) { let winW, winH, headerH, fileNavH, tabsBarH, findBarH, toolsBarH; // Determine width & height available winW = window.innerWidth; winH = window.innerHeight; // Apply sizes to various elements of the page headerH = 15, fileNavH = 38, tabsBarH = 27, findBarH = 28, toolsBarH = 30; this.header.style.width = this.tabsBar.style.width = this.findBar.style.width = winW + "px"; this.files.style.width = this.editor.style.left = this.filesW + "px"; this.optionsFile.style.width = this.optionsEdit.style.width = this.optionsSettings.style.width = this.optionsHelp.style.width = this.filesW + "px"; this.filesFrame.style.height = (winH - headerH - fileNavH - 7 - toolsBarH) + "px"; this.versionsDisplay.style.left = (this.filesW + 10) + "px"; get("serverMessage").style.left = (this.filesW + 10) + "px"; this.splitPaneControls.style.left = parseInt( ((winW - this.filesW) / 2) + this.filesW - (get("splitPaneControls").getBoundingClientRect().width / 2) , 10) + "px"; this.splitPaneNamesMain.style.left = (parseInt((winW - this.filesW) * 0.25, 10) - 50 + this.filesW) + "px"; this.splitPaneNamesDiff.style.left = (parseInt((winW - this.filesW) * 0.75, 10) - 50 + this.filesW) + "px"; this.setTabWidths(false); // If we need to set the editor sizes if (false !== setEditor) { this.editor.style.width = this.content.style.width = (winW - this.filesW) + "px"; this.terminal.style.width = (winW - this.filesW) + "px"; this.output.style.width = (winW - this.filesW - 31) + "px"; this.database.style.width = (winW - this.filesW) + "px"; this.git.style.width = (winW - this.filesW - 31) + "px"; this.content.style.height = (winH - headerH - tabsBarH - findBarH - toolsBarH) + "px"; this.terminal.style.height = this.output.style.height = this.database.style.height = this.git.style.height = this.terminal.style.top = this.output.style.top = this.database.style.top = this.git.style.top = winH + "px"; if (false !== this.showingTool) { get(this.showingTool).style.top = 0; } // Resize the CodeMirror instances to match the window size setTimeout(function(ic){ for (let i = 0; i < ic.openFiles.length; i++) { // Done the long way here as we need to call them in specific order to stop showing background and so avoiding a flicker effect if (false === ic.splitPane) { ic.content.contentWindow['cM' + ic.cMInstances[i]].setSize(ic.splitPaneLeftPerc + "%", ic.content.style.height); } ic.content.contentWindow['cM' + ic.cMInstances[i] + 'diff'].setSize((100 - ic.splitPaneLeftPerc) + "%", ic.content.style.height); ic.content.contentWindow['cM'+ ic.cMInstances[i] + 'diff'].getWrapperElement().style.left = ic.splitPaneLeftPerc + "%"; if (true === ic.splitPane) { ic.content.contentWindow['cM' + ic.cMInstances[i]].setSize(ic.splitPaneLeftPerc + "%", ic.content.style.height); } } // Place resultsBar on-top scrollbar ic.content.contentWindow.document.getElementById('resultsBar').style.right = false === ic.splitPane ? 0 : parseInt(parseInt(ic.content.style.width, 10) / 2, 10) + "px"; }, 4, this); } }, // Set the layout as split pane or not setSplitPane: function(onOff) { let cM, cMdiff; this.splitPane = "on" === onOff ? true : false; get('splitPaneControlsOff').style.opacity = this.splitPane ? 0.2 : 0.8; get('splitPaneControlsOn').style.opacity = this.splitPane ? 0.8 : 0.2; get('splitPaneNamesMain').style.opacity = get('splitPaneNamesDiff').style.opacity = this.splitPane ? 1 : 0; this.setLayout(); // Also clear marks (if going to a single pane) or redo the marks (if split pane) if (true === this.splitPane) { this.updateDiffs(); // Also set the scroll position to match cM = this.getcMInstance(); this.cMonScroll(cM, 'cM' + this.cMInstances[this.selectedTab - 1]); } else { cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); if (cM) { // Clear all main pane marks cMmarks = cM.getAllMarks(); for (let i = 0; i < cMmarks.length; i++) { cMmarks[i].clear(); } // Clear all diff pane marks cMdiffMarks = cMdiff.getAllMarks(); for (let i = 0; i < cMdiffMarks.length; i++) { cMdiffMarks[i].clear(); } } } // Animate in/out the split pane // First, clear any existing split pane interval anim if ("undefined" !== typeof this.animSplitPaneInt) { clearInterval(this.animSplitPaneInt); } // Now set the interval to animate it in/out this.animSplitPaneInt = setInterval(function(ic) { // Animate split pane in if (ic.splitPane && ic.splitPaneLeftPerc > 50.1) { ic.splitPaneLeftPerc = ((ic.splitPaneLeftPerc - 50) / 1.8) + 50; // Animate split pane out } else if (!ic.splitPane && ic.splitPaneLeftPerc < 99.9) { ic.splitPaneLeftPerc = (50 - ((100 - ic.splitPaneLeftPerc) / 1.8)) + 50; // Finish animating split pane in/out } else { ic.splitPaneLeftPerc = ic.splitPane ? 50 : 100; clearInterval(ic.animSplitPaneInt); } ic.setLayout(); }, 4, this); }, // Tool show/hide toggle toolShowHideToggle: function(tool) { let winH; winH = window.innerHeight; if (-1 < ["terminal", "output", "database", "git"].indexOf(tool)) { // Set out of view as a start point get('terminal').style.top = winH + "px"; get('output').style.top = winH + "px"; get('database').style.top = winH + "px"; get('git').style.top = winH + "px"; // Now set tool requested, out of view, or in view get(tool).style.top = tool === this.showingTool ? winH + "px" : 0; // Carry out any extras... if (tool === "terminal") { // Focus on command prompt setTimeout(function(ic){ ic.terminal.contentWindow.document.getElementById('command').focus(); }, 0 ,this); } // Note which tool we're showing this.showingTool = this.showingTool !== tool ? tool : false; // Display and make close icon clickable to close this tool setTimeout(function() { get('closeIcon').style.display = ICEcoder.showingTool !== false ? "inline-block" : "none"; get('closeIcon').onclick = function() { ICEcoder.toolShowHideToggle(ICEcoder.showingTool); } }, this.showingTool !== false ? 200 : 0); } }, // Set the width of the file manager on demand changeFilesW: function(expandContract) { if (false === this.lockedNav || this.filesW === this.minFilesW) { if ("undefined" !== typeof this.changeFilesInt) {clearInterval(this.changeFilesInt)} this.changeFilesInt = setInterval(function(ic) {ic.changeFilesWStep(expandContract)}, 10, this); } }, // Expand/contract the file manager in half-steps changeFilesWStep: function (expandContract) { if ("expand" === expandContract) { this.filesW < this.maxFilesW - 1 ? this.filesW += Math.ceil((this.maxFilesW - this.filesW) / 2) : this.filesW = this.maxFilesW; } else { this.filesW > this.minFilesW + 1 ? this.filesW -= Math.ceil((this.filesW - this.minFilesW) / 2) : this.filesW = this.minFilesW; } if (("expand" === expandContract && this.filesW === this.maxFilesW) || ("contract" === expandContract && this.filesW === this.minFilesW)) { clearInterval(this.changeFilesInt); } // Redo the layout to match this.setLayout(); }, // Can click-drag file manager width? canResizeFilesW: function() { // If we have the cursor set we must be able! if (true === this.ready && "w-resize" === document.body.style.cursor) { // If our mouse is down (and went down on the CM instance's gutter) and we're within a 250px - half of avail width range if (true === this.mouseDown && "gutter" === this.mouseDownInCM) { this.filesW = this.maxFilesW = this.mouseX >= 250 && this.mouseX <= window.innerWidth / 2 ? this.mouseX : this.mouseX < 250 ? 250 : window.innerWidth / 2; // Set various widths based on the new width this.files.style.width = this.filesFrame.style.width = this.filesW + "px"; this.setLayout(); this.draggingFilesW = true; } } else { this.draggingFilesW = false; } }, // Lock & unlock the file manager navigation on demand lockUnlockNav: function() { let lockIconOpen, lockIconClosed; lockIconOpen = this.filesFrame.contentWindow.document.getElementById('fmLockOpen'); lockIconClosed = this.filesFrame.contentWindow.document.getElementById('fmLockClosed'); this.lockedNav = false === this.lockedNav; lockIconOpen.style.display = this.lockedNav ? "none" : "inline-block"; lockIconClosed.style.display = this.lockedNav ? "inline-block" : "none"; }, // Show/hide the plugins on demand showHidePlugins: function(vis) { get('plugins').style.width = "show" === vis ? '55px' : '3px'; get('plugins').style.background = "show" === vis ? '#333' : 'transparent'; if ("show" === vis) { this.changeFilesW('expand'); } }, // ====== // EDITOR // ====== // Set editor stats setEditorStats: function() { this.getCaretPosition(); this.updateCharDisplay(); this.updateByteDisplay(); }, // On focus cMonFocus: function(thisCM, cMinstance) { this.setEditorStats(); this.editorFocusInstance = cMinstance; this.getCaretPosition(); }, // On blur cMonBlur: function(thisCM, cMinstance) { // Nothing as yet }, // On key up cMonKeyUp: function(thisCM, cMinstance, evt) { let key; key = evt.keyCode ?? evt.which ?? evt.charCode; // Return true (to continue) if we're not CTRL+dragging and // we have CTRL/Cmd key down, or if it's the CTRL key, or Cmd key now up if ("CTRL" !== this.draggingWithKey && (this.ctrlCmdKeyDown(evt) || 17 === key || this.isCmdKey(key))) { return true; } this.setEditorStats(); }, // On cursor activity cMonCursorActivity: function(thisCM, cMinstance) { let thisCMPrevLine; this.setEditorStats(); thisCM.removeLineClass(this['cMActiveLine'+cMinstance], "background"); if(thisCM.getCursor('start').line === thisCM.getCursor().line) { this['cMActiveLine' + cMinstance] = thisCM.addLineClass(thisCM.getCursor().line, "background", "cm-s-activeLine"); } if ("CSS" === this.caretLocType) { this.cssColorPreview(); } thisCMPrevLine = this.editorFocusInstance.indexOf('diff') > -1 ? this.prevLineDiff : this.prevLine; if (thisCMPrevLine !== thisCM.getCursor().line && thisCM.getLine(thisCMPrevLine) && thisCM.getLine(thisCMPrevLine).length > 0 && thisCM.getLine(thisCMPrevLine).replace(/\s/g, '').length === 0) { thisCM.replaceRange("", {line: thisCMPrevLine, ch: 0}, {line: thisCMPrevLine, ch: 1000000}, "+input"); } // Set the cursor to text height, not line height and update any old highlighted results message setTimeout(function(ic) { let paneMatch; // Loop through styles to check if we have to adjust cursor height for (let i = 0; i < ic.renderLineStyle.length; i++) { // We have no matching pane to start with paneMatch = false; // Is the pane we need to set the cursor on this pane? if ( ("diff" !== ic.renderLineStyle[i][0] && 1 === cMinstance.indexOf("diff")) || ("diff" === ic.renderLineStyle[i][0] && -1 < cMinstance.indexOf("diff")) ) {paneMatch = true;} // If the pane matches & also the line we're on is the line we have a style set for, set that cursor height if (paneMatch && thisCM.getCursor().line + 1 === ic.renderLineStyle[i][1]) { thisCM.setOption("cursorHeight", thisCM.defaultTextHeight() / thisCM.lineInfo(thisCM.getCursor().line).handle.height); } else { thisCM.setOption("cursorHeight", 1); } } // If we have no selection but still have highlighted result message, run updateResultsDisplay if ("" === thisCM.getSelection() && 0 === get('results').innerHTML.indexOf("Highlighted result")) { ic.updateResultsDisplay('show'); } }, 0, this); }, // On before change cMonBeforeChange: function(thisCM, cMinstance, changeObj, cM) { let sels, tagInfo, tagOpp, thisData; // Get the selections sels = thisCM.listSelections(); // For each of the user selections for (let i = 0; i < sels.length; i++) { // Get the matching tagInfo for current cursor position tagInfo = cM.findMatchingTag(thisCM, sels[i].anchor); // If we're not ending a tag (autocompletion) and we have tagInfo and not undoing/redoing (which handles changes itself) if (0 !== changeObj.text[0].indexOf(">") && "undefined" !== typeof tagInfo && "undo" !== changeObj.origin && "redo" !== changeObj.origin) { // If we also have both open and close tag info if ("undefined" !== typeof tagInfo.open && "undefined" !== typeof tagInfo.close) { // Log the opposite tag info tagOpp = tagInfo.at === "open" ? "close" : "open"; if (null !== tagInfo[tagOpp]) { thisData = tagInfo[tagOpp].tag + ";" + tagInfo[tagOpp].from.line + ":" + tagInfo[tagOpp].from.ch; // Check that string firstly isn't in array and if not, push it in if (-1 === this.oppTagReplaceData.indexOf(thisData)) { this.oppTagReplaceData.push(thisData); } } } } } }, // On change cMonChange: function(thisCM, cMinstance, changeObj, cM) { let sels, rData, theTag, thisLine, thisChar, tagInfo, charDiff, closeDiff, repl1, repl2, filepath, filename, fileExt; // Get the selections sels = thisCM.listSelections(); // If we're not loading the file, it's a change, so update tab if (false === this.loadingFile) { this.redoTabHighlight(this.selectedTab); } // Detect if we have a scrollbar & set layout again setTimeout(function(ic) { ic.scrollBarVisible = thisCM.getScrollInfo().height > thisCM.getScrollInfo().clientHeight; ic.setLayout(); }, 0, this); // If we're replacing opposite tag strings, do that if ("undefined" !== typeof this.oppTagReplaceData[0]) { // For each one of them, grab our data to work with for (let i = 0; i < this.oppTagReplaceData.length; i++) { // Extract data from that string rData = this.oppTagReplaceData[i].split(";"); theTag = rData[0]; thisLine = parseInt(rData[1].split(":")[0], 10); thisChar = parseInt(rData[1].split(":")[1], 10); // Get the tag info for matching tag if (sels[i]) { tagInfo = cM.findMatchingTag(thisCM, sels[i].anchor); } // If we have tagInfo if ("undefined" !== typeof tagInfo) { // Get the opposite tag string theTag = "open" === tagInfo.at ? tagInfo.open.tag : tagInfo.close.tag; // If we have changeObj.from info to work with if ("undefined" !== typeof changeObj.from) { // Same line changing needs a character pos shift charDiff = thisLine === changeObj.from.line ? changeObj.text[0].length - changeObj.removed[0].length : 0; // Also need to adjust if we're in the close tag on same line closeDiff = "close" === tagInfo.at && thisLine === changeObj.from.line ? changeObj.removed[0].length - changeObj.text[0].length + 1 : 1; // Work out the replace from and to positions repl1 = {line: thisLine, ch: thisChar + charDiff+("open" === tagInfo.at ? 2 : closeDiff)}; repl2 = {line: thisLine, ch: thisChar + charDiff+("open" === tagInfo.at ? 2 : closeDiff) + rData[0].length}; } } // Replace our string over the range, if this token string isn't blank and the end tag matches our original tag if ("" !== theTag.trim() && "undefined" !== typeof repl1 && "undefined" !== typeof repl2 && thisCM.getRange(repl1,repl2) === rData[0]) { thisCM.replaceRange(theTag, repl1, repl2, "+input"); // If at the close tag, don't autocomplete if (tagInfo.at === "close") { this.autocompleteSkip = true; } } } } // Reset our array for next time and redo editor stats this.oppTagReplaceData = []; this.setEditorStats(); // Update the list of functions and classes this.updateFunctionClassList(); filepath = this.openFiles[this.selectedTab - 1]; if (filepath) { filename = filepath.substr(filepath.lastIndexOf("/") + 1); fileExt = filename.substr(filename.lastIndexOf(".") + 1); } // Update diffs if we have a split pane if (true === this.splitPane) { // Need 0ms tickover so we handle char change first setTimeout(function(ic){ic.updateDiffs();}, 0, this); } // Highlight Git diff colors in gutter if (this.indexData) { this.highlightGitDiffs(); } // Clear any previous doFindTimeout var if (undefined !== typeof this.doFindTimeout) { clearInterval(this.doFindTimeout); } // If we have something to find in this document, find in 50 ms (unless cancelled by another keypress) if ("" !== get('find').value && t['this document'] === document.findAndReplace.target.value) { this.doFindTimeout = setTimeout(function (ic) { ic.findReplace(get('find').value, false, false, false); }, 50, this); } // Update HTML edited files live if (filepath && this.previewWindow.location && filepath !== "/[NEW]") { this.updatePreviewWindow(thisCM, filepath, filename, fileExt); } // Update the title tag to indicate any changes this.indicateChanges(); }, // On update cMonUpdate: function(thisCM, cMinstance) { // Nothing as yet }, // On scroll cMonScroll: function(thisCM, cMinstance) { let cM, cMdiff, otherCM; if (true === this.splitPane) { // Get both main & diff instance and work out the instance we're not scrolling cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); otherCM = cMinstance.indexOf('diff') > -1 ? cM : cMdiff; if (cM) { // Scroll other pane x & y to match this one we're scrolling, after a tickover to avoid judder // 0ms = drag scrollbar, 50 = mouse wheel setTimeout(function(){otherCM.scrollTo(thisCM.getScrollInfo().left, thisCM.getScrollInfo().top);}, true === this.mouseDown ? 0 : 50); } } }, // On input read cMonInputRead: function(thisCM, cMinstance) { if ("kepypress" === this.autoComplete && this.codeAssist) { if (!thisCM.state.completionActive) { if (!this.autocompleteSkip) { this.autocomplete(); } else { this.autocompleteSkip = false; } } } }, // On gutter click cMonGutterClick: function(thisCM, line, gutter, evt, cMinstance) { this.mouseDownInCM = "gutter"; }, // On mouse down cMonMouseDown: function(thisCM, cMinstance, evt) { this.mouseDownInCM = "editor"; }, // On paste cMonPaste: function(thisCM, cMinstance, evt, clipboardData) { // Get text from clipboard, the number of lines (excluding last) // and so the startLine and endLine and auto-indent for that range const text = clipboardData.getData('Text'); const num = text.split("\n").length - 1; const startLine = thisCM.getCursor().line; const endLine = startLine + num; // Now auto-indent after a 0ms tickover setTimeout(() => { parent.ICEcoder.autoIndentLines(startLine, endLine); }, 0); }, // On context menu cMonContextMenu: function(thisCM, cMinstance, evt) { // Set cursor const currCoords = thisCM.coordsChar({left: evt.pageX, top: evt.pageY}); thisCM.setCursor(currCoords); // If CTRL key down if (evt.ctrlKey) { setTimeout(function(ic) { ic.jumpToDefinition(); }, 0, this); } }, // On drag over cMonDragOver: function(thisCM, evt, cMinstance) { this.setDragCursor(evt, 'editor'); }, // On render line cMonRenderLine: function(thisCM, cMinstance, line, element) { let paneMatch; // Loop through styles to use when rendering lines for (let i = 0; i < this.renderLineStyle.length; i++) { // We have no matching pane to start with paneMatch = false; // Is the pane we need to style this pane? if ( ("diff" !== this.renderLineStyle[i][0] && -1 === cMinstance.indexOf("diff")) || ("diff" === this.renderLineStyle[i][0] && -1 < cMinstance.indexOf("diff")) ) {paneMatch = true;} // If the pane matches & also the line we're rendering is the line we have a style set for, set that style if (paneMatch && thisCM.lineInfo(line).line + 1 == this.renderLineStyle[i][1]) { element.style[this.renderLineStyle[i][2]] = this.renderLineStyle[i][3]; } } }, // Show function args tooltip functionArgsTooltip: function(e, area) { let numLintErrors; if (this.indexData && this.indexData.functions) { // If we have no files open, return early if (0 === this.openFiles.length) { get('tooltip').style.display = "none"; return true; } let i; // Get cM instance, and the word under mouse pointer const cM = this.getcMInstance(); const coordsChar = cM.coordsChar({left: this.mouseX - this.maxFilesW, top: this.mouseY - 72}); const word = (cM.getRange(cM.findWordAt(coordsChar).anchor, cM.findWordAt(coordsChar).head)); // If it's not a word, return early if ("" === word) { get('tooltip').style.display = "none"; return true; } // Get result and number of results for word in functions from index JSON object list let result = null; let numResults = 0; const filePath = this.openFiles[this.selectedTab - 1]; const filePathExt = filePath.substr(filePath.lastIndexOf(".") + 1); for(i in this.indexData.functions[filePathExt]) { if (i === word) { result = this.indexData.functions[filePathExt][i]; numResults++; } } // If we have a single result and the mouse pointer is not over the definition of it (that would be pointless), show tooltip if (1 === numResults && -1 === [null, "def"].indexOf(cM.getTokenTypeAt(coordsChar))) { get('tooltip').style.display = "block"; get('tooltip').style.left = (this.mouseX + 10) + "px"; numLintErrors = this.content.contentWindow.document.getElementsByClassName("CodeMirror-lint-tooltip")[0]; numLintErrors = numLintErrors && numLintErrors.childNodes ? numLintErrors.childNodes.length : 0; get('tooltip').style.top = (this.mouseY - 30 - (0 < numLintErrors ? 18 * numLintErrors : 0) ) + "px"; get('tooltip').style.zIndex = 1000; // Limit function args list to 200 char max if (result.params.length > 200) { result.params = result.params.substr(0, 200) + "...)"; } get('tooltip').innerHTML = result.params; // Else hide it } else { get('tooltip').style.display = "none"; } } }, // Update diffs shown to the user in each pane updateDiffs: function() { let cM, cMdiff, mainText, diffText, sm, opcodes, cMmarks, cMdiffMarks, amt, sDiffs; // Reset the style array container and main vs diff pane shift difference this.renderLineStyle = []; this.renderPaneShiftAmount = 0; cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); // Get the baseText and newText values from the two textboxes, and split them into lines mainText = cM ? difflib.stringAsLines(cM.getValue()) : ""; diffText = cMdiff ? difflib.stringAsLines(cMdiff.getValue()) : ""; // Create a SequenceMatcher instance that diffs the two sets of lines sm = new difflib.SequenceMatcher(mainText, diffText); // Get the opcodes from the SequenceMatcher instance // Opcodes is a list of 3-tuples describing what changes should be made to the base text in order to yield the new text opcodes = sm.get_opcodes(); if (cM) { // Clear all main pane marks cMmarks = cM.getAllMarks(); for (let i = 0; i < cMmarks.length; i++) { cMmarks[i].clear(); } // Clear all diff pane marks cMdiffMarks = cMdiff.getAllMarks(); for (let i = 0; i < cMdiffMarks.length; i++) { cMdiffMarks[i].clear(); } } if (cM && "" !== cMdiff.getValue()) { // For each opcode returned by jsdifflib for (let i = 0; i < opcodes.length; i++) { // If not 'equal' status for the section, we have a 'replace', 'delete' or 'insert' status, so do something if ("equal" !== opcodes[i][0]) { // ========= // MAIN PANE // ========= // Replacing? Pad out main pane line to match equivalent last line in diff pane if ("replace" === opcodes[i][0]) { // Line amount is diff between end of both panes at this point in our loop, plus 1 line and our overall document shift, multiplied by font size amt = ((opcodes[i][4] - opcodes[i][2] + 1 + this.renderPaneShiftAmount) * cM.defaultTextHeight()); // Add on the extra heights for any wrapped lines for (let j = opcodes[i][4] - 1; j <= opcodes[i][2] - 1; j++) { if (cMdiff.getLineHandle(j).height > cM.defaultTextHeight()) { amt += cMdiff.getLineHandle(j).height - cM.defaultTextHeight(); } } // If we have an height greater than the default text height, add a new style if (amt > cM.defaultTextHeight()) { this.renderLineStyle.push(["main", opcodes[i][2], "height", amt + "px"]); } // Mark text in 2 colours, for each line for (let j = 0; j<(opcodes[i][2]) - (opcodes[i][1]); j++) { sDiffs = (this.findStringDiffs(cM.getLine(opcodes[i][1] + j),cMdiff.getLine(opcodes[i][3] + j))); cM.markText({line: opcodes[i][1]+j, ch: 0}, {line: opcodes[i][3] + j + this.renderPaneShiftAmount, ch: sDiffs[0]}, {className: "diffGreyLighter"}); cM.markText({line: opcodes[i][1]+j, ch: sDiffs[0]}, {line: opcodes[i][3] + j + this.renderPaneShiftAmount, ch: sDiffs[0] + sDiffs[1]}, {className: "diffGrey"}); cM.markText({line: opcodes[i][1]+j, ch: sDiffs[0] + sDiffs[1]}, {line: opcodes[i][3] + j + this.renderPaneShiftAmount, ch: 1000000}, {className: "diffGreyLighter"}); } // Inserting } else { cM.markText({line: opcodes[i][1], ch: 0}, {line: opcodes[i][2] - 1, ch: 1000000}, {className: "diffGreen"}); } // If inserting or deleting and the main pane hasn't changed, we need to pad out the line in that pane if ("replace" !== opcodes[i][0] && opcodes[i][1] === opcodes[i][2]) { this.renderLineStyle.push(["main", opcodes[i][2], "height", ((opcodes[i][4] - opcodes[i][3] + 1) * cM.defaultTextHeight()) + "px"]); // Mark the range with empty class cM.markText({line: opcodes[i][2] - 1, ch: 0}, {line: opcodes[i][2]-1, ch: 1000000}, {className: "diffNone"}); } // ========= // DIFF PANE // ========= // Replacing? Pad out diff pane line to match equivalent last line in main pane if ("replace" === opcodes[i][0]) { // Line amount is diff between end of both panes at this point in our loop, plus 1 line and our overall document shift, multiplied by font size amt = ((opcodes[i][2] - opcodes[i][4] + 1 - this.renderPaneShiftAmount) * cM.defaultTextHeight()); // Add on the extra heights for any wrapped lines for (let j = opcodes[i][4] - 1; j <= opcodes[i][2] - 1; j++) { if (cM.getLineHandle(j).height > cM.defaultTextHeight()) { amt += cM.getLineHandle(j).height - cM.defaultTextHeight(); } } // If we have an height greater than the default text height, add a new style if (amt > cM.defaultTextHeight()) { this.renderLineStyle.push(["diff", opcodes[i][4], "height", amt + "px"]); } // Mark text in 2 colours, for each line for (let j = 0; j<(opcodes[i][4]) - (opcodes[i][3]); j++) { sDiffs = (this.findStringDiffs(cM.getLine(opcodes[i][1] + j),cMdiff.getLine(opcodes[i][3] + j))); cMdiff.markText({line: opcodes[i][1] + j - this.renderPaneShiftAmount, ch: 0}, {line: opcodes[i][3] + j, ch: sDiffs[0]}, {className: "diffGreyLighter"}); cMdiff.markText({line: opcodes[i][1] + j - this.renderPaneShiftAmount, ch: sDiffs[0]}, {line: opcodes[i][3] + j, ch: sDiffs[0] + sDiffs[2]}, {className: "diffGrey"}); cMdiff.markText({line: opcodes[i][1] + j - this.renderPaneShiftAmount, ch: sDiffs[0] + sDiffs[2]}, {line: opcodes[i][3] + j, ch: 1000000}, {className: "diffGreyLighter"}); } // Deleting } else { cMdiff.markText({line: opcodes[i][3], ch: 0}, {line: opcodes[i][4] - 1, ch: 1000000}, {className: "diffRed"}); } // If inserting or deleting and the diff pane hasn't changed, we need to pad out the line in that pane if ("replace" !== opcodes[i][0] && opcodes[i][3] === opcodes[i][4]) { this.renderLineStyle.push(["diff", opcodes[i][4], "height", ((opcodes[i][2] - opcodes[i][1] + 1) * cM.defaultTextHeight()) + "px"]); // Mark the range with empty class cMdiff.markText({line: opcodes[i][4] - 1, ch: 0}, {line: opcodes[i][4] - 1, ch: 1000000}, {className: "diffNone"}); } // Finally, set the last amount shifted for this change this.renderPaneShiftAmount = (opcodes[i][2] - opcodes[i][4]); } } } }, // Find diffs between 2 strings findStringDiffs: function(a, b) { if ("undefined" == typeof a) {a = ""} if ("undefined" == typeof b) {b = ""} for (var c = 0, // start from the first character d = a.length, e = b.length; // and from the last characters of both strings a[c] && // if not at the end of the string and a[c] === b[c]; // if both strings are equal at this position c++); // go forward for (; d > c & e > c & // stop at the position found by the first loop a[d - 1] === b[e - 1]; // if both strings are equal at this position d--) e--; // go backward return[c, d - c, e - c] // return position and lengths of the two substrings found }, // Highlight git diffs (between what is in browser and in Git commits) highlightGitDiffs: function() { // Clear the timeout if we have one already if ("undefined" !== typeof highlightGitDiffTimeout) { clearTimeout(highlightGitDiffTimeout); } // If we have index data & Git data, after a timeout, if we have a matching path in that Git data if (this.indexData && this.indexData.gitContent) { highlightGitDiffTimeout = setTimeout(function(ic) { if (ic.indexData.gitContent[docRoot + ic.openFiles[ic.selectedTab - 1]]) { // Get the CodeMirror instance and clear the gutter for it cM = ic.getcMInstance(); cM.clearGutter("CodeMirror-linenumbers"); // Get the baseText and gitText values from the two sources, and split them into lines const mainText = cM ? difflib.stringAsLines(cM.getValue()) : ""; const gitText = difflib.stringAsLines(ic.indexData.gitContent[docRoot + ic.openFiles[ic.selectedTab - 1]].lastHashContent ?? ""); // Create a SequenceMatcher instance that diffs the two sets of lines const sm = new difflib.SequenceMatcher(gitText, mainText); // Get the opcodes from the SequenceMatcher instance // Opcodes is a list of 3-tuples describing what changes should be made to the base text in order to yield the new text const opcodes = sm.get_opcodes(); // For each opcode returned by jsdifflib for (let i = 0; i < opcodes.length; i++) { // If not 'equal' status for the section, we have a 'replace', 'delete' or 'insert' status, so do something if ("equal" !== opcodes[i][0]) { // Replacing if ("replace" === opcodes[i][0]) { // Mark text in one of 2 colours, for each line for (let j = opcodes[i][3]; j < opcodes[i][4]; j++) { let elem = document.createElement("DIV"); elem.className = "CodeMirror-linenumber"; // Only trim whitespace is different, grey if (gitText[j - (opcodes[i][4] - opcodes[i][2])] && mainText[j].trim() === gitText[j - (opcodes[i][4] - opcodes[i][2])].trim()) { elem.style.background = "#888"; // Something other than whitespace is different, orange } else { elem.style.background = "#f80"; } elem.style.color = "#111"; elem.innerHTML = j + 1; cM.setGutterMarker(j, "CodeMirror-linenumbers", elem); } // Inserting } else if ("insert" === opcodes[i][0]) { // Mark text in green for each line for (let j = opcodes[i][3]; j < opcodes[i][4]; j++) { let elem = document.createElement("DIV"); elem.className = "CodeMirror-linenumber"; elem.style.background = "#080"; elem.style.color = "#fff"; elem.innerHTML = j + 1; cM.setGutterMarker(j, "CodeMirror-linenumbers", elem); } // Deleting } else { // Add a red line to indicate where lines where deleted let elem = document.createElement("DIV"); elem.className = "CodeMirror-linenumber"; // If we haven't deleted content at end, line is above numbers if (cM.lineCount() > opcodes[i][3]) { elem.style.borderTop = "solid #b00 1px"; elem.innerHTML = opcodes[i][3] + 1; cM.setGutterMarker(opcodes[i][3], "CodeMirror-linenumbers", elem); // Otherwise, line is below last number } else { elem.style.borderBottom = "solid #b00 1px"; elem.innerHTML = opcodes[i][3]; cM.setGutterMarker(opcodes[i][3] - 1, "CodeMirror-linenumbers", elem); } } } } } }, this.loadingFile ? 100 : 0, this); } }, // Update Git diff pane (the diffs between saved content and git commits) updateGitDiffPane: function() { let gitDiffList = ""; get("toolLinkGit").className = 0 < this.indexData.gitDiff.paths.length ? "highlight info" : ""; for (let i = 0; i < this.indexData.gitDiff.paths.length; i++) { gitDiffList += '" + "\n"; } get("git").innerHTML = gitDiffList + "

"; }, // Update preview window content updatePreviewWindow: function(thisCM, filepath, filename, fileExt) { if (this.previewWindow.location.pathname === filepath) { if (-1 < ["htm", "html", "txt"].indexOf(fileExt)) { this.previewWindow.document.documentElement.innerHTML = thisCM.getValue(); } else if (-1 < ["md"].indexOf(fileExt)) { this.previewWindow.document.documentElement.innerHTML = mmd(thisCM.getValue()); } } else if (-1 < ["css"].indexOf(fileExt)) { if (-1 < this.previewWindow.document.documentElement.innerHTML.indexOf(filename)) { let css = thisCM.getValue(); let style = document.createElement('style'); style.type = 'text/css'; style.id = "ICEcoder" + filepath.replace(/\//g,"_"); if (style.styleSheet){ style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } if (this.previewWindow.document.getElementById(style.id)) { this.previewWindow.document.documentElement.removeChild(this.previewWindow.document.getElementById(style.id)); } this.previewWindow.document.documentElement.appendChild(style); } } // Do the pesticide plugin if it exists try {this.doPesticide();} catch(err) {} // Do the stats.js plugin if it exists try {this.doStatsJS('update');} catch(err) {} // Do the responsive plugin if it exists try {this.doResponsive();} catch(err) {} }, // Clean up our loaded code contentCleanUp: function() { let thisCM, content; thisCM = this.getThisCM(); // Replace any temp /textarea value content = thisCM.getValue(); content = content.replace(//g, '<\/textarea>'); // Then set the content in the editor & clear the history thisCM.setValue(content); thisCM.clearHistory(); this.savedPoints[this.selectedTab - 1] = thisCM.changeGeneration(); this.savedContents[this.selectedTab - 1] = thisCM.getValue(); }, // Undo last change undo: function() { this.getThisCM().undo(); }, // Redo change redo: function() { this.getThisCM().redo(); }, // Indent more/less indent: function(moreLess) { if ("more" === moreLess) { this.content.contentWindow.CodeMirror.commands.indentMore(this.getThisCM()); } else { this.content.contentWindow.CodeMirror.commands.indentLess(this.getThisCM()); } }, // Indent line range with smart indenting, falls back to indenting to prev line if no smart indenting for mode autoIndentLines: function(startLine, endLine) { for (let i = startLine; i <= endLine; i++) { this.getcMInstance().indentLine(i, "smart"); } }, // Move current line up/down moveLines: function(dir) { let thisCM, lineStart, lineEnd, swapLineNo, swapLine; thisCM = this.getThisCM(); // Get start & end lines plus the line we'll swap with lineStart = thisCM.getCursor('start'); lineEnd = thisCM.getCursor('end'); if ("up" === dir && 0 < lineStart.line) {swapLineNo = lineStart.line - 1} if ("down" === dir && lineEnd.line < thisCM.lineCount() - 1) {swapLineNo = lineEnd.line + 1} // If we have a line to swap with if (!isNaN(swapLineNo)) { // Get the content of the swap line and carry out the swap in a single operation swapLine = thisCM.getLine(swapLineNo); thisCM.operation(function() { // Move lines in turn up if ("up" === dir) { for (let i = lineStart.line; i <= lineEnd.line; i++) { thisCM.replaceRange(thisCM.getLine(i), {line: i - 1, ch: 0}, {line: i - 1, ch: 1000000}, "+input"); } // ...or down } else { for (let i = lineEnd.line; i >= lineStart.line; i--) { thisCM.replaceRange(thisCM.getLine(i), {line: i + 1, ch: 0}, {line: i + 1, ch: 1000000}, "+input"); } } // Now swap our final line with the swap line to complete the move thisCM.replaceRange(swapLine, {line: "up" === dir ? lineEnd.line : lineStart.line, ch: 0}, {line: "up" === dir ? lineEnd.line : lineStart.line, ch: 1000000}, "+input"); // Finally set the moved selection thisCM.setSelection( {line: lineStart.line + ("up" === dir ? -1 : 1), ch: lineStart.ch}, {line: lineEnd.line + ("up" === dir ? -1 : 1), ch: lineEnd.ch} ); // Auto-indent the lines we're moving (but not the swapped line) ICEcoder.autoIndentLines(lineStart.line - ("up" === dir ? 1 : -1), lineEnd.line + ("up" === dir ? -1 : 1)); }) } }, // Highlight specified line highlightLine: function(line) { let thisCM; thisCM = this.getThisCM(); thisCM.setSelection({line: line, ch: 0}, {line: line, ch: thisCM.lineInfo(line).text.length}); }, // Focus the editor focus: function(diff) { let cM, cMdiff, thisCM; if (!(/iPhone|iPad|iPod/i.test(navigator.userAgent))) { cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); thisCM = diff ? cMdiff : cM; if (thisCM) { thisCM.focus(); } } }, // Go to a specific line number goToLine: function(lineNo, charNo, noFocus) { let thisCM, hiddenLines, tgtLineNo; // Return early if trying to find line and no files open if (0 === this.openFiles.length) { return false; } lineNo = lineNo ? lineNo - 1 : get('goToLineNo').value - 1; charNo = charNo ? charNo : 0; thisCM = this.getThisCM(); this.scrollingOnLine = thisCM.getCursor().line; // Clear any existing interval handling the scrolling if ("undefined" !== typeof this.scrollInt) { clearInterval(this.scrollInt); } // Start array of hidden (folded away) lines we should ignore hiddenLines = []; // Get all marks const allMarks = thisCM.getAllMarks(); if ("undefined" !== typeof allMarks[0]) { // For each of the marks, if it's a fold type marker for (let i = 0; i < allMarks.length; i++) { if (true === allMarks[i].__isFold) { // Get the line number of first child in marker range const firstLine = thisCM.getLineNumber(allMarks[i].lines[0]); // For each of the children in marker range, after first (as that's still visible, subsequent lines not) for (let j = 1; j < allMarks[i].lines.length; j++) { // If not already in hidden lines array we need to also ignore (this check covers nested folds) if (-1 === hiddenLines.indexOf(firstLine + j + 1)) { hiddenLines.push(firstLine + j + 1); } } } } } // The target line is same as requested line, for now tgtLineNo = lineNo; // Go through each of the lines that are folded away (hidden), if the number is less // than the requested line number, deduct 1 as it's hidden for (let i = 0; i < hiddenLines.length; i++) { if (hiddenLines[i] < lineNo) { tgtLineNo--; } } this.scrollInt = setInterval(function(ic) { // Aim for this step to go 1/5th towards the target line we want as next scroll "step" // 1 = instant, 20 = very slow ic.scrollingOnLine = ic.scrollingOnLine + ((tgtLineNo - ic.scrollingOnLine) / ICEcoder.goToLineScrollSpeed); // Scroll on the Y axis to the pixels in this step + 8 to handle margin - 1/10th of editor visible height thisCM.scrollTo(0, (thisCM.defaultTextHeight() * ic.scrollingOnLine) + 8 - (thisCM.getScrollInfo().clientHeight / 10)); // Clear interval if we're at the target line now if (tgtLineNo === Math.round(ic.scrollingOnLine)) { clearInterval(ic.scrollInt); } }, 10, this); thisCM.setCursor(lineNo, charNo); if (!noFocus) { this.focus(); // Also do this after a 0ms tickover incase DOM wasn't ready setTimeout(function(ic) {ic.focus();}, 0, this); } return false; }, // Comment/uncomment line or selected range on keypress lineCommentToggle: function() { let thisCM, cursorPos, linePos, lineContent, lCLen; thisCM = this.getThisCM(); cursorPos = thisCM.getCursor().ch; linePos = thisCM.getCursor().line; lineContent = thisCM.getLine(linePos); lCLen = lineContent.length; this.lineCommentToggleSub(thisCM, cursorPos, linePos, lineContent, lCLen); }, // Wrap our selected text/cursor with tags tagWrapper: function(tag) { let thisCM, tagStart, tagEnd, startLine, endLine; thisCM = this.getThisCM(); tagStart = tag; tagEnd = tag; if ('div' === tag) { startLine = thisCM.getCursor('start').line; endLine = thisCM.getCursor().line; thisCM.operation(function() { thisCM.replaceSelection("
\n" + thisCM.getSelection() + "\n
", "around"); for (let i = startLine + 1; i <= endLine + 1; i++) { thisCM.indentLine(i); } thisCM.indentLine(endLine + 2, 'prev'); thisCM.indentLine(endLine + 2, 'subtract'); }); } else { if ( -1 < ['p', 'a', 'h1', 'h2', 'h3'].indexOf(tag) && thisCM.getSelection().substr(0,tag.length + 1) === "<" + tagStart && thisCM.getSelection().substr(-(tag.length + 3)) === "") { // Undo wrapper thisCM.replaceSelection( thisCM.getSelection().substr(thisCM.getSelection().indexOf(">") + 1, thisCM.getSelection().length-thisCM.getSelection().indexOf(">") - 1 - tag.length - 3), "around"); } else { if ("a" === tag) {tagStart = 'a href=""';} // Do wrapper thisCM.replaceSelection("<" + tagStart + ">" + thisCM.getSelection() + "", "around"); if ("a" === tag) {thisCM.setCursor({line: thisCM.getCursor('start').line, ch: thisCM.getCursor('start').ch + 9})} } } }, // Add a line break at end of current or specified line addLineBreakAtEnd: function(line) { let thisCM; thisCM = this.getThisCM(); if (!line) {line = thisCM.getCursor().line} thisCM.replaceRange(thisCM.getLine(line) + "
", {line: line, ch: 0}, {line : line, ch: 1000000}, "+input"); }, // Insert a line before and auto-indent insertLineBefore: function(line) { let thisCM; thisCM = this.getThisCM(); if (!line) {line = thisCM.getCursor().line} thisCM.operation(function() { thisCM.replaceRange("\n" + thisCM.getLine(line), {line: line, ch: 0}, {line: line, ch: 1000000}, "+input"); thisCM.setCursor({line: thisCM.getCursor().line - 1, ch: 0}); thisCM.execCommand('indentAuto'); }); }, // Insert a line after and auto-indent insertLineAfter: function(line) { let thisCM; thisCM = this.getThisCM(); if (!line) {line = thisCM.getCursor().line} thisCM.operation(function() { thisCM.replaceRange(thisCM.getLine(line) + "\n", {line: line, ch: 0}, {line: line, ch: 1000000}, "+input"); thisCM.execCommand('indentAuto'); }); }, // Duplicate line duplicateLines: function(line) { let thisCM, ch, lineExtra, userSelStart, userSelEnd; thisCM = this.getThisCM(); if (!line && thisCM.somethingSelected()) { userSelStart = thisCM.getCursor('start'); userSelEnd = thisCM.getCursor('end'); lineExtra = userSelStart.line !== userSelEnd.line && userSelEnd.ch === thisCM.getLine(userSelEnd.line).length ? "\n" : ""; thisCM.replaceSelection(thisCM.getSelection() + lineExtra+thisCM.getSelection(), "end"); thisCM.setSelection(userSelStart, userSelEnd); } else { if (!line) {line = thisCM.getCursor().line} ch = thisCM.getCursor().ch; thisCM.replaceRange(thisCM.getLine(line) + "\n" + thisCM.getLine(line), {line: line, ch: 0}, {line: line, ch: 1000000}, "+input"); thisCM.setCursor(line + 1, ch); } }, // Remove line removeLines: function(line) { let thisCM, ch; thisCM = this.getThisCM(); if (!line && thisCM.somethingSelected()) { thisCM.replaceSelection("", "end"); } else { if (!line) {line = thisCM.getCursor().line} ch = thisCM.getCursor().ch; thisCM.execCommand('deleteLine'); thisCM.setCursor(line - 1, ch); } }, // Jump to and highlight the function definition current token or back again to where we were jumpToDefinition: function() { let thisCM, word, cursorBack1Char, result, numResults, filePath, filePathExt; thisCM = this.getThisCM(); // We have an original cursor or selection position, so we'll jump back to it if (false !== this.origCursorPos || false !== this.origSelectionPos) { // Jump to the original position (selection/cursor) we have and set selection/cursor if (false !== this.origSelectionPos) { this.goToLine(this.origSelectionPos.anchor.line + 1); thisCM.setSelection(this.origSelectionPos.anchor, this.origSelectionPos.head); } else { this.goToLine(this.origCursorPos.line + 1); thisCM.setCursor(this.origCursorPos); } // Reset flags for next time this.origSelectionPos = false; this.origCursorPos = false; } else { // Set flags for original section or cursor so we can return next time if (thisCM.listSelections()[0]) { this.origSelectionPos = thisCM.listSelections()[0]; } else { this.origCursorPos = thisCM.getCursor(); } // Get word word = thisCM.getRange(thisCM.findWordAt(thisCM.getCursor()).anchor, thisCM.findWordAt(thisCM.getCursor()).head); // If we got parens, try back 1 character to get word if (-1 < word.indexOf("(")) { cursorBack1Char = {line: thisCM.getCursor().line, ch: thisCM.getCursor().ch -1}; word = thisCM.getRange(thisCM.findWordAt(cursorBack1Char).anchor, thisCM.findWordAt(cursorBack1Char).head); } // Set start point for result and number of results for word in functions and classes from index JSON object list result = null; numResults = 0; // Identify the file path and extension filePath = this.openFiles[this.selectedTab - 1]; filePathExt = filePath.substr(filePath.lastIndexOf(".") + 1); // Find the word within extention type in functions list data if ("undefined" !== typeof this.indexData.functions) { for(i in this.indexData.functions[filePathExt]) { if (i === word) { result = this.indexData.functions[filePathExt][i]; numResults++; } } } // Find the word within extention type in classes list data if ("undefined" !== typeof this.indexData.class) { for (i in this.indexData.classes[filePathExt]) { if (i === word) { result = this.indexData.classes[filePathExt][i]; numResults++; } } } // If we have a single result and the cursor isn't already on the definition of it, we can jump to where it's defined if (1 === numResults && -1 === [null, "def"].indexOf(thisCM.getTokenTypeAt(thisCM.getCursor()))) { // Open file (or switch tab to it if already open) and find when editor showing this.openFile(result.filePath.replace(docRoot, "")); this.goFindAfterOpenInt = setInterval(function(result) { if (ICEcoder.openFiles[ICEcoder.selectedTab - 1] == result.filePath.replace(docRoot, "") && !ICEcoder.loadingFile) { thisCM = ICEcoder.getcMInstance(); setTimeout(function(result) { ICEcoder.goToLine(result.range.from.line + 1); thisCM.setSelection({line: result.range.from.line, ch: result.range.from.ch}, {line: result.range.to.line, ch: result.range.to.ch}); }, 20, result); clearInterval(ICEcoder.goFindAfterOpenInt); } }, 20, result); } this.mouseDownInCM = "editor"; } }, // Update function & class list { updateFunctionClassList: function() { let cM; cM = this.getcMInstance(); this.functionClassList = []; if (cM) { // For each line, establish if there's a function or class item on it cM.doc.eachLine(function(handle){ICEcoder.updateFunctionClassListItems(handle)}); } }, // Update function/class list items updateFunctionClassListItems: function(handle) { let cM, functionClassText; cM = this.getcMInstance(); functionClassText = ""; // Get function declaration lines if (handle.text.indexOf("function ") > -1 && handle.text.replace(/\$function/g,"").indexOf("function ") > -1) { functionClassText = handle.text.substring(handle.text.indexOf("function ") + 9); } // Get class declaration lines if (handle.text.indexOf("class ") > -1 && handle.text.replace(/\$class/g,"").indexOf("class ") > -1) { functionClassText = handle.text.substring(handle.text.indexOf("class ") + 6); } // Get just the name of the function/class functionClassText = functionClassText.trim().split("{")[0].split("("); // Push items into array if (functionClassText[0] !== "") { this.functionClassList.push({ line: cM.getLineNumber(handle), name: functionClassText[0], params: "(" + (functionClassText[1] ? functionClassText[1].replace(/[,]/g,", ") : ""), verified: false }); // After a 0ms tickover, verify the item setTimeout(function(ic) { // If we're defining a function/class if (!handle.styles || (-1 < handle.styles && handle.styles.indexOf('def') && cM.getLineNumber(handle))) { // Find our item in the array and mark it as verified for (let i = 0; i < ic.functionClassList.length; i++) { if (ic.functionClassList[i]['line'] == cM.getLineNumber(handle)) { ic.functionClassList[i]['verified'] = true; } }; } }, 0, this); } }, // Autocomplete autocomplete: function() { this.content.contentWindow.CodeMirror.commands.autocomplete(this.getThisCM()); }, // Paste a URL, locally or absolutely if CTRL/Cmd key down pasteURL: function(url) { if("CTRL" === this.draggingWithKey) { url = window.location.protocol + "//" + window.location.hostname + url; } this.getThisCM().replaceSelection(url, "around"); }, // Search for selected text online searchForSelected: function() { let thisCM; thisCM = this.getThisCM(); if (this.caretLocType) { if ("" !== thisCM.getSelection()) { let searchPrefix = this.caretLocType.toLowerCase() + " "; if (this.caretLocType === "Content") { searchPrefix = ""; } window.open("https://www.google.com/search?q=" + searchPrefix + thisCM.getSelection()); } else { this.message(t['No text selected...']); } } }, // Return character num from start of doc to cursor getCharNumFromCursor: function() { return this.getThisCM().indexFromPos(this.getThisCM().getCursor()); }, // Set the cursor according to num of characters from start of doc setCursorByCharNum: function(num) { ICEcoder.getThisCM().setCursor(ICEcoder.getThisCM().posFromIndex(num)); }, // Determine which area of the document we're in caretLocationType: function() { let thisCM, caretLocType, caretChunk, fileName, fileExt; thisCM = this.getThisCM(); caretLocType = "Unknown"; caretChunk = thisCM.getValue().substr(0, this.caretPos + 1); if (caretChunk.lastIndexOf("<\?") > caretChunk.lastIndexOf("?\>") && "Unknown" === caretLocType) {caretLocType = "PHP";} else if (caretChunk.lastIndexOf("<\%") > caretChunk.lastIndexOf("%\>") && "Unknown" === caretLocType) {caretLocType = "Ruby";} else if (caretChunk.lastIndexOf("") > caretChunk.lastIndexOf("<\/script>") && "Unknown" === caretLocType) {caretLocType = "JavaScript";} else if (caretChunk.lastIndexOf(" caretChunk.lastIndexOf("/style>") && "Unknown" === caretLocType) {caretLocType = "CSS";} else if (caretChunk.lastIndexOf("<") > caretChunk.lastIndexOf(">") && "Unknown" === caretLocType) {caretLocType = "HTML";} else if ("Unknown" === caretLocType) {caretLocType = "Content";} fileName = this.openFiles[this.selectedTab - 1]; if ("Content" === caretLocType && fileName) { fileExt = fileName.split("."); fileExt = fileExt[fileExt.length - 1]; caretLocType = fileExt === "js" ? "JavaScript" : fileExt === "json" ? "JSON" : fileExt === "coffee" ? "CoffeeScript" : fileExt === "ts" ? "TypeScript" : fileExt === "py" ? "Python" : fileExt === "mpy" ? "Python" : fileExt === "rb" ? "Ruby" : fileExt === "css" ? "CSS" : fileExt === "less" ? "LESS" : fileExt === "md" ? "Markdown" : fileExt === "xml" ? "XML" : fileExt === "sql" ? "SQL" : fileExt === "yaml" ? "YAML" : fileExt === "java" ? "Java" : fileExt === "erl" ? "Erlang" : fileExt === "jl" ? "Julia" : fileExt === "c" ? "C" : fileExt === "h" ? "C" : fileExt === "cpp" ? "C++" : fileExt === "ino" ? "C++" : fileExt === "cs" ? "C#" : fileExt === "go" ? "Go" : fileExt === "lua" ? "Lua" : fileExt === "pl" ? "Perl" : fileExt === "scss" ? "Sass" : "Content"; } this.caretLocType = caretLocType; }, // Comment/uncomment line or selected range on keypress lineCommentToggleSub: function(cM, cursorPos, linePos, lineContent, lCLen) { let comments, startLine, endLine, commentCH, commentBS, commentBE; // Language specific commenting if (-1 < ["JavaScript", "CoffeeScript", "TypeScript", "PHP", "Python", "Ruby", "CSS", "SQL", "Erlang", "Julia", "Java", "YAML", "C", "C++", "C#", "Go", "Lua", "Perl", "Sass"].indexOf(this.caretLocType)) { comments = { "JavaScript" : ["// ", "/* ", " */"], "CoffeeScript" : ["# ", "### ", " ###"], "TypeScript" : ["// ", "/* ", " */"], "PHP" : ["// ", "/* ", " */"], "Python" : ["# ", "/* ", " */"], "Ruby" : ["# ", "/* ", " */"], "CSS" : ["// ", "/* ", " */"], "SQL" : ["// ", "/* ", " */"], "Erlang" : ["% ", "/* ", " */"], "Julia" : ["# ", "/* ", " */"], "Java" : ["// ", "/* ", " */"], "YAML" : ["# ", "/* ", " */"], "C" : ["// ", "/* ", " */"], "C++" : ["// ", "/* ", " */"], "C#" : ["// ", "/* ", " */"], "Go" : ["// ", "/* ", " */"], "Lua" : ["-- ", "--[[ ", " ]]"], "Perl" : ["# ", "/* ", " */"], "Sass" : ["// ", "/* ", " */"] } // Identify the single line, block start and block end comment chars commentCH = comments[this.caretLocType][0]; commentBS = comments[this.caretLocType][1]; commentBE = comments[this.caretLocType][2]; // Block commenting if (cM.somethingSelected()) { // Language has no block commenting, so repeating singles are needed if (-1 < ["Ruby", "Python", "Erlang", "Julia", "YAML", "Perl"].indexOf(this.caretLocType)) { startLine = cM.getCursor(true).line; endLine = cM.getCursor().line; for (let i = startLine; i <= endLine; i++) { cM.replaceRange(cM.getLine(i).slice(0, commentCH.length) != commentCH ? commentCH + cM.getLine(i) : cM.getLine(i).slice(commentCH.length, cM.getLine(i).length), {line:i, ch:0}, {line:i, ch:1000000}, "+input"); } // Language has block commenting } else { cM.replaceSelection(cM.getSelection().slice(0,commentBS.length) != commentBS ? commentBS + cM.getSelection() + commentBE : cM.getSelection().slice(commentBS.length, cM.getSelection().length - commentBE.length), "around"); } // Single line commenting } else { if (-1 < ["CSS", "SQL"].indexOf(this.caretLocType)) { cM.replaceRange(lineContent.slice(0,commentBS.length) != commentBS ? commentBS + lineContent + commentBE : lineContent.slice(commentBS.length, lCLen - commentBE.length), {line: linePos, ch: 0}, {line: linePos, ch: 1000000}, "+input"); adjustCursor = commentBS.length; if (lineContent.slice(0,commentBS.length) == commentBS) {adjustCursor = -adjustCursor} } else { cM.replaceRange(lineContent.slice(0,commentCH.length) != commentCH ? commentCH + lineContent : lineContent.slice(commentCH.length,lCLen), {line: linePos, ch: 0}, {line: linePos, ch: 1000000}, "+input"); adjustCursor = commentCH.length; if (lineContent.slice(0,commentCH.length) == commentCH) {adjustCursor = -adjustCursor} } } // HTML style commenting } else { if (cM.somethingSelected()) { cM.replaceSelection(cM.getSelection().slice(0,4) !== "<\!--" ? "<\!--" + cM.getSelection() + "//-->" : cM.getSelection().slice(4, cM.getSelection().length - 5),"around"); } else { cM.replaceRange(lineContent.slice(0,4) !== "<\!--" ? "<\!--" + lineContent + "//-->" : lineContent.slice(4, lCLen-5), {line: linePos, ch: 0}, {line: linePos, ch: 1000000}, "+input"); adjustCursor = lineContent.slice(0,4) === "<\!--" ? -4 : 4; } } if (!cM.somethingSelected()) {cM.setCursor(linePos, cursorPos + adjustCursor)} }, // ===== // FILES // ===== // Actions on file manager fmAction: function(evt, action) { let selElem, sPN, fileFolder, goElem; // Get selected elem, the parent node of that, if it's a file/folder and set elem to go to next selElem = get('filesFrame').contentWindow.document.getElementById(this.selectedFiles[this.selectedFiles.length - 1] + "_perms").parentNode; sPN = selElem.parentNode; fileFolder = selElem.onmouseover.toString().indexOf("'folder'") > -1 ? "folder" : "file"; goElem = false; if ("up" === action) { if (sPN.previousSibling && sPN.previousSibling.previousSibling) { // Jump to previous sibling goElem = sPN.previousSibling.previousSibling; if ("UL" === goElem.tagName) { // Jump to last item in previous sibling dir goElem = goElem.childNodes[goElem.childNodes.length - 1]; } } else if (sPN.parentNode.previousSibling) { // Jump to parent dir goElem = sPN.parentNode.previousSibling; } if (goElem) {goElem = goElem.childNodes[0]} } if ("down" === action) { if (sPN.nextSibling && sPN.nextSibling.childNodes[0]) { // Jump to first item in dir goElem = sPN.nextSibling.childNodes[0]; } else if (sPN.nextSibling && sPN.nextSibling.nextSibling) { // Jump to next sibling goElem = sPN.nextSibling.nextSibling; } else if (sPN.parentNode.nextSibling) { // Jump to next parent sibling item goElem = sPN.parentNode.nextSibling.nextSibling; } if (goElem) {goElem = goElem.childNodes[0]} } if (action == "left") { if ("folder" === fileFolder && sPN.parentNode.previousSibling) { // contract dir this.openCloseDir(selElem,false); } } if ("right" === action || "enter" === action) { "folder" === fileFolder // expand dir ? this.openCloseDir(selElem,true) // open file : this.openFile(selElem.childNodes[1].id.replace(/\|/g, "/")); } if (goElem && goElem.childNodes[1]) { // If we have an elem to go to, select it this.overFileFolder(fileFolder, goElem.childNodes[1].id); this.selectFileFolder(evt); } }, // Open/close dirs on demand openCloseDir: function(dir, load) { let node, d; dir.onclick = function(event) { if(!event.ctrlKey && !this.cmdKey) { ICEcoder.openCloseDir(this, !load); } }; node = dir.parentNode; if (node.nextSibling) {node = node.nextSibling} dir.parentNode.className = dir.className = "pft-directory dirOpen"; if (node && "UL" === node.tagName) { d = "none" === node.style.display; d ? load = true : node.style.display = "none"; dir.parentNode.className = dir.className = "pft-directory"; } if (load) { this.filesFrame.contentWindow.frames['fileControl'].location.href = this.iceLoc + "/lib/get-branch.php?location=" + dir.childNodes[1].id + "&csrf=" + this.csrf; } else if("UL" === node.tagName) { node.parentNode.removeChild(node); } return false; }, // Note which files or folders we are over on mouseover/mouseout overFileFolder: function(type, link) { this.thisFileFolderType = type; this.thisFileFolderLink = link; }, // Note which files or folders we are over on mouseover/mouseout highlightFileFolder: function(link, highlight) { this.filesFrame.contentWindow.document.getElementById(link).style.background = true === highlight ? this.colorDropTgtBGFile : ''; }, // Detect and return dir/file/false for this DOM ref (false for not found) isFileFolder: function(ref) { let domElem; domElem = get('filesFrame').contentWindow.document.getElementById(ref.replace(iceRoot,"").replace(/\/$/, "").replace(/\//g, "|")); if (domElem) { return domElem.parentNode.parentNode.className.indexOf("directory") > -1 ? "folder" : "file"; } else { return false; } }, // Select file or folder on demand selectFileFolder: function(evt, ctrlSim, shiftSim) { let tgtFile, shortURL, selecting, dirList, lastFileClicked, startFile, endFile, thisFileObj; // If we've clicked somewhere other than a file/folder if ("" === this.thisFileFolderLink) { if (!ctrlSim && !evt.ctrlKey && !this.cmdKey) { this.deselectAllFiles(); } } else if (this.thisFileFolderLink) { // Get file URL, with pipes instead of slashes & target DOM elem shortURL = this.thisFileFolderLink.replace(/\//g,"|"); tgtFile = this.filesFrame.contentWindow.document.getElementById(shortURL); // If we have the CTRL/Cmd key down if (ctrlSim || evt.ctrlKey || this.cmdKey) { // Deselect or select file if (-1 < this.selectedFiles.indexOf(shortURL)) { this.selectDeselectFile('deselect', tgtFile); this.selectedFiles.splice(this.selectedFiles.indexOf(shortURL), 1); } else { this.selectDeselectFile('select', tgtFile); this.selectedFiles.push(shortURL); } // Select from last click to this one } else if (shiftSim || evt.shiftKey) { selecting = false; dirList = tgtFile.parentNode.parentNode.parentNode; lastFileClicked = this.selectedFiles[this.selectedFiles.length - 1]; // Prefix numbers with up to 20 leading zeros // This is so we can have some kind of natural comparison on the regex below function prefixer(match, p1, offset, string) { return ('00000000000000000000' + match).substr(-20); } startFile = shortURL.replace(/\d+/g, prefixer) < lastFileClicked.replace(/\d+/g, prefixer) ? shortURL : lastFileClicked; endFile = shortURL.replace(/\d+/g, prefixer) > lastFileClicked.replace(/\d+/g, prefixer) ? shortURL : lastFileClicked; if (0 < this.selectedFiles.length && startFile.substr(0, startFile.lastIndexOf("|")) === endFile.substr(0, endFile.lastIndexOf("|"))) { for (let i = 0; i < 1000000; i += 2) { // Something bad has happened with what we're trying to select, so break if ("undefined" === typeof dirList.childNodes[i] || dirList.childNodes[i].nodeName !== "LI") {break;} thisFileObj = dirList.childNodes[i].childNodes[0].childNodes[1]; if (thisFileObj.id === startFile) { selecting = true; } if (true === selecting && -1 === this.selectedFiles.indexOf(thisFileObj.id)) { this.selectDeselectFile('select', thisFileObj); this.selectedFiles.push(thisFileObj.id); } if (thisFileObj.id === endFile) { break; } } } else { this.selectDeselectFile('select', tgtFile); this.selectedFiles.push(shortURL); } // We are single clicking } else { this.deselectAllFiles(); // Add our URL and highlight the file this.selectDeselectFile('select', tgtFile); this.selectedFiles.push(shortURL); } } // Adjust the file & replace select dropdown values accordingly document.findAndReplace.target[2].innerHTML = !this.selectedFiles[0] ? t['all files'] : t['selected files']; document.findAndReplace.target[3].innerHTML = !this.selectedFiles[0] ? t['all filenames'] : t['selected filenames']; // Hide the file menu incase it's showing this.hideFileMenu(); }, // Deselect all files deselectAllFiles: function() { let tgtFile; for (let i = 0; i < this.selectedFiles.length; i++) { tgtFile = this.filesFrame.contentWindow.document.getElementById(this.selectedFiles[i]); this.selectDeselectFile('deselect', tgtFile); } this.selectedFiles.length = 0; }, // Select or deselect file selectDeselectFile: function(action, file) { let isOpen, isCurrent; if (file) { isOpen = -1 < this.openFiles.indexOf(file.id.replace(/\|/g, "/")); isCurrent = this.openFiles[this.selectedTab-1] === file.id.replace(/\|/g, "/"); // Selected dir/file if ("select" === action) { file.style.backgroundColor = this.colorSelectedBG; file.style.color = this.colorSelectedText; // File is current tab } else if (true === isCurrent) { file.style.backgroundColor = this.colorCurrentBG; file.style.color = this.colorCurrentText; // File is open } else if (true === isOpen) { file.style.backgroundColor = this.colorOpenBG; file.style.color = this.colorOpenTextFile; // Dir/file isn't selected } else { file.style.backgroundColor = ''; file.style.color = ''; } } }, // Box select files boxSelect: function(evt, mouseAction) { let fmDragBox, positive; fmDragBox = this.filesFrame.contentWindow.document.getElementById('fmDragBox'); // On mouse down, set start X & Y and reset first and last items in box area select if ("down" === mouseAction) { this.fmDragBoxStartX = this.mouseX; this.fmDragBoxStartY = this.mouseY; this.fmDragSelectFirst = ""; this.fmDragSelectLast = ""; if ("" === this.thisFileFolderLink) { this.deselectAllFiles(); } } // On mouse drag, state we're dragging, set the box size and position properties and select files if(this.mouseDown && !this.mouseDownInCM && "drag" === mouseAction) { this.fmDraggedBox = true; // Handle X-axis properties positive = 0 < this.mouseX - this.fmDragBoxStartX; fmDragBox.style.left = (positive ? this.fmDragBoxStartX : this.mouseX) + "px"; fmDragBox.style.width = Math.abs(this.mouseX - this.fmDragBoxStartX) + "px"; // Handle Y-axis properties positive = 0 < this.mouseY - this.fmDragBoxStartY; fmDragBox.style.top = (positive ? this.fmDragBoxStartY - 70 : this.mouseY - 70) + "px"; fmDragBox.style.height = Math.abs(this.mouseY - this.fmDragBoxStartY) + "px"; // Select the files if ("" !== this.thisFileFolderLink) { if ("" === this.fmDragSelectFirst) { this.fmDragSelectFirst = this.thisFileFolderLink; this.overFileFolder(this.thisFileFolderLink.indexOf('.') > 0 ? 'file' : 'folder', this.fmDragSelectFirst); this.selectFileFolder(evt); } else { this.fmDragSelectLast = this.thisFileFolderLink; this.overFileFolder(this.thisFileFolderLink.indexOf('.') > 0 ? 'file' : 'folder', this.fmDragSelectLast); this.selectFileFolder(evt, false, 'shiftSim'); } } } // On mouse up, set width and height to 0 to hide if("up" === mouseAction) { fmDragBox.style.width = 0; fmDragBox.style.height = 0; } }, // Create a new file (start & trigger save) newFile: function() { this.newTab(true); }, // Create a new folder newFolder: function() { let shortURL, newFolder; shortURL = this.selectedFiles[this.selectedFiles.length - 1].replace(/\|/g, "/"); newFolder = this.getInput('Enter new folder name at ' + shortURL, ''); if (newFolder) { newFolder = (shortURL + "/" + newFolder).replace(/\/\//, "/"); this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=newFolder&csrf=" + this.csrf, encodeURIComponent(newFolder.replace(/\//g, "|"))); this.serverMessage('' + t['Creating Folder'] + ' ' + newFolder.replace(/^\/|/g, '')); } }, // Provide a path and line ref and we return the separate pieces returnFileAndLine: function(fileLink) { let line = 1; const re = /^([^ ]*)\s+(on\s+)?(line\s+)?(\d+)/; const reMatch = re.exec(fileLink); // "on" or "line" word used followed by line number if (null !== reMatch) { line = reMatch[4]; fileLink = reMatch[1]; // :// protocol host separator used } else if (fileLink.indexOf('://') > 0) { // We have a : then number (and : not same index as ://) if (fileLink.lastIndexOf(':') !== fileLink.indexOf('://')) { line = fileLink.split(':')[2]; fileLink = fileLink.substr(0, fileLink.lastIndexOf(":")); } // :/ drive path separator used, likely Windows } else if (fileLink.indexOf(':/') > 0) { // We have a : then number (and : not same index as :/) if (fileLink.lastIndexOf(':') !== fileLink.indexOf(':/')) { line = fileLink.split(':')[2]; fileLink = fileLink.substr(0, fileLink.lastIndexOf(":")); } // We have a : } else if (fileLink.indexOf(':') > 0) { line = fileLink.split(':')[1]; fileLink = fileLink.split(':')[0]; } // () Brackets used if ((fileLink.indexOf('(') > 0) && (fileLink.indexOf(')') > 0)) { line = fileLink.split('(')[1].split(')')[0]; fileLink = fileLink.split('(')[0]; } return [fileLink, line]; }, // Open a file openFile: function(fileLink) { let flSplit, line, shortURL, canOpenFile; if ("undefined" !== typeof fileLink) { flSplit = this.returnFileAndLine(fileLink); fileLink = flSplit[0]; line = flSplit[1]; } else { fileLink = this.thisFileFolderLink; } if ("/[NEW]" !== fileLink && false !== this.isOpen(fileLink)) { this.switchTab(this.isOpen(fileLink) + 1); if (1 < line){ this.goToLine(line); } } else if ("" !== fileLink) { // work out a shortened URL for the file shortURL = fileLink.replace(/\|/g, "/"); // No reason why we can't open a file (so far) canOpenFile = true; // Limit to 100 files open at a time if (100 <= this.openFiles.length) { this.message(t['Sorry you can...']); canOpenFile = false; } // if we're still OK to open it... if (canOpenFile) { if ("/[NEW]" !== shortURL) { fileLink = fileLink.replace(/\//g, "|"); this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=load&file=" + encodeURIComponent(fileLink) + "&csrf=" + this.csrf + "&lineNumber=" + line, encodeURIComponent(fileLink)); this.serverMessage('' + t['Opening File'] + ' ' + shortURL.substr(shortURL.lastIndexOf("/") + 1)); } else { this.createNewTab(true, shortURL); } this.fMIconVis('fMView', 1); } } }, // Open selected files openFilesFromList: function(fileList) { for (let i = 0; i < fileList.length; i++) { this.openFile(fileList[i].replace('|', '/')); } }, // Show file prompt to open file openPrompt: function() { let fileLink; if (fileLink = this.getInput(t['Enter relative file...'], '')) { fileLink.indexOf("://") > -1 ? this.getRemoteFile(fileLink) : this.openFile(fileLink); } }, // Get remote file contents getRemoteFile: function(remoteFile) { let flSplit, line; if ("undefined" !== typeof remoteFile) { flSplit = this.returnFileAndLine(remoteFile); remoteFile = flSplit[0]; line = flSplit[1]; } this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=getRemoteFile&csrf=" + this.csrf + "&lineNumber=" + line, encodeURIComponent(remoteFile)); this.serverMessage('' + t['Getting'] + ' ' + remoteFile); }, // Get changes to save (used when simply saving, gets diff changes between current and last known version) getChangesToSave: function() { let cM, savedText, newText, sm, opcodes; cM = this.getcMInstance(); // Get the last known saved version of file from array savedText = this.savedContents[this.selectedTab - 1]; // Get the text values and split it into lines newText = difflib.stringAsLines(cM.getValue()); savedText = difflib.stringAsLines(savedText); // Create a SequenceMatcher instance that diffs the two sets of lines sm = new difflib.SequenceMatcher(savedText, newText); // Get the opcodes from the SequenceMatcher instance // Opcodes is a list of 3-tuples describing what changes should be made to the base text in order to yield the new text opcodes = sm.get_opcodes(); for (let i = 0; i < opcodes.length; i++) { // opcode events may be: // equal = do nothing for this range // replace = replace [1]-[2] with [3]-[4] // insert = replace [1]-[2] with [3]-[4] // delete = replace [1]-[2] with [3]-[4] for (let j = opcodes[i][3]; j < opcodes[i][4]; j++) { if ("equal" !== opcodes[i][0]) { // Add a new array item if we don't have one yet if ("undefined" === typeof opcodes[i][5]) { opcodes[i][5] = ""; } // Add text line from newText to that array item along with line break opcodes[i][5] += newText[j] + "\n"; } } } return JSON.stringify(opcodes); }, // Save a file saveFile: function(saveAs, newFileAutoSave) { let changes, saveType, filePath, fileExt, pathPrefix; let prettierVersion, editorText, prettierText, sm, opcodes, docShift, startShift, endShift, newContent; filePath = this.openFiles[this.selectedTab - 1]; fileExt = filePath.substr(filePath.lastIndexOf(".") + 1); if ("undefined" !== typeof prettier && ["js", "json", "ts", "css", "scss", "less", "html", "xml", "yaml", "md", "php"].indexOf(fileExt) > -1) { switch (fileExt) { case "js": parser = "babel"; break; case "json": parser = "json"; break; case "ts": parser = "typescript"; break; case "css": parser = "css"; break; case "scss": parser = "scss"; break; case "less": parser = "less"; break; case "html": parser = "html"; break; case "xml": parser = "html"; break; case "yaml": parser = "yaml"; break; case "md": parser = "markdown"; break; case "php": parser = "php"; break; } try { prettierVersion = prettier.formatWithCursor( this.getThisCM().getValue(), { parser: parser, plugins: prettierPlugins, tabWidth: this.indentSize, useTabs: "tabs" === this.indentType, cursorOffset: this.getCharNumFromCursor() } ); // Get the text values and split it into lines editorText = difflib.stringAsLines(this.getThisCM().getValue()); prettierText = difflib.stringAsLines(prettierVersion.formatted); // Create a SequenceMatcher instance that diffs the two sets of lines sm = new difflib.SequenceMatcher(editorText, prettierText); // Get the opcodes from the SequenceMatcher instance // Opcodes is a list of 3-tuples describing what changes should be made to the base text in order to yield the new text opcodes = sm.get_opcodes(); docShift = 0; for (let i = 0; i < opcodes.length; i++) { // opcode events may be: // equal = do nothing for this range // replace = replace [1]-[2] with [3]-[4] // insert = replace [1]-[2] with [3]-[4] // delete = replace [1]-[2] with [3]-[4] // Params to determine if we need to set 1 or 0 shift the start line and end line startShift = "delete" === opcodes[i][0] && editorText.length === opcodes[i][2] ? 1 : 0; endShift = "replace" === opcodes[i][0] ? 1 : 0; if ("equal" !== opcodes[i][0]) { // Replace or insert if ("replace" === opcodes[i][0] || "insert" === opcodes[i][0]) { newContent = ""; // For each of the replace/insert lines in Prettier's version for (let j = opcodes[i][3]; j < opcodes[i][4]; j++) { // Build up newContent lines and end with a new line char if not the last line in the range newContent += prettierText[j]; if (j < opcodes[i][4] - 1) { newContent += "\n"; } } } // Delete if ("delete" === opcodes[i][0]) { // Not the last line in doc, the newContent is the line after the section we're deleting in editors version // Else if it's the last line in doc, the content after the section we're deleting is nothing newContent = editorText.length > opcodes[i][2] ? editorText[opcodes[i][2]] : ""; } // Replace the range with newContent. The range start line and end line adjust according to // startShift and endShift 1/0 values plus also the +/- docShift which is how much the // editor document has shifted so far during replace ranges this.getThisCM().replaceRange(newContent, {line: opcodes[i][1] - docShift - startShift, ch: 0}, {line: opcodes[i][2] - docShift - endShift, ch: 1000000}, "+input"); // Work out the +/- document shift based on difference between the editors last line in // this diff range and Prettiers last line in this diff range docShift = opcodes[i][2] - opcodes[i][4]; } } // If we don't have text selected, we have a cursor, so move the cursor to new place in // the prettified version now we've made adjustments if (false === this.getThisCM().somethingSelected()) { this.setCursorByCharNum(prettierVersion.cursorOffset); } } catch(err) { get("toolLinkOutput").className = "highlight error"; this.outputMsg('
Syntax error in ' + this.openFiles[this.selectedTab - 1].replace(iceRoot, "") + '
\n' + err.message.replace(//g, '>')); } } setTimeout(function() { // If we're not 'saving as', establish changes between current and known saved version from array if (false === saveAs) { changes = ic.getChangesToSave(); } saveType = saveAs ? "saveAs" : "save"; filePath = ic.openFiles[ic.selectedTab - 1].replace(iceRoot, "").replace(/\//g, "|"); if ("|[NEW]" === filePath && 0 < ic.selectedFiles.length) { pathPrefix = ic.selectedFiles[0]; filePath = -1 == pathPrefix.lastIndexOf(".") || pathPrefix.lastIndexOf(".") < pathPrefix.lastIndexOf("|") ? pathPrefix + filePath : "|[NEW]"; } filePath = filePath.replace("||", "|"); ic.serverQueue("add", ic.iceLoc + "/lib/file-control.php?action=save&fileMDT=" + ic.openFileMDTs[ic.selectedTab - 1] + "&fileVersion=" + ic.openFileVersions[ic.selectedTab - 1] + "&saveType=" + saveType + "&newFileAutoSave=" + newFileAutoSave + "&tabNum=" + ic.selectedTab + "&csrf=" + ic.csrf,encodeURIComponent(filePath), changes); ic.serverMessage('' + t['Saving'] + ' ' + ic.openFiles[ic.selectedTab - 1].replace(iceRoot, "").replace(/^\/|/g, '')); }, 0, ic); }, // Prompt a rename dialog renameFile: function(oldName, newName) { let shortURL, fileName, i; if (!oldName) { shortURL = this.selectedFiles[this.selectedFiles.length - 1].replace(/\|/g, "/"); oldName = this.selectedFiles[this.selectedFiles.length - 1].replace(/\|/g, "/"); } else { shortURL = oldName.replace(/\|/g, "/"); } if (!newName) { newName = this.getInput(t['Please enter the...'], shortURL); } if (newName) { this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=rename&oldFileName=" + encodeURIComponent(oldName.replace(/\|/g, "/")) + "&csrf=" + this.csrf,encodeURIComponent(newName)); this.serverMessage('' + t['Renaming to'] + ' ' + newName.replace(/^\/|/g, '')); this.setPreviousFiles(); } }, // Move a file from old location to new moveFile: function(oldName, newName) { let i, closeTabLink, fileName; if (newName && newName !== oldName) { if (this.ask("Are you sure you want to move file " + oldName + " to " + newName + " ?")){ this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=move&oldFileName=" + encodeURIComponent(oldName.replace(/\//g, "|")) + "&csrf=" + this.csrf, encodeURIComponent(newName.replace(/\//g, "|"))); this.serverMessage('' + t['Moving to'] + ' ' + newName.replace(/^\/|/g, '')); } this.setPreviousFiles(); } }, // Delete a file deleteFiles: function(fileList) { let tgtFiles, tgtListDisplay; tgtFiles = fileList ? fileList : this.selectedFiles; tgtListDisplay = tgtFiles.toString().replace(/\|/g, "/").replace(/,/g, "\n"); if (0 < tgtFiles.length && this.ask('Delete:\n\n' + tgtListDisplay + '?')) { this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=delete&csrf=" + this.csrf,encodeURIComponent(tgtFiles.join(";"))); this.serverMessage('' + t['Deleting File'] + ' ' + tgtListDisplay.replace(/^\/|/g, '')); } }, // Copy files copyFiles: function(fileList, dontShowPaste, dontHide) { this.copiedFiles = []; for (let i = 0; i < fileList.length; i++) { this.copiedFiles[i] = fileList[i]; } if (!dontShowPaste) { get('fmMenuPasteOption').style.display = "block"; } if (!dontHide) { this.hideFileMenu(); } }, // Paste files pasteFiles: function(location) { if (this.copiedFiles) { for (let i = 0; i < this.copiedFiles.length; i++) { if ("|" !== this.copiedFiles[i]) { this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=paste&location=" + location + "&csrf=" + this.csrf, encodeURIComponent(this.copiedFiles[i])); this.serverMessage('' + t['Pasting File'] + ' ' + this.copiedFiles[i].toString().replace(/\|/g, "/").replace(/,/g, "\n").replace(/^\/|/g, '')); } else { this.message(t['Sorry cannot paste...']); } } } else { this.message(t['Nothing to paste...']); } }, // Duplicate (copy & paste) files duplicateFiles: function(fileList) { let copiedFiles, location; // Take a snapshot of copied files if (this.copiedFiles) { copiedFiles = this.copiedFiles; } this.copyFiles(fileList, 'dontShowPaste', 'dontHide'); location = fileList[0].substr(0, fileList[0].lastIndexOf("|")); this.pasteFiles(location); // Restore copied files back to the snapshot if ("undefined" !== typeof copiedFiles) { this.copiedFiles = copiedFiles; } }, // Upload file(s) - select & submit uploadFilesSelect: function(location) { get('uploadDir').value = location; get("fileInput").click(); }, uploadFilesSubmit: function(obj) { if ("" !== get('fileInput').value) { this.showHide('show', get('loadingMask')); get('uploadFilesForm').submit(); event.preventDefault(); } }, // Show/hide file manager nav options showHideFileNav: function(vis, elem) { let options = ["optionsFile", "optionsEdit", "optionsSettings", "optionsHelp"]; if ("hide" === vis) { fileNavInt = setTimeout(function(ic) { for (let i = 0; i < options.length; i++) { ic.showHide('hide', get(options[i])); get(options[i] + 'Nav').style.color = ''; } }, 150, this); } else { for (let i = 0; i < options.length; i++) { this.showHide('hide', get(options[i])); get(options[i] + 'Nav').style.color = ''; } } get('fileOptions').style.opacity = "0"; if ("show" === vis) { if ("undefined" !== typeof fileNavInt) { clearTimeout(fileNavInt); } this.showHide(vis, get(elem)); get(elem + 'Nav').style.color = '#fff'; get('fileOptions').style.opacity = "1"; } }, // Is a specified path a folder? (Note: path is string encoded path with / replaced with |) isPathFolder: function(path) { // let's enumerate all folders to find whether clicked file is a folder or not const dir = this.filesFrame.contentDocument.getElementsByClassName("pft-directory"); const thisFileId = this.selectedFiles[0]; let liNode, aNode, spanNode; for (let i = 0; i < dir.length; i++){ liNode = dir[i]; if ("undefined" !== typeof liNode){ aNode = liNode.childNodes[0]; if ("undefined" !== typeof aNode){ spanNode = aNode.childNodes[1]; if ("undefined" !== typeof spanNode){ if (thisFileId === spanNode.getAttribute('id')){ // It's a folder return true; } } } } } // It's a file return false; }, // Check for existence of a file/dir checkExists: function(path) { let xhr, statusObj, timeStart; path = path.replace(/\|/g, "/"); // Clear any prefixed iceRoot from path if (0 === path.indexOf(iceRoot)) { path = path.replace(iceRoot, ""); } // Start a separate XHR call. We run separately rather than add into the serverQueue because we may need to run // immediately, eg need to if a file/dir exists mid flow in 'Save As' function, so can't go into queue xhr = this.xhrObj(); xhr.onreadystatechange=function() { if (4 === xhr.readyState) { // OK response? if (200 === xhr.status) { // Parse the response as a JSON object statusObj = JSON.parse(xhr.responseText); // Set the action end time and time taken in JSON object statusObj.action.timeEnd = new Date().getTime(); statusObj.action.timeTaken = statusObj.action.timeEnd - statusObj.action.timeStart; // User wanted raw (or both) output of the response? if (0 <= ["raw", "both"].indexOf(ICEcoder.fileDirResOutput)) { console.log(xhr.responseText); } // User wanted object (or both) output of the response? if (0 <= ["object", "both"].indexOf(ICEcoder.fileDirResOutput)) { console.log(statusObj); } // Also store the statusObj ICEcoder.lastFileDirCheckStatusObj = statusObj; // If error, show that, otherwise do whatever we're required to do next if (statusObj.status.error) { ICEcoder.message(statusObj.status.errorMsg); console.log("ICEcoder error info for your request..."); console.log(statusObj); ICEcoder.serverMessage(); ICEcoder.serverQueue('del'); } else { eval(statusObj.action.doNext); } // Some other response? Display a message about that } else { ICEcoder.message(t['Sorry there was...']); console.log("ICEcoder error info for your request..."); console.log(statusObj); ICEcoder.serverMessage(); ICEcoder.serverQueue('del'); } } }; xhr.open("POST", this.iceLoc + "/lib/file-control.php?action=checkExists&csrf=" + this.csrf, true); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); timeStart = new Date().getTime(); xhr.send('timeStart=' + timeStart + '&file=' + encodeURIComponent(path)); }, // Show menu on right clicking in file manager showMenu: function(evt) { let menuType, menuHeight, winH, fmXPos, fmYPos; if (0 === this.selectedFiles.length || -1 === this.selectedFiles.indexOf(this.selectedFiles[this.selectedFiles.length-1].replace(/\//g, "|"))) { this.selectFileFolder(evt); } menuHeight = 124 + 5; // general options height in px plus 5px space winH = window.innerHeight; if ("undefined" !== typeof this.thisFileFolderLink && "" !== this.thisFileFolderLink) { menuType = this.isPathFolder(this.selectedFiles[0]) ? "folder" : "file"; get('folderMenuItems').style.display = "folder" === menuType && 1 === this.selectedFiles.length ? "block" : "none"; if ("folder" === menuType && 1 === this.selectedFiles.length) { menuHeight += 20 + 20 + 1 + 23 + 1 + 2; // new file, new folder, hr, upload files(s), hr, padding if ("block" === get('fmMenuPasteOption').style.display) { menuHeight += 19; } } get('singleFileMenuItems').style.display = this.selectedFiles.length > 1 ? "none" : "block"; if (1 === this.selectedFiles.length) { menuHeight += 43; } get('fileMenu').style.display = "inline-block"; setTimeout(function() {get('fileMenu').style.opacity = "1"}, 4); fmXPos = this.mouseX - this.filesFrame.contentWindow.scrollX + 20; fmYPos = this.mouseY - this.filesFrame.contentWindow.scrollY - 10; if (fmYPos + menuHeight > winH) { fmYPos -= (fmYPos + menuHeight - winH); } get('fileMenu').style.left = fmXPos + "px"; get('fileMenu').style.top = fmYPos + "px"; } return false; }, // Continue to show the file menu showFileMenu: function() { get('fileMenu').style.display = 'inline-block'; setTimeout(function() {get('fileMenu').style.opacity = "1"}, 4); }, // Hide the file menu hideFileMenu: function() { get('fileMenu').style.display = 'none'; get('fileMenu').style.opacity = "0"; }, // Update the file manager tree list on demand updateFileManagerList: function(action, location, file, perms, oldName, uploaded, fileOrFolder) { let actionElemType, cssStyle, targetElem, locNest, newText, innerLI, permColors, newUL, newLI, elemType, nameLI, shortURL; perms = parseInt(perms, 10); // Adding files if ("add" === action && !get('filesFrame').contentWindow.document.getElementById(location.replace(iceRoot, "").replace(/\/$/, "").replace(/\//g, "|") + "|" + file)) { // Is this is a file or folder and based on that, set the CSS styling & link actionElemType = fileOrFolder; cssStyle = "file" === actionElemType ? "pft-file ext-" + file.substr(file.indexOf(".") + 1) : "pft-directory"; perms = "file" === actionElemType ? this.newFilePerms : this.newDirPerms; // Identify our target element & the first child element in it's location if (!location) {location = "/"} location = location.replace(iceRoot, "/").replace("//", "/"); targetElem = get('filesFrame').contentWindow.document.getElementById(location.replace(/\//g, "|")); locNest = targetElem.parentNode.parentNode.nextSibling; newText = document.createTextNode("\n"); permColors = 777 === perms ? 'background: #800; color: #eee' : 'color: #888'; innerLI = '        '+file+' '+perms+''; // If we don't have a locNest or at least 3 DOM items in there, it's an empty folder if (!locNest || 3 > locNest.childNodes.length) { // We now need to begin a new UL list newUL = document.createElement("ul"); locNest = targetElem.parentNode.parentNode; locNest.parentNode.insertBefore(newUL, locNest.nextSibling); // Now we can add the first LI for this file/folder we're adding newLI = document.createElement("li"); newLI.className = cssStyle; newLI.draggable = false; newLI.ondragstart = function(event) {parent.ICEcoder.addDefaultDragData(this, event)}; newLI.ondrag = function(event) {parent.ICEcoder.draggingWithKeyTest(event); if (parent.ICEcoder.getcMInstance()) {parent.ICEcoder.editorFocusInstance.indexOf('diff') == -1 ? parent.ICEcoder.getcMInstance().focus() : parent.ICEcoder.getcMdiffInstance().focus()}}; newLI.ondragover = function(event) {parent.ICEcoder.setDragCursor(event, "folder" === actionElemType ? 'folder' : 'file')}; newLI.ondragend = function() {parent.ICEcoder.dropFile(this)}; newLI.innerHTML = innerLI; locNest.nextSibling.appendChild(newLI); locNest.nextSibling.appendChild(newText); // There are items in that location, so add our new item in the right position } else { for (let i = 0; i < locNest.childNodes.length; i++) { if (locNest.childNodes[i].className) { // Identify if the item we're considering is a file or folder elemType = 0 < locNest.childNodes[i].className.indexOf('directory') ? "folder" : "file"; // Get the name of the item nameLI = locNest.childNodes[i].getElementsByTagName('span')[0].innerHTML; // If it's of the same type & the name is greater, or we're adding a folder and it's a file or if we're at the end of the list if ((elemType == actionElemType && nameLI > file) || ("folder" === actionElemType && "file" === elemType) || i == locNest.childNodes.length - 1) { newLI = document.createElement("li"); newLI.className = cssStyle; newLI.draggable = false; newLI.ondragstart = function(event) {parent.ICEcoder.addDefaultDragData(this, event)}; newLI.ondrag = function(event) {parent.ICEcoder.draggingWithKeyTest(event); if (parent.ICEcoder.getcMInstance()) {parent.ICEcoder.editorFocusInstance.indexOf('diff') == -1 ? parent.ICEcoder.getcMInstance().focus() : parent.ICEcoder.getcMdiffInstance().focus()}}; newLI.ondragover = function(event) {parent.ICEcoder.setDragCursor(event, "folder" === actionElemType ? 'folder' : 'file')}; newLI.ondragend = function() {parent.ICEcoder.dropFile(this)}; newLI.innerHTML = innerLI; // Append or insert depending on which of the above if statements is true if (i == locNest.childNodes.length - 1) { locNest.appendChild(newLI); locNest.appendChild(newText); } else { locNest.insertBefore(newLI,locNest.childNodes[i]); locNest.insertBefore(newText,locNest.childNodes[i + 1]); } break; } } } } // If we added a new file, we've saved it under a new filename, so set that if ("file" === actionElemType && !oldName && !uploaded) { this.openFiles[this.selectedTab - 1] = location + "/" + file; } } // Renaming files if ("rename" === action) { // If dir is the same as before, it's a simple rename if (location === oldName.substr(0, oldName.lastIndexOf('/'))) { // Get short URL of our right clicked file and get target elem based on this shortURL = oldName.replace(/\//g, "|"); targetElem = get('filesFrame').contentWindow.document.getElementById(shortURL); // Set the name to be as per our new file/folder name targetElem.innerHTML = file; // Update the ID of the target & set a new title and perms ID targetElem.id = location.replace(/\//g, "|") + "|" + file; targetElem.parentNode.title = targetElem.id.replace(/\|/g, "/"); targetElemPerms = get('filesFrame').contentWindow.document.getElementById(shortURL + "_perms"); targetElemPerms.id = location.replace(/\//g, "|") + "|" + file + "_perms"; // Rename in selected files this.renameInSelectedFiles(shortURL, location.replace(/\//g, "|") + "|" + file); // Rename also within any children this.renameInChildren(targetElem, oldName, location, file); // Update data for any tabs we have open where we've changed a dir it resides in for (let i = 0; i < this.openFiles.length; i++) { if (0 === this.openFiles[i].indexOf(oldName + "/")) { this.renameTab(i + 1, this.openFiles[i].replace(oldName, location + "/" + file)); } } // If dir has changed, handle dir change and possibly also filename change } else { // Target is root, or another dir? const tgtClass = location === "" ? this.filesFrame.contentWindow.document.getElementById("|").parentNode.parentNode.className : this.filesFrame.contentWindow.document.getElementById(location.replace(/\//g, "|")).parentNode.parentNode.className; // Source is a dir or file? const srcClass = this.filesFrame.contentWindow.document.getElementById(oldName.replace(/\//g, "|")).parentNode.parentNode.className; fileOrFolder = srcClass.indexOf("pft-directory") > -1 ? "folder" : "file"; // Only add file into view if the dir is open if (-1 < tgtClass.indexOf('dirOpen')) { this.updateFileManagerList("add", location, file, false, oldName, false, fileOrFolder); } this.updateFileManagerList("delete", oldName.substr(0, oldName.lastIndexOf("/")), oldName.substr(oldName.lastIndexOf("/")+1), false, oldName, false, fileOrFolder); this.selectedFiles = []; } } // Moving files if ("move" === action) { // Target is root, or another dir? const tgtClass = location === "" ? this.filesFrame.contentWindow.document.getElementById("|").parentNode.parentNode.className : this.filesFrame.contentWindow.document.getElementById(location.replace(/\//g, "|")).parentNode.parentNode.className; // Only add file into view if the dir is open if (-1 < tgtClass.indexOf('dirOpen')) { this.updateFileManagerList("add", location, file, false, oldName, false, fileOrFolder); } this.updateFileManagerList("delete", oldName.substr(0, oldName.lastIndexOf("/")), file, false, oldName, false, fileOrFolder); this.selectedFiles = []; } // Chmod on files if ("chmod" === action) { // Get short URL for our file and get our target elem based on this shortURL = this.selectedFiles[this.selectedFiles.length - 1].replace(/\|/g, "/"); targetElem = get('filesFrame').contentWindow.document.getElementById(shortURL.replace(/\//g, "|") + "_perms"); // Set the color for the perms targetElem.style.background = 777 === perms ? '#800' : 'none'; targetElem.style.color = 777 === perms ? '#eee' : '#888'; // Set the new perms targetElem.innerHTML = perms; } // Deleting files if ("delete" === action) { if (!location) {location = ""} location = location.replace(iceRoot, "/"); location = location.replace("//", "/"); location = location.replace(/\/$/, "").replace(/\//g, "|"); targetElem = (location + "|" + file).replace("||", "|"); targetElem = get('filesFrame').contentWindow.document.getElementById(targetElem).parentNode.parentNode; this.openCloseDir(targetElem.childNodes[0], false); targetElem.parentNode.removeChild(targetElem); if (!oldName) { // Close any tabs we have open which would have had a file deleted for (let i = this.openFiles.length - 1; i >= 0; i--) { if ("folder" === fileOrFolder && 0 === this.openFiles[i].indexOf(location.replace(/\|/g, "/") + "/" + file + "/")) { this.closeTab(i + 1, 'dontSetPV', 'dontAsk'); } if ("file" === fileOrFolder && location.replace(/\|/g, "/") + "/" + file === this.openFiles[i]) { this.closeTab(i + 1, 'dontSetPV', 'dontAsk'); } } } } // Finally, switch to selectedTab to refresh items this.switchTab(this.selectedTab); }, // Rename in selected files renameInSelectedFiles: function(oldName, newName) { for (let i = 0; i < this.selectedFiles.length; i++) { if (oldName === this.selectedFiles[i]) { this.selectedFiles[i] = newName; } } }, // Rename node attributes within any renamed dirs recursively renameInChildren: function(elem, oldName, location, file) { let innerItems, targetElem, targetElemPerms; // If our elem has a sibling and it's a UL, we renamed a dir if (elem.parentNode.parentNode.nextSibling && "UL" === elem.parentNode.parentNode.nextSibling.nodeName) { innerItems = elem.parentNode.parentNode.nextSibling; // For each one of the children in the UL, if it's a LI (may be a file or dir) for (let i = 0; i < innerItems.childNodes.length; i++) { if ("LI" === innerItems.childNodes[i].nodeName) { // Get the span elem inside as our targetElem targetElem = innerItems.childNodes[i].childNodes[0].childNodes[1]; // Update the ID of the target & set a new title targetElem.id = targetElem.id.replace(oldName.replace(/\//g, "|"),location.replace(/\//g, "|") + "|" + file); targetElem.parentNode.title = targetElem.id.replace(/\|/g, "/"); // Also update the perms ID targetElemPerms = get('filesFrame').contentWindow.document.getElementById(targetElem.id).nextSibling.nextSibling; targetElemPerms.id = targetElem.id + "_perms"; // Finally, test this node for ULs next to it also, incase it's a dir this.renameInChildren(targetElem, oldName, location, file); } } } }, // Refresh file manager refreshFileManager: function() { this.filesFrame.contentWindow.location.reload(true); this.filesFrame.style.opacity = "0"; this.filesFrame.onload = function() { ICEcoder.filesFrame.style.opacity = "1"; } }, // Detect CTRL/Cmd key down whilst dragging files draggingWithKeyTest: function(evt) { this.draggingWithKey = this.ctrlCmdKeyDown(evt) ? "CTRL" : false; }, // Add default drag data (dragging in Firefox on DOM elems not possible otherwise) addDefaultDragData: function(elem, evt) { evt.dataTransfer.setData('Text', elem.id); }, // Set a copy, move or none drag cursor type setDragCursor: function(evt, dropType) { let cursorIcon; // Prevent the default and establish if CTRL key is down evt.preventDefault(); this.draggingWithKeyTest(evt); // Establish the cursor to show cursorIcon = "editor" === dropType ? "CTRL" === this.draggingWithKey ? "copy" : "link" : "folder" === dropType ? "CTRL" === this.draggingWithKey ? "copy" : "move" : "none"; evt.dataTransfer.dropEffect = cursorIcon; }, // On dropping a file, do something dropFile: function(elem) { let filePath, tgtPath; filePath = elem.childNodes[0].childNodes[1].id.replace(/\|/g, "/"); fileName = filePath.substr(filePath.lastIndexOf("/") + 1); if ('editor' === this.area) { this.pasteURL(filePath); } if ('files' === this.area) { setTimeout(function(ic) { tgtPath = "folder" === ic.thisFileFolderType ? ic.thisFileFolderLink : ic.thisFileFolderLink.substr(0, ic.thisFileFolderLink.lastIndexOf("|")); if ("" === tgtPath) {tgtPath = "|";} if("CTRL" === ic.draggingWithKey) { ic.copyFiles(ic.selectedFiles); ic.pasteFiles(tgtPath); } else { // Clear the background of item you just dropped onto this.filesFrame.contentWindow.document.getElementById(tgtPath.replace(/\//g, "|")).style.background = ''; // If the tgtPath is not the root, postfix the path with a pipe if ("|" !== tgtPath) {tgtPath += "|"}; ic.moveFile(filePath, tgtPath.replace(/\|/g, "/") + fileName); } }, 4, this); } this.mouseDown = false; this.mouseDownInCM = false; }, // ============== // FIND & REPLACE // ============== // Backslash escape regex special chars in string escapeRegex: function(string) { // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, // Test an input for balanced regex brackets regexIsBalanced: function(input) { const brackets = "[]{}()"; const bracketsSlash = brackets + "\\"; let stack = []; let remainder = ""; // Remove backslash escaped brackets & slashes before testing for (let i = 0; i < bracketsSlash.length; i++) { input = input.replace(new RegExp('\\\\\\' + bracketsSlash[i], 'g'), 'ICECODERC' + i) } // Go thru each char in input for (let char of input) { // Find index of this char in brackets string let bracketsIndex = brackets.indexOf(char) // Not one of the bracket chars, continue to next char if (bracketsIndex === -1) { remainder += char; continue; } // If a bracket start: [ { ( push closing bracket onto stack if (bracketsIndex % 2 === 0) { stack.push(bracketsIndex + 1) } else { // When popping brackets off the stack, compare and return false if not matching if (stack.pop() !== bracketsIndex) { return false; } } } // Replace backslash escaped brackets & slashes for (let i = 0; i < bracketsSlash.length; i++) { remainder = remainder.replace(new RegExp('ICECODERC' + i, 'g'), '\\' + bracketsSlash[i]) } // If we have items left on our stack, we have an unbalanced input return { "balanced" : stack.length === 0, "remainder" : remainder }; }, // Toggle on/off regex matching during find findRegexToggle: function() { // Toggle to opposite bool value and set background according to value this.findRegex = !this.findRegex; get('findRegexToggle').style.background = true === this.findRegex ? "#49d" : ""; // Find again now we've changed regex option state ICEcoder.findReplace(get('find').value, true, false, false); }, // Update find & replace options based on user selection findReplaceOptions: function() { get('rText').style.display = get('replace').style.display = get('rTarget').style.display = document.findAndReplace.connector.value === t['and'] ? "inline-block" : "none"; }, findOnInput: function() { let thisCM, selectNext; // Realtime finding - only action for finding in current doc if ("" !== get('find').value && t['this document'] === document.findAndReplace.target.value) { // Get CM pane thisCM = this.getThisCM(); // Consider selecting next on value input, according to not having result selected already and user setting selectNext = thisCM.getSelection() !== get('find').value && true === ICEcoder.selectNextOnFindInput; ICEcoder.findReplace(get('find').value, selectNext, false, false); get("find").focus(); // Reset results display } else { ICEcoder.findReplace(get('find').value, false, false, false); } }, // Find & replace text according to user selections findReplace: function(find, selectNext, canActionChanges, findPrevious) { let replace, results, rExp, thisCM, thisSelection, rBlocks, rExpMatch0String, replaceQS, targetQS, filesQS, currRBlock; // Get our replace value and results display replace = get('replace').value; results = get('results'); // If we're finding with regex and have a problem with it, highlight find box red and return, avoids CPU crash if (true === parent.ICEcoder.findRegex) { const balancedInfo = this.regexIsBalanced(find); // Turn input box red if empty, or no balancedInfo data, or we have unbalanced bracket pairs, or the remainder // is empty aside from brackets, or find or only has ^ or $ or .* if ("" !== find && ( false === balancedInfo || false === balancedInfo['balanced'] || ("" === balancedInfo['remainder'].replace(/\[\]\{\}\(\)/g, "")) || "" === find.replace(/\^|\$|\.\*/g, "") )) { this.clearResultsDisplays(); get('find').style.background = "#800"; return false; } else { get('find').style.background = ""; } } // Determine our find rExp, inside a try/catch incase there's an uncaught issue with format try { rExp = new RegExp(true === parent.ICEcoder.findRegex ? find : ICEcoder.escapeRegex(find), "gi"); } catch(e) { return false; } // Get CM pane thisCM = this.getThisCM(); // Finding in this document only if (thisCM && 0 < find.length && t['this document'] === document.findAndReplace.target.value) { // Get this selection, either as regex escaped version or regular, for comparisons thisSelection = true === parent.ICEcoder.findRegex ? ICEcoder.escapeRegex(thisCM.getSelection()) : thisCM.getSelection(); // Replacing? if (t['and'] === document.findAndReplace.connector.value && true === canActionChanges) { // Find & replace the next instance, or all? if (t['replace'] === document.findAndReplace.replaceAction.value && thisSelection.toLowerCase() === find.toLowerCase()) { thisCM.replaceSelection(replace, "around"); } else if (t['replace all'] === document.findAndReplace.replaceAction.value) { thisCM.setValue(thisCM.getValue().replace(rExp, replace)); } } // Start looking for results rData = ICEcoder.findInCMContent(thisCM, rExp, selectNext); // Set results, resultsLines and findResult plus rBlocks which shows DOM elems in results bar // and rExpMatch0String which is the matching string used to set correct selection length this.results = rData.results; this.resultsLines = rData.resultsLines; this.findResult = rData.findResult; rBlocks = rData.rBlocks; rExpMatch0String = rData.rExpMatch0String; // Increment findResult one more if our selection is what we want to find and we want to find next // but we're not replacing (replacing removes from array so should not increment thru array index also) if ( find.toLowerCase() === thisSelection.toLowerCase() && false === findPrevious && (t['and'] !== document.findAndReplace.connector.value || false === canActionChanges) ) { this.findResult++; } if (findPrevious) { // Find & replace backwards using previous button = 1, else just find = 1 this.findResult -= true === canActionChanges ? 1 : 2; } // If we have results if (this.results.length > 0) { // Show results only if (false === selectNext) { results.innerHTML = this.results.length + " results"; // We may want to take action instead } else { // Looking for next and hit end, loop round to start if (false === findPrevious && this.findResult > this.results.length - 1) { this.findResult = 0 } // Looking for previous and hit start, loop round to end if (findPrevious && 0 > this.findResult) { this.findResult = this.results.length - 1; } // If we somehow ended up with a number under 0, set to 0 if (this.findResult < 0) { this.findResult = 0; } // Update results display results.innerHTML = "Highlighted result " + (this.findResult + 1) + " of " + this.results.length + " results"; // Scroll to that line in the editor this.goToLine(this.results[this.findResult][0], this.results[this.findResult][1], true); // Finally, highlight our selection and focus on CM pane thisCM.setSelection( {"line": this.results[this.findResult][0]-1, "ch": this.results[this.findResult][1]}, {"line": this.results[this.findResult][0]-1, "ch": this.results[this.findResult][1] + rExpMatch0String.length} ); this.focus(); } // Display the find results bar this.content.contentWindow.document.getElementById('resultsBar').innerHTML = rBlocks; this.content.contentWindow.document.getElementById('resultsBar').style.display = "inline-block"; // Mark the currRBlock (result for current line) in red currRBlock = this.content.contentWindow.document.getElementById('rBlock' + (thisCM.getCursor().line + 1)); if (currRBlock) { currRBlock.style.background = "#06c"; } return true; } else { this.clearResultsDisplays(); return false; } } else { // Show the relevant multiple results popup if (find !== "" && true === canActionChanges) { // Set replace, target and files query string to empty replaceQS = ""; targetQS = ""; filesQS = ""; // Replacing? if (t['and'] === document.findAndReplace.connector.value) { replaceQS = "&replace=" + replace; } // Target? if (0 <= document.findAndReplace.target.value.indexOf(t['file'])) { targetQS = "&target=" + document.findAndReplace.target.value.replace(/ /g, "-"); } // Files? if (t['selected files'] === document.findAndReplace.target.value) { filesQS = "&selectedFiles=" + this.selectedFiles.join(":"); } // Establish find find = find.replace(/\'/g, '\''); find !== encodeURIComponent(find) ? find = 'ICEcoder:' + encodeURIComponent(find) : find; // Finally, show loading mask and open multiple results pane using QS params this.showHide('show',get('loadingMask')); get('mediaContainer').innerHTML = ''; // We have nothing to search for, blank it all out } else { this.clearResultsDisplays(); } } }, clearResultsDisplays: function() { results.innerHTML = this.openFiles.length > 0 ? "No results" : ""; this.content.contentWindow.document.getElementById('resultsBar').innerHTML = ""; this.content.contentWindow.document.getElementById('resultsBar').style.display = "none"; }, findInCMContent: function(thisCM, rExp, selectNext) { let avgBlockH, addPadding, rBlocks, haveMatch, rExpMatch0String, rBlockTop, lastRBlockTop; // Start new iterators for line & last line let i = 0; let lastLine = -1; // Set results, resultsLines and findResult to defaults let results = []; let resultsLines = []; let findResult = 0; // Get lineNum and chNum from cursor const lineNum = thisCM.getCursor(true === selectNext ? "anchor" : "head").line + 1; const chNum = thisCM.getCursor(true === selectNext ? "anchor" : "head").ch; // Work out the avg block - is either line height or fraction of space available, but a min of 1px avgBlockH = Math.max(1, !this.scrollBarVisible ? thisCM.defaultTextHeight() : parseInt(this.content.style.height, 10) / thisCM.lineCount()); // Need to add padding if there's no scrollbar, so current line highlighting lines up with it addPadding = !this.scrollBarVisible ? thisCM.heightAtLine(0) : 0; // Result blocks string empty to start, ready to hold DOM elems to show in results bar rBlocks = ""; rExpMatch0String = ""; lastRBlockTop = 0; thisCM.eachLine(function(line) { i++; haveMatch = false; // If we have matches for our regex for this line while ((match = rExp.exec(line.text)) !== null) { // rBlockTop is either: // - the default text height (if no scrollbar), or // - screen height divided by num lines (if scrollbar) // multiply whichever by the line number, plus add padding rBlockTop = parseInt((( !ICEcoder.scrollBarVisible ? thisCM.defaultTextHeight() : parseInt(this.content.style.height, 10) / thisCM.lineCount()) * (i - 1)) + addPadding, 10); rExpMatch0String = match[0]; haveMatch = true; // Not the same as last line, add to resultsLines if (lastLine !== i) { resultsLines.push(match.index); lastLine = i; } // If the line containing a result is less than than the cursors line or // if the character position of the match is less than the cursor position, increment findResult if (i < lineNum || (i === lineNum && match.index < chNum)) { findResult++; } // Push the line & char position coords into results results.push([i, match.index]); } // If we have a match, add the DOM elem into our rBlocks string if (true === haveMatch && rBlockTop !== lastRBlockTop) { rBlocks += '
'; lastRBlockTop = rBlockTop; } }); return { "results": results, "resultsLines": resultsLines, "findResult": findResult, "rBlocks": rBlocks, "rExpMatch0String": rExpMatch0String } }, // Replace text in a file replaceInFile: function(fileRef, find, replace) { this.serverQueue( "add", this.iceLoc + "/lib/file-control.php?action=replaceText&find=" + find + "&replace=" + replace + "&csrf=" + this.csrf, encodeURIComponent(fileRef.replace(/\//g, "|"))); this.serverMessage('' + t['Replacing text in'] + ' ' + fileRef.replace(/^\/|/g, '')); }, // ============== // INFO & DISPLAY // ============== // Get the caret position getCaretPosition: function() { let thisCM, line, ch, chPos; thisCM = this.getThisCM(); line = thisCM.getCursor().line; ch = thisCM.getCursor().ch; chPos = 0; for (let i = 0; i < line; i++) { chPos += thisCM.getLine(i).length + 1; } this.caretPos = (chPos + ch - 1); }, // Update the code type, line & character display updateCharDisplay: function() { let thisCM; thisCM = this.getThisCM(); this.caretLocationType(); this.charDisplay.innerHTML = this.caretLocType + ", Line: " + (thisCM.getCursor().line + 1) + ", Char: " + thisCM.getCursor().ch; }, // Update version display updateVersionsDisplay: function() { let versionsCount = this.openFileVersions[this.selectedTab - 1]; get('versionsDisplay').innerHTML = "undefined" !== typeof versionsCount ? this.openFileVersions[this.selectedTab - 1] + " backup" + (1 !== versionsCount ? "s" : "") : ""; }, // Update the byte display updateByteDisplay: function() { this.byteDisplay.innerHTML = this.getThisCM().getValue().length.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " bytes"; }, // Toggle the char/byte display showDisplay: function(show) { this.byteDisplay.style.display = "byte" === show ? "inline-block" : "none"; this.charDisplay.style.display = "char" === show ? "inline-block" : "none"; }, // Show & hide target element showHide: function(doVis, elem) { elem.style.visibility = "show" === doVis ? 'visible' : 'hidden'; }, // Determine the CodeMirror instance we're using getcMInstance: function(tab) { ic = this; // No content, set to parent if (!ic.content) { ic = parent.ICEcoder; // No content still, set to parent again if (!ic.content) { ic = parent.ICEcoder; } } return ic.content.contentWindow[ // target specific tab false === isNaN(tab) ? 'cM' + ICEcoder.cMInstances[tab - 1] // new tab or selected tab : "new" === tab || ("new" !== tab && 0 < this.openFiles.length) ? 'cM' + ICEcoder.cMInstances[this.selectedTab - 1] // fallback to first tab : 'cM1' ]; }, // Determine the CodeMirror instance we're using getcMdiffInstance: function(tab, context) { if ("undefined" === typeof context) { context = ICEcoder; } return context.content.contentWindow[ (// target specific tab false === isNaN(tab) ? 'cM' + ICEcoder.cMInstances[tab - 1] // new tab or selected tab : "new" === tab || ("new" !== tab && 0 < this.openFiles.length) ? 'cM' + ICEcoder.cMInstances[this.selectedTab - 1] // fallback to first tab : 'cM1') + 'diff' ]; }, // Get the mouse position getMouseXY: function(e, area) { this.mouseX = e.pageX ? e.pageX : e.clientX + document.body.scrollLeft; this.mouseY = e.pageY ? e.pageY : e.clientY + document.body.scrollTop; this.area = area; if ("top" !== area) { this.mouseY += 25 + 45; } if ("editor" === area) { this.mouseX += this.filesW; } this.dragCursorTest(); if (62 < this.mouseY) {this.setTabWidths(false);} }, // Test if we need to show a drag cursor or not dragCursorTest: function() { let diffX, cursorName, zone; // Dragging tabs, started after dragging for 10px from origin diffX = this.mouseX - this.diffStartX; if (false !== this.draggingTab && this.diffStartX && (-10 >= diffX || 10 <= diffX)) { if (this.mouseX > parseInt(this.files.style.width, 10)) { this.tabDragMouseX = this.mouseX - parseInt(this.files.style.width, 10) - this.tabDragMouseXStart; this.tabDragMove(); } } // Dragging file manager, possible within 7px of file manager edge if (this.ready) { if (false === this.mouseDown) {this.draggingFilesW = false} cursorName = (false === this.draggingTab && ((this.mouseX > this.filesW - 7 && this.mouseX < this.filesW + 7) || true === this.draggingFilesW)) ? "w-resize" : "auto"; if (this.content.contentWindow.document && this.filesFrame.contentWindow) { document.body.style.cursor = cursorName; if (zone = this.content.contentWindow.document.body) {zone.style.cursor = cursorName} if (zone = this.filesFrame.contentWindow.document.body) {zone.style.cursor = cursorName} } } }, // Show or hide a server message serverMessage: function(message) { let serverMessage; serverMessage = get('serverMessage'); if (message) { serverMessage.innerHTML = this.xssClean(message).replace(/\<b\>/g, "").replace(/\<\/b\>/g, ""); } serverMessage.style.opacity = message ? 1 : 0; get("versionsDisplay").style.opacity = message ? 0 : 1; }, // Show a CSS color block next to our text cursor cssColorPreview: function() { let thisCM, string, rx, match, oldBlock, newBlock; thisCM = this.getThisCM(); if (thisCM) { string = thisCM.getLine(thisCM.getCursor().line); rx = /(#[\da-f]{3}(?:[\da-f]{3})?\b|\b(?:rgb|hsl)a?\([\s\d%,.-]+\)|\b[a-z]+\b)/gi; while((match = rx.exec(string)) && thisCM.getCursor().ch > match.index + match[0].length); oldBlock = get('content').contentWindow.document.getElementById('cssColor'); if (oldBlock) {oldBlock.parentNode.removeChild(oldBlock)} if (true === this.codeAssist && "CSS" === this.caretLocType) { newBlock = document.createElement("div"); newBlock.id = "cssColor"; newBlock.style.position = "absolute"; newBlock.style.display = "block"; newBlock.style.width = newBlock.style.height = "20px"; newBlock.style.zIndex = "1000"; newBlock.style.background = match ? match[0] : ''; newBlock.style.cursor = "pointer"; newBlock.onclick = function() {ICEcoder.showColorPicker(match[0])}; if ("" == newBlock.style.backgroundColor) { newBlock.style.display = "none"; } get('header').appendChild(newBlock); thisCM.addWidget(thisCM.getCursor(), get('cssColor'), true); } } }, // Show color picker showColorPicker: function(color) { get('blackMask').style.visibility = "visible"; get('mediaContainer').innerHTML = '


'+ ''+ '

'+ ''+ ''; farbtastic('picker','color'); // If we have a color value, set it in picker if (color) { // If a RGB(A) value, convert to Hex if (-1 < color.toLowerCase().indexOf("rgb")) { color = color.replace(/rgba?|\(|\)|\s+/gi, "").split(","); color = "#" + this.rgbToHex(...color); } get('picker').farbtastic.setColor(color); } }, // Init the canvas by drawing the image and setting the floating containers background size (5x zoom) initCanvasImage: function (imgThis) { let canvas, img; canvas = get('canvasPicker').getContext('2d'); img = new Image(); img.crossOrigin = "Anonymous"; img.src = imgThis.src; // Issue with loading, display CORS error info img.onerror = function() { get('floatingContainer').style.visibility = "hidden"; get('canvasPickerColorInfo').style.display = "none"; get('canvasPickerCORSInfo').style.display = "block"; }; // On image load img.onload = function() { // Get width and height and draw this image into the canvas get('canvasPicker').width = imgThis.width; get('canvasPicker').height = imgThis.height; canvas.drawImage(img, 0, 0, imgThis.width, imgThis.height); // Display color picker info and hide CORS message get('canvasPickerColorInfo').style.display = "block"; get('canvasPickerCORSInfo').style.display = "none"; // Show image preview box on mouse over get('canvasPicker').onmouseover = function() { get('floatingContainer').style.visibility = "visible"; ICEcoder.overPopup = true; }; // Hide image preview box on mouse out get('canvasPicker').onmouseout = function() { get('floatingContainer').style.visibility = "hidden"; ICEcoder.overPopup = false; }; }; document.getElementById('floatingContainer').style.backgroundSize = (imgThis.naturalWidth * 5) + "px " + (imgThis.naturalHeight * 5) + "px"; }, // Interact with the canvas image interactCanvasImage: function (imgThis) { let canvas, x, y, imgData, r, g, b, rgbStr, hex, textColor, fcElem, fcBGX, fcBGY; canvas = get('canvasPicker').getContext('2d'); // Show pointer colors on mouse move over canvas get('canvasPicker').onmousemove = function(event) { // get mouse x & y x = event.pageX - this.offsetLeft; y = event.pageY - this.offsetTop; // get image data & then RGB values imgData = canvas.getImageData(x, y, 1, 1).data; r = imgData[0]; g = imgData[1]; b = imgData[2]; rgbStr = r + ',' + g + ',' + b; // Get hex from RGB value hex = ICEcoder.rgbToHex(r, g, b); // set the values & BG colours of the input boxes get('rgbMouseXY').value = rgbStr; get('hexMouseXY').value = '#' + hex; get('hexMouseXY').style.backgroundColor = get('rgbMouseXY').style.backgroundColor = '#' + hex; textColor = r < 128 || g < 128 || b < 128 && (r < 200 && g < 200 && b > 50) ? '#fff' : '#000'; get('hexMouseXY').style.color = get('rgbMouseXY').style.color = textColor; // Move the floating container to follow mouse pointer fcElem = get('floatingContainer'); fcElem.style.left = this.mouseX + 20 + "px"; fcElem.style.top = this.mouseY + "px"; // Move the background image for the container to match also // 5 x zoom, account for scaling down of large images and shift 25px of the hover div size // (55px is the 11x11 grid of pixels), minus 5px for centre row/col fcBGX = -((x * 5) * (imgThis.naturalWidth / imgThis.width)) + 25; fcBGY = -((y * 5) * (imgThis.naturalHeight / imgThis.height)) + 25; fcElem.style.backgroundPosition = fcBGX + "px " + fcBGY + "px"; }; // Set pointer colors on clicking canvas get('canvasPicker').onclick = function() { get('rgb').value = get('rgbMouseXY').value; get('hex').value = get('hexMouseXY').value; get('hex').style.backgroundColor = get('rgb').style.backgroundColor = get('hex').value; get('hex').style.color = get('rgb').style.color = textColor; } }, // Convert RGB(A) values to Hex rgbToHex: function(r, g, b, a) { // Ignore alpha, use r, g, b return this.toHex(r) + this.toHex(g) + this.toHex(b); }, // Return numbers as hex equivalent toHex: function(num) { let hex; num = parseInt(num, 10); if (isNaN(num)) return "00"; hex = num.toString(16); return 1 === hex.length ? "0" + hex : hex; }, // Insert new color value insertColorValue: function(color) { let thisCM, cursor; thisCM = this.getThisCM(); cursor = thisCM.getTokenAt(thisCM.getCursor()); thisCM.replaceRange(color, {line:thisCM.getCursor().line, ch:cursor.start}, {line:thisCM.getCursor().line, ch:cursor.end}, "+input"); }, // Change opacity of the file manager icons fMIconVis: function(icon, vis) { let i; if (i = get(icon)) { i.style.opacity = vis; } }, // Check if a file is already open isOpen: function(file) { let i; file = file.replace(/\|/g, "/").replace(docRoot + iceRoot, ""); i = this.openFiles.indexOf(file); // return the array position or false return -1 < i ? i : false; }, // ====== // SYSTEM // ====== getThisCM: function() { return -1 < this.editorFocusInstance.indexOf('diff') ? this.getcMdiffInstance() : this.getcMInstance(); }, // Start running plugin intervals according to given specifics startPluginIntervals: function(plugRef, plugURL, plugTarget, plugTimer) { // Add CSRF to URL if it has QS params if (-1 < plugURL.indexOf("?")) { plugURL = plugURL + "&csrf=" + this.csrf; } this['plugTimer' + plugRef] = // This window instances -1 < ["_parent", "_top", "_self", ""].indexOf(plugTarget) ? this['plugTimer' + plugRef] = setInterval('window.location=\'' + plugURL + '\'', plugTimer * 1000 * 60) // fileControl iframe instances : 0 === plugTarget.indexOf("fileControl") ? this['plugTimer' + plugRef] = setInterval(function(ic) { ic.serverQueue("add", plugURL); ic.serverMessage(plugTarget.split(":")[1]); }, plugTimer * 1000 * 60, this) // _blank or named target window instances : this['plugTimer' + plugRef] = setInterval('window.open(\'' + plugURL + '\', \'' + plugTarget + '\')', plugTimer * 1000 * 60); // push the plugin ref into our array this.pluginIntervalRefs.push(plugRef); }, // Turning on/off the Code Assist codeAssistToggle: function() { let cM, cMdiff, fileName, fileExt; this.codeAssist = !this.codeAssist; this.cssColorPreview(); this.focus(-1 < this.editorFocusInstance.indexOf('diff') ? 'diff' : false); for (let i = 0; i < this.cMInstances.length; i++) { fileName = this.openFiles[i]; fileExt = fileName.split("."); fileExt = fileExt[fileExt.length - 1]; if ("js" === fileExt || "json" === fileExt) { cM = this.content.contentWindow['cM' + this.cMInstances[i]]; cMdiff = this.content.contentWindow['cM' + this.cMInstances[i] + 'diff']; if (!this.codeAssist) { cM.clearGutter("CodeMirror-lint-markers"); cM.setOption("lint", false); cMdiff.clearGutter("CodeMirror-lint-markers"); cMdiff.setOption("lint", false); } else { cM.setOption("lint", true); cMdiff.setOption("lint", true); } } } }, // Queue items up for processing in turn serverQueue: function(action, item, file, changes) { var cM, nextSaveID, txtArea, topSaveID, element, xhr, statusObj, timeStart; // If we have this exact item URL, it's almost certain we've got a repetitive save // situation and so clear the message and server queue item to avoid save jamming if ("add" === action && 0 < this.serverQueueItems.length && 0 < item.indexOf('action=save') && this.serverQueueItems[0].file === file) { this.serverMessage(); this.serverQueue("del"); return; } cM = this.getcMInstance(); // Firstly, work out how many saves we have to carry out nextSaveID = 0; for (let i = 0; i < this.serverQueueItems.length; i++) { if (0 < this.serverQueueItems[i].item.indexOf('action=save')) { nextSaveID++; } } nextSaveID++; // Add to end of array or remove from beginning on demand, plus add or remove if necessary if ("add" === action) { this.serverQueueItems.push( { "item" : item, "file" : file, "changes" : changes } ); if (0 < item.indexOf('action=save')) { txtArea = document.createElement('textarea'); txtArea.setAttribute('id', 'saveTemp' + nextSaveID); document.body.appendChild(txtArea); // If we're saving as or the file version is undefined, set the temp save value as the contents if (0 < item.indexOf('saveType=saveAs') || 0 < item.indexOf('fileVersion=undefined')) { get('saveTemp' + nextSaveID).value = cM.getValue(); // Else we can save the JSON version of the changes to implement } else { get('saveTemp' + nextSaveID).value = changes; } } } else if ("del" === action) { if (this.serverQueueItems[0] && 0 < this.serverQueueItems[0].item.indexOf('action=save')) { topSaveID = nextSaveID - 1; for (var i = 1; i < topSaveID; i++) { get('saveTemp' + i).value = get('saveTemp' + (i + 1)).value; } element = get('saveTemp' + topSaveID); element.parentNode.removeChild(element); } this.serverQueueItems.splice(0, 1); } // If we've just removed from the array and there's another action queued up, or we're triggering for the first time // then do the next requested process, stored at array pos 0 if ("del" === action && 1 <= this.serverQueueItems.length || 1 === this.serverQueueItems.length) { // If we have an item, we're not saving previous file refs and not loading if (this.serverQueueItems[0].item && ( -1 === this.serverQueueItems[0].item.indexOf('saveFiles=') && -1 === this.serverQueueItems[0].item.indexOf('action=load') )) { xhr = this.xhrObj(); xhr.onreadystatechange=function() { if (4 === xhr.readyState) { // OK response? if (200 === xhr.status) { // Parse the response as a JSON object statusObj = JSON.parse(xhr.responseText); // Set the action end time and time taken in JSON object statusObj.action.timeEnd = new Date().getTime(); statusObj.action.timeTaken = statusObj.action.timeEnd - statusObj.action.timeStart; // User wanted raw (or both) output of the response? if (0 <= ["raw", "both"].indexOf(ICEcoder.fileDirResOutput)) { console.log(xhr.responseText); } // User wanted object (or both) output of the response? if (0 <= ["object", "both"].indexOf(ICEcoder.fileDirResOutput)) { console.log(statusObj); } // If error, show that, otherwise do whatever we're required to do next if (statusObj.status.error) { ICEcoder.message(statusObj.status.errorMsg); console.log("ICEcoder error info for your request..."); console.log(statusObj); ICEcoder.serverMessage(); ICEcoder.serverQueue('del'); // Successful, process the requested action to take now } else { eval(statusObj.action.doNext); // If we need to update the multiple results pane with new info now a task is done successfully if (ICEcoder.findUpdateMultiInfoID[0]) { get('multipleResultsIFrame').contentWindow.document.getElementById(ICEcoder.findUpdateMultiInfoID[0]) .innerHTML = ICEcoder.findUpdateMultiInfoID[1]; ICEcoder.findUpdateMultiInfoID = []; } } // Some other response? Display a message about that } else { ICEcoder.message(t['Sorry there was...']); console.log("ICEcoder error info for your request..."); console.log(statusObj); ICEcoder.serverMessage(); ICEcoder.serverQueue('del'); } } }; xhr.open("POST", this.serverQueueItems[0].item, true); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); timeStart = new Date().getTime(); // Save as events need to send all contents if (0 < this.serverQueueItems[0].item.indexOf('action=saveAs')) { xhr.send( 'timeStart=' + timeStart + '&file=' + this.serverQueueItems[0].file + '&contents=' + encodeURIComponent(document.getElementById('saveTemp1').value) ); // Save events can just send the changes } else if (0 < this.serverQueueItems[0].item.indexOf('action=save')) { xhr.send( 'timeStart=' + timeStart + '&file=' + this.serverQueueItems[0].file + '&changes='+encodeURIComponent(document.getElementById('saveTemp1').value) ); // Another type of event } else { xhr.send( 'timeStart=' + timeStart + '&file=' + this.serverQueueItems[0].file ); } } else { // File loading done via fileControl iFrame setTimeout(function(ic) { if ("undefined" != typeof ic.serverQueueItems[0]) { ic.filesFrame.contentWindow.frames['fileControl'].location.href = ic.serverQueueItems[0].item; } }, 1, this); } } }, // Cancel all actions on pressing Esc in non content areas cancelAllActions: function() { // Stop whatever the parent may be loading and clear tasks other than the current one window.stop(); if (0 < this.serverQueueItems.length) { this.serverQueueItems.splice(1, this.serverQueueItems.length); } this.showHide('hide', get('loadingMask')); this.serverMessage('' + t['Cancelled tasks'] + ''); setTimeout(function(ic) {ic.serverMessage();}, 2000, this); }, // Set the current previousFiles in the settings file setPreviousFiles: function() { let previousFiles; previousFiles = this.openFiles .join(',') .replace(/\//g, "|") .replace(/(\|\[NEW\])|(,\|\[NEW\])/g, "") .replace(/(^,)|(,$)/g, ""); if ("" == previousFiles) { previousFiles = "CLEAR"; } // Then send through to the settings page to update setting this.serverQueue( "add", this.iceLoc + "/lib/settings.php?saveFiles=" + encodeURIComponent(previousFiles) + "&csrf=" + this.csrf, encodeURIComponent(previousFiles) ); this.updateLast10List(previousFiles); }, // Update the list of 10 previous files in browser updateLast10List: function(previousFiles) { let newFile, last10Files, last10FilesList; // Split our previous files string into an array previousFiles = previousFiles.split(','); // For each one of those, if it's not 'CLEAR' we can maybe rotate the list for (let i = 0; i < previousFiles.length; i++) { if ("CLEAR" !== previousFiles[i]) { // Set the new file LI item to maybe insert at top of the list, including trailing new line to split on in future newFile = "
  • " + previousFiles[i].replace(/\|/g,"/") + "
  • \n"; // Get DOM elem for last 10 files last10Files = this.content.contentWindow.document.getElementById('last10Files'); // If the innerHTML of that doesn't contain our new item, we can insert it if(-1 === last10Files.innerHTML.indexOf(newFile)) { // Get the last 10 files list, pop the last one off and add newFile at start last10FilesList = last10Files.innerHTML.split("\n"); if ( // No more than 8 + 1 we're about to add last10FilesList.length > 8 || // Clear out placeholder last10FilesList[0] === '
    [none]


    ' || // No empty array items "" == last10FilesList[last10FilesList.length-1] ) { last10FilesList.pop(); } // Update the list last10Files.innerHTML = newFile + (last10FilesList.join("\n")); } } } }, // Opens the last files we had open autoOpenFiles: function() { if (0 < this.previousFiles.length) { for (let i = 0; i < this.previousFiles.length; i++) { this.openFile(this.previousFiles[i].replace('|', '/')); } } }, // Show the settings screen settingsScreen: function(hide, tab) { let tabExtra; if (false === hide) { tabExtra = tab ? '?tab=' + tab + '&csrf=' + this.csrf : ''; get('mediaContainer').innerHTML = ''; } this.showHide(hide ? 'hide' : 'show', get('blackMask')); }, // Show the help screen helpScreen: function() { get('mediaContainer').innerHTML = ''; this.showHide('show', get('blackMask')); }, // Show the backup versions screen versionsScreen: function(file) { get('mediaContainer').innerHTML = ''; this.showHide('show', get('blackMask')); }, // Show the ICEcoder manual, loaded remotely showManual: function(version, section) { let sectionExtra; sectionExtra = section ? "#" + section : ""; get('mediaContainer').innerHTML = ''; this.showHide('show', get('blackMask')); }, // Show the properties screen propertiesScreen: function(fileName) { get('mediaContainer').innerHTML = ''; this.showHide('show', get('blackMask')); }, // Show the auto-logout warning screen autoLogoutWarningScreen: function() { get('mediaContainer').innerHTML = ''; this.showHide('show', get('blackMask')); }, // Show the bug report screen bugReportScreen: function() { get('mediaContainer').innerHTML = ''; this.showHide('show', get('blackMask')); }, // Show the plugins manager pluginsManager: function() { get('mediaContainer').innerHTML = ''; this.showHide('show', get('blackMask')); }, // Update the settings used when we make a change to them useNewSettings: function(settings) { let styleNode, thisCSS, strCSS, activeLineBG, activeLineNum; const lightThemes = ["base16-light", "chrome-devtools", "duotone-light", "eclipse", "eiffel", "elegant", "mdn-like", "idle", "iplastic", "ir_white", "johnny", "juicy", "neat", "neo", "solarized", "ttcn", "xq-light"]; const darkThemes = ["3024-night", "all-hallow-eve", "black-pearl-ii", "blackboard", "colorforth", "django", "emacs-strict", "fade-to-grey", "fake", "glitterbomb", "isotope", "ir_black", "liquibyte", "monokai-fannonedition", "oceanic", "night", "spectacular", "sunburst", "the-matrix", "tomorrow-night-blue", "tomorrow-night-bright", "tomorrow-night-eighties", "vibrant-ink", "xq-dark", "zenburn"]; // Set iceRoot and update in settings display iceRoot = settings.iceRoot; this.content.contentWindow.document.getElementById('iceRootDisplay').innerText = "" !== iceRoot ? iceRoot : "[Default]"; // Cut out path prefix, .css file extension and ?microtime= querystring const newTheme = settings.themeURL.replace(/.+\/|.css.+/g, ""); // If theme was not changed - no need to do all these tricks if (this.theme !== newTheme){ // Add new stylesheet for selected theme to editor this.theme = newTheme; styleNode = document.createElement('link'); styleNode.setAttribute('rel', 'stylesheet'); styleNode.setAttribute('type', 'text/css'); styleNode.setAttribute('href', settings.themeURL); this.content.contentWindow.document.getElementsByTagName('head')[0].appendChild(styleNode); this.switchTab(this.selectedTab); } // Set the active line color activeLineBG = // Light themes -1 < lightThemes.indexOf(this.theme) ? "#ccc" // Dark themes : -1 < darkThemes.indexOf(this.theme) ? "#222" // Other themes : "#000"; // Set the active line color activeLineNum = // Light themes -1 < lightThemes.indexOf(this.theme) ? "#222" // Dark themes : -1 < darkThemes.indexOf(this.theme) ? "#ccc" // Other themes : "#ccc"; // Check/uncheck Code Assist setting if (settings.codeAssist !== this.codeAssist) { this.codeAssistToggle(); } // Unlock/lock the file manager if (settings.lockedNav !== this.lockedNav) { this.lockUnlockNav(); this.changeFilesW(!settings.lockedNav ? 'contract' : 'expand'); this.hideFileMenu(); }; // Update font size at top level thisCSS = document.styleSheets[0]; strCSS = thisCSS.rules ? 'rules' : 'cssRules'; thisCSS[strCSS][0].style['fontSize'] = settings.fontSize; // Update font size in file manager thisCSS = this.filesFrame.contentWindow.document.styleSheets[4]; strCSS = thisCSS.rules ? 'rules' : 'cssRules'; thisCSS[strCSS][0].style['fontSize'] = settings.fontSize; // Update styles in editor thisCSS = this.content.contentWindow.document.styleSheets[6]; strCSS = thisCSS.rules ? 'rules' : 'cssRules'; thisCSS[strCSS][0].style['fontSize'] = settings.fontSize; thisCSS[strCSS][4].style['border-left-width'] = settings.visibleTabs ? '1px' : '0'; thisCSS[strCSS][4].style['margin-left'] = settings.visibleTabs ? '-1px' : '0'; thisCSS[strCSS][2].style.cssText = "background-color: " + activeLineBG + " !important"; thisCSS[strCSS][5].style.cssText = "color: " + activeLineNum + " !important"; // Set many of the ICEcoder settings this.lineWrapping = settings.lineWrapping; this.lineNumbers = settings.lineNumbers; this.showTrailingSpace = settings.showTrailingSpace; this.matchBrackets = settings.matchBrackets; this.autoCloseTags = settings.autoCloseTags; this.autoCloseBrackets = settings.autoCloseBrackets; this.indentType = settings.indentType; this.indentSize = settings.indentSize; this.indentAuto = settings.indentAuto; this.scrollbarStyle = settings.scrollbarStyle; // Then apply the settings to each editor instance for (let i = 0; i < this.cMInstances.length; i++) { // Main pane this.content.contentWindow['cM' + this.cMInstances[i]].setOption("lineWrapping", this.lineWrapping); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("lineNumbers", this.lineNumbers); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("showTrailingSpace", this.showTrailingSpace); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("matchBrackets", this.matchBrackets); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("autoCloseTags", this.autoCloseTags); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("autoCloseBrackets", this.autoCloseBrackets); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("indentWithTabs", "tabs" === this.indentType); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("indentUnit", this.indentSize); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("tabSize", this.indentSize); this.content.contentWindow['cM' + this.cMInstances[i]].setOption("scrollbarStyle", this.scrollbarStyle); this.content.contentWindow['cM' + this.cMInstances[i]].refresh(); // Diff pane this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("lineWrapping", this.lineWrapping); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("lineNumbers", this.lineNumbers); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("showTrailingSpace", this.showTrailingSpace); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("matchBrackets", this.matchBrackets); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("autoCloseTags", this.autoCloseTags); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("autoCloseBrackets", this.autoCloseBrackets); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("indentWithTabs", "tabs" === this.indentType); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("indentUnit", this.indentSize); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("tabSize", this.indentSize); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].setOption("scrollbarStyle", this.scrollbarStyle); this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].refresh(); } if (settings.tagWrapperCommand !== this.tagWrapperCommand) { this.tagWrapperCommand = settings.tagWrapperCommand; } if (settings.autoComplete !== this.autoComplete) { this.autoComplete = settings.autoComplete; } get('plugins').style.left = "left" === settings.pluginPanelAligned ? "0" : "auto"; get('plugins').style.right = "right" === settings.pluginPanelAligned ? "0" : "auto"; // Enable/disable select next on find input and set goToLine scroll speed this.selectNextOnFindInput = settings.selectNextOnFindInput; this.goToLineScrollSpeed = settings.goToLineScrollSpeed; // Restart bug checking this.bugFilePaths = settings.bugFilePaths; this.bugFileCheckTimer = settings.bugFileCheckTimer; this.bugFileMaxLines = settings.bugFileMaxLines; if ("" != this.bugFilePaths[0]) { this.startBugChecking(); } else { if ("undefined" != typeof this.bugFileCheckInt) { get('bugIcon').style.color = ""; get('bugIcon').title = "Bug reporting not active"; clearInterval(this.bugFileCheckInt); } } // Update diffs if we have a split pane if (true === this.splitPane) { this.updateDiffs(); } // Set the flag to indicate if we update diff pane on save this.updateDiffOnSave = settings.updateDiffOnSave; // Set the auto-logout mins value this.autoLogoutMins = settings.autoLogoutMins; // Finally, refresh the file manager if we need to if (true === settings.refreshFM) { this.refreshFileManager(); }; }, // Update and show/hide found results display? updateResultsDisplay: function(showHide) { this.findReplace(get('find').value, false, false, false); get('results').style.display = "show" === showHide ? 'inline-block' : 'none'; }, // Toggle full screen on/off fullScreenSwitcher: function() { // Future use if ("undefined" != typeof document.cancelFullScreen) { document.fullScreen ? document.cancelFullScreen() : document.body.requestFullScreen(); // Moz specific } else if ("undefined" != typeof document.mozCancelFullScreen) { document.mozFullScreen ? document.mozCancelFullScreen() : document.body.mozRequestFullScreen(); // Chrome specific } else if ("undefined" != typeof document.webkitCancelFullScreen) { document.webkitIsFullScreen ? document.webkitCancelFullScreen() : document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); } }, // Pass target file/folder to Zip It! zipIt: function(tgt) { tgt=tgt.replace(/\//g, "|"); this.filesFrame.contentWindow.frames['fileControl'].location.href = this.iceLoc + "/plugins/zip-it/index.php?zip=" + tgt + "&csrf=" + this.csrf; }, // Prompt to download our file downloadFile: function(file) { file=file.replace(/\//g, "|"); this.filesFrame.contentWindow.frames['fileControl'].location.href = this.iceLoc + "/lib/download.php?file=" + file + "&csrf=" + this.csrf; }, // Change permissions on a file/folder chmod: function(file, perms) { file = file.replace(iceRoot, ""); this.showHide('hide', get('blackMask')); this.serverQueue("add", this.iceLoc + "/lib/file-control.php?action=perms&perms=" + perms + "&csrf=" + this.csrf, encodeURIComponent(file)); this.serverMessage('chMod ' + perms + ' on ' + file.replace(/\|/g, "/").replace(/^\/|/g, "")); }, // Open/show the preview window openPreviewWindow: function() { if (0 < this.openFiles.length) { let filepath, filename, fileExt; filepath = this.openFiles[this.selectedTab - 1]; filename = filepath.substr(filepath.lastIndexOf("/") + 1); fileExt = filename.substr(filename.lastIndexOf(".") + 1); this.previewWindowLoading = true; this.previewWindow = window.open(filepath, "previewWindow", 500, 500); if (-1 < ["md"].indexOf(fileExt)) { this.previewWindow.addEventListener('load', function(ic, content) { ic.previewWindowLoading = false; ic.previewWindow.document.documentElement.innerHTML = "" setTimeout(function() { ic.previewWindow.document.documentElement.innerHTML = content; }, 100); }(ic, mmd(ic.getThisCM().getValue())), false); } else { this.previewWindow.onload = function() { this.previewWindowLoading = false; // Do the pesticide plugin if it exists try {this.doPesticide();} catch(err) {}; // Do the stats.js plugin if it exists try {this.doStatsJS('open');} catch(err) {}; // Do the responsive plugin if it exists try {this.doResponsive();} catch(err) {}; } } } }, // Reset auto-logout timer resetAutoLogoutTimer: function() { if (1 < this.autoLogoutMins && this.autoLogoutTimer > (this.autoLogoutMins * 60) - 60) { this.showHide('hide', get('blackMask')); } this.autoLogoutTimer = 0; }, // Logout of ICEcoder logout: function(type) { window.location = window.location + "?logout&" + (type ? "type=" + type + "&" : "") + "csrf=" + this.csrf; }, // Show a message outputMsg: function(msg) { let output = this.output.innerHTML; // If only placeholder, clear that if ("Output
    via ICEcoder.output(message);

    " === output) { output = ""; } this.output.innerHTML = msg + "

    " + output + ("" !== output ? "

    " : ""); }, // Show a message message: function(msg) { alert(msg); }, // Ask for confirmation ask: function(question) { return confirm(question); }, // Get the users input getInput: function(question, defaultValue) { return prompt(question, defaultValue); }, // Show a data screen message dataMessage: function(message) { let dM; dM = this.content.contentWindow.document.getElementById('dataMessage'); dM.style.display = "block"; dM.innerHTML = message; }, // Update ICEcoder // update: function() { // if (true == confirm(t['Please note for...'])) { // this.showHide('show', get('loadingMask')); // window.location = this.iceLoc + "/lib/updater.php"; // } else { // window.open("https://icecoder.net"); // } // }, // ICEcoder just updated updated: function() { get('blackMask').style.visibility = "visible"; get('mediaContainer').innerHTML = '

    Thanks for updating to v' + this.versionNo + '!

    ' + '

    Click anywhere to continue using this...

    '; }, // XHR object xhrObj: function(){ try {return new XMLHttpRequest();}catch(e){} return null; }, // Open bug report openBugReport: function() { if ("off" === this.bugReportStatus) { this.message(t['You can start...']); } if ("error" === this.bugReportStatus) { this.message(t['Error cannot find...']); } if ("ok" === this.bugReportStatus) { this.message(t['No new errors...']); } if ("bugs" === this.bugReportStatus) { // Show bug report screen and set the bugs state as seen this.bugReportScreen(); this.bugFilesSizesSeen = this.bugFilesSizesActual; } }, // Start bug checking by looking in bug file paths on a timer startBugChecking: function() { var bugCheckURL; if (0 !== this.bugFileCheckTimer) { // Clear any existing interval if ("undefined" != typeof this.bugFileCheckInt) { clearInterval(this.bugFileCheckInt); } // Start a new timer this.bugFilesSizesSeen = []; this.bugFileCheckInt = setInterval(function(ic) { bugCheckURL = ic.iceLoc + "/lib/bug-files-check.php?" + "files=" + ("" !== ic.bugFilePaths[0] ? ic.bugFilePaths.join() : "null").replace(/\//g, "|") + "&filesSizesSeen="; if (ic.bugFilesSizesSeen.length !== ic.bugFilePaths.length) { // Fill the array with nulls for (let i = 0; i < ic.bugFilePaths.length; i++) { ic.bugFilesSizesSeen[i] = "null"; } } bugCheckURL += ic.bugFilesSizesSeen.join() + "&maxLines=" + ic.bugFileMaxLines + "&csrf=" + ic.csrf; var xhr = ic.xhrObj(); xhr.onreadystatechange=function() { if (4 === xhr.readyState && 200 === xhr.status) { var statusArray = JSON.parse(xhr.responseText); get('bugIcon').style.color = statusArray['result'] == "off" ? "" : statusArray['result'] == "ok" ? "#080" : statusArray['result'] == "bugs" ? "#b00" : "#f80"; // if the result is 'error' or another value get('bugIcon').title = statusArray['result'] == "off" ? "Bug reporting not active" : statusArray['result'] == "ok" ? "No new errors found" : statusArray['result'] == "bugs" ? "New bugs found, click to view" : "Unable to find bug log file specified"; // Setup error ic.bugReportStatus = statusArray['result']; if ("null" == ic.bugFilesSizesSeen[0]) { ic.bugFilesSizesSeen = statusArray['filesSizesSeen']; } ic.bugFilesSizesActual = statusArray['filesSizesSeen']; ic.bugReportPath = statusArray['bugReportPath']; } }; xhr.open("GET", bugCheckURL, true); xhr.send(); }, parseInt(this.bugFileCheckTimer * 1000, 10), this); // State that we're checking for bugs this.bugReportStatus = "ok"; } else { if ("undefined" != typeof this.bugFileCheckInt) { get('bugIcon').style.color = ""; get('bugIcon').title = "Bug reporting not active"; clearInterval(this.bugFileCheckInt); } } }, // Return safe HTML equivalents xssClean: function(data) { return data .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }, // Print code of current tab printCode: function() { let thisCM, printIFrame; thisCM = this.getThisCM(); printIFrame = this.filesFrame.contentWindow.frames['fileControl']; // Print page content injected into iFrame, escaped with pre and xssClean printIFrame.window.document.body.innerHTML = '' + this.openFiles[this.selectedTab - 1] + '
    ' +
                this.xssClean(thisCM.getValue()) +
                '
    '; printIFrame.focus(); printIFrame.print(); // Focus back on code thisCM.focus(); }, // Update the title tag to indicate any changes indicateChanges: function() { let winTitle; if (false === this.loadingFile) { winTitle = "ICEcoder " + this.versionNo; for(let i = 1; i <= this.savedPoints.length; i++) { if (this.savedPoints[i-1] !== this.getcMInstance(i).changeGeneration()) { // We have an unsaved tab, indicate that in the title winTitle += " \u2744"; break; } } document.title = winTitle; } }, // ==== // TABS // ==== // Change tabs by switching visibility of instances switchTab: function(newTab, noFocus) { var cM, cMdiff, thisCM; // If we're not switching to same tab (for some reason), note the previous tab if (newTab !== this.selectedTab) { this.prevTab = this.selectedTab; } // Identify tab that's currently selected & get the instance this.selectedTab = newTab; cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); thisCM = this.editorFocusInstance.indexOf('diff') > -1 ? cMdiff : cM; if (thisCM) { // Switch mode to HTML, PHP, CSS etc this.switchMode(); // Set all cM instances to be hidden, then make our selected instance visible for (var i = 0; i < this.cMInstances.length; i++) { this.content.contentWindow['cM' + this.cMInstances[i]].getWrapperElement().style.display = "none"; this.content.contentWindow['cM' + this.cMInstances[i] + "diff"].getWrapperElement().style.display = "none"; } cM.setOption('theme', this.theme); cMdiff.setOption('theme', this.theme + " diff"); cM.getWrapperElement().style.display = "block"; cMdiff.getWrapperElement().style.display = "block"; // Redo our diffs if split pane if (this.splitPane) { this.updateDiffs(); } // Focus on & refresh our selected instance if (!noFocus) { setTimeout(function(ic) { ic.focus(); }, 4, this); } cM.refresh(); cMdiff.refresh(); // Update list of functions & classes plus Git diffs this.updateFunctionClassList(); this.highlightGitDiffs(); // Highlight the selected tab this.redoTabHighlight(this.selectedTab); // Update our versions display this.updateVersionsDisplay(); // Detect if we have a scrollbar & set layout again setTimeout(function(ic) { ic.scrollBarVisible = thisCM.getScrollInfo().height > thisCM.getScrollInfo().clientHeight; ic.findReplace(get('find').value, false, false, false); ic.setLayout(); }, 0, this); // Finally, update the cursor display this.getCaretPosition(); this.updateCharDisplay(); this.updateByteDisplay(); } }, // Starts a new file by setting a few vars & creating a new cM instance newTab: function(autoSave) { var cM; this.cMInstances.push(this.nextcMInstance); this.selectedTab = this.cMInstances.length; this.showHide('show', this.content); this.content.contentWindow.createNewCMInstance(this.nextcMInstance); this.setLayout(); this.openFile('/[NEW]'); cM = this.getcMInstance('new'); this.switchTab(this.openFiles.length); cM.removeLineClass(this['cMActiveLinecM' + this.cMInstances[this.selectedTab - 1]], "background"); this['cMActiveLinecM' + this.selectedTab] = cM.addLineClass(0, "background", "cm-s-activeLine"); this.nextcMInstance++; // Also auto trigger save if (true === autoSave) { this.saveFile(false, true); } }, // Create a new tab for a file createNewTab: function(isNew, shortURL) { let closeTabLink, fileName, fileExt; // Push new file into array this.openFiles.push(shortURL); // Setup a new tab closeTabLink = ''; get('tab' + (this.openFiles.length)).style.display = "inline-block"; fileName = this.openFiles[this.openFiles.length - 1]; fileExt = fileName.substr(fileName.lastIndexOf(".") + 1); get('tab' + (this.openFiles.length)).innerHTML = closeTabLink + "" + fileName.slice(fileName.lastIndexOf("/")).replace(/\//, ""); get('tab' + (this.openFiles.length)).title = "/" + this.openFiles[this.openFiles.length - 1].replace(/\//, ""); get('tab' + (this.openFiles.length)).className = "tab ext-" + fileExt; // Set the widths this.setTabWidths(false); // Highlight it and state it's selected this.redoTabHighlight(this.openFiles.length); this.selectedTab = this.openFiles.length; // Add a new value ready to indicate if this content has been changed this.savedPoints.push(0); this.savedContents.push(""); if (!isNew) { this.setPreviousFiles(); } }, // Cycle to next tab nextTab: function() { let goToTab; goToTab = this.selectedTab + 1 <= this.openFiles.length ? this.selectedTab + 1 : 1; this.switchTab(goToTab, 'noFocus'); }, // Cycle to next tab previousTab: function() { let goToTab; goToTab = this.selectedTab - 1 >= 1 ? this.selectedTab - 1 : this.openFiles.length; this.switchTab(goToTab, 'noFocus'); }, // Create a new tab for a file renameTab: function(tabNum, newName) { var closeTabLink, fileName, fileExt; // Push new file into array this.openFiles[tabNum - 1] = newName; // Setup a new tab closeTabLink = ''; fileName = this.openFiles[tabNum - 1]; fileExt = fileName.substr(fileName.lastIndexOf(".") + 1); get('tab' + tabNum).innerHTML = closeTabLink + "" + fileName.slice(fileName.lastIndexOf("/")).replace(/\//, ""); get('tab' + tabNum).title = "/" + this.openFiles[tabNum - 1].replace(/\//, ""); get('tab' + tabNum).className = "tab ext-" + fileExt; }, // Reset all tabs to be without a highlight and then highlight the selected redoTabHighlight: function(selectedTab) { let folderFileElems, fileLink; // For all open tabs... for (let i = 1; i <= this.savedPoints.length; i++) { // Set the close tab icon BG color according to save status if (get('tab' + i).childNodes[0]) { get('tab' + i).childNodes[0].childNodes[0].style.backgroundColor = this.savedPoints[i - 1] != this.getcMInstance(i).changeGeneration() ? "#b00" : ""; } // Set the BG and text color for tabs according to if it's the current tab or not get('tab' + i).style.color = i === selectedTab ? this.colorCurrentText : this.colorOpenTextTab; get('tab' + i).style.background = i === selectedTab ? this.colorCurrentBG : this.colorOpenBG; } // Now we can set about setting the coloring of dirs/files in the file manager // First we clear the highlighing, then highlight the open dirs/files, then highlight the current // file that's open as a tab (overides open highlighting) and finally highlight all of the // user selected dirs/files (overrides previous highlighting too) // Clear all highlighting folderFileElems = this.filesFrame.contentWindow.document.getElementsByTagName("SPAN"); for (let i = 0; i < folderFileElems.length; i++) { if (-1 === folderFileElems[i].id.indexOf("_perms") && "" !== folderFileElems[i].style.backgroundColor) { folderFileElems[i].style.backgroundColor = ""; folderFileElems[i].style.color = ""; } } // Highlight all open files for (let i = 0; i < this.openFiles.length; i++) { fileLink = this.filesFrame.contentWindow.document.getElementById(this.openFiles[i].replace(/\//g, "|")); if (fileLink) { fileLink.style.backgroundColor = this.colorOpenBG; fileLink.style.color = this.colorOpenTextFile; } } // Highlight the file that's the current tab if (1 <= this.selectedTab) { fileLink = this.filesFrame.contentWindow.document.getElementById(this.openFiles[this.selectedTab - 1].replace(/\//g, "|")); if (fileLink) { fileLink.style.backgroundColor = this.colorCurrentBG; fileLink.style.color = this.colorCurrentText; } } // Highlight all user selected files for (let i = 0; i < this.selectedFiles.length; i++) { fileLink = this.filesFrame.contentWindow.document.getElementById(this.selectedFiles[i]); if (fileLink) { fileLink.style.backgroundColor = this.colorSelectedBG; fileLink.style.color = this.colorSelectedText; } } }, // Close the tab upon request closeTab: function(closeTabNum, dontSetPV, dontAsk) { let okToRemove, closeFileName; // If we haven't specified, close current tab if (!closeTabNum) { closeTabNum = this.selectedTab; }; okToRemove = true; // Only confirm if we're OK to ask and... if (!dontAsk && ( ("/[NEW]" === this.openFiles[closeTabNum - 1] // ...it's a new file that's not empty ? "" !== this.getcMInstance(closeTabNum).getValue() // ...or it's not a new file and it's not saved : this.savedPoints[closeTabNum - 1] !== this.getcMInstance(closeTabNum).changeGeneration() ) )) { okToRemove = this.ask(t['You have made...'] + "\n\n" + this.openFiles[closeTabNum - 1]); } if (true === okToRemove) { // Get the filename of tab we're closing closeFileName = this.openFiles[closeTabNum - 1]; // Recursively copy over all tabs & data from the tab to the right, if there is one for (let i = closeTabNum; i < this.openFiles.length; i++) { this.renameTab(i, this.openFiles[i]); this.openFiles[i - 1] = this.openFiles[i]; this.openFileMDTs[i - 1] = this.openFileMDTs[i]; this.openFileVersions[i - 1] = this.openFileVersions[i]; } // Hide the instance we're closing by setting the hide class and removing from the array this.content.contentWindow['cM' + this.cMInstances[closeTabNum - 1]].getWrapperElement().style.display = "none"; this.content.contentWindow['cM' + this.cMInstances[closeTabNum - 1] + 'diff'].getWrapperElement().style.display = "none"; this.cMInstances.splice(closeTabNum - 1, 1); // clear the rightmost tab (or only one left in a 1 tab scenario) & remove from the array get('tab' + this.openFiles.length).style.display = "none"; get('tab' + this.openFiles.length).innerHTML = ""; get('tab' + this.openFiles.length).title = ""; get('tab' + this.openFiles.length).className = ""; this.openFiles.pop(); this.openFileMDTs.pop(); this.openFileVersions.pop(); // If we're closing the selected tab, determine the new selectedTab number, reduced by 1 if we have some tabs, 0 for a reset state if (this.selectedTab === closeTabNum) { 0 < this.openFiles.length ? this.selectedTab -= 1 : this.selectedTab = 0; } // If we're closing tab to left of selectedTab, will need to reduce selectedTab if (closeTabNum < ICEcoder.selectedTab) { this.selectedTab--; } // Handle removing a tab from start or end as safely fallback if (0 < this.openFiles.length && this.selectedTab === 0) { this.selectedTab = 1; }; if (0 < this.openFiles.length && this.selectedTab > this.openFiles.length) { this.selectedTab = this.openFiles.length; }; // Grey out the view icon if (0 === this.openFiles.length) { this.fMIconVis('fMView', 0.3); // Also clear any find results this.clearResultsDisplays(); } else { // Switch the mode & the tab this.switchMode(); this.switchTab(this.selectedTab); } // Highlight the selected tab after splicing the change state out of the array this.savedPoints.splice(closeTabNum - 1, 1); this.savedContents.splice(closeTabNum - 1, 1); this.redoTabHighlight(this.selectedTab); // Remove any highlighting from the file manager this.selectDeselectFile( 'deselect', this.filesFrame.contentWindow.document.getElementById( closeFileName.replace(/\//g, "|") ) ); if (!dontSetPV) { this.setPreviousFiles(); } // Update the versions display this.updateVersionsDisplay(); // Update the title tag to indicate any changes this.indicateChanges(); // Set bool value to false to avoid being stuck on true this.overCloseLink = false; } // Lastly, set the widths this.setTabWidths(true); }, // Close all tabs closeAllTabs: function() { if (0 < this.cMInstances.length && this.ask(t['Close all tabs'])) { for (let i = this.cMInstances.length; 0 < i; i--) { this.closeTab(i, i > 1 ? true : false); } } // Update the title tag to indicate any changes this.indicateChanges(); }, // Set the tab widths setTabWidths: function(posOnlyNewTab) { let availWidth, avgWidth, tabWidth, lastLeft, lastWidth; if (this.ready) { availWidth = parseInt(this.content.style.width, 10) - 53 - 22 - 10; // - left margin - new tab - right margin avgWidth = (availWidth / this.openFiles.length ) - 18; tabWidth = -18; // Incl 18px offset lastLeft = 53; lastWidth = 0; this.tabLeftPos = []; for (let i = 0; i < this.openFiles.length; i++) { if (true === posOnlyNewTab) { i = this.openFiles.length; }; tabWidth = this.openFiles.length * (150 + 18) > availWidth ? parseInt(avgWidth * i, 10) - parseInt(avgWidth * (i - 1), 10) : 150; lastLeft = 0 === i ? 53 : parseInt(get('tab' + i).style.left, 10); lastWidth = 0 === i ? 0 : parseInt(get('tab' + i).style.width, 10) + 18; if (false === posOnlyNewTab) { get('tab' + (i + 1)).style.left = (lastLeft + lastWidth) + "px"; get('tab' + (i + 1)).style.width = tabWidth + "px"; } else { tabWidth = -18; } this.tabLeftPos.push(lastLeft + lastWidth); } get('newTab').style.left = (lastLeft + lastWidth + tabWidth + 18) + "px"; } }, // Tab dragging start tabDragStart: function(tab) { let fileName, fileExt; this.draggingTab = tab; this.diffStartX = this.mouseX; this.tabDragMouseXStart = (this.mouseX - (parseInt(this.files.style.width, 10) + 53 + 18)) % 150; // Put tab we're dragging over others get('tab' + tab).style.zIndex = 2; // Set classes for other tabs (tabSlide) and the one we're dragging (tabDrag) for (let i = 1; i <= this.openFiles.length; i++) { fileName = this.openFiles[i - 1]; fileExt = fileName.substr(fileName.lastIndexOf(".") + 1); get('tab' + i).className = i !== tab ? "tab ext-" + fileExt + " tabSlide" : "tab ext-" + fileExt + " tabDrag"; } }, // Tab dragging tabDragMove: function() { let lastTabWidth, thisLeft, dragTabNo, tabWidth; lastTabWidth = parseInt(get('tab' + this.openFiles.length).style.width, 10) + 18; // Set the left position but stay within left side (53) and new tab this.thisLeft = thisLeft = this.tabDragMouseX >= 53 ? this.tabDragMouseX <= parseInt(get('newTab').style.left, 10) - lastTabWidth ? this.tabDragMouseX : (parseInt(get('newTab').style.left, 10) - lastTabWidth) : 53; get('tab' + this.draggingTab).style.left = thisLeft + "px"; this.dragTabNo = dragTabNo = this.draggingTab; // Set the opacities of tabs then positions of tabs we're not dragging for (let i = 1; i <= this.openFiles.length; i++) { get('tab' + i).style.opacity = i === this.draggingTab ? 1 : 0.5; tabWidth = this.tabLeftPos[i] ? this.tabLeftPos[i] - this.tabLeftPos[i - 1] : tabWidth; if (i !== this.draggingTab) { if (i < this.draggingTab) { get('tab' + i).style.left = thisLeft <= this.tabLeftPos[i - 1] ? this.tabLeftPos[i - 1] + tabWidth : this.tabLeftPos[i - 1]; } else { get('tab' + i).style.left = thisLeft >= this.tabLeftPos[i - 1] ? this.tabLeftPos[i - 1] - tabWidth : this.tabLeftPos[i - 1]; } } } }, // Tab dragging end tabDragEnd: function() { let swapWith, fileName, fileExt, tempArray; // Set the tab widths this.setTabWidths(false); // Determine what tabs we've swapped and reset classname, opacity & z-index for all for (let i = 1; i <= this.openFiles.length; i++) { if (this.thisLeft >= this.tabLeftPos[i - 1]) { swapWith = this.thisLeft === this.tabLeftPos[0] ? 1 : this.dragTabNo > i ? i + 1 : i; } fileName = this.openFiles[i - 1]; fileExt = fileName.substr(fileName.lastIndexOf(".") + 1); get('tab' + i).className = "tab ext-" + fileExt; get('tab' + i).style.opacity = 1; if (i !== this.dragTabNo) { get('tab' + i).style.zIndex = 1; } else { if ("undefined" !== typeof swapWith) { setTimeout(function (num) { get('tab' + num).style.zIndex = 1; }, 150, swapWith); } } } if (this.thisLeft && this.thisLeft !== false) { // Make a number ascending array tempArray = []; for (let i = 1; i <= this.openFiles.length; i++) { tempArray.push(i); } // Then swap our tab numbers tempArray.splice(this.dragTabNo - 1, 1); tempArray.splice(swapWith - 1, 0, this.dragTabNo); // Now we have an order to sort against this.sortTabs(tempArray); } this.setTabWidths(false); this.draggingTab = false; this.thisLeft = false; }, // Sort tabs into new order sortTabs: function(newOrder) { let a, b, savedPoints = [], savedContents = [], openFiles = [], openFileMDTs = [], openFileVersions = [], cMInstances = [], selectedTabWillBe; // Setup an array of our actual arrays and the blank ones a = [this.savedPoints, this.savedContents, this.openFiles, this.openFileMDTs, this.openFileVersions, this.cMInstances]; b = [savedPoints, savedContents, openFiles, openFileMDTs, openFileVersions, cMInstances]; // Push the new order values into array b then set into array a for (let i = 0; i < a.length; i++) { for (let j = 0; j < a[i].length; j++) { b[i].push(a[i][newOrder[j] - 1]); } a[i] = b[i]; } // Begin swapping tab id's around to an ascending order and work out new selectedTab for (let i = 0; i < newOrder.length; i++) { get('tab' + newOrder[i]).id = "tab" + (i + 1) + ".temp"; if (this.selectedTab === newOrder[i]) { selectedTabWillBe = (i + 1); } } // Now remove the .temp part from all tabs to get new ascending order for (var i = 0; i < newOrder.length; i++) { get('tab' + (i + 1) + '.temp').id = "tab" + (i + 1); } // Set the array values, tab widths and switch tab this.savedPoints = a[0]; this.savedContents = a[1]; this.openFiles = a[2]; this.openFileMDTs = a[3]; this.openFileVersions = a[4]; this.cMInstances = a[5]; // Set tab widths and switch to this tab this.setTabWidths(false); this.switchTab(selectedTabWillBe); }, // Alphabetize tabs alphaTabs: function() { let fileName, fileExt, currentArray, currentArrayFull, alphaArray, nextValue, nextValueFull, nextPos; if (0 < this.openFiles.length) { currentArray = []; currentArrayFull = []; alphaArray = []; // Get filenames, full paths and set classname for sliding for (let i = 0; i < this.openFiles.length; i++) { currentArray.push(this.openFiles[i].slice(this.openFiles[i].lastIndexOf('/') + 1)); currentArrayFull.push(this.openFiles[i]); fileName = this.openFiles[i]; fileExt = fileName.substr(fileName.lastIndexOf(".") + 1); get('tab' + (i + 1)).className = "tab ext-" + fileExt + " tabSlide"; } // Get our next value, which is the next filename alpha lowest value and full path while (0 < currentArray.length) { nextValue = currentArray[0]; nextValueFull = currentArrayFull[0]; nextPos = 0; for (let i = 0; i < currentArray.length; i++) { if (currentArray[i] < nextValue) { nextValue = currentArray[i]; nextValueFull = this.openFiles[this.openFiles.indexOf(currentArrayFull[i])]; nextPos = i; } } // When we've got it, push into alphaArray and splice out of arrays alphaArray.push((this.openFiles.indexOf(nextValueFull) + 1)); currentArray.splice(nextPos, 1); currentArrayFull.splice(nextPos, 1); } // Once done, sort our tabs into new order this.sortTabs(alphaArray); } }, // Focus/unfocus tab by contracting/expanding file manager focusUnfocusTab: function() { // Switch current lock state and change display of file manager ICEcoder.lockUnlockNav(); this.changeFilesW(true === this.lockedNav ? 'expand' : 'contract'); }, // == // UI // == // Return bool of true if an Apple Mac Cmd key isCmdKey: function(key) { // Mac command key handling (224 = Moz, 91/93 = Webkit Left/Right Apple) return -1 < [224, 91, 93].indexOf(key); }, // Return bool of true if either CTRL or Cmd key is down ctrlCmdKeyDown: function(evt) { let key; key = evt.keyCode ?? evt.which ?? evt.charCode; // Return bool of true if either is true, false otherwise return evt.ctrlKey || this.isCmdKey(key); }, // Detect keys/combos plus identify our area and set the vars, perform actions interceptKeys: function(area, evt) { let key, ctrlOrCmd, cM, thisCM; key = evt.keyCode ?? evt.which ?? evt.charCode; // Mac command key handling this.cmdKey = this.isCmdKey(key); // Reset the auto-logout timer this.resetAutoLogoutTimer(); // Set bool based on CTRL or Cmd key being pressed ctrlOrCmd = this.ctrlCmdKeyDown(evt); // F1 (zoom code out non declaration lines) if (112 === key) { evt.preventDefault(); if (true === this.codeZoomedOut) { return; } this.codeZoomedOut = true; cM = this.getcMInstance(); // For every line in the current editor, add code-zoomed-out class if not a function/class declaration line for (let i = 0; i < cM.lineCount(); i++) { let nonDeclareLine = true; for (let j = 0; j < this.functionClassList.length; j++) { if (this.functionClassList[j].line === i) { nonDeclareLine = false; } } if (true === nonDeclareLine) { cM.addLineClass(i, "wrap", "code-zoomed-out"); } } // Refresh is necessary to re-draw lines cM.refresh(); return false; }; // DEL (Delete file) if (46 === key && "files" === area) { this.deleteFiles(); return false; }; // Alt key down? if (evt.altKey) { // Detect alt right let isAltRight = ctrlOrCmd; // Tag wrapper, add line break at end or focus on file manager if ( ("ctrl+alt" === this.tagWrapperCommand && true === isAltRight) // CTRL/Cmd + alt left + key || alt right + key || ("alt-left" === this.tagWrapperCommand && false === isAltRight) // alt left + key ) { if ("content" === area) { switch(key) { // d case 68: this.tagWrapper('div'); break; // s case 83: this.tagWrapper('span'); break; // p case 80: this.tagWrapper('p'); break; // a case 65: this.tagWrapper('a'); break; // 1 case 49: this.tagWrapper('h1'); break; // 2 case 50: this.tagWrapper('h2'); break; // 3 case 51: this.tagWrapper('h3'); break; // Enter case 13: this.addLineBreakAtEnd(); break; // Shift case 16: this.filesFrame.contentWindow.focus(); break; default: return key; } return false; } // Shift and not focused on content area, set focus to last editor pane if (16 === key) { this.focus(this.editorFocusInstance.indexOf('diff') > -1 ? true : false); return false; } return key; // Alt + Enter (Insert Line After) } else if (13 === key) { this.insertLineAfter(); return false; } else { return key; } } else { // Shift + Enter (Insert Line Before) if (13 === key && true === evt.shiftKey) { this.insertLineBefore(); return false; // CTRL/Cmd + F (Find next) // and // CTRL/Cmd + G (Find previous) } else if (-1 < [70, 71].indexOf(key) && true === ctrlOrCmd) { let find = get('find'); let selections = this.getThisCM().getSelections(); if (0 < selections.length){ if (0 < selections[0].length){ find.value = selections[0]; } } find.select(); // This is trick for Chrome - after you have used Ctrl-F once, when // you try using Ctrl + F another time, for some reason Chrome still thinks, // that find has focus and refuses to give it focus second time. get('goToLineNo').focus(); find.focus(); // Trigger the find/replace operation (70 = F (next), 71 = G (prev)) this.findReplace(find.value, true, true, 70 !== key); return false; // CTRL/Cmd+L (Go to line) } else if (76 === key && true === ctrlOrCmd) { let goToLineInput = get('goToLineNo'); goToLineInput.select(); // this is trick for Chrome - after you have used Ctrl-F once, when // you try using Ctrl + F another time, for some reason Chrome still thinks, // that find has focus and refuses to give it focus second time. get('find').focus(); goToLineInput.focus(); return false; // CTRL/Cmd + I (Get info) } else if (73 === key && true === ctrlOrCmd && "content" === area) { this.searchForSelected(); return false; // CTRL/Cmd + backspace (Go to previous tab selected) } else if (8 === key && true === ctrlOrCmd) { if (0 !== this.prevTab) { this.switchTab(this.prevTab); } return false; // CTRL/Cmd + right arrow (Tab to right) } else if (39 === key && true === ctrlOrCmd && "content" !== area) { this.nextTab(); return false; // CTRL/Cmd + left arrow (Tab to left) } else if (37 === key && true === ctrlOrCmd && "content" !== area) { this.previousTab(); return false; // CTRL/Cmd + up arrow (Move line up) } else if (38 === key && true === ctrlOrCmd && "content" === area) { this.moveLines('up'); return false; // CTRL/Cmd + down arrow (Move line down) } else if (40 === key && true === ctrlOrCmd && "content" === area) { this.moveLines('down'); return false; // CTRL/Cmd + numeric plus (New tab) } else if ((107 === key || 187 === key) && true === ctrlOrCmd) { "content" === area ? this.duplicateLines() : this.newTab(false); return false; // CTRL/Cmd + numeric minus (Close tab) } else if ((109 === key || 189 === key) && true === ctrlOrCmd) { "content" === area ? this.removeLines() : this.closeTab(this.selectedTab); return false; // CTRL/Cmd + S (Save), CTRL/Cmd + Shift + S (Save As) } else if (83 === key && true === ctrlOrCmd) { this.saveFile(true === evt.shiftKey, false); return false; // CTRL/Cmd + Enter (Open Webpage) } else if (13 === key && true === ctrlOrCmd && "/[NEW]" !== this.openFiles[this.selectedTab - 1]) { this.resetKeys(evt); window.open(this.openFiles[this.selectedTab - 1]); return false; // Enter (Expand dir/open file) } else if (13 === key && "files" === area) { if (false === ctrlOrCmd) { if (0 === this.selectedFiles.length) { this.overFileFolder('folder', '|'); this.selectFileFolder('init'); } this.fmAction(evt,'enter'); } return false; // Up/down/left/right arrows (traverse files) } else if (-1 < [37, 38, 39, 40].indexOf(key) && "files" === area) { if (false === ctrlOrCmd) { if (0 === this.selectedFiles.length) { this.overFileFolder('folder', '|'); this.selectFileFolder('init'); } this.fmAction(evt, ['left', 'up', 'right', 'down'][key - 37]); } return false; // CTRL/Cmd + O (Open Prompt) } else if (79 === key && true === ctrlOrCmd) { this.openPrompt(); return false; // CTRL/Cmd + space (Add snippet) } else if (32 === key && true === ctrlOrCmd && "content" === area) { this.addSnippet(); return false; // CTRL/Cmd + J (Jump to definition/back again) } else if (74 === key && true === ctrlOrCmd && "content" === area) { this.jumpToDefinition(); return false; // CTRL + ` (Lock/Unlock file manager) } else if (223 === key && true === ctrlOrCmd) { this.lockUnlockNav(); this.changeFilesW(true === this.lockedNav ? 'expand' : 'contract'); return false; // CTRL + . (Fold/unfold current line) } else if (190 === key && true === ctrlOrCmd) { thisCM = this.getThisCM(); thisCM.foldCode(thisCM.getCursor()); return false; // ESC in content area (Comment/Uncomment line) } else if (27 === key && "content" === area) { thisCM = this.getThisCM(); 1 < thisCM.getSelections().length ? thisCM.execCommand("singleSelection") : this.lineCommentToggle(); return false; // ESC not in content area (Cancel all actions) } else if (27 === key && "content" !== area) { this.cancelAllActions(); return false; // Any other key } else { return key; } } }, // Reset the state of keys back to the normal state resetKeys: function(evt) { let key, thisCM; key = evt.keyCode ?? evt.which ?? evt.charCode; if (112 === key && true === this.codeZoomedOut) { thisCM = this.getcMInstance(); // For every line in the current editor, remove code-zoomed-out class if not a function/class declaration line for (let i = 0; i < thisCM.lineCount(); i++) { let nonDeclareLine = true; for (let j = 0; j < this.functionClassList.length; j++) { if (i === this.functionClassList[j].line) { nonDeclareLine = false; } } if (true === nonDeclareLine) { thisCM.removeLineClass(i, "wrap", "code-zoomed-out"); } } // Refresh is necessary to re-draw lines thisCM.refresh(); // Go to line chosen if any let cursor = thisCM.getCursor(); this.goToLine(cursor.line + 1, cursor.ch, false); this.codeZoomedOut = false; } this.cmdKey = false; this.draggingWithKey = false; }, // Handle Enter and Escape keys in modals handleModalKeyUp: function(evt, page) { const key = evt.keyCode ?? evt.which ?? evt.charCode; const target = get('blackMask') ?? parent.get('blackMask'); if ("settings" === page && 13 === key) { get(page + 'IFrame').contentWindow.submitSettings(); } if (27 === key) { this.showHide('hide', target); } }, // Add snippet code completion addSnippet: function() { let thisCM, lineNo, whiteSpace, content; // Get line content after trimming whitespace thisCM = this.getThisCM(); lineNo = thisCM.getCursor().line; whiteSpace = thisCM.getLine(lineNo).length - thisCM.getLine(lineNo).replace(/^\s\s*/, '').length; content = thisCM.getLine(lineNo).slice(whiteSpace); // function snippet if ("function" === content.slice(0, 8)) { this.doSnippet('function', 'function VAR() {\nINDENT\tCURSOR\nINDENT}'); // if snippet } else if ("if" === content.slice(0, 2)) { this.doSnippet('if', 'if (CURSOR) {\nINDENT\t\nINDENT}'); // for snippet } else if ("for" === content.slice(0, 3)) { this.doSnippet('for', 'for (let i = 0; i

    Code editor awesomeness ...in your browser", "message": "View the quick start tutorial? (Well worthwhile!) or skip it.", "button": "view tutorial" }, 1: { "width": 250, "height": 55, "top": 0, "left": 0, "title": "Options and settings", "message": "Here you can perform file and editor content actions, plus also customise ICEcoders' settings, switch to other file manager sources, view help, search for info and more.", "button": "next >" }, 2: { "width": 250, "height": winH - 73, "top": 50, "left": 0, "title": "File manager", "message": "This is the file manager. Click a dir to open/close, double click a file to open it and right click on dirs/files to get relevant options. You can drag and drop too.", "button": "next >" }, 3: { "width": 43, "height": 102, "top": 50, "left": 195, "title": "File manager options and plugins", "message": "Here you can unlock/lock the file manager to collapse/expand it, refresh the file manager plus view and install plugins. (Also, move your mouse to left edge of file manager for quick access to the plugins).", "button": "next >" }, 4: { "width": 250, "height": 35, "top": winH - 30, "left": 0, "title": "Extra tools", "message": "Get access to the terminal, output, database and Git interfaces here, displayed as an overlay to get the largest display, click option again to slide overlay out.", "button": "next >" }, 5: { "width": winW - 250, "height": 42, "top": 0, "left": 250, "title": "Editor tabs", "message": "Your opened tabs will appear here. Icons displayed are to close all, alphabetize tabs and add new tab. You can drag your open tabs left/right to sort them too.", "button": "next >" }, 6: { "width": 440, "height": 28, "top": 42, "left": 250, "title": "Find and replace builder", "message": "This is the find and replace builder. Here you can use the text fields and dropdown menus to build up sentences of what you'd like to do, such as find and replace in editor content, files and filenames.", "button": "next >" }, 7: { "width": 200, "height": 28, "top": 42, "left": winW - 200, "title": "Editor options and bug reporting", "message": "Here you can specify the line to jump to (editor jumps as you type, hit Enter to focus on editor), plus options to view the current tab as a webpage in new browser window and view bugs as you code (once targeted at error logs).", "button": "next >" }, 8: { "width": 580, "height": 500, "top": 70, "left": 250, "title": "System info", "message": "This is general info about your server, paths, browser and more. Worth noting to ensure settings seem correct. Viewable when no tabs showing.", "button": "next >" }, 9: { "width": 120, "height": 30, "top": winH - 30, "left": 250, "title": "Editor version control", "message": "When you have a tab open, on every save, it makes a copy - click the number of backups it indicates, to view differences and options to restore old versions.", "button": "next >" }, 10: { "width": 100, "height": 30, "top": winH - 30, "left": (((winW + 250) / 2) - 50), "title": "Editor pane mode", "message": "Switch between single pane and diff pane modes. The diff pane automatically sets a copy on each save, so you can undo/redo to cycle through those. The gutters indicate additions, changes and deletions on each line.", "button": "next >" }, 11: { "width": 100, "height": 30, "top": winH, "left": (((winW + 250) / 2) - 50), "title": "Let's get started!", "message": "This really only scratches the surface of what ICEcoder can do, so have a look around and try things out. The plugins also supercharge ICEcoder with amazing powers, a great place to start, or you can just get started without plugins.", "button": "plugins >" }, }; // Make both the info black mask and message display get("infoBlackMask").style.display = "block"; get("infoMessageContainer").style.display = "block"; // No step specified means starting from beginning if (false === step) { // Set margin-top to be above screen get("infoMessageContainer").style.marginTop = -300 + "px"; // After 100ms show border and message text (still above screen) setTimeout(function() { get("infoBlackMask").style.border = "solid 10000px rgba(0, 0, 0, 0.8)"; get("infoMessageContainer").style.opacity = "1"; }, 100); // After requested delay, slide in message but account for logo setTimeout(function() { get("infoMessageContainer").style.marginTop = (winH / 2) + 70 + "px"; }, delay); // Set message text and return to go no further ICEcoder.viewTutorial(0); return; } if (8 === step) { if (cM) { cM.getWrapperElement().style.visibility = "hidden"; cMDiff.getWrapperElement().style.visibility = "hidden"; } } if (9 === step) { if (cM) { cM.getWrapperElement().style.visibility = ""; cMDiff.getWrapperElement().style.visibility = ""; } if ("" === get("versionsDisplay").innerText) { get("versionsDisplay").innerText = "12345 backups"; } steps[9].width = get("versionsDisplay").innerText.length * 9; } if (10 === step) { if ("12345 backups" === get("versionsDisplay").innerText) { get("versionsDisplay").innerText = ""; } } // If we're going beyond the last step, we're finishing if (11 < step) { // Reset styles ready for next time get("infoBlackMask").style.border = "solid 10000px rgba(0, 0, 0, 0)"; get("infoMessageContainer").style.opacity = "0"; setTimeout(function() { get("infoBlackMask").style.display = "none"; get("infoMessageContainer").style.display = "none"; }, 500); // Mark tutorial as done in users settings and return xhr = this.xhrObj(); xhr.open("POST", this.iceLoc + "/lib/settings.php?action=turnOffTutorialOnLogin&csrf=" + this.csrf, true); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.send(); } if (12 === step) { setTimeout(function() { ICEcoder.pluginsManager(); }, 500); return; } if (99 === step) { return; } // Steps 1 onwards have no logo, normal margin-top needed if (1 <= step) { get("infoMessageContainer").style.marginTop = (winH / 2) + "px"; } // We have something do display, so set info black mask to highlight item get("infoBlackMask").style.height = steps[step].height; get("infoBlackMask").style.width = steps[step].width; get("infoBlackMask").style.top = -10000 + steps[step].top + "px"; get("infoBlackMask").style.left = -10000 + steps[step].left + "px"; // Set info title, message and button get("infoMessage").innerHTML = '
    ' + steps[step].title + '
    ' + steps[step].message + '

    ' + steps[step].button + '
    '; }, };