From 1950cc8db0330cc17c6385c08dc78f99a2dd747f Mon Sep 17 00:00:00 2001 From: Michael Macias Date: Sat, 18 Feb 2012 02:40:56 -0600 Subject: [PATCH] Refactor frontend * restructured JavaScript using backbone.js * replaced highlight.js with CodeMirror for its editor * added CodeMirror Solarized (dark) theme based on Ethan Schoonover's solarized.vim * changed `POST /document` to accept real JSON * cleaned up template and stylesheet --- lib/document_handler.js | 2 +- static/application.css | 289 +++++++++++--------- static/application.js | 555 ++++++++++++-------------------------- static/application.min.js | 2 +- static/backbone.min.js | 37 +++ static/codemirror.min.js | 1 + static/highlight.min.js | 1 - static/index.html | 91 ++----- static/solarized_dark.css | 98 ------- static/underscore.min.js | 31 +++ 10 files changed, 426 insertions(+), 681 deletions(-) create mode 100644 static/backbone.min.js create mode 100644 static/codemirror.min.js delete mode 100644 static/highlight.min.js delete mode 100644 static/solarized_dark.css create mode 100644 static/underscore.min.js diff --git a/lib/document_handler.js b/lib/document_handler.js index 0ecc773..b65683c 100644 --- a/lib/document_handler.js +++ b/lib/document_handler.js @@ -55,7 +55,7 @@ DocumentHandler.prototype.handlePost = function(request, response) { if (!buffer) { response.writeHead(200, { 'content-type': 'application/json' }); } - buffer += data.toString(); + buffer += JSON.parse(data.toString()).data; if (_this.maxLength && buffer.length > _this.maxLength) { cancelled = true; winston.warn('document >maxLength', { maxLength: _this.maxLength }); diff --git a/static/application.css b/static/application.css index 55e9469..1725140 100644 --- a/static/application.css +++ b/static/application.css @@ -1,168 +1,201 @@ +html, body, div, pre, textarea, header, h1, a, nav, ul, li { + margin: 0; + padding: 0; +} + body { - background: #002B36; - padding: 20px 50px; - margin: 0px; + font: 13px monospace; } -/* textarea */ - -textarea { - background: transparent; - border: 0px; - color: #fff; - padding: 0px; - width: 100%; - height: 100%; - font-family: monospace; - outline: none; - resize: none; - font-size: 13px; +header { + position: fixed; + top: 0; + right: 0; + z-index: 1000; } -/* the line numbers */ - -#linenos { - color: #7d7d7d; - z-index: -1000; - position: absolute; - top: 20px; - left: 0px; - width: 30px; /* 30 to get 20 away from box */ - font-size: 13px; - font-family: monospace; - text-align: right; +header h1 { + background: #00222b; + padding: 5px 22px; } -/* code box when locked */ - -#box { - padding: 0px; - margin: 0px; - width: 100%; - border: 0px; - outline: none; - font-size: 13px; +header h1 a { + background: transparent url('logo.png') no-repeat top center; + display: block; + overflow: hidden; + text-indent: -9999px; + width: 126px; + height: 42px; } -#box code { - padding: 0px; - background: transparent !important; /* don't hide hastebox */ +header h1 a:hover { + background-position: bottom center; } -/* key */ - -#key { - position: fixed; - top: 0px; - right: 0px; - z-index: +1000; /* watch out */ +header ul { + background: #08323c; + font-size: 0; + list-style: none; + /*overflow: hidden;*/ + text-align: center; } -#box1 { - padding: 5px; - text-align: center; - background: #00222b; +header ul li { + display: inline-block; + position: relative; } -#box2 { - background: #08323c; - font-size: 0px; - padding: 0px 5px; +header ul li .pointer { + background: transparent url('hover-dropdown-tip.png') no-repeat; + display: inline-block; + text-align: center; + width: 10px; + height: 5px; } -#box1 a.logo, #box1 a.logo:visited { - display: inline-block; - background: url(logo.png); - width: 126px; - height: 42px; +header ul li a { + background: transparent url('function-icons.png'); + display: block; + overflow: hidden; + text-indent: -9999px; + width: 32px; + height: 37px; } -#box1 a.logo:hover { - background-position: 0 bottom; +header ul li a.disabled { + cursor: default; } -#box2 .function { - background: url(function-icons.png); - width: 32px; - height: 37px; - display: inline-block; - position: relative; +header li a.save { background-position: -5px center; } +header li a.save:hover { background-position: -5px bottom; } +header li a.save.disabled { background-position: -5px top; } + +header li a.new { background-position: -42px center; } +header li a.new:hover { background-position: -42px bottom; } +header li a.new.disabled { background-position: -42px top; } + +header li a.edit { background-position: -79px center; } +header li a.edit:hover { background-position: -79px bottom; } +header li a.edit.disabled { background-position: -79px top; } + +header li a.raw { background-position: -116px center; } +header li a.raw:hover { background-position: -116px bottom; } +header li a.raw.disabled { background-position: -116px top; } + +header li a.twitter { background-position: -153px center; } +header li a.twitter:hover { background-position: -153px bottom; } +header li a.twitter.disabled { background-position: -153px top; } + +#editor { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; } -#box2 .link embed { - vertical-align: bottom; /* fix for zeroClipboard style */ +.CodeMirror { + line-height: 1em; + height: 100%; } -#box2 .function.enabled:hover { - cursor: hand; - cursor: pointer; +.CodeMirror-scroll { + height: 100%; + overflow: auto; + position: relative; } -#pointer { - display: block; - height: 5px; - width: 10px; - background: url(hover-dropdown-tip.png); - bottom: 0px; - position: absolute; - margin: auto; - left: 0px; - right: 0px; +.CodeMirror-gutter { + height: 100%; + min-width: 2em; + position: absolute; + top: 0; + left: 0; } -#box3, #messages li { - background: #173e48; - font-family: Helvetica, sans-serif; - font-size: 12px; - line-height: 14px; - padding: 10px 15px; +.CodeMirror-gutter-text { + text-align: right; + padding: 0.4em 0.2em 0.4em 0.4em; + white-space: pre; } -#box3 .label, #messages li { - color: #fff; - font-weight: bold; +.CodeMirror-lines { + padding: 0.4em; } -#box3 .shortcut { - color: #c4dce3; - font-weight: normal; +.CodeMirror textarea { + outline: 0; } -#box2 .function.save { background-position: -5px top; } -#box2 .function.enabled.save { background-position: -5px center; } -#box2 .function.enabled.save:hover { background-position: -5px bottom; } - -#box2 .function.new { background-position: -42px top; } -#box2 .function.enabled.new { background-position: -42px center; } -#box2 .function.enabled.new:hover { background-position: -42px bottom; } - -#box2 .function.duplicate { background-position: -79px top; } -#box2 .function.enabled.duplicate { background-position: -79px center; } -#box2 .function.enabled.duplicate:hover { background-position: -79px bottom; } - -#box2 .function.raw { background-position: -116px top; } -#box2 .function.enabled.raw { background-position: -116px center; } -#box2 .function.enabled.raw:hover { background-position: -116px bottom; } - -#box2 .function.twitter { background-position: -153px top; } -#box2 .function.enabled.twitter { background-position: -153px center; } -#box2 .function.enabled.twitter:hover { background-position: -153px bottom; } - -#messages { - position:fixed; - top:0px; - right:138px; - margin:0; - padding:0; - width:400px; +.CodeMirror pre.CodeMirror-cursor { + position: absolute; + visibility: hidden; + z-index: 10; } -#messages li { - background:rgba(23,62,72,0.8); - margin:0 auto; - list-style:none; +.CodeMirror-focused pre.CodeMirror-cursor { + visibility: visible; } -#messages li.error { - background:rgba(102,8,0,0.8); +span.cm-header, span.cm-strong { + font-weight: bold; } + +span.cm-em { + font-style: italic; +} + +span.cm-emstrong { + font-style: italic; font-weight: bold; +} + +span.cm-link { + text-decoration: underline; +} + +/* Solarized (dark) theme */ + +.cm-s-solarized-dark { + background: #002b36; + color: #839496; +} + +.cm-s-solarized-dark div.CodeMirror-selected { + background: #586e75; +} + +.cm-s-solarized-dark .CodeMirror-gutter { + background: #073642; +} + +.cm-s-solarized-dark .CodeMirror-gutter-text { + color: #586e75; +} + +.cm-s-solarized-dark .CodeMirror-cursor { + border-left: 1px solid #839496; +} + +.cm-s-solarized-dark span.cm-keyword { color: #268bd2; } +.cm-s-solarized-dark span.cm-atom { color: #b58900; } +.cm-s-solarized-dark span.cm-number { color: #2aa198; } +.cm-s-solarized-dark span.cm-def { color: #839496; } +.cm-s-solarized-dark span.cm-variable { color: #839496; } +.cm-s-solarized-dark span.cm-variable-2 { color: #b58900; } +.cm-s-solarized-dark span.cm-variable-3 { color: #268bd2; } +.cm-s-solarized-dark span.cm-property { color: #859900; } +.cm-s-solarized-dark span.cm-operator { color: #2aa198; } +.cm-s-solarized-dark span.cm-comment { color: #586e75; } +.cm-s-solarized-dark span.cm-string { color: #2aa198; } +.cm-s-solarized-dark span.cm-string-2 { color: #2aa198; } +.cm-s-solarized-dark span.cm-meta { color: #586e75; } +.cm-s-solarized-dark span.cm-error { color: #dc322f; } +.cm-s-solarized-dark span.cm-qualifier { color: #268bd2; } +.cm-s-solarized-dark span.cm-builtin { color: #b58900; } +.cm-s-solarized-dark span.cm-bracket { color: #dc322f; } +.cm-s-solarized-dark span.cm-tag { color: #268bd2; } +.cm-s-solarized-dark span.cm-attribute { color: #839496; } +.cm-s-solarized-dark span.cm-header { color: #cb4b16; } +.cm-s-solarized-dark span.cm-quote { color: #586e75; } +.cm-s-solarized-dark span.cm-hr { color: #cb4b16; } +.cm-s-solarized-dark span.cm-link { color: #6c71c4; } diff --git a/static/application.js b/static/application.js index 7afb014..96e6753 100644 --- a/static/application.js +++ b/static/application.js @@ -1,396 +1,171 @@ -///// represents a single document +window.Haste = { + Models: {}, + Views: {}, + Routers: {}, -var haste_document = function() { - this.locked = false; -}; + extensionMap: { + clj: 'clojure', coffee: 'coffeescript', css: 'css', diff: 'diff', go: 'go', + hs: 'haskell', html: 'htmlmixed', js: 'javascript', lua: 'lua', + md: 'markdown', markdown: 'markdown', sql: 'mysql', pl: 'perl', php: 'php', + py: 'python', r: 'r', rb: 'ruby', scm: 'scheme', xml: 'xml', yml: 'yaml' + }, -// Escapes HTML tag characters -haste_document.prototype.htmlEscape = function(s) { - return s - .replace(/&/g, '&') - .replace(/>/g, '>') - .replace(/'+msg+''); - $('#messages').prepend(msgBox); - setTimeout(function() { - msgBox.slideUp('fast', function() { $(this).remove(); }); - }, 3000); -}; - -// Show the light key -haste.prototype.lightKey = function() { - this.configureKey(['new', 'save']); -}; - -// Show the full key -haste.prototype.fullKey = function() { - this.configureKey(['new', 'duplicate', 'twitter', 'raw']); -}; - -// Set the key up for certain things to be enabled -haste.prototype.configureKey = function(enable) { - var $this, i = 0; - $('#box2 .function').each(function() { - $this = $(this); - for (i = 0; i < enable.length; i++) { - if ($this.hasClass(enable[i])) { - $this.addClass('enabled'); - return true; - } - } - $this.removeClass('enabled'); - }); -}; - -// Remove the current document (if there is one) -// and set up for a new one -haste.prototype.newDocument = function(hideHistory) { - this.$box.hide(); - this.doc = new haste_document(); - if (!hideHistory) { - window.history.pushState(null, this.appName, '/'); - } - this.setTitle(); - this.lightKey(); - this.$textarea.val('').show('fast', function() { - this.focus(); - }); - this.removeLineNumbers(); -}; - -// Map of common extensions -// Note: this list does not need to include anything that IS its extension, -// due to the behavior of lookupTypeByExtension and lookupExtensionByType -// Note: optimized for lookupTypeByExtension -haste.extensionMap = { - rb: 'ruby', py: 'python', pl: 'perl', php: 'php', scala: 'scala', go: 'go', - xml: 'xml', html: 'xml', htm: 'xml', css: 'css', js: 'javascript', vbs: 'vbscript', - lua: 'lua', pas: 'delphi', java: 'java', cpp: 'cpp', cc: 'cpp', m: 'objectivec', - vala: 'vala', cs: 'cs', sql: 'sql', sm: 'smalltalk', lisp: 'lisp', ini: 'ini', - diff: 'diff', bash: 'bash', sh: 'bash', tex: 'tex', erl: 'erlang', hs: 'haskell', - md: 'markdown', txt: '', coffee: 'coffee' -}; - -// Look up the extension preferred for a type -// If not found, return the type itself - which we'll place as the extension -haste.prototype.lookupExtensionByType = function(type) { - for (var key in haste.extensionMap) { - if (haste.extensionMap[key] === type) return key; - } - return type; -}; - -// Look up the type for a given extension -// If not found, return the extension - which we'll attempt to use as the type -haste.prototype.lookupTypeByExtension = function(ext) { - return haste.extensionMap[ext] || ext; -}; - -// Add line numbers to the document -// For the specified number of lines -haste.prototype.addLineNumbers = function(lineCount) { - var h = ''; - for (var i = 0; i < lineCount; i++) { - h += (i + 1).toString() + '
'; - } - $('#linenos').html(h); -}; - -// Remove the line numbers -haste.prototype.removeLineNumbers = function() { - $('#linenos').html('>'); -}; - -// Load a document and show it -haste.prototype.loadDocument = function(key) { - // Split the key up - var parts = key.split('.', 2); - // Ask for what we want - var _this = this; - _this.doc = new haste_document(); - _this.doc.load(parts[0], function(ret) { - if (ret) { - _this.$code.html(ret.value); - _this.setTitle(ret.key); - _this.fullKey(); - _this.$textarea.val('').hide(); - _this.$box.show().focus(); - _this.addLineNumbers(ret.lineCount); - } - else { - _this.newDocument(); - } - }, this.lookupTypeByExtension(parts[1])); -}; - -// Duplicate the current document - only if locked -haste.prototype.duplicateDocument = function() { - if (this.doc.locked) { - var currentData = this.doc.data; - this.newDocument(); - this.$textarea.val(currentData); - } -}; - -// Lock the current document -haste.prototype.lockDocument = function() { - var _this = this; - this.doc.save(this.$textarea.val(), function(err, ret) { - if (err) { - _this.showMessage(err.message, 'error'); - } - else if (ret) { - _this.$code.html(ret.value); - _this.setTitle(ret.key); - var file = '/' + ret.key; - if (ret.language) { - file += '.' + _this.lookupExtensionByType(ret.language); - } - window.history.pushState(null, _this.appName + '-' + ret.key, file); - _this.fullKey(); - _this.$textarea.val('').hide(); - _this.$box.show().focus(); - _this.addLineNumbers(ret.lineCount); - } - }); -}; - -haste.prototype.configureButtons = function() { - var _this = this; - this.buttons = [ - { - $where: $('#box2 .save'), - label: 'Save', - shortcutDescription: 'control + s', - shortcut: function(evt) { - return evt.ctrlKey && (evt.keyCode === 83); - }, - action: function() { - if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') { - _this.lockDocument(); - } - } - }, - { - $where: $('#box2 .new'), - label: 'New', - shortcut: function(evt) { - return evt.ctrlKey && evt.keyCode === 78 - }, - shortcutDescription: 'control + n', - action: function() { - _this.newDocument(!_this.doc.key); - } - }, - { - $where: $('#box2 .duplicate'), - label: 'Duplicate & Edit', - shortcut: function(evt) { - return _this.doc.locked && evt.ctrlKey && evt.keyCode === 68; - }, - shortcutDescription: 'control + d', - action: function() { - _this.duplicateDocument(); - } - }, - { - $where: $('#box2 .raw'), - label: 'Just Text', - shortcut: function(evt) { - return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82; - }, - shortcutDescription: 'control + shift + r', - action: function() { - window.location.href = '/raw/' + _this.doc.key; - } - }, - { - $where: $('#box2 .twitter'), - label: 'Twitter', - shortcut: function(evt) { - return _this.options.twitter && _this.doc.locked && evt.ctrlKey && evt.keyCode == 84; - }, - shortcutDescription: 'control + t', - action: function() { - window.open('https://twitter.com/share?url=' + encodeURI(window.location.href)); - } - } - ]; - for (var i = 0; i < this.buttons.length; i++) { - this.configureButton(this.buttons[i]); - } -}; - -haste.prototype.configureButton = function(options) { - // Handle the click action - options.$where.click(function(evt) { - evt.preventDefault(); - if (!options.clickDisabled && $(this).hasClass('enabled')) { - options.action(); - } - }); - // Show the label - options.$where.mouseenter(function(evt) { - $('#box3 .label').text(options.label); - $('#box3 .shortcut').text(options.shortcutDescription || ''); - $('#box3').show(); - $(this).append($('#pointer').remove().show()); - }); - // Hide the label - options.$where.mouseleave(function(evt) { - $('#box3').hide(); - $('#pointer').hide(); - }); -}; - -// Configure keyboard shortcuts for the textarea -haste.prototype.configureShortcuts = function() { - var _this = this; - $(document.body).keydown(function(evt) { - var button; - for (var i = 0 ; i < _this.buttons.length; i++) { - button = _this.buttons[i]; - if (button.shortcut && button.shortcut(evt)) { - evt.preventDefault(); - button.action(); - return; - } - } - }); -}; - -///// Tab behavior in the textarea - 2 spaces per tab -$(function() { - - $('textarea').keydown(function(evt) { - if (evt.keyCode === 9) { - evt.preventDefault(); - var myValue = ' '; - // http://stackoverflow.com/questions/946534/insert-text-into-textarea-with-jquery - // For browsers like Internet Explorer - if (document.selection) { - this.focus(); - sel = document.selection.createRange(); - sel.text = myValue; - this.focus(); - } - // Mozilla and Webkit - else if (this.selectionStart || this.selectionStart == '0') { - var startPos = this.selectionStart; - var endPos = this.selectionEnd; - var scrollTop = this.scrollTop; - this.value = this.value.substring(0, startPos) + myValue + - this.value.substring(endPos,this.value.length); - this.focus(); - this.selectionStart = startPos + myValue.length; - this.selectionEnd = startPos + myValue.length; - this.scrollTop = scrollTop; - } - else { - this.value += myValue; - this.focus(); - } - } - }); - +Haste.Models.Document = Backbone.Model.extend({ + idAttribute: 'key', + urlRoot: '/documents' +}); + +Haste.Routers.Document = Backbone.Router.extend({ + routes: { + ':id.:extension': 'show', + ':id': 'show', + '': 'new' + }, + + initialize: function() { + this.editor = new Haste.Views.EditorView(); + }, + + show: function(id, extension) { + this.editor.load(id, extension); + }, + + new: function() { + this.editor.new(); + } +}); + +Haste.Views.ActionsView = Backbone.View.extend({ + el: 'header', + + events: { + 'click .new': 'new', + 'click .save': 'save', + 'click .edit': 'edit', + 'click .raw': 'raw', + 'click .twitter': 'raw' + }, + + initialize: function() { + this.parent = this.options.parent; + }, + + toggleActions: function() { + var klass = 'disabled'; + + if (this.parent.model.isNew()) { + $('.save', this.el).removeClass(klass); + $('.edit, .raw, .twitter', this.el).addClass(klass); + } else { + $('.save', this.el).addClass(klass); + $('.edit, .raw, .twitter', this.el).removeClass(klass); + } + + this.setLink('.raw', 'raw/' + this.parent.model.id); + this.setLink('.twitter', 'https://twitter.com/share?url=' + encodeURI(window.location.href)); + }, + + setLink: function(el, href) { + if (this.parent.model.isNew()) { + href = '#'; + } + + $(el, this.el).attr('href', href); + }, + + new: function(event) { + event.preventDefault(); + this.parent.new(); + Backbone.history.navigate(''); + }, + + save: function(event) { + event.preventDefault(); + + if (!this.parent.model.isNew()) { return; } + + this.parent.save(); + }, + + edit: function(event) { + event.preventDefault(); + + if (this.parent.model.isNew()) { return; } + + this.parent.model.set('key', null); + Backbone.history.navigate('/'); + }, + + raw: function(event) { + if (this.model.isNew()) { + event.preventDefault(); + } + }, +}); + +Haste.Views.EditorView = Backbone.View.extend({ + el: 'textarea', + + initialize: function() { + this.codeMirror = CodeMirror.fromTextArea(this.el, { + mode: 'null', + lineNumbers: true, + theme: 'solarized-dark' + }); + + this.actionsView = new Haste.Views.ActionsView({ parent: this }); + }, + + render: function() { + this.codeMirror.setOption('mode', this.model.get('mode') || 'null'); + this.codeMirror.setValue(this.model.get('data') || ''); + + return this; + }, + + new: function() { + this.model = new Haste.Models.Document(); + + this.model.on('change', this.render, this); + this.model.on('change', this.toggleLock, this); + this.model.on('change', this.actionsView.toggleActions, this.actionsView); + + this.model.trigger('change'); + }, + + load: function(key, extension) { + this.new(); + + var mode = Haste.extensionMap[extension]; + this.model.set({ key: key, mode: mode }, { silent: true }); + + this.model.fetch(); + }, + + save: function() { + var data = this.codeMirror.getValue(); + + if (!data) { return; } + + this.model.save('data', data, { + success: function(model, response) { + Backbone.history.navigate(model.id); + } + }); + }, + + toggleLock: function() { + this.codeMirror.setOption('readOnly', !this.model.isNew()); + this.actionsView.toggleActions(); + } +}); + +$(function() { + Haste.init(); }); diff --git a/static/application.min.js b/static/application.min.js index 5c5ce83..c361c48 100644 --- a/static/application.min.js +++ b/static/application.min.js @@ -1 +1 @@ -var haste_document=function(){this.locked=!1};haste_document.prototype.htmlEscape=function(a){return a.replace(/&/g,"&").replace(/>/g,">").replace(/'+a+"");$("#messages").prepend(c),setTimeout(function(){c.slideUp("fast",function(){$(this).remove()})},3e3)},haste.prototype.lightKey=function(){this.configureKey(["new","save"])},haste.prototype.fullKey=function(){this.configureKey(["new","duplicate","twitter","raw"])},haste.prototype.configureKey=function(a){var b,c=0;$("#box2 .function").each(function(){b=$(this);for(c=0;c";$("#linenos").html(b)},haste.prototype.removeLineNumbers=function(){$("#linenos").html(">")},haste.prototype.loadDocument=function(a){var b=a.split(".",2),c=this;c.doc=new haste_document,c.doc.load(b[0],function(a){a?(c.$code.html(a.value),c.setTitle(a.key),c.fullKey(),c.$textarea.val("").hide(),c.$box.show().focus(),c.addLineNumbers(a.lineCount)):c.newDocument()},this.lookupTypeByExtension(b[1]))},haste.prototype.duplicateDocument=function(){if(this.doc.locked){var a=this.doc.data;this.newDocument(),this.$textarea.val(a)}},haste.prototype.lockDocument=function(){var a=this;this.doc.save(this.$textarea.val(),function(b,c){if(b)a.showMessage(b.message,"error");else if(c){a.$code.html(c.value),a.setTitle(c.key);var d="/"+c.key;c.language&&(d+="."+a.lookupExtensionByType(c.language)),window.history.pushState(null,a.appName+"-"+c.key,d),a.fullKey(),a.$textarea.val("").hide(),a.$box.show().focus(),a.addLineNumbers(c.lineCount)}})},haste.prototype.configureButtons=function(){var a=this;this.buttons=[{$where:$("#box2 .save"),label:"Save",shortcutDescription:"control + s",shortcut:function(a){return a.ctrlKey&&a.keyCode===83},action:function(){a.$textarea.val().replace(/^\s+|\s+$/g,"")!==""&&a.lockDocument()}},{$where:$("#box2 .new"),label:"New",shortcut:function(a){return a.ctrlKey&&a.keyCode===78},shortcutDescription:"control + n",action:function(){a.newDocument(!a.doc.key)}},{$where:$("#box2 .duplicate"),label:"Duplicate & Edit",shortcut:function(b){return a.doc.locked&&b.ctrlKey&&b.keyCode===68},shortcutDescription:"control + d",action:function(){a.duplicateDocument()}},{$where:$("#box2 .raw"),label:"Just Text",shortcut:function(a){return a.ctrlKey&&a.shiftKey&&a.keyCode===82},shortcutDescription:"control + shift + r",action:function(){window.location.href="/raw/"+a.doc.key}},{$where:$("#box2 .twitter"),label:"Twitter",shortcut:function(b){return a.options.twitter&&a.doc.locked&&b.ctrlKey&&b.keyCode==84},shortcutDescription:"control + t",action:function(){window.open("https://twitter.com/share?url="+encodeURI(window.location.href))}}];for(var b=0;b=b))this.iframe=h('