mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 01:26:18 -05:00
FEATURE: Replace composer editor with ember version
This commit is contained in:
parent
fc27b7442f
commit
47495a5713
51 changed files with 771 additions and 3420 deletions
|
@ -6,7 +6,6 @@ languages:
|
||||||
|
|
||||||
exclude_paths:
|
exclude_paths:
|
||||||
- "app/assets/javascripts/defer/*"
|
- "app/assets/javascripts/defer/*"
|
||||||
- "app/assets/javascripts/discourse/lib/Markdown.Editor.js"
|
|
||||||
- "app/assets/javascripts/ember-addons/*"
|
- "app/assets/javascripts/ember-addons/*"
|
||||||
- "lib/autospec/*"
|
- "lib/autospec/*"
|
||||||
- "lib/es6_module_transpiler/*"
|
- "lib/es6_module_transpiler/*"
|
||||||
|
|
|
@ -6,9 +6,7 @@ app/assets/javascripts/pagedown_custom.js
|
||||||
app/assets/javascripts/vendor.js
|
app/assets/javascripts/vendor.js
|
||||||
app/assets/javascripts/locales/i18n.js
|
app/assets/javascripts/locales/i18n.js
|
||||||
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
||||||
app/assets/javascripts/discourse/lib/Markdown.Editor.js
|
|
||||||
app/assets/javascripts/ember-addons/
|
app/assets/javascripts/ember-addons/
|
||||||
jsapp/lib/Markdown.Editor.js
|
|
||||||
lib/javascripts/locale/
|
lib/javascripts/locale/
|
||||||
lib/javascripts/messageformat.js
|
lib/javascripts/messageformat.js
|
||||||
lib/javascripts/moment.js
|
lib/javascripts/moment.js
|
||||||
|
|
|
@ -0,0 +1,354 @@
|
||||||
|
import userSearch from 'discourse/lib/user-search';
|
||||||
|
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ['wmd-controls'],
|
||||||
|
classNameBindings: [':wmd-controls', 'showPreview', 'showPreview::hide-preview'],
|
||||||
|
|
||||||
|
uploadProgress: 0,
|
||||||
|
showPreview: true,
|
||||||
|
_xhr: null,
|
||||||
|
|
||||||
|
@computed
|
||||||
|
uploadPlaceholder() {
|
||||||
|
return `[${I18n.t('uploading')}]() `;
|
||||||
|
},
|
||||||
|
|
||||||
|
@on('init')
|
||||||
|
_setupPreview() {
|
||||||
|
const val = (Discourse.Mobile.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
|
||||||
|
this.set('showPreview', val === 'true');
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('showPreview')
|
||||||
|
toggleText: function(showPreview) {
|
||||||
|
return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
markdownOptions() {
|
||||||
|
return {
|
||||||
|
lookupAvatarByPostNumber: (postNumber, topicId) => {
|
||||||
|
const topic = this.get('topic');
|
||||||
|
if (!topic) { return; }
|
||||||
|
|
||||||
|
const posts = topic.get('postStream.posts');
|
||||||
|
if (posts && topicId === topic.get('id')) {
|
||||||
|
const quotedPost = posts.findProperty("post_number", postNumber);
|
||||||
|
if (quotedPost) {
|
||||||
|
return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
@on('didInsertElement')
|
||||||
|
_composerEditorInit() {
|
||||||
|
const topicId = this.get('topic.id');
|
||||||
|
const template = this.container.lookup('template:user-selector-autocomplete.raw');
|
||||||
|
const $input = this.$('.d-editor-input');
|
||||||
|
$input.autocomplete({
|
||||||
|
template,
|
||||||
|
dataSource: term => userSearch({ term, topicId, includeGroups: true }),
|
||||||
|
key: "@",
|
||||||
|
transformComplete: v => v.username || v.usernames.join(", @")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus on the body unless we have a title
|
||||||
|
if (!this.get('composer.canEditTitle') && !Discourse.Mobile.mobileView) {
|
||||||
|
this.$('.d-editor-input').putCursorAtEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bindUploadTarget();
|
||||||
|
this.appEvents.trigger('composer:opened');
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('composer.reply', 'composer.replyLength', 'composer.missingReplyCharacters', 'composer.minimumPostLength', 'lastValidatedAt')
|
||||||
|
validation(reply, replyLength, missingReplyCharacters, minimumPostLength, lastValidatedAt) {
|
||||||
|
const postType = this.get('composer.post.post_type');
|
||||||
|
if (postType === this.site.get('post_types.small_action')) { return; }
|
||||||
|
|
||||||
|
let reason;
|
||||||
|
if (replyLength < 1) {
|
||||||
|
reason = I18n.t('composer.error.post_missing');
|
||||||
|
} else if (missingReplyCharacters > 0) {
|
||||||
|
reason = I18n.t('composer.error.post_length', {min: minimumPostLength});
|
||||||
|
const tl = Discourse.User.currentProp("trust_level");
|
||||||
|
if (tl === 0 || tl === 1) {
|
||||||
|
reason += "<br/>" + I18n.t('composer.error.try_like');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
return Discourse.InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderUnseen: function($preview, unseen) {
|
||||||
|
fetchUnseenMentions($preview, unseen, this.siteSettings).then(() => {
|
||||||
|
linkSeenMentions($preview, this.siteSettings);
|
||||||
|
this.trigger('previewRefreshed', $preview);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetUpload() {
|
||||||
|
this.setProperties({ uploadProgress: 0, isUploading: false });
|
||||||
|
this.set('composer.reply', this.get('composer.reply').replace(this.get('uploadPlaceholder'), ""));
|
||||||
|
},
|
||||||
|
|
||||||
|
_bindUploadTarget() {
|
||||||
|
this._unbindUploadTarget(); // in case it's still bound, let's clean it up first
|
||||||
|
|
||||||
|
const $element = this.$();;
|
||||||
|
const csrf = this.session.get('csrfToken');
|
||||||
|
const uploadPlaceholder = this.get('uploadPlaceholder');
|
||||||
|
|
||||||
|
$element.fileupload({
|
||||||
|
url: Discourse.getURL(`/uploads.json?client_id=${this.messageBus.clientId}&authenticity_token=${encodeURIComponent(csrf)}`),
|
||||||
|
dataType: "json",
|
||||||
|
pasteZone: $element,
|
||||||
|
});
|
||||||
|
|
||||||
|
$element.on('fileuploadsubmit', (e, data) => {
|
||||||
|
const isUploading = Discourse.Utilities.validateUploadedFiles(data.files);
|
||||||
|
data.formData = { type: "composer" };
|
||||||
|
this.setProperties({ uploadProgress: 0, isUploading });
|
||||||
|
return isUploading;
|
||||||
|
});
|
||||||
|
|
||||||
|
$element.on("fileuploadprogressall", (e, data) => {
|
||||||
|
this.set("uploadProgress", parseInt(data.loaded / data.total * 100, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
$element.on("fileuploadsend", (e, data) => {
|
||||||
|
// add upload placeholder
|
||||||
|
this.appEvents.trigger('composer:insert-text', uploadPlaceholder);
|
||||||
|
|
||||||
|
if (data.xhr) {
|
||||||
|
this._xhr = data.xhr();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$element.on("fileuploadfail", (e, data) => {
|
||||||
|
this._resetUpload();
|
||||||
|
|
||||||
|
const userCancelled = this._xhr && this._xhr._userCancelled;
|
||||||
|
this._xhr = null;
|
||||||
|
|
||||||
|
if (!userCancelled) {
|
||||||
|
Discourse.Utilities.displayErrorForUpload(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.messageBus.subscribe("/uploads/composer", upload => {
|
||||||
|
// replace upload placeholder
|
||||||
|
if (upload && upload.url) {
|
||||||
|
if (!this._xhr || !this._xhr._userCancelled) {
|
||||||
|
const markdown = Discourse.Utilities.getUploadMarkdown(upload);
|
||||||
|
this.set('composer.reply', this.get('composer.reply').replace(uploadPlaceholder, markdown));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Discourse.Utilities.displayErrorForUpload(upload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset upload state
|
||||||
|
this._resetUpload();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Discourse.Mobile.mobileView) {
|
||||||
|
this.$(".mobile-file-upload").on("click.uploader", function () {
|
||||||
|
// redirect the click on the hidden file input
|
||||||
|
$("#mobile-uploader").click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._firefoxPastingHack();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Believe it or not pasting an image in Firefox doesn't work without this code
|
||||||
|
_firefoxPastingHack() {
|
||||||
|
const uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/);
|
||||||
|
if (uaMatch && parseInt(uaMatch[1]) >= 24) {
|
||||||
|
this.$().append( Ember.$("<div id='contenteditable' contenteditable='true' style='height: 0; width: 0; overflow: hidden'></div>") );
|
||||||
|
this.$("textarea").off('keydown.contenteditable');
|
||||||
|
this.$("textarea").on('keydown.contenteditable', event => {
|
||||||
|
// Catch Ctrl+v / Cmd+v and hijack focus to a contenteditable div. We can't
|
||||||
|
// use the onpaste event because for some reason the paste isn't resumed
|
||||||
|
// after we switch focus, probably because it is being executed too late.
|
||||||
|
if ((event.ctrlKey || event.metaKey) && (event.keyCode === 86)) {
|
||||||
|
// Save the current textarea selection.
|
||||||
|
const textarea = this.$("textarea")[0];
|
||||||
|
const selectionStart = textarea.selectionStart;
|
||||||
|
const selectionEnd = textarea.selectionEnd;
|
||||||
|
|
||||||
|
// Focus the contenteditable div.
|
||||||
|
const contentEditableDiv = this.$('#contenteditable');
|
||||||
|
contentEditableDiv.focus();
|
||||||
|
|
||||||
|
// The paste doesn't finish immediately and we don't have any onpaste
|
||||||
|
// event, so wait for 100ms which _should_ be enough time.
|
||||||
|
setTimeout(() => {
|
||||||
|
const pastedImg = contentEditableDiv.find('img');
|
||||||
|
|
||||||
|
if ( pastedImg.length === 1 ) {
|
||||||
|
pastedImg.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For restoring the selection.
|
||||||
|
textarea.focus();
|
||||||
|
const textareaContent = $(textarea).val(),
|
||||||
|
startContent = textareaContent.substring(0, selectionStart),
|
||||||
|
endContent = textareaContent.substring(selectionEnd);
|
||||||
|
|
||||||
|
const restoreSelection = function(pastedText) {
|
||||||
|
$(textarea).val( startContent + pastedText + endContent );
|
||||||
|
textarea.selectionStart = selectionStart + pastedText.length;
|
||||||
|
textarea.selectionEnd = textarea.selectionStart;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (contentEditableDiv.html().length > 0) {
|
||||||
|
// If the image wasn't the only pasted content we just give up and
|
||||||
|
// fall back to the original pasted text.
|
||||||
|
contentEditableDiv.find("br").replaceWith("\n");
|
||||||
|
restoreSelection(contentEditableDiv.text());
|
||||||
|
} else {
|
||||||
|
// Depending on how the image is pasted in, we may get either a
|
||||||
|
// normal URL or a data URI. If we get a data URI we can convert it
|
||||||
|
// to a Blob and upload that, but if it is a regular URL that
|
||||||
|
// operation is prevented for security purposes. When we get a regular
|
||||||
|
// URL let's just create an <img> tag for the image.
|
||||||
|
const imageSrc = pastedImg.attr('src');
|
||||||
|
|
||||||
|
if (imageSrc.match(/^data:image/)) {
|
||||||
|
// Restore the cursor position, and remove any selected text.
|
||||||
|
restoreSelection("");
|
||||||
|
|
||||||
|
// Create a Blob to upload.
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = function() {
|
||||||
|
// Create a new canvas.
|
||||||
|
const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
|
||||||
|
canvas.height = image.height;
|
||||||
|
canvas.width = image.width;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
canvas.toBlob(blob => this.$().fileupload('add', {files: blob}));
|
||||||
|
};
|
||||||
|
image.src = imageSrc;
|
||||||
|
} else {
|
||||||
|
restoreSelection("<img src='" + imageSrc + "'>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentEditableDiv.html('');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@on('willDestroyElement')
|
||||||
|
_unbindUploadTarget() {
|
||||||
|
this.$(".mobile-file-upload").off("click.uploader");
|
||||||
|
this.messageBus.unsubscribe("/uploads/composer");
|
||||||
|
const $uploadTarget = this.$();
|
||||||
|
try { $uploadTarget.fileupload("destroy"); }
|
||||||
|
catch (e) { /* wasn't initialized yet */ }
|
||||||
|
$uploadTarget.off();
|
||||||
|
},
|
||||||
|
|
||||||
|
@on('willDestroyElement')
|
||||||
|
_composerClosed() {
|
||||||
|
Ember.run.next(() => {
|
||||||
|
$('#main-outlet').css('padding-bottom', 0);
|
||||||
|
// need to wait a bit for the "slide down" transition of the composer
|
||||||
|
Ember.run.later(() => this.appEvents.trigger("composer:closed"), 400);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
importQuote(toolbarEvent) {
|
||||||
|
this.sendAction('importQuote', toolbarEvent);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelUpload() {
|
||||||
|
if (this._xhr) {
|
||||||
|
this._xhr._userCancelled = true;
|
||||||
|
this._xhr.abort();
|
||||||
|
this._resetUpload();
|
||||||
|
}
|
||||||
|
this._resetUpload();
|
||||||
|
},
|
||||||
|
|
||||||
|
showOptions() {
|
||||||
|
const myPos = this.$().position();
|
||||||
|
const buttonPos = this.$('.options').position();
|
||||||
|
|
||||||
|
this.sendAction('showOptions', { position: "absolute",
|
||||||
|
left: myPos.left + buttonPos.left,
|
||||||
|
top: myPos.top + buttonPos.top });
|
||||||
|
},
|
||||||
|
|
||||||
|
showUploadModal(toolbarEvent) {
|
||||||
|
this.sendAction('showUploadSelector', toolbarEvent);
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePreview() {
|
||||||
|
this.toggleProperty('showPreview');
|
||||||
|
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
||||||
|
},
|
||||||
|
|
||||||
|
extraButtons(toolbar) {
|
||||||
|
toolbar.addButton({
|
||||||
|
id: 'quote',
|
||||||
|
group: 'fontStyles',
|
||||||
|
icon: 'comment-o',
|
||||||
|
sendAction: 'importQuote',
|
||||||
|
title: 'composer.quote_post_title',
|
||||||
|
unshift: true
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbar.addButton({
|
||||||
|
id: 'upload',
|
||||||
|
group: 'insertions',
|
||||||
|
icon: 'upload',
|
||||||
|
title: 'upload',
|
||||||
|
sendAction: 'showUploadModal'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.get('canWhisper')) {
|
||||||
|
toolbar.addButton({
|
||||||
|
id: 'options',
|
||||||
|
group: 'extras',
|
||||||
|
icon: 'gear',
|
||||||
|
title: 'composer.options',
|
||||||
|
sendAction: 'showOptions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
previewUpdated($preview) {
|
||||||
|
// Paint mentions
|
||||||
|
const unseen = linkSeenMentions($preview, this.siteSettings);
|
||||||
|
if (unseen.length) {
|
||||||
|
Ember.run.debounce(this, this._renderUnseen, $preview, unseen, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = this.get('composer.post');
|
||||||
|
let refresh = false;
|
||||||
|
|
||||||
|
// If we are editing a post, we'll refresh its contents once. This is a feature that
|
||||||
|
// allows a user to refresh its contents once.
|
||||||
|
if (post && !post.get('refreshedPost')) {
|
||||||
|
refresh = true;
|
||||||
|
post.set('refreshedPost', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint oneboxes
|
||||||
|
$('a.onebox', $preview).each((i, e) => Discourse.Onebox.load(e, refresh));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,15 +0,0 @@
|
||||||
export default Ember.TextArea.extend({
|
|
||||||
classNameBindings: [':wmd-input'],
|
|
||||||
|
|
||||||
placeholder: function() {
|
|
||||||
return I18n.t('composer.reply_placeholder');
|
|
||||||
}.property('placeholderKey'),
|
|
||||||
|
|
||||||
_signalParentInsert: function() {
|
|
||||||
this.get('parentView').childDidInsertElement(this);
|
|
||||||
}.on('didInsertElement'),
|
|
||||||
|
|
||||||
_signalParentDestroy: function() {
|
|
||||||
this.get('parentView').childWillDestroyElement(this);
|
|
||||||
}.on('willDestroyElement')
|
|
||||||
});
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ['title-input'],
|
||||||
|
|
||||||
|
@on('didInsertElement')
|
||||||
|
_focusOnReply() {
|
||||||
|
if (!Discourse.Mobile.mobileView) {
|
||||||
|
this.$('input').putCursorAtEnd();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('composer.titleLength', 'composer.missingTitleCharacters', 'composer.minimumTitleLength', 'lastValidatedAt')
|
||||||
|
validation(titleLength, missingTitleChars, minimumTitleLength, lastValidatedAt) {
|
||||||
|
|
||||||
|
let reason;
|
||||||
|
if (titleLength < 1) {
|
||||||
|
reason = I18n.t('composer.error.title_missing');
|
||||||
|
} else if (missingTitleChars > 0) {
|
||||||
|
reason = I18n.t('composer.error.title_too_short', {min: minimumTitleLength});
|
||||||
|
} else if (titleLength > this.siteSettings.max_topic_title_length) {
|
||||||
|
reason = I18n.t('composer.error.title_too_long', {max: this.siteSettings.max_topic_title_length});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
return Discourse.InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
/*global Mousetrap:true */
|
/*global Mousetrap:true */
|
||||||
import loadScript from 'discourse/lib/load-script';
|
import loadScript from 'discourse/lib/load-script';
|
||||||
import { default as property, on } from 'ember-addons/ember-computed-decorators';
|
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||||
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
|
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
|
||||||
|
|
||||||
// Our head can be a static string or a function that returns a string
|
// Our head can be a static string or a function that returns a string
|
||||||
|
@ -111,6 +111,10 @@ Toolbar.prototype.addButton = function(button) {
|
||||||
perform: button.perform || Ember.K
|
perform: button.perform || Ember.K
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (button.sendAction) {
|
||||||
|
createdButton.sendAction = button.sendAction;
|
||||||
|
}
|
||||||
|
|
||||||
const title = I18n.t(button.title || `composer.${button.id}_title`);
|
const title = I18n.t(button.title || `composer.${button.id}_title`);
|
||||||
if (button.shortcut) {
|
if (button.shortcut) {
|
||||||
const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||||
|
@ -130,7 +134,11 @@ Toolbar.prototype.addButton = function(button) {
|
||||||
createdButton.title = title;
|
createdButton.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
g.buttons.push(createdButton);
|
if (button.unshift) {
|
||||||
|
g.buttons.unshift(createdButton);
|
||||||
|
} else {
|
||||||
|
g.buttons.push(createdButton);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function onToolbarCreate(func) {
|
export function onToolbarCreate(func) {
|
||||||
|
@ -144,9 +152,16 @@ export default Ember.Component.extend({
|
||||||
link: '',
|
link: '',
|
||||||
lastSel: null,
|
lastSel: null,
|
||||||
|
|
||||||
|
@computed('placeholder')
|
||||||
|
placeholderTranslated(placeholder) {
|
||||||
|
if (placeholder) return I18n.t(placeholder);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
@on('didInsertElement')
|
@on('didInsertElement')
|
||||||
_startUp() {
|
_startUp() {
|
||||||
this._applyEmojiAutocomplete();
|
this._applyEmojiAutocomplete();
|
||||||
|
|
||||||
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
|
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
|
||||||
|
|
||||||
const shortcuts = this.get('toolbar.shortcuts');
|
const shortcuts = this.get('toolbar.shortcuts');
|
||||||
|
@ -156,27 +171,52 @@ export default Ember.Component.extend({
|
||||||
this.send(button.action, button);
|
this.send(button.action, button);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// disable clicking on links in the preview
|
||||||
|
this.$('.d-editor-preview').on('click.preview', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.appEvents.on('composer:insert-text', text => {
|
||||||
|
this._addText(this._getSelected(), text);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@on('willDestroyElement')
|
@on('willDestroyElement')
|
||||||
_shutDown() {
|
_shutDown() {
|
||||||
|
this.appEvents.off('composer:insert-text');
|
||||||
|
|
||||||
Ember.keys(this.get('toolbar.shortcuts')).forEach(sc => {
|
Ember.keys(this.get('toolbar.shortcuts')).forEach(sc => {
|
||||||
Mousetrap(this.$('.d-editor-input')[0]).unbind(sc);
|
Mousetrap(this.$('.d-editor-input')[0]).unbind(sc);
|
||||||
});
|
});
|
||||||
|
this.$('.d-editor-preview').off('click.preview');
|
||||||
},
|
},
|
||||||
|
|
||||||
@property
|
@computed
|
||||||
toolbar() {
|
toolbar() {
|
||||||
const toolbar = new Toolbar();
|
const toolbar = new Toolbar();
|
||||||
_createCallbacks.forEach(cb => cb(toolbar));
|
_createCallbacks.forEach(cb => cb(toolbar));
|
||||||
|
this.sendAction('extraButtons', toolbar);
|
||||||
return toolbar;
|
return toolbar;
|
||||||
},
|
},
|
||||||
|
|
||||||
@property('ready', 'value')
|
@computed('ready', 'value')
|
||||||
preview(ready, value) {
|
preview(ready, value) {
|
||||||
if (!ready) { return; }
|
if (!ready) { return; }
|
||||||
|
|
||||||
const text = Discourse.Dialect.cook(value || "", {sanitize: true});
|
const markdownOptions = this.get('markdownOptions') || {};
|
||||||
|
markdownOptions.sanitize = true;
|
||||||
|
|
||||||
|
const text = Discourse.Dialect.cook(value || "", markdownOptions);
|
||||||
|
Ember.run.scheduleOnce('afterRender', () => {
|
||||||
|
if (this._state !== "inDOM") { return; }
|
||||||
|
const $preview = this.$('.d-editor-preview');
|
||||||
|
if ($preview.length === 0) return;
|
||||||
|
|
||||||
|
this.sendAction('previewUpdated', $preview);
|
||||||
|
});
|
||||||
|
|
||||||
return text ? text : "";
|
return text ? text : "";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -339,12 +379,18 @@ export default Ember.Component.extend({
|
||||||
actions: {
|
actions: {
|
||||||
toolbarButton(button) {
|
toolbarButton(button) {
|
||||||
const selected = this._getSelected();
|
const selected = this._getSelected();
|
||||||
button.perform({
|
const toolbarEvent = {
|
||||||
selected,
|
selected,
|
||||||
applySurround: (head, tail, exampleKey) => this._applySurround(selected, head, tail, exampleKey),
|
applySurround: (head, tail, exampleKey) => this._applySurround(selected, head, tail, exampleKey),
|
||||||
applyList: (head, exampleKey) => this._applyList(selected, head, exampleKey),
|
applyList: (head, exampleKey) => this._applyList(selected, head, exampleKey),
|
||||||
addText: text => this._addText(selected, text)
|
addText: text => this._addText(selected, text)
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (button.sendAction) {
|
||||||
|
return this.sendAction(button.sendAction, toolbarEvent);
|
||||||
|
} else {
|
||||||
|
button.perform(toolbarEvent);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showLinkModal() {
|
showLinkModal() {
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default buildCategoryPanel('topic-template', {
|
||||||
if (this.get('activeTab')) {
|
if (this.get('activeTab')) {
|
||||||
const self = this;
|
const self = this;
|
||||||
Ember.run.schedule('afterRender', function() {
|
Ember.run.schedule('afterRender', function() {
|
||||||
self.$('.wmd-input').focus();
|
self.$('.d-editor-input').focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}.observes('activeTab')
|
}.observes('activeTab')
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import property from 'ember-addons/ember-computed-decorators';
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
import UploadMixin from "discourse/mixins/upload";
|
import UploadMixin from "discourse/mixins/upload";
|
||||||
|
|
||||||
export default Em.Component.extend(UploadMixin, {
|
export default Em.Component.extend(UploadMixin, {
|
||||||
classNames: ["image-uploader"],
|
classNames: ["image-uploader"],
|
||||||
|
|
||||||
@property('imageUrl')
|
@computed('imageUrl')
|
||||||
backgroundStyle(imageUrl) {
|
backgroundStyle(imageUrl) {
|
||||||
if (Em.isNone(imageUrl)) { return; }
|
if (Em.isNone(imageUrl)) { return; }
|
||||||
return `background-image: url(${imageUrl})`.htmlSafe();
|
return `background-image: url(${imageUrl})`.htmlSafe();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
export default Ember.Component.extend(StringBuffer, {
|
export default Ember.Component.extend(StringBuffer, {
|
||||||
classNameBindings: [':popup-tip', 'good', 'bad', 'shownAt::hide'],
|
classNameBindings: [':popup-tip', 'good', 'bad', 'lastShownAt::hide'],
|
||||||
animateAttribute: null,
|
animateAttribute: null,
|
||||||
bouncePixels: 6,
|
bouncePixels: 6,
|
||||||
bounceDelay: 100,
|
bounceDelay: 100,
|
||||||
|
@ -16,9 +16,14 @@ export default Ember.Component.extend(StringBuffer, {
|
||||||
bad: Ember.computed.alias("validation.failed"),
|
bad: Ember.computed.alias("validation.failed"),
|
||||||
good: Ember.computed.not("bad"),
|
good: Ember.computed.not("bad"),
|
||||||
|
|
||||||
@observes("shownAt")
|
@computed('shownAt', 'validation.lastShownAt')
|
||||||
|
lastShownAt(shownAt, lastShownAt) {
|
||||||
|
return shownAt || lastShownAt;
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('lastShownAt')
|
||||||
bounce() {
|
bounce() {
|
||||||
if (this.get("shownAt")) {
|
if (this.get("lastShownAt")) {
|
||||||
var $elem = this.$();
|
var $elem = this.$();
|
||||||
if (!this.animateAttribute) {
|
if (!this.animateAttribute) {
|
||||||
this.animateAttribute = $elem.css('left') === 'auto' ? 'right' : 'left';
|
this.animateAttribute = $elem.css('left') === 'auto' ? 'right' : 'left';
|
||||||
|
|
|
@ -2,7 +2,7 @@ const MAX_SHOWN = 5;
|
||||||
|
|
||||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||||
import property from 'ember-addons/ember-computed-decorators';
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
const { get, isEmpty, Component } = Ember;
|
const { get, isEmpty, Component } = Ember;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export default Component.extend(StringBuffer, {
|
||||||
rerenderTriggers: ['expanded'],
|
rerenderTriggers: ['expanded'],
|
||||||
|
|
||||||
// Roll up links to avoid duplicates
|
// Roll up links to avoid duplicates
|
||||||
@property('links')
|
@computed('links')
|
||||||
collapsed(links) {
|
collapsed(links) {
|
||||||
const seen = {};
|
const seen = {};
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { setting } from 'discourse/lib/computed';
|
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
import Quote from 'discourse/lib/quote';
|
import Quote from 'discourse/lib/quote';
|
||||||
import Draft from 'discourse/models/draft';
|
import Draft from 'discourse/models/draft';
|
||||||
import Composer from 'discourse/models/composer';
|
import Composer from 'discourse/models/composer';
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
function loadDraft(store, opts) {
|
function loadDraft(store, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
@ -50,17 +49,17 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
showEditReason: false,
|
showEditReason: false,
|
||||||
editReason: null,
|
editReason: null,
|
||||||
maxTitleLength: setting('max_topic_title_length'),
|
|
||||||
scopedCategoryId: null,
|
scopedCategoryId: null,
|
||||||
similarTopics: null,
|
similarTopics: null,
|
||||||
similarTopicsMessage: null,
|
similarTopicsMessage: null,
|
||||||
lastSimilaritySearch: null,
|
lastSimilaritySearch: null,
|
||||||
optionsVisible: false,
|
optionsVisible: false,
|
||||||
|
|
||||||
topic: null,
|
lastValidatedAt: null,
|
||||||
|
|
||||||
// TODO: Remove this, very bad
|
isUploading: false,
|
||||||
view: null,
|
|
||||||
|
topic: null,
|
||||||
|
|
||||||
_initializeSimilar: function() {
|
_initializeSimilar: function() {
|
||||||
this.set('similarTopics', []);
|
this.set('similarTopics', []);
|
||||||
|
@ -109,7 +108,7 @@ export default Ember.Controller.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Import a quote from the post
|
// Import a quote from the post
|
||||||
importQuote() {
|
importQuote(toolbarEvent) {
|
||||||
const postStream = this.get('topic.postStream');
|
const postStream = this.get('topic.postStream');
|
||||||
let postId = this.get('model.post.id');
|
let postId = this.get('model.post.id');
|
||||||
|
|
||||||
|
@ -135,7 +134,7 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
return this.store.find('post', postId).then(function(post) {
|
return this.store.find('post', postId).then(function(post) {
|
||||||
const quote = Quote.build(post, post.get("raw"), {raw: true, full: true});
|
const quote = Quote.build(post, post.get("raw"), {raw: true, full: true});
|
||||||
composer.appendBlockAtCursor(quote);
|
toolbarEvent.addText(quote);
|
||||||
composer.set('model.loading', false);
|
composer.set('model.loading', false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -173,39 +172,10 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
appendText(text, opts) {
|
|
||||||
const c = this.get('model');
|
|
||||||
if (c) {
|
|
||||||
opts = opts || {};
|
|
||||||
const wmd = $('.wmd-input'),
|
|
||||||
val = wmd.val() || '',
|
|
||||||
position = opts.position === "cursor" ? wmd.caret() : val.length,
|
|
||||||
caret = c.appendText(text, position, opts);
|
|
||||||
|
|
||||||
if (wmd[0]) {
|
|
||||||
Em.run.next(() => Discourse.Utilities.setCaretPosition(wmd[0], caret));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
appendTextAtCursor(text, opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
opts.position = "cursor";
|
|
||||||
this.appendText(text, opts);
|
|
||||||
},
|
|
||||||
|
|
||||||
appendBlockAtCursor(text, opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
opts.position = "cursor";
|
|
||||||
opts.block = true;
|
|
||||||
this.appendText(text, opts);
|
|
||||||
},
|
|
||||||
|
|
||||||
categories: function() {
|
categories: function() {
|
||||||
return Discourse.Category.list();
|
return Discourse.Category.list();
|
||||||
}.property(),
|
}.property(),
|
||||||
|
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.closeAutocomplete();
|
this.closeAutocomplete();
|
||||||
switch (this.get('model.composeState')) {
|
switch (this.get('model.composeState')) {
|
||||||
|
@ -225,7 +195,7 @@ export default Ember.Controller.extend({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
disableSubmit: Ember.computed.or("model.loading", "view.isUploading"),
|
disableSubmit: Ember.computed.or("model.loading", "isUploading"),
|
||||||
|
|
||||||
save(force) {
|
save(force) {
|
||||||
const composer = this.get('model');
|
const composer = this.get('model');
|
||||||
|
@ -237,12 +207,7 @@ export default Ember.Controller.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (composer.get('cantSubmitPost')) {
|
if (composer.get('cantSubmitPost')) {
|
||||||
const now = Date.now();
|
this.set('lastValidatedAt', Date.now());
|
||||||
this.setProperties({
|
|
||||||
'view.showTitleTip': now,
|
|
||||||
'view.showCategoryTip': now,
|
|
||||||
'view.showReplyTip': now
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,10 +256,18 @@ export default Ember.Controller.extend({
|
||||||
var staged = false;
|
var staged = false;
|
||||||
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
|
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
|
||||||
|
|
||||||
const promise = composer.save({
|
// TODO: This should not happen in model
|
||||||
imageSizes: this.get('view').imageSizes(),
|
const imageSizes = {};
|
||||||
editReason: this.get("editReason")
|
$('#reply-control .d-editor-preview img').each((i, e) => {
|
||||||
}).then(function(result) {
|
const $img = $(e);
|
||||||
|
const src = $img.prop('src');
|
||||||
|
|
||||||
|
if (src && src.length) {
|
||||||
|
imageSizes[src] = { width: $img.width(), height: $img.height() };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = composer.save({ imageSizes, editReason: this.get("editReason")}).then(function(result) {
|
||||||
if (result.responseJson.action === "enqueued") {
|
if (result.responseJson.action === "enqueued") {
|
||||||
self.send('postWasEnqueued', result.responseJson);
|
self.send('postWasEnqueued', result.responseJson);
|
||||||
self.destroyDraft();
|
self.destroyDraft();
|
||||||
|
@ -366,8 +339,8 @@ export default Ember.Controller.extend({
|
||||||
// We don't care about similar topics unless creating a topic
|
// We don't care about similar topics unless creating a topic
|
||||||
if (!this.get('model.creatingTopic')) { return; }
|
if (!this.get('model.creatingTopic')) { return; }
|
||||||
|
|
||||||
let body = this.get('model.reply');
|
let body = this.get('model.reply') || '';
|
||||||
const title = this.get('model.title');
|
const title = this.get('model.title') || '';
|
||||||
|
|
||||||
// Ensure the fields are of the minimum length
|
// Ensure the fields are of the minimum length
|
||||||
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
|
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
|
||||||
|
@ -405,11 +378,6 @@ export default Ember.Controller.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
saveDraft() {
|
|
||||||
const model = this.get('model');
|
|
||||||
if (model) { model.saveDraft(); }
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Open the composer view
|
Open the composer view
|
||||||
|
|
||||||
|
@ -502,7 +470,7 @@ export default Ember.Controller.extend({
|
||||||
composerModel.set('composeState', Discourse.Composer.OPEN);
|
composerModel.set('composeState', Discourse.Composer.OPEN);
|
||||||
composerModel.set('isWarning', false);
|
composerModel.set('isWarning', false);
|
||||||
|
|
||||||
if (opts.topicTitle && opts.topicTitle.length <= this.get('maxTitleLength')) {
|
if (opts.topicTitle && opts.topicTitle.length <= this.siteSettings.max_topic_title_length) {
|
||||||
this.set('model.title', opts.topicTitle);
|
this.set('model.title', opts.topicTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,7 +540,6 @@ export default Ember.Controller.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
shrink() {
|
shrink() {
|
||||||
if (this.get('model.replyDirty')) {
|
if (this.get('model.replyDirty')) {
|
||||||
this.collapse();
|
this.collapse();
|
||||||
|
@ -581,22 +548,34 @@ export default Ember.Controller.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_saveDraft() {
|
||||||
|
const model = this.get('model');
|
||||||
|
if (model) { model.saveDraft(); };
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('model.reply', 'model.title')
|
||||||
|
_shouldSaveDraft() {
|
||||||
|
Ember.run.debounce(this, this._saveDraft, 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('model.categoryId', 'lastValidatedAt')
|
||||||
|
categoryValidation(categoryId, lastValidatedAt) {
|
||||||
|
if( !this.siteSettings.allow_uncategorized_topics && !categoryId) {
|
||||||
|
return Discourse.InputValidation.create({ failed: true, reason: I18n.t('composer.error.category_missing'), lastShownAt: lastValidatedAt });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
collapse() {
|
collapse() {
|
||||||
this.saveDraft();
|
this._saveDraft();
|
||||||
this.set('model.composeState', Discourse.Composer.DRAFT);
|
this.set('model.composeState', Discourse.Composer.DRAFT);
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.setProperties({
|
this.setProperties({ model: null, lastValidatedAt: null });
|
||||||
model: null,
|
|
||||||
'view.showTitleTip': false,
|
|
||||||
'view.showCategoryTip': false,
|
|
||||||
'view.showReplyTip': false
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
closeAutocomplete() {
|
closeAutocomplete() {
|
||||||
$('.wmd-input').autocomplete({ cancel: true });
|
$('.d-editor-input').autocomplete({ cancel: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
showOptions() {
|
showOptions() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import loadScript from 'discourse/lib/load-script';
|
import loadScript from 'discourse/lib/load-script';
|
||||||
import Quote from 'discourse/lib/quote';
|
import Quote from 'discourse/lib/quote';
|
||||||
import property from 'ember-addons/ember-computed-decorators';
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
needs: ['topic', 'composer'],
|
needs: ['topic', 'composer'],
|
||||||
|
@ -9,7 +9,7 @@ export default Ember.Controller.extend({
|
||||||
loadScript('defer/html-sanitizer-bundle');
|
loadScript('defer/html-sanitizer-bundle');
|
||||||
}.on('init'),
|
}.on('init'),
|
||||||
|
|
||||||
@property('buffer', 'postId')
|
@computed('buffer', 'postId')
|
||||||
post(buffer, postId) {
|
post(buffer, postId) {
|
||||||
if (!postId || Ember.isEmpty(buffer)) { return null; }
|
if (!postId || Ember.isEmpty(buffer)) { return null; }
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ export default Ember.Controller.extend({
|
||||||
const quotedText = Quote.build(post, buffer);
|
const quotedText = Quote.build(post, buffer);
|
||||||
composerOpts.quote = quotedText;
|
composerOpts.quote = quotedText;
|
||||||
if (composerController.get('content.viewOpen') || composerController.get('content.viewDraft')) {
|
if (composerController.get('content.viewOpen') || composerController.get('content.viewDraft')) {
|
||||||
composerController.appendBlockAtCursor(quotedText.trim());
|
this.appEvents.trigger('composer:insert-text', quotedText.trim());
|
||||||
} else {
|
} else {
|
||||||
composerController.open(composerOpts);
|
composerController.open(composerOpts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import SelectedPostsCount from 'discourse/mixins/selected-posts-count';
|
||||||
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
|
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
|
||||||
import Topic from 'discourse/models/topic';
|
import Topic from 'discourse/models/topic';
|
||||||
import Quote from 'discourse/lib/quote';
|
import Quote from 'discourse/lib/quote';
|
||||||
import { setting } from 'discourse/lib/computed';
|
|
||||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
@ -24,8 +23,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
||||||
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
|
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
|
||||||
|
|
||||||
maxTitleLength: setting('max_topic_title_length'),
|
|
||||||
|
|
||||||
_titleChanged: function() {
|
_titleChanged: function() {
|
||||||
const title = this.get('model.title');
|
const title = this.get('model.title');
|
||||||
if (!Ember.isEmpty(title)) {
|
if (!Ember.isEmpty(title)) {
|
||||||
|
|
|
@ -1,14 +1,58 @@
|
||||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||||
|
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export function uploadTranslate(key, options) {
|
||||||
|
options = options || {};
|
||||||
|
if (Discourse.Utilities.allowsAttachments()) { key += "_with_attachments"; }
|
||||||
|
return I18n.t(`upload_selector.${key}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
export default Ember.Controller.extend(ModalFunctionality, {
|
export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
showMore: false,
|
showMore: false,
|
||||||
local: true,
|
local: true,
|
||||||
|
imageUrl: null,
|
||||||
|
imageLink: null,
|
||||||
remote: Ember.computed.not("local"),
|
remote: Ember.computed.not("local"),
|
||||||
|
|
||||||
|
@computed
|
||||||
|
uploadIcon() {
|
||||||
|
return Discourse.Utilities.allowsAttachments() ? "upload" : "picture-o";
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('controller.local')
|
||||||
|
tip(local) {
|
||||||
|
const source = local ? "local" : "remote";
|
||||||
|
const authorized_extensions = Discourse.Utilities.authorizesAllExtensions() ? "" : `(${Discourse.Utilities.authorizedExtensions()})`;
|
||||||
|
return uploadTranslate(`${source}_tip`, { authorized_extensions });
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
useLocal() { this.setProperties({ local: true, showMore: false}); },
|
upload() {
|
||||||
useRemote() { this.set("local", false); },
|
if (this.get('local')) {
|
||||||
toggleShowMore() { this.toggleProperty("showMore"); }
|
$('#reply-control').fileupload('add', { fileInput: $('#filename-input') });
|
||||||
|
} else {
|
||||||
|
const imageUrl = this.get('imageUrl') || '';
|
||||||
|
const imageLink = this.get('imageLink') || '';
|
||||||
|
const toolbarEvent = this.get('toolbarEvent');
|
||||||
|
|
||||||
|
if (this.get('showMore') && imageLink.length > 3) {
|
||||||
|
toolbarEvent.addText(`[![](${imageUrl})](${imageLink})`);
|
||||||
|
} else {
|
||||||
|
toolbarEvent.addText(imageUrl);
|
||||||
|
}
|
||||||
|
this.send('closeModal');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useLocal() {
|
||||||
|
this.setProperties({ local: true, showMore: false});
|
||||||
|
},
|
||||||
|
useRemote() {
|
||||||
|
this.set("local", false);
|
||||||
|
},
|
||||||
|
toggleShowMore() {
|
||||||
|
this.toggleProperty("showMore");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
|
|
||||||
import { onToolbarCreate } from 'discourse/components/d-editor';
|
import { onToolbarCreate } from 'discourse/components/d-editor';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -8,7 +7,6 @@ export default {
|
||||||
const siteSettings = container.lookup('site-settings:main');
|
const siteSettings = container.lookup('site-settings:main');
|
||||||
|
|
||||||
if (siteSettings.enable_emoji) {
|
if (siteSettings.enable_emoji) {
|
||||||
|
|
||||||
onToolbarCreate(toolbar => {
|
onToolbarCreate(toolbar => {
|
||||||
toolbar.addButton({
|
toolbar.addButton({
|
||||||
id: 'emoji',
|
id: 'emoji',
|
||||||
|
@ -20,20 +18,6 @@ export default {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.PagedownCustom.appendButtons.push({
|
|
||||||
id: 'wmd-emoji-button',
|
|
||||||
description: I18n.t("composer.emoji"),
|
|
||||||
execute() {
|
|
||||||
showSelector({
|
|
||||||
container,
|
|
||||||
onSelect(title) {
|
|
||||||
const composerController = container.lookup('controller:composer');
|
|
||||||
composerController.appendTextAtCursor(`:${title}:`, {space: true});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// enable plugin emojis
|
// enable plugin emojis
|
||||||
Discourse.Emoji.applyCustomEmojis();
|
Discourse.Emoji.applyCustomEmojis();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,6 @@ export default {
|
||||||
const style = 'max-width:' + width + 'px;' +
|
const style = 'max-width:' + width + 'px;' +
|
||||||
'max-height:' + height + 'px;';
|
'max-height:' + height + 'px;';
|
||||||
|
|
||||||
$('<style id="image-sizing-hack">#reply-control .wmd-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}</style>').appendTo('head');
|
$('<style id="image-sizing-hack">#reply-control .d-editor-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}</style>').appendTo('head');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,3 @@
|
||||||
/*global Markdown, console */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Contains methods to help us with markdown formatting.
|
Contains methods to help us with markdown formatting.
|
||||||
|
|
||||||
|
@ -152,58 +150,6 @@ Discourse.Markdown = {
|
||||||
return this.markdownConverter(opts).makeHtml(raw);
|
return this.markdownConverter(opts).makeHtml(raw);
|
||||||
},
|
},
|
||||||
|
|
||||||
createEditor: function(options) {
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
// By default we always sanitize content in the editor
|
|
||||||
options.sanitize = true;
|
|
||||||
|
|
||||||
var markdownConverter = Discourse.Markdown.markdownConverter(options);
|
|
||||||
|
|
||||||
var editorOptions = {
|
|
||||||
containerElement: options.containerElement,
|
|
||||||
strings: {
|
|
||||||
bold: I18n.t("composer.bold_title") + " <strong> Ctrl+B",
|
|
||||||
boldexample: I18n.t("composer.bold_text"),
|
|
||||||
|
|
||||||
italic: I18n.t("composer.italic_title") + " <em> Ctrl+I",
|
|
||||||
italicexample: I18n.t("composer.italic_text"),
|
|
||||||
|
|
||||||
link: I18n.t("composer.link_title") + " <a> Ctrl+L",
|
|
||||||
linkdescription: I18n.t("composer.link_description"),
|
|
||||||
linkdialog: "<p><b>" + I18n.t("composer.link_dialog_title") + "</b></p><p>http://example.com/ \"" +
|
|
||||||
I18n.t("composer.link_optional_text") + "\"</p>",
|
|
||||||
|
|
||||||
quote: I18n.t("composer.quote_title") + " <blockquote> Ctrl+Q",
|
|
||||||
quoteexample: I18n.t("composer.quote_text"),
|
|
||||||
|
|
||||||
code: I18n.t("composer.code_title") + " <pre><code> Ctrl+K",
|
|
||||||
codeexample: I18n.t("composer.code_text"),
|
|
||||||
|
|
||||||
image: I18n.t("composer.upload_title") + " - Ctrl+G",
|
|
||||||
imagedescription: I18n.t("composer.upload_description"),
|
|
||||||
|
|
||||||
olist: I18n.t("composer.olist_title") + " <ol> Ctrl+O",
|
|
||||||
ulist: I18n.t("composer.ulist_title") + " <ul> Ctrl+U",
|
|
||||||
litem: I18n.t("composer.list_item"),
|
|
||||||
|
|
||||||
heading: I18n.t("composer.heading_title") + " <h1>/<h2> Ctrl+H",
|
|
||||||
headingexample: I18n.t("composer.heading_text"),
|
|
||||||
|
|
||||||
hr: I18n.t("composer.hr_title") + " <hr> Ctrl+R",
|
|
||||||
|
|
||||||
undo: I18n.t("composer.undo_title") + " - Ctrl+Z",
|
|
||||||
redo: I18n.t("composer.redo_title") + " - Ctrl+Y",
|
|
||||||
redomac: I18n.t("composer.redo_title") + " - Ctrl+Shift+Z",
|
|
||||||
|
|
||||||
help: I18n.t("composer.help")
|
|
||||||
},
|
|
||||||
appendButtons: options.appendButtons
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Markdown.Editor(markdownConverter, undefined, editorOptions);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks to see if a URL is allowed in the cooked content
|
Checks to see if a URL is allowed in the cooked content
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default (name, opts) => {
|
export default function(name, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
|
||||||
if (opts.__type) {
|
if (opts.__type) {
|
||||||
|
|
|
@ -215,10 +215,6 @@ Discourse.Utilities = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getUploadPlaceholder: function() {
|
|
||||||
return "[" + I18n.t("uploading") + "]() ";
|
|
||||||
},
|
|
||||||
|
|
||||||
isAnImage: function(path) {
|
isAnImage: function(path) {
|
||||||
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
|
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
|
||||||
},
|
},
|
||||||
|
|
|
@ -173,11 +173,6 @@ const Composer = RestModel.extend({
|
||||||
|
|
||||||
}.property('action', 'post', 'topic', 'topic.title'),
|
}.property('action', 'post', 'topic', 'topic.title'),
|
||||||
|
|
||||||
toggleText: function() {
|
|
||||||
return this.get('showPreview') ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
|
|
||||||
}.property('showPreview'),
|
|
||||||
|
|
||||||
hidePreview: Em.computed.not('showPreview'),
|
|
||||||
|
|
||||||
// whether to disable the post button
|
// whether to disable the post button
|
||||||
cantSubmitPost: function() {
|
cantSubmitPost: function() {
|
||||||
|
@ -311,8 +306,6 @@ const Composer = RestModel.extend({
|
||||||
}.property('reply'),
|
}.property('reply'),
|
||||||
|
|
||||||
_setupComposer: function() {
|
_setupComposer: function() {
|
||||||
const val = (Discourse.Mobile.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true'));
|
|
||||||
this.set('showPreview', val === 'true');
|
|
||||||
this.set('archetypeId', this.site.get('default_archetype'));
|
this.set('archetypeId', this.site.get('default_archetype'));
|
||||||
}.on('init'),
|
}.on('init'),
|
||||||
|
|
||||||
|
@ -364,11 +357,6 @@ const Composer = RestModel.extend({
|
||||||
return before.length + text.length;
|
return before.length + text.length;
|
||||||
},
|
},
|
||||||
|
|
||||||
togglePreview() {
|
|
||||||
this.toggleProperty('showPreview');
|
|
||||||
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
|
||||||
},
|
|
||||||
|
|
||||||
applyTopicTemplate(oldCategoryId, categoryId) {
|
applyTopicTemplate(oldCategoryId, categoryId) {
|
||||||
if (this.get('action') !== CREATE_TOPIC) { return; }
|
if (this.get('action') !== CREATE_TOPIC) { return; }
|
||||||
let reply = this.get('reply');
|
let reply = this.get('reply');
|
||||||
|
@ -680,7 +668,7 @@ const Composer = RestModel.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getCookedHtml() {
|
getCookedHtml() {
|
||||||
return $('#reply-control .wmd-preview').html().replace(/<span class="marker"><\/span>/g, '');
|
return $('#reply-control .d-editor-preview').html().replace(/<span class="marker"><\/span>/g, '');
|
||||||
},
|
},
|
||||||
|
|
||||||
saveDraft() {
|
saveDraft() {
|
||||||
|
|
|
@ -6,24 +6,29 @@ export default {
|
||||||
initialize(container, application) {
|
initialize(container, application) {
|
||||||
const $html = $('html'),
|
const $html = $('html'),
|
||||||
touch = $html.hasClass('touch') || (Modernizr.prefixed("MaxTouchPoints", navigator) > 1),
|
touch = $html.hasClass('touch') || (Modernizr.prefixed("MaxTouchPoints", navigator) > 1),
|
||||||
caps = Ember.Object.create();
|
caps = {touch};
|
||||||
|
|
||||||
// Store the touch ability in our capabilities object
|
// Store the touch ability in our capabilities object
|
||||||
caps.set('touch', touch);
|
|
||||||
$html.addClass(touch ? 'discourse-touch' : 'discourse-no-touch');
|
$html.addClass(touch ? 'discourse-touch' : 'discourse-no-touch');
|
||||||
|
|
||||||
// Detect Devices
|
// Detect Devices
|
||||||
if (navigator) {
|
if (navigator) {
|
||||||
const ua = navigator.userAgent;
|
const ua = navigator.userAgent;
|
||||||
if (ua) {
|
if (ua) {
|
||||||
caps.set('android', ua.indexOf('Android') !== -1);
|
caps.isAndroid = ua.indexOf('Android') !== -1;
|
||||||
caps.set('winphone', ua.indexOf('Windows Phone') !== -1);
|
caps.isWinphone = ua.indexOf('Windows Phone') !== -1;
|
||||||
|
|
||||||
|
caps.isOpera = !!window.opera || ua.indexOf(' OPR/') >= 0;
|
||||||
|
caps.isFirefox = typeof InstallTrigger !== 'undefined';
|
||||||
|
caps.isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
|
||||||
|
caps.isChrome = !!window.chrome && !caps.isOpera;
|
||||||
|
caps.canPasteImages = caps.isChrome || caps.isFirefox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We consider high res a device with 1280 horizontal pixels. High DPI tablets like
|
// We consider high res a device with 1280 horizontal pixels. High DPI tablets like
|
||||||
// iPads should report as 1024.
|
// iPads should report as 1024.
|
||||||
caps.set('highRes', window.screen.width >= 1280);
|
caps.highRes = window.screen.width >= 1280;
|
||||||
|
|
||||||
// Inject it
|
// Inject it
|
||||||
application.register('capabilities:main', caps, { instantiate: false });
|
application.register('capabilities:main', caps, { instantiate: false });
|
||||||
|
|
|
@ -89,13 +89,11 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||||
},
|
},
|
||||||
|
|
||||||
showNotActivated(props) {
|
showNotActivated(props) {
|
||||||
const controller = showModal('not-activated', {title: 'log_in' });
|
showModal('not-activated', {title: 'log_in' }).setProperties(props);
|
||||||
controller.setProperties(props);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showUploadSelector(composerView) {
|
showUploadSelector(toolbarEvent) {
|
||||||
showModal('uploadSelector');
|
showModal('uploadSelector').setProperties({ toolbarEvent, imageUrl: null, imageLink: null });
|
||||||
this.controllerFor('upload-selector').setProperties({ composerView: composerView });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showKeyboardShortcutsHelp() {
|
showKeyboardShortcutsHelp() {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{d-editor tabindex="4"
|
||||||
|
value=composer.reply
|
||||||
|
placeholder="composer.reply_placeholder"
|
||||||
|
previewUpdated="previewUpdated"
|
||||||
|
markdownOptions=markdownOptions
|
||||||
|
extraButtons="extraButtons"
|
||||||
|
importQuote="importQuote"
|
||||||
|
showOptions="showOptions"
|
||||||
|
showUploadModal="showUploadModal"
|
||||||
|
validation=validation
|
||||||
|
loading=composer.loading}}
|
||||||
|
|
||||||
|
<div class="composer-bottom-right">
|
||||||
|
{{#if site.mobileView}}
|
||||||
|
<input type="file" id="mobile-uploader" />
|
||||||
|
<a class="mobile-file-upload {{if isUploading 'hidden'}}">{{i18n 'upload'}}</a>
|
||||||
|
{{else}}
|
||||||
|
<a href {{action "togglePreview"}} class='toggle-preview'>{{{toggleText}}}</a>
|
||||||
|
{{/if}}
|
||||||
|
{{#if isUploading}}
|
||||||
|
<div id="file-uploading">
|
||||||
|
{{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}}
|
||||||
|
{{uploadProgress}}%
|
||||||
|
<a href id="cancel-file-upload" {{action "cancelUpload"}}>{{fa-icon "times"}}</a>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<div id='draft-status' class="{{if isUploading 'hidden'}}">
|
||||||
|
{{draftStatus}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{text-field value=composer.title
|
||||||
|
tabindex="2"
|
||||||
|
id="reply-title"
|
||||||
|
maxLength=siteSettings.max_topic_title_length
|
||||||
|
placeholderKey="composer.title_placeholder"
|
||||||
|
disabled=composer.loading}}
|
||||||
|
|
||||||
|
{{popup-input-tip validation=validation}}
|
|
@ -17,10 +17,17 @@
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class='d-editor-preview-header'></div>
|
||||||
|
|
||||||
{{textarea value=value class="d-editor-input"}}
|
<div class="d-editor-textarea-wrapper">
|
||||||
|
{{conditional-loading-spinner condition=loading}}
|
||||||
|
{{textarea tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated}}
|
||||||
|
{{popup-input-tip validation=validation}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-editor-preview {{unless preview 'hidden'}}">
|
<div class="d-editor-preview-wrapper">
|
||||||
{{{preview}}}
|
<div class="d-editor-preview">
|
||||||
|
{{{preview}}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -56,15 +56,12 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="title-input">
|
{{composer-title composer=model lastValidatedAt=lastValidatedAt}}
|
||||||
{{text-field value=model.title tabindex="2" id="reply-title" maxLength=maxTitleLength placeholderKey="composer.title_placeholder"}}
|
|
||||||
{{popup-input-tip validation=view.titleValidation shownAt=view.showTitleTip}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if model.showCategoryChooser}}
|
{{#if model.showCategoryChooser}}
|
||||||
<div class="category-input">
|
<div class="category-input">
|
||||||
{{category-chooser valueAttribute="id" value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}}
|
{{category-chooser valueAttribute="id" value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}}
|
||||||
{{popup-input-tip validation=view.categoryValidation shownAt=view.showCategoryTip}}
|
{{popup-input-tip validation=categoryValidation}}
|
||||||
</div>
|
</div>
|
||||||
{{#if model.archetype.hasOptions}}
|
{{#if model.archetype.hasOptions}}
|
||||||
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button>
|
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button>
|
||||||
|
@ -77,35 +74,15 @@
|
||||||
{{plugin-outlet "composer-fields"}}
|
{{plugin-outlet "composer-fields"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='wmd-controls'>
|
{{composer-editor topic=topic
|
||||||
<div class='textarea-wrapper'>
|
composer=model
|
||||||
<div class='wmd-button-bar'></div>
|
lastValidatedAt=lastValidatedAt
|
||||||
<div class='wmd-preview-scroller'></div>
|
canWhisper=canWhisper
|
||||||
{{conditional-loading-spinner condition=model.loading}}
|
draftStatus=model.draftStatus
|
||||||
{{composer-text-area tabindex="4" value=model.reply}}
|
isUploading=isUploading
|
||||||
{{popup-input-tip validation=view.replyValidation shownAt=view.showReplyTip}}
|
importQuote="importQuote"
|
||||||
</div>
|
showOptions="showOptions"
|
||||||
<!-- keep the classes here in sync with post.hbs -->
|
showUploadSelector="showUploadSelector"}}
|
||||||
<div class='preview-wrapper regular'>
|
|
||||||
<div class="wmd-preview cooked {{if model.hidePreview 'hidden'}}"></div>
|
|
||||||
</div>
|
|
||||||
<div class="composer-bottom-right">
|
|
||||||
{{#if site.mobileView}}
|
|
||||||
<input type="file" id="mobile-uploader" />
|
|
||||||
<a class="mobile-file-upload {{if view.isUploading 'hidden'}}">{{i18n 'upload'}}</a>
|
|
||||||
{{else}}
|
|
||||||
<a href {{action "togglePreview"}} class='toggle-preview'>{{{model.toggleText}}}</a>
|
|
||||||
{{/if}}
|
|
||||||
{{#if view.isUploading}}
|
|
||||||
<div id="file-uploading">
|
|
||||||
{{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}% <a id="cancel-file-upload">{{fa-icon "times"}}</a>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<div id='draft-status' class="{{if view.isUploading 'hidden'}}">
|
|
||||||
{{model.draftStatus}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if currentUser}}
|
{{#if currentUser}}
|
||||||
<div class='submit-panel'>
|
<div class='submit-panel'>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{{#if local}}
|
{{#if local}}
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
<input type="file" id="filename-input" multiple><br>
|
<input type="file" id="filename-input" multiple><br>
|
||||||
<span class="description">{{unbound view.tip}}</span>
|
<span class="description">{{tip}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,31 +14,34 @@
|
||||||
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
||||||
{{#if remote}}
|
{{#if remote}}
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
<input type="text" id="fileurl-input" placeholder="http://example.com/image.png"><br>
|
{{input value=imageUrl placeholder="http://example.com/image.png"}}
|
||||||
<span class="description">{{unbound view.tip}}</span>
|
<span class="description">{{tip}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{#if showMore}}
|
{{#if showMore}}
|
||||||
<div class="radios">
|
<div class="radios">
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
<input type="text" id="link-input" placeholder="http://example.com"><br>
|
{{input value=imageLink laceholder="http://example.com"}}
|
||||||
<span class="description">{{i18n 'upload_selector.image_link'}}</span>
|
<span class="description">{{i18n 'upload_selector.image_link'}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="radios">
|
<div class="radios">
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
<p class="hint">{{unbound view.hint}}</p>
|
<p class="hint">
|
||||||
|
{{#if capabilities.canPasteImages}}
|
||||||
|
{{i18n 'upload_selector.hint'}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n 'upload_selector.hint_for_supported_browsers'}}
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-primary" {{action "upload" target="view"}}>
|
{{d-button action="upload" class='btn-primary' icon=uploadIcon label='upload'}}
|
||||||
<span class='add-upload'><i {{bind-attr class=":fa view.uploadIcon"}}></i></span>
|
<a href {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||||
{{i18n 'upload'}}
|
|
||||||
</button>
|
|
||||||
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
|
||||||
{{#if remote}}<a {{action "toggleShowMore"}} class="pull-right">{{i18n 'show_more'}}</a>{{/if}}
|
{{#if remote}}<a {{action "toggleShowMore"}} class="pull-right">{{i18n 'show_more'}}</a>{{/if}}
|
||||||
</div>
|
</div>
|
|
@ -15,9 +15,9 @@
|
||||||
{{#if editingTopic}}
|
{{#if editingTopic}}
|
||||||
{{#if model.isPrivateMessage}}
|
{{#if model.isPrivateMessage}}
|
||||||
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
||||||
{{autofocus-text-field id="edit-title" value=buffered.title maxLength=maxTitleLength}}
|
{{autofocus-text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{autofocus-text-field id="edit-title" value=buffered.title maxLength=maxTitleLength}}
|
{{autofocus-text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length}}
|
||||||
<br>
|
<br>
|
||||||
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,57 +1,25 @@
|
||||||
import userSearch from 'discourse/lib/user-search';
|
|
||||||
import afterTransition from 'discourse/lib/after-transition';
|
import afterTransition from 'discourse/lib/after-transition';
|
||||||
import loadScript from 'discourse/lib/load-script';
|
|
||||||
import positioningWorkaround from 'discourse/lib/safari-hacks';
|
import positioningWorkaround from 'discourse/lib/safari-hacks';
|
||||||
import debounce from 'discourse/lib/debounce';
|
|
||||||
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
|
||||||
import { headerHeight } from 'discourse/views/header';
|
import { headerHeight } from 'discourse/views/header';
|
||||||
import { showSelector } from 'discourse/lib/emoji/emoji-toolbar';
|
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import Composer from 'discourse/models/composer';
|
||||||
|
|
||||||
const ComposerView = Ember.View.extend(Ember.Evented, {
|
const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||||
_lastKeyTimeout: null,
|
_lastKeyTimeout: null,
|
||||||
templateName: 'composer',
|
|
||||||
elementId: 'reply-control',
|
elementId: 'reply-control',
|
||||||
classNameBindings: ['model.creatingPrivateMessage:private-message',
|
classNameBindings: ['composer.creatingPrivateMessage:private-message',
|
||||||
'composeState',
|
'composeState',
|
||||||
'model.loading',
|
'composer.loading',
|
||||||
'model.canEditTitle:edit-title',
|
'composer.canEditTitle:edit-title',
|
||||||
'postMade',
|
'composer.createdPost:created-post',
|
||||||
'model.creatingTopic:topic',
|
'composer.creatingTopic:topic'],
|
||||||
'model.showPreview',
|
|
||||||
'model.hidePreview'],
|
|
||||||
|
|
||||||
model: Em.computed.alias('controller.model'),
|
composer: Em.computed.alias('controller.model'),
|
||||||
|
|
||||||
// This is just in case something still references content. Can probably be removed
|
@computed('composer.composeState')
|
||||||
content: Em.computed.alias('model'),
|
composeState(composeState) {
|
||||||
|
return composeState || Composer.CLOSED;
|
||||||
composeState: function() {
|
},
|
||||||
return this.get('model.composeState') || Discourse.Composer.CLOSED;
|
|
||||||
}.property('model.composeState'),
|
|
||||||
|
|
||||||
// Disable fields when we're loading
|
|
||||||
loadingChanged: function() {
|
|
||||||
if (this.get('loading')) {
|
|
||||||
this.$('.wmd-input, #reply-title').prop('disabled', 'disabled');
|
|
||||||
} else {
|
|
||||||
this.$('.wmd-input, #reply-title').prop('disabled', '');
|
|
||||||
}
|
|
||||||
}.observes('loading'),
|
|
||||||
|
|
||||||
postMade: function() {
|
|
||||||
return !Ember.isEmpty(this.get('model.createdPost')) ? 'created-post' : null;
|
|
||||||
}.property('model.createdPost'),
|
|
||||||
|
|
||||||
refreshPreview: debounce(function() {
|
|
||||||
if (this.editor) {
|
|
||||||
this.editor.refreshPreview();
|
|
||||||
}
|
|
||||||
}, 30),
|
|
||||||
|
|
||||||
observeReplyChanges: function() {
|
|
||||||
if (this.get('model.hidePreview')) return;
|
|
||||||
Ember.run.scheduleOnce('afterRender', this, 'refreshPreview');
|
|
||||||
}.observes('model.reply', 'model.hidePreview'),
|
|
||||||
|
|
||||||
movePanels(sizePx) {
|
movePanels(sizePx) {
|
||||||
$('#main-outlet').css('padding-bottom', sizePx);
|
$('#main-outlet').css('padding-bottom', sizePx);
|
||||||
|
@ -60,44 +28,41 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||||
this.appEvents.trigger("composer:resized");
|
this.appEvents.trigger("composer:resized");
|
||||||
},
|
},
|
||||||
|
|
||||||
resize: function() {
|
@observes('composeState', 'composer.action')
|
||||||
|
resize() {
|
||||||
Ember.run.scheduleOnce('afterRender', () => {
|
Ember.run.scheduleOnce('afterRender', () => {
|
||||||
let h = $('#reply-control').height() || 0;
|
const h = $('#reply-control').height() || 0;
|
||||||
this.movePanels(h + "px");
|
this.movePanels(h + "px");
|
||||||
|
|
||||||
// Figure out the size of the fields
|
// Figure out the size of the fields
|
||||||
const $fields = this.$('.composer-fields');
|
const $fields = this.$('.composer-fields');
|
||||||
let pos = $fields.position();
|
const fieldPos = $fields.position();
|
||||||
|
if (fieldPos) {
|
||||||
if (pos) {
|
this.$('.wmd-controls').css('top', $fields.height() + fieldPos.top + 5);
|
||||||
this.$('.wmd-controls').css('top', $fields.height() + pos.top + 5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the submit panel height
|
// get the submit panel height
|
||||||
pos = this.$('.submit-panel').position();
|
const submitPos = this.$('.submit-panel').position();
|
||||||
if (pos) {
|
if (submitPos) {
|
||||||
this.$('.wmd-controls').css('bottom', h - pos.top + 7);
|
this.$('.wmd-controls').css('bottom', h - submitPos.top + 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}.observes('model.composeState', 'model.action'),
|
},
|
||||||
|
|
||||||
keyUp() {
|
keyUp() {
|
||||||
const controller = this.get('controller');
|
const controller = this.get('controller');
|
||||||
controller.checkReplyLength();
|
controller.checkReplyLength();
|
||||||
|
|
||||||
this.get('controller.model').typing();
|
this.get('composer').typing();
|
||||||
|
|
||||||
const lastKeyUp = new Date();
|
const lastKeyUp = new Date();
|
||||||
this.set('lastKeyUp', lastKeyUp);
|
this._lastKeyUp = lastKeyUp;
|
||||||
|
|
||||||
// One second from now, check to see if the last key was hit when
|
// One second from now, check to see if the last key was hit when
|
||||||
// we recorded it. If it was, the user paused typing.
|
// we recorded it. If it was, the user paused typing.
|
||||||
const self = this;
|
|
||||||
|
|
||||||
Ember.run.cancel(this._lastKeyTimeout);
|
Ember.run.cancel(this._lastKeyTimeout);
|
||||||
this._lastKeyTimeout = Ember.run.later(function() {
|
this._lastKeyTimeout = Ember.run.later(() => {
|
||||||
if (lastKeyUp !== self.get('lastKeyUp')) return;
|
if (lastKeyUp !== this._lastKeyUp) { return; }
|
||||||
|
|
||||||
// Search for similar topics if the user pauses typing
|
// Search for similar topics if the user pauses typing
|
||||||
controller.findSimilarTopics();
|
controller.findSimilarTopics();
|
||||||
|
@ -106,7 +71,6 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||||
|
|
||||||
keyDown(e) {
|
keyDown(e) {
|
||||||
if (e.which === 27) {
|
if (e.which === 27) {
|
||||||
// ESC
|
|
||||||
this.get('controller').send('hitEsc');
|
this.get('controller').send('hitEsc');
|
||||||
return false;
|
return false;
|
||||||
} else if (e.which === 13 && (e.ctrlKey || e.metaKey)) {
|
} else if (e.which === 13 && (e.ctrlKey || e.metaKey)) {
|
||||||
|
@ -116,557 +80,25 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_enableResizing: function() {
|
@on('didInsertElement')
|
||||||
|
_enableResizing() {
|
||||||
const $replyControl = $('#reply-control');
|
const $replyControl = $('#reply-control');
|
||||||
|
const resize = () => Ember.run(() => this.resize());
|
||||||
const runResize = () => {
|
|
||||||
Ember.run(() => this.resize());
|
|
||||||
};
|
|
||||||
|
|
||||||
$replyControl.DivResizer({
|
$replyControl.DivResizer({
|
||||||
maxHeight(winHeight) {
|
resize,
|
||||||
return winHeight - headerHeight();
|
maxHeight: winHeight => winHeight - headerHeight(),
|
||||||
},
|
onDrag: sizePx => this.movePanels(sizePx)
|
||||||
resize: runResize,
|
|
||||||
onDrag: (sizePx) => this.movePanels(sizePx)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterTransition($replyControl, runResize);
|
afterTransition($replyControl, resize);
|
||||||
this.set('controller.view', this);
|
|
||||||
|
|
||||||
positioningWorkaround(this.$());
|
positioningWorkaround(this.$());
|
||||||
}.on('didInsertElement'),
|
},
|
||||||
|
|
||||||
_unlinkView: function() {
|
|
||||||
this.set('controller.view', null);
|
|
||||||
}.on('willDestroyElement'),
|
|
||||||
|
|
||||||
click() {
|
click() {
|
||||||
this.get('controller').send('openIfDraft');
|
this.get('controller').send('openIfDraft');
|
||||||
},
|
}
|
||||||
|
|
||||||
// Called after the preview renders. Debounced for performance
|
|
||||||
afterRender() {
|
|
||||||
if (this._state !== "inDOM") { return; }
|
|
||||||
|
|
||||||
const $wmdPreview = this.$('.wmd-preview');
|
|
||||||
if ($wmdPreview.length === 0) return;
|
|
||||||
|
|
||||||
const post = this.get('model.post');
|
|
||||||
let refresh = false;
|
|
||||||
|
|
||||||
// If we are editing a post, we'll refresh its contents once. This is a feature that
|
|
||||||
// allows a user to refresh its contents once.
|
|
||||||
if (post && !post.get('refreshedPost')) {
|
|
||||||
refresh = true;
|
|
||||||
post.set('refreshedPost', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the post processing effects
|
|
||||||
$('a.onebox', $wmdPreview).each(function(i, e) {
|
|
||||||
Discourse.Onebox.load(e, refresh);
|
|
||||||
});
|
|
||||||
|
|
||||||
const unseen = linkSeenMentions($wmdPreview, this.siteSettings);
|
|
||||||
if (unseen.length) {
|
|
||||||
Ember.run.debounce(this, this._renderUnseen, $wmdPreview, unseen, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trigger('previewRefreshed', $wmdPreview);
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderUnseen: function($wmdPreview, unseen) {
|
|
||||||
fetchUnseenMentions($wmdPreview, unseen, this.siteSettings).then(() => {
|
|
||||||
linkSeenMentions($wmdPreview, this.siteSettings);
|
|
||||||
this.trigger('previewRefreshed', $wmdPreview);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_applyEmojiAutocomplete() {
|
|
||||||
if (!this.siteSettings.enable_emoji) { return; }
|
|
||||||
|
|
||||||
const container = this.container;
|
|
||||||
const template = container.lookup('template:emoji-selector-autocomplete.raw');
|
|
||||||
const controller = this.get('controller');
|
|
||||||
|
|
||||||
this.$('.wmd-input').autocomplete({
|
|
||||||
template: template,
|
|
||||||
key: ":",
|
|
||||||
|
|
||||||
transformComplete(v) {
|
|
||||||
if (v.code) {
|
|
||||||
return `${v.code}:`;
|
|
||||||
} else {
|
|
||||||
showSelector({
|
|
||||||
container,
|
|
||||||
onSelect(title) {
|
|
||||||
controller.appendTextAtCursor(title + ':', {space: false});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
dataSource(term) {
|
|
||||||
return new Ember.RSVP.Promise(resolve => {
|
|
||||||
const full = `:${term}`;
|
|
||||||
term = term.toLowerCase();
|
|
||||||
|
|
||||||
if (term === "") {
|
|
||||||
return resolve(["smile", "smiley", "wink", "sunny", "blush"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Discourse.Emoji.translations[full]) {
|
|
||||||
return resolve([Discourse.Emoji.translations[full]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = Discourse.Emoji.search(term, {maxResults: 5});
|
|
||||||
|
|
||||||
return resolve(options);
|
|
||||||
}).then(list => list.map(code => {
|
|
||||||
return {code, src: Discourse.Emoji.urlFor(code)};
|
|
||||||
})).then(list => {
|
|
||||||
if (list.length) {
|
|
||||||
list.push({ label: I18n.t("composer.more_emoji") });
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
initEditor() {
|
|
||||||
// not quite right, need a callback to pass in, meaning this gets called once,
|
|
||||||
// but if you start replying to another topic it will get the avatars wrong
|
|
||||||
let $wmdInput;
|
|
||||||
const self = this;
|
|
||||||
const controller = this.get('controller');
|
|
||||||
|
|
||||||
this.wmdInput = $wmdInput = this.$('.wmd-input');
|
|
||||||
if ($wmdInput.length === 0 || $wmdInput.data('init') === true) return;
|
|
||||||
|
|
||||||
loadScript('defer/html-sanitizer-bundle');
|
|
||||||
ComposerView.trigger("initWmdEditor");
|
|
||||||
this._applyEmojiAutocomplete();
|
|
||||||
|
|
||||||
const template = this.container.lookup('template:user-selector-autocomplete.raw');
|
|
||||||
$wmdInput.data('init', true);
|
|
||||||
$wmdInput.autocomplete({
|
|
||||||
template: template,
|
|
||||||
dataSource(term) {
|
|
||||||
return userSearch({
|
|
||||||
term: term,
|
|
||||||
topicId: controller.get('controllers.topic.model.id'),
|
|
||||||
includeGroups: true
|
|
||||||
});
|
|
||||||
},
|
|
||||||
key: "@",
|
|
||||||
transformComplete(v) {
|
|
||||||
return v.username ? v.username : v.usernames.join(", @");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
containerElement: this.element,
|
|
||||||
lookupAvatarByPostNumber(postNumber, topicId) {
|
|
||||||
const posts = controller.get('controllers.topic.model.postStream.posts');
|
|
||||||
if (posts && topicId === controller.get('controllers.topic.model.id')) {
|
|
||||||
const quotedPost = posts.findProperty("post_number", postNumber);
|
|
||||||
if (quotedPost) {
|
|
||||||
return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showOptions = controller.get('canWhisper');
|
|
||||||
if (showOptions) {
|
|
||||||
options.appendButtons = [{
|
|
||||||
id: 'wmd-composer-options',
|
|
||||||
description: I18n.t("composer.options"),
|
|
||||||
execute() {
|
|
||||||
const toolbarPos = self.$('.wmd-controls').position();
|
|
||||||
const pos = self.$('.wmd-composer-options').position();
|
|
||||||
|
|
||||||
const location = {
|
|
||||||
position: "absolute",
|
|
||||||
left: toolbarPos.left + pos.left,
|
|
||||||
top: toolbarPos.top + pos.top,
|
|
||||||
};
|
|
||||||
controller.send('showOptions', location);
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editor = Discourse.Markdown.createEditor(options);
|
|
||||||
|
|
||||||
// HACK to change the upload icon of the composer's toolbar
|
|
||||||
if (!Discourse.Utilities.allowsAttachments()) {
|
|
||||||
Em.run.scheduleOnce("afterRender", function() {
|
|
||||||
$("#wmd-image-button").addClass("image-only");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editor.hooks.insertImageDialog = function(callback) {
|
|
||||||
callback(null);
|
|
||||||
controller.send('showUploadSelector', self);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.editor.hooks.onPreviewRefresh = function() {
|
|
||||||
return self.afterRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.editor.run();
|
|
||||||
this.set('editor', this.editor);
|
|
||||||
this.loadingChanged();
|
|
||||||
|
|
||||||
const saveDraft = debounce((function() {
|
|
||||||
return controller.saveDraft();
|
|
||||||
}), 2000);
|
|
||||||
|
|
||||||
$wmdInput.keyup(function() {
|
|
||||||
saveDraft();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const $replyTitle = $('#reply-title');
|
|
||||||
|
|
||||||
$replyTitle.keyup(function() {
|
|
||||||
saveDraft();
|
|
||||||
// removes the red background once the requirements are met
|
|
||||||
if (self.get('model.missingTitleCharacters') <= 0) {
|
|
||||||
$replyTitle.removeClass("requirements-not-met");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// when the title field loses the focus...
|
|
||||||
$replyTitle.blur(function(){
|
|
||||||
// ...and the requirements are not met (ie. the minimum number of characters)
|
|
||||||
if (self.get('model.missingTitleCharacters') > 0) {
|
|
||||||
// then, "redify" the background
|
|
||||||
$replyTitle.toggleClass("requirements-not-met", true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// in case it's still bound somehow
|
|
||||||
this._unbindUploadTarget();
|
|
||||||
|
|
||||||
const $uploadTarget = $("#reply-control"),
|
|
||||||
csrf = Discourse.Session.currentProp("csrfToken"),
|
|
||||||
reset = () => this.setProperties({ uploadProgress: 0, isUploading: false });
|
|
||||||
|
|
||||||
var cancelledByTheUser;
|
|
||||||
|
|
||||||
this.messageBus.subscribe("/uploads/composer", upload => {
|
|
||||||
// reset upload state
|
|
||||||
reset();
|
|
||||||
// replace upload placeholder
|
|
||||||
if (upload && upload.url) {
|
|
||||||
if (!cancelledByTheUser) {
|
|
||||||
const uploadPlaceholder = Discourse.Utilities.getUploadPlaceholder(),
|
|
||||||
markdown = Discourse.Utilities.getUploadMarkdown(upload);
|
|
||||||
this.replaceMarkdown(uploadPlaceholder, markdown);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Discourse.Utilities.displayErrorForUpload(upload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$uploadTarget.fileupload({
|
|
||||||
url: Discourse.getURL("/uploads.json?client_id=" + this.messageBus.clientId + "&authenticity_token=" + encodeURIComponent(csrf)),
|
|
||||||
dataType: "json",
|
|
||||||
pasteZone: $uploadTarget,
|
|
||||||
});
|
|
||||||
|
|
||||||
$uploadTarget.on("fileuploadsubmit", (e, data) => {
|
|
||||||
const isValid = Discourse.Utilities.validateUploadedFiles(data.files);
|
|
||||||
data.formData = { type: "composer" };
|
|
||||||
this.setProperties({ uploadProgress: 0, isUploading: isValid });
|
|
||||||
return isValid;
|
|
||||||
});
|
|
||||||
|
|
||||||
$uploadTarget.on("fileuploadsend", (e, data) => {
|
|
||||||
// hide the "file selector" modal
|
|
||||||
controller.send("closeModal");
|
|
||||||
// deal with cancellation
|
|
||||||
cancelledByTheUser = false;
|
|
||||||
// add upload placeholder
|
|
||||||
const uploadPlaceholder = Discourse.Utilities.getUploadPlaceholder();
|
|
||||||
this.addMarkdown(uploadPlaceholder);
|
|
||||||
|
|
||||||
if (data["xhr"]) {
|
|
||||||
const jqHXR = data.xhr();
|
|
||||||
if (jqHXR) {
|
|
||||||
// need to wait for the link to show up in the DOM
|
|
||||||
Em.run.schedule("afterRender", () => {
|
|
||||||
const $cancel = $("#cancel-file-upload");
|
|
||||||
$cancel.on("click", () => {
|
|
||||||
if (jqHXR) {
|
|
||||||
// signal the upload was cancelled by the user
|
|
||||||
cancelledByTheUser = true;
|
|
||||||
// immediately remove upload placeholder
|
|
||||||
this.replaceMarkdown(uploadPlaceholder, "");
|
|
||||||
// might trigger a "fileuploadfail" event with status = 0
|
|
||||||
jqHXR.abort();
|
|
||||||
// make sure we always reset the uploading status
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
// unbind
|
|
||||||
$cancel.off("click");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$uploadTarget.on("fileuploadprogressall", (e, data) => {
|
|
||||||
const progress = parseInt(data.loaded / data.total * 100, 10);
|
|
||||||
this.set("uploadProgress", progress);
|
|
||||||
});
|
|
||||||
|
|
||||||
$uploadTarget.on("fileuploadfail", (e, data) => {
|
|
||||||
// reset upload state
|
|
||||||
reset();
|
|
||||||
|
|
||||||
if (!cancelledByTheUser) {
|
|
||||||
// remove upload placeholder when there's a failure
|
|
||||||
const uploadPlaceholder = Discourse.Utilities.getUploadPlaceholder();
|
|
||||||
this.replaceMarkdown(uploadPlaceholder, "");
|
|
||||||
// display the error
|
|
||||||
Discourse.Utilities.displayErrorForUpload(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// contenteditable div hack for getting image paste to upload working in
|
|
||||||
// Firefox. This is pretty dangerous because it can potentially break
|
|
||||||
// Ctrl+v to paste so we should be conservative about what browsers this runs
|
|
||||||
// in.
|
|
||||||
const uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/);
|
|
||||||
if (uaMatch && parseInt(uaMatch[1]) >= 24) {
|
|
||||||
self.$().append( Ember.$("<div id='contenteditable' contenteditable='true' style='height: 0; width: 0; overflow: hidden'></div>") );
|
|
||||||
self.$("textarea").off('keydown.contenteditable');
|
|
||||||
self.$("textarea").on('keydown.contenteditable', function(event) {
|
|
||||||
// Catch Ctrl+v / Cmd+v and hijack focus to a contenteditable div. We can't
|
|
||||||
// use the onpaste event because for some reason the paste isn't resumed
|
|
||||||
// after we switch focus, probably because it is being executed too late.
|
|
||||||
if ((event.ctrlKey || event.metaKey) && (event.keyCode === 86)) {
|
|
||||||
// Save the current textarea selection.
|
|
||||||
const textarea = self.$("textarea")[0],
|
|
||||||
selectionStart = textarea.selectionStart,
|
|
||||||
selectionEnd = textarea.selectionEnd;
|
|
||||||
|
|
||||||
// Focus the contenteditable div.
|
|
||||||
const contentEditableDiv = self.$('#contenteditable');
|
|
||||||
contentEditableDiv.focus();
|
|
||||||
|
|
||||||
// The paste doesn't finish immediately and we don't have any onpaste
|
|
||||||
// event, so wait for 100ms which _should_ be enough time.
|
|
||||||
setTimeout(function() {
|
|
||||||
const pastedImg = contentEditableDiv.find('img');
|
|
||||||
|
|
||||||
if ( pastedImg.length === 1 ) {
|
|
||||||
pastedImg.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For restoring the selection.
|
|
||||||
textarea.focus();
|
|
||||||
const textareaContent = $(textarea).val(),
|
|
||||||
startContent = textareaContent.substring(0, selectionStart),
|
|
||||||
endContent = textareaContent.substring(selectionEnd);
|
|
||||||
|
|
||||||
const restoreSelection = function(pastedText) {
|
|
||||||
$(textarea).val( startContent + pastedText + endContent );
|
|
||||||
textarea.selectionStart = selectionStart + pastedText.length;
|
|
||||||
textarea.selectionEnd = textarea.selectionStart;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (contentEditableDiv.html().length > 0) {
|
|
||||||
// If the image wasn't the only pasted content we just give up and
|
|
||||||
// fall back to the original pasted text.
|
|
||||||
contentEditableDiv.find("br").replaceWith("\n");
|
|
||||||
restoreSelection(contentEditableDiv.text());
|
|
||||||
} else {
|
|
||||||
// Depending on how the image is pasted in, we may get either a
|
|
||||||
// normal URL or a data URI. If we get a data URI we can convert it
|
|
||||||
// to a Blob and upload that, but if it is a regular URL that
|
|
||||||
// operation is prevented for security purposes. When we get a regular
|
|
||||||
// URL let's just create an <img> tag for the image.
|
|
||||||
const imageSrc = pastedImg.attr('src');
|
|
||||||
|
|
||||||
if (imageSrc.match(/^data:image/)) {
|
|
||||||
// Restore the cursor position, and remove any selected text.
|
|
||||||
restoreSelection("");
|
|
||||||
|
|
||||||
// Create a Blob to upload.
|
|
||||||
const image = new Image();
|
|
||||||
image.onload = function() {
|
|
||||||
// Create a new canvas.
|
|
||||||
const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
|
|
||||||
canvas.height = image.height;
|
|
||||||
canvas.width = image.width;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.drawImage(image, 0, 0);
|
|
||||||
|
|
||||||
canvas.toBlob(function(blob) {
|
|
||||||
$uploadTarget.fileupload('add', {files: blob});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
image.src = imageSrc;
|
|
||||||
} else {
|
|
||||||
restoreSelection("<img src='" + imageSrc + "'>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentEditableDiv.html('');
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Discourse.Mobile.mobileView) {
|
|
||||||
$(".mobile-file-upload").on("click.uploader", function () {
|
|
||||||
// redirect the click on the hidden file input
|
|
||||||
$("#mobile-uploader").click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to wait a bit for the "slide up" transition of the composer
|
|
||||||
// we could use .on("transitionend") but it's not firing when the transition isn't completed :(
|
|
||||||
Em.run.later(function() {
|
|
||||||
self.resize();
|
|
||||||
self.refreshPreview();
|
|
||||||
if ($replyTitle.length) {
|
|
||||||
$replyTitle.putCursorAtEnd();
|
|
||||||
} else {
|
|
||||||
$wmdInput.putCursorAtEnd();
|
|
||||||
}
|
|
||||||
self.appEvents.trigger("composer:opened");
|
|
||||||
}, 400);
|
|
||||||
},
|
|
||||||
|
|
||||||
addMarkdown(text) {
|
|
||||||
const ctrl = this.$('.wmd-input').get(0),
|
|
||||||
reply = this.get('model.reply'),
|
|
||||||
caretPosition = Discourse.Utilities.caretPosition(ctrl);
|
|
||||||
|
|
||||||
this.set('model.reply', reply.substring(0, caretPosition) + text + reply.substring(caretPosition, reply.length));
|
|
||||||
|
|
||||||
Em.run.schedule('afterRender', () => Discourse.Utilities.setCaretPosition(ctrl, caretPosition + text.length));
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceMarkdown(old, text) {
|
|
||||||
const ctrl = this.$(".wmd-input").get(0),
|
|
||||||
reply = this.get("model.reply"),
|
|
||||||
beforeCaretPosition = Discourse.Utilities.caretPosition(ctrl),
|
|
||||||
afterCaretPosition = beforeCaretPosition <= reply.indexOf(old) ? beforeCaretPosition : beforeCaretPosition - old.length + text.length;
|
|
||||||
|
|
||||||
this.set("model.reply", reply.replace(old, text));
|
|
||||||
|
|
||||||
Ember.run.schedule("afterRender", () => Discourse.Utilities.setCaretPosition(ctrl, afterCaretPosition));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Uses javascript to get the image sizes from the preview, if present
|
|
||||||
imageSizes() {
|
|
||||||
const result = {};
|
|
||||||
this.$('.wmd-preview img').each(function(i, e) {
|
|
||||||
const $img = $(e),
|
|
||||||
src = $img.prop('src');
|
|
||||||
|
|
||||||
if (src && src.length) {
|
|
||||||
result[src] = { width: $img.width(), height: $img.height() };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
childDidInsertElement() {
|
|
||||||
this.initEditor();
|
|
||||||
|
|
||||||
// Disable links in the preview
|
|
||||||
this.$('.wmd-preview').on('click.preview', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
childWillDestroyElement() {
|
|
||||||
this._unbindUploadTarget();
|
|
||||||
|
|
||||||
this.$('.wmd-preview').off('click.preview');
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
Em.run.next(() => {
|
|
||||||
$('#main-outlet').css('padding-bottom', 0);
|
|
||||||
// need to wait a bit for the "slide down" transition of the composer
|
|
||||||
Em.run.later(() => {
|
|
||||||
if (self.get('composeState') !== Discourse.Composer.CLOSED) {
|
|
||||||
$('#main-outlet').css('padding-bottom', $('#reply-control').height());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.appEvents.trigger("composer:closed");
|
|
||||||
}, 400);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_unbindUploadTarget() {
|
|
||||||
this.messageBus.unsubscribe("/uploads/composer");
|
|
||||||
const $uploadTarget = $("#reply-control");
|
|
||||||
try { $uploadTarget.fileupload("destroy"); }
|
|
||||||
catch (e) { /* wasn't initialized yet */ }
|
|
||||||
$uploadTarget.off();
|
|
||||||
},
|
|
||||||
|
|
||||||
titleValidation: function() {
|
|
||||||
const titleLength = this.get('model.titleLength'),
|
|
||||||
missingChars = this.get('model.missingTitleCharacters');
|
|
||||||
let reason;
|
|
||||||
if( titleLength < 1 ){
|
|
||||||
reason = I18n.t('composer.error.title_missing');
|
|
||||||
} else if( missingChars > 0 ) {
|
|
||||||
reason = I18n.t('composer.error.title_too_short', {min: this.get('model.minimumTitleLength')});
|
|
||||||
} else if( titleLength > Discourse.SiteSettings.max_topic_title_length ) {
|
|
||||||
reason = I18n.t('composer.error.title_too_long', {max: Discourse.SiteSettings.max_topic_title_length});
|
|
||||||
}
|
|
||||||
|
|
||||||
if( reason ) {
|
|
||||||
return Discourse.InputValidation.create({ failed: true, reason: reason });
|
|
||||||
}
|
|
||||||
}.property('model.titleLength', 'model.missingTitleCharacters', 'model.minimumTitleLength'),
|
|
||||||
|
|
||||||
categoryValidation: function() {
|
|
||||||
if( !Discourse.SiteSettings.allow_uncategorized_topics && !this.get('model.categoryId')) {
|
|
||||||
return Discourse.InputValidation.create({ failed: true, reason: I18n.t('composer.error.category_missing') });
|
|
||||||
}
|
|
||||||
}.property('model.categoryId'),
|
|
||||||
|
|
||||||
replyValidation: function() {
|
|
||||||
const postType = this.get('model.post.post_type');
|
|
||||||
if (postType === this.site.get('post_types.small_action')) { return; }
|
|
||||||
|
|
||||||
const replyLength = this.get('model.replyLength'),
|
|
||||||
missingChars = this.get('model.missingReplyCharacters');
|
|
||||||
|
|
||||||
let reason;
|
|
||||||
if (replyLength < 1) {
|
|
||||||
reason = I18n.t('composer.error.post_missing');
|
|
||||||
} else if (missingChars > 0) {
|
|
||||||
reason = I18n.t('composer.error.post_length', {min: this.get('model.minimumPostLength')});
|
|
||||||
const tl = Discourse.User.currentProp("trust_level");
|
|
||||||
if (tl === 0 || tl === 1) {
|
|
||||||
reason += "<br/>" + I18n.t('composer.error.try_like');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reason) {
|
|
||||||
return Discourse.InputValidation.create({ failed: true, reason });
|
|
||||||
}
|
|
||||||
}.property('model.reply', 'model.replyLength', 'model.missingReplyCharacters', 'model.minimumPostLength'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
RSVP.EventTarget.mixin(ComposerView);
|
RSVP.EventTarget.mixin(ComposerView);
|
||||||
|
|
||||||
export default ComposerView;
|
export default ComposerView;
|
||||||
|
|
|
@ -36,9 +36,8 @@ export default Ember.View.extend({
|
||||||
// the quote reply widget
|
// the quote reply widget
|
||||||
//
|
//
|
||||||
// Same hack applied to Android cause it has unreliable touchend
|
// Same hack applied to Android cause it has unreliable touchend
|
||||||
const caps = this.capabilities;
|
const isAndroid = this.capabilities.isAndroid;
|
||||||
const android = caps.get('android');
|
if (this.capabilities.isWinphone || isAndroid) {
|
||||||
if (caps.get('winphone') || android) {
|
|
||||||
onSelectionChanged = _.debounce(onSelectionChanged, 500);
|
onSelectionChanged = _.debounce(onSelectionChanged, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +71,7 @@ export default Ember.View.extend({
|
||||||
|
|
||||||
// Android is dodgy, touchend often will not fire
|
// Android is dodgy, touchend often will not fire
|
||||||
// https://code.google.com/p/android/issues/detail?id=19827
|
// https://code.google.com/p/android/issues/detail?id=19827
|
||||||
if (!android) {
|
if (!isAndroid) {
|
||||||
$(document)
|
$(document)
|
||||||
.on('touchstart.quote-button', function(){
|
.on('touchstart.quote-button', function(){
|
||||||
view.set('isTouchInProgress', true);
|
view.set('isTouchInProgress', true);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import ContainerView from 'discourse/views/container';
|
import ContainerView from 'discourse/views/container';
|
||||||
import { default as property, observes, on } from 'ember-addons/ember-computed-decorators';
|
import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
export default ContainerView.extend({
|
export default ContainerView.extend({
|
||||||
classNameBindings: ['hidden', ':topic-map'],
|
classNameBindings: ['hidden', ':topic-map'],
|
||||||
|
@ -9,7 +9,7 @@ export default ContainerView.extend({
|
||||||
Ember.run.once(this, 'rerender');
|
Ember.run.once(this, 'rerender');
|
||||||
},
|
},
|
||||||
|
|
||||||
@property
|
@computed
|
||||||
hidden() {
|
hidden() {
|
||||||
if (!this.get('post.firstPost')) return true;
|
if (!this.get('post.firstPost')) return true;
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ export default Ember.View.extend({
|
||||||
_focusWhenOpened: function() {
|
_focusWhenOpened: function() {
|
||||||
|
|
||||||
// Don't focus on mobile or touch
|
// Don't focus on mobile or touch
|
||||||
if (Discourse.Mobile.mobileView || this.capabilities.get('touch')) {
|
if (Discourse.Mobile.mobileView || this.capabilities.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,74 +1,33 @@
|
||||||
import ModalBodyView from "discourse/views/modal-body";
|
import ModalBodyView from "discourse/views/modal-body";
|
||||||
|
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import { uploadTranslate } from 'discourse/controllers/upload-selector';
|
||||||
|
|
||||||
function uploadTranslate(key, options) {
|
|
||||||
const opts = options || {};
|
|
||||||
if (Discourse.Utilities.allowsAttachments()) { key += "_with_attachments"; }
|
|
||||||
return I18n.t("upload_selector." + key, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModalBodyView.extend({
|
export default ModalBodyView.extend({
|
||||||
templateName: 'modal/upload_selector',
|
templateName: 'modal/upload-selector',
|
||||||
classNames: ['upload-selector'],
|
classNames: ['upload-selector'],
|
||||||
|
|
||||||
// cf. http://stackoverflow.com/a/9851769/11983
|
@computed()
|
||||||
isOpera: !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0,
|
title() {
|
||||||
isFirefox: typeof InstallTrigger !== 'undefined',
|
return uploadTranslate("title");
|
||||||
isSafari: Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0,
|
},
|
||||||
isChrome: !!window.chrome && !this.isOpera,
|
|
||||||
|
|
||||||
title: function() { return uploadTranslate("title"); }.property(),
|
touchStart(evt) {
|
||||||
uploadIcon: function() { return Discourse.Utilities.allowsAttachments() ? "fa-upload" : "fa-picture-o"; }.property(),
|
|
||||||
|
|
||||||
touchStart: function(evt) {
|
|
||||||
// HACK: workaround Safari iOS being really weird and not shipping click events
|
// HACK: workaround Safari iOS being really weird and not shipping click events
|
||||||
if (this.isSafari && evt.target.id === "filename-input") {
|
if (this.capabilities.isSafari && evt.target.id === "filename-input") {
|
||||||
this.$('#filename-input').click();
|
this.$('#filename-input').click();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
tip: function() {
|
@on('didInsertElement')
|
||||||
const source = this.get("controller.local") ? "local" : "remote";
|
@observes('controller.local')
|
||||||
const authorized_extensions = Discourse.Utilities.authorizesAllExtensions() ? "" : `(${Discourse.Utilities.authorizedExtensions()})`;
|
selectedChanged() {
|
||||||
return uploadTranslate(source + "_tip", { authorized_extensions });
|
Ember.run.next(() => {
|
||||||
}.property("controller.local"),
|
|
||||||
|
|
||||||
hint: function() {
|
|
||||||
const isSupported = this.isChrome || this.isFirefox;
|
|
||||||
// chrome is the only browser that support copy & paste of images.
|
|
||||||
return I18n.t("upload_selector.hint" + (isSupported ? "_for_supported_browsers" : ""));
|
|
||||||
}.property(),
|
|
||||||
|
|
||||||
_selectOnInsert: function() {
|
|
||||||
this.selectedChanged();
|
|
||||||
}.on('didInsertElement'),
|
|
||||||
|
|
||||||
selectedChanged: function() {
|
|
||||||
const self = this;
|
|
||||||
Em.run.next(function() {
|
|
||||||
// *HACK* to select the proper radio button
|
// *HACK* to select the proper radio button
|
||||||
var value = self.get('controller.local') ? 'local' : 'remote';
|
const value = this.get('controller.local') ? 'local' : 'remote';
|
||||||
$('input:radio[name="upload"]').val([value]);
|
$('input:radio[name="upload"]').val([value]);
|
||||||
// focus the input
|
|
||||||
$('.inputs input:first').focus();
|
$('.inputs input:first').focus();
|
||||||
});
|
});
|
||||||
}.observes('controller.local'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
upload: function() {
|
|
||||||
if (this.get("controller.local")) {
|
|
||||||
$('#reply-control').fileupload('add', { fileInput: $('#filename-input') });
|
|
||||||
} else {
|
|
||||||
const imageUrl = $('#fileurl-input').val(),
|
|
||||||
imageLink = $('#link-input').val(),
|
|
||||||
composerView = this.get('controller.composerView');
|
|
||||||
if (this.get("controller.showMore") && imageLink.length > 3) {
|
|
||||||
composerView.addMarkdown("[![](" + imageUrl +")](" + imageLink + ")");
|
|
||||||
} else {
|
|
||||||
composerView.addMarkdown(imageUrl);
|
|
||||||
}
|
|
||||||
this.get('controller').send('closeModal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
//= require ./discourse/mixins/ajax
|
//= require ./discourse/mixins/ajax
|
||||||
//= require ./discourse
|
//= require ./discourse
|
||||||
|
|
||||||
// Pagedown customizations
|
|
||||||
//= require ./pagedown_custom.js
|
|
||||||
|
|
||||||
// Stuff we need to load first
|
// Stuff we need to load first
|
||||||
//= require_tree ./ember-addons/utils
|
//= require_tree ./ember-addons/utils
|
||||||
//= require ./ember-addons/decorator-alias
|
//= require ./ember-addons/decorator-alias
|
||||||
|
@ -77,6 +74,7 @@
|
||||||
//= require ./discourse/lib/emoji/emoji
|
//= require ./discourse/lib/emoji/emoji
|
||||||
//= require ./discourse/lib/emoji/emoji-groups
|
//= require ./discourse/lib/emoji/emoji-groups
|
||||||
//= require ./discourse/lib/emoji/emoji-toolbar
|
//= require ./discourse/lib/emoji/emoji-toolbar
|
||||||
|
//= require ./discourse/components/d-editor
|
||||||
//= require ./discourse/views/composer
|
//= require ./discourse/views/composer
|
||||||
//= require ./discourse/lib/show-modal
|
//= require ./discourse/lib/show-modal
|
||||||
//= require ./discourse/lib/screen-track
|
//= require ./discourse/lib/screen-track
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
window.PagedownCustom = {
|
|
||||||
insertButtons: [
|
|
||||||
{
|
|
||||||
id: 'wmd-quote-post',
|
|
||||||
description: I18n.t("composer.quote_post_title"),
|
|
||||||
execute: function() {
|
|
||||||
return Discourse.__container__.lookup('controller:composer').send('importQuote');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
appendButtons: [],
|
|
||||||
|
|
||||||
customActions: {
|
|
||||||
"doBlockquote": function(chunk, postProcessing, oldDoBlockquote) {
|
|
||||||
|
|
||||||
// When traditional linebreaks are set, use the default Pagedown implementation
|
|
||||||
if (Discourse.SiteSettings.traditional_markdown_linebreaks) {
|
|
||||||
return oldDoBlockquote.call(this, chunk, postProcessing);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our custom blockquote for non-traditional markdown linebreaks
|
|
||||||
var result = [];
|
|
||||||
chunk.selection.split(/\n/).forEach(function (line) {
|
|
||||||
var newLine = "";
|
|
||||||
if (line.indexOf("> ") === 0) {
|
|
||||||
newLine += line.substr(2);
|
|
||||||
} else {
|
|
||||||
if (/\S/.test(line)) { newLine += "> " + line; }
|
|
||||||
}
|
|
||||||
result.push(newLine);
|
|
||||||
});
|
|
||||||
chunk.selection = result.join("\n");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1188,7 +1188,7 @@ table.api-keys {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmd-input {
|
.d-editor-input {
|
||||||
width: 98%;
|
width: 98%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-wrapper .spinner {
|
.d-editor-textarea-wrapper .spinner {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
margin-top: 5em;
|
margin-top: 5em;
|
||||||
}
|
}
|
||||||
|
@ -99,10 +99,6 @@ div.ac-wrap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#reply-control.topic #wmd-quote-post {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auto-close-fields {
|
.auto-close-fields {
|
||||||
div:not(:first-child) {
|
div:not(:first-child) {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
@ -175,7 +171,7 @@ div.ac-wrap {
|
||||||
|
|
||||||
// this removes the topmost margin from the first element in the topic post
|
// 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
|
// if we don't do this, all posts would have extra space at the top
|
||||||
.wmd-preview > *:first-child {
|
.d-editor-preview > *:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.cooked > *:first-child {
|
.cooked > *:first-child {
|
||||||
|
|
|
@ -149,7 +149,7 @@ body {
|
||||||
background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%));
|
background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%));
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmd-input {
|
.d-editor-input {
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
// styles that apply to the PageDown editor
|
|
||||||
// http://code.google.com/p/pagedown/
|
|
||||||
|
|
||||||
.wmd-panel {
|
|
||||||
margin-left: 25%;
|
|
||||||
margin-right: 25%;
|
|
||||||
width: 50%;
|
|
||||||
min-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-button-bar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-button-row {
|
|
||||||
margin: 5px;
|
|
||||||
padding: 0;
|
|
||||||
height: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-spacer {
|
|
||||||
width: 1px;
|
|
||||||
height: 20px;
|
|
||||||
margin-right: 8px;
|
|
||||||
margin-left: 5px;
|
|
||||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
display: inline-block;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-button {
|
|
||||||
margin-right: 5px;
|
|
||||||
border: 0;
|
|
||||||
position: relative;
|
|
||||||
float: left;
|
|
||||||
font-family: FontAwesome;
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
text-decoration: inherit;
|
|
||||||
display: inline;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
line-height: normal;
|
|
||||||
vertical-align: baseline;
|
|
||||||
background-image: none !important;
|
|
||||||
background-position: 0 0;
|
|
||||||
background-repeat: repeat;
|
|
||||||
background: transparent;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-button:hover {
|
|
||||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.wmd-bold-button:before {
|
|
||||||
content: "\f032";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-italic-button:before {
|
|
||||||
content: "\f033";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-link-button:before {
|
|
||||||
content: "\f0c1";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-quote-button:before {
|
|
||||||
content: "\f10e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-code-button:before {
|
|
||||||
content: "\f121";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-image-button:before {
|
|
||||||
content: "\f093";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-image-button.image-only:before {
|
|
||||||
content: "\f03e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-olist-button:before {
|
|
||||||
content: "\f0cb";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-ulist-button:before {
|
|
||||||
content: "\f0ca";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-heading-button:before {
|
|
||||||
content: "\f031";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-hr-button:before {
|
|
||||||
content: "\f068";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-undo-button:before {
|
|
||||||
content: "\f0e2";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-redo-button:before {
|
|
||||||
content: "\f01e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-quote-post:before {
|
|
||||||
content: "\f0e5";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-composer-options:before {
|
|
||||||
content: "\f013";
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-prompt-background {
|
|
||||||
background-color: #111;
|
|
||||||
box-shadow: 0 3px 7px rgba(0,0,0, .8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-prompt-dialog {
|
|
||||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-prompt-dialog > div {
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-family: arial, helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-prompt-dialog > form > input[type="text"] {
|
|
||||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
color: $primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wmd-prompt-dialog > form > input[type="button"] {
|
|
||||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
|
||||||
background: dark-light-choose(initial, blend-primary-secondary(50%));
|
|
||||||
color: dark-light-choose(inherit, $secondary);
|
|
||||||
font-family: trebuchet MS, helvetica, sans-serif;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
|
@ -26,7 +26,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// global styles for the cooked HTML content in posts (and preview)
|
// global styles for the cooked HTML content in posts (and preview)
|
||||||
.cooked, .wmd-preview {
|
.cooked, .d-editor-preview {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
h1, h2, h3, h4, h5, h6 { margin: 30px 0 10px; }
|
h1, h2, h3, h4, h5, h6 { margin: 30px 0 10px; }
|
||||||
h1 { line-height: 1em; } /* normalize.css sets h1 font size but not line height */
|
h1 { line-height: 1em; } /* normalize.css sets h1 font size but not line height */
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.cooked, .wmd-preview {
|
.cooked, .d-editor-preview {
|
||||||
video {
|
video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,14 +278,14 @@
|
||||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.wmd-input:disabled {
|
.d-editor-input:disabled {
|
||||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
}
|
}
|
||||||
.wmd-input, .wmd-preview {
|
.d-editor-input, .d-editor-preview {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmd-preview {
|
.d-editor-preview {
|
||||||
border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%);
|
border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
@ -303,7 +303,7 @@
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.wmd-input {
|
.d-editor-input {
|
||||||
bottom: 35px;
|
bottom: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,19 +351,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#reply-control {
|
#reply-control {
|
||||||
&.hide-preview {
|
.wmd-controls.hide-preview {
|
||||||
.wmd-controls {
|
.d-editor-input {
|
||||||
.wmd-input {
|
width: 100%;
|
||||||
width: 100%;
|
}
|
||||||
}
|
.d-editor-preview-wrapper {
|
||||||
.preview-wrapper {
|
display: none;
|
||||||
display: none;
|
}
|
||||||
}
|
.d-editor-textarea-wrapper {
|
||||||
.textarea-wrapper {
|
width: 100%;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wmd-controls {
|
.wmd-controls {
|
||||||
left: 30px;
|
left: 30px;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
|
@ -372,7 +371,7 @@
|
||||||
top: 50px;
|
top: 50px;
|
||||||
|
|
||||||
|
|
||||||
.wmd-input, .wmd-preview-scroller, .wmd-preview {
|
.d-editor-input, .d-editor-preview {
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -383,7 +382,7 @@
|
||||||
background-color: $secondary;
|
background-color: $secondary;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.wmd-input, .wmd-preview-scroller {
|
.d-editor-input, .d-editor-preview-header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -391,18 +390,17 @@
|
||||||
border-top: 30px solid transparent;
|
border-top: 30px solid transparent;
|
||||||
@include border-radius-all(0);
|
@include border-radius-all(0);
|
||||||
}
|
}
|
||||||
.wmd-preview-scroller {
|
.d-editor-preview-header {
|
||||||
font-size: 0.929em;
|
font-size: 0.929em;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
visibility: hidden;
|
|
||||||
.marker, .caret {
|
.marker, .caret {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.textarea-wrapper, .preview-wrapper {
|
.d-editor, .d-editor-container, .d-editor-textarea-wrapper, .d-editor-preview-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -410,9 +408,9 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 50%;
|
|
||||||
}
|
}
|
||||||
.textarea-wrapper {
|
.d-editor-textarea-wrapper {
|
||||||
|
width: 50%;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
float: left;
|
float: left;
|
||||||
.popup-tip {
|
.popup-tip {
|
||||||
|
@ -420,12 +418,13 @@
|
||||||
right: 4px;
|
right: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.preview-wrapper {
|
.d-editor-preview-wrapper {
|
||||||
|
width: 50%;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.wmd-button-bar {
|
.d-editor-button-bar {
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
width: $topic-body-width;
|
width: $topic-body-width;
|
||||||
float: left;
|
float: left;
|
||||||
|
|
||||||
.wmd-input {
|
.d-editor-input {
|
||||||
width: 98%;
|
width: 98%;
|
||||||
height: 15em;
|
height: 15em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bio-composer #wmd-quote-post {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.static {
|
.static {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -163,13 +163,13 @@ input {
|
||||||
background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
|
background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.wmd-input:disabled {
|
.d-editor-input:disabled {
|
||||||
background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
|
background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
|
||||||
}
|
}
|
||||||
.wmd-input {
|
.d-editor-input {
|
||||||
color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%));
|
color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%));
|
||||||
}
|
}
|
||||||
.wmd-input {
|
.d-editor-input {
|
||||||
bottom: 35px;
|
bottom: 35px;
|
||||||
}
|
}
|
||||||
.submit-panel {
|
.submit-panel {
|
||||||
|
@ -196,7 +196,7 @@ input {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
}
|
}
|
||||||
.textarea-wrapper .popup-tip {
|
.d-editor-textarea-wrapper .popup-tip {
|
||||||
top: 28px;
|
top: 28px;
|
||||||
}
|
}
|
||||||
button.btn.no-text {
|
button.btn.no-text {
|
||||||
|
@ -221,23 +221,22 @@ input {
|
||||||
top: 40px;
|
top: 40px;
|
||||||
bottom: 50px;
|
bottom: 50px;
|
||||||
display: block;
|
display: block;
|
||||||
|
.d-editor-container {
|
||||||
.wmd-input {
|
padding: 0;
|
||||||
|
}
|
||||||
|
.d-editor-preview-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.d-editor-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 180px;
|
||||||
min-height: 100%;
|
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: $secondary;
|
background-color: $secondary;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.wmd-input {
|
.d-editor-textarea-wrapper {
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.textarea-wrapper {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -250,7 +249,7 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.wmd-button-bar {
|
.d-editor-button-bar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,10 +63,6 @@
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bio-composer #wmd-quote-post {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {width: 100%;}
|
textarea {width: 100%;}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,10 +95,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bio-composer #wmd-quote-post {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.static {
|
.static {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -179,12 +179,12 @@ var runTests = function() {
|
||||||
|
|
||||||
$("#create-topic").click();
|
$("#create-topic").click();
|
||||||
$("#reply-title").val(title).trigger("change");
|
$("#reply-title").val(title).trigger("change");
|
||||||
$("#reply-control .wmd-input").val(post).trigger("change");
|
$("#reply-control .d-editor-input").val(post).trigger("change");
|
||||||
$("#reply-control .wmd-input").focus()[0].setSelectionRange(post.length, post.length);
|
$("#reply-control .d-editor-input").focus()[0].setSelectionRange(post.length, post.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
exec("open upload modal", function() {
|
exec("open upload modal", function() {
|
||||||
$(".wmd-image-button").click();
|
$(".d-editor-button-bar .upload").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("upload modal is open", function() {
|
test("upload modal is open", function() {
|
||||||
|
@ -214,16 +214,16 @@ var runTests = function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("composer is open", function() {
|
test("composer is open", function() {
|
||||||
return document.querySelector("#reply-control .wmd-input");
|
return document.querySelector("#reply-control .d-editor-input");
|
||||||
});
|
});
|
||||||
|
|
||||||
exec("compose reply", function() {
|
exec("compose reply", function() {
|
||||||
var post = "I can even write a reply inside the smoke test ;) (" + (+new Date()) + ")";
|
var post = "I can even write a reply inside the smoke test ;) (" + (+new Date()) + ")";
|
||||||
$("#reply-control .wmd-input").val(post).trigger("change");
|
$("#reply-control .d-editor-input").val(post).trigger("change");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("waiting for the preview", function() {
|
test("waiting for the preview", function() {
|
||||||
return $(".wmd-preview").text().trim().indexOf("I can even write") === 0;
|
return $(".d-editor-preview").text().trim().indexOf("I can even write") === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
execAsync("submit the reply", 6000, function() {
|
execAsync("submit the reply", 6000, function() {
|
||||||
|
|
|
@ -10,25 +10,25 @@ test("Tests the Composer controls", () => {
|
||||||
|
|
||||||
click('#create-topic');
|
click('#create-topic');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(exists('.wmd-input'), 'the composer input is visible');
|
ok(exists('.d-editor-input'), 'the composer input is visible');
|
||||||
ok(exists('.title-input .popup-tip.bad.hide'), 'title errors are hidden by default');
|
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');
|
ok(exists('.d-editor-textarea-wrapper .popup-tip.bad.hide'), 'body errors are hidden by default');
|
||||||
});
|
});
|
||||||
|
|
||||||
click('a.toggle-preview');
|
click('a.toggle-preview');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(!exists('.wmd-preview:visible'), "clicking the toggle hides the preview");
|
ok(!exists('.d-editor-preview:visible'), "clicking the toggle hides the preview");
|
||||||
});
|
});
|
||||||
|
|
||||||
click('a.toggle-preview');
|
click('a.toggle-preview');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(exists('.wmd-preview:visible'), "clicking the toggle shows the preview again");
|
ok(exists('.d-editor-preview:visible'), "clicking the toggle shows the preview again");
|
||||||
});
|
});
|
||||||
|
|
||||||
click('#reply-control button.create');
|
click('#reply-control button.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(!exists('.title-input .popup-tip.bad.hide'), 'it shows the empty title error');
|
ok(!exists('.title-input .popup-tip.bad.hide'), 'it shows the empty title error');
|
||||||
ok(!exists('.textarea-wrapper .popup-tip.bad.hide'), 'it shows the empty body error');
|
ok(!exists('.d-editor-wrapper .popup-tip.bad.hide'), 'it shows the empty body error');
|
||||||
});
|
});
|
||||||
|
|
||||||
fillIn('#reply-title', "this is my new topic title");
|
fillIn('#reply-title', "this is my new topic title");
|
||||||
|
@ -36,10 +36,10 @@ test("Tests the Composer controls", () => {
|
||||||
ok(exists('.title-input .popup-tip.good'), 'the title is now good');
|
ok(exists('.title-input .popup-tip.good'), 'the title is now good');
|
||||||
});
|
});
|
||||||
|
|
||||||
fillIn('.wmd-input', "this is the *content* of a post");
|
fillIn('.d-editor-input', "this is the *content* of a post");
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-preview').html(), "<p>this is the <em>content</em> of a post</p>", "it previews content");
|
equal(find('.d-editor-preview').html().trim(), "<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');
|
ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good');
|
||||||
});
|
});
|
||||||
|
|
||||||
click('#reply-control a.cancel');
|
click('#reply-control a.cancel');
|
||||||
|
@ -58,7 +58,7 @@ test("Create a topic with server side errors", () => {
|
||||||
visit("/");
|
visit("/");
|
||||||
click('#create-topic');
|
click('#create-topic');
|
||||||
fillIn('#reply-title', "this title triggers an error");
|
fillIn('#reply-title', "this title triggers an error");
|
||||||
fillIn('.wmd-input', "this is the *content* of a post");
|
fillIn('.d-editor-input', "this is the *content* of a post");
|
||||||
click('#reply-control button.create');
|
click('#reply-control button.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(exists('.bootbox.modal'), 'it pops up an error message');
|
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');
|
click('.bootbox.modal a.btn-primary');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(!exists('.bootbox.modal'), 'it dismisses the error');
|
ok(!exists('.bootbox.modal'), 'it dismisses the error');
|
||||||
ok(exists('.wmd-input'), 'the composer input is visible');
|
ok(exists('.d-editor-input'), 'the composer input is visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ test("Create a Topic", () => {
|
||||||
visit("/");
|
visit("/");
|
||||||
click('#create-topic');
|
click('#create-topic');
|
||||||
fillIn('#reply-title', "Internationalization Localization");
|
fillIn('#reply-title', "Internationalization Localization");
|
||||||
fillIn('.wmd-input', "this is the *content* of a new topic post");
|
fillIn('.d-editor-input', "this is the *content* of a new topic post");
|
||||||
click('#reply-control button.create');
|
click('#reply-control button.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(currentURL(), "/t/internationalization-localization/280", "it transitions to the newly created topic URL");
|
equal(currentURL(), "/t/internationalization-localization/280", "it transitions to the newly created topic URL");
|
||||||
|
@ -85,7 +85,7 @@ test("Create an enqueued Topic", () => {
|
||||||
visit("/");
|
visit("/");
|
||||||
click('#create-topic');
|
click('#create-topic');
|
||||||
fillIn('#reply-title', "Internationalization Localization");
|
fillIn('#reply-title', "Internationalization Localization");
|
||||||
fillIn('.wmd-input', "enqueue this content please");
|
fillIn('.d-editor-input', "enqueue this content please");
|
||||||
click('#reply-control button.create');
|
click('#reply-control button.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(visible('#discourse-modal'), 'it pops up a modal');
|
ok(visible('#discourse-modal'), 'it pops up a modal');
|
||||||
|
@ -108,11 +108,11 @@ test("Create a Reply", () => {
|
||||||
|
|
||||||
click('#topic-footer-buttons .btn.create');
|
click('#topic-footer-buttons .btn.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(exists('.wmd-input'), 'the composer input is visible');
|
ok(exists('.d-editor-input'), 'the composer input is visible');
|
||||||
ok(!exists('#reply-title'), 'there is no title since this is a reply');
|
ok(!exists('#reply-title'), 'there is no title since this is a reply');
|
||||||
});
|
});
|
||||||
|
|
||||||
fillIn('.wmd-input', 'this is the content of my reply');
|
fillIn('.d-editor-input', 'this is the content of my reply');
|
||||||
click('#reply-control button.create');
|
click('#reply-control button.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.cooked:last p').text(), 'this is the content of my reply');
|
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) => {
|
test("Posting on a different topic", (assert) => {
|
||||||
visit("/t/internationalization-localization/280");
|
visit("/t/internationalization-localization/280");
|
||||||
click('#topic-footer-buttons .btn.create');
|
click('#topic-footer-buttons .btn.create');
|
||||||
fillIn('.wmd-input', 'this is the content for a different topic');
|
fillIn('.d-editor-input', 'this is the content for a different topic');
|
||||||
|
|
||||||
visit("/t/1-3-0beta9-no-rate-limit-popups/28830");
|
visit("/t/1-3-0beta9-no-rate-limit-popups/28830");
|
||||||
andThen(function() {
|
andThen(function() {
|
||||||
|
@ -145,11 +145,11 @@ test("Create an enqueued Reply", () => {
|
||||||
|
|
||||||
click('#topic-footer-buttons .btn.create');
|
click('#topic-footer-buttons .btn.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(exists('.wmd-input'), 'the composer input is visible');
|
ok(exists('.d-editor-input'), 'the composer input is visible');
|
||||||
ok(!exists('#reply-title'), 'there is no title since this is a reply');
|
ok(!exists('#reply-title'), 'there is no title since this is a reply');
|
||||||
});
|
});
|
||||||
|
|
||||||
fillIn('.wmd-input', 'enqueue this content please');
|
fillIn('.d-editor-input', 'enqueue this content please');
|
||||||
click('#reply-control button.create');
|
click('#reply-control button.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(find('.cooked:last p').text() !== 'enqueue this content please', "it doesn't insert the post");
|
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=showMoreActions]');
|
||||||
click('.topic-post:eq(0) button[data-action=edit]');
|
click('.topic-post:eq(0) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-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('.d-editor-input', "This is the new text for the post");
|
||||||
fillIn('#reply-title', "This is the new text for the title");
|
fillIn('#reply-title', "This is the new text for the title");
|
||||||
click('#reply-control button.create');
|
click('#reply-control button.create');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(!exists('.wmd-input'), 'it closes the composer');
|
ok(!exists('.d-editor-input'), 'it closes the composer');
|
||||||
ok(exists('.topic-post:eq(0) .post-info.edits'), 'it has the edits icon');
|
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-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');
|
ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post');
|
||||||
|
@ -192,11 +192,11 @@ test("Composer can switch between edits", () => {
|
||||||
|
|
||||||
click('.topic-post:eq(0) button[data-action=edit]');
|
click('.topic-post:eq(0) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
||||||
});
|
});
|
||||||
click('.topic-post:eq(1) button[data-action=edit]');
|
click('.topic-post:eq(1) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('This is the second post.'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-input').val().indexOf('This is the second post.'), 0, 'it populates the input with the post text');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -204,14 +204,14 @@ test("Composer with dirty edit can toggle to another edit", () => {
|
||||||
visit("/t/this-is-a-test-topic/9");
|
visit("/t/this-is-a-test-topic/9");
|
||||||
|
|
||||||
click('.topic-post:eq(0) button[data-action=edit]');
|
click('.topic-post:eq(0) button[data-action=edit]');
|
||||||
fillIn('.wmd-input', 'This is a dirty reply');
|
fillIn('.d-editor-input', 'This is a dirty reply');
|
||||||
click('.topic-post:eq(1) button[data-action=edit]');
|
click('.topic-post:eq(1) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
|
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
|
||||||
});
|
});
|
||||||
click('.modal-footer a:eq(0)');
|
click('.modal-footer a:eq(0)');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('This is the second post.'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-input').val().indexOf('This is the second post.'), 0, 'it populates the input with the post text');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -220,15 +220,15 @@ test("Composer can toggle between edit and reply", () => {
|
||||||
|
|
||||||
click('.topic-post:eq(0) button[data-action=edit]');
|
click('.topic-post:eq(0) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
||||||
});
|
});
|
||||||
click('.topic-post:eq(0) button[data-action=reply]');
|
click('.topic-post:eq(0) button[data-action=reply]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val(), "", 'it clears the input');
|
equal(find('.d-editor-input').val(), "", 'it clears the input');
|
||||||
});
|
});
|
||||||
click('.topic-post:eq(0) button[data-action=edit]');
|
click('.topic-post:eq(0) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -236,14 +236,14 @@ test("Composer with dirty reply can toggle to edit", () => {
|
||||||
visit("/t/this-is-a-test-topic/9");
|
visit("/t/this-is-a-test-topic/9");
|
||||||
|
|
||||||
click('.topic-post:eq(0) button[data-action=reply]');
|
click('.topic-post:eq(0) button[data-action=reply]');
|
||||||
fillIn('.wmd-input', 'This is a dirty reply');
|
fillIn('.d-editor-input', 'This is a dirty reply');
|
||||||
click('.topic-post:eq(0) button[data-action=edit]');
|
click('.topic-post:eq(0) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
|
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
|
||||||
});
|
});
|
||||||
click('.modal-footer a:eq(0)');
|
click('.modal-footer a:eq(0)');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ test("Composer draft with dirty reply can toggle to edit", () => {
|
||||||
visit("/t/this-is-a-test-topic/9");
|
visit("/t/this-is-a-test-topic/9");
|
||||||
|
|
||||||
click('.topic-post:eq(0) button[data-action=reply]');
|
click('.topic-post:eq(0) button[data-action=reply]');
|
||||||
fillIn('.wmd-input', 'This is a dirty reply');
|
fillIn('.d-editor-input', 'This is a dirty reply');
|
||||||
click('.toggler');
|
click('.toggler');
|
||||||
click('.topic-post:eq(0) button[data-action=edit]');
|
click('.topic-post:eq(0) button[data-action=edit]');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
|
@ -259,6 +259,6 @@ test("Composer draft with dirty reply can toggle to edit", () => {
|
||||||
});
|
});
|
||||||
click('.modal-footer a:eq(0)');
|
click('.modal-footer a:eq(0)');
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
equal(find('.wmd-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,13 +8,10 @@ componentTest('preview updates with markdown', {
|
||||||
|
|
||||||
test(assert) {
|
test(assert) {
|
||||||
assert.ok(this.$('.d-editor-button-bar').length);
|
assert.ok(this.$('.d-editor-button-bar').length);
|
||||||
assert.equal(this.$('.d-editor-preview.hidden').length, 1);
|
|
||||||
|
|
||||||
fillIn('.d-editor-input', 'hello **world**');
|
fillIn('.d-editor-input', 'hello **world**');
|
||||||
|
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
assert.equal(this.get('value'), 'hello **world**');
|
assert.equal(this.get('value'), 'hello **world**');
|
||||||
assert.equal(this.$('.d-editor-preview.hidden').length, 0);
|
|
||||||
assert.equal(this.$('.d-editor-preview').html().trim(), '<p>hello <strong>world</strong></p>');
|
assert.equal(this.$('.d-editor-preview').html().trim(), '<p>hello <strong>world</strong></p>');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { blank } from 'helpers/qunit-helpers';
|
import { blank } from 'helpers/qunit-helpers';
|
||||||
import { currentUser } from 'helpers/qunit-helpers';
|
import { currentUser } from 'helpers/qunit-helpers';
|
||||||
import KeyValueStore from 'discourse/lib/key-value-store';
|
|
||||||
import Composer from 'discourse/models/composer';
|
import Composer from 'discourse/models/composer';
|
||||||
import createStore from 'helpers/create-store';
|
import createStore from 'helpers/create-store';
|
||||||
|
|
||||||
module("model:composer");
|
module("model:composer");
|
||||||
|
|
||||||
const keyValueStore = new KeyValueStore("_test_composer");
|
|
||||||
|
|
||||||
function createComposer(opts) {
|
function createComposer(opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
opts.user = opts.user || currentUser();
|
opts.user = opts.user || currentUser();
|
||||||
|
@ -185,22 +182,6 @@ test('initial category when uncategorized is not allowed', function() {
|
||||||
ok(!composer.get('categoryId'), "Uncategorized by default. Must choose a category.");
|
ok(!composer.get('categoryId'), "Uncategorized by default. Must choose a category.");
|
||||||
});
|
});
|
||||||
|
|
||||||
test('showPreview', function() {
|
|
||||||
const newComposer = function() {
|
|
||||||
return openComposer({action: 'createTopic', draftKey: 'asfd', draftSequence: 1});
|
|
||||||
};
|
|
||||||
|
|
||||||
Discourse.Mobile.mobileView = true;
|
|
||||||
equal(newComposer().get('showPreview'), false, "Don't show preview in mobile view");
|
|
||||||
|
|
||||||
keyValueStore.set({ key: 'composer.showPreview', value: 'true' });
|
|
||||||
equal(newComposer().get('showPreview'), false, "Don't show preview in mobile view even if KeyValueStore wants to");
|
|
||||||
keyValueStore.remove('composer.showPreview');
|
|
||||||
|
|
||||||
Discourse.Mobile.mobileView = false;
|
|
||||||
equal(newComposer().get('showPreview'), true, "Show preview by default in desktop view");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('open with a quote', function() {
|
test('open with a quote', function() {
|
||||||
const quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]';
|
const quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]';
|
||||||
const newComposer = function() {
|
const newComposer = function() {
|
||||||
|
|
|
@ -21,9 +21,6 @@
|
||||||
//= require ../../app/assets/javascripts/locales/i18n
|
//= require ../../app/assets/javascripts/locales/i18n
|
||||||
//= require ../../app/assets/javascripts/locales/en
|
//= require ../../app/assets/javascripts/locales/en
|
||||||
|
|
||||||
// Pagedown customizations
|
|
||||||
//= require ../../app/assets/javascripts/pagedown_custom.js
|
|
||||||
|
|
||||||
//= require vendor
|
//= require vendor
|
||||||
|
|
||||||
//= require htmlparser.js
|
//= require htmlparser.js
|
||||||
|
|
Loading…
Reference in a new issue