Files
ICEcoder/assets/js/icecoder.js

5498 lines
247 KiB
JavaScript

// 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 +=
'<div class="link" onclick="ICEcoder.toolShowHideToggle(\'git\'); ICEcoder.openFile(\'/' +
ICEcoder.indexData.gitDiff.paths[i] +
"')\">" +
ICEcoder.indexData.gitDiff.paths[i] +
"</div>" +
"\n";
}
get("git").innerHTML = gitDiffList + "<br><br>";
},
// 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(/<ICEcoder:\/:textarea>/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("<div>\n" + thisCM.getSelection() + "\n</div>", "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)) === "</" + tagEnd + ">") {
// 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() + "</" + tagEnd + ">", "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) + "<br>", {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("<script\>") > caretChunk.lastIndexOf("<\/script>") && "Unknown" === caretLocType) {caretLocType = "JavaScript";}
else if (caretChunk.lastIndexOf("<style") > 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('<b>' + t['Creating Folder'] + '</b> ' + 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('<b>' + t['Opening File'] + '</b> ' + 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('<b>' + t['Getting'] + '</b> ' + 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('<div style="background: #b00; padding: 1px 3px; border-radius: 3px; font-family: monospace;">Syntax error in ' + this.openFiles[this.selectedTab - 1].replace(iceRoot, "") + '</div>\n' + err.message.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
}
}
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('<b>' + t['Saving'] + '</b> ' + 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('<b>' + t['Renaming to'] + '</b> ' + 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('<b>' + t['Moving to'] + '</b> ' + 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('<b>' + t['Deleting File'] + '</b> ' + 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('<b>' + t['Pasting File'] + '</b> ' + 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 = '<a nohref title="'+location.replace(/\/$/, "")+"/"+file+'" onMouseOver="parentNode.draggable=true;parent.ICEcoder.overFileFolder(\''+actionElemType+'\',this.childNodes[1].id)" onMouseOut="parentNode.draggable=false;parent.ICEcoder.overFileFolder(\''+actionElemType+'\',\'\')" '+
("folder" === actionElemType
? 'ondragover="parent.ICEcoder.overFileFolder(\'folder\', this.childNodes[1].id); parent.ICEcoder.highlightFileFolder(this.childNodes[1].id, true); if(parentNode.nextSibling && parentNode.nextSibling.tagName != \'UL\' && parent.ICEcoder.thisFileFolderLink !== this.childNodes[1].id) {parent.ICEcoder.openCloseDir(this,true);}"'
: 'ondragover="parent.ICEcoder.overFileFolder(\'file\', this.childNodes[1].id); parent.ICEcoder.highlightFileFolder(this.parentNode.parentNode.previousSibling.childNodes[0].childNodes[1].id, true);"'
) +
("folder" === actionElemType
? 'ondragleave="parent.ICEcoder.highlightFileFolder(this.childNodes[1].id, false);"'
: 'ondragleave="parent.ICEcoder.highlightFileFolder(this.parentNode.parentNode.previousSibling.childNodes[0].childNodes[1].id, false);"'
) +
' onClick="if(!event.ctrlKey && !parent.ICEcoder.cmdKey) {' +
("folder" === actionElemType ? 'parent.ICEcoder.openCloseDir(this,' + ("folder" === actionElemType ? 'true' : 'false') + ');' : '') +
' if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) {parent.ICEcoder.openFile()}}" style="position: relative; left:-22px">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span id="'+location.replace(/\/$/, "").replace(/\//g,"|")+"|"+file+'">'+file+'</span> <span style="'+permColors+'; font-size: 8px" id="'+location.replace(/\/$/, "").replace(/\//g,"|")+"|"+file+'_perms">'+perms+'</span></a>';
// 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, '\&#39;');
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 = '<iframe src="' +
this.iceLoc + '/lib/multiple-results.php?find=' + find
+ replaceQS + targetQS + filesQS +
'&csrf=' + this.csrf +
'" id="multipleResultsIFrame" style="width: 700px; height: 500px"></iframe>';
// 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 += '<div class="rBlock" style="height:' + avgBlockH + 'px; top: ' + rBlockTop + 'px" id="rBlock' + i +'"></div>';
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('<b>' + t['Replacing text in'] + '</b> ' + 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(/\&lt;b\&gt;/g, "<b>").replace(/\&lt;\/b\&gt;/g, "</b>");
}
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 = '<div id="picker" class="picker" onmouseover="ICEcoder.overPopup=true" onmouseout="ICEcoder.overPopup=false"></div><br><br>'+
'<input type="text" id="color" name="color" value="#000" class="colorValue">'+
'<input type="button" onClick="ICEcoder.insertColorValue(get(\'color\').value)" value="insert &gt;" class="insertColorValue"><br><br>'+
'<input type="text" id="colorRGB" name="colorRGB" value="rgb(0,0,0)" class="colorValue">'+
'<input type="button" onClick="ICEcoder.insertColorValue(get(\'colorRGB\').value)" value="insert &gt;" class="insertColorValue">';
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('<b style="color: #d00">' + t['Cancelled tasks'] + '</b>');
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 = "<li class=\"pft-file ext-" +
previousFiles[i].substring(previousFiles[i].lastIndexOf(".") + 1) +
"\" style=\"margin-left: -21px\"><a style=\"cursor:pointer\" onclick=\"parent.ICEcoder.openFile('" +
previousFiles[i].replace(/\|/g,"/") + "')\">" +
previousFiles[i].replace(/\|/g,"/") + "</a></li>\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] === '<div style="display: inline-block; margin-left: -39px; margin-top: -4px">[none]</div><br><br>' ||
// 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 =
'<iframe src="' +
this.iceLoc +
'/lib/settings-screen.php' +
tabExtra +
'" id="settingsIFrame" style="width: 970px; height: 610px"></iframe>';
}
this.showHide(hide ? 'hide' : 'show', get('blackMask'));
},
// Show the help screen
helpScreen: function() {
get('mediaContainer').innerHTML =
'<iframe src="' +
this.iceLoc +
'/lib/help.php" id="helpIFrame" style="width: 840px; height: 495px"></iframe>';
this.showHide('show', get('blackMask'));
},
// Show the backup versions screen
versionsScreen: function(file) {
get('mediaContainer').innerHTML =
'<iframe src="' +
this.iceLoc +
'/lib/backup-versions.php?file=' +
file +
'&csrf=' +
this.csrf +
'" id="versionsIFrame" style="width: 970px; height: 640px"></iframe>';
this.showHide('show', get('blackMask'));
},
// Show the ICEcoder manual, loaded remotely
showManual: function(version, section) {
let sectionExtra;
sectionExtra = section ? "#" + section : "";
get('mediaContainer').innerHTML =
'<iframe src="https://icecoder.net/manual?version=' +
version +
sectionExtra +
'" id="manualIFrame" style="width: 800px; height: 470px"></iframe>';
this.showHide('show', get('blackMask'));
},
// Show the properties screen
propertiesScreen: function(fileName) {
get('mediaContainer').innerHTML = '<iframe src="' +
this.iceLoc +
'/lib/properties.php?fileName=' +
fileName.replace(/\//g,"|") +
'&csrf=' +
this.csrf +
'" id="propertiesIFrame" style="width: 660px; height: 330px"></iframe>';
this.showHide('show', get('blackMask'));
},
// Show the auto-logout warning screen
autoLogoutWarningScreen: function() {
get('mediaContainer').innerHTML = '<iframe src="' +
this.iceLoc +
'/lib/auto-logout-warning.php" id="autoLogoutIFrame" style="width: 400px; height: 160px"></iframe>';
this.showHide('show', get('blackMask'));
},
// Show the bug report screen
bugReportScreen: function() {
get('mediaContainer').innerHTML = '<iframe src="' +
this.iceLoc +
'/lib/bug-report.php" id="bugReportIFrame" style="width: 970px; height: 610px"></iframe>';
this.showHide('show', get('blackMask'));
},
// Show the plugins manager
pluginsManager: function() {
get('mediaContainer').innerHTML = '<iframe src="' +
this.iceLoc +
'/lib/plugins-manager.php" id="pluginsManagerIFrame" style="width: 800px; height: 450px"></iframe>';
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('<b>chMod ' + perms + ' on </b> ' + 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 ("<b>Output</b><br>via ICEcoder.output(message);<br><br>" === output) {
output = "";
}
this.output.innerHTML = msg + "<br><br>" + output + ("" !== output ? "<br><br>" : "");
},
// 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 = '<h1 style="color: #fff; cursor: default">Thanks for updating to v' + this.versionNo + '!</h1>'
+ '<h2 style="color: #888; cursor: default">Click anywhere to continue using this...</h2>';
},
// 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
},
// 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 =
'<!DOCTYPE html><head><title>' +
this.openFiles[this.selectedTab - 1] +
'</title></head><body><pre style="white-space: pre-wrap">' +
this.xssClean(thisCM.getValue()) +
'</pre></body></html>';
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 = '<a nohref onClick="ICEcoder.closeTab(parseInt(this.parentNode.id.slice(3), 10))"><img src="' + this.assetsLoc + '/images/nav-close.gif" class="closeTab" onMouseOver="prevBG = this.style.backgroundColor; this.style.backgroundColor = \'#333\'; parent.ICEcoder.overCloseLink = true" onMouseOut="this.style.backgroundColor = prevBG; parent.ICEcoder.overCloseLink = false"></a>';
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 + "<span style=\"display: inline-block; width: 19px\"></span>" + 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 = '<a nohref onClick="ICEcoder.closeTab(parseInt(this.parentNode.id.slice(3), 10))"><img src="' + this.assetsLoc + '/images/nav-close.gif" class="closeTab" onMouseOver="prevBG = this.style.backgroundColor; this.style.backgroundColor = \'#333\'; parent.ICEcoder.overCloseLink = true" onMouseOut="this.style.backgroundColor = prevBG; parent.ICEcoder.overCloseLink = false"></a>';
fileName = this.openFiles[tabNum - 1];
fileExt = fileName.substr(fileName.lastIndexOf(".") + 1);
get('tab' + tabNum).innerHTML = closeTabLink + "<span style=\"display: inline-block; width: 19px\"></span>" + 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 <CURSOR; i++) {\nINDENT\t\nINDENT}');
}
},
// Action a snippet
doSnippet: function(tgtString, replaceString) {
let thisCM, lineNo, lineContents, remainder, strPos, replacedLine, whiteSpace, curPos, sPos, lineNoCount;
// Get line contents
thisCM = this.getThisCM();
lineNo = thisCM.getCursor().line;
lineContents = thisCM.getLine(lineNo);
// Find our target string
if (-1 < lineContents.indexOf(tgtString)) {
// Get text on the line from our target to the end
remainder = thisCM.getLine(lineNo);
strPos = remainder.indexOf(tgtString);
remainder = remainder.slice(remainder.indexOf(tgtString) + tgtString.length + 1);
// Replace the function name if any
replaceString = replaceString.replace(/VAR/g, remainder);
// Get replaced string from start to our strPos
replacedLine = thisCM.getLine(lineNo).slice(0, strPos);
// Trim whitespace from start
whiteSpace = thisCM.getLine(lineNo).length - thisCM.getLine(lineNo).replace(/^\s\s*/, '').length;
whiteSpace = thisCM.getLine(lineNo).slice(0, whiteSpace);
// Replace indent with whatever whitespace we have
replaceString = replaceString.replace(/INDENT/g, whiteSpace);
replacedLine += replaceString;
// Get cursor position
curPos = replacedLine.indexOf("CURSOR");
sPos = 0;
lineNoCount = lineNo;
for (let i = 0; i < replacedLine.length; i++) {
if (replacedLine.indexOf("\n", sPos) < replacedLine.indexOf("CURSOR")) {
sPos = replacedLine.indexOf("\n", sPos) + 1;
lineNoCount = lineNoCount + 1;
}
}
// Clear the cursor string and set the cursor there
thisCM.replaceRange(replacedLine.replace("CURSOR", ""), {line: lineNo, ch: 0}, {line: lineNo, ch: 1000000}, "+input");
thisCM.setCursor(lineNoCount, curPos);
// Finally, focus on the editor
this.focus(-1 < this.editorFocusInstance.indexOf('diff') ? true : false);
}
},
viewTutorial: function(step, delay) {
let winW, winH, cM, cMDiff;
winW = window.innerWidth;
winH = window.innerHeight;
cM = this.getcMInstance();
cMDiff = this.getcMdiffInstance();
let steps = {
0: {
"width": 250,
"height": 55,
"top": -55,
"left": 0,
"title": "<img src=\"" + this.assetsLoc + "/images/icecoder.png\" style=\"position: absolute; margin: -105px 0 0 -55px\"><br><br>Code editor awesomeness ...in your browser",
"message": "View the quick start tutorial? (Well worthwhile!) or <a onclick=\"ICEcoder.viewTutorial(99, 0)\" style=\"font-size: 14px; text-decoration: underline; cursor: pointer\">skip it</a>.",
"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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 &gt;"
},
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 <a onclick=\"ICEcoder.viewTutorial(99, 0)\" style=\"font-size: 14px; text-decoration: underline; cursor: pointer\">get started without plugins</a>.",
"button": "plugins &gt;"
},
};
// 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 = '<div class="title">' + steps[step].title + '</div>' + steps[step].message + '<br><br><div class="button" onclick="ICEcoder.viewTutorial(' + (step + 1) + ')">' + steps[step].button + '</div>';
},
};