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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -33
  3. data/app/assets/javascripts/thredded/components/currently_online.es6 +28 -21
  4. data/app/assets/javascripts/thredded/components/flash_messages.es6 +5 -7
  5. data/app/assets/javascripts/thredded/components/mention_autocompletion.es6 +39 -0
  6. data/app/assets/javascripts/thredded/components/post_form.es6 +28 -33
  7. data/app/assets/javascripts/thredded/components/preview_area.es6 +27 -23
  8. data/app/assets/javascripts/thredded/components/quote_post.es6 +5 -1
  9. data/app/assets/javascripts/thredded/components/time_stamps.es6 +24 -9
  10. data/app/assets/javascripts/thredded/components/topic_form.es6 +72 -54
  11. data/app/assets/javascripts/thredded/components/topics.es6 +28 -19
  12. data/app/assets/javascripts/thredded/components/turboforms.es6 +23 -13
  13. data/app/assets/javascripts/thredded/components/user_preferences_form.es6 +33 -31
  14. data/app/assets/javascripts/thredded/components/user_textcomplete.es6 +47 -0
  15. data/app/assets/javascripts/thredded/components/users_select.es6 +102 -52
  16. data/app/assets/javascripts/thredded/core/debounce.es6 +1 -1
  17. data/app/assets/javascripts/thredded/core/escape_html.es6 +7 -0
  18. data/app/assets/javascripts/thredded/core/hide_soft_keyboard.es6 +1 -1
  19. data/app/assets/javascripts/thredded/core/on_page_load.es6 +1 -1
  20. data/app/assets/javascripts/thredded/core/serialize_form.es6 +9 -0
  21. data/app/assets/javascripts/thredded/dependencies.js +2 -5
  22. data/app/assets/javascripts/thredded/dependencies/textcomplete.js +1 -0
  23. data/app/assets/javascripts/thredded/dependencies/timeago.js +1 -0
  24. data/app/assets/javascripts/thredded/dependencies/ujs.js +1 -1
  25. data/app/assets/stylesheets/thredded/_dependencies.scss +0 -1
  26. data/app/assets/stylesheets/thredded/_thredded.scss +0 -1
  27. data/app/assets/stylesheets/thredded/components/_mention-autocomplete.scss +15 -2
  28. data/app/controllers/concerns/thredded/new_private_topic_params.rb +2 -2
  29. data/app/controllers/thredded/autocomplete_users_controller.rb +0 -1
  30. data/app/forms/thredded/private_topic_form.rb +46 -2
  31. data/app/helpers/thredded/application_helper.rb +12 -14
  32. data/app/views/thredded/private_topics/_form.html.erb +7 -6
  33. data/app/views/thredded/topics/_topic.html.erb +2 -2
  34. data/config/locales/pl.yml +1 -1
  35. data/lib/thredded.rb +1 -3
  36. data/lib/thredded/version.rb +1 -1
  37. data/vendor/assets/javascripts/textcomplete.min.js +1 -0
  38. metadata +12 -37
  39. data/app/assets/javascripts/thredded/core/mention_autocompletion.es6 +0 -54
  40. data/app/assets/javascripts/thredded/dependencies/jquery.js +0 -1
  41. data/app/assets/stylesheets/thredded/components/_select2.scss +0 -112
  42. data/vendor/assets/javascripts/jquery.textcomplete.js +0 -1488
@@ -1,8 +1,12 @@
1
- (($) => {
2
- const COMPONENT_SELECTOR = '[data-thredded-topics]';
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 TOPIC_SELECTOR = 'article';
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
- class ThreddedTopics {
21
- init($nodes) {
22
- $nodes.on('click', TOPIC_LINK_SELECTOR, (evt) => {
23
- const $topic = $(evt.target).closest(TOPIC_SELECTOR);
24
- if (pageNumber($topic.find('a').prop('href')) == totalPages(+$topic.find(POSTS_COUNT_SELECTOR).text())) {
25
- $topic.addClass(TOPIC_READ_CLASS).removeClass(TOPIC_UNREAD_CLASS);
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
- window.Thredded.onPageLoad(() => {
32
- const $nodes = $(COMPONENT_SELECTOR);
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
- })(jQuery);
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
- (function($) {
3
- if (window.Turbolinks && window.Turbolinks.supported) {
4
- window.Thredded.onPageLoad(() => {
5
- $('[data-thredded-turboform]').on('submit', function(evt) {
6
- evt.preventDefault();
7
- Turbolinks.visit(this.action + (this.action.indexOf('?') === -1 ? '?' : '&') + $(this).serialize());
8
-
9
- // On mobile the soft keyboard doesn't won't go away after the submit since we're submitting with
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
- })(jQuery);
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($form, genericCheckboxName, messageboardCheckboxName) {
8
- this.$genericCheckbox = $form.find(`:checkbox[name="${genericCheckboxName}"]`);
9
- this.$messageboardCheckbox = $form.find(`:checkbox[name="${messageboardCheckboxName}"]`);
10
- this.$messageboardCheckbox.on('change', () => {
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
- this.$genericCheckbox.on('change', () => {
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.$messageboardCheckbox.filter(':checkbox').prop('checked');
30
+ this.messageboardCheckedWas = this.messageboardCheckbox.checked;
22
31
  }
23
32
 
24
33
  updateMessageboardCheckbox() {
25
- const enabled = this.$genericCheckbox.prop('checked');
26
- this.$messageboardCheckbox
27
- .prop('disabled', !enabled)
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($form, $sourceElement, targetName) {
34
- const $target = $form.find(`:checkbox[name="${targetName}"]`);
35
- if (!$target.length) return;
36
- $sourceElement.on('change', () => {
37
- $target.prop('checked', $sourceElement.prop('checked'));
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
- const $form = $(form);
45
- $form.find(`input[${BOUND_MESSAGEBOARD_NAME}]`).each((index, element) => {
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
- $form.find(`input[${UPDATE_ON_CHANGE_NAME}]`).each((index, element) => {
50
- const $elem = $(element);
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
- window.Thredded.onPageLoad(() => {
57
- const $forms = $(COMPONENT_SELECTOR);
58
- if ($forms.length) {
59
- $forms.each(function() {
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
- })(jQuery);
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
- const COMPONENT_SELECTOR = '[data-thredded-users-select]';
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
- let formatUser = (user, container, query, escapeHtml) => {
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
- let formatUserSelection = (user, container, escapeHtml) => {
14
- return `<span class='thredded--select2-user-selection'>` +
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
- let initSelection = ($el, callback) => {
21
- let ids = ($el.val() || '').split(',');
22
- if (ids.length && ids[0] != '') {
23
- $.ajax(`${$el.data('autocompleteUrl')}?ids=${ids.join(',')}`, {dataType: 'json'}).done(data => callback(data.results));
24
- } else {
25
- callback([]);
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
- let initOne = $el => {
30
- $el.select2({
31
- ajax: {
32
- cache: true,
33
- data: query => ({q: query}),
34
- results: data => data,
35
- dataType: 'json',
36
- url: $el.data('autocompleteUrl')
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
- let init = () => {
49
- $(COMPONENT_SELECTOR).each(function() {
50
- initOne($(this));
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
- let destroy = () => {
55
- $(COMPONENT_SELECTOR).each(function() {
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
- $('.select2-drop, .select2-drop-mask').remove();
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
- init()
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
- // Turbolinks 5 clones the body node for caching, losing all the bound
67
- // events. Undo the select2 transformation before storing to cache,
68
- // so that it applies cleanly on restore.
69
- destroy()
117
+ Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {
118
+ destroyUsersSelect(node);
119
+ });
70
120
  });
71
121
 
72
- })(jQuery);
122
+ })();
@@ -1,4 +1,4 @@
1
- //= require ./thredded
1
+ //= require thredded/core/thredded
2
2
 
3
3
  /**
4
4
  * Return a function, that, as long as it continues to be invoked, will
@@ -0,0 +1,7 @@
1
+ //= require thredded/core/thredded
2
+
3
+ window.Thredded.escapeHtml = function(text) {
4
+ const node = document.createElement('div');
5
+ node.textContent = text;
6
+ return node.innerHTML;
7
+ };
@@ -1,4 +1,4 @@
1
- //= require ./thredded
1
+ //= require thredded/core/thredded
2
2
 
3
3
  window.Thredded.hideSoftKeyboard = () => {
4
4
  const activeElement = document.activeElement;
@@ -1,4 +1,4 @@
1
- //= require ./thredded
1
+ //= require thredded/core/thredded
2
2
 
3
3
  (() => {
4
4
  const isTurbolinks = 'Turbolinks' in window && window.Turbolinks.supported;
@@ -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
+ };