thredded 0.12.4 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +30 -33
- data/app/assets/javascripts/thredded/components/currently_online.es6 +28 -21
- data/app/assets/javascripts/thredded/components/flash_messages.es6 +5 -7
- data/app/assets/javascripts/thredded/components/mention_autocompletion.es6 +39 -0
- data/app/assets/javascripts/thredded/components/post_form.es6 +28 -33
- data/app/assets/javascripts/thredded/components/preview_area.es6 +27 -23
- data/app/assets/javascripts/thredded/components/quote_post.es6 +5 -1
- data/app/assets/javascripts/thredded/components/time_stamps.es6 +24 -9
- data/app/assets/javascripts/thredded/components/topic_form.es6 +72 -54
- data/app/assets/javascripts/thredded/components/topics.es6 +28 -19
- data/app/assets/javascripts/thredded/components/turboforms.es6 +23 -13
- data/app/assets/javascripts/thredded/components/user_preferences_form.es6 +33 -31
- data/app/assets/javascripts/thredded/components/user_textcomplete.es6 +47 -0
- data/app/assets/javascripts/thredded/components/users_select.es6 +102 -52
- data/app/assets/javascripts/thredded/core/debounce.es6 +1 -1
- data/app/assets/javascripts/thredded/core/escape_html.es6 +7 -0
- data/app/assets/javascripts/thredded/core/hide_soft_keyboard.es6 +1 -1
- data/app/assets/javascripts/thredded/core/on_page_load.es6 +1 -1
- data/app/assets/javascripts/thredded/core/serialize_form.es6 +9 -0
- data/app/assets/javascripts/thredded/dependencies.js +2 -5
- data/app/assets/javascripts/thredded/dependencies/textcomplete.js +1 -0
- data/app/assets/javascripts/thredded/dependencies/timeago.js +1 -0
- data/app/assets/javascripts/thredded/dependencies/ujs.js +1 -1
- data/app/assets/stylesheets/thredded/_dependencies.scss +0 -1
- data/app/assets/stylesheets/thredded/_thredded.scss +0 -1
- data/app/assets/stylesheets/thredded/components/_mention-autocomplete.scss +15 -2
- data/app/controllers/concerns/thredded/new_private_topic_params.rb +2 -2
- data/app/controllers/thredded/autocomplete_users_controller.rb +0 -1
- data/app/forms/thredded/private_topic_form.rb +46 -2
- data/app/helpers/thredded/application_helper.rb +12 -14
- data/app/views/thredded/private_topics/_form.html.erb +7 -6
- data/app/views/thredded/topics/_topic.html.erb +2 -2
- data/config/locales/pl.yml +1 -1
- data/lib/thredded.rb +1 -3
- data/lib/thredded/version.rb +1 -1
- data/vendor/assets/javascripts/textcomplete.min.js +1 -0
- metadata +12 -37
- data/app/assets/javascripts/thredded/core/mention_autocompletion.es6 +0 -54
- data/app/assets/javascripts/thredded/dependencies/jquery.js +0 -1
- data/app/assets/stylesheets/thredded/components/_select2.scss +0 -112
- data/vendor/assets/javascripts/jquery.textcomplete.js +0 -1488
@@ -1,8 +1,12 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
//= require thredded/core/on_page_load
|
2
|
+
//= require thredded/core/serialize_form
|
3
|
+
|
4
|
+
// Makes topics in the list appear read as soon as the topic link is clicked,
|
5
|
+
// iff the topic link leads to the last page of the topic.
|
6
|
+
(() => {
|
7
|
+
const Thredded = window.Thredded;
|
3
8
|
|
4
|
-
const
|
5
|
-
const TOPIC_LINK_SELECTOR = 'h1 a';
|
9
|
+
const COMPONENT_SELECTOR = '[data-thredded-topics]';
|
6
10
|
const TOPIC_UNREAD_CLASS = 'thredded--topic-unread';
|
7
11
|
const TOPIC_READ_CLASS = 'thredded--topic-read';
|
8
12
|
const POSTS_COUNT_SELECTOR = '.thredded--topics--posts-count';
|
@@ -17,21 +21,26 @@
|
|
17
21
|
return Math.ceil(numPosts / POSTS_PER_PAGE);
|
18
22
|
}
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
function getTopicNode(node) {
|
25
|
+
do {
|
26
|
+
node = node.parentNode;
|
27
|
+
} while (node && node.tagName !== 'ARTICLE');
|
28
|
+
return node;
|
29
|
+
}
|
30
|
+
|
31
|
+
function initTopicsList(topicsList) {
|
32
|
+
topicsList.addEventListener('click', (evt) => {
|
33
|
+
const link = evt.target;
|
34
|
+
if (link.tagName !== 'A' || link.parentNode.tagName !== 'H1') return;
|
35
|
+
const topic = getTopicNode(link);
|
36
|
+
if (pageNumber(link.href) === totalPages(+topic.querySelector(POSTS_COUNT_SELECTOR).textContent)) {
|
37
|
+
topic.classList.add(TOPIC_READ_CLASS);
|
38
|
+
topic.classList.remove(TOPIC_UNREAD_CLASS);
|
39
|
+
}
|
40
|
+
});
|
29
41
|
}
|
30
42
|
|
31
|
-
|
32
|
-
|
33
|
-
if ($nodes.length) {
|
34
|
-
new ThreddedTopics().init($nodes);
|
35
|
-
}
|
43
|
+
Thredded.onPageLoad(() => {
|
44
|
+
Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), initTopicsList);
|
36
45
|
});
|
37
|
-
})(
|
46
|
+
})();
|
@@ -1,15 +1,25 @@
|
|
1
|
+
//= require thredded/core/on_page_load
|
2
|
+
//= require thredded/core/serialize_form
|
3
|
+
|
1
4
|
// Submit GET forms with turbolinks
|
2
|
-
(
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
// Turbolinks. Hide it:
|
11
|
-
window.Thredded.hideSoftKeyboard();
|
12
|
-
});
|
5
|
+
(() => {
|
6
|
+
const Thredded = window.Thredded;
|
7
|
+
const Turbolinks = window.Turbolinks;
|
8
|
+
|
9
|
+
Thredded.onPageLoad(() => {
|
10
|
+
if (!Turbolinks || !Turbolinks.supported) return;
|
11
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-thredded-turboform]'), (form) => {
|
12
|
+
form.addEventListener('submit', handleSubmit);
|
13
13
|
});
|
14
|
-
}
|
15
|
-
|
14
|
+
});
|
15
|
+
|
16
|
+
const handleSubmit = (evt) => {
|
17
|
+
evt.preventDefault();
|
18
|
+
const form = evt.currentTarget;
|
19
|
+
Turbolinks.visit(form.action + (form.action.indexOf('?') === -1 ? '?' : '&') + Thredded.serializeForm(form));
|
20
|
+
|
21
|
+
// On mobile the soft keyboard doesn't won't go away after the submit since we're submitting with
|
22
|
+
// Turbolinks. Hide it:
|
23
|
+
Thredded.hideSoftKeyboard();
|
24
|
+
};
|
25
|
+
})();
|
@@ -1,64 +1,66 @@
|
|
1
|
-
|
1
|
+
//= require thredded/core/on_page_load
|
2
|
+
|
3
|
+
// Reflects the logic of user preference settings by enabling/disabling certain inputs.
|
4
|
+
(() => {
|
5
|
+
const Thredded = window.Thredded;
|
6
|
+
|
2
7
|
const COMPONENT_SELECTOR = '[data-thredded-user-preferences-form]';
|
3
8
|
const BOUND_MESSAGEBOARD_NAME = 'data-thredded-bound-messageboard-pref';
|
4
9
|
const UPDATE_ON_CHANGE_NAME = 'data-thredded-update-checkbox-on-change';
|
5
10
|
|
6
11
|
class MessageboardPreferenceBinding {
|
7
|
-
constructor(
|
8
|
-
this
|
9
|
-
this
|
10
|
-
|
12
|
+
constructor(form, genericCheckboxName, messageboardCheckboxName) {
|
13
|
+
this.messageboardCheckbox = form.querySelector(`[type="checkbox"][name="${messageboardCheckboxName}"]`);
|
14
|
+
if (!this.messageboardCheckbox) {
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
this.messageboardCheckbox.addEventListener('change', () => {
|
11
18
|
this.rememberMessageboardChecked();
|
12
19
|
});
|
13
20
|
this.rememberMessageboardChecked();
|
14
|
-
|
21
|
+
|
22
|
+
this.genericCheckbox = form.querySelector(`[type="checkbox"][name="${genericCheckboxName}"]`);
|
23
|
+
this.genericCheckbox.addEventListener('change', () => {
|
15
24
|
this.updateMessageboardCheckbox();
|
16
25
|
});
|
17
26
|
this.updateMessageboardCheckbox();
|
18
27
|
}
|
19
28
|
|
20
29
|
rememberMessageboardChecked() {
|
21
|
-
this.messageboardCheckedWas = this
|
30
|
+
this.messageboardCheckedWas = this.messageboardCheckbox.checked;
|
22
31
|
}
|
23
32
|
|
24
33
|
updateMessageboardCheckbox() {
|
25
|
-
const enabled = this
|
26
|
-
this
|
27
|
-
|
28
|
-
.filter(':checkbox').prop('checked', enabled ? this.messageboardCheckedWas : false);
|
34
|
+
const enabled = this.genericCheckbox.checked;
|
35
|
+
this.messageboardCheckbox.disabled = !enabled;
|
36
|
+
this.messageboardCheckbox.checked = enabled ? this.messageboardCheckedWas : false;
|
29
37
|
}
|
30
38
|
}
|
31
39
|
|
32
40
|
class UpdateOnChange {
|
33
|
-
constructor(
|
34
|
-
const
|
35
|
-
if (
|
36
|
-
|
37
|
-
|
41
|
+
constructor(form, sourceElement, targetName) {
|
42
|
+
const target = form.querySelector(`[type="checkbox"][name="${targetName}"]`);
|
43
|
+
if (!target) return;
|
44
|
+
sourceElement.addEventListener('change', () => {
|
45
|
+
target.checked = sourceElement.checked;
|
38
46
|
});
|
39
47
|
}
|
40
48
|
}
|
41
49
|
|
42
50
|
class UserPreferencesForm {
|
43
51
|
constructor(form) {
|
44
|
-
|
45
|
-
|
46
|
-
const $elem = $(element);
|
47
|
-
new MessageboardPreferenceBinding($form, $elem.attr('name'), $elem.attr(BOUND_MESSAGEBOARD_NAME));
|
52
|
+
Array.prototype.forEach.call(form.querySelectorAll(`input[${BOUND_MESSAGEBOARD_NAME}]`), (element) => {
|
53
|
+
new MessageboardPreferenceBinding(form, element.name, element.getAttribute(BOUND_MESSAGEBOARD_NAME));
|
48
54
|
});
|
49
|
-
|
50
|
-
|
51
|
-
new UpdateOnChange($form, $elem, $elem.attr(UPDATE_ON_CHANGE_NAME))
|
55
|
+
Array.prototype.forEach.call(form.querySelectorAll(`input[${UPDATE_ON_CHANGE_NAME}]`), (element) => {
|
56
|
+
new UpdateOnChange(form, element, element.getAttribute(UPDATE_ON_CHANGE_NAME));
|
52
57
|
});
|
53
58
|
}
|
54
59
|
}
|
55
60
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
new UserPreferencesForm(this);
|
61
|
-
});
|
62
|
-
}
|
61
|
+
Thredded.onPageLoad(() => {
|
62
|
+
Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (form) => {
|
63
|
+
new UserPreferencesForm(form);
|
64
|
+
});
|
63
65
|
});
|
64
|
-
})(
|
66
|
+
})();
|
@@ -0,0 +1,47 @@
|
|
1
|
+
//= require thredded/core/thredded
|
2
|
+
//= require thredded/core/escape_html
|
3
|
+
|
4
|
+
(() => {
|
5
|
+
const Thredded = window.Thredded;
|
6
|
+
|
7
|
+
Thredded.UserTextcomplete = {
|
8
|
+
DROPDOWN_CLASS_NAME: 'thredded--textcomplete-dropdown',
|
9
|
+
|
10
|
+
formatUser({avatar_url, name}) {
|
11
|
+
return "<div class='thredded--textcomplete-user-result'>" +
|
12
|
+
`<img class='thredded--textcomplete-user-result__avatar' src='${Thredded.escapeHtml(avatar_url)}' >` +
|
13
|
+
`<span class='thredded--textcomplete-user-result__name'>${Thredded.escapeHtml(name)}</span>` +
|
14
|
+
'</div>';
|
15
|
+
},
|
16
|
+
|
17
|
+
searchFn({url, autocompleteMinLength}) {
|
18
|
+
return function search(term, callback, match) {
|
19
|
+
if (term.length < autocompleteMinLength) {
|
20
|
+
callback([]);
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
const request = new XMLHttpRequest();
|
24
|
+
request.open('GET', `${url}?q=${term}`, /* async */ true);
|
25
|
+
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
26
|
+
request.onload = () => {
|
27
|
+
// Ignore errors
|
28
|
+
if (request.status < 200 || request.status >= 400) {
|
29
|
+
callback([]);
|
30
|
+
return;
|
31
|
+
}
|
32
|
+
callback(JSON.parse(request.responseText).results.map(({avatar_url, id, name}) => {
|
33
|
+
return {avatar_url, id, name, match};
|
34
|
+
}));
|
35
|
+
};
|
36
|
+
request.send();
|
37
|
+
}
|
38
|
+
}
|
39
|
+
};
|
40
|
+
|
41
|
+
document.addEventListener('turbolinks:before-cache', () => {
|
42
|
+
Array.prototype.forEach.call(
|
43
|
+
document.getElementsByClassName(Thredded.UserTextcomplete.DROPDOWN_CLASS_NAME), (node) => {
|
44
|
+
node.parentNode.removeChild(node);
|
45
|
+
});
|
46
|
+
});
|
47
|
+
})();
|
@@ -1,72 +1,122 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
//= require thredded/core/on_page_load
|
2
|
+
//= require thredded/components/user_textcomplete
|
3
|
+
//= require autosize
|
3
4
|
|
5
|
+
(() => {
|
6
|
+
const Thredded = window.Thredded;
|
7
|
+
const autosize = window.autosize;
|
4
8
|
|
5
|
-
|
6
|
-
if (user.loading) return user.text;
|
7
|
-
return "<div class='thredded--select2-user-result'>" +
|
8
|
-
`<img class='thredded--select2-user-result__avatar' src='${escapeHtml(user.avatar_url)}' >` +
|
9
|
-
`<span class='thredded--select2-user-result__name'>${escapeHtml(user.name)}</span>` +
|
10
|
-
'</div>';
|
11
|
-
};
|
9
|
+
const COMPONENT_SELECTOR = '[data-thredded-users-select]';
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
`<img class='thredded--select2-user-selection__avatar' src='${escapeHtml(user.avatar_url)}' >` +
|
16
|
-
`<span class='thredded--select2-user-selection__name'>${escapeHtml(user.name)}</span>` +
|
17
|
-
'</span>';
|
11
|
+
Thredded.UsersSelect = {
|
12
|
+
DROPDOWN_MAX_COUNT: 6,
|
18
13
|
};
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
15
|
+
function parseNames(text) {
|
16
|
+
const result = [];
|
17
|
+
let current = [];
|
18
|
+
let currentIndex = 0;
|
19
|
+
let inQuoted = false;
|
20
|
+
let inName = false;
|
21
|
+
for (let i = 0; i < text.length; ++i) {
|
22
|
+
const char = text.charAt(i);
|
23
|
+
switch (char) {
|
24
|
+
case '"':
|
25
|
+
inQuoted = !inQuoted;
|
26
|
+
break;
|
27
|
+
case ' ':
|
28
|
+
if (inName) current.push(char);
|
29
|
+
break;
|
30
|
+
case ',':
|
31
|
+
if (inQuoted) {
|
32
|
+
current.push(char);
|
33
|
+
} else {
|
34
|
+
inName = false;
|
35
|
+
if (current.length) {
|
36
|
+
result.push({name: current.join(''), index: currentIndex});
|
37
|
+
current.length = 0;
|
38
|
+
}
|
39
|
+
}
|
40
|
+
break;
|
41
|
+
default:
|
42
|
+
if (!inName) currentIndex = i;
|
43
|
+
inName = true;
|
44
|
+
current.push(char);
|
45
|
+
}
|
26
46
|
}
|
27
|
-
|
47
|
+
if (current.length) result.push({name: current.join(''), index: currentIndex});
|
48
|
+
return result;
|
49
|
+
}
|
28
50
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
51
|
+
const initUsersSelect = (textarea) => {
|
52
|
+
autosize(textarea);
|
53
|
+
// Prevent multiple lines
|
54
|
+
textarea.addEventListener('keypress', (evt) => {
|
55
|
+
if (evt.keyCode === 13 || evt.keyCode === 10) {
|
56
|
+
evt.preventDefault()
|
57
|
+
}
|
58
|
+
});
|
59
|
+
const editor = new Textcomplete.editors.Textarea(textarea);
|
60
|
+
const textcomplete = new Textcomplete(editor, {
|
61
|
+
dropdown: {
|
62
|
+
className: Thredded.UserTextcomplete.DROPDOWN_CLASS_NAME,
|
63
|
+
maxCount: Thredded.UsersSelect.DROPDOWN_MAX_COUNT,
|
37
64
|
},
|
38
|
-
containerCssClass: 'thredded--select2-container',
|
39
|
-
dropdownCssClass: 'thredded--select2-drop',
|
40
|
-
initSelection: initSelection,
|
41
|
-
minimumInputLength: $el.data('autocompleteMinLength'),
|
42
|
-
multiple: true,
|
43
|
-
formatResult: formatUser,
|
44
|
-
formatSelection: formatUserSelection
|
45
65
|
});
|
46
|
-
};
|
47
66
|
|
48
|
-
|
49
|
-
|
50
|
-
|
67
|
+
const searchFn = Thredded.UserTextcomplete.searchFn({
|
68
|
+
url: textarea.getAttribute('data-autocomplete-url'),
|
69
|
+
autocompleteMinLength: parseInt(textarea.getAttribute('data-autocomplete-min-length'), 10)
|
51
70
|
});
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
$(this).select2('destroy');
|
71
|
+
textcomplete.on('rendered', function() {
|
72
|
+
if (textcomplete.dropdown.items.length) {
|
73
|
+
textcomplete.dropdown.items[0].activate();
|
74
|
+
}
|
57
75
|
});
|
58
|
-
|
76
|
+
textcomplete.register([{
|
77
|
+
index: 0,
|
78
|
+
match: (text) => {
|
79
|
+
const names = parseNames(text);
|
80
|
+
if (names.length) {
|
81
|
+
const {name, index} = names[names.length - 1];
|
82
|
+
const matchData = [name];
|
83
|
+
matchData.index = index;
|
84
|
+
return matchData;
|
85
|
+
} else {
|
86
|
+
return null;
|
87
|
+
}
|
88
|
+
},
|
89
|
+
search (term, callback, match) {
|
90
|
+
searchFn(term, function(results) {
|
91
|
+
const names = parseNames(textarea.value).map(({name}) => name);
|
92
|
+
callback(results.filter((result) => names.indexOf(result.name) === -1));
|
93
|
+
}, match);
|
94
|
+
},
|
95
|
+
template: Thredded.UserTextcomplete.formatUser,
|
96
|
+
replace ({name}) {
|
97
|
+
if (/,/.test(name)) {
|
98
|
+
return `"${name}", `
|
99
|
+
} else {
|
100
|
+
return `${name}, `
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}]);
|
59
104
|
};
|
60
105
|
|
106
|
+
function destroyUsersSelect(textarea) {
|
107
|
+
autosize.destroy(textarea);
|
108
|
+
}
|
109
|
+
|
61
110
|
window.Thredded.onPageLoad(() => {
|
62
|
-
|
111
|
+
Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {
|
112
|
+
initUsersSelect(node);
|
113
|
+
});
|
63
114
|
});
|
64
115
|
|
65
116
|
document.addEventListener('turbolinks:before-cache', () => {
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
destroy()
|
117
|
+
Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {
|
118
|
+
destroyUsersSelect(node);
|
119
|
+
});
|
70
120
|
});
|
71
121
|
|
72
|
-
})(
|
122
|
+
})();
|
@@ -0,0 +1,9 @@
|
|
1
|
+
//= require thredded/core/thredded
|
2
|
+
|
3
|
+
window.Thredded.serializeForm = (form) => {
|
4
|
+
// Can't use new FormData(form).entries() because it's not supported on any IE
|
5
|
+
// The below is not a full replacement, but enough for Thredded's purposes.
|
6
|
+
return Array.prototype.map.call(form.querySelectorAll('[name]'), (e) => {
|
7
|
+
return `${encodeURIComponent(e.name)}=${encodeURIComponent(e.value)}`;
|
8
|
+
}).join('&');
|
9
|
+
};
|