Version bump
This commit is contained in:
commit
50da33eee1
@ -6,7 +6,6 @@ languages:
|
||||
|
||||
exclude_paths:
|
||||
- "app/assets/javascripts/defer/*"
|
||||
- "app/assets/javascripts/discourse/lib/Markdown.Editor.js"
|
||||
- "app/assets/javascripts/ember-addons/*"
|
||||
- "lib/autospec/*"
|
||||
- "lib/es6_module_transpiler/*"
|
||||
|
||||
@ -6,9 +6,7 @@ app/assets/javascripts/pagedown_custom.js
|
||||
app/assets/javascripts/vendor.js
|
||||
app/assets/javascripts/locales/i18n.js
|
||||
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
||||
app/assets/javascripts/discourse/lib/Markdown.Editor.js
|
||||
app/assets/javascripts/ember-addons/
|
||||
jsapp/lib/Markdown.Editor.js
|
||||
lib/javascripts/locale/
|
||||
lib/javascripts/messageformat.js
|
||||
lib/javascripts/moment.js
|
||||
|
||||
1
Gemfile
1
Gemfile
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
},
|
||||
}
|
||||
});
|
||||
@ -1,15 +0,0 @@
|
||||
export default Ember.TextArea.extend({
|
||||
classNameBindings: [':wmd-input'],
|
||||
|
||||
placeholder: function() {
|
||||
return I18n.t('composer.reply_placeholder');
|
||||
}.property('placeholderKey'),
|
||||
|
||||
_signalParentInsert: function() {
|
||||
this.get('parentView').childDidInsertElement(this);
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_signalParentDestroy: function() {
|
||||
this.get('parentView').childWillDestroyElement(this);
|
||||
}.on('willDestroyElement')
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['title-input'],
|
||||
|
||||
@on('didInsertElement')
|
||||
_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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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', '');
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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')
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -2,7 +2,7 @@ const MAX_SHOWN = 5;
|
||||
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import property from 'ember-addons/ember-computed-decorators';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const { get, isEmpty, Component } = Ember;
|
||||
|
||||
@ -12,7 +12,7 @@ export default Component.extend(StringBuffer, {
|
||||
rerenderTriggers: ['expanded'],
|
||||
|
||||
// Roll up links to avoid duplicates
|
||||
@property('links')
|
||||
@computed('links')
|
||||
collapsed(links) {
|
||||
const seen = {};
|
||||
const result = [];
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
function loadDraft(store, opts) {
|
||||
opts = opts || {};
|
||||
@ -50,17 +49,17 @@ export default Ember.Controller.extend({
|
||||
|
||||
showEditReason: false,
|
||||
editReason: null,
|
||||
maxTitleLength: setting('max_topic_title_length'),
|
||||
scopedCategoryId: null,
|
||||
similarTopics: null,
|
||||
similarTopicsMessage: null,
|
||||
lastSimilaritySearch: null,
|
||||
optionsVisible: false,
|
||||
|
||||
topic: null,
|
||||
lastValidatedAt: null,
|
||||
|
||||
// TODO: Remove this, very bad
|
||||
view: null,
|
||||
isUploading: false,
|
||||
|
||||
topic: null,
|
||||
|
||||
_initializeSimilar: function() {
|
||||
this.set('similarTopics', []);
|
||||
@ -109,7 +108,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
// Import a quote from the post
|
||||
importQuote() {
|
||||
importQuote(toolbarEvent) {
|
||||
const postStream = this.get('topic.postStream');
|
||||
let postId = this.get('model.post.id');
|
||||
|
||||
@ -135,7 +134,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
return this.store.find('post', postId).then(function(post) {
|
||||
const quote = Quote.build(post, post.get("raw"), {raw: true, full: true});
|
||||
composer.appendBlockAtCursor(quote);
|
||||
toolbarEvent.addText(quote);
|
||||
composer.set('model.loading', false);
|
||||
});
|
||||
}
|
||||
@ -173,39 +172,10 @@ export default Ember.Controller.extend({
|
||||
|
||||
},
|
||||
|
||||
appendText(text, opts) {
|
||||
const c = this.get('model');
|
||||
if (c) {
|
||||
opts = opts || {};
|
||||
const wmd = $('.wmd-input'),
|
||||
val = wmd.val() || '',
|
||||
position = opts.position === "cursor" ? wmd.caret() : val.length,
|
||||
caret = c.appendText(text, position, opts);
|
||||
|
||||
if (wmd[0]) {
|
||||
Em.run.next(() => Discourse.Utilities.setCaretPosition(wmd[0], caret));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
appendTextAtCursor(text, opts) {
|
||||
opts = opts || {};
|
||||
opts.position = "cursor";
|
||||
this.appendText(text, opts);
|
||||
},
|
||||
|
||||
appendBlockAtCursor(text, opts) {
|
||||
opts = opts || {};
|
||||
opts.position = "cursor";
|
||||
opts.block = true;
|
||||
this.appendText(text, opts);
|
||||
},
|
||||
|
||||
categories: function() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
|
||||
|
||||
toggle() {
|
||||
this.closeAutocomplete();
|
||||
switch (this.get('model.composeState')) {
|
||||
@ -225,7 +195,7 @@ export default Ember.Controller.extend({
|
||||
return false;
|
||||
},
|
||||
|
||||
disableSubmit: Ember.computed.or("model.loading", "view.isUploading"),
|
||||
disableSubmit: Ember.computed.or("model.loading", "isUploading"),
|
||||
|
||||
save(force) {
|
||||
const composer = this.get('model');
|
||||
@ -237,12 +207,7 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
if (composer.get('cantSubmitPost')) {
|
||||
const now = Date.now();
|
||||
this.setProperties({
|
||||
'view.showTitleTip': now,
|
||||
'view.showCategoryTip': now,
|
||||
'view.showReplyTip': now
|
||||
});
|
||||
this.set('lastValidatedAt', Date.now());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -291,10 +256,18 @@ export default Ember.Controller.extend({
|
||||
var staged = false;
|
||||
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
|
||||
|
||||
const promise = composer.save({
|
||||
imageSizes: this.get('view').imageSizes(),
|
||||
editReason: this.get("editReason")
|
||||
}).then(function(result) {
|
||||
// TODO: This should not happen in model
|
||||
const imageSizes = {};
|
||||
$('#reply-control .d-editor-preview img').each((i, e) => {
|
||||
const $img = $(e);
|
||||
const src = $img.prop('src');
|
||||
|
||||
if (src && src.length) {
|
||||
imageSizes[src] = { width: $img.width(), height: $img.height() };
|
||||
}
|
||||
});
|
||||
|
||||
const promise = composer.save({ imageSizes, editReason: this.get("editReason")}).then(function(result) {
|
||||
if (result.responseJson.action === "enqueued") {
|
||||
self.send('postWasEnqueued', result.responseJson);
|
||||
self.destroyDraft();
|
||||
@ -366,8 +339,8 @@ export default Ember.Controller.extend({
|
||||
// We don't care about similar topics unless creating a topic
|
||||
if (!this.get('model.creatingTopic')) { return; }
|
||||
|
||||
let body = this.get('model.reply');
|
||||
const title = this.get('model.title');
|
||||
let body = this.get('model.reply') || '';
|
||||
const title = this.get('model.title') || '';
|
||||
|
||||
// Ensure the fields are of the minimum length
|
||||
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
|
||||
@ -405,11 +378,6 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
saveDraft() {
|
||||
const model = this.get('model');
|
||||
if (model) { model.saveDraft(); }
|
||||
},
|
||||
|
||||
/**
|
||||
Open the composer view
|
||||
|
||||
@ -502,7 +470,7 @@ export default Ember.Controller.extend({
|
||||
composerModel.set('composeState', Discourse.Composer.OPEN);
|
||||
composerModel.set('isWarning', false);
|
||||
|
||||
if (opts.topicTitle && opts.topicTitle.length <= this.get('maxTitleLength')) {
|
||||
if (opts.topicTitle && opts.topicTitle.length <= this.siteSettings.max_topic_title_length) {
|
||||
this.set('model.title', opts.topicTitle);
|
||||
}
|
||||
|
||||
@ -572,7 +540,6 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
shrink() {
|
||||
if (this.get('model.replyDirty')) {
|
||||
this.collapse();
|
||||
@ -581,22 +548,34 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
_saveDraft() {
|
||||
const model = this.get('model');
|
||||
if (model) { model.saveDraft(); };
|
||||
},
|
||||
|
||||
@observes('model.reply', 'model.title')
|
||||
_shouldSaveDraft() {
|
||||
Ember.run.debounce(this, this._saveDraft, 2000);
|
||||
},
|
||||
|
||||
@computed('model.categoryId', 'lastValidatedAt')
|
||||
categoryValidation(categoryId, lastValidatedAt) {
|
||||
if( !this.siteSettings.allow_uncategorized_topics && !categoryId) {
|
||||
return Discourse.InputValidation.create({ failed: true, reason: I18n.t('composer.error.category_missing'), lastShownAt: lastValidatedAt });
|
||||
}
|
||||
},
|
||||
|
||||
collapse() {
|
||||
this.saveDraft();
|
||||
this._saveDraft();
|
||||
this.set('model.composeState', Discourse.Composer.DRAFT);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.setProperties({
|
||||
model: null,
|
||||
'view.showTitleTip': false,
|
||||
'view.showCategoryTip': false,
|
||||
'view.showReplyTip': false
|
||||
});
|
||||
this.setProperties({ model: null, lastValidatedAt: null });
|
||||
},
|
||||
|
||||
closeAutocomplete() {
|
||||
$('.wmd-input').autocomplete({ cancel: true });
|
||||
$('.d-editor-input').autocomplete({ cancel: true });
|
||||
},
|
||||
|
||||
showOptions() {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -111,7 +111,6 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed('q')
|
||||
showLikeCount(q) {
|
||||
console.log(q);
|
||||
return q && q.indexOf("order:likes") > -1;
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -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(`[](${imageLink})`);
|
||||
} else {
|
||||
toolbarEvent.addText(imageUrl);
|
||||
}
|
||||
}
|
||||
this.send('closeModal');
|
||||
},
|
||||
|
||||
useLocal() {
|
||||
this.setProperties({ local: true, showMore: false});
|
||||
},
|
||||
useRemote() {
|
||||
this.set("local", false);
|
||||
},
|
||||
toggleShowMore() {
|
||||
this.toggleProperty("showMore");
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -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]; });
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
@ -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;
|
||||
|
||||
@ -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"]++;
|
||||
|
||||
@ -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); });
|
||||
|
||||
@ -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-*');
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -19,7 +19,7 @@ const ScrollingDOMMethods = {
|
||||
},
|
||||
|
||||
screenNotFull() {
|
||||
return $(window).height() > $(document).height();
|
||||
return $(window).height() > $("#main").height();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const rootURL = Discourse.BaseUri && Discourse.BaseUri !== "/" ? Discourse.BaseUri : undefined;
|
||||
const rootURL = Discourse.BaseUri;
|
||||
|
||||
const BareRouter = Ember.Router.extend({
|
||||
rootURL,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
{{d-editor tabindex="4"
|
||||
value=composer.reply
|
||||
placeholder="composer.reply_placeholder"
|
||||
previewUpdated="previewUpdated"
|
||||
markdownOptions=markdownOptions
|
||||
extraButtons="extraButtons"
|
||||
importQuote="importQuote"
|
||||
showOptions="showOptions"
|
||||
showUploadModal="showUploadModal"
|
||||
validation=validation
|
||||
loading=composer.loading}}
|
||||
|
||||
<div class="composer-bottom-right">
|
||||
{{#if site.mobileView}}
|
||||
<input type="file" id="mobile-uploader" />
|
||||
<a class="mobile-file-upload {{if isUploading 'hidden'}}">{{i18n 'upload'}}</a>
|
||||
{{else}}
|
||||
<a href {{action "togglePreview"}} class='toggle-preview'>{{{toggleText}}}</a>
|
||||
{{/if}}
|
||||
{{#if isUploading}}
|
||||
<div id="file-uploading">
|
||||
{{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}}
|
||||
{{uploadProgress}}%
|
||||
<a href id="cancel-file-upload" {{action "cancelUpload"}}>{{fa-icon "times"}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div id='draft-status' class="{{if isUploading 'hidden'}}">
|
||||
{{draftStatus}}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,8 @@
|
||||
{{text-field value=composer.title
|
||||
tabindex="2"
|
||||
id="reply-title"
|
||||
maxLength=siteSettings.max_topic_title_length
|
||||
placeholderKey="composer.title_placeholder"
|
||||
disabled=composer.loading}}
|
||||
|
||||
{{popup-input-tip validation=validation}}
|
||||
@ -17,10 +17,17 @@
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class='d-editor-preview-header'></div>
|
||||
|
||||
{{textarea value=value class="d-editor-input"}}
|
||||
<div class="d-editor-textarea-wrapper">
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
{{textarea tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated}}
|
||||
{{popup-input-tip validation=validation}}
|
||||
</div>
|
||||
|
||||
<div class="d-editor-preview {{unless preview 'hidden'}}">
|
||||
{{{preview}}}
|
||||
<div class="d-editor-preview-wrapper">
|
||||
<div class="d-editor-preview">
|
||||
{{{preview}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -56,15 +56,12 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="title-input">
|
||||
{{text-field value=model.title tabindex="2" id="reply-title" maxLength=maxTitleLength placeholderKey="composer.title_placeholder"}}
|
||||
{{popup-input-tip validation=view.titleValidation shownAt=view.showTitleTip}}
|
||||
</div>
|
||||
{{composer-title composer=model lastValidatedAt=lastValidatedAt}}
|
||||
|
||||
{{#if model.showCategoryChooser}}
|
||||
<div class="category-input">
|
||||
{{category-chooser valueAttribute="id" value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}}
|
||||
{{popup-input-tip validation=view.categoryValidation shownAt=view.showCategoryTip}}
|
||||
{{popup-input-tip validation=categoryValidation}}
|
||||
</div>
|
||||
{{#if model.archetype.hasOptions}}
|
||||
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button>
|
||||
@ -77,35 +74,15 @@
|
||||
{{plugin-outlet "composer-fields"}}
|
||||
</div>
|
||||
|
||||
<div class='wmd-controls'>
|
||||
<div class='textarea-wrapper'>
|
||||
<div class='wmd-button-bar'></div>
|
||||
<div class='wmd-preview-scroller'></div>
|
||||
{{conditional-loading-spinner condition=model.loading}}
|
||||
{{composer-text-area tabindex="4" value=model.reply}}
|
||||
{{popup-input-tip validation=view.replyValidation shownAt=view.showReplyTip}}
|
||||
</div>
|
||||
<!-- keep the classes here in sync with post.hbs -->
|
||||
<div class='preview-wrapper regular'>
|
||||
<div class="wmd-preview cooked {{if model.hidePreview 'hidden'}}"></div>
|
||||
</div>
|
||||
<div class="composer-bottom-right">
|
||||
{{#if site.mobileView}}
|
||||
<input type="file" id="mobile-uploader" />
|
||||
<a class="mobile-file-upload {{if view.isUploading 'hidden'}}">{{i18n 'upload'}}</a>
|
||||
{{else}}
|
||||
<a href {{action "togglePreview"}} class='toggle-preview'>{{{model.toggleText}}}</a>
|
||||
{{/if}}
|
||||
{{#if view.isUploading}}
|
||||
<div id="file-uploading">
|
||||
{{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}% <a id="cancel-file-upload">{{fa-icon "times"}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div id='draft-status' class="{{if view.isUploading 'hidden'}}">
|
||||
{{model.draftStatus}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{composer-editor topic=topic
|
||||
composer=model
|
||||
lastValidatedAt=lastValidatedAt
|
||||
canWhisper=canWhisper
|
||||
draftStatus=model.draftStatus
|
||||
isUploading=isUploading
|
||||
importQuote="importQuote"
|
||||
showOptions="showOptions"
|
||||
showUploadSelector="showUploadSelector"}}
|
||||
|
||||
{{#if currentUser}}
|
||||
<div class='submit-panel'>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}}
|
||||
|
||||
@ -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}}}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{{#if local}}
|
||||
<div class="inputs">
|
||||
<input type="file" id="filename-input" multiple><br>
|
||||
<span class="description">{{unbound view.tip}}</span>
|
||||
<span class="description">{{tip}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -14,31 +14,34 @@
|
||||
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
||||
{{#if remote}}
|
||||
<div class="inputs">
|
||||
<input type="text" id="fileurl-input" placeholder="http://example.com/image.png"><br>
|
||||
<span class="description">{{unbound view.tip}}</span>
|
||||
{{input value=imageUrl placeholder="http://example.com/image.png"}}
|
||||
<span class="description">{{tip}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if showMore}}
|
||||
<div class="radios">
|
||||
<div class="inputs">
|
||||
<input type="text" id="link-input" placeholder="http://example.com"><br>
|
||||
{{input value=imageLink laceholder="http://example.com"}}
|
||||
<span class="description">{{i18n 'upload_selector.image_link'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="radios">
|
||||
<div class="inputs">
|
||||
<p class="hint">{{unbound view.hint}}</p>
|
||||
<p class="hint">
|
||||
{{#if capabilities.canPasteImages}}
|
||||
{{i18n 'upload_selector.hint'}}
|
||||
{{else}}
|
||||
{{i18n 'upload_selector.hint_for_supported_browsers'}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" {{action "upload" target="view"}}>
|
||||
<span class='add-upload'><i {{bind-attr class=":fa view.uploadIcon"}}></i></span>
|
||||
{{i18n 'upload'}}
|
||||
</button>
|
||||
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
{{d-button action="upload" class='btn-primary' icon=uploadIcon label='upload'}}
|
||||
<a href {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
{{#if remote}}<a {{action "toggleShowMore"}} class="pull-right">{{i18n 'show_more'}}</a>{{/if}}
|
||||
</div>
|
||||
@ -15,9 +15,9 @@
|
||||
{{#if editingTopic}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
||||
{{autofocus-text-field id="edit-title" value=buffered.title maxLength=maxTitleLength}}
|
||||
{{autofocus-text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length}}
|
||||
{{else}}
|
||||
{{autofocus-text-field id="edit-title" value=buffered.title maxLength=maxTitleLength}}
|
||||
{{autofocus-text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length}}
|
||||
<br>
|
||||
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,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;
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
|
||||
@ -36,9 +36,8 @@ export default Ember.View.extend({
|
||||
// the quote reply widget
|
||||
//
|
||||
// Same hack applied to Android cause it has unreliable touchend
|
||||
const caps = this.capabilities;
|
||||
const android = caps.get('android');
|
||||
if (caps.get('winphone') || android) {
|
||||
const isAndroid = this.capabilities.isAndroid;
|
||||
if (this.capabilities.isWinphone || isAndroid) {
|
||||
onSelectionChanged = _.debounce(onSelectionChanged, 500);
|
||||
}
|
||||
|
||||
@ -72,7 +71,7 @@ export default Ember.View.extend({
|
||||
|
||||
// Android is dodgy, touchend often will not fire
|
||||
// https://code.google.com/p/android/issues/detail?id=19827
|
||||
if (!android) {
|
||||
if (!isAndroid) {
|
||||
$(document)
|
||||
.on('touchstart.quote-button', function(){
|
||||
view.set('isTouchInProgress', true);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import ContainerView from 'discourse/views/container';
|
||||
import { default as property, observes, on } from 'ember-addons/ember-computed-decorators';
|
||||
import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default ContainerView.extend({
|
||||
classNameBindings: ['hidden', ':topic-map'],
|
||||
@ -9,7 +9,7 @@ export default ContainerView.extend({
|
||||
Ember.run.once(this, 'rerender');
|
||||
},
|
||||
|
||||
@property
|
||||
@computed
|
||||
hidden() {
|
||||
if (!this.get('post.firstPost')) return true;
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ export default Ember.View.extend({
|
||||
_focusWhenOpened: function() {
|
||||
|
||||
// Don't focus on mobile or touch
|
||||
if (Discourse.Mobile.mobileView || this.capabilities.get('touch')) {
|
||||
if (Discourse.Mobile.mobileView || this.capabilities.touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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("[](" + imageLink + ")");
|
||||
} else {
|
||||
composerView.addMarkdown(imageUrl);
|
||||
}
|
||||
this.get('controller').send('closeModal');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
//= require ./discourse/mixins/ajax
|
||||
//= require ./discourse
|
||||
|
||||
// Pagedown customizations
|
||||
//= require ./pagedown_custom.js
|
||||
|
||||
// Stuff we need to load first
|
||||
//= require_tree ./ember-addons/utils
|
||||
//= require ./ember-addons/decorator-alias
|
||||
@ -77,6 +74,7 @@
|
||||
//= require ./discourse/lib/emoji/emoji
|
||||
//= require ./discourse/lib/emoji/emoji-groups
|
||||
//= require ./discourse/lib/emoji/emoji-toolbar
|
||||
//= require ./discourse/components/d-editor
|
||||
//= require ./discourse/views/composer
|
||||
//= require ./discourse/lib/show-modal
|
||||
//= require ./discourse/lib/screen-track
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
window.PagedownCustom = {
|
||||
insertButtons: [
|
||||
{
|
||||
id: 'wmd-quote-post',
|
||||
description: I18n.t("composer.quote_post_title"),
|
||||
execute: function() {
|
||||
return Discourse.__container__.lookup('controller:composer').send('importQuote');
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
appendButtons: [],
|
||||
|
||||
customActions: {
|
||||
"doBlockquote": function(chunk, postProcessing, oldDoBlockquote) {
|
||||
|
||||
// When traditional linebreaks are set, use the default Pagedown implementation
|
||||
if (Discourse.SiteSettings.traditional_markdown_linebreaks) {
|
||||
return oldDoBlockquote.call(this, chunk, postProcessing);
|
||||
}
|
||||
|
||||
// Our custom blockquote for non-traditional markdown linebreaks
|
||||
var result = [];
|
||||
chunk.selection.split(/\n/).forEach(function (line) {
|
||||
var newLine = "";
|
||||
if (line.indexOf("> ") === 0) {
|
||||
newLine += line.substr(2);
|
||||
} else {
|
||||
if (/\S/.test(line)) { newLine += "> " + line; }
|
||||
}
|
||||
result.push(newLine);
|
||||
});
|
||||
chunk.selection = result.join("\n");
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1188,7 +1188,7 @@ table.api-keys {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
width: 98%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
@ -38,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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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%));
|
||||
|
||||
@ -1,145 +0,0 @@
|
||||
// styles that apply to the PageDown editor
|
||||
// http://code.google.com/p/pagedown/
|
||||
|
||||
.wmd-panel {
|
||||
margin-left: 25%;
|
||||
margin-right: 25%;
|
||||
width: 50%;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.wmd-button-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wmd-button-row {
|
||||
margin: 5px;
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wmd-spacer {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
margin-left: 5px;
|
||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.wmd-button {
|
||||
margin-right: 5px;
|
||||
border: 0;
|
||||
position: relative;
|
||||
float: left;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
text-decoration: inherit;
|
||||
display: inline;
|
||||
width: auto;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
vertical-align: baseline;
|
||||
background-image: none !important;
|
||||
background-position: 0 0;
|
||||
background-repeat: repeat;
|
||||
background: transparent;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.wmd-button:hover {
|
||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
}
|
||||
|
||||
|
||||
.wmd-bold-button:before {
|
||||
content: "\f032";
|
||||
}
|
||||
|
||||
.wmd-italic-button:before {
|
||||
content: "\f033";
|
||||
}
|
||||
|
||||
.wmd-link-button:before {
|
||||
content: "\f0c1";
|
||||
}
|
||||
|
||||
.wmd-quote-button:before {
|
||||
content: "\f10e";
|
||||
}
|
||||
|
||||
.wmd-code-button:before {
|
||||
content: "\f121";
|
||||
}
|
||||
|
||||
.wmd-image-button:before {
|
||||
content: "\f093";
|
||||
}
|
||||
|
||||
.wmd-image-button.image-only:before {
|
||||
content: "\f03e";
|
||||
}
|
||||
|
||||
.wmd-olist-button:before {
|
||||
content: "\f0cb";
|
||||
}
|
||||
|
||||
.wmd-ulist-button:before {
|
||||
content: "\f0ca";
|
||||
}
|
||||
|
||||
.wmd-heading-button:before {
|
||||
content: "\f031";
|
||||
}
|
||||
|
||||
.wmd-hr-button:before {
|
||||
content: "\f068";
|
||||
}
|
||||
|
||||
.wmd-undo-button:before {
|
||||
content: "\f0e2";
|
||||
}
|
||||
|
||||
.wmd-redo-button:before {
|
||||
content: "\f01e";
|
||||
}
|
||||
|
||||
.wmd-quote-post:before {
|
||||
content: "\f0e5";
|
||||
}
|
||||
|
||||
.wmd-composer-options:before {
|
||||
content: "\f013";
|
||||
}
|
||||
|
||||
.wmd-prompt-background {
|
||||
background-color: #111;
|
||||
box-shadow: 0 3px 7px rgba(0,0,0, .8);
|
||||
}
|
||||
|
||||
.wmd-prompt-dialog {
|
||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
}
|
||||
|
||||
.wmd-prompt-dialog > div {
|
||||
font-size: 0.8em;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.wmd-prompt-dialog > form > input[type="text"] {
|
||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.wmd-prompt-dialog > form > input[type="button"] {
|
||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
background: dark-light-choose(initial, blend-primary-secondary(50%));
|
||||
color: dark-light-choose(inherit, $secondary);
|
||||
font-family: trebuchet MS, helvetica, sans-serif;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
// global styles for the cooked HTML content in posts (and preview)
|
||||
.cooked, .wmd-preview {
|
||||
.cooked, .d-editor-preview {
|
||||
word-wrap: break-word;
|
||||
h1, h2, h3, h4, h5, h6 { margin: 30px 0 10px; }
|
||||
h1 { line-height: 1em; } /* normalize.css sets h1 font size but not line height */
|
||||
@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
|
||||
.cooked, .wmd-preview {
|
||||
.cooked, .d-editor-preview {
|
||||
video {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@ -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%);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
width: $topic-body-width;
|
||||
float: left;
|
||||
|
||||
.wmd-input {
|
||||
.d-editor-input {
|
||||
width: 98%;
|
||||
height: 15em;
|
||||
}
|
||||
|
||||
@ -39,10 +39,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bio-composer #wmd-quote-post {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.static {
|
||||
color: $primary;
|
||||
display: inline-block;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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%));
|
||||
}
|
||||
|
||||
@ -92,8 +92,8 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
&.new-topic {
|
||||
padding-right: 0;
|
||||
top: -3px;
|
||||
padding: 0;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
width: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 500;
|
||||
z-index: 950;
|
||||
margin-right: 148px;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
Reference in New Issue
Block a user