From 5dc442c61c52c59730750cb03cec71ade2595c93 Mon Sep 17 00:00:00 2001 From: Will Gutierrez Date: Mon, 1 Feb 2021 12:07:54 -0800 Subject: [PATCH 1/8] merge editor refactor branches #9004 --- app/assets/javascripts/editor.js | 188 +++++++++----------- app/assets/javascripts/editorToolbar.js | 69 ++------ app/assets/javascripts/post.js | 2 +- app/helpers/application_helper.rb | 14 +- app/views/comments/_edit.html.erb | 204 ++++++++-------------- app/views/comments/_form.html.erb | 54 +++--- 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 | 14 +- test/system/post_test.rb | 4 +- 17 files changed, 255 insertions(+), 355 deletions(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 997f0b176c..70393b00ca 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,74 @@ $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() { - // 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: - // 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. - }, + $E.preview = $("#comment-preview-" + commentFormID); + } // 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) { + 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 +101,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 +138,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..870e26083d 100644 --- a/app/assets/javascripts/editorToolbar.js +++ b/app/assets/javascripts/editorToolbar.js @@ -1,29 +1,9 @@ -// this script is used in a variety of different contexts including: +// this script is used wherever the legacy editor is used. // pages (wikis, questions, research notes) with multiple comments & editors for each comment // pages with JUST ONE form, and no other comments, eg. /wiki/new & /wiki/edit // /app/views/features/_form.html.erb // /app/views/map/edit.html.erb -// the legacy editor: /app/views/editor/_editor.html.erb (if it's still in use live?) - -const getEditorParams = (targetDiv) => { - const closestCommentFormWrapper = targetDiv.closest('div.comment-form-wrapper'); // this returns null if there is no match - let params = {}; - // there are no .comment-form-wrappers on /wiki/edit or /wiki/new - // these pages just have a single text-input form. - if (closestCommentFormWrapper) { - params['dSelected'] = $(closestCommentFormWrapper); - // assign the ID of the textarea within the closest comment-form-wrapper - 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'; - // #preview-main should be unique as well - params['preview'] = 'comment-preview-main'; - } - return params; -}; +// and wherever /app/views/editor/editor.html.erb is still used in production const progressAll = (elem, data) => { var progress = parseInt(data.loaded / data.total * 100, 10); @@ -36,13 +16,8 @@ const progressAll = (elem, data) => { // attach eventListeners on document.load for toolbar rich-text buttons & image upload .dropzones $(function() { // for rich-text buttons (bold, italic, header, and link): - // click eventHandler that assigns $D.selected to the appropriate comment form - // on pages with multiple comments, $D.selected needs to be accurate so that rich-text changes (bold, italic, etc.) go into the right comment form $('.rich-text-button').on('click', function(e) { - const { textArea, preview, dSelected } = getEditorParams(e.target); - // assign dSelected - if (dSelected) { $D.selected = dSelected; } - $E.setState(textArea, preview); + $E.setState(e.currentTarget.dataset.formId); // string that is: "main", "reply-123", "edit-123" etc. const action = e.currentTarget.dataset.action // 'bold', 'italic', etc. $E[action](); // call the appropriate editor function }); @@ -65,10 +40,8 @@ $(function() { // runs on drag & drop $(this).on('drop',function(e) { - const { textArea, preview, dSelected } = getEditorParams(e.target); e.preventDefault(); - if (dSelected) { $D.selected = dSelected; } - $E.setState(textArea, preview); + $E.setState(e.currentTarget.dataset.formId); // string that is: "main", "reply-123", "edit-123" etc. }); $(this).fileupload({ @@ -84,22 +57,18 @@ $(function() { // 1. when user drag-and-drops image // 2. when user clicks on upload button. start: function(e) { + $E.setState(e.target.dataset.formId); // string that is: "main", "reply-123", "edit-123" etc. $(e.target).removeClass('hover'); - // for click-upload-button scenarios, it's important to set $D.selected here, because the 'drop' listener above doesn't run in those: - $D.selected = $(e.target).closest('div.comment-form-wrapper'); - // the above line is redundant in drag & drop, because it's assigned in 'drop' listener too. - // on /wiki/new & /wiki/edit, $D.selected will = undefined from this assignment - elem = $($D.selected).closest('div.comment-form-wrapper').eq(0); - elem.find('.progress-bar-container').eq(0).show(); - elem.find('.uploading-text').eq(0).show(); - elem.find('.choose-one-prompt-text').eq(0).hide(); + console.log($("#image-upload-progress-container-" + $E.commentFormID)); + $("#image-upload-progress-container-" + $E.commentFormID).show(); + $("#image-upload-text-" + $E.commentFormID).show(); + $("#dropzone-choose-one-" + $E.commentFormID).hide(); }, done: function (e, data) { - elem = $($D.selected).closest('div.comment-form-wrapper').eq(0); - elem.find('.progress-bar-container').hide(); - elem.find('.progress-bar').css('width', 0); - elem.find('.uploading-text').hide(); - elem.find('.choose-one-prompt-text').show(); + $("#image-upload-progress-container-" + $E.commentFormID).hide(); + $("#image-upload-text-" + $E.commentFormID).hide(); + $("#dropzone-choose-one-" + $E.commentFormID).show(); + $("#image-upload-progress-bar-" + $E.commentFormID).css('width', 0); var extension = data.result['filename'].split('.')[data.result['filename'].split('.').length - 1]; var file_url = data.result.url.split('?')[0]; var file_type; if (['gif', 'GIF', 'jpeg', 'JPEG', 'jpg', 'JPG', 'png', 'PNG'].includes(extension)) file_type = 'image' @@ -108,27 +77,23 @@ $(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); }, progressall: function (e, data) { - const closestProgressBar = $($D.selected).closest('div.comment-form-wrapper').find('.progress-bar').eq(0); + const closestProgressBar = $("#image-upload-progress-bar-" + $E.commentFormID); return progressAll(closestProgressBar, data); } }); 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..d18c36385e 100644 --- a/app/views/comments/_edit.html.erb +++ b/app/views/comments/_edit.html.erb @@ -1,155 +1,103 @@ -