Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit def0a4b

Browse files
authoredDec 23, 2020
Merge pull request #3 from topcoder-platform/issue-303
Issues:308: supporting old and new formats
2 parents a38d3f5 + cd0c71b commit def0a4b

File tree

3 files changed

+178
-97
lines changed

3 files changed

+178
-97
lines changed
 

‎TopcoderEditorPlugin.php

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
<?php
22

3+
if (!class_exists('League\HTMLToMarkdown\HtmlConverter')){
4+
require __DIR__ . '/vendor/autoload.php';
5+
}
6+
7+
38
use Vanilla\Formatting\Formats\MarkdownFormat;
49
use \Vanilla\Formatting\Formats;
10+
use Vanilla\Formatting\Formats\RichFormat;
11+
use League\HTMLToMarkdown\HtmlConverter;
12+
513

614
/**
715
* Plugin class for the Topcoder Editor
@@ -74,16 +82,11 @@ private function beforeRender($sender){
7482
$sender->addJsFile('topcodereditor.js', 'plugins/TopcoderEditor');
7583
$c = Gdn::controller();
7684

77-
// Set definitions for JavaScript to read
78-
$c->addDefinition('editorVersion', $this->pluginInfo['Version']);
79-
$c->addDefinition('editorInputFormat', $this->Format);
80-
$c->addDefinition('editorPluginAssets', $this->AssetPath);
81-
82-
$additionalDefinitions = [];
83-
$this->EventArguments['definitions'] = &$additionalDefinitions;
84-
$this->fireEvent('GetJSDefinitions');
85+
// Set formats
86+
$c->addDefinition('defaultInputFormat', c('Garden.InputFormatter'));
87+
$c->addDefinition('defaultMobileInputFormat', c('Garden.MobileInputFormatter'));
8588

86-
// Set variables for file uploads
89+
// Set file uploads vars
8790
$postMaxSize = Gdn_Upload::unformatFileSize(ini_get('post_max_size'));
8891
$fileMaxSize = Gdn_Upload::unformatFileSize(ini_get('upload_max_filesize'));
8992
$configMaxSize = Gdn_Upload::unformatFileSize(c('Garden.Upload.MaxFileSize', '1MB'));
@@ -99,6 +102,16 @@ private function beforeRender($sender){
99102
$c->addDefinition('allowedFileExtensions', json_encode($allowedFileExtensions));
100103
// Get max file uploads, to be used for max drops at once.
101104
$c->addDefinition('maxFileUploads', ini_get('max_file_uploads'));
105+
106+
// Set editor definitions
107+
$c->addDefinition('editorVersion', $this->pluginInfo['Version']);
108+
$c->addDefinition('editorInputFormat', ucfirst(self::FORMAT_NAME));
109+
$c->addDefinition('editorPluginAssets', $this->AssetPath);
110+
111+
$additionalDefinitions = [];
112+
$this->EventArguments['definitions'] = &$additionalDefinitions;
113+
$this->fireEvent('GetJSDefinitions');
114+
102115
}
103116

104117
/**
@@ -141,6 +154,19 @@ public function isFormMarkDown(Gdn_Form $form): bool {
141154
return strcasecmp($format, MarkdownFormat::FORMAT_KEY) === 0;
142155
}
143156

157+
/**
158+
* Check to see if we should be using the Topcoder Editor
159+
*
160+
* @param Gdn_Form $form - A form instance.
161+
*
162+
* @return bool
163+
*/
164+
public function isFormWysiwyg(Gdn_Form $form): bool {
165+
$data = $form->formData();
166+
$format = $data['Format'] ?? null;
167+
return strcasecmp($format, Vanilla\Formatting\Formats\WysiwygFormat::FORMAT_KEY) === 0;
168+
}
169+
144170
public function isInputFormatterMarkDown(): bool {
145171
return strcasecmp(Gdn_Format::defaultFormat(), MarkdownFormat::FORMAT_KEY) === 0;
146172
}
@@ -157,6 +183,17 @@ public function getPostFormats_handler(array $postFormats): array {
157183
return $postFormats;
158184
}
159185

186+
public function postController_beforeEditDiscussion_handler($sender, $args) {
187+
$discussion = &$args['Discussion'];
188+
if($discussion) {
189+
if (strcasecmp($discussion->Format, Vanilla\Formatting\Formats\WysiwygFormat::FORMAT_KEY) === 0) {
190+
$converter = new HtmlConverter();
191+
$discussion->Body = $converter->convert($discussion->Body) ;
192+
$discussion->Format = 'Markdown';
193+
}
194+
}
195+
}
196+
160197
/**
161198
* Attach editor anywhere 'BodyBox' is used.
162199
*
@@ -170,11 +207,13 @@ public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) {
170207
if (val('Attributes', $args)) {
171208
$attributes = val('Attributes', $args);
172209
}
210+
/** @var Gdn_Controller $controller */
211+
$controller = Gdn::controller();
212+
$data = $sender->formData();
213+
$controller->addDefinition('originalFormat', $data['Format']);
173214

174-
if ($this->isFormMarkDown($sender)) {
175-
/** @var Gdn_Controller $controller */
176-
$controller = Gdn::controller();
177-
$controller->CssClass .= ' hasTopcoderEditor';
215+
if ($this->isFormMarkDown($sender) || $this->isFormWysiwyg($sender) ) {
216+
$controller->CssClass .= 'hasRichEditor hasTopcoderEditor'; // hasRichEditor = to support Rich editor
178217

179218
$editorID = $this->getEditorID();
180219

@@ -194,8 +233,8 @@ public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) {
194233
$originalRecord = $sender->formData();
195234
$newBodyValue = null;
196235
$body = $originalRecord['Body'] ?? false;
197-
$originalFormat = $originalRecord['Format'] ?? false;
198-
236+
$originalRecord = $sender->formData();
237+
$originalFormat = $originalRecord['Format']? strtolower($originalRecord['Format']) : false;
199238
/*
200239
Allow rich content to be rendered and modified if the InputFormat
201240
is different from the original format in no longer applicable or
@@ -205,14 +244,18 @@ public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) {
205244
switch (strtolower(c('Garden.InputFormatter', 'unknown'))) {
206245
case Formats\TextFormat::FORMAT_KEY:
207246
case Formats\TextExFormat::FORMAT_KEY:
247+
$newBodyValue = $this->formatService->renderPlainText($body, Formats\TextFormat::FORMAT_KEY);
248+
$sender->setValue("Body", $newBodyValue);
249+
break;
250+
case Formats\RichFormat::FORMAT_KEY:
208251
$newBodyValue = $this->formatService->renderPlainText($body, Formats\RichFormat::FORMAT_KEY);
209252
$sender->setValue("Body", $newBodyValue);
210253
break;
211254
case 'unknown':
212255
// Do nothing
213256
break;
214257
default:
215-
$newBodyValue = $this->formatService->renderHTML($body, Formats\HtmlFormat::FORMAT_KEY);
258+
$newBodyValue = $this->formatService->renderPlainText($body, Formats\HtmlFormat::FORMAT_KEY);
216259
$sender->setValue("Body", $newBodyValue);
217260
}
218261
}
@@ -281,7 +324,7 @@ protected function addQuoteButton($sender, $args) {
281324
*
282325
* @return string The built up form html
283326
*/
284-
public function postingSettings_formatSpecificFormItems_handler(
327+
public function postingSettings_formatSpecificFormItems_handler1(
285328
string $additionalFormItemHTML,
286329
Gdn_Form $form,
287330
Gdn_ConfigurationModel $configModel

‎composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"require": {
3+
"league/html-to-markdown": "^4.10"
4+
}
5+
}

‎js/topcodereditor.js

Lines changed: 113 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(function($) {
22
$.fn.setAsEditor = function(selector) {
3-
selector = selector || 'textarea#Form_Body';
3+
selector = selector || '.BodyBox,.js-bodybox';
44

55
// If editor can be loaded, add class to body
66
$('body').addClass('topcodereditor-active');
@@ -11,9 +11,11 @@
1111
var editor,
1212
editorCacheBreakValue = Math.random(),
1313
editorVersion = gdn.definition('editorVersion', editorCacheBreakValue),
14-
formatOriginal = gdn.definition('editorInputFormat', 'Markdown'),
14+
defaultInputFormat = gdn.definition('defaultInputFormat', 'Markdown'),
15+
defaultMobileInputFormat = gdn.definition('defaultMobileInputFormat', 'Markdown'),
16+
editorInputFormat = gdn.definition('editorInputFormat', 'Markdown'),
1517
topcoderEditorToolbar = gdn.definition('topcoderEditorToolbar'),
16-
debug = true;
18+
debug = false;
1719

1820
var toolbarCustomActions = [{
1921
name: "mentions",
@@ -45,7 +47,9 @@
4547
}
4648

4749
function logMessage(message) {
48-
console.log('TopcoderPlugin::'+ message);
50+
if (debug) {
51+
console.log('TopcoderPlugin::'+ message);
52+
}
4953
}
5054

5155
function topcoderHandles(cm, option) {
@@ -123,97 +127,126 @@
123127
* Initialize editor on the page.
124128
*
125129
*/
126-
var editorInit = function(textareaObj) {
127-
var $currentEditableTextarea = $(textareaObj);
128-
129-
// if found, perform operation
130-
if ($currentEditableTextarea.length) {
131-
// instantiate new editor
132-
var editor = new EasyMDE({
133-
shortcuts: {
134-
"mentions":"Ctrl-Space",
135-
},
136-
autofocus: false,
137-
forceSync: true, // true, force text changes made in EasyMDE to be immediately stored in original text area.
138-
placeholder: '',
139-
element: $currentEditableTextarea[0],
140-
hintOptions: {hint: topcoderHandles},
141-
// toolbar: topcoderEditorToolbar,
142-
toolbar: ["bold", "italic", "strikethrough", "|",
143-
"heading-1", "heading-2", "heading-3", "|", "code", "quote", "|", "unordered-list",
144-
"ordered-list", "clean-block", "|", {
145-
name: "mentions",
146-
action: function mentions(editor) {
147-
completeAfter(editor.codemirror);
148-
},
149-
className: "fa fa-at",
150-
title: "Mention a Topcoder User",
151-
152-
}, "link", "image", "table", "horizontal-rule", "|", "fullscreen", "|", "guide"],
153-
hideIcons: ["guide", "heading", "preview", "side-by-side"],
154-
insertTexts: {
155-
horizontalRule: ["", "\n\n-----\n\n"],
156-
image: ["![](https://", ")"],
157-
link: ["[", "](https://)"],
158-
table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],
159-
},
160-
// uploadImage: false by default, If set to true, enables the image upload functionality, which can be triggered by drag&drop, copy-paste and through the browse-file window (opened when the user click on the upload-image icon). Defaults to false.
161-
// imageMaxSize: Maximum image size in bytes, checked before upload (note: never trust client, always check image size at server-side). Defaults to 1024*1024*2 (2Mb).
162-
// imageAccept: A comma-separated list of mime-types used to check image type before upload (note: never trust client, always check file types at server-side). Defaults to image/png, image/jpeg.
163-
// imageUploadEndpoint: The endpoint where the images data will be sent, via an asynchronous POST request
164-
// imageTexts:
165-
// errorMessages: Errors displayed to the user, using the errorCallback option,
166-
// errorCallback: A callback function used to define how to display an error message.
167-
// renderingConfig: Adjust settings for parsing the Markdown during previewing (not editing)
168-
// showIcons: An array of icon names to show. Can be used to show specific icons hidden by default without completely customizing the toolbar.
169-
// sideBySideFullscreen: If set to false, allows side-by-side editing without going into fullscreen. Defaults to true.
170-
//theme: Override the theme. Defaults to easymde.
171-
});
172-
173-
// forceSync = true, need to clear form after async requests
174-
$currentEditableTextarea.closest('form').on('complete', function(frm, btn) {
175-
editor.codemirror.setValue('');
176-
});
130+
var editorInit = function (textareaObj) {
131+
var $currentEditableTextarea = $(textareaObj);
132+
var $postForm = $(textareaObj).closest('form');
133+
var currentFormFormat = $postForm.find('input[name="Format"]');
134+
var currentTextBoxWrapper; // div wrapper
135+
136+
// TODO: how many formats
137+
if (currentFormFormat.length) {
138+
// TODO:
139+
// there might be different formats if there are several comments
140+
currentFormFormat = currentFormFormat[0].value.toLowerCase();
141+
}
142+
143+
logMessage('The default format is '+ editorInputFormat);
144+
logMessage('The form format is '+ JSON.stringify(currentFormFormat));
145+
146+
currentTextBoxWrapper = $currentEditableTextarea.parent('.TextBoxWrapper');
147+
// If singleInstance is false, then odds are the editor is being
148+
// loaded inline and there are other instances on page.
149+
var singleInstance = true;
150+
151+
// Determine if editing a comment, or not. When editing a comment,
152+
// it has a comment id, while adding a new comment has an empty
153+
// comment id. The value is a hidden input.
154+
var commentId = $postForm.find('#Form_CommentID').val();
155+
logMessage('CommentID='+commentId);
156+
157+
if (typeof commentId != 'undefined' && commentId != '') {
158+
singleInstance = false;
159+
}
160+
161+
logMessage('isSingleInstance='+singleInstance);
162+
163+
if ($currentEditableTextarea.length) {
164+
// instantiate new editor
165+
var editor = new EasyMDE({
166+
shortcuts: {
167+
"mentions": "Ctrl-Space",
168+
},
169+
autofocus: false,
170+
forceSync: true, // true, force text changes made in EasyMDE to be immediately stored in original text area.
171+
placeholder: '',
172+
element: $currentEditableTextarea[0],
173+
hintOptions: { hint: topcoderHandles },
174+
// toolbar: topcoderEditorToolbar,
175+
toolbar: ["bold", "italic", "strikethrough", "|",
176+
"heading-1", "heading-2", "heading-3", "|", "code", "quote", "|", "unordered-list",
177+
"ordered-list", "clean-block", "|", {
178+
name: "mentions",
179+
action: function mentions (editor) {
180+
completeAfter(editor.codemirror);
181+
},
182+
className: "fa fa-at",
183+
title: "Mention a Topcoder User",
184+
185+
}, "link", "image", "table", "horizontal-rule", "|", "fullscreen", "|", "guide"],
186+
hideIcons: ["guide", "heading", "preview", "side-by-side"],
187+
insertTexts: {
188+
horizontalRule: ["", "\n\n-----\n\n"],
189+
image: ["![](https://", ")"],
190+
link: ["[", "](https://)"],
191+
table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],
192+
},
193+
// uploadImage: false by default, If set to true, enables the image upload functionality, which can be triggered by drag&drop, copy-paste and through the browse-file window (opened when the user click on the upload-image icon). Defaults to false.
194+
// imageMaxSize: Maximum image size in bytes, checked before upload (note: never trust client, always check image size at server-side). Defaults to 1024*1024*2 (2Mb).
195+
// imageAccept: A comma-separated list of mime-types used to check image type before upload (note: never trust client, always check file types at server-side). Defaults to image/png, image/jpeg.
196+
// imageUploadEndpoint: The endpoint where the images data will be sent, via an asynchronous POST request
197+
// imageTexts:
198+
// errorMessages: Errors displayed to the user, using the errorCallback option,
199+
// errorCallback: A callback function used to define how to display an error message.
200+
// renderingConfig: Adjust settings for parsing the Markdown during previewing (not editing)
201+
// showIcons: An array of icon names to show. Can be used to show specific icons hidden by default without completely customizing the toolbar.
202+
// sideBySideFullscreen: If set to false, allows side-by-side editing without going into fullscreen. Defaults to true.
203+
//theme: Override the theme. Defaults to easymde.
204+
});
205+
206+
// forceSync = true, need to clear form after async requests
207+
$currentEditableTextarea.closest('form').on('complete', function (frm, btn) {
208+
editor.codemirror.setValue('');
209+
});
177210

178211
editor.codemirror.on('change', function (cm, changeObj){
179212
// logMessage('onChange:'+cm.getCursor().ch);
180213
});
181214

182-
editor.codemirror.on('keydown', function (cm, event){
183-
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/) {
184-
if(event.key == '@') {
185-
var currentCursorPosition = cm.getCursor();
186-
if(currentCursorPosition.ch === 0) {
187-
cm.showHint({ completeSingle: false, alignWithWord: true });
188-
return;
189-
}
190-
191-
var backwardCursorPosition = {
192-
line: currentCursorPosition.line,
193-
ch: currentCursorPosition.ch - 1
194-
};
195-
var backwardCharacter = cm.getRange(backwardCursorPosition, currentCursorPosition);
196-
if (backwardCharacter === ' ') { // space
197-
cm.showHint({ completeSingle: false, alignWithWord: true });
215+
editor.codemirror.on('keydown', function (cm, event) {
216+
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/) {
217+
if (event.key == '@') {
218+
var currentCursorPosition = cm.getCursor();
219+
if (currentCursorPosition.ch === 0) {
220+
cm.showHint({ completeSingle: false, alignWithWord: true });
221+
return;
222+
}
223+
224+
var backwardCursorPosition = {
225+
line: currentCursorPosition.line,
226+
ch: currentCursorPosition.ch - 1
227+
};
228+
var backwardCharacter = cm.getRange(backwardCursorPosition, currentCursorPosition);
229+
if (backwardCharacter === ' ') { // space
230+
cm.showHint({ completeSingle: false, alignWithWord: true });
231+
}
198232
}
199233
}
200-
}
201-
});
202-
}
203-
} //editorInit
234+
});
235+
}
204236

205-
editorInit(this);
237+
}; //editorInit
206238

239+
editorInit(this);
207240
// jQuery chaining
208241
return this;
209242
};
210243

211244
$(document).on('contentLoad', function(e) {
212-
if ($('textarea#Form_Body', e.target).length === 0) {
213-
console.log('Couldn\'t load EasyMDE: missing #Form_Body');
245+
if ($('.BodyBox[format="Markdown"], .BodyBox[format="wysiwyg"],.js-bodybox[format="Markdown"], .js-bodybox[format="wysiwyg"]', e.target).length === 0) {
246+
console.log('Supported only [format="Markdown"][format="wysiwyg"]');
214247
return;
215248
}
216-
// Vanilla Form
217-
$('textarea#Form_Body', e.target).setAsEditor();
249+
// Multiple editors are supported on a page
250+
$('.BodyBox[format="Markdown"], .BodyBox[format="wysiwyg"],.js-bodybox[format="Markdown"], .js-bodybox[format="wysiwyg"]', e.target).setAsEditor();
218251
});
219252
}(jQuery));

0 commit comments

Comments
 (0)
Please sign in to comment.