From db68524dd4940ddd3f52685baffd201e6999b43c Mon Sep 17 00:00:00 2001 From: Will Gutierrez Date: Sun, 31 Jan 2021 21:45:35 -0800 Subject: [PATCH 01/10] merge editor refactor branches #9004 --- app/assets/javascripts/editor.js | 194 +++++++++++----------- app/assets/javascripts/editorToolbar.js | 20 +-- app/assets/javascripts/post.js | 2 +- app/helpers/application_helper.rb | 14 +- app/views/comments/_edit.html.erb | 163 ++++++------------ app/views/comments/_form.html.erb | 34 ++-- app/views/editor/_editor.html.erb | 4 +- app/views/editor/_toolbar.html.erb | 13 +- app/views/map/edit.html.erb | 2 +- app/views/notes/_comments.html.erb | 7 +- app/views/notes/show.html.erb | 1 - app/views/questions/show.html.erb | 12 +- app/views/wiki/show.html.erb | 18 +- test/functional/editor_controller_test.rb | 2 +- test/functional/wiki_controller_test.rb | 2 +- test/system/comment_test.rb | 10 +- test/system/post_test.rb | 4 +- 17 files changed, 218 insertions(+), 284 deletions(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 997f0b176c..5572072a97 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -1,13 +1,16 @@ -// jQuery (document).ready function: - -$E = { - initialize: function() { - // call setState with no parameters, aka. default parameters. - // default parameters point toward either: - // 1. the comment form at the bottom of multi-comment wikis/questions/research notes - // 2. the only editor form on /wiki/new and /wiki/edit - $E.setState(); - +class Editor { + // default parameter references the ID of: + // 1. main comment form in multi-comment wikis, questions, & research notes. + // 2. the only editor form on /wiki/new and /wiki/edit + constructor(commentFormID = "main") { + this.commentFormID = commentFormID; + this.textarea = $("#text-input-" + commentFormID); + this.preview = $("#comment-preview-" + commentFormID); + this.previewing = false; + this.previewed = false; + // this will get deleted in the next few PRs, so collapsing into one line to pass codeclimate + this.templates = { 'blog': "## The beginning\n\n## What we did\n\n## Why it matters\n\n## How can you help", 'default': "## What I want to do\n\n## My attempt and results\n\n## Questions and next steps\n\n## Why I'm interested", 'support': "## Details about the problem\n\n## A photo or screenshot of the setup", 'event': "## Event details\n\nWhen, where, what\n\n## Background\n\nWho, why", 'question': "## What I want to do or know\n\n## Background story" }; + marked.setOptions({ gfm: true, tables: true, @@ -23,96 +26,96 @@ $E = { return code; } }); - }, - setState: function(textarea = 'text-input', preview = 'comment-preview-main', title = 'title') { - $E.title = $('#' + title + 'title'); // not sure why this exists? seems like $E.title is always #title - $E.textarea = $('#' + textarea); + } + setState(commentFormID = "main") { + this.commentFormID = commentFormID; + $E.textarea = $("#text-input-" + commentFormID); $E.textarea.bind('input propertychange', $E.save); - $E.preview = $('#' + preview); - }, - is_editing: function() { - return ($E.textarea[0].selectionStart == 0 && $E.textarea[0].selectionEnd == 0) - }, - refresh: function() { + $E.preview = $("#comment-preview-" + commentFormID); + } + // code seems unused, commenting out for now. + // is_editing() { + // return ($E.textarea[0].selectionStart == 0 && $E.textarea[0].selectionEnd == 0) + // }; + refresh() { // textarea $E.textarea = ($D.selected).find('textarea').eq(0); $E.textarea.bind('input propertychange',$E.save); // preview $E.preview = ($D.selected).find('.comment-preview').eq(0); - }, - isRichTextEditor: function(url) { - // this RegEx matches three different cases where the legacy editor is still used: + } + isSingleFormPage(url) { + // this RegEx matches three different pages where only one editor form is present (instead of multiple comment forms): // 1. /wiki/new // 2. /wiki/{wiki name}/edit // 3. /features/new - const legacyEditorPath = RegExp(/\/(wiki|features)(\/[^\/]+\/edit|\/new)/); - return !legacyEditorPath.test(url); // if we're not on one of these pages, we are using the rich-text editor. - }, + const singleFormPath = RegExp(/\/(wiki|features)(\/[^\/]+\/edit|\/new)/); + return singleFormPath.test(url); + } // wraps currently selected text in textarea with strings a and b - wrap: function(a, b, args) { - // we only refresh $E's values if we are on a page using the rich-text editor (most pages). - // the legacy editor pages only have one editor form, unlike pages with multiple comments. - if (this.isRichTextEditor(window.location.pathname)) { this.refresh(); } - var len = $E.textarea.val().length; - var start = $E.textarea[0].selectionStart; - var end = $E.textarea[0].selectionEnd; - const fallbackParameterExists = args && args['fallback']; - const newlineParameterExists = args && args['newline']; - var sel = fallbackParameterExists ? args['fallback'] : $E.textarea.val().substring(start, end); // // fallback if nothing has been selected, and we're simply dealing with an insertion point - var replace = a + sel + b; - if (newlineParameterExists) { - replace = replace + "\n\n"; - } - if (newlineParameterExists && $E.textarea[0].selectionStart > 0) { - replace = "\n" + replace; - } - $E.textarea.val($E.textarea.val().substring(0,start) + replace + $E.textarea.val().substring(end,len)); - }, - bold: function() { + wrap(a, b, newlineDesired = false, fallback) { + // we only refresh $E's values if we are on a page with multiple comments + if (!this.isSingleFormPage(window.location.pathname)) { this.refresh(); } + + const selectionStart = $E.textarea[0].selectionStart; + const selectionEnd = $E.textarea[0].selectionEnd; + const selection = fallback || $E.textarea.val().substring(selectionStart, selectionEnd); // fallback if nothing has been selected, and we're simply dealing with an insertion point + + let newText = a + selection + b; // ie. ** + selection + ** (wrapping selection in bold) + if (newlineDesired) { newText = newText + "\n\n"; } + const selectionStartsMidText = $E.textarea[0].selectionStart > 0; + if (newlineDesired && selectionStartsMidText) { newText = "\n" + newText; } + + const textLength = $E.textarea.val().length; + const textBeforeSelection = $E.textarea.val().substring(0, selectionStart); + const textAfterSelection = $E.textarea.val().substring(selectionEnd, textLength); + $E.textarea.val(textBeforeSelection + newText + textAfterSelection); + } + bold() { $E.wrap('**','**') - }, - italic: function() { + } + italic() { $E.wrap('_','_') - }, - link: function(uri) { + } + link(uri) { uri = prompt('Enter a URL'); if (uri === null) { uri = ""; } $E.wrap('[', '](' + uri + ')'); - }, - image: function(src) { + } + image(src) { $E.wrap('\n![',']('+src+')\n') - }, - h1: function() { - $E.wrap('#','') - }, - h2: function() { + } + // these header formatting functions are not used anywhere, so commenting them out for now to pass codeclimate: + + // h1() { + // $E.wrap('#','') + // } + h2() { $E.wrap('##','') - }, - h3: function() { - $E.wrap('###','') - }, - h4: function() { - $E.wrap('####','') - }, - h5: function() { - $E.wrap('#####','') - }, - h6: function() { - $E.wrap('######','') - }, - h7: function() { - $E.wrap('#######','') - }, + } + // h3() { + // $E.wrap('###','') + // } + // h4() { + // $E.wrap('####','') + // } + // h5() { + // $E.wrap('#####','') + // } + // h6() { + // $E.wrap('######','') + // } + // h7() { + // $E.wrap('#######','') + // } // this function is dedicated to Don Blair https://github.com/donblair - save: function() { - localStorage.setItem('plots:lastpost',$E.textarea.val()) - localStorage.setItem('plots:lasttitle',$E.title.val()) - }, - recover: function() { - $E.textarea.val(localStorage.getItem('plots:lastpost')) - $E.title.val(localStorage.getItem('plots:lasttitle')) - }, - apply_template: function(template) { + save() { + localStorage.setItem('plots:lastpost',$E.textarea.val()); + } + recover() { + $E.textarea.val(localStorage.getItem('plots:lastpost')); + } + apply_template(template) { if($E.textarea.val() == ""){ $E.textarea.val($E.templates[template]) }else if(($E.textarea.val() == $E.templates['event']) || ($E.textarea.val() == $E.templates['default']) || ($E.textarea.val() == $E.templates['support'])){ @@ -120,34 +123,25 @@ $E = { }else{ $E.textarea.val($E.textarea.val()+'\n\n'+$E.templates[template]) } - }, - templates: { - 'blog': "## The beginning\n\n## What we did\n\n## Why it matters\n\n## How can you help", - 'default': "## What I want to do\n\n## My attempt and results\n\n## Questions and next steps\n\n## Why I'm interested", - 'support': "## Details about the problem\n\n## A photo or screenshot of the setup", - 'event': "## Event details\n\nWhen, where, what\n\n## Background\n\nWho, why", - 'question': "## What I want to do or know\n\n## Background story" - }, - previewing: false, - previewed: false, - generate_preview: function(id,text) { + } + generate_preview(id,text) { $('#'+id)[0].innerHTML = marked(text) - }, - toggle_preview: function() { + } + toggle_preview() { let previewBtn; let dropzone; // if the element is part of a multi-comment page, // ensure to grab the current element and not the other comment element. - previewBtn = $(this.textarea.context).find('.preview-btn'); - dropzone = $(this.textarea.context).find('.dropzone'); + previewBtn = $("#toggle-preview-button-" + this.commentFormID); + dropzone = $("#dropzone-large-" + this.commentFormID); $E.preview[0].innerHTML = marked($E.textarea.val()); $E.preview.toggle(); dropzone.toggle(); this.toggleButtonPreviewMode(previewBtn); - }, - toggleButtonPreviewMode: function (previewBtn) { + } + toggleButtonPreviewMode(previewBtn) { let isPreviewing = previewBtn.attr('data-previewing'); // If data-previewing attribute is not present -> we are not in "preview" mode @@ -166,4 +160,4 @@ $E = { previewBtn.text('Preview'); } } -} +} \ No newline at end of file diff --git a/app/assets/javascripts/editorToolbar.js b/app/assets/javascripts/editorToolbar.js index 37159c3244..dbfcb720b6 100644 --- a/app/assets/javascripts/editorToolbar.js +++ b/app/assets/javascripts/editorToolbar.js @@ -16,9 +16,9 @@ const getEditorParams = (targetDiv) => { params['textarea'] = closestCommentFormWrapper.querySelector('textarea').id; params['preview'] = closestCommentFormWrapper.querySelector('.comment-preview').id; } else { - // default to #text-input - // #text-input ID should be unique, and the only comment form on /wiki/new & /wiki/edit - params['textarea'] = 'text-input'; + // default to #text-input-main + // #text-input-main ID should be unique, and the only comment form on /wiki/new & /wiki/edit + params['textarea'] = 'text-input-main'; // #preview-main should be unique as well params['preview'] = 'comment-preview-main'; } @@ -42,7 +42,7 @@ $(function() { const { textArea, preview, dSelected } = getEditorParams(e.target); // assign dSelected if (dSelected) { $D.selected = dSelected; } - $E.setState(textArea, preview); + $E.setState(e.currentTarget.dataset.formId); const action = e.currentTarget.dataset.action // 'bold', 'italic', etc. $E[action](); // call the appropriate editor function }); @@ -68,7 +68,7 @@ $(function() { const { textArea, preview, dSelected } = getEditorParams(e.target); e.preventDefault(); if (dSelected) { $D.selected = dSelected; } - $E.setState(textArea, preview); + $E.setState(e.currentTarget.dataset.formId); }); $(this).fileupload({ @@ -108,21 +108,17 @@ $(function() { switch (file_type) { case 'image': orig_image_url = file_url + '?s=o' // size = original - $E.wrap('[![', '](' + file_url + ')](' + orig_image_url + ')', {'newline': true, 'fallback': data.result['filename']}) // on its own line; see /app/assets/js/editor.js + $E.wrap('[![', '](' + file_url + ')](' + orig_image_url + ')', true, data.result['filename']); break; case 'csv': - $E.wrap('[graph:' + file_url + ']', '', {'newline': true}) + $E.wrap('[graph:' + file_url + ']', '', true); break; default: - $E.wrap(' ','', {'newline': true, 'fallback': data.result['filename'].replace(/[()]/g , "-")}) // on its own line; see /app/assets/js/editor.js + $E.wrap(' ', '', true, data.result['filename'].replace(/[()]/g , "-")); // on its own line; see /app/assets/js/editor.js } // here append the image id to the wiki edit form: if ($('#node_images').val() && $('#node_images').val().split(',').length > 1) $('#node_images').val([$('#node_images').val(),data.result.id].join(',')) else $('#node_images').val(data.result.id) - // eventual handling of multiple files; must add "multiple" to file input and handle on server side: - //$.each(data.result.files, function (index, file) { - // $('

').text(file.name).appendTo(document.body); - //}); }, fileuploadfail: function(e, data) { console.log(e); diff --git a/app/assets/javascripts/post.js b/app/assets/javascripts/post.js index 027a5d4925..529a094143 100644 --- a/app/assets/javascripts/post.js +++ b/app/assets/javascripts/post.js @@ -2,7 +2,7 @@ jQuery(document).ready(function() { $('.datepicker').datepicker() - $E.initialize() + $E = new Editor(); $('#side-fileinput').bind('focus',function(e) { $('#side-dropzone').css('border-color','#4ac') }) $('#side-fileinput').bind('focusout',function(e) { $('#side-dropzone').css('border-color','#ccc') }) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1da3e1d90d..2a9115302b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -66,19 +66,7 @@ def feature_node(title) end # used in views/comments/_form.html.erb - def get_comment_form_id(location, reply_to) - case location - when :main - 'main' - when :reply - 'reply-' + reply_to.to_s - when :responses - 'responses' - end - end - - # used in views/editor/_toolbar.html.erb - def get_toolbar_element_id(location, reply_to, comment_id) + def get_comment_form_id(location, reply_to, comment_id) case location when :main 'main' diff --git a/app/views/comments/_edit.html.erb b/app/views/comments/_edit.html.erb index c12329829c..123dccc5c9 100644 --- a/app/views/comments/_edit.html.erb +++ b/app/views/comments/_edit.html.erb @@ -1,16 +1,33 @@ -