diff --git a/TopcoderEditorPlugin.php b/TopcoderEditorPlugin.php index dbc4184..0e1bb7a 100644 --- a/TopcoderEditorPlugin.php +++ b/TopcoderEditorPlugin.php @@ -1,7 +1,15 @@ 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')); @@ -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'); + } /** @@ -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; } @@ -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. * @@ -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(); @@ -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 @@ -205,6 +244,10 @@ 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; @@ -212,7 +255,7 @@ public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) { // 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); } } @@ -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 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9378a47 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "league/html-to-markdown": "^4.10" + } +} diff --git a/js/topcodereditor.js b/js/topcodereditor.js index ea67982..78fa8a3 100644 --- a/js/topcodereditor.js +++ b/js/topcodereditor.js @@ -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'); @@ -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", @@ -45,7 +47,9 @@ } function logMessage(message) { - console.log('TopcoderPlugin::'+ message); + if (debug) { + console.log('TopcoderPlugin::'+ message); + } } function topcoderHandles(cm, option) { @@ -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));