Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No Page Content Showing #252

Closed
maxexcloo opened this issue Feb 9, 2018 · 11 comments
Closed

No Page Content Showing #252

maxexcloo opened this issue Feb 9, 2018 · 11 comments

Comments

@maxexcloo
Copy link

I'm working on a rails project and I'm stumped, the web console shows up when I attempt to manually include it using <% console %> but the page content disappears, I've been googling around and haven't had any luck.

This is what the result looks like: https://i.imgur.com/WSVdXuf.png

My Gemfile: https://gist.github.com/anonymous/d2dc9e7c5438acfcb4ff2746b71a711b

@gsamokovarov
Copy link
Collaborator

Can you show the contents of the View Source Page option from your browser? Since the web-console augments your page with it'\s own HTML/CSS/JS content, if you have unclosed tags before, that may interfere with your content.

@maxexcloo
Copy link
Author

maxexcloo commented Feb 9, 2018 via email

@maxexcloo
Copy link
Author

maxexcloo commented Feb 12, 2018

#<Rack::BodyProxy:0x00007ff6db8c53c8><div id="console"
  data-mount-point='/__web_console'
  data-session-id='873aa542fa8ebb1c0ff250b120ab8490'
  data-prompt-label='>> '>
</div>


<script type="text/javascript" data-template="console">
(function() {
  /**
 * Constructor for command storage.
 * It uses localStorage if available. Otherwise fallback to normal JS array.
 */
function CommandStorage() {
  this.previousCommands = [];
  var previousCommandOffset = 0;
  var hasLocalStorage = typeof window.localStorage !== 'undefined';
  var STORAGE_KEY = "web_console_previous_commands";
  var MAX_STORAGE = 100;

  if (hasLocalStorage) {
    this.previousCommands = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
    previousCommandOffset = this.previousCommands.length;
  }

  this.addCommand = function(command) {
    previousCommandOffset = this.previousCommands.push(command);

    if (previousCommandOffset > MAX_STORAGE) {
      this.previousCommands.splice(0, 1);
      previousCommandOffset = MAX_STORAGE;
    }

    if (hasLocalStorage) {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(this.previousCommands));
    }
  };

  this.navigate = function(offset) {
    previousCommandOffset += offset;

    if (previousCommandOffset < 0) {
      previousCommandOffset = -1;
      return null;
    }

    if (previousCommandOffset >= this.previousCommands.length) {
      previousCommandOffset = this.previousCommands.length;
      return null;
    }

    return this.previousCommands[previousCommandOffset];
  }
}

function Autocomplete(_words, prefix) {
  this.words = prepareWords(_words);
  this.current = -1;
  this.left = 0; // [left, right)
  this.right = this.words.length;
  this.confirmed = false;

  function createSpan(label, className) {
    var el = document.createElement('span');
    addClass(el, className);
    el.innerText = label;
    return el;
  }

  function prepareWords(words) {
    // convert into an object with priority and element
    var res = new Array(words.length);
    for (var i = 0, ind = 0; i < words.length; ++i) {
      res[i] = new Array(words[i].length);
      for (var j = 0; j < words[i].length; ++j) {
        res[i][j] = {
          word: words[i][j],
          priority: i,
          element: createSpan(words[i][j], 'trimmed keyword')
        };
      }
    }
    // flatten and sort by alphabetical order to refine incrementally
    res = flatten(res);
    res.sort(function(a, b) { return a.word == b.word ? 0 : (a.word < b.word ? -1 : 1); });
    for (var i = 0; i < res.length; ++i) res[i].element.dataset.index = i;
    return res;
  }

  this.view = document.createElement('pre');
  addClass(this.view, 'auto-complete console-message');
  this.view.appendChild(this.prefix = createSpan('...', 'trimmed keyword'));
  this.view.appendChild(this.stage = document.createElement('span'));
  this.elements = this.stage.children;
  this.view.appendChild(this.suffix = createSpan('...', 'trimmed keyword'));

  this.refine(prefix || '');
}

Autocomplete.prototype.getSelectedWord = function() {
  return this.lastSelected && this.lastSelected.innerText;
};

Autocomplete.prototype.onFinished = function(callback) {
  this.onFinishedCallback = callback;
  if (this.confirmed) callback(this.confirmed);
};

Autocomplete.prototype.onKeyDown = function(ev) {
  var self = this;
  if (!this.elements.length) return;

  function move(nextCurrent) {
    if (self.lastSelected) removeClass(self.lastSelected, 'selected');
    addClass(self.lastSelected = self.elements[nextCurrent], 'selected');
    self.trim(self.current, true);
    self.trim(nextCurrent, false);
    self.current = nextCurrent;
  }

  switch (ev.keyCode) {
    case 69:
      if (ev.ctrlKey) {
        move(this.current + 1 >= this.elements.length ? 0 : this.current + 1);
        return true;
      }
      return false;
    case 9: // Tab
      if (ev.shiftKey) { // move back
        move(this.current - 1 < 0 ? this.elements.length - 1 : this.current - 1);
      } else { // move next
        move(this.current + 1 >= this.elements.length ? 0 : this.current + 1);
      }
      return true;
    case 13: // Enter
      this.finish();
      return true;
    case 27: // Esc
      this.cancel();
      return true;
    case 37: case 38: case 39: case 40: // disable using arrow keys on completing
      return true;
  }

  return false;
};

Autocomplete.prototype.trim = function(from, needToTrim) {
  var self = this;
  var num = 5;

  if (this.elements.length > num) {
    (0 < from ? removeClass : addClass)(this.prefix, 'trimmed');
    (from + num < this.elements.length ? removeClass : addClass)(this.suffix, 'trimmed');
  } else {
    addClass(this.prefix, 'trimmed');
    addClass(this.suffix, 'trimmed');
  }

  function iterate(x) {
    for (var i = 0; i < num; ++i, ++x) if (0 <= x && x < self.elements.length) {
      toggleClass(self.elements[x], 'trimmed');
    }
  }

  var toggleClass = needToTrim ? addClass : removeClass;
  if (from < 0) {
    iterate(0);
  } else if (from + num - 1 >= this.elements.length) {
    iterate(this.elements.length - num);
  } else {
    iterate(from);
  }
};

Autocomplete.prototype.refine = function(prefix) {
  if (this.confirmed) return;
  var inc = !this.prev || (prefix.length >= this.prev.length);
  this.prev = prefix;
  var self = this;

  function remove(parent, child) {
    if (parent == child.parentNode) parent.removeChild(child);
  }

  function toggle(el) {
    return inc ? remove(self.stage, el) : self.stage.appendChild(el);
  }

  function startsWith(str, prefix) {
    return !prefix || str.substr(0, prefix.length) === prefix;
  }

  function moveRight(l, r) {
    while (l < r && inc !== startsWith(self.words[l].word, prefix)) toggle(self.words[l++].element);
    return l;
  }

  function moveLeft(l, r) {
    while (l < r - 1 && inc !== startsWith(self.words[r-1].word, prefix)) toggle(self.words[--r].element);
    return r;
  }

  self.trim(self.current, true); // reset trimming

  // Refine the range of words having same prefix
  if (inc) {
    self.left = moveRight(self.left, self.right);
    self.right = moveLeft(self.left, self.right);
  } else {
    self.left = moveLeft(-1, self.left);
    self.right = moveRight(self.right, self.words.length);
  }

  // Render elements with sorting by scope groups
  var words = this.words.slice(this.left, this.right);
  words.sort(function(a, b) { return a.priority == b.priority ? (a.word < b.word ? -1 : 1) : (a.priority < b.priority ? -1 : 1); });
  removeAllChildren(this.elements);
  for (var i = 0; i < words.length; ++i) {
    this.stage.appendChild(words[i].element);
  }

  // Keep a previous selected element if the refined range includes the element
  if (this.lastSelected && this.left <= this.lastSelected.dataset.index && this.lastSelected.dataset.index < this.right) {
    this.current = Array.prototype.indexOf.call(this.elements, this.lastSelected);
    this.trim(this.current, false);
  } else {
    if (this.lastSelected) removeClass(this.lastSelected, 'selected');
    this.lastSelected = null;
    this.current = -1;
    this.trim(0, false);
  }

  if (self.left + 1 == self.right) {
    self.current = 0;
    self.finish();
  } else if (self.left == self.right) {
    self.cancel();
  }
};

Autocomplete.prototype.finish = function() {
  if (0 <= this.current && this.current < this.elements.length) {
    this.confirmed = this.elements[this.current].innerText;
    if (this.onFinishedCallback) this.onFinishedCallback(this.confirmed);
    this.removeView();
  } else {
    this.cancel();
  }
};

Autocomplete.prototype.cancel = function() {
  if (this.onFinishedCallback) this.onFinishedCallback();
  this.removeView();
};

Autocomplete.prototype.removeView = function() {
  if (this.view.parentNode) this.view.parentNode.removeChild(this.view);
  removeAllChildren(this.view);
}

// HTML strings for dynamic elements.
var consoleInnerHtml = "<div class=\'resizer layer\'><\/div>\n<div class=\'console-outer layer\'>\n  <div class=\'console-actions\'>\n    <div class=\'close-button button\' title=\'close\'>x<\/div>\n  <\/div>\n  <div class=\'console-inner\'><\/div>\n<\/div>\n<input class=\'clipboard\' type=\'text\'>\n"
;
var promptBoxHtml = "<span class=\'console-prompt-label\'><\/span>\n<pre class=\'console-prompt-display\'><\/pre>\n"
;
// CSS
var consoleStyleCss = ".console .pos-absolute { position: absolute; }\n.console .pos-fixed { position: fixed; }\n.console .pos-right { right: 0; }\n.console .border-box { box-sizing: border-box; }\n.console .layer { width: 100%; height: 100%; }\n.console .layer.console-outer { z-index: 1; }\n.console .layer.resizer { z-index: 2; }\n.console { position: fixed; left: 0; bottom: 0; width: 100%; height: 148px; padding: 0; margin: 0; background: none repeat scroll 0% 0% #333; z-index: 9999; }\n.console .console-outer { overflow: auto; padding-top: 4px; }\n.console .console-inner { font-family: monospace; font-size: 11px; width: 100%; height: 100%; overflow: none; background: #333; }\n.console .console-prompt-box { color: #FFF; }\n.console .console-message { color: #1AD027; margin: 0; border: 0; white-space: pre-wrap; background-color: #333; padding: 0; }\n.console .console-message.error-message { color: #fc9; }\n.console .console-message.auto-complete { word-break: break-all; }\n.console .console-message.auto-complete .keyword { margin-right: 11px; }\n.console .console-message.auto-complete .keyword.selected { background: #FFF; color: #000; }\n.console .console-message.auto-complete .hidden { display: none; }\n.console .console-message.auto-complete .trimmed { display: none; }\n.console .console-hint { color: #096; }\n.console .console-focus .console-cursor { background: #FEFEFE; color: #333; font-weight: bold; }\n.console .resizer { background: #333; width: 100%; height: 4px; cursor: ns-resize; }\n.console .console-actions { padding-right: 3px; }\n.console .console-actions .button { float: left; }\n.console .button { cursor: pointer; border-radius: 1px; font-family: monospace; font-size: 13px; width: 14px; height: 14px; line-height: 14px; text-align: center; color: #ccc; }\n.console .button:hover { background: #666; color: #fff; }\n.console .button.close-button:hover { background: #966; }\n.console .clipboard { height: 0px; padding: 0px; margin: 0px; width: 0px; margin-left: -1000px; }\n.console .console-prompt-label { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; }\n.console .console-prompt-display { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; }\n.console.full-screen { height: 100%; }\n.console.full-screen .console-outer { padding-top: 3px; }\n.console.full-screen .resizer { display: none; }\n.console.full-screen .close-button { display: none; }\n"
;
// Insert a style element with the unique ID
var styleElementId = 'sr02459pvbvrmhco';

// REPLConsole Constructor
function REPLConsole(config) {
  function getConfig(key, defaultValue) {
    return config && config[key] || defaultValue;
  }

  this.commandStorage = new CommandStorage();
  this.prompt = getConfig('promptLabel', ' >>');
  this.mountPoint = getConfig('mountPoint');
  this.sessionId = getConfig('sessionId');
  this.autocomplete = false;
}

REPLConsole.prototype.getSessionUrl = function(path) {
  var parts = [ this.mountPoint, 'repl_sessions', this.sessionId ];
  if (path) {
    parts.push(path);
  }
  // Join and remove duplicate slashes.
  return parts.join('/').replace(/([^:]\/)\/+/g, '$1');
};

REPLConsole.prototype.contextRequest = function(keyword, callback) {
  putRequest(this.getSessionUrl(), 'context=' + getContext(keyword), function(xhr) {
    if (xhr.status == 200) {
      callback(null, JSON.parse(xhr.responseText));
    } else {
      callback(xhr.statusText);
    }
  });
};

REPLConsole.prototype.commandHandle = function(line, callback) {
  var self = this;
  var params = 'input=' + encodeURIComponent(line);
  callback = callback || function() {};

  function isSuccess(status) {
    return status >= 200 && status < 300 || status === 304;
  }

  function parseJSON(text) {
    try {
      return JSON.parse(text);
    } catch (e) {
      return null;
    }
  }

  function getErrorText(xhr) {
    if (!xhr.status) {
      return "Connection Refused";
    } else {
      return xhr.status + ' ' + xhr.statusText;
    }
  }

  putRequest(self.getSessionUrl(), params, function(xhr) {
    var response = parseJSON(xhr.responseText);
    var result   = isSuccess(xhr.status);
    if (result) {
      self.writeOutput(response.output);
    } else {
      if (response && response.output) {
        self.writeError(response.output);
      } else {
        self.writeError(getErrorText(xhr));
      }
    }
    callback(result, response);
  });
};

REPLConsole.prototype.uninstall = function() {
  this.container.parentNode.removeChild(this.container);
};

REPLConsole.prototype.install = function(container) {
  var _this = this;

  document.onkeydown = function(ev) {
    if (_this.focused) { _this.onKeyDown(ev); }
  };

  document.onkeypress = function(ev) {
    if (_this.focused) { _this.onKeyPress(ev); }
  };

  document.addEventListener('mousedown', function(ev) {
    var el = ev.target || ev.srcElement;

    if (el) {
      do {
        if (el === container) {
          _this.focus();
          return;
        }
      } while (el = el.parentNode);

      _this.blur();
    }
  });

  // Render the console.
  container.innerHTML = consoleInnerHtml;

  var consoleOuter = findChild(container, 'console-outer');
  var consoleActions = findChild(consoleOuter, 'console-actions');

  addClass(container, 'console');
  addClass(container.getElementsByClassName('layer'), 'pos-absolute border-box');
  addClass(container.getElementsByClassName('button'), 'border-box');
  addClass(consoleActions, 'pos-fixed pos-right');

  // Make the console resizable.
  function resizeContainer(ev) {
    var startY              = ev.clientY;
    var startHeight         = parseInt(document.defaultView.getComputedStyle(container).height, 10);
    var scrollTopStart      = consoleOuter.scrollTop;
    var clientHeightStart   = consoleOuter.clientHeight;

    var doDrag = function(e) {
      container.style.height = (startHeight + startY - e.clientY) + 'px';
      consoleOuter.scrollTop = scrollTopStart + (clientHeightStart - consoleOuter.clientHeight);
      shiftConsoleActions();
    };

    var stopDrag = function(e) {
      document.documentElement.removeEventListener('mousemove', doDrag, false);
      document.documentElement.removeEventListener('mouseup', stopDrag, false);
    };

    document.documentElement.addEventListener('mousemove', doDrag, false);
    document.documentElement.addEventListener('mouseup', stopDrag, false);
  }

  function closeContainer(ev) {
    container.parentNode.removeChild(container);
  }

  var shifted = false;
  function shiftConsoleActions() {
    if (consoleOuter.scrollHeight > consoleOuter.clientHeight) {
      var widthDiff = document.documentElement.clientWidth - consoleOuter.clientWidth;
      if (shifted || ! widthDiff) return;
      shifted = true;
      consoleActions.style.marginRight = widthDiff + 'px';
    } else if (shifted) {
      shifted = false;
      consoleActions.style.marginRight = '0px';
    }
  }

  // Initialize
  this.container = container;
  this.outer = consoleOuter;
  this.inner = findChild(this.outer, 'console-inner');
  this.clipboard = findChild(container, 'clipboard');
  this.suggestWait = 1500;
  this.newPromptBox();
  this.insertCss();

  findChild(container, 'resizer').addEventListener('mousedown', resizeContainer);
  findChild(consoleActions, 'close-button').addEventListener('click', closeContainer);
  consoleOuter.addEventListener('DOMNodeInserted', shiftConsoleActions);

  REPLConsole.currentSession = this;
};

// Add CSS styles dynamically. This probably doesnt work for IE <8.
REPLConsole.prototype.insertCss = function() {
  if (document.getElementById(styleElementId)) {
    return; // already inserted
  }
  var style = document.createElement('style');
  style.type = 'text/css';
  style.innerHTML = consoleStyleCss;
  style.id = styleElementId;
  document.getElementsByTagName('head')[0].appendChild(style);
};

REPLConsole.prototype.focus = function() {
  if (! this.focused) {
    this.focused = true;
    if (! hasClass(this.inner, "console-focus")) {
      addClass(this.inner, "console-focus");
    }
    this.scrollToBottom();
  }
};

REPLConsole.prototype.blur = function() {
  this.focused = false;
  removeClass(this.inner, "console-focus");
};

/**
 * Add a new empty prompt box to the console.
 */
REPLConsole.prototype.newPromptBox = function() {
  // Remove the caret from previous prompt display if any.
  if (this.promptDisplay) {
    this.removeCaretFromPrompt();
  }

  var promptBox = document.createElement('div');
  promptBox.className = "console-prompt-box";
  promptBox.innerHTML = promptBoxHtml;
  this.promptLabel = promptBox.getElementsByClassName('console-prompt-label')[0];
  this.promptDisplay = promptBox.getElementsByClassName('console-prompt-display')[0];
  // Render the prompt box
  this.setInput("");
  this.promptLabel.innerHTML = this.prompt;
  this.inner.appendChild(promptBox);
  this.scrollToBottom();
};

/**
 * Remove the caret from the prompt box,
 * mainly before adding a new prompt box.
 * For simplicity, just re-render the prompt box
 * with caret position -1.
 */
REPLConsole.prototype.removeCaretFromPrompt = function() {
  this.setInput(this._input, -1);
};

REPLConsole.prototype.getSuggestion = function(keyword) {
  var self = this;

  function show(found) {
    if (!found) return;
    var hint = self.promptDisplay.childNodes[1];
    hint.className = 'console-hint';
    hint.dataset.keyword = found;
    hint.innerText = found.substr(self.suggestKeyword.length);
    // clear hinting information after timeout in a few time 
    if (self.suggestTimeout) clearTimeout(self.suggestTimeout);
    self.suggestTimeout = setTimeout(function() { self.renderInput() }, self.suggestWait);
  }

  function find(context) {
    var k = self.suggestKeyword;
    for (var i = 0; i < context.length; ++i) if (context[i].substr(0, k.length) === k) {
      if (context[i] === k) return;
      return context[i];
    }
  }

  function request(keyword, callback) {
    self.contextRequest(keyword, function(err, res) {
      if (err) throw new Error(err);
      var c = flatten(res['context']);
      c.sort();
      callback(c);
    });
  }

  self.suggestKeyword = keyword;
  var input = getContext(keyword);
  if (keyword.length - input.length < 3) return;

  if (self.suggestInput !== input) {
    self.suggestInput = input;
    request(keyword, function(c) {
      show(find(self.suggestContext = c));
    });
  } else if (self.suggestContext) {
    show(find(self.suggestContext));
  }
};

REPLConsole.prototype.getHintKeyword = function() {
  var hint = this.promptDisplay.childNodes[1];
  return hint.className === 'console-hint' && hint.dataset.keyword;
};

REPLConsole.prototype.setInput = function(input, caretPos) {
  if (input == null) return; // keep value if input is undefined
  this._caretPos = caretPos === undefined ? input.length : caretPos;
  this._input = input;
  if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord());
  this.renderInput();
  if (!this.autocomplete && input.length == this._caretPos) this.getSuggestion(this.getCurrentWord());
};

/**
 * Add some text to the existing input.
 */
REPLConsole.prototype.addToInput = function(val, caretPos) {
  caretPos = caretPos || this._caretPos;
  var before = this._input.substring(0, caretPos);
  var after = this._input.substring(caretPos, this._input.length);
  var newInput =  before + val + after;
  this.setInput(newInput, caretPos + val.length);
};

/**
 * Render the input prompt. This is called whenever
 * the user input changes, sometimes not very efficient.
 */
REPLConsole.prototype.renderInput = function() {
  // Clear the current input.
  removeAllChildren(this.promptDisplay);

  var before, current, after;
  var center = document.createElement('span');

  if (this._caretPos < 0) {
    before = this._input;
    current = after = "";
  } else if (this._caretPos === this._input.length) {
    before = this._input;
    current = "\u00A0";
    after = "";
  } else {
    before = this._input.substring(0, this._caretPos);
    current = this._input.charAt(this._caretPos);
    after = this._input.substring(this._caretPos + 1, this._input.length);
  }

  this.promptDisplay.appendChild(document.createTextNode(before));
  this.promptDisplay.appendChild(center);
  this.promptDisplay.appendChild(document.createTextNode(after));

  var hint = this.autocomplete && this.autocomplete.getSelectedWord();
  addClass(center, hint ? 'console-hint' : 'console-cursor');
  center.appendChild(document.createTextNode(hint ? hint.substr(this.getCurrentWord().length) : current));
};

REPLConsole.prototype.writeOutput = function(output) {
  var consoleMessage = document.createElement('pre');
  consoleMessage.className = "console-message";
  consoleMessage.innerHTML = escapeHTML(output);
  this.inner.appendChild(consoleMessage);
  this.newPromptBox();
  return consoleMessage;
};

REPLConsole.prototype.writeError = function(output) {
  var consoleMessage = this.writeOutput(output);
  addClass(consoleMessage, "error-message");
  return consoleMessage;
};

REPLConsole.prototype.onEnterKey = function() {
  var input = this._input;

  if(input != "" && input !== undefined) {
    this.commandStorage.addCommand(input);
  }

  this.commandHandle(input);
};

REPLConsole.prototype.onTabKey = function() {
  var self = this;

  var hintKeyword;
  if (hintKeyword = self.getHintKeyword()) {
    self.swapCurrentWord(hintKeyword);
    return;
  }

  if (self.autocomplete) return;
  self.autocomplete = new Autocomplete([]);

  self.contextRequest(self.getCurrentWord(), function(err, obj) {
    if (err) return self.autocomplete = false;
    self.autocomplete = new Autocomplete(obj['context'], self.getCurrentWord());
    self.inner.appendChild(self.autocomplete.view);
    self.autocomplete.onFinished(function(word) {
      self.swapCurrentWord(word);
      self.autocomplete = false;
    });
    self.scrollToBottom();
  });
};

REPLConsole.prototype.onNavigateHistory = function(offset) {
  var command = this.commandStorage.navigate(offset) || "";
  this.setInput(command);
};

/**
 * Handle control keys like up, down, left, right.
 */
REPLConsole.prototype.onKeyDown = function(ev) {
  if (this.autocomplete && this.autocomplete.onKeyDown(ev)) {
    this.renderInput();
    ev.preventDefault();
    ev.stopPropagation();
    return;
  }

  switch (ev.keyCode) {
    case 69:
      // Ctrl-E
      if (ev.ctrlKey) {
        this.onTabKey();
        ev.preventDefault();
      }
      break;
    case 9:
      // Tab
      this.onTabKey();
      ev.preventDefault();
      break;
    case 13:
      // Enter key
      this.onEnterKey();
      ev.preventDefault();
      break;
    case 80:
      // Ctrl-P
      if (! ev.ctrlKey) break;
    case 38:
      // Up arrow
      this.onNavigateHistory(-1);
      ev.preventDefault();
      break;
    case 78:
      // Ctrl-N
      if (! ev.ctrlKey) break;
    case 40:
      // Down arrow
      this.onNavigateHistory(1);
      ev.preventDefault();
      break;
    case 37:
      // Left arrow
      var caretPos = this._caretPos > 0 ? this._caretPos - 1 : this._caretPos;
      this.setInput(this._input, caretPos);
      ev.preventDefault();
      break;
    case 39:
      // Right arrow
      var length = this._input.length;
      var caretPos = this._caretPos < length ? this._caretPos + 1 : this._caretPos;
      this.setInput(this._input, caretPos);
      ev.preventDefault();
      break;
    case 8:
      // Delete
      this.deleteAtCurrent();
      ev.preventDefault();
      break;
    default:
      break;
  }

  if (ev.ctrlKey || ev.metaKey) {
    // Set focus to our clipboard in case they hit the "v" key
    this.clipboard.focus();
    if (ev.keyCode == 86) {
      // Pasting to clipboard doesn't happen immediately,
      // so we have to wait for a while to get the pasted text.
      var _this = this;
      setTimeout(function() {
        _this.addToInput(_this.clipboard.value);
        _this.clipboard.value = "";
        _this.clipboard.blur();
      }, 10);
    }
  }

  ev.stopPropagation();
};

/**
 * Handle input key press.
 */
REPLConsole.prototype.onKeyPress = function(ev) {
  // Only write to the console if it's a single key press.
  if (ev.ctrlKey || ev.metaKey) { return; }
  var keyCode = ev.keyCode || ev.which;
  this.insertAtCurrent(String.fromCharCode(keyCode));
  ev.stopPropagation();
  ev.preventDefault();
};

/**
 * Delete a character at the current position.
 */
REPLConsole.prototype.deleteAtCurrent = function() {
  if (this._caretPos > 0) {
    var caretPos = this._caretPos - 1;
    var before = this._input.substring(0, caretPos);
    var after = this._input.substring(this._caretPos, this._input.length);
    this.setInput(before + after, caretPos);

    if (!this._input) {
      this.autocomplete && this.autocomplete.cancel();
      this.autocomplete = false;
    }
  }
};

/**
 * Insert a character at the current position.
 */
REPLConsole.prototype.insertAtCurrent = function(char) {
  var before = this._input.substring(0, this._caretPos);
  var after = this._input.substring(this._caretPos, this._input.length);
  this.setInput(before + char + after, this._caretPos + 1);
};

REPLConsole.prototype.swapCurrentWord = function(next) {
  function right(s, pos) {
    var x = s.indexOf(' ', pos);
    return x === -1 ? s.length : x;
  }

  function swap(s, pos) {
    return s.substr(0, s.lastIndexOf(' ', pos) + 1) + next + s.substr(right(s, pos))
  }

  if (!next) return;
  var swapped = swap(this._input, this._caretPos);
  this.setInput(swapped, this._caretPos + swapped.length - this._input.length);
};

REPLConsole.prototype.getCurrentWord = function() {
  return (function(s, pos) {
    var left = s.lastIndexOf(' ', pos);
    if (left === -1) left = 0;
    var right = s.indexOf(' ', pos)
    if (right === -1) right = s.length - 1;
    return s.substr(left, right - left + 1).replace(/^\s+|\s+$/g,'');
  })(this._input, this._caretPos);
};

REPLConsole.prototype.scrollToBottom = function() {
  this.outer.scrollTop = this.outer.scrollHeight;
};

// Change the binding of the console
REPLConsole.prototype.switchBindingTo = function(frameId, callback) {
  var url = this.getSessionUrl('trace');
  var params = "frame_id=" + encodeURIComponent(frameId);
  postRequest(url, params, callback);
};

/**
 * Install the console into the element with a specific ID.
 * Example: REPLConsole.installInto("target-id")
 */
REPLConsole.installInto = function(id, options) {
  var consoleElement = document.getElementById(id);

  options = options || {};

  for (var prop in consoleElement.dataset) {
    options[prop] = options[prop] || consoleElement.dataset[prop];
  }

  var replConsole = new REPLConsole(options);
  replConsole.install(consoleElement);
  return replConsole;
};

// This is to store the latest single session, and the stored session
// is updated by the REPLConsole#install() method.
// It allows to operate the current session from the other scripts.
REPLConsole.currentSession = null;

// This line is for the Firefox Add-on, because it doesn't have XMLHttpRequest as default.
// And so we need to require a module compatible with XMLHttpRequest from SDK.
REPLConsole.XMLHttpRequest = typeof XMLHttpRequest === 'undefined' ? null : XMLHttpRequest;

REPLConsole.request = function request(method, url, params, callback) {
  var xhr = new REPLConsole.XMLHttpRequest();

  xhr.open(method, url, true);
  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
  xhr.setRequestHeader("Accept", "application/vnd.web-console.v2");
  xhr.send(params);

  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      callback(xhr);
    }
  };
};

// DOM helpers
function hasClass(el, className) {
  var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
  return el.className && el.className.match(regex);
}

function isNodeList(el) {
  return typeof el.length === 'number' &&
    typeof el.item === 'function';
}

function addClass(el, className) {
  if (isNodeList(el)) {
    for (var i = 0; i < el.length; ++ i) {
      addClass(el[i], className);
    }
  } else if (!hasClass(el, className)) {
    el.className += " " + className;
  }
}

function removeClass(el, className) {
  var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
  el.className = el.className.replace(regex, '');
}

function removeAllChildren(el) {
  while (el.firstChild) {
    el.removeChild(el.firstChild);
  }
}

function findChild(el, className) {
  for (var i = 0; i < el.childNodes.length; ++ i) {
    if (hasClass(el.childNodes[i], className)) {
      return el.childNodes[i];
    }
  }
}

function escapeHTML(html) {
  return html
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/`/g, '&#x60;');
}

// XHR helpers
function postRequest() {
  REPLConsole.request.apply(this, ["POST"].concat([].slice.call(arguments)));
}

function putRequest() {
  REPLConsole.request.apply(this, ["PUT"].concat([].slice.call(arguments)));
}

if (typeof exports === 'object') {
  exports.REPLConsole = REPLConsole;
} else {
  window.REPLConsole = REPLConsole;
}

// Split string by module operators of ruby
function getContext(s) {
  var methodOp = s.lastIndexOf('.');
  var moduleOp = s.lastIndexOf('::');
  var x = methodOp > moduleOp ? methodOp : moduleOp;
  return x !== -1 ? s.substr(0, x) : '';
}

function flatten(arrays) {
  return Array.prototype.concat.apply([], arrays);
}

}).call(this);
</script>

<script type="text/javascript" data-template="main">
(function() {
  REPLConsole.installInto('console');

}).call(this);
</script>

@maxexcloo
Copy link
Author

If I remove the <% console %> tag, everything returns to normal.

@siman-man
Copy link

I encounted same problem too.

I think this issue reason is implementation changed from Rack::Response to WebConsole::Response in #162.

I don't know what expected type of WebConsole::Response#body. but if body is Rack::BodyProxy object, content dosen't display well. (unfortunately, I don't know well about the case where Rack::BodyProxy is passed.)

temporary workaround the following seems to work fine.

    def write(content)
      raw_body_obj = Array(body).first

      if raw_body_obj.respond_to?(:body)
        raw_body = raw_body_obj.body
      else
        raw_body = raw_body_obj.to_s
      end

      if position = raw_body.rindex('</body>')
        raw_body.insert(position, content)
      else
        raw_body << content
      end

      self.body = raw_body
    end

@gsamokovarov
Copy link
Collaborator

@siman-man can you reproduce this in a simple rails app or give a bit more context about your experience with this issue?

@siman-man
Copy link

@gsamokovarov

I found the cause of this issue. (In my case)

  • Rails: 5.1.6
  • Ruby: ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
  • web-console: 3.6.0

I created a new Rails app (scaffold user), and set expires_now to ApplicationController.

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :expires_now
end

In this state web-console did not work well.

without expires_now

without_expires_now

with expires_now

with_expires_now

@gsamokovarov
Copy link
Collaborator

@siman-man is the Gemfile standard? Can I see it?

@siman-man
Copy link

siman-man commented Apr 18, 2018

@gsamokovarov

This is my Gemfile and Gemfile.lock. (it's seem to standard)

Gemfile
source 'https://rubygems.org'

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.1.6'
# Use mysql as the database for Active Record
gem 'mysql2', '>= 0.3.18', '< 0.6.0'
# Use Puma as the app server
gem 'puma', '~> 3.7'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '~> 2.13'
  gem 'selenium-webdriver'
end

group :development do
  # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    actioncable (5.1.6)
      actionpack (= 5.1.6)
      nio4r (~> 2.0)
      websocket-driver (~> 0.6.1)
    actionmailer (5.1.6)
      actionpack (= 5.1.6)
      actionview (= 5.1.6)
      activejob (= 5.1.6)
      mail (~> 2.5, >= 2.5.4)
      rails-dom-testing (~> 2.0)
    actionpack (5.1.6)
      actionview (= 5.1.6)
      activesupport (= 5.1.6)
      rack (~> 2.0)
      rack-test (>= 0.6.3)
      rails-dom-testing (~> 2.0)
      rails-html-sanitizer (~> 1.0, >= 1.0.2)
    actionview (5.1.6)
      activesupport (= 5.1.6)
      builder (~> 3.1)
      erubi (~> 1.4)
      rails-dom-testing (~> 2.0)
      rails-html-sanitizer (~> 1.0, >= 1.0.3)
    activejob (5.1.6)
      activesupport (= 5.1.6)
      globalid (>= 0.3.6)
    activemodel (5.1.6)
      activesupport (= 5.1.6)
    activerecord (5.1.6)
      activemodel (= 5.1.6)
      activesupport (= 5.1.6)
      arel (~> 8.0)
    activesupport (5.1.6)
      concurrent-ruby (~> 1.0, >= 1.0.2)
      i18n (>= 0.7, < 2)
      minitest (~> 5.1)
      tzinfo (~> 1.1)
    addressable (2.5.2)
      public_suffix (>= 2.0.2, < 4.0)
    arel (8.0.0)
    bindex (0.5.0)
    builder (3.2.3)
    byebug (10.0.2)
    capybara (2.18.0)
      addressable
      mini_mime (>= 0.1.3)
      nokogiri (>= 1.3.3)
      rack (>= 1.0.0)
      rack-test (>= 0.5.4)
      xpath (>= 2.0, < 4.0)
    childprocess (0.9.0)
      ffi (~> 1.0, >= 1.0.11)
    coffee-rails (4.2.2)
      coffee-script (>= 2.2.0)
      railties (>= 4.0.0)
    coffee-script (2.4.1)
      coffee-script-source
      execjs
    coffee-script-source (1.12.2)
    concurrent-ruby (1.0.5)
    crass (1.0.4)
    erubi (1.7.1)
    execjs (2.7.0)
    ffi (1.9.23)
    globalid (0.4.1)
      activesupport (>= 4.2.0)
    i18n (1.0.1)
      concurrent-ruby (~> 1.0)
    jbuilder (2.7.0)
      activesupport (>= 4.2.0)
      multi_json (>= 1.2)
    listen (3.1.5)
      rb-fsevent (~> 0.9, >= 0.9.4)
      rb-inotify (~> 0.9, >= 0.9.7)
      ruby_dep (~> 1.2)
    loofah (2.2.2)
      crass (~> 1.0.2)
      nokogiri (>= 1.5.9)
    mail (2.7.0)
      mini_mime (>= 0.1.1)
    method_source (0.9.0)
    mini_mime (1.0.0)
    mini_portile2 (2.3.0)
    minitest (5.11.3)
    multi_json (1.13.1)
    mysql2 (0.5.1)
    nio4r (2.3.0)
    nokogiri (1.8.2)
      mini_portile2 (~> 2.3.0)
    public_suffix (3.0.2)
    puma (3.11.4)
    rack (2.0.4)
    rack-test (1.0.0)
      rack (>= 1.0, < 3)
    rails (5.1.6)
      actioncable (= 5.1.6)
      actionmailer (= 5.1.6)
      actionpack (= 5.1.6)
      actionview (= 5.1.6)
      activejob (= 5.1.6)
      activemodel (= 5.1.6)
      activerecord (= 5.1.6)
      activesupport (= 5.1.6)
      bundler (>= 1.3.0)
      railties (= 5.1.6)
      sprockets-rails (>= 2.0.0)
    rails-dom-testing (2.0.3)
      activesupport (>= 4.2.0)
      nokogiri (>= 1.6)
    rails-html-sanitizer (1.0.4)
      loofah (~> 2.2, >= 2.2.2)
    railties (5.1.6)
      actionpack (= 5.1.6)
      activesupport (= 5.1.6)
      method_source
      rake (>= 0.8.7)
      thor (>= 0.18.1, < 2.0)
    rake (12.3.1)
    rb-fsevent (0.10.3)
    rb-inotify (0.9.10)
      ffi (>= 0.5.0, < 2)
    ruby_dep (1.5.0)
    rubyzip (1.2.1)
    sass (3.5.6)
      sass-listen (~> 4.0.0)
    sass-listen (4.0.0)
      rb-fsevent (~> 0.9, >= 0.9.4)
      rb-inotify (~> 0.9, >= 0.9.7)
    sass-rails (5.0.7)
      railties (>= 4.0.0, < 6)
      sass (~> 3.1)
      sprockets (>= 2.8, < 4.0)
      sprockets-rails (>= 2.0, < 4.0)
      tilt (>= 1.1, < 3)
    selenium-webdriver (3.11.0)
      childprocess (~> 0.5)
      rubyzip (~> 1.2)
    spring (2.0.2)
      activesupport (>= 4.2)
    spring-watcher-listen (2.0.1)
      listen (>= 2.7, < 4.0)
      spring (>= 1.2, < 3.0)
    sprockets (3.7.1)
      concurrent-ruby (~> 1.0)
      rack (> 1, < 3)
    sprockets-rails (3.2.1)
      actionpack (>= 4.0)
      activesupport (>= 4.0)
      sprockets (>= 3.0.0)
    thor (0.20.0)
    thread_safe (0.3.6)
    tilt (2.0.8)
    turbolinks (5.1.1)
      turbolinks-source (~> 5.1)
    turbolinks-source (5.1.0)
    tzinfo (1.2.5)
      thread_safe (~> 0.1)
    uglifier (4.1.9)
      execjs (>= 0.3.0, < 3)
    web-console (3.6.0)
      actionview (>= 5.0)
      activemodel (>= 5.0)
      bindex (>= 0.4.0)
      railties (>= 5.0)
    websocket-driver (0.6.5)
      websocket-extensions (>= 0.1.0)
    websocket-extensions (0.1.3)
    xpath (3.0.0)
      nokogiri (~> 1.8)

PLATFORMS
  ruby

DEPENDENCIES
  byebug
  capybara (~> 2.13)
  coffee-rails (~> 4.2)
  jbuilder (~> 2.5)
  listen (>= 3.0.5, < 3.2)
  mysql2 (>= 0.3.18, < 0.6.0)
  puma (~> 3.7)
  rails (~> 5.1.6)
  sass-rails (~> 5.0)
  selenium-webdriver
  spring
  spring-watcher-listen (~> 2.0.0)
  turbolinks (~> 5)
  tzinfo-data
  uglifier (>= 1.3.0)
  web-console (>= 3.3.0)

BUNDLED WITH
   1.16.1

@gsamokovarov
Copy link
Collaborator

@siman-man thanks for the hints, I was able to reproduce the issue and fix it. Will release 3.6.1 tomorrow morning.

@siman-man
Copy link

siman-man commented Apr 18, 2018

@gsamokovarov thanks!

I confirmed this issue solved in master branch. I'm look forward to 3.6.1 release. 😄

MasoudvWork pushed a commit to visfleet/web-console that referenced this issue Jan 17, 2022
I cannot be sure that every body can be coerced as an array, the only
thing we can rely on is that all the bodies will respond to each. This
fixes rails#252.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants