thredded 0.12.4 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ };