FIX: Allow multiple pagedown editors at once.

This commit is contained in:
Robin Ward 2015-07-13 16:36:38 -04:00
parent 4f103f7cc5
commit 20a338362f
26 changed files with 109 additions and 122 deletions

View file

@ -1,5 +1,5 @@
export default Ember.TextArea.extend({
elementId: 'wmd-input',
classNameBindings: [':wmd-input'],
placeholder: function() {
return I18n.t('composer.reply_placeholder');

View file

@ -3,8 +3,9 @@ import { buildCategoryPanel } from 'discourse/components/edit-category-panel';
export default buildCategoryPanel('topic-template', {
_activeTabChanged: function() {
if (this.get('activeTab')) {
const self = this;
Ember.run.schedule('afterRender', function() {
$('#wmd-input').focus();
self.$('.wmd-input').focus();
});
}
}.observes('activeTab')

View file

@ -1,13 +1,13 @@
import loadScript from 'discourse/lib/load-script';
export default Ember.Component.extend({
elementId: 'pagedown-editor',
classNameBindings: [':pagedown-editor'],
_initializeWmd: function() {
const self = this;
loadScript('defer/html-sanitizer-bundle').then(function() {
$('#wmd-input').data('init', true);
self._editor = Discourse.Markdown.createEditor();
self.$('.wmd-input').data('init', true);
self._editor = Discourse.Markdown.createEditor({ containerElement: self.element });
self._editor.run();
Ember.run.scheduleOnce('afterRender', self, self._refreshPreview);
});

View file

@ -115,7 +115,7 @@ export default Ember.ObjectController.extend(Presence, {
const c = this.get('model');
if (c) {
opts = opts || {};
const wmd = $('#wmd-input'),
const wmd = $('.wmd-input'),
val = wmd.val() || '',
position = opts.position === "cursor" ? wmd.caret() : val.length,
caret = c.appendText(text, position, opts);
@ -536,7 +536,7 @@ export default Ember.ObjectController.extend(Presence, {
},
closeAutocomplete() {
$('#wmd-input').autocomplete({ cancel: true });
$('.wmd-input').autocomplete({ cancel: true });
},
showOptions() {

View file

@ -1,19 +1,11 @@
import ObjectController from 'discourse/controllers/object';
/**
This controller supports actions related to updating your "About Me" bio
@class PreferencesAboutController
@extends ObjectController
@namespace Discourse
@module Discourse
**/
export default ObjectController.extend({
saving: false,
newBio: null,
saveButtonText: function() {
if (this.get('saving')) return I18n.t("saving");
return I18n.t("user.change");
return this.get('saving') ? I18n.t("saving") : I18n.t('user.change');
}.property('saving')
});

View file

@ -145,7 +145,7 @@
if (panels)
return; // already initialized
panels = new PanelCollection(idPostfix);
panels = new PanelCollection(options.containerElement);
var commandManager = new CommandManager(hooks, getString);
var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); });
var undoManager, uiManager;
@ -305,12 +305,19 @@
// end of Chunks
function firstByClass(doc, containerElement, className) {
var elements = doc.getElementsByClassName(className);
if (elements && elements.length) {
return elements[0];
}
}
// A collection of the important regions on the page.
// Cached so we don't have to keep traversing the DOM.
function PanelCollection(postfix) {
this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
this.preview = doc.getElementById("wmd-preview" + postfix);
this.input = doc.getElementById("wmd-input" + postfix);
function PanelCollection(containerElement) {
this.buttonBar = firstByClass(doc, containerElement, 'wmd-button-bar');
this.preview = firstByClass(doc, containerElement, 'wmd-preview');
this.input = firstByClass(doc, containerElement, 'wmd-input');
};
// Returns true if the DOM element is visible, false if it's hidden.
@ -1393,15 +1400,13 @@
var buttonBar = panels.buttonBar;
var buttonRow = document.createElement("div");
buttonRow.id = "wmd-button-row" + postfix;
buttonRow.className = 'wmd-button-row';
buttonRow = buttonBar.appendChild(buttonRow);
var xPosition = 0;
var makeButton = function (id, title, textOp) {
var button = document.createElement("button");
button.className = "wmd-button";
button.className = "wmd-button " + id;
xPosition += 25;
button.id = id + postfix;
button.title = title;
// we really should just use jquery here
if (button.setAttribute) {

View file

@ -152,13 +152,6 @@ Discourse.Markdown = {
return this.markdownConverter(opts).makeHtml(raw);
},
/**
Creates a new pagedown markdown editor, supplying i18n translations.
@method createEditor
@param {Object} converterOptions custom options for our markdown converter
@return {Markdown.Editor} the editor instance
**/
createEditor: function(converterOptions) {
if (!converterOptions) converterOptions = {};
@ -168,6 +161,7 @@ Discourse.Markdown = {
var markdownConverter = Discourse.Markdown.markdownConverter(converterOptions);
var editorOptions = {
containerElement: converterOptions.containerElement,
strings: {
bold: I18n.t("composer.bold_title") + " <strong> Ctrl+B",
boldexample: I18n.t("composer.bold_text"),

View file

@ -586,7 +586,7 @@ const Composer = RestModel.extend({
},
getCookedHtml() {
return $('#wmd-preview').html().replace(/<span class="marker"><\/span>/g, '');
return $('#reply-control .wmd-preview').html().replace(/<span class="marker"><\/span>/g, '');
},
saveDraft() {

View file

@ -10,7 +10,7 @@ export default RestrictedUserRoute.extend({
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newBio: model.get('bio_raw') });
controller.setProperties({ model, newBio: model.get('bio_raw') });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet

View file

@ -1,4 +1,4 @@
<div id='wmd-button-bar'></div>
{{textarea value=value elementId="wmd-input"}}
<div id='wmd-preview' {{bind-attr class=":preview value::hidden"}}>
<div class='wmd-button-bar'></div>
{{textarea value=value class="wmd-input"}}
<div class="wmd-preview preview {{unless value 'hidden'}}">
</div>

View file

@ -65,17 +65,17 @@
<div class='wmd-controls'>
<div class='textarea-wrapper'>
<div class='wmd-button-bar' id='wmd-button-bar'></div>
<div id='wmd-preview-scroller'></div>
<div class='wmd-button-bar'></div>
<div class='wmd-preview-scroller'></div>
{{composer-text-area tabindex="4" value=model.reply}}
{{popup-input-tip validation=view.replyValidation shownAt=view.showReplyTip}}
</div>
<!-- keep the classes here in sync with post.hbs -->
<div class='preview-wrapper regular'>
<div id='wmd-preview' {{bind-attr class="model.hidePreview:hidden :cooked"}}></div>
<div class="wmd-preview cooked {{if model.hidePreview 'hidden'}}"></div>
</div>
<div class="composer-bottom-right">
<a href="#" {{action "togglePreview"}} class='toggle-preview'>{{{model.toggleText}}}</a>
<a href {{action "togglePreview"}} class='toggle-preview'>{{{model.toggleText}}}</a>
<div id="file-uploading" {{bind-attr class="view.isUploading::hidden"}}>
{{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}% <a id="cancel-file-upload">{{i18n 'cancel'}}</a>
</div>

View file

@ -6,25 +6,18 @@
</div>
</div>
{{#if error}}
<div class="control-group">
<div class="instructions">
<div class='alert alert-error'>{{i18n 'user.change_about.error'}}</div>
</div>
</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{i18n 'user.bio'}}</label>
<div class="controls">
{{pagedown-editor value=bio_raw}}
{{pagedown-editor value=model.bio_raw}}
</div>
</div>
<div class="control-group">
<div class="controls">
<button {{action "changeAbout"}} {{bind-attr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
{{#if saved}}{{i18n 'saved'}}{{/if}}
{{#d-button action="changeAbout" class="btn btn-primary"}}
{{saveButtonText}}
{{/d-button}}
</div>
</div>

View file

@ -30,9 +30,9 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
// Disable fields when we're loading
loadingChanged: function() {
if (this.get('loading')) {
$('#wmd-input, #reply-title').prop('disabled', 'disabled');
this.$('.wmd-input, #reply-title').prop('disabled', 'disabled');
} else {
$('#wmd-input, #reply-title').prop('disabled', '');
this.$('.wmd-input, #reply-title').prop('disabled', '');
}
}.observes('loading'),
@ -150,7 +150,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
'max-width:' + Discourse.SiteSettings.max_image_width + 'px;' +
'max-height:' + Discourse.SiteSettings.max_image_height + 'px;';
$('<style>#wmd-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}</style>').appendTo('head');
$('<style>#reply-control .wmd-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}</style>').appendTo('head');
},
click() {
@ -159,7 +159,9 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
// Called after the preview renders. Debounced for performance
afterRender() {
const $wmdPreview = $('#wmd-preview');
if (this._state !== "inDOM") { return; }
const $wmdPreview = this.$('.wmd-preview');
if ($wmdPreview.length === 0) return;
const post = this.get('model.post');
@ -196,7 +198,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
if (!this.siteSettings.enable_emoji) { return; }
const template = this.container.lookup('template:emoji-selector-autocomplete.raw');
$('#wmd-input').autocomplete({
this.$('.wmd-input').autocomplete({
template: template,
key: ":",
transformComplete(v) { return v.code + ":"; },
@ -230,7 +232,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
// but if you start replying to another topic it will get the avatars wrong
let $wmdInput, editor;
const self = this;
this.wmdInput = $wmdInput = $('#wmd-input');
this.wmdInput = $wmdInput = this.$('.wmd-input');
if ($wmdInput.length === 0 || $wmdInput.data('init') === true) return;
loadScript('defer/html-sanitizer-bundle');
@ -255,6 +257,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
});
this.editor = editor = Discourse.Markdown.createEditor({
containerElement: this.element,
lookupAvatarByPostNumber(postNumber) {
const posts = self.get('controller.controllers.topic.model.postStream.posts');
if (posts) {
@ -501,7 +504,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
},
addMarkdown(text) {
const ctrl = $('#wmd-input').get(0),
const ctrl = this.$('.wmd-input').get(0),
caretPosition = Discourse.Utilities.caretPosition(ctrl),
current = this.get('model.reply');
this.set('model.reply', current.substring(0, caretPosition) + text + current.substring(caretPosition, current.length));
@ -514,7 +517,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
// Uses javascript to get the image sizes from the preview, if present
imageSizes() {
const result = {};
$('#wmd-preview img').each(function(i, e) {
this.$('.wmd-preview img').each(function(i, e) {
const $img = $(e),
src = $img.prop('src');
@ -529,7 +532,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
this.initEditor();
// Disable links in the preview
$('#wmd-preview').on('click.preview', (e) => {
this.$('.wmd-preview').on('click.preview', (e) => {
e.preventDefault();
return false;
});
@ -538,7 +541,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
childWillDestroyElement() {
this._unbindUploadTarget();
$('#wmd-preview').off('click.preview');
this.$('.wmd-preview').off('click.preview');
Em.run.next(() => {
$('#main-outlet').css('padding-bottom', 0);

View file

@ -1037,7 +1037,7 @@ table.api-keys {
margin-top: 10px;
}
#pagedown-editor {
.pagedown-editor {
width: 98%;
}
@ -1046,7 +1046,7 @@ table.api-keys {
height: 200px;
}
#wmd-input {
.wmd-input {
width: 98%;
height: 200px;
}
@ -1349,7 +1349,7 @@ and (max-width : 500px) {
.content-editor {
width: 100%;
#pagedown-editor {
.pagedown-editor {
box-sizing: border-box;
}
}

View file

@ -143,7 +143,7 @@ div.ac-wrap {
// this removes the topmost margin from the first element in the topic post
// if we don't do this, all posts would have extra space at the top
#wmd-preview > *:first-child {
.wmd-preview > *:first-child {
margin-top: 0;
}
.cooked > *:first-child {

View file

@ -141,11 +141,11 @@ body {
}
#wmd-input {
.wmd-input {
resize: none;
}
#pagedown-editor {
.pagedown-editor {
width: 540px;
background-color: $secondary;
padding: 0 10px 13px 10px;

View file

@ -104,7 +104,7 @@
.modal.edit-category-modal {
.modal-body {
#pagedown-editor {
.pagedown-editor {
width: 98%;
}

View file

@ -67,60 +67,59 @@
}
#wmd-bold-button:before {
.wmd-bold-button:before {
content: "\f032";
}
#wmd-italic-button:before {
.wmd-italic-button:before {
content: "\f033";
}
#wmd-link-button:before {
.wmd-link-button:before {
content: "\f0c1";
}
#wmd-quote-button:before {
.wmd-quote-button:before {
content: "\f10e";
}
#wmd-code-button:before {
.wmd-code-button:before {
content: "\f121";
}
#wmd-image-button:before {
.wmd-image-button:before {
content: "\f093";
}
#wmd-image-button.image-only:before {
.wmd-image-button.image-only:before {
content: "\f03e";
}
#wmd-olist-button:before {
.wmd-olist-button:before {
content: "\f0cb";
}
#wmd-ulist-button:before {
.wmd-ulist-button:before {
content: "\f0ca";
}
#wmd-heading-button:before {
.wmd-heading-button:before {
content: "\f031";
}
#wmd-hr-button:before {
.wmd-hr-button:before {
content: "\f068";
}
#wmd-undo-button:before {
.wmd-undo-button:before {
content: "\f0e2";
}
#wmd-redo-button:before {
.wmd-redo-button:before {
content: "\f01e";
}
#wmd-quote-post:before {
.wmd-quote-post:before {
content: "\f0e5";
}

View file

@ -26,7 +26,7 @@
}
// global styles for the cooked HTML content in posts (and preview)
.cooked, #wmd-preview {
.cooked, .wmd-preview {
word-wrap: break-word;
h1, h2, h3, h4, h5, h6 { margin: 30px 0 10px; }
h1 { line-height: 1em; } /* normalize.css sets h1 font size but not line height */
@ -34,7 +34,7 @@
}
.cooked, #wmd-preview {
.cooked, .wmd-preview {
video {
max-width: 100%;
}

View file

@ -269,14 +269,14 @@
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
}
}
#wmd-input:disabled {
.wmd-input:disabled {
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
}
#wmd-input, #wmd-preview {
.wmd-input, .wmd-preview {
color: $primary;
}
#wmd-preview {
.wmd-preview {
border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%);
overflow: auto;
visibility: visible;
@ -294,7 +294,7 @@
visibility: hidden;
}
}
#wmd-input {
.wmd-input {
bottom: 35px;
}
@ -344,7 +344,7 @@
#reply-control {
&.hide-preview {
.wmd-controls {
#wmd-input {
.wmd-input {
width: 100%;
}
.preview-wrapper {
@ -363,7 +363,7 @@
top: 50px;
#wmd-input, #wmd-preview-scroller, #wmd-preview {
.wmd-input, .wmd-preview-scroller, .wmd-preview {
box-sizing: border-box;
width: 100%;
height: 100%;
@ -373,7 +373,7 @@
background-color: $secondary;
word-wrap: break-word;
}
#wmd-input, #wmd-preview-scroller {
.wmd-input, .wmd-preview-scroller {
position: absolute;
left: 0;
top: 0;
@ -381,7 +381,7 @@
border-top: 30px solid transparent;
@include border-radius-all(0);
}
#wmd-preview-scroller {
.wmd-preview-scroller {
font-size: 0.929em;
line-height: 18px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
@ -414,7 +414,7 @@
float: right;
}
}
#wmd-button-bar {
.wmd-button-bar {
top: 0;
position: absolute;
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);

View file

@ -18,7 +18,7 @@
width: $topic-body-width;
float: left;
#wmd-input {
.wmd-input {
width: 98%;
height: 15em;
}

View file

@ -39,7 +39,7 @@
}
}
#pagedown-editor {
.pagedown-editor {
width: 450px;
textarea {

View file

@ -168,13 +168,13 @@ display: none !important; // can be removed if inline JS CSS is removed from com
background-color: scale-color($primary, $lightness: 75%);
}
}
#wmd-input:disabled {
.wmd-input:disabled {
background-color: scale-color($primary, $lightness: 75%);
}
#wmd-input {
.wmd-input {
color: darken($primary, 40%);
}
#wmd-input {
.wmd-input {
bottom: 35px;
}
.submit-panel {
@ -226,7 +226,7 @@ display: none !important; // can be removed if inline JS CSS is removed from com
bottom: 50px;
display: block;
#wmd-input {
.wmd-input {
width: 100%;
height: 100%;
min-height: 100%;
@ -236,7 +236,7 @@ display: none !important; // can be removed if inline JS CSS is removed from com
word-wrap: break-word;
box-sizing: border-box;
}
#wmd-input {
.wmd-input {
position: absolute;
left: 0;
top: 0;
@ -254,7 +254,7 @@ display: none !important; // can be removed if inline JS CSS is removed from com
}
}
}
#wmd-button-bar {
.wmd-button-bar {
display: none;
}
}

View file

@ -67,7 +67,7 @@
display: none;
}
#pagedown-editor {
.pagedown-editor {
width: 100%;
}

View file

@ -33,7 +33,7 @@ test("Change the topic template", (assert) => {
click('.edit-category');
click('.edit-category-topic-template');
fillIn('#wmd-input', 'this is the new topic template');
fillIn('.wmd-input', 'this is the new topic template');
click('#save-category');
andThen(() => {
assert.ok(!visible('#discourse-modal'), 'it closes the modal');

View file

@ -10,19 +10,19 @@ test("Tests the Composer controls", () => {
click('#create-topic');
andThen(() => {
ok(exists('#wmd-input'), 'the composer input is visible');
ok(exists('.wmd-input'), 'the composer input is visible');
ok(exists('.title-input .popup-tip.bad.hide'), 'title errors are hidden by default');
ok(exists('.textarea-wrapper .popup-tip.bad.hide'), 'body errors are hidden by default');
});
click('a.toggle-preview');
andThen(() => {
ok(!exists('#wmd-preview:visible'), "clicking the toggle hides the preview");
ok(!exists('.wmd-preview:visible'), "clicking the toggle hides the preview");
});
click('a.toggle-preview');
andThen(() => {
ok(exists('#wmd-preview:visible'), "clicking the toggle shows the preview again");
ok(exists('.wmd-preview:visible'), "clicking the toggle shows the preview again");
});
click('#reply-control button.create');
@ -36,9 +36,9 @@ test("Tests the Composer controls", () => {
ok(exists('.title-input .popup-tip.good'), 'the title is now good');
});
fillIn('#wmd-input', "this is the *content* of a post");
fillIn('.wmd-input', "this is the *content* of a post");
andThen(() => {
equal(find('#wmd-preview').html(), "<p>this is the <em>content</em> of a post</p>", "it previews content");
equal(find('.wmd-preview').html(), "<p>this is the <em>content</em> of a post</p>", "it previews content");
ok(exists('.textarea-wrapper .popup-tip.good'), 'the body is now good');
});
@ -58,7 +58,7 @@ test("Create a topic with server side errors", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', "this title triggers an error");
fillIn('#wmd-input', "this is the *content* of a post");
fillIn('.wmd-input', "this is the *content* of a post");
click('#reply-control button.create');
andThen(() => {
ok(exists('.bootbox.modal'), 'it pops up an error message');
@ -66,7 +66,7 @@ test("Create a topic with server side errors", () => {
click('.bootbox.modal a.btn-primary');
andThen(() => {
ok(!exists('.bootbox.modal'), 'it dismisses the error');
ok(exists('#wmd-input'), 'the composer input is visible');
ok(exists('.wmd-input'), 'the composer input is visible');
});
});
@ -74,7 +74,7 @@ test("Create a Topic", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', "Internationalization Localization");
fillIn('#wmd-input', "this is the *content* of a new topic post");
fillIn('.wmd-input', "this is the *content* of a new topic post");
click('#reply-control button.create');
andThen(() => {
equal(currentURL(), "/t/internationalization-localization/280", "it transitions to the newly created topic URL");
@ -85,7 +85,7 @@ test("Create an enqueued Topic", () => {
visit("/");
click('#create-topic');
fillIn('#reply-title', "Internationalization Localization");
fillIn('#wmd-input', "enqueue this content please");
fillIn('.wmd-input', "enqueue this content please");
click('#reply-control button.create');
andThen(() => {
ok(visible('#discourse-modal'), 'it pops up a modal');
@ -108,11 +108,11 @@ test("Create a Reply", () => {
click('#topic-footer-buttons .btn.create');
andThen(() => {
ok(exists('#wmd-input'), 'the composer input is visible');
ok(exists('.wmd-input'), 'the composer input is visible');
ok(!exists('#reply-title'), 'there is no title since this is a reply');
});
fillIn('#wmd-input', 'this is the content of my reply');
fillIn('.wmd-input', 'this is the content of my reply');
click('#reply-control button.create');
andThen(() => {
equal(find('.cooked:last p').text(), 'this is the content of my reply');
@ -122,7 +122,7 @@ test("Create a Reply", () => {
test("Posting on a different topic", (assert) => {
visit("/t/internationalization-localization/280");
click('#topic-footer-buttons .btn.create');
fillIn('#wmd-input', 'this is the content for a different topic');
fillIn('.wmd-input', 'this is the content for a different topic');
visit("/t/1-3-0beta9-no-rate-limit-popups/28830");
andThen(function() {
@ -145,11 +145,11 @@ test("Create an enqueued Reply", () => {
click('#topic-footer-buttons .btn.create');
andThen(() => {
ok(exists('#wmd-input'), 'the composer input is visible');
ok(exists('.wmd-input'), 'the composer input is visible');
ok(!exists('#reply-title'), 'there is no title since this is a reply');
});
fillIn('#wmd-input', 'enqueue this content please');
fillIn('.wmd-input', 'enqueue this content please');
click('#reply-control button.create');
andThen(() => {
ok(find('.cooked:last p').text() !== 'enqueue this content please', "it doesn't insert the post");
@ -173,14 +173,14 @@ test("Edit the first post", () => {
click('.topic-post:eq(0) button[data-action=showMoreActions]');
click('.topic-post:eq(0) button[data-action=edit]');
andThen(() => {
equal(find('#wmd-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text');
equal(find('.wmd-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text');
});
fillIn('#wmd-input', "This is the new text for the post");
fillIn('.wmd-input', "This is the new text for the post");
fillIn('#reply-title', "This is the new text for the title");
click('#reply-control button.create');
andThen(() => {
ok(!exists('#wmd-input'), 'it closes the composer');
ok(!exists('.wmd-input'), 'it closes the composer');
ok(exists('.topic-post:eq(0) .post-info.edits'), 'it has the edits icon');
ok(find('#topic-title h1').text().indexOf('This is the new text for the title') !== -1, 'it shows the new title');
ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post');