From d8e87997c1312828b4a83156503e6c5b20f48659 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 15 Jan 2018 17:17:28 +0000 Subject: [PATCH 1/9] Install codemirror --- services/web/npm-shrinkwrap.json | 5 + services/web/package.json | 1 + .../public/stylesheets/_style_includes.less | 3 + .../public/stylesheets/vendor/codemirror.css | 346 ++++++++++++++++++ 4 files changed, 355 insertions(+) create mode 100644 services/web/public/stylesheets/vendor/codemirror.css diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json index 765fc1f03e..68f9cbd92a 100644 --- a/services/web/npm-shrinkwrap.json +++ b/services/web/npm-shrinkwrap.json @@ -1354,6 +1354,11 @@ "from": "code-point-at@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" }, + "codemirror": { + "version": "5.33.0", + "from": "codemirror@latest", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.33.0.tgz" + }, "coffee-script": { "version": "1.12.4", "from": "coffee-script@1.12.4", diff --git a/services/web/package.json b/services/web/package.json index 7cb82b05d6..860d98733a 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -30,6 +30,7 @@ "bcrypt": "1.0.1", "body-parser": "^1.13.1", "bufferedstream": "1.6.0", + "codemirror": "^5.33.0", "connect-redis": "^3.1.0", "cookie": "^0.2.3", "cookie-parser": "1.3.5", diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less index 54539a4cb4..1c27a37014 100644 --- a/services/web/public/stylesheets/_style_includes.less +++ b/services/web/public/stylesheets/_style_includes.less @@ -48,6 +48,9 @@ @import "components/popovers.less"; @import "components/carousel.less"; +// CodeMirror +@import "vendor/codemirror.css"; + // ngTagsInput @import "components/tags-input.less"; diff --git a/services/web/public/stylesheets/vendor/codemirror.css b/services/web/public/stylesheets/vendor/codemirror.css new file mode 100644 index 0000000000..c7a8ae7047 --- /dev/null +++ b/services/web/public/stylesheets/vendor/codemirror.css @@ -0,0 +1,346 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; + direction: ltr; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} +.cm-fat-cursor-mark { + background-color: rgba(20, 255, 20, 0.5); + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; +} +@-moz-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@-webkit-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} + +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror-overwrite .CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-rulers { + position: absolute; + left: 0; right: 0; top: -50px; bottom: -20px; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; bottom: 0; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + min-height: 100%; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -30px; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + padding: 0.1px; /* Force widget margins to stay inside of the container */ +} + +.CodeMirror-widget {} + +.CodeMirror-rtl pre { direction: rtl; } + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.CodeMirror-cursor { + position: absolute; + pointer-events: none; +} +.CodeMirror-measure pre { position: static; } + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +div.CodeMirror-dragcursors { + visibility: visible; +} + +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background-color: #ffa; + background-color: rgba(255, 255, 0, .4); +} + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } From f5b6d3ff3e01578deb9b925c34a8001910156968 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 16 Jan 2018 17:51:30 +0000 Subject: [PATCH 2/9] Load basic CM instance with dummy content --- services/web/app/views/project/editor.pug | 6 ++++++ services/web/public/es/rich-text.js | 6 +++++- services/web/webpack.config.js | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 13939ee5d6..58c56bdce8 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -158,6 +158,12 @@ block requirejs window.pdfCMapsPath = "#{pdfCMapsPath}" window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); + // TODO: inject based on environment + script( + src="http://localhost:3809/public/js/es/richText.js" + type="text/javascript" + ) + script( data-main=buildJsPath("ide.js", {hashedPath:false}), baseurl=fullJsPath, diff --git a/services/web/public/es/rich-text.js b/services/web/public/es/rich-text.js index 0b7ef955ad..b07155829d 100644 --- a/services/web/public/es/rich-text.js +++ b/services/web/public/es/rich-text.js @@ -1 +1,5 @@ -console.log('hello from rich text') +import CodeMirror from 'codemirror' + +export function init(rootEl) { + return CodeMirror(rootEl) +} diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 78e21f4982..b9474cffac 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -4,7 +4,7 @@ module.exports = { // Defines the "entry point(s)" for the application - i.e. the file which // bootstraps the application entry: { - 'rich-text': './public/es/rich-text.js' + richText: './public/es/rich-text.js' }, // Define where and how the bundle will be output to disk From 09fbafa1f63efa5b5ac93cb038a8275a72f09793 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 2 Feb 2018 17:24:27 +0000 Subject: [PATCH 3/9] Include ShareJS CM adapter in compiled lib and fix bug with CM adapter Because the CM & Ace scripts are naively concat-ed together, the Ace applyToShareJS function would be overwritten by the CM version. Also fixes bugs where adapter was calling old version of ShareJS api and the old CM api --- services/web/Gruntfile.coffee | 1 + services/web/Makefile | 3 ++- .../ide/editor/sharejs/vendor/client/ace.coffee | 4 ++-- .../ide/editor/sharejs/vendor/client/cm.coffee | 14 +++++++------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 2b8cc27421..e4478a9fb7 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -108,6 +108,7 @@ module.exports = (grunt) -> "public/coffee/ide/editor/sharejs/vendor/client/microevent.coffee" "public/coffee/ide/editor/sharejs/vendor/client/doc.coffee" "public/coffee/ide/editor/sharejs/vendor/client/ace.coffee" + "public/coffee/ide/editor/sharejs/vendor/client/cm.coffee" ] client: diff --git a/services/web/Makefile b/services/web/Makefile index 919a204880..e9c796b6a0 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -21,7 +21,8 @@ SHAREJS_COFFEE_FILES := \ public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee \ public/coffee/ide/editor/sharejs/vendor/client/microevent.coffee \ public/coffee/ide/editor/sharejs/vendor/client/doc.coffee \ - public/coffee/ide/editor/sharejs/vendor/client/ace.coffee + public/coffee/ide/editor/sharejs/vendor/client/ace.coffee \ + public/coffee/ide/editor/sharejs/vendor/client/cm.coffee LESS_FILES := $(shell find public/stylesheets -name '*.less') CSS_FILES := public/stylesheets/style.css public/stylesheets/ol-style.css diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/ace.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/ace.coffee index ac0db65b1a..177af1317e 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/ace.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/ace.coffee @@ -3,7 +3,7 @@ Range = ace.require("ace/range").Range # Convert an ace delta into an op understood by share.js -applyToShareJS = (editorDoc, delta, doc, fromUndo) -> +applyAceToShareJS = (editorDoc, delta, doc, fromUndo) -> # Get the start position of the range, in no. of characters getStartOffsetPosition = (start) -> # This is quite inefficient - getLines makes a copy of the entire @@ -81,7 +81,7 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength fromUndo = !!(editor.getSession().$fromUndo or editor.getSession().$fromReject) - applyToShareJS editorDoc, change, doc, fromUndo + applyAceToShareJS editorDoc, change, doc, fromUndo check() diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/cm.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/cm.coffee index 3c79e4ecfb..7b32ed541d 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/cm.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/cm.coffee @@ -3,7 +3,7 @@ # It is heavily inspired from the Ace editor hook. # Convert a CodeMirror delta into an op understood by share.js -applyToShareJS = (editorDoc, delta, doc) -> +applyCMToShareJS = (editorDoc, delta, doc) -> # CodeMirror deltas give a text replacement. # I tuned this operation a little bit, for speed. startPos = 0 # Get character position from # of chars in each line. @@ -26,7 +26,7 @@ applyToShareJS = (editorDoc, delta, doc) -> doc.del startPos, delLen doc.insert startPos, delta.text.join '\n' if delta.text - applyToShareJS editorDoc, delta.next, doc if delta.next + applyCMToShareJS editorDoc, delta.next, doc if delta.next # Attach a CodeMirror editor to the document. The editor's contents are replaced # with the document's contents unless keepEditorContents is true. (In which case @@ -39,14 +39,14 @@ window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) -> check = -> window.setTimeout -> editorText = editor.getValue() - otText = sharedoc.getValue() + otText = sharedoc.getText() if editorText != otText console.error "Text does not match!" console.error "editor: #{editorText}" console.error "ot: #{otText}" # Replace the editor text with the doc snapshot. - editor.setValue sharedoc.getValue() + editor.setValue sharedoc.getText() , 0 if keepEditorContents @@ -64,11 +64,11 @@ window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) -> # Listen for edits in CodeMirror. editorListener = (ed, change) -> return if suppress - applyToShareJS editor, change, sharedoc + applyCMToShareJS editor, change, sharedoc check() - editor.setOption 'onChange', editorListener + editor.on 'change', editorListener @on 'insert', (pos, text) -> suppress = true @@ -87,7 +87,7 @@ window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) -> @detach_cm = -> # TODO: can we remove the insert and delete event callbacks? - editor.setOption 'onChange', null + editor.off 'onChange', editorListener delete @detach_cm return From 7783e18a504b703d23ab1faadd1d9172debebd6d Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 2 Feb 2018 17:31:47 +0000 Subject: [PATCH 4/9] Allow Documents to be attached to CM --- .../public/coffee/ide/editor/Document.coffee | 48 ++++++++++++------- .../coffee/ide/editor/ShareJsDoc.coffee | 3 ++ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index b6ea177fc8..afdd924588 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -26,7 +26,8 @@ define [ @connected = @ide.socket.socket.connected @joined = false @wantToBeJoined = false - @_checkConsistency = _.bind(@_checkConsistency, @) + @_checkAceConsistency = _.bind(@_checkConsistency, @, @ace) + @_checkCMConsistency = _.bind(@_checkConsistency, @, @cm) @inconsistentCount = 0 @_bindToEditorEvents() @_bindToSocketEvents() @@ -34,32 +35,43 @@ define [ attachToAce: (@ace) -> @doc?.attachToAce(@ace) editorDoc = @ace.getSession().getDocument() - editorDoc.on "change", @_checkConsistency + editorDoc.on "change", @_checkAceConsistency @ide.$scope.$emit 'document:opened', @doc detachFromAce: () -> @doc?.detachFromAce() editorDoc = @ace?.getSession().getDocument() - editorDoc?.off "change", @_checkConsistency + editorDoc?.off "change", @_checkAceConsistency @ide.$scope.$emit 'document:closed', @doc - + + attachToCM: (@cm) -> + @doc?.attachToCM(@cm) + @cm?.on "change", @_checkCMConsistency + @ide.$scope.$emit 'document:opened', @doc + + detachFromCM: () -> + @doc?.detachFromCM() + @cm?.off "change", @_checkCMConsistency + @ide.$scope.$emit 'document:closed', @doc + submitOp: (args...) -> @doc?.submitOp(args...) - _checkConsistency: () -> - # We've been seeing a lot of errors when I think there shouldn't be - # any, which may be related to this check happening before the change is - # applied. If we use a timeout, hopefully we can reduce this. - setTimeout () => - editorValue = @ace?.getValue() - sharejsValue = @doc?.getSnapshot() - if editorValue != sharejsValue - @inconsistentCount++ - else - @inconsistentCount = 0 + _checkConsistency: (editor) -> + return () => + # We've been seeing a lot of errors when I think there shouldn't be + # any, which may be related to this check happening before the change is + # applied. If we use a timeout, hopefully we can reduce this. + setTimeout () => + editorValue = editor?.getValue() + sharejsValue = @doc?.getSnapshot() + if editorValue != sharejsValue + @inconsistentCount++ + else + @inconsistentCount = 0 - if @inconsistentCount >= 3 - @_onError new Error("Editor text does not match server text") - , 0 + if @inconsistentCount >= 3 + @_onError new Error("Editor text does not match server text") + , 0 getSnapshot: () -> @doc?.getSnapshot() diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index 569a6c7a9f..63fca95856 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -120,6 +120,9 @@ define [ attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength) detachFromAce: () -> @_doc.detach_ace?() + attachToCM: (cm) -> @_doc.attach_cm(cm, false) + detachFromCM: () -> @_doc.detach_cm?() + INFLIGHT_OP_TIMEOUT: 5000 # Retry sending ops after 5 seconds without an ack WAIT_FOR_CONNECTION_TIMEOUT: 500 # If we're waiting for the project to join, try again in 0.5 seconds _startInflightOpTimeout: (update) -> From b9327cb2d4302090865c693abb4be4637c82f2a4 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 19 Jan 2018 16:28:56 +0000 Subject: [PATCH 5/9] Add toolbar with button to toggle rich text --- services/web/app/views/project/editor/editor.pug | 11 ++++++++++- .../web/public/coffee/ide/editor/EditorManager.coffee | 7 +++++++ .../editor/controllers/EditorToolbarController.coffee | 7 +++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 services/web/public/coffee/ide/editor/controllers/EditorToolbarController.coffee diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 525d1034ca..69864374f9 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -30,9 +30,12 @@ div.full-size( i.fa.fa-arrow-left |   #{translate("open_a_file_on_the_left")} + .toolbar.toolbar-editor(ng-controller="EditorToolbarController") + button(ng-click="toggleRichText()") Rich Text + #editor( ace-editor="editor", - ng-show="!!editor.sharejs_doc && !editor.opening" + ng-if="!!editor.sharejs_doc && !editor.opening && !editor.richText" theme="settings.theme", keybindings="settings.mode", font-size="settings.fontSize", @@ -63,6 +66,12 @@ div.full-size( renderer-data="reviewPanel.rendererData" ) + #editor-rich-text( + cm-editor + ng-if="!editor.opening && editor.richText" + sharejs-doc="editor.sharejs_doc" + ) + include ./review-panel .ui-layout-east diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index bf752ccffb..2b08605b9b 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -1,7 +1,9 @@ define [ "ide/editor/Document" "ide/editor/directives/aceEditor" + "ide/editor/directives/cmEditor" "ide/editor/controllers/SavingNotificationController" + "ide/editor/controllers/EditorToolbarController" ], (Document) -> class EditorManager constructor: (@ide, @$scope) -> @@ -12,6 +14,7 @@ define [ opening: true trackChanges: false wantTrackChanges: false + richText: false } @$scope.$on "entity:selected", (event, entity) => @@ -186,3 +189,7 @@ define [ @$scope.editor.trackChanges = want else @_syncTimeout = setTimeout tryToggle, 100 + + toggleRichText: () -> + @$scope.editor.richText = !@$scope.editor.richText + diff --git a/services/web/public/coffee/ide/editor/controllers/EditorToolbarController.coffee b/services/web/public/coffee/ide/editor/controllers/EditorToolbarController.coffee new file mode 100644 index 0000000000..9649899ba8 --- /dev/null +++ b/services/web/public/coffee/ide/editor/controllers/EditorToolbarController.coffee @@ -0,0 +1,7 @@ +define [ + "base" + "ide/editor/Document" +], (App, Document) -> + App.controller "EditorToolbarController", ($scope, ide) -> + $scope.toggleRichText = () -> + ide.editorManager.toggleRichText() From d15efc6fb6ae495c4f62a27753c0a2e2324d5d8b Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 19 Jan 2018 16:28:56 +0000 Subject: [PATCH 6/9] Create cmEditor directive to connect to ShareJS --- .../ide/editor/directives/aceEditor.coffee | 3 ++ .../ide/editor/directives/cmEditor.coffee | 32 +++++++++++++++++++ .../web/public/stylesheets/app/editor.less | 11 ++++++- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 services/web/public/coffee/ide/editor/directives/cmEditor.coffee diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 3b43a05afd..989bd75127 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -428,6 +428,9 @@ define [ if rendererData? rendererData.lineHeight = editor.renderer.lineHeight + scope.$on '$destroy', () -> + detachFromAce(scope.sharejsDoc) + template: """
+ App.directive "cmEditor", () -> + return { + scope: { + sharejsDoc: "=" + } + + link: (scope, element, attrs) -> + cm = Frontend.richText.init(element.find('.cm-editor-wrapper')[0]) + + scope.$watch "sharejsDoc", (sharejsDoc, oldSharejsDoc) -> + if oldSharejsDoc? + detachFromCM(oldSharejsDoc) + if sharejsDoc? + attachToCM(sharejsDoc) + + attachToCM = (sharejsDoc) -> + cm.setValue(sharejsDoc.getSnapshot()) + sharejsDoc.attachToCM(cm) + + detachFromCM = (sharejsDoc) -> + sharejsDoc.detachFromCM() + + scope.$on 'destroy', () -> + detachFromCM(scope.sharejsDoc) + + template: """ +
+ """ + } \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 36084cd2a5..54fa3b97c1 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -75,8 +75,9 @@ top: 40px; } -#editor { +#editor, #editor-rich-text { .full-size; + top: 40px; // TODO: replace with toolbar height var } .loading-screen { @@ -190,6 +191,14 @@ } } +.cm-editor-wrapper { + height: 100%; + + .CodeMirror { + height: 100%; + } +} + // The internal components of the aceEditor directive .ace-editor-wrapper { .full-size; From 4c23a3c8f088ea363e340fa9ddb69f772ab6bf12 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 5 Feb 2018 11:49:25 +0000 Subject: [PATCH 7/9] Fix bug where spell check cache would error when toggling between ace & cm --- .../web/public/coffee/ide/editor/directives/aceEditor.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 989bd75127..716fd2ee9d 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -97,7 +97,7 @@ define [ scope.name = attrs.aceEditor if scope.spellCheck # only enable spellcheck when explicitly required - spellCheckCache = $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000}) + spellCheckCache = $cacheFactory.get("spellCheck-#{scope.name}") || $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000}) spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache, $http, $q) undoManager = new UndoManager(scope, editor, element) highlightsManager = new HighlightsManager(scope, editor, element) @@ -106,7 +106,6 @@ define [ metadataManager = new MetadataManager(scope, editor, element, metadata) autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files) - # Prevert Ctrl|Cmd-S from triggering save dialog editor.commands.addCommand name: "save", From bfda9c595b60e845f3c8bd95a6d24c8fad945ed7 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 5 Feb 2018 15:35:00 +0000 Subject: [PATCH 8/9] Hide rich text behind feature flag --- .../app/coffee/infrastructure/Features.coffee | 2 ++ services/web/app/views/project/editor.pug | 9 +++++---- services/web/app/views/project/editor/editor.pug | 16 +++++++++------- .../coffee/ide/editor/EditorManager.coffee | 1 - 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index a49e0ec124..6e39ee6370 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -14,5 +14,7 @@ module.exports = Features = return Settings.enableGithubSync when 'v1-return-message' return Settings.accountMerge? and Settings.overleaf? + when 'rich-text' + return Settings.showRichText else throw new Error("unknown feature: #{feature}") diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 58c56bdce8..88ca3364b8 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -159,10 +159,11 @@ block requirejs window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); // TODO: inject based on environment - script( - src="http://localhost:3809/public/js/es/richText.js" - type="text/javascript" - ) + if hasFeature('rich-text') + script( + src="http://localhost:3809/public/js/es/richText.js" + type="text/javascript" + ) script( data-main=buildJsPath("ide.js", {hashedPath:false}), diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 69864374f9..d1e2938714 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -30,8 +30,9 @@ div.full-size( i.fa.fa-arrow-left |   #{translate("open_a_file_on_the_left")} - .toolbar.toolbar-editor(ng-controller="EditorToolbarController") - button(ng-click="toggleRichText()") Rich Text + if hasFeature('rich-text') + .toolbar.toolbar-editor(ng-controller="EditorToolbarController") + button(ng-click="toggleRichText()") Rich Text #editor( ace-editor="editor", @@ -66,11 +67,12 @@ div.full-size( renderer-data="reviewPanel.rendererData" ) - #editor-rich-text( - cm-editor - ng-if="!editor.opening && editor.richText" - sharejs-doc="editor.sharejs_doc" - ) + if hasFeature('rich-text') + #editor-rich-text( + cm-editor + ng-if="!editor.opening && editor.richText" + sharejs-doc="editor.sharejs_doc" + ) include ./review-panel diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index 2b08605b9b..9c4d74e76f 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -192,4 +192,3 @@ define [ toggleRichText: () -> @$scope.editor.richText = !@$scope.editor.richText - From b7200066e91dad35860482988eabc200ece9fdf6 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 6 Feb 2018 14:04:28 +0000 Subject: [PATCH 9/9] Inject rich text variable to conditionally show rich text styles --- services/web/Gruntfile.coffee | 3 +++ services/web/public/stylesheets/_style_includes.less | 9 ++++++--- services/web/public/stylesheets/app/editor.less | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index e4478a9fb7..4104352e3d 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -1,5 +1,6 @@ fs = require "fs" PackageVersions = require "./app/coffee/infrastructure/PackageVersions" +Settings = require "settings-sharelatex" require('es6-promise').polyfill() module.exports = (grunt) -> @@ -153,6 +154,7 @@ module.exports = (grunt) -> sourceMapBasepath: "public/stylesheets" globalVars: 'is-overleaf': false + 'show-rich-text': Settings.showRichText files: "public/stylesheets/style.css": "public/stylesheets/style.less" ol: @@ -162,6 +164,7 @@ module.exports = (grunt) -> sourceMapBasepath: "public/stylesheets" globalVars: 'is-overleaf': true + 'show-rich-text': Settings.showRichText files: "public/stylesheets/ol-style.css": "public/stylesheets/ol-style.less" diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less index 1c27a37014..de6c4ff7fe 100644 --- a/services/web/public/stylesheets/_style_includes.less +++ b/services/web/public/stylesheets/_style_includes.less @@ -48,9 +48,6 @@ @import "components/popovers.less"; @import "components/carousel.less"; -// CodeMirror -@import "vendor/codemirror.css"; - // ngTagsInput @import "components/tags-input.less"; @@ -83,6 +80,12 @@ @import "app/error-pages.less"; @import "app/v1-badge.less"; +// Vendor CSS @import "../js/libs/pdfListView/TextLayer.css"; @import "../js/libs/pdfListView/AnnotationsLayer.css"; @import "../js/libs/pdfListView/HighlightsLayer.css"; + +// CodeMirror +& when (@show-rich-text) { + @import "vendor/codemirror.css"; +} diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 54fa3b97c1..fa39552c27 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -77,6 +77,12 @@ #editor, #editor-rich-text { .full-size; +} + +#editor when (@show-rich-text = true) { + top: 40px; // TODO: replace with toolbar height var +} +#editor-rich-text when (@show-rich-text = true) { top: 40px; // TODO: replace with toolbar height var }