mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 02:11:08 -06:00
FEATURE: Replace composer editor with ember version
This commit is contained in:
parent
fc27b7442f
commit
47495a5713
@ -6,7 +6,6 @@ languages:
|
||||
|
||||
exclude_paths:
|
||||
- "app/assets/javascripts/defer/*"
|
||||
- "app/assets/javascripts/discourse/lib/Markdown.Editor.js"
|
||||
- "app/assets/javascripts/ember-addons/*"
|
||||
- "lib/autospec/*"
|
||||
- "lib/es6_module_transpiler/*"
|
||||
|
@ -6,9 +6,7 @@ app/assets/javascripts/pagedown_custom.js
|
||||
app/assets/javascripts/vendor.js
|
||||
app/assets/javascripts/locales/i18n.js
|
||||
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
||||
app/assets/javascripts/discourse/lib/Markdown.Editor.js
|
||||
app/assets/javascripts/ember-addons/
|
||||
jsapp/lib/Markdown.Editor.js
|
||||
lib/javascripts/locale/
|
||||
lib/javascripts/messageformat.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 */
|
||||
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";
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
if (button.sendAction) {
|
||||
createdButton.sendAction = button.sendAction;
|
||||
}
|
||||
|
||||
const title = I18n.t(button.title || `composer.${button.id}_title`);
|
||||
if (button.shortcut) {
|
||||
const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
@ -130,7 +134,11 @@ Toolbar.prototype.addButton = function(button) {
|
||||
createdButton.title = title;
|
||||
}
|
||||
|
||||
if (button.unshift) {
|
||||
g.buttons.unshift(createdButton);
|
||||
} else {
|
||||
g.buttons.push(createdButton);
|
||||
}
|
||||
};
|
||||
|
||||
export function onToolbarCreate(func) {
|
||||
@ -144,9 +152,16 @@ export default Ember.Component.extend({
|
||||
link: '',
|
||||
lastSel: null,
|
||||
|
||||
@computed('placeholder')
|
||||
placeholderTranslated(placeholder) {
|
||||
if (placeholder) return I18n.t(placeholder);
|
||||
return null;
|
||||
},
|
||||
|
||||
@on('didInsertElement')
|
||||
_startUp() {
|
||||
this._applyEmojiAutocomplete();
|
||||
|
||||
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
|
||||
|
||||
const shortcuts = this.get('toolbar.shortcuts');
|
||||
@ -156,27 +171,52 @@ export default Ember.Component.extend({
|
||||
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')
|
||||
_shutDown() {
|
||||
this.appEvents.off('composer:insert-text');
|
||||
|
||||
Ember.keys(this.get('toolbar.shortcuts')).forEach(sc => {
|
||||
Mousetrap(this.$('.d-editor-input')[0]).unbind(sc);
|
||||
});
|
||||
this.$('.d-editor-preview').off('click.preview');
|
||||
},
|
||||
|
||||
@property
|
||||
@computed
|
||||
toolbar() {
|
||||
const toolbar = new Toolbar();
|
||||
_createCallbacks.forEach(cb => cb(toolbar));
|
||||
this.sendAction('extraButtons', toolbar);
|
||||
return toolbar;
|
||||
},
|
||||
|
||||
@property('ready', 'value')
|
||||
@computed('ready', 'value')
|
||||
preview(ready, value) {
|
||||
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 : "";
|
||||
},
|
||||
|
||||
@ -339,12 +379,18 @@ export default Ember.Component.extend({
|
||||
actions: {
|
||||
toolbarButton(button) {
|
||||
const selected = this._getSelected();
|
||||
button.perform({
|
||||
const toolbarEvent = {
|
||||
selected,
|
||||
applySurround: (head, tail, exampleKey) => this._applySurround(selected, head, tail, exampleKey),
|
||||
applyList: (head, exampleKey) => this._applyList(selected, head, exampleKey),
|
||||
addText: text => this._addText(selected, text)
|
||||
});
|
||||
};
|
||||
|
||||
if (button.sendAction) {
|
||||
return this.sendAction(button.sendAction, toolbarEvent);
|
||||
} else {
|
||||
button.perform(toolbarEvent);
|
||||
}
|
||||
},
|
||||
|
||||
showLinkModal() {
|
||||
|
@ -5,7 +5,7 @@ export default buildCategoryPanel('topic-template', {
|
||||
if (this.get('activeTab')) {
|
||||
const self = this;
|
||||
Ember.run.schedule('afterRender', function() {
|
||||
self.$('.wmd-input').focus();
|
||||
self.$('.d-editor-input').focus();
|
||||
});
|
||||
}
|
||||
}.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";
|
||||
|
||||
export default Em.Component.extend(UploadMixin, {
|
||||
classNames: ["image-uploader"],
|
||||
|
||||
@property('imageUrl')
|
||||
@computed('imageUrl')
|
||||
backgroundStyle(imageUrl) {
|
||||
if (Em.isNone(imageUrl)) { return; }
|
||||
return `background-image: url(${imageUrl})`.htmlSafe();
|
||||
|
@ -1,9 +1,9 @@
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
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, {
|
||||
classNameBindings: [':popup-tip', 'good', 'bad', 'shownAt::hide'],
|
||||
classNameBindings: [':popup-tip', 'good', 'bad', 'lastShownAt::hide'],
|
||||
animateAttribute: null,
|
||||
bouncePixels: 6,
|
||||
bounceDelay: 100,
|
||||
@ -16,9 +16,14 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
bad: Ember.computed.alias("validation.failed"),
|
||||
good: Ember.computed.not("bad"),
|
||||
|
||||
@observes("shownAt")
|
||||
@computed('shownAt', 'validation.lastShownAt')
|
||||
lastShownAt(shownAt, lastShownAt) {
|
||||
return shownAt || lastShownAt;
|
||||
},
|
||||
|
||||
@observes('lastShownAt')
|
||||
bounce() {
|
||||
if (this.get("shownAt")) {
|
||||
if (this.get("lastShownAt")) {
|
||||
var $elem = this.$();
|
||||
if (!this.animateAttribute) {
|
||||
this.animateAttribute = $elem.css('left') === 'auto' ? 'right' : 'left';
|
||||
|
@ -2,7 +2,7 @@ const MAX_SHOWN = 5;
|
||||
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
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;
|
||||
|
||||
@ -12,7 +12,7 @@ export default Component.extend(StringBuffer, {
|
||||
rerenderTriggers: ['expanded'],
|
||||
|
||||
// Roll up links to avoid duplicates
|
||||
@property('links')
|
||||
@computed('links')
|
||||
collapsed(links) {
|
||||
const seen = {};
|
||||
const result = [];
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
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) {
|
||||
opts = opts || {};
|
||||
@ -50,17 +49,17 @@ export default Ember.Controller.extend({
|
||||
|
||||
showEditReason: false,
|
||||
editReason: null,
|
||||
maxTitleLength: setting('max_topic_title_length'),
|
||||
scopedCategoryId: null,
|
||||
similarTopics: null,
|
||||
similarTopicsMessage: null,
|
||||
lastSimilaritySearch: null,
|
||||
optionsVisible: false,
|
||||
|
||||
topic: null,
|
||||
lastValidatedAt: null,
|
||||
|
||||
// TODO: Remove this, very bad
|
||||
view: null,
|
||||
isUploading: false,
|
||||
|
||||
topic: null,
|
||||
|
||||
_initializeSimilar: function() {
|
||||
this.set('similarTopics', []);
|
||||
@ -109,7 +108,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
// Import a quote from the post
|
||||
importQuote() {
|
||||
importQuote(toolbarEvent) {
|
||||
const postStream = this.get('topic.postStream');
|
||||
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) {
|
||||
const quote = Quote.build(post, post.get("raw"), {raw: true, full: true});
|
||||
composer.appendBlockAtCursor(quote);
|
||||
toolbarEvent.addText(quote);
|
||||
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() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
|
||||
|
||||
toggle() {
|
||||
this.closeAutocomplete();
|
||||
switch (this.get('model.composeState')) {
|
||||
@ -225,7 +195,7 @@ export default Ember.Controller.extend({
|
||||
return false;
|
||||
},
|
||||
|
||||
disableSubmit: Ember.computed.or("model.loading", "view.isUploading"),
|
||||
disableSubmit: Ember.computed.or("model.loading", "isUploading"),
|
||||
|
||||
save(force) {
|
||||
const composer = this.get('model');
|
||||
@ -237,12 +207,7 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
if (composer.get('cantSubmitPost')) {
|
||||
const now = Date.now();
|
||||
this.setProperties({
|
||||
'view.showTitleTip': now,
|
||||
'view.showCategoryTip': now,
|
||||
'view.showReplyTip': now
|
||||
});
|
||||
this.set('lastValidatedAt', Date.now());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -291,10 +256,18 @@ export default Ember.Controller.extend({
|
||||
var staged = false;
|
||||
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
|
||||
|
||||
const promise = composer.save({
|
||||
imageSizes: this.get('view').imageSizes(),
|
||||
editReason: this.get("editReason")
|
||||
}).then(function(result) {
|
||||
// TODO: This should not happen in model
|
||||
const imageSizes = {};
|
||||
$('#reply-control .d-editor-preview img').each((i, e) => {
|
||||
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") {
|
||||
self.send('postWasEnqueued', result.responseJson);
|
||||
self.destroyDraft();
|
||||
@ -366,8 +339,8 @@ export default Ember.Controller.extend({
|
||||
// We don't care about similar topics unless creating a topic
|
||||
if (!this.get('model.creatingTopic')) { return; }
|
||||
|
||||
let body = this.get('model.reply');
|
||||
const title = this.get('model.title');
|
||||
let body = this.get('model.reply') || '';
|
||||
const title = this.get('model.title') || '';
|
||||
|
||||
// Ensure the fields are of the minimum length
|
||||
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
|
||||
|
||||
@ -502,7 +470,7 @@ export default Ember.Controller.extend({
|
||||
composerModel.set('composeState', Discourse.Composer.OPEN);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -572,7 +540,6 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
shrink() {
|
||||
if (this.get('model.replyDirty')) {
|
||||
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() {
|
||||
this.saveDraft();
|
||||
this._saveDraft();
|
||||
this.set('model.composeState', Discourse.Composer.DRAFT);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.setProperties({
|
||||
model: null,
|
||||
'view.showTitleTip': false,
|
||||
'view.showCategoryTip': false,
|
||||
'view.showReplyTip': false
|
||||
});
|
||||
this.setProperties({ model: null, lastValidatedAt: null });
|
||||
},
|
||||
|
||||
closeAutocomplete() {
|
||||
$('.wmd-input').autocomplete({ cancel: true });
|
||||
$('.d-editor-input').autocomplete({ cancel: true });
|
||||
},
|
||||
|
||||
showOptions() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
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({
|
||||
needs: ['topic', 'composer'],
|
||||
@ -9,7 +9,7 @@ export default Ember.Controller.extend({
|
||||
loadScript('defer/html-sanitizer-bundle');
|
||||
}.on('init'),
|
||||
|
||||
@property('buffer', 'postId')
|
||||
@computed('buffer', 'postId')
|
||||
post(buffer, postId) {
|
||||
if (!postId || Ember.isEmpty(buffer)) { return null; }
|
||||
|
||||
@ -135,7 +135,7 @@ export default Ember.Controller.extend({
|
||||
const quotedText = Quote.build(post, buffer);
|
||||
composerOpts.quote = quotedText;
|
||||
if (composerController.get('content.viewOpen') || composerController.get('content.viewDraft')) {
|
||||
composerController.appendBlockAtCursor(quotedText.trim());
|
||||
this.appEvents.trigger('composer:insert-text', quotedText.trim());
|
||||
} else {
|
||||
composerController.open(composerOpts);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import SelectedPostsCount from 'discourse/mixins/selected-posts-count';
|
||||
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
|
||||
import Topic from 'discourse/models/topic';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
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'),
|
||||
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
|
||||
|
||||
maxTitleLength: setting('max_topic_title_length'),
|
||||
|
||||
_titleChanged: function() {
|
||||
const title = this.get('model.title');
|
||||
if (!Ember.isEmpty(title)) {
|
||||
|
@ -1,14 +1,58 @@
|
||||
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, {
|
||||
showMore: false,
|
||||
local: true,
|
||||
imageUrl: null,
|
||||
imageLink: null,
|
||||
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: {
|
||||
useLocal() { this.setProperties({ local: true, showMore: false}); },
|
||||
useRemote() { this.set("local", false); },
|
||||
toggleShowMore() { this.toggleProperty("showMore"); }
|
||||
upload() {
|
||||
if (this.get('local')) {
|
||||
$('#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';
|
||||
|
||||
export default {
|
||||
@ -8,7 +7,6 @@ export default {
|
||||
const siteSettings = container.lookup('site-settings:main');
|
||||
|
||||
if (siteSettings.enable_emoji) {
|
||||
|
||||
onToolbarCreate(toolbar => {
|
||||
toolbar.addButton({
|
||||
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
|
||||
Discourse.Emoji.applyCustomEmojis();
|
||||
}
|
||||
|
@ -18,6 +18,6 @@ export default {
|
||||
const style = 'max-width:' + width + '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.
|
||||
|
||||
@ -152,58 +150,6 @@ Discourse.Markdown = {
|
||||
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
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
export default (name, opts) => {
|
||||
export default function(name, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
if (opts.__type) {
|
||||
|
@ -215,10 +215,6 @@ Discourse.Utilities = {
|
||||
}
|
||||
},
|
||||
|
||||
getUploadPlaceholder: function() {
|
||||
return "[" + I18n.t("uploading") + "]() ";
|
||||
},
|
||||
|
||||
isAnImage: function(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'),
|
||||
|
||||
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
|
||||
cantSubmitPost: function() {
|
||||
@ -311,8 +306,6 @@ const Composer = RestModel.extend({
|
||||
}.property('reply'),
|
||||
|
||||
_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'));
|
||||
}.on('init'),
|
||||
|
||||
@ -364,11 +357,6 @@ const Composer = RestModel.extend({
|
||||
return before.length + text.length;
|
||||
},
|
||||
|
||||
togglePreview() {
|
||||
this.toggleProperty('showPreview');
|
||||
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
||||
},
|
||||
|
||||
applyTopicTemplate(oldCategoryId, categoryId) {
|
||||
if (this.get('action') !== CREATE_TOPIC) { return; }
|
||||
let reply = this.get('reply');
|
||||
@ -680,7 +668,7 @@ const Composer = RestModel.extend({
|
||||
},
|
||||
|
||||
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() {
|
||||
|
@ -6,24 +6,29 @@ export default {
|
||||
initialize(container, application) {
|
||||
const $html = $('html'),
|
||||
touch = $html.hasClass('touch') || (Modernizr.prefixed("MaxTouchPoints", navigator) > 1),
|
||||
caps = Ember.Object.create();
|
||||
caps = {touch};
|
||||
|
||||
// Store the touch ability in our capabilities object
|
||||
caps.set('touch', touch);
|
||||
$html.addClass(touch ? 'discourse-touch' : 'discourse-no-touch');
|
||||
|
||||
// Detect Devices
|
||||
if (navigator) {
|
||||
const ua = navigator.userAgent;
|
||||
if (ua) {
|
||||
caps.set('android', ua.indexOf('Android') !== -1);
|
||||
caps.set('winphone', ua.indexOf('Windows Phone') !== -1);
|
||||
caps.isAndroid = ua.indexOf('Android') !== -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
|
||||
// iPads should report as 1024.
|
||||
caps.set('highRes', window.screen.width >= 1280);
|
||||
caps.highRes = window.screen.width >= 1280;
|
||||
|
||||
// Inject it
|
||||
application.register('capabilities:main', caps, { instantiate: false });
|
||||
|
@ -89,13 +89,11 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||
},
|
||||
|
||||
showNotActivated(props) {
|
||||
const controller = showModal('not-activated', {title: 'log_in' });
|
||||
controller.setProperties(props);
|
||||
showModal('not-activated', {title: 'log_in' }).setProperties(props);
|
||||
},
|
||||
|
||||
showUploadSelector(composerView) {
|
||||
showModal('uploadSelector');
|
||||
this.controllerFor('upload-selector').setProperties({ composerView: composerView });
|
||||
showUploadSelector(toolbarEvent) {
|
||||
showModal('uploadSelector').setProperties({ toolbarEvent, imageUrl: null, imageLink: null });
|
||||
},
|
||||
|
||||
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}}
|
||||
{{/each}}
|
||||
</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">
|
||||
<div class="d-editor-preview">
|
||||
{{{preview}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,15 +56,12 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="title-input">
|
||||
{{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>
|
||||
{{composer-title composer=model lastValidatedAt=lastValidatedAt}}
|
||||
|
||||
{{#if model.showCategoryChooser}}
|
||||
<div class="category-input">
|
||||
{{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>
|
||||
{{#if model.archetype.hasOptions}}
|
||||
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button>
|
||||
@ -77,35 +74,15 @@
|
||||
{{plugin-outlet "composer-fields"}}
|
||||
</div>
|
||||
|
||||
<div class='wmd-controls'>
|
||||
<div class='textarea-wrapper'>
|
||||
<div class='wmd-button-bar'></div>
|
||||
<div class='wmd-preview-scroller'></div>
|
||||
{{conditional-loading-spinner condition=model.loading}}
|
||||
{{composer-text-area tabindex="4" value=model.reply}}
|
||||
{{popup-input-tip validation=view.replyValidation shownAt=view.showReplyTip}}
|
||||
</div>
|
||||
<!-- keep the classes here in sync with post.hbs -->
|
||||
<div class='preview-wrapper regular'>
|
||||
<div 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>
|
||||
{{composer-editor topic=topic
|
||||
composer=model
|
||||
lastValidatedAt=lastValidatedAt
|
||||
canWhisper=canWhisper
|
||||
draftStatus=model.draftStatus
|
||||
isUploading=isUploading
|
||||
importQuote="importQuote"
|
||||
showOptions="showOptions"
|
||||
showUploadSelector="showUploadSelector"}}
|
||||
|
||||
{{#if currentUser}}
|
||||
<div class='submit-panel'>
|
||||
|
@ -5,7 +5,7 @@
|
||||
{{#if local}}
|
||||
<div class="inputs">
|
||||
<input type="file" id="filename-input" multiple><br>
|
||||
<span class="description">{{unbound view.tip}}</span>
|
||||
<span class="description">{{tip}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -14,31 +14,34 @@
|
||||
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
||||
{{#if remote}}
|
||||
<div class="inputs">
|
||||
<input type="text" id="fileurl-input" placeholder="http://example.com/image.png"><br>
|
||||
<span class="description">{{unbound view.tip}}</span>
|
||||
{{input value=imageUrl placeholder="http://example.com/image.png"}}
|
||||
<span class="description">{{tip}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if showMore}}
|
||||
<div class="radios">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="radios">
|
||||
<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 class="modal-footer">
|
||||
<button class="btn btn-primary" {{action "upload" target="view"}}>
|
||||
<span class='add-upload'><i {{bind-attr class=":fa view.uploadIcon"}}></i></span>
|
||||
{{i18n 'upload'}}
|
||||
</button>
|
||||
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
{{d-button action="upload" class='btn-primary' icon=uploadIcon label='upload'}}
|
||||
<a href {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
{{#if remote}}<a {{action "toggleShowMore"}} class="pull-right">{{i18n 'show_more'}}</a>{{/if}}
|
||||
</div>
|
@ -15,9 +15,9 @@
|
||||
{{#if editingTopic}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<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}}
|
||||
{{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>
|
||||
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
@ -1,57 +1,25 @@
|
||||
import userSearch from 'discourse/lib/user-search';
|
||||
import afterTransition from 'discourse/lib/after-transition';
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
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 { 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, {
|
||||
_lastKeyTimeout: null,
|
||||
templateName: 'composer',
|
||||
elementId: 'reply-control',
|
||||
classNameBindings: ['model.creatingPrivateMessage:private-message',
|
||||
classNameBindings: ['composer.creatingPrivateMessage:private-message',
|
||||
'composeState',
|
||||
'model.loading',
|
||||
'model.canEditTitle:edit-title',
|
||||
'postMade',
|
||||
'model.creatingTopic:topic',
|
||||
'model.showPreview',
|
||||
'model.hidePreview'],
|
||||
'composer.loading',
|
||||
'composer.canEditTitle:edit-title',
|
||||
'composer.createdPost:created-post',
|
||||
'composer.creatingTopic:topic'],
|
||||
|
||||
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
|
||||
content: Em.computed.alias('model'),
|
||||
|
||||
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'),
|
||||
@computed('composer.composeState')
|
||||
composeState(composeState) {
|
||||
return composeState || Composer.CLOSED;
|
||||
},
|
||||
|
||||
movePanels(sizePx) {
|
||||
$('#main-outlet').css('padding-bottom', sizePx);
|
||||
@ -60,44 +28,41 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||
this.appEvents.trigger("composer:resized");
|
||||
},
|
||||
|
||||
resize: function() {
|
||||
@observes('composeState', 'composer.action')
|
||||
resize() {
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
let h = $('#reply-control').height() || 0;
|
||||
const h = $('#reply-control').height() || 0;
|
||||
this.movePanels(h + "px");
|
||||
|
||||
// Figure out the size of the fields
|
||||
const $fields = this.$('.composer-fields');
|
||||
let pos = $fields.position();
|
||||
|
||||
if (pos) {
|
||||
this.$('.wmd-controls').css('top', $fields.height() + pos.top + 5);
|
||||
const fieldPos = $fields.position();
|
||||
if (fieldPos) {
|
||||
this.$('.wmd-controls').css('top', $fields.height() + fieldPos.top + 5);
|
||||
}
|
||||
|
||||
// get the submit panel height
|
||||
pos = this.$('.submit-panel').position();
|
||||
if (pos) {
|
||||
this.$('.wmd-controls').css('bottom', h - pos.top + 7);
|
||||
const submitPos = this.$('.submit-panel').position();
|
||||
if (submitPos) {
|
||||
this.$('.wmd-controls').css('bottom', h - submitPos.top + 7);
|
||||
}
|
||||
|
||||
});
|
||||
}.observes('model.composeState', 'model.action'),
|
||||
},
|
||||
|
||||
keyUp() {
|
||||
const controller = this.get('controller');
|
||||
controller.checkReplyLength();
|
||||
|
||||
this.get('controller.model').typing();
|
||||
this.get('composer').typing();
|
||||
|
||||
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
|
||||
// we recorded it. If it was, the user paused typing.
|
||||
const self = this;
|
||||
|
||||
Ember.run.cancel(this._lastKeyTimeout);
|
||||
this._lastKeyTimeout = Ember.run.later(function() {
|
||||
if (lastKeyUp !== self.get('lastKeyUp')) return;
|
||||
this._lastKeyTimeout = Ember.run.later(() => {
|
||||
if (lastKeyUp !== this._lastKeyUp) { return; }
|
||||
|
||||
// Search for similar topics if the user pauses typing
|
||||
controller.findSimilarTopics();
|
||||
@ -106,7 +71,6 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||
|
||||
keyDown(e) {
|
||||
if (e.which === 27) {
|
||||
// ESC
|
||||
this.get('controller').send('hitEsc');
|
||||
return false;
|
||||
} 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 runResize = () => {
|
||||
Ember.run(() => this.resize());
|
||||
};
|
||||
const resize = () => Ember.run(() => this.resize());
|
||||
|
||||
$replyControl.DivResizer({
|
||||
maxHeight(winHeight) {
|
||||
return winHeight - headerHeight();
|
||||
},
|
||||
resize: runResize,
|
||||
onDrag: (sizePx) => this.movePanels(sizePx)
|
||||
resize,
|
||||
maxHeight: winHeight => winHeight - headerHeight(),
|
||||
onDrag: sizePx => this.movePanels(sizePx)
|
||||
});
|
||||
|
||||
afterTransition($replyControl, runResize);
|
||||
this.set('controller.view', this);
|
||||
|
||||
afterTransition($replyControl, resize);
|
||||
positioningWorkaround(this.$());
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_unlinkView: function() {
|
||||
this.set('controller.view', null);
|
||||
}.on('willDestroyElement'),
|
||||
},
|
||||
|
||||
click() {
|
||||
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);
|
||||
|
||||
export default ComposerView;
|
||||
|
@ -36,9 +36,8 @@ export default Ember.View.extend({
|
||||
// the quote reply widget
|
||||
//
|
||||
// Same hack applied to Android cause it has unreliable touchend
|
||||
const caps = this.capabilities;
|
||||
const android = caps.get('android');
|
||||
if (caps.get('winphone') || android) {
|
||||
const isAndroid = this.capabilities.isAndroid;
|
||||
if (this.capabilities.isWinphone || isAndroid) {
|
||||
onSelectionChanged = _.debounce(onSelectionChanged, 500);
|
||||
}
|
||||
|
||||
@ -72,7 +71,7 @@ export default Ember.View.extend({
|
||||
|
||||
// Android is dodgy, touchend often will not fire
|
||||
// https://code.google.com/p/android/issues/detail?id=19827
|
||||
if (!android) {
|
||||
if (!isAndroid) {
|
||||
$(document)
|
||||
.on('touchstart.quote-button', function(){
|
||||
view.set('isTouchInProgress', true);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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({
|
||||
classNameBindings: ['hidden', ':topic-map'],
|
||||
@ -9,7 +9,7 @@ export default ContainerView.extend({
|
||||
Ember.run.once(this, 'rerender');
|
||||
},
|
||||
|
||||
@property
|
||||
@computed
|
||||
hidden() {
|
||||
if (!this.get('post.firstPost')) return true;
|
||||
|
||||
|
@ -76,7 +76,7 @@ export default Ember.View.extend({
|
||||
_focusWhenOpened: function() {
|
||||
|
||||
// Don't focus on mobile or touch
|
||||
if (Discourse.Mobile.mobileView || this.capabilities.get('touch')) {
|
||||
if (Discourse.Mobile.mobileView || this.capabilities.touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,74 +1,33 @@
|
||||
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({
|
||||
templateName: 'modal/upload_selector',
|
||||
templateName: 'modal/upload-selector',
|
||||
classNames: ['upload-selector'],
|
||||
|
||||
// cf. http://stackoverflow.com/a/9851769/11983
|
||||
isOpera: !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0,
|
||||
isFirefox: typeof InstallTrigger !== 'undefined',
|
||||
isSafari: Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0,
|
||||
isChrome: !!window.chrome && !this.isOpera,
|
||||
@computed()
|
||||
title() {
|
||||
return uploadTranslate("title");
|
||||
},
|
||||
|
||||
title: function() { return uploadTranslate("title"); }.property(),
|
||||
uploadIcon: function() { return Discourse.Utilities.allowsAttachments() ? "fa-upload" : "fa-picture-o"; }.property(),
|
||||
|
||||
touchStart: function(evt) {
|
||||
touchStart(evt) {
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
|
||||
tip: function() {
|
||||
const source = this.get("controller.local") ? "local" : "remote";
|
||||
const authorized_extensions = Discourse.Utilities.authorizesAllExtensions() ? "" : `(${Discourse.Utilities.authorizedExtensions()})`;
|
||||
return uploadTranslate(source + "_tip", { authorized_extensions });
|
||||
}.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() {
|
||||
@on('didInsertElement')
|
||||
@observes('controller.local')
|
||||
selectedChanged() {
|
||||
Ember.run.next(() => {
|
||||
// *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]);
|
||||
// focus the input
|
||||
$('.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
|
||||
|
||||
// Pagedown customizations
|
||||
//= require ./pagedown_custom.js
|
||||
|
||||
// Stuff we need to load first
|
||||
//= require_tree ./ember-addons/utils
|
||||
//= require ./ember-addons/decorator-alias
|
||||
@ -77,6 +74,7 @@
|
||||
//= require ./discourse/lib/emoji/emoji
|
||||
//= require ./discourse/lib/emoji/emoji-groups
|
||||
//= require ./discourse/lib/emoji/emoji-toolbar
|
||||
//= require ./discourse/components/d-editor
|
||||
//= require ./discourse/views/composer
|
||||
//= require ./discourse/lib/show-modal
|
||||
//= 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;
|
||||
}
|
||||
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
width: 98%;
|
||||
height: 200px;
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper .spinner {
|
||||
.d-editor-textarea-wrapper .spinner {
|
||||
z-index: 1000;
|
||||
margin-top: 5em;
|
||||
}
|
||||
@ -99,10 +99,6 @@ div.ac-wrap {
|
||||
}
|
||||
}
|
||||
|
||||
#reply-control.topic #wmd-quote-post {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auto-close-fields {
|
||||
div:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
@ -175,7 +171,7 @@ div.ac-wrap {
|
||||
|
||||
// this removes the topmost margin from the first element in the topic post
|
||||
// if we don't do this, all posts would have extra space at the top
|
||||
.wmd-preview > *:first-child {
|
||||
.d-editor-preview > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.cooked > *:first-child {
|
||||
|
@ -149,7 +149,7 @@ body {
|
||||
background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%));
|
||||
}
|
||||
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
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)
|
||||
.cooked, .wmd-preview {
|
||||
.cooked, .d-editor-preview {
|
||||
word-wrap: break-word;
|
||||
h1, h2, h3, h4, h5, h6 { margin: 30px 0 10px; }
|
||||
h1 { line-height: 1em; } /* normalize.css sets h1 font size but not line height */
|
||||
@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
|
||||
.cooked, .wmd-preview {
|
||||
.cooked, .d-editor-preview {
|
||||
video {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@ -278,14 +278,14 @@
|
||||
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%);
|
||||
}
|
||||
.wmd-input, .wmd-preview {
|
||||
.d-editor-input, .d-editor-preview {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.wmd-preview {
|
||||
.d-editor-preview {
|
||||
border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
overflow: auto;
|
||||
visibility: visible;
|
||||
@ -303,7 +303,7 @@
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
bottom: 35px;
|
||||
}
|
||||
|
||||
@ -351,19 +351,18 @@
|
||||
}
|
||||
|
||||
#reply-control {
|
||||
&.hide-preview {
|
||||
.wmd-controls {
|
||||
.wmd-input {
|
||||
.wmd-controls.hide-preview {
|
||||
.d-editor-input {
|
||||
width: 100%;
|
||||
}
|
||||
.preview-wrapper {
|
||||
.d-editor-preview-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.textarea-wrapper {
|
||||
.d-editor-textarea-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wmd-controls {
|
||||
left: 30px;
|
||||
right: 30px;
|
||||
@ -372,7 +371,7 @@
|
||||
top: 50px;
|
||||
|
||||
|
||||
.wmd-input, .wmd-preview-scroller, .wmd-preview {
|
||||
.d-editor-input, .d-editor-preview {
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@ -383,7 +382,7 @@
|
||||
background-color: $secondary;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.wmd-input, .wmd-preview-scroller {
|
||||
.d-editor-input, .d-editor-preview-header {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@ -391,18 +390,17 @@
|
||||
border-top: 30px solid transparent;
|
||||
@include border-radius-all(0);
|
||||
}
|
||||
.wmd-preview-scroller {
|
||||
.d-editor-preview-header {
|
||||
font-size: 0.929em;
|
||||
line-height: 18px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
overflow: scroll;
|
||||
visibility: hidden;
|
||||
.marker, .caret {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
.textarea-wrapper, .preview-wrapper {
|
||||
.d-editor, .d-editor-container, .d-editor-textarea-wrapper, .d-editor-preview-wrapper {
|
||||
position: relative;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
@ -410,9 +408,9 @@
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 50%;
|
||||
}
|
||||
.textarea-wrapper {
|
||||
.d-editor-textarea-wrapper {
|
||||
width: 50%;
|
||||
padding-right: 5px;
|
||||
float: left;
|
||||
.popup-tip {
|
||||
@ -420,12 +418,13 @@
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
.preview-wrapper {
|
||||
.d-editor-preview-wrapper {
|
||||
width: 50%;
|
||||
padding-left: 5px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.wmd-button-bar {
|
||||
.d-editor-button-bar {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
|
@ -18,7 +18,7 @@
|
||||
width: $topic-body-width;
|
||||
float: left;
|
||||
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
width: 98%;
|
||||
height: 15em;
|
||||
}
|
||||
|
@ -39,10 +39,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bio-composer #wmd-quote-post {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.static {
|
||||
color: $primary;
|
||||
display: inline-block;
|
||||
|
@ -163,13 +163,13 @@ input {
|
||||
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%));
|
||||
}
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%));
|
||||
}
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
bottom: 35px;
|
||||
}
|
||||
.submit-panel {
|
||||
@ -196,7 +196,7 @@ input {
|
||||
width: 240px;
|
||||
right: 5px;
|
||||
}
|
||||
.textarea-wrapper .popup-tip {
|
||||
.d-editor-textarea-wrapper .popup-tip {
|
||||
top: 28px;
|
||||
}
|
||||
button.btn.no-text {
|
||||
@ -221,23 +221,22 @@ input {
|
||||
top: 40px;
|
||||
bottom: 50px;
|
||||
display: block;
|
||||
|
||||
.wmd-input {
|
||||
.d-editor-container {
|
||||
padding: 0;
|
||||
}
|
||||
.d-editor-preview-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.d-editor-input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
height: 180px;
|
||||
padding: 7px;
|
||||
margin: 0;
|
||||
background-color: $secondary;
|
||||
word-wrap: break-word;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.wmd-input {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.textarea-wrapper {
|
||||
.d-editor-textarea-wrapper {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
@ -250,7 +249,7 @@ input {
|
||||
}
|
||||
}
|
||||
}
|
||||
.wmd-button-bar {
|
||||
.d-editor-button-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -63,10 +63,6 @@
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.bio-composer #wmd-quote-post {
|
||||
display: none;
|
||||
}
|
||||
|
||||
textarea {width: 100%;}
|
||||
}
|
||||
|
||||
@ -99,10 +95,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bio-composer #wmd-quote-post {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.static {
|
||||
color: $primary;
|
||||
display: inline-block;
|
||||
|
@ -179,12 +179,12 @@ var runTests = function() {
|
||||
|
||||
$("#create-topic").click();
|
||||
$("#reply-title").val(title).trigger("change");
|
||||
$("#reply-control .wmd-input").val(post).trigger("change");
|
||||
$("#reply-control .wmd-input").focus()[0].setSelectionRange(post.length, post.length);
|
||||
$("#reply-control .d-editor-input").val(post).trigger("change");
|
||||
$("#reply-control .d-editor-input").focus()[0].setSelectionRange(post.length, post.length);
|
||||
});
|
||||
|
||||
exec("open upload modal", function() {
|
||||
$(".wmd-image-button").click();
|
||||
$(".d-editor-button-bar .upload").click();
|
||||
});
|
||||
|
||||
test("upload modal is open", function() {
|
||||
@ -214,16 +214,16 @@ var runTests = 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() {
|
||||
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() {
|
||||
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() {
|
||||
|
@ -10,25 +10,25 @@ test("Tests the Composer controls", () => {
|
||||
|
||||
click('#create-topic');
|
||||
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('.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');
|
||||
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');
|
||||
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');
|
||||
andThen(() => {
|
||||
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");
|
||||
@ -36,10 +36,10 @@ test("Tests the Composer controls", () => {
|
||||
ok(exists('.title-input .popup-tip.good'), 'the title is now good');
|
||||
});
|
||||
|
||||
fillIn('.wmd-input', "this is the *content* of a post");
|
||||
fillIn('.d-editor-input', "this is the *content* of a post");
|
||||
andThen(() => {
|
||||
equal(find('.wmd-preview').html(), "<p>this is the <em>content</em> of a post</p>", "it previews content");
|
||||
ok(exists('.textarea-wrapper .popup-tip.good'), 'the body is now good');
|
||||
equal(find('.d-editor-preview').html().trim(), "<p>this is the <em>content</em> of a post</p>", "it previews content");
|
||||
ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good');
|
||||
});
|
||||
|
||||
click('#reply-control a.cancel');
|
||||
@ -58,7 +58,7 @@ test("Create a topic with server side errors", () => {
|
||||
visit("/");
|
||||
click('#create-topic');
|
||||
fillIn('#reply-title', "this title triggers an error");
|
||||
fillIn('.wmd-input', "this is the *content* of a post");
|
||||
fillIn('.d-editor-input', "this is the *content* of a post");
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
ok(exists('.bootbox.modal'), 'it pops up an error message');
|
||||
@ -66,7 +66,7 @@ test("Create a topic with server side errors", () => {
|
||||
click('.bootbox.modal a.btn-primary');
|
||||
andThen(() => {
|
||||
ok(!exists('.bootbox.modal'), 'it dismisses the error');
|
||||
ok(exists('.wmd-input'), 'the composer input is visible');
|
||||
ok(exists('.d-editor-input'), 'the composer input is visible');
|
||||
});
|
||||
});
|
||||
|
||||
@ -74,7 +74,7 @@ test("Create a Topic", () => {
|
||||
visit("/");
|
||||
click('#create-topic');
|
||||
fillIn('#reply-title', "Internationalization Localization");
|
||||
fillIn('.wmd-input', "this is the *content* of a new topic post");
|
||||
fillIn('.d-editor-input', "this is the *content* of a new topic post");
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
equal(currentURL(), "/t/internationalization-localization/280", "it transitions to the newly created topic URL");
|
||||
@ -85,7 +85,7 @@ test("Create an enqueued Topic", () => {
|
||||
visit("/");
|
||||
click('#create-topic');
|
||||
fillIn('#reply-title', "Internationalization Localization");
|
||||
fillIn('.wmd-input', "enqueue this content please");
|
||||
fillIn('.d-editor-input', "enqueue this content please");
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
ok(visible('#discourse-modal'), 'it pops up a modal');
|
||||
@ -108,11 +108,11 @@ test("Create a Reply", () => {
|
||||
|
||||
click('#topic-footer-buttons .btn.create');
|
||||
andThen(() => {
|
||||
ok(exists('.wmd-input'), 'the composer input is visible');
|
||||
ok(exists('.d-editor-input'), 'the composer input is visible');
|
||||
ok(!exists('#reply-title'), 'there is no title since this is a reply');
|
||||
});
|
||||
|
||||
fillIn('.wmd-input', 'this is the content of my reply');
|
||||
fillIn('.d-editor-input', 'this is the content of my reply');
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
equal(find('.cooked:last p').text(), 'this is the content of my reply');
|
||||
@ -122,7 +122,7 @@ test("Create a Reply", () => {
|
||||
test("Posting on a different topic", (assert) => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
click('#topic-footer-buttons .btn.create');
|
||||
fillIn('.wmd-input', 'this is the content for a different topic');
|
||||
fillIn('.d-editor-input', 'this is the content for a different topic');
|
||||
|
||||
visit("/t/1-3-0beta9-no-rate-limit-popups/28830");
|
||||
andThen(function() {
|
||||
@ -145,11 +145,11 @@ test("Create an enqueued Reply", () => {
|
||||
|
||||
click('#topic-footer-buttons .btn.create');
|
||||
andThen(() => {
|
||||
ok(exists('.wmd-input'), 'the composer input is visible');
|
||||
ok(exists('.d-editor-input'), 'the composer input is visible');
|
||||
ok(!exists('#reply-title'), 'there is no title since this is a reply');
|
||||
});
|
||||
|
||||
fillIn('.wmd-input', 'enqueue this content please');
|
||||
fillIn('.d-editor-input', 'enqueue this content please');
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
ok(find('.cooked:last p').text() !== 'enqueue this content please', "it doesn't insert the post");
|
||||
@ -173,14 +173,14 @@ test("Edit the first post", () => {
|
||||
click('.topic-post:eq(0) button[data-action=showMoreActions]');
|
||||
click('.topic-post:eq(0) button[data-action=edit]');
|
||||
andThen(() => {
|
||||
equal(find('.wmd-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text');
|
||||
equal(find('.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");
|
||||
click('#reply-control button.create');
|
||||
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(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');
|
||||
@ -192,11 +192,11 @@ test("Composer can switch between edits", () => {
|
||||
|
||||
click('.topic-post:eq(0) button[data-action=edit]');
|
||||
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]');
|
||||
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");
|
||||
|
||||
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]');
|
||||
andThen(() => {
|
||||
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
|
||||
});
|
||||
click('.modal-footer a:eq(0)');
|
||||
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]');
|
||||
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]');
|
||||
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]');
|
||||
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");
|
||||
|
||||
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]');
|
||||
andThen(() => {
|
||||
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
|
||||
});
|
||||
click('.modal-footer a:eq(0)');
|
||||
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");
|
||||
|
||||
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('.topic-post:eq(0) button[data-action=edit]');
|
||||
andThen(() => {
|
||||
@ -259,6 +259,6 @@ test("Composer draft with dirty reply can toggle to edit", () => {
|
||||
});
|
||||
click('.modal-footer a:eq(0)');
|
||||
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) {
|
||||
assert.ok(this.$('.d-editor-button-bar').length);
|
||||
assert.equal(this.$('.d-editor-preview.hidden').length, 1);
|
||||
|
||||
fillIn('.d-editor-input', 'hello **world**');
|
||||
|
||||
andThen(() => {
|
||||
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>');
|
||||
});
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { blank } 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 createStore from 'helpers/create-store';
|
||||
|
||||
module("model:composer");
|
||||
|
||||
const keyValueStore = new KeyValueStore("_test_composer");
|
||||
|
||||
function createComposer(opts) {
|
||||
opts = opts || {};
|
||||
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.");
|
||||
});
|
||||
|
||||
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() {
|
||||
const quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]';
|
||||
const newComposer = function() {
|
||||
|
@ -21,9 +21,6 @@
|
||||
//= require ../../app/assets/javascripts/locales/i18n
|
||||
//= require ../../app/assets/javascripts/locales/en
|
||||
|
||||
// Pagedown customizations
|
||||
//= require ../../app/assets/javascripts/pagedown_custom.js
|
||||
|
||||
//= require vendor
|
||||
|
||||
//= require htmlparser.js
|
||||
|
Loading…
Reference in New Issue
Block a user