thredded 0.12.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
};
|