Skip to content

Issues:308: supporting old and new formats #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 60 additions & 17 deletions TopcoderEditorPlugin.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
<?php

if (!class_exists('League\HTMLToMarkdown\HtmlConverter')){
require __DIR__ . '/vendor/autoload.php';
}


use Vanilla\Formatting\Formats\MarkdownFormat;
use \Vanilla\Formatting\Formats;
use Vanilla\Formatting\Formats\RichFormat;
use League\HTMLToMarkdown\HtmlConverter;


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

// Set definitions for JavaScript to read
$c->addDefinition('editorVersion', $this->pluginInfo['Version']);
$c->addDefinition('editorInputFormat', $this->Format);
$c->addDefinition('editorPluginAssets', $this->AssetPath);

$additionalDefinitions = [];
$this->EventArguments['definitions'] = &$additionalDefinitions;
$this->fireEvent('GetJSDefinitions');
// Set formats
$c->addDefinition('defaultInputFormat', c('Garden.InputFormatter'));
$c->addDefinition('defaultMobileInputFormat', c('Garden.MobileInputFormatter'));

// Set variables for file uploads
// Set file uploads vars
$postMaxSize = Gdn_Upload::unformatFileSize(ini_get('post_max_size'));
$fileMaxSize = Gdn_Upload::unformatFileSize(ini_get('upload_max_filesize'));
$configMaxSize = Gdn_Upload::unformatFileSize(c('Garden.Upload.MaxFileSize', '1MB'));
Expand All @@ -99,6 +102,16 @@ private function beforeRender($sender){
$c->addDefinition('allowedFileExtensions', json_encode($allowedFileExtensions));
// Get max file uploads, to be used for max drops at once.
$c->addDefinition('maxFileUploads', ini_get('max_file_uploads'));

// Set editor definitions
$c->addDefinition('editorVersion', $this->pluginInfo['Version']);
$c->addDefinition('editorInputFormat', ucfirst(self::FORMAT_NAME));
$c->addDefinition('editorPluginAssets', $this->AssetPath);

$additionalDefinitions = [];
$this->EventArguments['definitions'] = &$additionalDefinitions;
$this->fireEvent('GetJSDefinitions');

}

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

/**
* Check to see if we should be using the Topcoder Editor
*
* @param Gdn_Form $form - A form instance.
*
* @return bool
*/
public function isFormWysiwyg(Gdn_Form $form): bool {
$data = $form->formData();
$format = $data['Format'] ?? null;
return strcasecmp($format, Vanilla\Formatting\Formats\WysiwygFormat::FORMAT_KEY) === 0;
}

public function isInputFormatterMarkDown(): bool {
return strcasecmp(Gdn_Format::defaultFormat(), MarkdownFormat::FORMAT_KEY) === 0;
}
Expand All @@ -157,6 +183,17 @@ public function getPostFormats_handler(array $postFormats): array {
return $postFormats;
}

public function postController_beforeEditDiscussion_handler($sender, $args) {
$discussion = &$args['Discussion'];
if($discussion) {
if (strcasecmp($discussion->Format, Vanilla\Formatting\Formats\WysiwygFormat::FORMAT_KEY) === 0) {
$converter = new HtmlConverter();
$discussion->Body = $converter->convert($discussion->Body) ;
$discussion->Format = 'Markdown';
}
}
}

/**
* Attach editor anywhere 'BodyBox' is used.
*
Expand All @@ -170,11 +207,13 @@ public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) {
if (val('Attributes', $args)) {
$attributes = val('Attributes', $args);
}
/** @var Gdn_Controller $controller */
$controller = Gdn::controller();
$data = $sender->formData();
$controller->addDefinition('originalFormat', $data['Format']);

if ($this->isFormMarkDown($sender)) {
/** @var Gdn_Controller $controller */
$controller = Gdn::controller();
$controller->CssClass .= ' hasTopcoderEditor';
if ($this->isFormMarkDown($sender) || $this->isFormWysiwyg($sender) ) {
$controller->CssClass .= 'hasRichEditor hasTopcoderEditor'; // hasRichEditor = to support Rich editor

$editorID = $this->getEditorID();

Expand All @@ -194,8 +233,8 @@ public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) {
$originalRecord = $sender->formData();
$newBodyValue = null;
$body = $originalRecord['Body'] ?? false;
$originalFormat = $originalRecord['Format'] ?? false;

$originalRecord = $sender->formData();
$originalFormat = $originalRecord['Format']? strtolower($originalRecord['Format']) : false;
/*
Allow rich content to be rendered and modified if the InputFormat
is different from the original format in no longer applicable or
Expand All @@ -205,14 +244,18 @@ public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) {
switch (strtolower(c('Garden.InputFormatter', 'unknown'))) {
case Formats\TextFormat::FORMAT_KEY:
case Formats\TextExFormat::FORMAT_KEY:
$newBodyValue = $this->formatService->renderPlainText($body, Formats\TextFormat::FORMAT_KEY);
$sender->setValue("Body", $newBodyValue);
break;
case Formats\RichFormat::FORMAT_KEY:
$newBodyValue = $this->formatService->renderPlainText($body, Formats\RichFormat::FORMAT_KEY);
$sender->setValue("Body", $newBodyValue);
break;
case 'unknown':
// Do nothing
break;
default:
$newBodyValue = $this->formatService->renderHTML($body, Formats\HtmlFormat::FORMAT_KEY);
$newBodyValue = $this->formatService->renderPlainText($body, Formats\HtmlFormat::FORMAT_KEY);
$sender->setValue("Body", $newBodyValue);
}
}
Expand Down Expand Up @@ -281,7 +324,7 @@ protected function addQuoteButton($sender, $args) {
*
* @return string The built up form html
*/
public function postingSettings_formatSpecificFormItems_handler(
public function postingSettings_formatSpecificFormItems_handler1(
string $additionalFormItemHTML,
Gdn_Form $form,
Gdn_ConfigurationModel $configModel
Expand Down
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"require": {
"league/html-to-markdown": "^4.10"
}
}
193 changes: 113 additions & 80 deletions js/topcodereditor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(function($) {
$.fn.setAsEditor = function(selector) {
selector = selector || 'textarea#Form_Body';
selector = selector || '.BodyBox,.js-bodybox';

// If editor can be loaded, add class to body
$('body').addClass('topcodereditor-active');
Expand All @@ -11,9 +11,11 @@
var editor,
editorCacheBreakValue = Math.random(),
editorVersion = gdn.definition('editorVersion', editorCacheBreakValue),
formatOriginal = gdn.definition('editorInputFormat', 'Markdown'),
defaultInputFormat = gdn.definition('defaultInputFormat', 'Markdown'),
defaultMobileInputFormat = gdn.definition('defaultMobileInputFormat', 'Markdown'),
editorInputFormat = gdn.definition('editorInputFormat', 'Markdown'),
topcoderEditorToolbar = gdn.definition('topcoderEditorToolbar'),
debug = true;
debug = false;

var toolbarCustomActions = [{
name: "mentions",
Expand Down Expand Up @@ -45,7 +47,9 @@
}

function logMessage(message) {
console.log('TopcoderPlugin::'+ message);
if (debug) {
console.log('TopcoderPlugin::'+ message);
}
}

function topcoderHandles(cm, option) {
Expand Down Expand Up @@ -123,97 +127,126 @@
* Initialize editor on the page.
*
*/
var editorInit = function(textareaObj) {
var $currentEditableTextarea = $(textareaObj);

// if found, perform operation
if ($currentEditableTextarea.length) {
// instantiate new editor
var editor = new EasyMDE({
shortcuts: {
"mentions":"Ctrl-Space",
},
autofocus: false,
forceSync: true, // true, force text changes made in EasyMDE to be immediately stored in original text area.
placeholder: '',
element: $currentEditableTextarea[0],
hintOptions: {hint: topcoderHandles},
// toolbar: topcoderEditorToolbar,
toolbar: ["bold", "italic", "strikethrough", "|",
"heading-1", "heading-2", "heading-3", "|", "code", "quote", "|", "unordered-list",
"ordered-list", "clean-block", "|", {
name: "mentions",
action: function mentions(editor) {
completeAfter(editor.codemirror);
},
className: "fa fa-at",
title: "Mention a Topcoder User",

}, "link", "image", "table", "horizontal-rule", "|", "fullscreen", "|", "guide"],
hideIcons: ["guide", "heading", "preview", "side-by-side"],
insertTexts: {
horizontalRule: ["", "\n\n-----\n\n"],
image: ["![](https://", ")"],
link: ["[", "](https://)"],
table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],
},
// 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.
// 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).
// 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.
// imageUploadEndpoint: The endpoint where the images data will be sent, via an asynchronous POST request
// imageTexts:
// errorMessages: Errors displayed to the user, using the errorCallback option,
// errorCallback: A callback function used to define how to display an error message.
// renderingConfig: Adjust settings for parsing the Markdown during previewing (not editing)
// showIcons: An array of icon names to show. Can be used to show specific icons hidden by default without completely customizing the toolbar.
// sideBySideFullscreen: If set to false, allows side-by-side editing without going into fullscreen. Defaults to true.
//theme: Override the theme. Defaults to easymde.
});

// forceSync = true, need to clear form after async requests
$currentEditableTextarea.closest('form').on('complete', function(frm, btn) {
editor.codemirror.setValue('');
});
var editorInit = function (textareaObj) {
var $currentEditableTextarea = $(textareaObj);
var $postForm = $(textareaObj).closest('form');
var currentFormFormat = $postForm.find('input[name="Format"]');
var currentTextBoxWrapper; // div wrapper

// TODO: how many formats
if (currentFormFormat.length) {
// TODO:
// there might be different formats if there are several comments
currentFormFormat = currentFormFormat[0].value.toLowerCase();
}

logMessage('The default format is '+ editorInputFormat);
logMessage('The form format is '+ JSON.stringify(currentFormFormat));

currentTextBoxWrapper = $currentEditableTextarea.parent('.TextBoxWrapper');
// If singleInstance is false, then odds are the editor is being
// loaded inline and there are other instances on page.
var singleInstance = true;

// Determine if editing a comment, or not. When editing a comment,
// it has a comment id, while adding a new comment has an empty
// comment id. The value is a hidden input.
var commentId = $postForm.find('#Form_CommentID').val();
logMessage('CommentID='+commentId);

if (typeof commentId != 'undefined' && commentId != '') {
singleInstance = false;
}

logMessage('isSingleInstance='+singleInstance);

if ($currentEditableTextarea.length) {
// instantiate new editor
var editor = new EasyMDE({
shortcuts: {
"mentions": "Ctrl-Space",
},
autofocus: false,
forceSync: true, // true, force text changes made in EasyMDE to be immediately stored in original text area.
placeholder: '',
element: $currentEditableTextarea[0],
hintOptions: { hint: topcoderHandles },
// toolbar: topcoderEditorToolbar,
toolbar: ["bold", "italic", "strikethrough", "|",
"heading-1", "heading-2", "heading-3", "|", "code", "quote", "|", "unordered-list",
"ordered-list", "clean-block", "|", {
name: "mentions",
action: function mentions (editor) {
completeAfter(editor.codemirror);
},
className: "fa fa-at",
title: "Mention a Topcoder User",

}, "link", "image", "table", "horizontal-rule", "|", "fullscreen", "|", "guide"],
hideIcons: ["guide", "heading", "preview", "side-by-side"],
insertTexts: {
horizontalRule: ["", "\n\n-----\n\n"],
image: ["![](https://", ")"],
link: ["[", "](https://)"],
table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],
},
// 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.
// 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).
// 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.
// imageUploadEndpoint: The endpoint where the images data will be sent, via an asynchronous POST request
// imageTexts:
// errorMessages: Errors displayed to the user, using the errorCallback option,
// errorCallback: A callback function used to define how to display an error message.
// renderingConfig: Adjust settings for parsing the Markdown during previewing (not editing)
// showIcons: An array of icon names to show. Can be used to show specific icons hidden by default without completely customizing the toolbar.
// sideBySideFullscreen: If set to false, allows side-by-side editing without going into fullscreen. Defaults to true.
//theme: Override the theme. Defaults to easymde.
});

// forceSync = true, need to clear form after async requests
$currentEditableTextarea.closest('form').on('complete', function (frm, btn) {
editor.codemirror.setValue('');
});

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

editor.codemirror.on('keydown', function (cm, event){
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/) {
if(event.key == '@') {
var currentCursorPosition = cm.getCursor();
if(currentCursorPosition.ch === 0) {
cm.showHint({ completeSingle: false, alignWithWord: true });
return;
}

var backwardCursorPosition = {
line: currentCursorPosition.line,
ch: currentCursorPosition.ch - 1
};
var backwardCharacter = cm.getRange(backwardCursorPosition, currentCursorPosition);
if (backwardCharacter === ' ') { // space
cm.showHint({ completeSingle: false, alignWithWord: true });
editor.codemirror.on('keydown', function (cm, event) {
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/) {
if (event.key == '@') {
var currentCursorPosition = cm.getCursor();
if (currentCursorPosition.ch === 0) {
cm.showHint({ completeSingle: false, alignWithWord: true });
return;
}

var backwardCursorPosition = {
line: currentCursorPosition.line,
ch: currentCursorPosition.ch - 1
};
var backwardCharacter = cm.getRange(backwardCursorPosition, currentCursorPosition);
if (backwardCharacter === ' ') { // space
cm.showHint({ completeSingle: false, alignWithWord: true });
}
}
}
}
});
}
} //editorInit
});
}

editorInit(this);
}; //editorInit

editorInit(this);
// jQuery chaining
return this;
};

$(document).on('contentLoad', function(e) {
if ($('textarea#Form_Body', e.target).length === 0) {
console.log('Couldn\'t load EasyMDE: missing #Form_Body');
if ($('.BodyBox[format="Markdown"], .BodyBox[format="wysiwyg"],.js-bodybox[format="Markdown"], .js-bodybox[format="wysiwyg"]', e.target).length === 0) {
console.log('Supported only [format="Markdown"][format="wysiwyg"]');
return;
}
// Vanilla Form
$('textarea#Form_Body', e.target).setAsEditor();
// Multiple editors are supported on a page
$('.BodyBox[format="Markdown"], .BodyBox[format="wysiwyg"],.js-bodybox[format="Markdown"], .js-bodybox[format="wysiwyg"]', e.target).setAsEditor();
});
}(jQuery));