Version bump

This commit is contained in:
Neil Lalonde 2015-11-17 11:40:04 -05:00
commit 50da33eee1
242 changed files with 2891 additions and 4467 deletions

View File

@ -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/*"

View File

@ -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

View File

@ -120,6 +120,7 @@ group :test, :development do
gem 'simplecov', require: false
gem 'timecop'
gem 'rspec-given'
gem 'rspec-html-matchers'
gem 'pry-nav'
gem 'spork-rails'
gem 'byebug', require: ENV['RM_INFO'].nil?

View File

@ -299,7 +299,7 @@ GEM
loofah (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
rails_multisite (1.0.2)
rails_multisite (1.0.3)
railties (4.2.4)
actionpack (= 4.2.4)
activesupport (= 4.2.4)
@ -337,6 +337,9 @@ GEM
rspec-given (3.5.4)
given_core (= 3.5.4)
rspec (>= 2.12)
rspec-html-matchers (0.7.0)
nokogiri (~> 1)
rspec (~> 3)
rspec-logsplit (0.1.3)
rspec-mocks (3.2.1)
diff-lcs (>= 1.2.0, < 2.0)
@ -511,6 +514,7 @@ DEPENDENCIES
rmmseg-cpp
rspec (~> 3.2.0)
rspec-given
rspec-html-matchers
rspec-rails
rtlit
ruby-readability

View File

@ -67,6 +67,22 @@ export default Ember.Controller.extend({
});
},
removeOwner(member) {
const self = this,
message = I18n.t("admin.groups.delete_owner_confirm", { username: member.get("username"), group: this.get("model.name") });
return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) {
if (confirm) {
self.get("model").removeOwner(member);
}
});
},
addOwners() {
if (Em.isEmpty(this.get("model.ownerUsernames"))) { return; }
this.get("model").addOwners(this.get("model.ownerUsernames")).catch(popupAjaxError);
this.set("model.ownerUsernames", null);
},
addMembers() {
if (Em.isEmpty(this.get("model.usernames"))) { return; }
this.get("model").addMembers(this.get("model.usernames")).catch(popupAjaxError);

View File

@ -53,6 +53,10 @@
{{embedding-setting field="embed_blacklist_selector"
value=embedding.embed_blacklist_selector
placeholder=".ad-unit, header"}}
{{embedding-setting field="embed_classname_whitelist"
value=embedding.embed_classname_whitelist
placeholder="emoji, classname"}}
</div>
<div class='embedding-secondary'>

View File

@ -10,6 +10,23 @@
</div>
{{#if model.id}}
{{#unless model.automatic}}
{{#if model.hasOwners}}
<div>
<label for='owner-list'>{{i18n 'admin.groups.group_owners'}}</label>
<div class="ac-wrap clearfix" id='owner-list'>
{{#each model.owners as |member|}}
{{group-member member=member removeAction="removeOwner"}}
{{/each}}
</div>
</div>
{{/if}}
<div>
<label for="owner-selector">{{i18n 'admin.groups.add_owners'}}</label>
{{user-selector usernames=model.ownerUsernames placeholderKey="admin.groups.selector_placeholder" id="owner-selector"}}
{{d-button action="addOwners" class="add" icon="plus" label="admin.groups.add"}}
</div>
{{/unless}}
<div>
<label>{{i18n 'admin.groups.group_members'}} ({{model.user_count}})</label>
<div>

View File

@ -7,5 +7,5 @@
{{/if}}
<button title="{{i18n 'admin.flags.agree_flag_title'}}" {{action "agreeFlagKeepPost"}} class="btn"><i class="fa fa-thumbs-o-up"></i>{{i18n 'admin.flags.agree_flag'}}</button>
{{#if model.canDeleteAsSpammer}}
<button title="{{i18n 'admin.flags.delete_spammer_title'}}" {{action "deleteSpammer" user}} class="btn btn-danger"><i class="fa fa-exclamation-triangle"></i>{{i18n 'admin.flags.delete_spammer'}}</button>
<button title="{{i18n 'admin.flags.delete_spammer_title'}}" {{action "deleteSpammer" model.user}} class="btn btn-danger"><i class="fa fa-exclamation-triangle"></i>{{i18n 'admin.flags.delete_spammer'}}</button>
{{/if}}

View File

@ -1,5 +1,5 @@
<button title="{{i18n 'admin.flags.delete_post_defer_flag_title'}}" {{action "deletePostDeferFlag"}} class="btn"><i class="fa fa-trash-o"></i><i class="fa fa-external-link"></i>{{i18n 'admin.flags.delete_post_defer_flag'}}</button>
<button title="{{i18n 'admin.flags.delete_post_agree_flag_title'}}" {{action "deletePostAgreeFlag"}} class="btn"><i class="fa fa-trash-o"></i><i class="fa fa-thumbs-o-up"></i>{{i18n 'admin.flags.delete_post_agree_flag'}}</button>
{{#if model.canDeleteAsSpammer}}
<button title="{{i18n 'admin.flags.delete_spammer_title'}}" {{action "deleteSpammer" user}} class="btn btn-danger"><i class="fa fa-exclamation-triangle"></i>{{i18n 'admin.flags.delete_spammer'}}</button>
<button title="{{i18n 'admin.flags.delete_spammer_title'}}" {{action "deleteSpammer" model.user}} class="btn btn-danger"><i class="fa fa-exclamation-triangle"></i>{{i18n 'admin.flags.delete_spammer'}}</button>
{{/if}}

View File

@ -16,13 +16,10 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
// if it's a non relative URL, return it.
if (url !== '/' && !/^\/[^\/]/.test(url)) return url;
var u = Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri;
if (url.indexOf(Discourse.BaseUri) !== -1) return url;
if (url[0] !== "/") url = "/" + url;
if (u[u.length-1] === '/') u = u.substring(0, u.length-1);
if (url.indexOf(u) !== -1) return url;
if (u.length > 0 && url[0] !== "/") url = "/" + url;
return u + url;
return Discourse.BaseUri + url;
},
getURLWithCDN: function(url) {

View File

@ -72,7 +72,8 @@ export default Ember.Component.extend({
}
const $elem = this.$();
$elem.select2({formatResult: this.comboTemplate, minimumResultsForSearch: 5, width: 'resolve'});
const minimumResultsForSearch = this.capabilities.touch ? -1 : 5;
$elem.select2({formatResult: this.comboTemplate, minimumResultsForSearch, width: 'resolve'});
const castInteger = this.get('castInteger');
const self = this;

View File

@ -0,0 +1,379 @@
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(", @")
});
$input.on('scroll', () => Ember.run.throttle(this, this._syncEditorAndPreviewScroll, 20));
// Focus on the body unless we have a title
if (!this.get('composer.canEditTitle') && !this.capabilities.touch) {
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 });
}
},
_syncEditorAndPreviewScroll() {
const $input = this.$('.d-editor-input');
const $preview = this.$('.d-editor-preview');
if ($input.scrollTop() === 0) {
$preview.scrollTop(0);
return;
}
const inputHeight = $input[0].scrollHeight;
const previewHeight = $preview[0].scrollHeight;
if (($input.height() + $input.scrollTop() + 100) > inputHeight) {
// cheat, special case for bottom
$preview.scrollTop(previewHeight);
return;
}
const scrollPosition = $input.scrollTop();
const factor = previewHeight / inputHeight;
const desired = scrollPosition * factor;
$preview.scrollTop(desired + 50);
},
_renderUnseen: function($preview, unseen) {
fetchUnseenMentions($preview, unseen, this.siteSettings).then(() => {
linkSeenMentions($preview, this.siteSettings);
});
},
_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));
this.trigger('previewRefreshed', $preview);
},
}
});

View File

@ -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')
});

View File

@ -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')
_focusOnTitle() {
if (!this.capabilities.touch) {
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 });
}
}
});

View File

@ -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, observes } 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,26 +111,42 @@ 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);
const mod = mac ? 'Meta' : 'Ctrl';
createdButton.title = `${title} (${mod}+${button.shortcut})`;
var shortcutTitle = `${mod}+${button.shortcut}`;
// Mac users are used to glyphs for shortcut keys
if (mac) {
createdButton.title = createdButton.title.replace('Shift', "\u21E7")
.replace('Meta', "\u2318")
.replace('Alt', "\u2325")
.replace(/\+/g, '');
shortcutTitle = shortcutTitle
.replace('Shift', "\u21E7")
.replace('Meta', "\u2318")
.replace('Alt', "\u2325")
.replace(/\+/g, '');
} else {
shortcutTitle = shortcutTitle
.replace('Shift', I18n.t('shortcut_modifier_key.shift'))
.replace('Ctrl', I18n.t('shortcut_modifier_key.ctrl'))
.replace('Alt', I18n.t('shortcut_modifier_key.alt'));
}
createdButton.title = `${title} (${shortcutTitle})`;
this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton;
} else {
createdButton.title = title;
}
g.buttons.push(createdButton);
if (button.unshift) {
g.buttons.unshift(createdButton);
} else {
g.buttons.push(createdButton);
}
};
export function onToolbarCreate(func) {
@ -144,9 +160,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');
@ -154,30 +177,58 @@ export default Ember.Component.extend({
const button = shortcuts[sc];
Mousetrap(this.$('.d-editor-input')[0]).bind(sc, () => {
this.send(button.action, button);
return false;
});
});
// 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')
preview(ready, value) {
if (!ready) { return; }
_updatePreview() {
const value = this.get('value');
const markdownOptions = this.get('markdownOptions') || {};
markdownOptions.sanitize = true;
const text = Discourse.Dialect.cook(value || "", {sanitize: true});
return text ? text : "";
this.set('preview', 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);
});
},
@observes('ready', 'value')
_watchForChanges() {
if (!this.get('ready')) { return; }
Ember.run.debounce(this, this._updatePreview, 30);
},
_applyEmojiAutocomplete() {
@ -198,7 +249,7 @@ export default Ember.Component.extend({
showSelector({
appendTo: self.$(),
container,
onSelect: title => self._addText(this._getSelected(), `${title}:`)
onSelect: title => self._addText(self._getSelected(), `${title}:`)
});
return "";
}
@ -236,12 +287,8 @@ export default Ember.Component.extend({
if (!this.get('ready')) { return; }
const textarea = this.$('textarea.d-editor-input')[0];
let start = textarea.selectionStart;
let end = textarea.selectionEnd;
if (start === end) {
start = end = textarea.value.length;
}
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value.substring(start, end);
const pre = textarea.value.slice(0, start);
@ -253,7 +300,6 @@ export default Ember.Component.extend({
_selectText(from, length) {
Ember.run.scheduleOnce('afterRender', () => {
const textarea = this.$('textarea.d-editor-input')[0];
textarea.focus();
textarea.selectionStart = from;
textarea.selectionEnd = textarea.selectionStart + length;
});
@ -334,17 +380,24 @@ export default Ember.Component.extend({
const insert = `${sel.pre}${text}`;
this.set('value', `${insert}${sel.post}`);
this._selectText(insert.length, 0);
Ember.run.once("afterRender", () => { $("textarea.d-editor-input").focus(); } );
},
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() {
@ -362,7 +415,8 @@ export default Ember.Component.extend({
const remaining = link.replace(m[0], '');
this._addText(this._lastSel, `[${description}](${remaining})`);
} else {
this._addText(this._lastSel, `[${link}](${link})`);
const selectedValue = this._lastSel.value || link;
this._addText(this._lastSel, `[${selectedValue}](${link})`);
}
this.set('link', '');

View File

@ -21,7 +21,7 @@ export default Ember.Component.extend({
params.push(model);
}
return router.router.generate.apply(router.router, params);
return Discourse.getURL(router.router.generate.apply(router.router, params));
}
}

View File

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

View File

@ -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();

View File

@ -2,7 +2,7 @@ import { default as computed, on, observes } from 'ember-addons/ember-computed-d
import { headerHeight } from 'discourse/views/header';
const PANEL_BODY_MARGIN = 30;
const mutationSupport = !!window['MutationObserver'];
const mutationSupport = !Ember.testing && !!window['MutationObserver'];
export default Ember.Component.extend({
classNameBindings: [':menu-panel', 'visible::hidden', 'viewMode'],
@ -24,13 +24,19 @@ export default Ember.Component.extend({
const $panelBody = this.$('.panel-body');
let contentHeight = parseInt(this.$('.panel-body-contents').height());
// We use a mutationObserver to check for style changes, so it's important
// we don't set it if it doesn't change. Same goes for the $panelBody!
const style = this.$().prop('style');
if (viewMode === 'drop-down') {
const $buttonPanel = $('header ul.icons');
if ($buttonPanel.length === 0) { return; }
// These values need to be set here, not in the css file - this is to deal with the
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
this.$().css({ top: '100%', height: 'auto' });
if (style.top !== '100%' || style.height !== 'auto') {
this.$().css({ top: '100%', height: 'auto' });
}
// adjust panel height
const fullHeight = parseInt($window.height());
@ -40,7 +46,9 @@ export default Ember.Component.extend({
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
}
$panelBody.height(contentHeight);
if ($panelBody.height() !== contentHeight) {
$panelBody.height(contentHeight);
}
$('body').addClass('drop-down-visible');
} else {
const menuTop = headerHeight();
@ -53,8 +61,12 @@ export default Ember.Component.extend({
height = winHeight - menuTop;
}
$panelBody.height('100%');
this.$().css({ top: menuTop + "px", height });
if ($panelBody.prop('style').height !== '100%') {
$panelBody.height('100%');
}
if (style.top !== menuTop + "px" || style.height !== height) {
this.$().css({ top: menuTop + "px", height });
}
$('body').removeClass('drop-down-visible');
}
@ -127,7 +139,7 @@ export default Ember.Component.extend({
_watchSizeChanges() {
if (mutationSupport) {
this._observer.disconnect();
this._observer.observe(this.element, { childList: true, subtree: true });
this._observer.observe(this.element, { childList: true, subtree: true, characterData: true, attributes: true });
} else {
clearInterval(this._resizeInterval);
this._resizeInterval = setInterval(() => {
@ -176,7 +188,7 @@ export default Ember.Component.extend({
if (mutationSupport) {
this._observer = new MutationObserver(() => {
Ember.run(() => this.performLayout());
Ember.run.debounce(this, this.performLayout, 50);
});
}

View File

@ -1,24 +1,30 @@
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,
rerenderTriggers: ['validation.reason'],
click() {
this.set('shownAt', false);
this.set('shownAt', null);
this.set('validation.lastShownAt', null);
},
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';
@ -35,8 +41,7 @@ export default Ember.Component.extend(StringBuffer, {
const reason = this.get('validation.reason');
if (!reason) { return; }
buffer.push("<span class='close'>" + iconHTML('times-circle') + "</span>");
buffer.push(reason);
buffer.push(`<span class='close'>${iconHTML('times-circle')}</span>${reason}`);
},
bounceLeft($elem) {

View File

@ -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 = [];

View File

@ -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() {

View File

@ -122,12 +122,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.fetchUserDetails();
}.observes('model.username'),
fetchUserDetails: function() {
if( Discourse.User.currentProp('staff') && this.get('model.username') ) {
const flagController = this;
Discourse.AdminUser.find(this.get('model.username').toLowerCase()).then(function(user){
flagController.set('userDetails', user);
});
fetchUserDetails() {
if (Discourse.User.currentProp('staff') && this.get('model.username')) {
Discourse.AdminUser.find(this.get('model.username').toLowerCase())
.then(user => this.set('userDetails', user));
}
}

View File

@ -111,7 +111,6 @@ export default Ember.Controller.extend({
@computed('q')
showLikeCount(q) {
console.log(q);
return q && q.indexOf("order:likes") > -1;
},

View File

@ -1,9 +1,33 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend({
loading: false,
limit: null,
offset: null,
isOwner: function() {
if (this.get('currentUser.admin')) {
return true;
}
const owners = this.get('model.owners');
const currentUserId = this.get('currentUser.id');
if (currentUserId) {
return !!owners.findBy('id', currentUserId);
}
}.property('model.owners.@each'),
actions: {
removeMember(user) {
this.get('model').removeMember(user);
},
addMembers() {
const usernames = this.get('usernames');
if (usernames && usernames.length > 0) {
this.get('model').addMembers(usernames).then(() => this.set('usernames', [])).catch(popupAjaxError);
}
},
loadMore() {
if (this.get("loading")) { return; }
// we've reached the end

View File

@ -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);
}

View File

@ -7,7 +7,7 @@ export default Ember.Controller.extend({
toPostIndex: null,
actions: {
toggleExpansion: function(opts) {
toggleExpansion(opts) {
this.toggleProperty('expanded');
if (this.get('expanded')) {
this.set('toPostIndex', this.get('progressPosition'));
@ -20,7 +20,7 @@ export default Ember.Controller.extend({
}
},
jumpPost: function() {
jumpPost() {
var postIndex = parseInt(this.get('toPostIndex'), 10);
// Validate the post index first
@ -52,17 +52,17 @@ export default Ember.Controller.extend({
}
},
jumpTop: function() {
jumpTop() {
this.jumpTo(this.get('model.firstPostUrl'));
},
jumpBottom: function() {
jumpBottom() {
this.jumpTo(this.get('model.lastPostUrl'));
}
},
// Route and close the expansion
jumpTo: function(url) {
jumpTo(url) {
this.set('expanded', false);
DiscourseURL.routeTo(url);
},

View File

@ -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)) {
@ -106,7 +103,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
composerController.get('content.action') === Discourse.Composer.REPLY) {
composerController.set('content.post', post);
composerController.set('content.composeState', Discourse.Composer.OPEN);
composerController.appendText(quotedText);
this.appEvents.trigger('composer:insert-text', quotedText.trim());
} else {
const opts = {
@ -398,9 +395,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}).then(() => {
return Em.isEmpty(quotedText) ? Discourse.Post.loadQuote(post.get('id')) : quotedText;
}).then(q => {
const postUrl = `${location.protocol}//${location.host}${post.get('url')}`,
postLink = `[${Handlebars.escapeExpression(self.get('model.title'))}](${postUrl})`;
composerController.appendText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`);
const postUrl = `${location.protocol}//${location.host}${post.get('url')}`;
const postLink = `[${Handlebars.escapeExpression(self.get('model.title'))}](${postUrl})`;
this.appEvents.trigger('composer:insert-text', `${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`);
});
},
@ -631,20 +629,27 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
}.observes('model.currentPost'),
readPosts(topicId, postNumbers) {
const postStream = this.get('model.postStream');
const topic = this.get("model"),
postStream = topic.get("postStream");
if (postStream.get('topic.id') === topicId){
_.each(postStream.get('posts'), function(post){
// optimise heavy loop
// TODO identity map for postNumber
if(_.include(postNumbers,post.post_number) && !post.read){
if (topic.get("id") === topicId) {
// TODO identity map for postNumber
_.each(postStream.get('posts'), post => {
if (_.include(postNumbers, post.post_number) && !post.read) {
post.set("read", true);
}
});
const max = _.max(postNumbers);
if(max > this.get('model.last_read_post_number')){
this.set('model.last_read_post_number', max);
if (max > topic.get("last_read_post_number")) {
topic.set("last_read_post_number", max);
}
if (this.siteSettings.automatically_unpin_topics && this.currentUser) {
// automatically unpin topics when the user reaches the bottom
if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
Em.run.next(() => topic.clearPin());
}
}
}
},

View File

@ -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')) {
$('.wmd-controls').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");
}
}
});

View File

@ -133,7 +133,6 @@ Discourse.Markdown.whiteListTag('span', 'class', /^bbcode-[bius]$/);
Discourse.BBCode.replaceBBCode('ul', function(contents) { return ['ul'].concat(Discourse.BBCode.removeEmptyLines(contents)); });
Discourse.BBCode.replaceBBCode('ol', function(contents) { return ['ol'].concat(Discourse.BBCode.removeEmptyLines(contents)); });
Discourse.BBCode.replaceBBCode('li', function(contents) { return ['li'].concat(Discourse.BBCode.removeEmptyLines(contents)); });
Discourse.BBCode.replaceBBCode('spoiler', function(contents) { return ['span', {'class': 'spoiler'}].concat(contents); });
Discourse.BBCode.rawBBCode('img', function(contents) { return ['img', {href: contents}]; });
Discourse.BBCode.rawBBCode('email', function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; });

View File

@ -7,7 +7,7 @@ Discourse.Dialect.inlineRegexp({
start: '@',
// NOTE: we really should be using SiteSettings here, but it loads later in process
// also, if we do, we must ensure serverside version works as well
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9])/,
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9\_])/,
wordBoundary: true,
emitter: function(matches) {

View File

@ -81,14 +81,7 @@ function findOutlets(collection, callback) {
}
}
const dashedName = outletName.replace(/_/g, '-');
if (dashedName !== outletName) {
Ember.warn("DEPRECATION: You need to use dashes in outlet names, not underscores");
callback(dashedName, res, uniqueName);
} else {
callback(outletName, res, uniqueName);
}
callback(outletName, res, uniqueName);
}
});
}

View File

@ -1,4 +1,3 @@
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
import { onToolbarCreate } from 'discourse/components/d-editor';
export default {
@ -6,32 +5,20 @@ export default {
initialize(container) {
const siteSettings = container.lookup('site-settings:main');
if (siteSettings.enable_emoji) {
if (siteSettings.enable_emoji) {
onToolbarCreate(toolbar => {
toolbar.addButton({
id: 'emoji',
group: 'extras',
icon: 'smile-o',
action: 'emoji',
shortcut: 'Alt+E',
title: 'composer.emoji'
});
});
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();
}
}
};

View File

@ -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');
}
};

View File

@ -1,19 +0,0 @@
import loadScript from 'discourse/lib/load-script';
export default {
name: 'lab-deprecation',
initialize() {
if (window.$LAB) { return; }
window.$LAB = {
script(path) {
Ember.warn('$LAB is not included with Discouse anymore. Use `loadScript` instead.');
const promise = loadScript(path);
promise.wait = promise.then;
return promise;
}
};
}
};

View File

@ -11,14 +11,6 @@ export default {
user = container.lookup('current-user:main'),
siteSettings = container.lookup('site-settings:main');
const deprecatedBus = {};
deprecatedBus.prototype = messageBus;
deprecatedBus.subscribe = function() {
Ember.warn("Discourse.MessageBus is deprecated. Use `this.messageBus` instead");
messageBus.subscribe.apply(messageBus, Array.prototype.slice(arguments));
};
Discourse.MessageBus = deprecatedBus;
messageBus.alwaysLongPoll = Discourse.Environment === "development";
messageBus.start();

View File

@ -4,14 +4,6 @@ export default {
name: 'sharing-sources',
initialize: function() {
// Backwards compatibility
Discourse.ShareLink = {};
Discourse.ShareLink.addTarget = function(id, source) {
Ember.warn('Discourse.ShareLink.addTarget is deprecated. Import `Sharing` and call `addSource` instead.');
source.id = id;
Sharing.addSource(source);
};
Sharing.addSource({
id: 'twitter',
faIcon: 'fa-twitter-square',

File diff suppressed because it is too large Load Diff

View File

@ -62,11 +62,9 @@ const DiscourseLocation = Ember.Object.extend({
*/
getURL() {
const location = get(this, 'location');
let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri);
let url = location.pathname;
rootURL = rootURL.replace(/\/$/, '');
url = url.replace(rootURL, '');
url = url.replace(Discourse.BaseUri, '');
const search = location.search || '';
url += search;

View File

@ -39,7 +39,7 @@ function initializeUngroupedIcons() {
}
function trackEmojiUsage(title) {
const recent = keyValueStore.getObject(EMOJI_USAGE);
const recent = keyValueStore.getObject(EMOJI_USAGE) || {};
if (!recent[title]) { recent[title] = { title: title, usage: 0 }; }
recent[title]["usage"]++;

View File

@ -13,10 +13,20 @@ Discourse.Dialect.registerEmoji = function(code, url) {
};
// This method is used by PrettyText to reset custom emojis in multisites
Discourse.Dialect.resetEmoji = function() {
Discourse.Dialect.resetEmojis = function() {
extendedEmoji = {};
};
var customEmojiCallbacks = [];
Discourse.Emoji.addCustomEmojis = function(cb) {
customEmojiCallbacks.push(cb);
};
Discourse.Emoji.applyCustomEmojis = function() {
var self = this;
_.each(customEmojiCallbacks, function(cb) { cb.apply(self); });
};
Discourse.Emoji.list = function(){
var list = emoji.slice(0);
_.each(extendedEmoji, function(v,k){ list.push(k); });

View File

@ -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
@ -304,8 +250,6 @@ Discourse.Markdown.whiteListTag('div', 'class', 'title');
Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls');
Discourse.Markdown.whiteListTag('span', 'class', 'mention');
Discourse.Markdown.whiteListTag('span', 'class', 'spoiler');
Discourse.Markdown.whiteListTag('div', 'class', 'spoiler');
Discourse.Markdown.whiteListTag('aside', 'class', 'quote');
Discourse.Markdown.whiteListTag('aside', 'data-*');

View File

@ -1,5 +1,6 @@
let _decorateId = 0;
import ComposerEditor from 'discourse/components/composer-editor';
let _decorateId = 0;
function decorate(klass, evt, cb) {
const mixin = {};
mixin["_decorate_" + (_decorateId++)] = function($elem) { cb($elem); }.on(evt);
@ -10,7 +11,7 @@ export function decorateCooked(container, cb) {
const postView = container.lookupFactory('view:post');
decorate(postView, 'postViewInserted', cb);
decorate(postView, 'postViewUpdated', cb);
decorate(container.lookupFactory('view:composer'), 'previewRefreshed', cb);
decorate(ComposerEditor, 'previewRefreshed', cb);
decorate(container.lookupFactory('view:embedded-post'), 'didInsertElement', cb);
decorate(container.lookupFactory('view:user-stream'), 'didInsertElement', cb);
}

View File

@ -1,11 +1,5 @@
export default (name, opts) => {
export default function(name, opts) {
opts = opts || {};
if (opts.__type) {
Ember.warn("showModal now takes `opts` as a second param instead of a model");
opts = {model: opts};
}
const container = Discourse.__container__;
// We use the container here because modals are like singletons

View File

@ -6,7 +6,7 @@ const configs = {
"privacy": "privacy_policy_url"
};
export default (page) => {
export default function(page) {
return Discourse.Route.extend({
renderTemplate() {
this.render("static");

View File

@ -122,7 +122,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
// Scroll to the same page, different anchor
if (path.indexOf('#') === 0) {
this.scrollToId(path);
history.replaceState(undefined, undefined, path);
this.replaceState(path);
return;
}

View File

@ -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);
},

View File

@ -35,11 +35,8 @@ Discourse.Ajax = Em.Mixin.create({
args = arguments[1];
}
if (args.success) {
throw "Discourse.ajax should use promises, received 'success' callback";
}
if (args.error) {
throw "DEPRECATION: Discourse.ajax should use promises, received 'error' callback";
if (args.success || args.error) {
throw "Discourse.ajax should use promises";
}
var performAjax = function(resolve, reject) {

View File

@ -19,7 +19,7 @@ const ScrollingDOMMethods = {
},
screenNotFull() {
return $(window).height() > $(document).height();
return $(window).height() > $("#main").height();
}
};

View File

@ -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() {

View File

@ -1,12 +1,17 @@
import computed from 'ember-addons/ember-computed-decorators';
const Group = Discourse.Model.extend({
limit: 50,
offset: 0,
user_count: 0,
owners: [],
emailDomains: function() {
var value = this.get("automatic_membership_email_domains");
hasOwners: Ember.computed.notEmpty('owners'),
@computed("automatic_membership_email_domains")
emailDomains(value) {
return Em.isEmpty(value) ? "" : value;
}.property("automatic_membership_email_domains"),
},
type: function() {
return this.get("automatic") ? "automatic" : "custom";
@ -24,18 +29,38 @@ const Group = Discourse.Model.extend({
const self = this, offset = Math.min(this.get("user_count"), Math.max(this.get("offset"), 0));
return Discourse.Group.loadMembers(this.get("name"), offset, this.get("limit")).then(function (result) {
var ownerIds = {};
result.owners.forEach(owner => ownerIds[owner.id] = true);
self.setProperties({
user_count: result.meta.total,
limit: result.meta.limit,
offset: result.meta.offset,
members: result.members.map(member => Discourse.User.create(member))
members: result.members.map(member => {
if (ownerIds[member.id]) {
member.owner = true;
}
return Discourse.User.create(member);
}),
owners: result.owners.map(owner => Discourse.User.create(owner)),
});
});
},
removeOwner(member) {
var self = this;
return Discourse.ajax('/admin/groups/' + this.get('id') + '/owners.json', {
type: "DELETE",
data: { user_id: member.get("id") }
}).then(function() {
// reload member list
self.findMembers();
});
},
removeMember(member) {
var self = this;
return Discourse.ajax('/admin/groups/' + this.get('id') + '/members.json', {
return Discourse.ajax('/groups/' + this.get('id') + '/members.json', {
type: "DELETE",
data: { user_id: member.get("id") }
}).then(function() {
@ -46,7 +71,17 @@ const Group = Discourse.Model.extend({
addMembers(usernames) {
var self = this;
return Discourse.ajax('/admin/groups/' + this.get('id') + '/members.json', {
return Discourse.ajax('/groups/' + this.get('id') + '/members.json', {
type: "PUT",
data: { usernames: usernames }
}).then(function() {
self.findMembers();
});
},
addOwners(usernames) {
var self = this;
return Discourse.ajax('/admin/groups/' + this.get('id') + '/owners.json', {
type: "PUT",
data: { usernames: usernames }
}).then(function() {

View File

@ -135,11 +135,6 @@ TopicList.reopenClass({
return store.findFiltered('topicList', {filter, params});
},
list(filter) {
Ember.warn('`Discourse.TopicList.list` is deprecated. Use the store instead');
return this.find(filter);
},
// hide the category when it has no children
hideUniformCategory(list, category) {
list.set('hideCategory', category && !category.get("has_children"));

View File

@ -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 });

View File

@ -1,4 +1,4 @@
const rootURL = Discourse.BaseUri && Discourse.BaseUri !== "/" ? Discourse.BaseUri : undefined;
const rootURL = Discourse.BaseUri;
const BareRouter = Ember.Router.extend({
rootURL,

View File

@ -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() {

View File

@ -5,17 +5,16 @@
{{model.displayName}}
</h1>
<table class='badges-listing'>
<tbody>
<tr>
<td class='badge'>{{user-badge badge=model}}</td>
<td class='description'>{{{model.displayDescriptionHtml}}}</td>
<td class='grant-count'>{{i18n 'badges.granted' count=model.grant_count}}</td>
<td class='info'>{{i18n 'badges.allow_title'}} {{{view.allowTitle}}}<br>{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}}
</td>
</tr>
</tbody>
</table>
<div class='badges-listing'>
<div class='row'>
<div class='badge'>{{user-badge badge=model}}</div>
<div class='description'>{{{model.displayDescriptionHtml}}}</div>
<div class='grant-count'>{{i18n 'badges.granted' count=model.grant_count}}</div>
<div class='info'>{{i18n 'badges.allow_title'}} {{{view.allowTitle}}}<br>{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}}
</div>
</div>
</div>
{{#if showLongDescription}}
<div class='long-description banner'>

View File

@ -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>

View File

@ -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}}

View File

@ -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'}}">
{{{preview}}}
<div class="d-editor-preview-wrapper">
<div class="d-editor-preview">
{{{preview}}}
</div>
</div>
</div>

View File

@ -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'>

View File

@ -1,18 +1,40 @@
{{#if model}}
{{#if isOwner}}
<div class='clearfix'>
<form id='add-user-to-group' autocomplete="off">
{{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}}
{{d-button action="addMembers" class="add" icon="plus" label="groups.add"}}
</form>
</div>
{{/if}}
<table class='group-members'>
<tr>
<th colspan="2">{{i18n 'last_post'}}</th>
<th>{{i18n 'last_seen'}}</th>
{{#if isOwner}}
<th></th>
{{/if}}
</tr>
{{#each model.members as |m|}}
<tr>
<td class='avatar'>{{user-small user=m}}</td>
<td class='avatar'>{{user-small user=m}}
{{#if m.owner}}
<span class='is-owner'>{{i18n "groups.owner"}}</span>
{{/if}}
</td>
<td>
<span class="text">{{bound-date m.last_posted_at}}</span>
</td>
<td>
<span class="text">{{bound-date m.last_seen_at}}</span>
</td>
{{#if isOwner}}
<td class='remove-user'>
{{#unless m.owner}}
<a class="remove-link" {{action "removeMember" m}}><i class="fa fa-times"></i></a>
{{/unless}}
</td>
{{/if}}
</tr>
{{/each}}
</table>

View File

@ -1,8 +1,5 @@
{{#if view.showBadges}}
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}
{{else}}
{{#if topic.unseen}}
<span class="badge-notification new-topic"></span>
{{/if}}
{{raw "list/posts-count-column" topic=topic tagName="div"}}
{{/if}}
{{/if}}

View File

@ -8,7 +8,9 @@
<div class='main-link'>
{{topic-status topic=t}}
{{topic-link t}}
{{#if t.unseen}}
<span class="badge-notification new-topic"></span>
{{/if}}
{{#if t.hasExcerpt}}
<div class="topic-excerpt">
{{{t.excerpt}}}

View File

@ -21,8 +21,10 @@
<div class='topic-inset'>
{{topic-status topic=t}}
{{topic-link t}}
{{#if t.unseen}}
<span class="badge-notification new-topic"></span>
{{/if}}
<span class="{{cold-age-class t.last_posted_at}}" title='{{raw-date t.last_posted_at}}'>{{{format-age t.last_posted_at}}}</span>
{{#if t.hasExcerpt}}
<div class="topic-excerpt">
{{{t.excerpt}}}

View File

@ -10,6 +10,9 @@
<div class='main-link'>
{{raw "topic-status" topic=content}}
{{topic-link content}}
{{#if content.unseen}}
<span class="badge-notification new-topic"></span>
{{/if}}
{{raw "list/topic-excerpt" topic=content}}
</div>

View File

@ -50,6 +50,8 @@
</tr>
{{/if}}
{{plugin-outlet "create-account-before-password"}}
{{#if passwordRequired}}
<tr class="input">
<td class="label"><label for='new-account-password'>{{i18n 'user.password.title'}}</label></td>

View File

@ -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>

View File

@ -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}}

View File

@ -1,103 +1,70 @@
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, {
const ComposerView = Ember.View.extend({
_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);
$('.composer-popup').css('bottom', sizePx);
// signal the progress bar it should move!
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 +73,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 +82,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;

View File

@ -1,11 +1,16 @@
import UrlRefresh from 'discourse/mixins/url-refresh';
import { on } from 'ember-addons/ember-computed-decorators';
const CATEGORIES_LIST_BODY_CLASS = "categories-list";
export default Ember.View.extend(UrlRefresh, {
_addBodyClass: function() {
$('body').addClass('categories-list');
}.on('didInsertElement'),
@on("didInsertElement")
addBodyClass() {
$('body').addClass(CATEGORIES_LIST_BODY_CLASS);
},
_removeBodyClass: function() {
$('body').removeClass('categories-list');
}.on('willDestroyElement')
@on("willDestroyElement")
removeBodyClass() {
$('body').removeClass(CATEGORIES_LIST_BODY_CLASS);
},
});

View File

@ -0,0 +1,15 @@
import { on } from 'ember-addons/ember-computed-decorators';
const CATEGORIES_BODY_CLASS = "navigation-categories";
export default Ember.View.extend({
@on("didInsertElement")
addBodyClass() {
$('body').addClass(CATEGORIES_BODY_CLASS);
},
@on("willDestroyElement")
removeBodyClass() {
$('body').removeClass(CATEGORIES_BODY_CLASS);
},
});

View File

@ -1,5 +1,5 @@
import AddCategoryClass from 'discourse/mixins/add-category-class';
export default Em.View.extend(AddCategoryClass, {
categoryFullSlug: Em.computed.alias('controller.category.fullSlug')
export default Ember.View.extend(AddCategoryClass, {
categoryFullSlug: Ember.computed.alias('controller.category.fullSlug')
});

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
@ -98,6 +98,7 @@ export default Ember.View.extend({
const controller = this.get('controller');
if (controller.get('expanded')) {
if (e.keyCode === 13) {
this.$('input').blur();
controller.send('jumpPost');
} else if (e.keyCode === 27) {
controller.send('toggleExpansion');

View File

@ -4,7 +4,6 @@ import ClickTrack from 'discourse/lib/click-track';
import { listenForViewEvent } from 'discourse/lib/app-events';
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Scrolling from 'discourse/mixins/scrolling';
import isElementInViewport from "discourse/lib/is-element-in-viewport";
const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolling, {
templateName: 'topic',
@ -95,7 +94,6 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
// The user has scrolled the window, or it is finished rendering and ready for processing.
scrolled() {
if (this.isDestroyed || this.isDestroying || this._state !== 'inDOM') {
return;
}
@ -118,14 +116,6 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded'));
}
// automatically unpin topics when the user reaches the bottom
if (topic.get("pinned")) {
const $topicFooterButtons = $("#topic-footer-buttons");
if ($topicFooterButtons.length > 0 && isElementInViewport($topicFooterButtons)) {
Em.run.next(() => topic.clearPin());
}
}
// Trigger a scrolled event
this.appEvents.trigger('topic:scrolled', offset);
},

View File

@ -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');
}
}
}
});

View File

@ -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

View File

@ -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");
}
}
};

View File

@ -1188,7 +1188,7 @@ table.api-keys {
height: 200px;
}
.wmd-input {
.d-editor-input {
width: 98%;
height: 200px;
}

View File

@ -38,11 +38,28 @@
}
}
.textarea-wrapper .spinner {
z-index: 1000;
margin-top: 5em;
#reply-control {
.d-editor-textarea-wrapper .spinner {
z-index: 1000;
margin-top: 5em;
}
.d-editor-button-bar {
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 0px;
padding: 5px;
border-bottom: 2px solid dark-light-diff($primary, $secondary, 90%, -60%);
height: 33px;
}
textarea {
box-shadow: none;
}
}
.saving-text .spinner {
display: inline-block;
left: 5px;
@ -58,6 +75,23 @@ div.ac-wrap.disabled {
}
}
div.ac-wrap div.item a.remove, .remove-link {
margin-left: 4px;
font-size: 11px;
line-height: 10px;
padding: 1.5px 1.5px 1.5px 2.5px;
border-radius: 12px;
width: 10px;
display: inline-block;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
&:hover {
background-color: scale-color($danger, $lightness: 75%);
border: 1px solid scale-color($danger, $lightness: 30%);
text-decoration: none;
color: $danger;
}
}
div.ac-wrap {
background-color: $secondary;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
@ -71,22 +105,6 @@ div.ac-wrap {
display: inline-block;
line-height: 20px;
}
a.remove {
margin-left: 4px;
font-size: 11px;
line-height: 10px;
padding: 1.5px 1.5px 1.5px 2.5px;
border-radius: 12px;
width: 10px;
display: inline-block;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
&:hover {
background-color: scale-color($danger, $lightness: 75%);
border: 1px solid scale-color($danger, $lightness: 30%);
text-decoration: none;
color: $danger;
}
}
}
input[type="text"] {
float: left;
@ -99,10 +117,6 @@ div.ac-wrap {
}
}
#reply-control.topic #wmd-quote-post {
display: none;
}
.auto-close-fields {
div:not(:first-child) {
margin-top: 10px;
@ -175,7 +189,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 {

View File

@ -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;
}

View File

@ -14,7 +14,7 @@ body img.emoji {
left: 50%;
top: 50%;
width: 445px;
height: 264px;
min-height: 264px;
margin-top: -132px;
margin-left: -222px;
background-color: dark-light-choose(#dadada, blend-primary-secondary(5%));

View File

@ -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;
}

View File

@ -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%;
}

View File

@ -65,57 +65,80 @@
}
/* Badge listing in /badges. */
table.badges-listing {
.badges-listing {
margin: 20px 0;
tr {
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
td {
padding: 10px 0;
}
}
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
width: 90%;
padding: 10px;
display: table;
.row {
display: table-row;
> div {
display: table-cell;
vertical-align: middle;
}
}
.user-badge {
font-size: $base-font-size;
}
tr.title td {
padding-top: 30px;
padding-bottom: 15px;
}
tr.title {
border-top: 0px solid;
}
td {
padding: 10px 0px;
}
td.granted{
color: $success;
font-size: 120%;
}
td.grant-count {
.grant-count {
text-align: center;
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%));
font-size: 120%;
}
td.badge, td.grant-count {
.badge, .grant-count {
white-space: nowrap;
padding-right: 10px;
}
td.info {
.info {
font-size: 0.9em;
text-align: right;
}
td.description {
.description {
}
tr {
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
@media all and (max-width: 750px) {
.show-badge .user-badge-with-posts .badge-user a.post-link {
width: auto;
}
.show-badge div.badge-user {
padding: 0;
}
.badges-listing {
display: block;
.info, .grant-count {
text-align: left;
}
.row > div.info { display: none; }
.row {
display: block;
> div {
display: block;
margin-top: 10px;
}
}
}
}
/* /badges/:id/:slug page styling. */
.show-badge {
.badge-user {
@ -124,6 +147,7 @@ table.badges-listing {
padding: 5px 10px;
margin-bottom: 10px;
display: inline-block;
vertical-align: top;
.details {
margin: 0 10px;
@ -131,6 +155,10 @@ table.badges-listing {
color: $primary;
}
.username {
word-wrap: break-word;
}
.date {
display: block;
color: lighten($primary, 40%);

View File

@ -3,8 +3,7 @@
}
.topic-list tr.selected td:first-child, .topic-list-item.selected td:first-child, .topic-post.selected {
box-shadow: -2px 0 0 $danger;
border-left: 1px solid $danger;
box-shadow: -3px 0 0 $danger;
}
.topic-list-item.selected {

View File

@ -10,10 +10,12 @@
position: absolute;
background-color: black;
opacity: 0.8;
z-index: 1000;
}
.d-editor-modals {
position: absolute;
z-index: 1001;
}
.d-editor .d-editor-modal {
@ -71,7 +73,6 @@
color: $primary;
border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%);
overflow: auto;
visibility: visible;
cursor: default;
margin-top: 8px;
padding: 8px 8px 0 8px;

View File

@ -65,7 +65,7 @@
}
.similar-topics {
background-color: dark-light-choose(scale-color($tertiary, $lightness: 60%), scale-color($tertiary, $lightness: -60%));
background-color: dark-light-diff($tertiary, $secondary, 85%, -65%);
a[href] {
color: dark-light-diff($primary, $secondary, -10%, 10%);
@ -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 {
width: 100%;
}
.preview-wrapper {
display: none;
}
.textarea-wrapper {
width: 100%;
}
.wmd-controls.hide-preview {
.d-editor-input {
width: 100%;
}
.d-editor-preview-wrapper {
display: none;
}
.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,18 +418,27 @@
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%);
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
background-color: $secondary;
z-index: 100;
overflow: hidden;
width: 50%;
-moz-box-sizing: border-box;
box-sizing: border-box;
button {
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
}
}
}

View File

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

View File

@ -39,10 +39,6 @@
}
}
.bio-composer #wmd-quote-post {
display: none;
}
.static {
color: $primary;
display: inline-block;
@ -105,6 +101,21 @@
.user-main {
margin-bottom: 50px;
// name hacky so lastpass does not freak out
// -search- means it is bypassed
#add-user-to-group {
button, .ac-wrap {
float: left;
}
button {
margin-top: 3px;
margin-left: 10px;
}
#user-search-selector {
width: 400px;
}
}
table.group-members {
width: 100%;
p {
@ -120,6 +131,16 @@
}
td.avatar {
width: 60px;
position: relative;
.is-owner {
position: absolute;
right: 0;
top: 20px;
color: dark-light-diff($primary, $secondary, 50%, -50%);
}
}
td.remove-user {
text-align: right;
}
td {
padding: 0.5em;

View File

@ -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 {
@ -214,6 +214,14 @@ input {
#reply-control {
.d-editor {
height: 100%;
}
.d-editor-container {
height: 100%;
}
.wmd-controls {
left: 10px;
right: 10px;
@ -221,23 +229,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%;
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 +257,7 @@ input {
}
}
}
.wmd-button-bar {
.d-editor-button-bar {
display: none;
}
}

View File

@ -1,3 +1,10 @@
.emoji-table-wrapper {
min-width: 320px;
}
.emoji-modal {
width: 340px;
margin-top: -132px;
margin-left: -170px;
background-color: dark-light-choose(#dadada, blend-primary-secondary(5%));
}

View File

@ -92,8 +92,8 @@
margin-right: 0;
}
&.new-topic {
padding-right: 0;
top: -3px;
padding: 0;
top: -2px;
}
}

View File

@ -344,10 +344,8 @@ span.post-count {
padding: 15px 0;
}
.topic-post {
&.moderator {
background-color: dark-light-diff($highlight, $secondary, 70%, -80%);
}
.moderator .topic-body {
background-color: dark-light-diff($highlight, $secondary, 70%, -80%);
}
.quote-button.visible {
@ -528,4 +526,3 @@ span.highlighted {
margin-left: -10px;
margin-right: -10px;
}

View File

@ -56,7 +56,7 @@
width: 0;
right: 0;
bottom: 0;
z-index: 500;
z-index: 950;
margin-right: 148px;
}

View File

@ -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;

View File

@ -107,58 +107,30 @@ class Admin::GroupsController < Admin::AdminController
render json: success_json
end
def add_members
def add_owners
group = Group.find(params.require(:id))
return can_not_modify_automatic if group.automatic
if params[:usernames].present?
users = User.where(username: params[:usernames].split(","))
elsif params[:user_ids].present?
users = User.find(params[:user_ids].split(","))
elsif params[:user_emails].present?
users = User.where(email: params[:user_emails].split(","))
else
raise Discourse::InvalidParameters.new('user_ids or usernames or user_emails must be present')
end
users = User.where(username: params[:usernames].split(","))
users.each do |user|
if !group.users.include?(user)
group.add(user)
else
return render_json_error I18n.t('groups.errors.member_already_exist', username: user.username)
end
group.group_users.where(user_id: user.id).update_all(owner: true)
end
if group.save
render json: success_json
else
render_json_error(group)
end
render json: success_json
end
def remove_member
def remove_owner
group = Group.find(params.require(:id))
return can_not_modify_automatic if group.automatic
if params[:user_id].present?
user = User.find(params[:user_id])
elsif params[:username].present?
user = User.find_by_username(params[:username])
else
raise Discourse::InvalidParameters.new('user_id or username must be present')
end
user = User.find(params[:user_id].to_i)
group.group_users.where(user_id: user.id).update_all(owner: false)
user.primary_group_id = nil if user.primary_group_id == group.id
group.users.delete(user.id)
if group.save && user.save
render json: success_json
else
render_json_error(group)
end
render json: success_json
end
protected

View File

@ -138,13 +138,8 @@ class ApplicationController < ActionController::Base
end
def set_locale
I18n.locale = if current_user
current_user.effective_locale
else
SiteSetting.default_locale
end
I18n.fallbacks.ensure_loaded!
I18n.locale = current_user.try(:effective_locale) || SiteSetting.default_locale
I18n.ensure_all_loaded!
end
def store_preloaded(key, json)

View File

@ -23,10 +23,12 @@ class GroupsController < ApplicationController
offset = params[:offset].to_i
total = group.users.count
members = group.users.order(:username_lower).limit(limit).offset(offset)
members = group.users.order('NOT group_users.owner').order(:username_lower).limit(limit).offset(offset)
owners = group.users.order(:username_lower).where('group_users.owner')
render json: {
members: serialize_data(members, GroupUserSerializer),
owners: serialize_data(owners, GroupUserSerializer),
meta: {
total: total,
limit: limit,
@ -36,35 +38,56 @@ class GroupsController < ApplicationController
end
def add_members
guardian.ensure_can_edit!(the_group)
group = Group.find(params[:id])
guardian.ensure_can_edit!(group)
added_users = []
usernames = params.require(:usernames)
usernames.split(",").each do |username|
if user = User.find_by_username(username)
unless the_group.users.include?(user)
the_group.add(user)
added_users << user
end
if params[:usernames].present?
users = User.where(username: params[:usernames].split(","))
elsif params[:user_ids].present?
users = User.find(params[:user_ids].split(","))
elsif params[:user_emails].present?
users = User.where(email: params[:user_emails].split(","))
else
raise Discourse::InvalidParameters.new('user_ids or usernames or user_emails must be present')
end
users.each do |user|
if !group.users.include?(user)
group.add(user)
else
return render_json_error I18n.t('groups.errors.member_already_exist', username: user.username)
end
end
# always succeeds, even if bogus usernames were provided
render_serialized(added_users, GroupUserSerializer)
if group.save
render json: success_json
else
render_json_error(group)
end
end
def remove_member
guardian.ensure_can_edit!(the_group)
group = Group.find(params[:id])
guardian.ensure_can_edit!(group)
removed_users = []
username = params.require(:username)
if user = User.find_by_username(username)
the_group.remove(user)
removed_users << user
if params[:user_id].present?
user = User.find(params[:user_id])
elsif params[:username].present?
user = User.find_by_username(params[:username])
else
raise Discourse::InvalidParameters.new('user_id or username must be present')
end
user.primary_group_id = nil if user.primary_group_id == group.id
group.users.delete(user.id)
if group.save && user.save
render json: success_json
else
render_json_error(group)
end
# always succeeds, even if user was not a member
render_serialized(removed_users, GroupUserSerializer)
end
private

Some files were not shown because too many files have changed in this diff Show More