From 4ec646842397f2edaed407be4547ab153069dc92 Mon Sep 17 00:00:00 2001 From: James Beard Date: Sun, 5 Apr 2026 23:35:00 +1000 Subject: [PATCH 1/7] Low hanging fruit tidy up. Define a few implicitly defined variables. Remainder of changes are prettier standardising on double quotes, indenting, and wrapping a few long lines. --- static/js/main.js | 76 ++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/static/js/main.js b/static/js/main.js index 61ae35a..9adb602 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -11,6 +11,10 @@ $(document).ready(function () { var $save_video_form_button; var $post_video_form; var $post_video_form_button; + let $upload_image_input; + let $link_to_video; + let $video_shake_id; + let $shake_selector; var init_dom = function () { $new_post_panel = $("#new-post-panel"); @@ -336,7 +340,7 @@ $(document).ready(function () { // when we hit enter on a form, we want to submit it // even though we don't have an type="submit" input // available, since we're using a styled button. - $sign_in_form = $("#sign-in-form"); + const $sign_in_form = $("#sign-in-form"); $("input", $sign_in_form).keydown(function (e) { if (e.keyCode == 13) { $sign_in_form.submit(); @@ -1494,7 +1498,7 @@ $(document).ready(function () { }); var init_notification_invitation_request = function () { - $notification_invitation_request = $( + const $notification_invitation_request = $( "#notification-block-invitation-request", ); if ($notification_invitation_request.length > 0) { @@ -1523,7 +1527,7 @@ $(document).ready(function () { }); /* Site Nav dropdown */ - $site_nav = $("#site-nav"); + const $site_nav = $("#site-nav"); var site_nav_expanded = false; $("#site-nav .site-nav--toggle").click(function (event) { event.stopPropagation(); @@ -1546,7 +1550,7 @@ $(document).ready(function () { }); /* Choose a shake dropdown */ - $choose_a_shake = $("#choose-a-shake"); + const $choose_a_shake = $("#choose-a-shake"); var shake_expanded = false; $("#choose-a-shake .choose-a-shake--toggle").click(function (event) { event.stopPropagation(); @@ -1877,17 +1881,26 @@ $(document).ready(function () { if ("views" in result) { $root .find(".views") - .attr("title", UserCounts.format(result["views"]) + " views") + .attr( + "title", + UserCounts.format(result["views"]) + " views", + ) .find(".num") .html(UserCounts.formatBrief(result["views"])); $root .find(".saves") - .attr("title", UserCounts.format(result["saves"]) + " saves") + .attr( + "title", + UserCounts.format(result["saves"]) + " saves", + ) .find(".num") .html(UserCounts.formatBrief(result["saves"])); $root .find(".likes") - .attr("title", UserCounts.format(result["likes"]) + " likes") + .attr( + "title", + UserCounts.format(result["likes"]) + " likes", + ) .find(".num") .html(UserCounts.formatBrief(result["likes"])); } @@ -1899,12 +1912,12 @@ $(document).ready(function () { // Format number in a way that won't need excessive space to // display. Abbreviate with suffixes and limit to one // decimal place. - + const n = parseInt(str_num, 10); - + // Handle garbage input (somewhat) gracefully. if (Number.isNaN(n)) { - return '0'; + return "0"; } // Anything up to and including 9,999 return verbatim as @@ -1912,12 +1925,12 @@ $(document).ready(function () { if (n < 10000) { return n.toLocaleString(); } - + const suffixes = [ - { threshold: 1e12, suffix: 'T' }, - { threshold: 1e9, suffix: 'B' }, - { threshold: 1e6, suffix: 'M' }, - { threshold: 1e3, suffix: 'K' } + { threshold: 1e12, suffix: "T" }, + { threshold: 1e9, suffix: "B" }, + { threshold: 1e6, suffix: "M" }, + { threshold: 1e3, suffix: "K" }, ]; for (const { threshold, suffix } of suffixes) { @@ -1925,8 +1938,9 @@ $(document).ready(function () { // value. if (n >= threshold) { // Truncate to 1 decimal place. - const truncated = Math.floor((n / threshold) * 10) / 10; - + const truncated = + Math.floor((n / threshold) * 10) / 10; + // If the decimal part is 0 trim it. if (truncated % 1 === 0) { return truncated.toFixed(0) + suffix; @@ -1935,7 +1949,7 @@ $(document).ready(function () { } } } - + return n.toLocaleString(); }, }; @@ -2190,33 +2204,33 @@ $(document).ready(function () { } // Support for sticky site header - const $siteHeader = $('.site-header'); + const $siteHeader = $(".site-header"); if ($siteHeader.length > 0) { let lastScrollY = window.scrollY; const scrollHandler = () => { - if (!$siteHeader.hasClass('docked')) { + if (!$siteHeader.hasClass("docked")) { if (window.scrollY > 120) { - $siteHeader.addClass('docked'); + $siteHeader.addClass("docked"); } } else { if (window.scrollY <= 120) { - $siteHeader.removeClass('docked visible hidden'); + $siteHeader.removeClass("docked visible hidden"); } } - if ($siteHeader.hasClass('docked')) { - const isVisible = $siteHeader.hasClass('visible'); + if ($siteHeader.hasClass("docked")) { + const isVisible = $siteHeader.hasClass("visible"); if (!isVisible && window.scrollY < lastScrollY) { - // triggering delta can be 1px - $siteHeader.addClass('visible'); - $siteHeader.removeClass('hidden'); + // triggering delta can be 1px + $siteHeader.addClass("visible"); + $siteHeader.removeClass("hidden"); } else if (isVisible && window.scrollY > lastScrollY + 20) { - // triggering delta must be ~20px - $siteHeader.removeClass('visible'); - $siteHeader.addClass('hidden'); + // triggering delta must be ~20px + $siteHeader.removeClass("visible"); + $siteHeader.addClass("hidden"); } } lastScrollY = window.scrollY; }; - $(window).on('scroll', scrollHandler); + $(window).on("scroll", scrollHandler); } }); From 7732108165cd90b769679b3d11bf4eecefe19f13 Mon Sep 17 00:00:00 2001 From: James Beard Date: Sun, 5 Apr 2026 23:53:06 +1000 Subject: [PATCH 2/7] Pulled several jquery prototype extension "classes" out into seperate files in anticipation of turning them into real classes. Not entirely necessary, though should help with making sure decoupled from main code. Code only copied verbatim in this commit. --- static/js/NSFWCover.js | 55 ++ static/js/NotificationInvitationContainer.js | 29 + static/js/NotificationInvitationRequest.js | 50 ++ static/js/PermalinkCommentsView.js | 44 ++ static/js/RecommendedShakeCategory.js | 36 + static/js/RequestInvitation.js | 30 + static/js/SaveThisView.js | 121 ++++ static/js/ShakeMemberList.js | 35 + static/js/StreamStatsView.js | 234 +++++++ static/js/main.js | 666 ------------------- 10 files changed, 634 insertions(+), 666 deletions(-) create mode 100644 static/js/NSFWCover.js create mode 100644 static/js/NotificationInvitationContainer.js create mode 100644 static/js/NotificationInvitationRequest.js create mode 100644 static/js/PermalinkCommentsView.js create mode 100644 static/js/RecommendedShakeCategory.js create mode 100644 static/js/RequestInvitation.js create mode 100644 static/js/SaveThisView.js create mode 100644 static/js/ShakeMemberList.js create mode 100644 static/js/StreamStatsView.js diff --git a/static/js/NSFWCover.js b/static/js/NSFWCover.js new file mode 100644 index 0000000..281ebce --- /dev/null +++ b/static/js/NSFWCover.js @@ -0,0 +1,55 @@ +var NSFWCover = function ($root) { + this.$root = $root; + this.init(); +}; + +$.extend(NSFWCover.prototype, { + init: function () { + this.$root.delegate("a", "click", $.proxy(this.click_show_image, this)); + }, + + click_show_image: function (ev) { + var location = document.location, + host = location.host, + protocol = location.protocol, + base_path = location.protocol + "//" + location.host, + file_path = $(ev.target).attr("href"); + $.get( + base_path + + "/services/oembed?include_embed=1&url=" + + escape(base_path + file_path), + $.proxy(this.load_image, this), + "json", + ); + return false; + }, + + load_image: function (response) { + var parent = this.$root.parent(), + parent_height = parent.height(); + + if (response["type"] === "photo") { + parent + .css("min-height", parent_height + "px") + .html(''); + } else if (response["embed_html"]) { + parent + .css("min-height", parent_height + "px") + .html( + '
' + + response["embed_html"] + + "
", + ); + } else if (response["type"] === "video") { + var content = parent + .css("min-height", parent_height + "px") + .html( + response["html"].replace( + /Ok! Request sent."); + }, +}); diff --git a/static/js/SaveThisView.js b/static/js/SaveThisView.js new file mode 100644 index 0000000..2044d51 --- /dev/null +++ b/static/js/SaveThisView.js @@ -0,0 +1,121 @@ +var SaveThisView = function (container) { + this.$save_this = $(container); + this.init(); +}; + +$.extend(SaveThisView.prototype, { + init: function () { + this.init_dom(); + this.init_events(); + }, + + init_dom: function () { + this.$save_this_link = this.$save_this.find(".save-this-link"); + this.$form = this.$save_this.find("form"); + this.$shake_id_input = this.$save_this.find(".shake-id-input"); + this.$shake_selector = $( + "
", + ); + }, + + init_events: function () { + this.$save_this_link.click($.proxy(this.click_save_this, this)); + this.$save_this.delegate( + ".shake-link", + "click", + $.proxy(this.click_choose_shake, this), + ); + this.$save_this.delegate( + ".close", + "click", + $.proxy(this.click_close_selector, this), + ); + }, + + click_save_this: function (ev) { + ev.stopPropagation(); + if (this.$save_this_link.hasClass("save-this-link-multiple")) { + this.show_shake_selector(); + } else { + this.submit_image_save(); + } + return false; + }, + + click_choose_shake: function (ev) { + ev.stopPropagation(); + var shake_id = ev.target.id.replace(/[^\d]+/, ""); + this.$shake_id_input.val(shake_id); + this.submit_image_save(); + return false; + }, + + click_close_selector: function (ev) { + ev.stopPropagation(); + this.$shake_selector.remove(); + }, + + show_shake_selector: function () { + this.$save_this.append(this.$shake_selector); + $("body").one("click", $.proxy(this.click_close_selector, this)); + + // Only query once per page. + if (ShakesCache.fetch() !== false) { + this.fetch_available_shakes(ShakesCache.fetch()); + } else { + $.get( + "/account/shakes", + $.proxy(this.fetch_available_shakes, this), + "json", + ); + } + }, + + fetch_available_shakes: function (response) { + ShakesCache.store(response); + var html = '"; + this.$shake_selector + .removeClass("save-this-shake-selector-loading") + .html(html); + }, + + submit_image_save: function (ev) { + var url = this.$form.attr("action"); + var data = this.$form.serialize(); + $.post( + url, + data, + $.proxy(this.process_image_save_response, this), + "json", + ); + }, + + process_image_save_response: function (response) { + if (response["share_key"]) { + var count = response["count"]; + var share_key = response["share_key"]; + var new_share_key = response["new_share_key"]; + var count_string = to_text(count, "Save"); + $("#save-count-amount-" + share_key).html(count_string); + var output = + ''; + this.$shake_selector.remove(); + this.$save_this.html(output); + SidebarStatsView.refresh_saves(); + StreamStatsViewRegistry.refresh_saves(share_key); + } else { + return false; + } + }, +}); diff --git a/static/js/ShakeMemberList.js b/static/js/ShakeMemberList.js new file mode 100644 index 0000000..6b48d8d --- /dev/null +++ b/static/js/ShakeMemberList.js @@ -0,0 +1,35 @@ +var ShakeMemberList = function ($root) { + this.$root = $root; + this.init_events(); +}; + +$.extend(ShakeMemberList.prototype, { + init_events: function () { + this.$root.delegate( + ".remove-from-shake-button-link", + "click", + $.proxy(this.remove_from_shake, this), + ); + }, + + remove_from_shake: function (ev) { + var $target = $(ev.target), + $li = $target.parents("li"), + $form = $target.next(), + url = $form.attr("action"); + data = $form.serialize(); + + if ( + confirm( + "Are you sure you want to remove this user from a shake? If they have notifications on an email will be sent informing them of the change.", + ) + ) { + $.post(url, data, $.proxy(this.process_remove, $li)); + } + return false; + }, + + process_remove: function (response) { + this.remove(); + }, +}); diff --git a/static/js/StreamStatsView.js b/static/js/StreamStatsView.js new file mode 100644 index 0000000..ea684f5 --- /dev/null +++ b/static/js/StreamStatsView.js @@ -0,0 +1,234 @@ +var StreamStatsView = function ($image_content_footer) { + this.$image_content_footer = $image_content_footer; + this.share_key = this.$image_content_footer + .attr("id") + .replace("image-content-footer-", ""); + this.can_submit_comments = true; + this.init_dom(); + this.init_events(); +}; + +$.extend(StreamStatsView.prototype, { + init_dom: function () { + this.$likes_button = this.$image_content_footer.find(".likes"); + this.$saves_button = this.$image_content_footer.find(".saves"); + this.$comments_button = this.$image_content_footer.find(".comments"); + this.$inline_details = + this.$image_content_footer.find(".inline-details"); + this.init_comment_dom(); + }, + + init_comment_dom: function () { + this.$post_comment_inline = this.$inline_details.find( + ".post-comment-inline", + ); + this.$comment_form = this.$inline_details.find(".post-comment-form"); + this.$comment_textarea = this.$inline_details.find("textarea"); + this.$submit_comment_button = this.$inline_details.find( + ".submit-comment-button", + ); + this.$show_more_comments = this.$inline_details.find( + ".show-more-comments", + ); + this.$comment = this.$inline_details.find(".comment"); + this.$reply_to = this.$inline_details.find(".reply-to"); + this.$delete = this.$inline_details.find(".delete"); + }, + + init_events: function () { + this.$likes_button.click($.proxy(this.click_like, this)); + this.$saves_button.click($.proxy(this.click_saves, this)); + this.$comments_button.click($.proxy(this.click_comments, this)); + this.init_comment_events(); + }, + + init_comment_events: function () { + this.$comment_textarea.click( + $.proxy(this.click_comment_textarea, this), + ); + this.$show_more_comments.click($.proxy(this.click_more_comments, this)); + this.$reply_to.click($.proxy(this.click_reply_to, this)); + this.$delete.click($.proxy(this.click_delete, this)); + // Fix for Webkit bug where textarea looses focus incorrectly on mouseup. + // http://code.google.com/p/chromium/issues/detail?id=4505 + this.$comment_textarea.mouseup(function (e) { + e.preventDefault(); + }); + this.$comment_form.submit($.proxy(this.submit_comment, this)); + }, + + // Removes selected state from all tabs. + clear_tab_selection: function () { + this.$likes_button.removeClass("selected"); + this.$saves_button.removeClass("selected"); + this.$comments_button.removeClass("selected"); + }, + + // Start the "loading" state transition. + start_loading: function () { + this.$inline_details.addClass("inline-details-loading").html("").show(); + }, + + user_html: function (data) { + var html = ""; + for (var i = 0; i < data.result.length; i++) { + var result = data.result[i]; + var link; + if (result["action"] == "save") { + link = result["post_url"]; + } else { + link = "/user/" + result["user_name"]; + } + html += + '' + + '' + + '' + + result["user_name"] + + ""; + } + return html; + }, + + click_like: function () { + if (this.$likes_button.hasClass("selected")) { + this.clear_tab_selection(); + this.$inline_details.hide(); + return false; + } + this.clear_tab_selection(); + this.$likes_button.addClass("selected"); + this.start_loading(); + this.load_likes(); + return false; + }, + + load_likes: function () { + var url = "/p/" + this.share_key + "/likes"; + $.get(url, $.proxy(this.process_like_response, this), "json"); + }, + + process_like_response: function (data) { + var html = + '
' + this.user_html(data) + "
"; + this.$inline_details.html(html); + }, + + click_saves: function () { + if (this.$saves_button.hasClass("selected")) { + this.clear_tab_selection(); + this.$inline_details.hide(); + return false; + } + + this.clear_tab_selection(); + this.$saves_button.addClass("selected"); + this.start_loading(); + this.load_saves(); + return false; + }, + + load_saves: function () { + var url = "/p/" + this.share_key + "/saves"; + $.get(url, $.proxy(this.process_like_response, this), "json"); + }, + + click_comments: function () { + if (this.$comments_button.hasClass("selected")) { + this.clear_tab_selection(); + this.$inline_details.hide(); + return false; + } + + this.clear_tab_selection(); + this.$comments_button.addClass("selected"); + this.start_loading(); + var url = "/p/" + this.share_key + "/quick-comments"; + $.get(url, $.proxy(this.process_comments_response, this), "json"); + return false; + }, + + click_more_comments: function () { + this.$comment.show(); + this.$show_more_comments.hide(); + return false; + }, + + process_comments_response: function (data) { + this.can_submit_comments = true; + if (data["result"] == "ok") { + this.$comments_button.find("a").html(data["count"]); + this.$inline_details.html(data["html"]); + this.init_comment_dom(); + this.init_comment_events(); + } + }, + + click_reply_to: function (ev) { + this.click_comment_textarea(); + var username = $(ev.target) + .parents(".comment") + .find(".username") + .html(); + var username_clean = username.replace(/[^a-zA-Z0-9_\-]+/g, ""); + var current_text = this.$comment_textarea.val(); + this.$comment_textarea.val(current_text + "@" + username_clean + " "); + setCaret(this.$comment_textarea.get(0)); + return false; + }, + + click_delete: function (ev) { + var $delete_form = $("#" + ev.target.id + "-form"), + url = $delete_form.attr("action"), + data = $delete_form.serialize(); + if (confirm("Are you sure you want to delete this?")) { + $.post( + url, + data, + $.proxy(this.process_comments_response, this), + "json", + ); + } + return false; + }, + + submit_comment: function () { + if (this.can_submit_comments === false) { + return false; + } + this.can_submit_comments = false; + var url = this.$comment_form.attr("action"); + var data = this.$comment_form.serialize(); + $.post( + url, + data, + $.proxy(this.process_comments_response, this), + "json", + ); + return false; + }, + + click_comment_textarea: function (e) { + this.$post_comment_inline.addClass("post-comment-inline-expanded"); + if (this.$comment_textarea.val().indexOf("Write a comment") === 0) { + this.$comment_textarea.val(""); + } + this.click_more_comments(); + this.$comment_textarea.css("min-height", "60px"); + }, + + refresh_likes: function () { + if (this.$likes_button.hasClass("selected")) { + this.load_likes(); + } + }, + + refresh_saves: function () { + if (this.$saves_button.hasClass("selected")) { + this.load_saves(); + } + }, +}); diff --git a/static/js/main.js b/static/js/main.js index 9adb602..01d0398 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -205,128 +205,6 @@ $(document).ready(function () { }, }; - var SaveThisView = function (container) { - this.$save_this = $(container); - this.init(); - }; - - $.extend(SaveThisView.prototype, { - init: function () { - this.init_dom(); - this.init_events(); - }, - - init_dom: function () { - this.$save_this_link = this.$save_this.find(".save-this-link"); - this.$form = this.$save_this.find("form"); - this.$shake_id_input = this.$save_this.find(".shake-id-input"); - this.$shake_selector = $( - "
", - ); - }, - - init_events: function () { - this.$save_this_link.click($.proxy(this.click_save_this, this)); - this.$save_this.delegate( - ".shake-link", - "click", - $.proxy(this.click_choose_shake, this), - ); - this.$save_this.delegate( - ".close", - "click", - $.proxy(this.click_close_selector, this), - ); - }, - - click_save_this: function (ev) { - ev.stopPropagation(); - if (this.$save_this_link.hasClass("save-this-link-multiple")) { - this.show_shake_selector(); - } else { - this.submit_image_save(); - } - return false; - }, - - click_choose_shake: function (ev) { - ev.stopPropagation(); - var shake_id = ev.target.id.replace(/[^\d]+/, ""); - this.$shake_id_input.val(shake_id); - this.submit_image_save(); - return false; - }, - - click_close_selector: function (ev) { - ev.stopPropagation(); - this.$shake_selector.remove(); - }, - - show_shake_selector: function () { - this.$save_this.append(this.$shake_selector); - $("body").one("click", $.proxy(this.click_close_selector, this)); - - // Only query once per page. - if (ShakesCache.fetch() !== false) { - this.fetch_available_shakes(ShakesCache.fetch()); - } else { - $.get( - "/account/shakes", - $.proxy(this.fetch_available_shakes, this), - "json", - ); - } - }, - - fetch_available_shakes: function (response) { - ShakesCache.store(response); - var html = '"; - this.$shake_selector - .removeClass("save-this-shake-selector-loading") - .html(html); - }, - - submit_image_save: function (ev) { - var url = this.$form.attr("action"); - var data = this.$form.serialize(); - $.post( - url, - data, - $.proxy(this.process_image_save_response, this), - "json", - ); - }, - - process_image_save_response: function (response) { - if (response["share_key"]) { - var count = response["count"]; - var share_key = response["share_key"]; - var new_share_key = response["new_share_key"]; - var count_string = to_text(count, "Save"); - $("#save-count-amount-" + share_key).html(count_string); - var output = - ''; - this.$shake_selector.remove(); - this.$save_this.html(output); - SidebarStatsView.refresh_saves(); - StreamStatsViewRegistry.refresh_saves(share_key); - } else { - return false; - } - }, - }); - function screen_reader_focus(el) { el.setAttribute("tabindex", "0"); el.blur(); @@ -869,252 +747,6 @@ $(document).ready(function () { })("#sidebar-stats"); SidebarStatsView.init(); - var StreamStatsView = function ($image_content_footer) { - this.$image_content_footer = $image_content_footer; - this.share_key = this.$image_content_footer - .attr("id") - .replace("image-content-footer-", ""); - this.can_submit_comments = true; - this.init_dom(); - this.init_events(); - }; - - $.extend(StreamStatsView.prototype, { - init_dom: function () { - this.$likes_button = this.$image_content_footer.find(".likes"); - this.$saves_button = this.$image_content_footer.find(".saves"); - this.$comments_button = - this.$image_content_footer.find(".comments"); - this.$inline_details = - this.$image_content_footer.find(".inline-details"); - this.init_comment_dom(); - }, - - init_comment_dom: function () { - this.$post_comment_inline = this.$inline_details.find( - ".post-comment-inline", - ); - this.$comment_form = - this.$inline_details.find(".post-comment-form"); - this.$comment_textarea = this.$inline_details.find("textarea"); - this.$submit_comment_button = this.$inline_details.find( - ".submit-comment-button", - ); - this.$show_more_comments = this.$inline_details.find( - ".show-more-comments", - ); - this.$comment = this.$inline_details.find(".comment"); - this.$reply_to = this.$inline_details.find(".reply-to"); - this.$delete = this.$inline_details.find(".delete"); - }, - - init_events: function () { - this.$likes_button.click($.proxy(this.click_like, this)); - this.$saves_button.click($.proxy(this.click_saves, this)); - this.$comments_button.click($.proxy(this.click_comments, this)); - this.init_comment_events(); - }, - - init_comment_events: function () { - this.$comment_textarea.click( - $.proxy(this.click_comment_textarea, this), - ); - this.$show_more_comments.click( - $.proxy(this.click_more_comments, this), - ); - this.$reply_to.click($.proxy(this.click_reply_to, this)); - this.$delete.click($.proxy(this.click_delete, this)); - // Fix for Webkit bug where textarea looses focus incorrectly on mouseup. - // http://code.google.com/p/chromium/issues/detail?id=4505 - this.$comment_textarea.mouseup(function (e) { - e.preventDefault(); - }); - this.$comment_form.submit($.proxy(this.submit_comment, this)); - }, - - // Removes selected state from all tabs. - clear_tab_selection: function () { - this.$likes_button.removeClass("selected"); - this.$saves_button.removeClass("selected"); - this.$comments_button.removeClass("selected"); - }, - - // Start the "loading" state transition. - start_loading: function () { - this.$inline_details - .addClass("inline-details-loading") - .html("") - .show(); - }, - - user_html: function (data) { - var html = ""; - for (var i = 0; i < data.result.length; i++) { - var result = data.result[i]; - var link; - if (result["action"] == "save") { - link = result["post_url"]; - } else { - link = "/user/" + result["user_name"]; - } - html += - '' + - '' + - '' + - result["user_name"] + - ""; - } - return html; - }, - - click_like: function () { - if (this.$likes_button.hasClass("selected")) { - this.clear_tab_selection(); - this.$inline_details.hide(); - return false; - } - this.clear_tab_selection(); - this.$likes_button.addClass("selected"); - this.start_loading(); - this.load_likes(); - return false; - }, - - load_likes: function () { - var url = "/p/" + this.share_key + "/likes"; - $.get(url, $.proxy(this.process_like_response, this), "json"); - }, - - process_like_response: function (data) { - var html = - '
' + - this.user_html(data) + - "
"; - this.$inline_details.html(html); - }, - - click_saves: function () { - if (this.$saves_button.hasClass("selected")) { - this.clear_tab_selection(); - this.$inline_details.hide(); - return false; - } - - this.clear_tab_selection(); - this.$saves_button.addClass("selected"); - this.start_loading(); - this.load_saves(); - return false; - }, - - load_saves: function () { - var url = "/p/" + this.share_key + "/saves"; - $.get(url, $.proxy(this.process_like_response, this), "json"); - }, - - click_comments: function () { - if (this.$comments_button.hasClass("selected")) { - this.clear_tab_selection(); - this.$inline_details.hide(); - return false; - } - - this.clear_tab_selection(); - this.$comments_button.addClass("selected"); - this.start_loading(); - var url = "/p/" + this.share_key + "/quick-comments"; - $.get(url, $.proxy(this.process_comments_response, this), "json"); - return false; - }, - - click_more_comments: function () { - this.$comment.show(); - this.$show_more_comments.hide(); - return false; - }, - - process_comments_response: function (data) { - this.can_submit_comments = true; - if (data["result"] == "ok") { - this.$comments_button.find("a").html(data["count"]); - this.$inline_details.html(data["html"]); - this.init_comment_dom(); - this.init_comment_events(); - } - }, - - click_reply_to: function (ev) { - this.click_comment_textarea(); - var username = $(ev.target) - .parents(".comment") - .find(".username") - .html(); - var username_clean = username.replace(/[^a-zA-Z0-9_\-]+/g, ""); - var current_text = this.$comment_textarea.val(); - this.$comment_textarea.val( - current_text + "@" + username_clean + " ", - ); - setCaret(this.$comment_textarea.get(0)); - return false; - }, - - click_delete: function (ev) { - var $delete_form = $("#" + ev.target.id + "-form"), - url = $delete_form.attr("action"), - data = $delete_form.serialize(); - if (confirm("Are you sure you want to delete this?")) { - $.post( - url, - data, - $.proxy(this.process_comments_response, this), - "json", - ); - } - return false; - }, - - submit_comment: function () { - if (this.can_submit_comments === false) { - return false; - } - this.can_submit_comments = false; - var url = this.$comment_form.attr("action"); - var data = this.$comment_form.serialize(); - $.post( - url, - data, - $.proxy(this.process_comments_response, this), - "json", - ); - return false; - }, - - click_comment_textarea: function (e) { - this.$post_comment_inline.addClass("post-comment-inline-expanded"); - if (this.$comment_textarea.val().indexOf("Write a comment") === 0) { - this.$comment_textarea.val(""); - } - this.click_more_comments(); - this.$comment_textarea.css("min-height", "60px"); - }, - - refresh_likes: function () { - if (this.$likes_button.hasClass("selected")) { - this.load_likes(); - } - }, - - refresh_saves: function () { - if (this.$saves_button.hasClass("selected")) { - this.load_saves(); - } - }, - }); - var StreamStatsViewRegistry = { files_on_page: {}, register: function (view) { @@ -1147,68 +779,6 @@ $(document).ready(function () { }); } - var NSFWCover = function ($root) { - this.$root = $root; - this.init(); - }; - - $.extend(NSFWCover.prototype, { - init: function () { - this.$root.delegate( - "a", - "click", - $.proxy(this.click_show_image, this), - ); - }, - - click_show_image: function (ev) { - var location = document.location, - host = location.host, - protocol = location.protocol, - base_path = location.protocol + "//" + location.host, - file_path = $(ev.target).attr("href"); - $.get( - base_path + - "/services/oembed?include_embed=1&url=" + - escape(base_path + file_path), - $.proxy(this.load_image, this), - "json", - ); - return false; - }, - - load_image: function (response) { - var parent = this.$root.parent(), - parent_height = parent.height(); - - if (response["type"] === "photo") { - parent - .css("min-height", parent_height + "px") - .html( - '', - ); - } else if (response["embed_html"]) { - parent - .css("min-height", parent_height + "px") - .html( - '
' + - response["embed_html"] + - "
", - ); - } else if (response["type"] === "video") { - var content = parent - .css("min-height", parent_height + "px") - .html( - response["html"].replace( - / 0) { var new_comment = new PermalinkCommentsView($image_comments_permalink); @@ -1679,43 +1117,6 @@ $(document).ready(function () { // Tools: Recommended group shakes if ($("#shake-categories").length > 0) { - var RecommendedShakeCategory = function (root) { - this.root = root; - this.$root = $(root); - this.fetched = false; - this.init_events(); - }; - - $.extend(RecommendedShakeCategory.prototype, { - init_events: function () { - this.$toggle = this.$root.find(".shake-category-toggle"); - this.$body = this.$root.find(".shake-category-body"); - this.$toggle.click($.proxy(this.click_toggle, this)); - }, - - click_toggle: function () { - if (!this.fetched) { - var url = - "/tools/find-shakes/quick-fetch-category/" + - this.$toggle.attr("href").replace("#", ""); - $.get(url, $.proxy(this.populate_results, this)); - } else { - this.toggle(); - } - return false; - }, - - populate_results: function (results) { - this.fetched = true; - this.$body.html(results); - this.toggle(); - }, - - toggle: function (result) { - this.$root.toggleClass("shake-category-selected"); - }, - }); - $("#shake-categories .shake-category").each(function () { var new_category = new RecommendedShakeCategory(this); }); @@ -1957,37 +1358,6 @@ $(document).ready(function () { } /* Shake Page: Request invitation to shake */ - var RequestInvitation = function ($root) { - this.$root = $root; - this.init_dom(); - this.init_events(); - }; - - $.extend(RequestInvitation.prototype, { - init_dom: function () { - this.$form = this.$root.find("form"); - }, - - init_events: function () { - this.$root.delegate( - "form", - "submit", - $.proxy(this.submit_request, this), - ); - }, - - submit_request: function () { - var url = this.$form.attr("action"); - var data = this.$form.serialize(); - $.post(url, data, $.proxy(this.process_response, this)); - return false; - }, - - process_response: function () { - this.$root.html("Ok! Request sent."); - }, - }); - var $request_invitation = $("#request-invitation"); if ($request_invitation.length > 0) { request_invitation = new RequestInvitation($request_invitation); @@ -2142,42 +1512,6 @@ $(document).ready(function () { })(); /* Shake Page: Remove Members From Shake */ - var ShakeMemberList = function ($root) { - this.$root = $root; - this.init_events(); - }; - - $.extend(ShakeMemberList.prototype, { - init_events: function () { - this.$root.delegate( - ".remove-from-shake-button-link", - "click", - $.proxy(this.remove_from_shake, this), - ); - }, - - remove_from_shake: function (ev) { - var $target = $(ev.target), - $li = $target.parents("li"), - $form = $target.next(), - url = $form.attr("action"); - data = $form.serialize(); - - if ( - confirm( - "Are you sure you want to remove this user from a shake? If they have notifications on an email will be sent informing them of the change.", - ) - ) { - $.post(url, data, $.proxy(this.process_remove, $li)); - } - return false; - }, - - process_remove: function (response) { - this.remove(); - }, - }); - var $shake_member_list = $("#shake-members-list"); if ($shake_member_list.length > 0) { var shake_member_list = new ShakeMemberList($shake_member_list); From 0e11ca6a2668159c2fbc3f197cc20fdb0c891b90 Mon Sep 17 00:00:00 2001 From: James Beard Date: Mon, 6 Apr 2026 07:40:43 +1000 Subject: [PATCH 3/7] Pulled several IIFE and struct type structures out as well. --- static/js/ImageStats.js | 6 + static/js/InviteMember.js | 131 ++++++ static/js/NewPostPanel.js | 179 ++++++++ static/js/ShakesCache.js | 13 + static/js/SidebarStatsView.js | 207 +++++++++ static/js/StreamStatsViewRegistry.js | 21 + static/js/UserCounts.js | 88 ++++ static/js/main.js | 657 +-------------------------- 8 files changed, 652 insertions(+), 650 deletions(-) create mode 100644 static/js/ImageStats.js create mode 100644 static/js/InviteMember.js create mode 100644 static/js/NewPostPanel.js create mode 100644 static/js/ShakesCache.js create mode 100644 static/js/SidebarStatsView.js create mode 100644 static/js/StreamStatsViewRegistry.js create mode 100644 static/js/UserCounts.js diff --git a/static/js/ImageStats.js b/static/js/ImageStats.js new file mode 100644 index 0000000..7685c75 --- /dev/null +++ b/static/js/ImageStats.js @@ -0,0 +1,6 @@ +var ImageStats = function () { + return { + save_count: 0, + like_count: 0, + }; +}; diff --git a/static/js/InviteMember.js b/static/js/InviteMember.js new file mode 100644 index 0000000..f8ad089 --- /dev/null +++ b/static/js/InviteMember.js @@ -0,0 +1,131 @@ +var InviteMember = (function () { + var $main_module = $("#shake-invite-member"); + var $input_field = $main_module.find(".input-text"); + var $invite_button = $main_module.find(".invite-button"); + var $shake_results = $main_module.find(".shake-results"); + var $form = $main_module.find("form"); + var $title = $main_module.find("h3"); + var search_results = []; + var last_search = ""; + + $input_field.keyup(function (ev) { + InviteMember.search_names(ev); + }); + + $form.submit(function (ev) { + return InviteMember.submit_form(); + }); + + $shake_results.click(function (ev) { + InviteMember.select_user($(ev.target).text()); + }); + + $invite_button.click(function () { + InviteMember.send_invite(); + return false; + }); + + return { + search_names: function () { + if ($input_field.val() == "") { + this.clear_results(); + this.clear_input(); + return false; + } + + // don't search again if field hasn't changed. + if ($input_field.val() == last_search) { + return false; + } + + last_search = $input_field.val(); + var data = $form.serialize(); + var that = this; + $.post( + "/account/quick_name_search", + data, + function (response) { + if ("users" in response) { + that.update_results(response["users"]); + } + }, + "json", + ); + }, + + update_results: function (users) { + search_results = users; + if (search_results.length == 0) { + this.clear_results(); + } else { + this.render_results(); + } + }, + + render_results: function () { + $shake_results.html("").show(); + for (var i = 0; i < search_results.length; i++) { + $shake_results.append( + '
  • ' + + search_results[i].name + + "
  • ", + ); + } + }, + + select_user: function (user_name) { + this.clear_results(); + $input_field.val(user_name); + $invite_button.removeAttr("disabled"); + }, + + submit_form: function (ev) { + if ( + search_results.length == 1 && + search_results[0].name == $input_field.val() + ) { + this.select_user(search_results[0].name); + this.send_invite(); + this.clear_results(); + } + return false; + }, + + clear_results: function () { + last_search = ""; + $shake_results.hide().html(""); + }, + + clear_input: function () { + $input_field.val(""); + $invite_button.attr("disabled", "disabled"); + }, + + send_invite: function () { + if ($invite_button.disabled) { + return false; + } else { + var url = $form.attr("action"); + var data = $form.serialize(); + $.post( + url, + data, + function () { + InviteMember.data_sent(); + return false; + }, + "json", + ); + return false; + } + }, + + data_sent: function () { + $title.html("Your invitation has been sent"); + this.clear_input(); + this.clear_results(); + }, + }; +})(); diff --git a/static/js/NewPostPanel.js b/static/js/NewPostPanel.js new file mode 100644 index 0000000..1a25f6c --- /dev/null +++ b/static/js/NewPostPanel.js @@ -0,0 +1,179 @@ +var NewPostPanel = (function () { + var panel_expanded = false; + + var $new_post_panel; + var $new_post_panel_inner; + var $new_post_button; + var $save_video_form; + var $save_video_form_button; + var $post_video_form; + var $post_video_form_button; + let $upload_image_input; + let $link_to_video; + let $video_shake_id; + let $shake_selector; + + var init_dom = function () { + $new_post_panel = $("#new-post-panel"); + $new_post_panel_inner = $("#new-post-panel .new-post-panel--inner"); + $new_post_button = $("#new-post-button"); + // upload image + $upload_image_input = $("#upload-image-input"); + // link to video + $link_to_video = $("#link-to-video"); + $video_shake_id = $("#video-shake-id"); + // video preview screen + $save_video_form = $("#new-post-panel .save-video-form"); + $save_video_form_button = $("#new-post-panel .save-video-form .btn"); + $post_video_form = $("#new-post-panel .post-video-form"); + $post_video_form_button = $("#new-post-panel .post-video-form .btn"); + // shake selector + $shake_selector = $(".shake-selector"); + }; + init_dom(); + + $new_post_button.click(function () { + NewPostPanel.load_new_post(); + return false; + }); + + // We don't want click event on panel to bubble up to body + // since a click to body closes the panel. + $new_post_panel.click(function (ev) { + ev.stopPropagation(); + }); + + // The events that are inside the panel that we want to initialize + // when the panel loads. These are the events that are subject + // to change depending on content that is loaded. + var init_events = function () { + $link_to_video.click(function () { + NewPostPanel.load_post_video(); + return false; + }); + + $save_video_form_button.click(function (e) { + NewPostPanel.submit_save_video(); + return false; + }); + + $post_video_form_button.click(function (e) { + NewPostPanel.submit_post_video(); + return false; + }); + + $upload_image_input.change(function () { + $(this).closest("form").submit(); + }); + + $shake_selector.click(NewPostPanel.toggle_shake_selector); + $shake_selector.find("ul a").click(NewPostPanel.choose_shake); + }; + + var remove_events = function () { + $save_video_form_button.unbind(); + $post_video_form_button.unbind(); + $shake_selector.unbind(); + $link_to_video.unbind(); + }; + + return { + toggle_shake_selector: function (ev) { + $(this).toggleClass("is-active").find("ul").toggle(); + ev.stopPropagation(); + ev.preventDefault(); + }, + // Sets the text of the shake to the chosen one and + // sets a hidden input field with the proper shake id. + choose_shake: function () { + var $shake_selector = $(this).parents(".shake-selector"); + var $selected_shake = $shake_selector.find(".green"); + var $selected_shake_input = $shake_selector.find("input"); + var name = $(this).html(); + var id = $(this) + .attr("id") + .replace(/[^0-9]+/, ""); + $selected_shake.html(name); + $selected_shake_input.val(id); + }, + load_new_post: function () { + var url = "/tools/new-post"; + var that = this; + $.get(url, function (response) { + that.refresh_panel(response); + that.expand_panel(); + }); + return false; + }, + load_post_video: function () { + if ($video_shake_id.length > 0) { + var shake_suffix = "?shake_id=" + $video_shake_id.val(); + } else { + var shake_suffix = ""; + } + var url = "/tools/save-video" + shake_suffix; + var that = this; + $.get(url, function (response) { + that.refresh_panel(response); + that.expand_panel(); + }); + }, + expand_panel: function () { + panel_expanded = true; + $new_post_panel.slideDown(); + that = this; + $("body").one("click", $.proxy(this.close_panel, this)); + // we want to hide anything with a video since we can't + // overlap things like youtube embeds, which is an iframe + // that has an absolutely positioned flash element inside. + $(".the-image iframe").each(function () { + $(this).parent().css("height", $(this).height()); + $(this).parent().css("width", $(this).width()); + $(this).hide(); + }); + }, + close_panel: function () { + panel_expanded = false; + $new_post_panel.hide(); + remove_events(); + // show the videos again. + $(".the-image iframe").show(); + }, + submit_save_video: function () { + var url = $save_video_form.attr("action"); + var data = $save_video_form.serialize(); + var that = this; + $.get(url, data, function (response) { + that.refresh_panel(response); + }); + }, + submit_post_video: function () { + var url = $post_video_form.attr("action"); + var data = $post_video_form.serialize(); + var that = this; + $post_video_form_button + .unbind("click") + .find("span") + .html("Posting..."); + $.post( + url, + data, + function (response) { + document.location = + document.location.protocol + + "//" + + document.location.host + + response["path"]; + }, + "json", + ); + }, + refresh_panel: function (response) { + $new_post_panel_inner.html(response); + $new_post_panel_inner.html(); + remove_events(); + init_dom(); + init_events(); + }, + }; +})(); diff --git a/static/js/ShakesCache.js b/static/js/ShakesCache.js new file mode 100644 index 0000000..3afa4f1 --- /dev/null +++ b/static/js/ShakesCache.js @@ -0,0 +1,13 @@ +var ShakesCache = { + fetch: function () { + if (this.result !== undefined) { + return this.result; + } else { + return false; + } + }, + + store: function (result) { + this.result = result; + }, +}; diff --git a/static/js/SidebarStatsView.js b/static/js/SidebarStatsView.js new file mode 100644 index 0000000..27aae56 --- /dev/null +++ b/static/js/SidebarStatsView.js @@ -0,0 +1,207 @@ +var SidebarStatsView = (function (scope) { + // if we aren't on a permalink page, just expose a dummy public API + if ($(scope).length == 0) { + return { + init: function () {}, + refresh_likes: function () {}, + refresh_saves: function () {}, + }; + } + + var image_stats = new ImageStats(); + $save_count = $(".save-count", scope); + $like_count = $(".like-count", scope); + image_stats.save_count = parseInt($save_count.html(), 10); + image_stats.like_count = parseInt($like_count.html(), 10); + + var $save_button = $(".sidebar-stats-saves", scope); + var $like_button = $(".sidebar-stats-hearts", scope); + var $content = $(".sidebar-stats-content", scope); + + var saves_expanded = false; + var likes_expanded = false; + + return { + init: function () { + if (image_stats.save_count > 0) { + this.bind_saves(); + } else { + this.unbind_saves(); + } + if (image_stats.like_count > 0) { + this.bind_likes(); + this.toggle_likes(); + } else { + this.unbind_likes(); + } + }, + + refresh_likes: function () { + $like_count = $(".like-count", scope); + image_stats.like_count = parseInt($like_count.html(), 10); + if (likes_expanded) { + this.get_likes(); + } + if (image_stats.like_count > 0) { + this.bind_likes(); + } else { + likes_expanded = false; + this.unbind_likes(); + } + }, + + refresh_saves: function () { + $save_count = $(".save-count", scope); + image_stats.save_count = parseInt($save_count.html(), 10); + if (saves_expanded) { + this.get_saves(); + } + if (image_stats.save_count > 0) { + this.bind_saves(); + } else { + saves_expanded = false; + this.unbind_saves(); + } + }, + + bind_saves: function () { + $save_button.unbind("click"); + $save_button.addClass("enable-cursor"); + $save_button.click(function () { + SidebarStatsView.toggle_saves(); + }); + }, + + unbind_saves: function () { + $save_button.removeClass("enable-cursor"); + $save_button.unbind("click"); + this.collapse(); + }, + + bind_likes: function () { + $like_button.unbind("click"); + $like_button.addClass("enable-cursor"); + $like_button.click(function () { + SidebarStatsView.toggle_likes(); + }); + }, + + unbind_likes: function () { + $like_button.removeClass("enable-cursor"); + $like_button.unbind("click"); + this.collapse(); + }, + + toggle_saves: function () { + likes_expanded = false; + saves_expanded = !saves_expanded; + if (saves_expanded) { + $like_button.removeClass("selected"); + $content.addClass("loading").show(); + $save_button.addClass("selected"); + this.get_saves(); + } else { + this.collapse(); + } + }, + + get_saves: function () { + $.get( + document.location.pathname + "/saves", + function (response) { + if (response["result"]) { + SidebarStatsView.process_save(response); + } + }, + "json", + ); + }, + + toggle_likes: function () { + saves_expanded = false; + likes_expanded = !likes_expanded; + if (likes_expanded) { + $save_button.removeClass("selected"); + $content.addClass("loading").show(); + $like_button.addClass("selected"); + this.get_likes(); + } else { + this.collapse(); + } + }, + + get_likes: function () { + $.get( + document.location.pathname + "/likes", + function (response) { + if (response["result"]) { + SidebarStatsView.process_like(response); + } + }, + "json", + ); + }, + + process_save: function (response) { + if (response["count"] == 0) { + this.disable_saves(); + } else { + $save_count.html(this.to_text(response["count"], "Save")); + this.render_content(response); + } + }, + + process_like: function (response) { + if (response["count"] == 0) { + this.unbind_likes(); + } else { + $like_count.html(this.to_text(response["count"], "Like")); + this.render_content(response); + } + }, + + to_text: function (num, base) { + return num == 1 + ? num + " " + "" + base + "" + : num + " " + "" + base + "s" + ""; + }, + + collapse: function (repsponse) { + $like_button.removeClass("selected"); + $save_button.removeClass("selected"); + $content.hide(); + }, + + render_content: function (response) { + var html = ""; + for (var i = 0, len = response["result"].length; i < len; i++) { + var result = response["result"][i]; + var link; + if (result["action"] == "save") { + // for saves, we link to the saved post + link = result["post_url"]; + } else { + link = "/user/" + result["user_name"]; + } + html += '
    '; + html += ''; + html += + ''; + html += + '' + + result["user_name"] + + ""; + html += + '' + + result["posted_at_friendly"] + + ""; + html += "
    "; + } + $content.removeClass("loading").html(html); + }, + }; +})("#sidebar-stats"); diff --git a/static/js/StreamStatsViewRegistry.js b/static/js/StreamStatsViewRegistry.js new file mode 100644 index 0000000..f243d6c --- /dev/null +++ b/static/js/StreamStatsViewRegistry.js @@ -0,0 +1,21 @@ +var StreamStatsViewRegistry = { + files_on_page: {}, + register: function (view) { + this.files_on_page[view.share_key] = view; + }, + refresh_likes: function (share_key) { + view = this.get_view(share_key); + if (view !== undefined) { + view.refresh_likes(); + } + }, + refresh_saves: function (share_key) { + view = this.get_view(share_key); + if (view !== undefined) { + view.refresh_saves(); + } + }, + get_view: function (share_key) { + return this.files_on_page[share_key]; + }, +}; diff --git a/static/js/UserCounts.js b/static/js/UserCounts.js new file mode 100644 index 0000000..2cea8be --- /dev/null +++ b/static/js/UserCounts.js @@ -0,0 +1,88 @@ +var UserCounts = (function () { + var $root = $user_counts, + name = $root.attr("name"); + $.get( + "/user/" + name + "/counts", + function (result) { + UserCounts.display_results(result); + }, + "json", + ); + + return { + display_results: function (result) { + if ("views" in result) { + $root + .find(".views") + .attr( + "title", + UserCounts.format(result["views"]) + " views", + ) + .find(".num") + .html(UserCounts.formatBrief(result["views"])); + $root + .find(".saves") + .attr( + "title", + UserCounts.format(result["saves"]) + " saves", + ) + .find(".num") + .html(UserCounts.formatBrief(result["saves"])); + $root + .find(".likes") + .attr( + "title", + UserCounts.format(result["likes"]) + " likes", + ) + .find(".num") + .html(UserCounts.formatBrief(result["likes"])); + } + }, + format: function (str_num) { + return Number.parseInt(str_num).toLocaleString(); + }, + formatBrief: function (str_num) { + // Format number in a way that won't need excessive space to + // display. Abbreviate with suffixes and limit to one + // decimal place. + + const n = parseInt(str_num, 10); + + // Handle garbage input (somewhat) gracefully. + if (Number.isNaN(n)) { + return "0"; + } + + // Anything up to and including 9,999 return verbatim as + // we've got four characters minimum to play with. + if (n < 10000) { + return n.toLocaleString(); + } + + const suffixes = [ + { threshold: 1e12, suffix: "T" }, + { threshold: 1e9, suffix: "B" }, + { threshold: 1e6, suffix: "M" }, + { threshold: 1e3, suffix: "K" }, + ]; + + for (const { threshold, suffix } of suffixes) { + // Iterate until we find a suffix that can handle this + // value. + if (n >= threshold) { + // Truncate to 1 decimal place. + const truncated = Math.floor((n / threshold) * 10) / 10; + + // If the decimal part is 0 trim it. + if (truncated % 1 === 0) { + return truncated.toFixed(0) + suffix; + } else { + return truncated.toFixed(1) + suffix; + } + } + } + + return n.toLocaleString(); + }, + }; +})(); diff --git a/static/js/main.js b/static/js/main.js index 01d0398..fc99021 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,189 +1,7 @@ /* For now the core JS behavior needed accross the site */ $(document).ready(function () { - var NewPostPanel = (function () { - var panel_expanded = false; - - var $new_post_panel; - var $new_post_panel_inner; - var $new_post_button; - var $save_video_form; - var $save_video_form_button; - var $post_video_form; - var $post_video_form_button; - let $upload_image_input; - let $link_to_video; - let $video_shake_id; - let $shake_selector; - - var init_dom = function () { - $new_post_panel = $("#new-post-panel"); - $new_post_panel_inner = $("#new-post-panel .new-post-panel--inner"); - $new_post_button = $("#new-post-button"); - // upload image - $upload_image_input = $("#upload-image-input"); - // link to video - $link_to_video = $("#link-to-video"); - $video_shake_id = $("#video-shake-id"); - // video preview screen - $save_video_form = $("#new-post-panel .save-video-form"); - $save_video_form_button = $( - "#new-post-panel .save-video-form .btn", - ); - $post_video_form = $("#new-post-panel .post-video-form"); - $post_video_form_button = $( - "#new-post-panel .post-video-form .btn", - ); - // shake selector - $shake_selector = $(".shake-selector"); - }; - init_dom(); - - $new_post_button.click(function () { - NewPostPanel.load_new_post(); - return false; - }); - - // We don't want click event on panel to bubble up to body - // since a click to body closes the panel. - $new_post_panel.click(function (ev) { - ev.stopPropagation(); - }); - - // The events that are inside the panel that we want to initialize - // when the panel loads. These are the events that are subject - // to change depending on content that is loaded. - var init_events = function () { - $link_to_video.click(function () { - NewPostPanel.load_post_video(); - return false; - }); - - $save_video_form_button.click(function (e) { - NewPostPanel.submit_save_video(); - return false; - }); - - $post_video_form_button.click(function (e) { - NewPostPanel.submit_post_video(); - return false; - }); - - $upload_image_input.change(function () { - $(this).closest("form").submit(); - }); - - $shake_selector.click(NewPostPanel.toggle_shake_selector); - $shake_selector.find("ul a").click(NewPostPanel.choose_shake); - }; - - var remove_events = function () { - $save_video_form_button.unbind(); - $post_video_form_button.unbind(); - $shake_selector.unbind(); - $link_to_video.unbind(); - }; - - return { - toggle_shake_selector: function (ev) { - $(this).toggleClass("is-active").find("ul").toggle(); - ev.stopPropagation(); - ev.preventDefault(); - }, - // Sets the text of the shake to the chosen one and - // sets a hidden input field with the proper shake id. - choose_shake: function () { - var $shake_selector = $(this).parents(".shake-selector"); - var $selected_shake = $shake_selector.find(".green"); - var $selected_shake_input = $shake_selector.find("input"); - var name = $(this).html(); - var id = $(this) - .attr("id") - .replace(/[^0-9]+/, ""); - $selected_shake.html(name); - $selected_shake_input.val(id); - }, - load_new_post: function () { - var url = "/tools/new-post"; - var that = this; - $.get(url, function (response) { - that.refresh_panel(response); - that.expand_panel(); - }); - return false; - }, - load_post_video: function () { - if ($video_shake_id.length > 0) { - var shake_suffix = "?shake_id=" + $video_shake_id.val(); - } else { - var shake_suffix = ""; - } - var url = "/tools/save-video" + shake_suffix; - var that = this; - $.get(url, function (response) { - that.refresh_panel(response); - that.expand_panel(); - }); - }, - expand_panel: function () { - panel_expanded = true; - $new_post_panel.slideDown(); - that = this; - $("body").one("click", $.proxy(this.close_panel, this)); - // we want to hide anything with a video since we can't - // overlap things like youtube embeds, which is an iframe - // that has an absolutely positioned flash element inside. - $(".the-image iframe").each(function () { - $(this).parent().css("height", $(this).height()); - $(this).parent().css("width", $(this).width()); - $(this).hide(); - }); - }, - close_panel: function () { - panel_expanded = false; - $new_post_panel.hide(); - remove_events(); - // show the videos again. - $(".the-image iframe").show(); - }, - submit_save_video: function () { - var url = $save_video_form.attr("action"); - var data = $save_video_form.serialize(); - var that = this; - $.get(url, data, function (response) { - that.refresh_panel(response); - }); - }, - submit_post_video: function () { - var url = $post_video_form.attr("action"); - var data = $post_video_form.serialize(); - var that = this; - $post_video_form_button - .unbind("click") - .find("span") - .html("Posting..."); - $.post( - url, - data, - function (response) { - document.location = - document.location.protocol + - "//" + - document.location.host + - response["path"]; - }, - "json", - ); - }, - refresh_panel: function (response) { - $new_post_panel_inner.html(response); - $new_post_panel_inner.html(); - remove_events(); - init_dom(); - init_events(); - }, - }; - })(); + // TODO moved to NewPostPanel.js var to_text = function (num, base) { return num == 1 @@ -191,19 +9,7 @@ $(document).ready(function () { : num + " " + "" + base + "s" + ""; }; - var ShakesCache = { - fetch: function () { - if (this.result !== undefined) { - return this.result; - } else { - return false; - } - }, - - store: function (result) { - this.result = result; - }, - }; + // TODO moved to ShakesCache.js function screen_reader_focus(el) { el.setAttribute("tabindex", "0"); @@ -531,243 +337,12 @@ $(document).ready(function () { return false; }); - var ImageStats = function () { - return { - save_count: 0, - like_count: 0, - }; - }; - - var SidebarStatsView = (function (scope) { - // if we aren't on a permalink page, just expose a dummy public API - if ($(scope).length == 0) { - return { - init: function () {}, - refresh_likes: function () {}, - refresh_saves: function () {}, - }; - } + // TODO moved to ImageStats.js - var image_stats = new ImageStats(); - $save_count = $(".save-count", scope); - $like_count = $(".like-count", scope); - image_stats.save_count = parseInt($save_count.html(), 10); - image_stats.like_count = parseInt($like_count.html(), 10); - - var $save_button = $(".sidebar-stats-saves", scope); - var $like_button = $(".sidebar-stats-hearts", scope); - var $content = $(".sidebar-stats-content", scope); - - var saves_expanded = false; - var likes_expanded = false; - - return { - init: function () { - if (image_stats.save_count > 0) { - this.bind_saves(); - } else { - this.unbind_saves(); - } - if (image_stats.like_count > 0) { - this.bind_likes(); - this.toggle_likes(); - } else { - this.unbind_likes(); - } - }, - - refresh_likes: function () { - $like_count = $(".like-count", scope); - image_stats.like_count = parseInt($like_count.html(), 10); - if (likes_expanded) { - this.get_likes(); - } - if (image_stats.like_count > 0) { - this.bind_likes(); - } else { - likes_expanded = false; - this.unbind_likes(); - } - }, - - refresh_saves: function () { - $save_count = $(".save-count", scope); - image_stats.save_count = parseInt($save_count.html(), 10); - if (saves_expanded) { - this.get_saves(); - } - if (image_stats.save_count > 0) { - this.bind_saves(); - } else { - saves_expanded = false; - this.unbind_saves(); - } - }, - - bind_saves: function () { - $save_button.unbind("click"); - $save_button.addClass("enable-cursor"); - $save_button.click(function () { - SidebarStatsView.toggle_saves(); - }); - }, - - unbind_saves: function () { - $save_button.removeClass("enable-cursor"); - $save_button.unbind("click"); - this.collapse(); - }, - - bind_likes: function () { - $like_button.unbind("click"); - $like_button.addClass("enable-cursor"); - $like_button.click(function () { - SidebarStatsView.toggle_likes(); - }); - }, - - unbind_likes: function () { - $like_button.removeClass("enable-cursor"); - $like_button.unbind("click"); - this.collapse(); - }, - - toggle_saves: function () { - likes_expanded = false; - saves_expanded = !saves_expanded; - if (saves_expanded) { - $like_button.removeClass("selected"); - $content.addClass("loading").show(); - $save_button.addClass("selected"); - this.get_saves(); - } else { - this.collapse(); - } - }, - - get_saves: function () { - $.get( - document.location.pathname + "/saves", - function (response) { - if (response["result"]) { - SidebarStatsView.process_save(response); - } - }, - "json", - ); - }, - - toggle_likes: function () { - saves_expanded = false; - likes_expanded = !likes_expanded; - if (likes_expanded) { - $save_button.removeClass("selected"); - $content.addClass("loading").show(); - $like_button.addClass("selected"); - this.get_likes(); - } else { - this.collapse(); - } - }, - - get_likes: function () { - $.get( - document.location.pathname + "/likes", - function (response) { - if (response["result"]) { - SidebarStatsView.process_like(response); - } - }, - "json", - ); - }, - - process_save: function (response) { - if (response["count"] == 0) { - this.disable_saves(); - } else { - $save_count.html(this.to_text(response["count"], "Save")); - this.render_content(response); - } - }, - - process_like: function (response) { - if (response["count"] == 0) { - this.unbind_likes(); - } else { - $like_count.html(this.to_text(response["count"], "Like")); - this.render_content(response); - } - }, - - to_text: function (num, base) { - return num == 1 - ? num + " " + "" + base + "" - : num + " " + "" + base + "s" + ""; - }, - - collapse: function (repsponse) { - $like_button.removeClass("selected"); - $save_button.removeClass("selected"); - $content.hide(); - }, - - render_content: function (response) { - var html = ""; - for (var i = 0, len = response["result"].length; i < len; i++) { - var result = response["result"][i]; - var link; - if (result["action"] == "save") { - // for saves, we link to the saved post - link = result["post_url"]; - } else { - link = "/user/" + result["user_name"]; - } - html += '
    '; - html += ''; - html += - ''; - html += - '' + - result["user_name"] + - ""; - html += - '' + - result["posted_at_friendly"] + - ""; - html += "
    "; - } - $content.removeClass("loading").html(html); - }, - }; - })("#sidebar-stats"); + // TODO moved to SidebarStatsView.js SidebarStatsView.init(); - var StreamStatsViewRegistry = { - files_on_page: {}, - register: function (view) { - this.files_on_page[view.share_key] = view; - }, - refresh_likes: function (share_key) { - view = this.get_view(share_key); - if (view !== undefined) { - view.refresh_likes(); - } - }, - refresh_saves: function (share_key) { - view = this.get_view(share_key); - if (view !== undefined) { - view.refresh_saves(); - } - }, - get_view: function (share_key) { - return this.files_on_page[share_key]; - }, - }; + // TODO moved to StreamStatsViewRegistry.js function apply_hover_for_video(sel) { sel.hover(function () { @@ -1266,95 +841,7 @@ $(document).ready(function () { var $user_counts = $("#user-counts"); if ($user_counts.length > 0) { - var UserCounts = (function () { - var $root = $user_counts, - name = $root.attr("name"); - $.get( - "/user/" + name + "/counts", - function (result) { - UserCounts.display_results(result); - }, - "json", - ); - - return { - display_results: function (result) { - if ("views" in result) { - $root - .find(".views") - .attr( - "title", - UserCounts.format(result["views"]) + " views", - ) - .find(".num") - .html(UserCounts.formatBrief(result["views"])); - $root - .find(".saves") - .attr( - "title", - UserCounts.format(result["saves"]) + " saves", - ) - .find(".num") - .html(UserCounts.formatBrief(result["saves"])); - $root - .find(".likes") - .attr( - "title", - UserCounts.format(result["likes"]) + " likes", - ) - .find(".num") - .html(UserCounts.formatBrief(result["likes"])); - } - }, - format: function (str_num) { - return Number.parseInt(str_num).toLocaleString(); - }, - formatBrief: function (str_num) { - // Format number in a way that won't need excessive space to - // display. Abbreviate with suffixes and limit to one - // decimal place. - - const n = parseInt(str_num, 10); - - // Handle garbage input (somewhat) gracefully. - if (Number.isNaN(n)) { - return "0"; - } - - // Anything up to and including 9,999 return verbatim as - // we've got four characters minimum to play with. - if (n < 10000) { - return n.toLocaleString(); - } - - const suffixes = [ - { threshold: 1e12, suffix: "T" }, - { threshold: 1e9, suffix: "B" }, - { threshold: 1e6, suffix: "M" }, - { threshold: 1e3, suffix: "K" }, - ]; - - for (const { threshold, suffix } of suffixes) { - // Iterate until we find a suffix that can handle this - // value. - if (n >= threshold) { - // Truncate to 1 decimal place. - const truncated = - Math.floor((n / threshold) * 10) / 10; - - // If the decimal part is 0 trim it. - if (truncated % 1 === 0) { - return truncated.toFixed(0) + suffix; - } else { - return truncated.toFixed(1) + suffix; - } - } - } - - return n.toLocaleString(); - }, - }; - })(); + // TODO moved to UserCounts.js } /* Shake Page: Request invitation to shake */ @@ -1379,137 +866,7 @@ $(document).ready(function () { }); // Invite Member widget for the Shake administrator. - var InviteMember = (function () { - var $main_module = $("#shake-invite-member"); - var $input_field = $main_module.find(".input-text"); - var $invite_button = $main_module.find(".invite-button"); - var $shake_results = $main_module.find(".shake-results"); - var $form = $main_module.find("form"); - var $title = $main_module.find("h3"); - var search_results = []; - var last_search = ""; - - $input_field.keyup(function (ev) { - InviteMember.search_names(ev); - }); - - $form.submit(function (ev) { - return InviteMember.submit_form(); - }); - - $shake_results.click(function (ev) { - InviteMember.select_user($(ev.target).text()); - }); - - $invite_button.click(function () { - InviteMember.send_invite(); - return false; - }); - - return { - search_names: function () { - if ($input_field.val() == "") { - this.clear_results(); - this.clear_input(); - return false; - } - - // don't search again if field hasn't changed. - if ($input_field.val() == last_search) { - return false; - } - - last_search = $input_field.val(); - var data = $form.serialize(); - var that = this; - $.post( - "/account/quick_name_search", - data, - function (response) { - if ("users" in response) { - that.update_results(response["users"]); - } - }, - "json", - ); - }, - - update_results: function (users) { - search_results = users; - if (search_results.length == 0) { - this.clear_results(); - } else { - this.render_results(); - } - }, - - render_results: function () { - $shake_results.html("").show(); - for (var i = 0; i < search_results.length; i++) { - $shake_results.append( - '
  • ' + - search_results[i].name + - "
  • ", - ); - } - }, - - select_user: function (user_name) { - this.clear_results(); - $input_field.val(user_name); - $invite_button.removeAttr("disabled"); - }, - - submit_form: function (ev) { - if ( - search_results.length == 1 && - search_results[0].name == $input_field.val() - ) { - this.select_user(search_results[0].name); - this.send_invite(); - this.clear_results(); - } - return false; - }, - - clear_results: function () { - last_search = ""; - $shake_results.hide().html(""); - }, - - clear_input: function () { - $input_field.val(""); - $invite_button.attr("disabled", "disabled"); - }, - - send_invite: function () { - if ($invite_button.disabled) { - return false; - } else { - var url = $form.attr("action"); - var data = $form.serialize(); - $.post( - url, - data, - function () { - InviteMember.data_sent(); - return false; - }, - "json", - ); - return false; - } - }, - - data_sent: function () { - $title.html("Your invitation has been sent"); - this.clear_input(); - this.clear_results(); - }, - }; - })(); + // TODO moved to InviteMember.js /* Shake Page: Remove Members From Shake */ var $shake_member_list = $("#shake-members-list"); From 22de7dffc8059edf37b6ad10893810fba64cb2f7 Mon Sep 17 00:00:00 2001 From: James Beard Date: Mon, 6 Apr 2026 15:42:03 +1000 Subject: [PATCH 4/7] Hook modules back into main.js. Most existing functionality should work again. Made minimal changes to deliver equivalent behaviour e.g. singleton vs repeated objects per page, load automatically or on demand. --- static/js/ImageStats.js | 6 ------ static/js/InviteMember.js | 3 +++ static/js/NSFWCover.js | 2 ++ static/js/NewPostPanel.js | 4 +++- static/js/SaveThisView.js | 4 ++++ static/js/ShakesCache.js | 2 ++ static/js/SidebarStatsView.js | 6 +++++- static/js/StreamStatsView.js | 2 ++ static/js/StreamStatsViewRegistry.js | 2 ++ static/js/UserCounts.js | 23 ++++++++++++++--------- static/js/main.js | 23 ++++++++++------------- templates/base.html | 2 +- 12 files changed, 48 insertions(+), 31 deletions(-) delete mode 100644 static/js/ImageStats.js diff --git a/static/js/ImageStats.js b/static/js/ImageStats.js deleted file mode 100644 index 7685c75..0000000 --- a/static/js/ImageStats.js +++ /dev/null @@ -1,6 +0,0 @@ -var ImageStats = function () { - return { - save_count: 0, - like_count: 0, - }; -}; diff --git a/static/js/InviteMember.js b/static/js/InviteMember.js index f8ad089..47c001d 100644 --- a/static/js/InviteMember.js +++ b/static/js/InviteMember.js @@ -1,3 +1,4 @@ +// Invite Member widget for the Shake administrator. var InviteMember = (function () { var $main_module = $("#shake-invite-member"); var $input_field = $main_module.find(".input-text"); @@ -129,3 +130,5 @@ var InviteMember = (function () { }, }; })(); + +export { InviteMember }; diff --git a/static/js/NSFWCover.js b/static/js/NSFWCover.js index 281ebce..602e300 100644 --- a/static/js/NSFWCover.js +++ b/static/js/NSFWCover.js @@ -53,3 +53,5 @@ $.extend(NSFWCover.prototype, { } }, }); + +export { NSFWCover }; diff --git a/static/js/NewPostPanel.js b/static/js/NewPostPanel.js index 1a25f6c..107621d 100644 --- a/static/js/NewPostPanel.js +++ b/static/js/NewPostPanel.js @@ -121,7 +121,7 @@ var NewPostPanel = (function () { expand_panel: function () { panel_expanded = true; $new_post_panel.slideDown(); - that = this; + var that = this; $("body").one("click", $.proxy(this.close_panel, this)); // we want to hide anything with a video since we can't // overlap things like youtube embeds, which is an iframe @@ -177,3 +177,5 @@ var NewPostPanel = (function () { }, }; })(); + +export { NewPostPanel }; diff --git a/static/js/SaveThisView.js b/static/js/SaveThisView.js index 2044d51..b7aedf1 100644 --- a/static/js/SaveThisView.js +++ b/static/js/SaveThisView.js @@ -1,3 +1,5 @@ +import { ShakesCache } from "./ShakesCache.js"; + var SaveThisView = function (container) { this.$save_this = $(container); this.init(); @@ -119,3 +121,5 @@ $.extend(SaveThisView.prototype, { } }, }); + +export { SaveThisView }; diff --git a/static/js/ShakesCache.js b/static/js/ShakesCache.js index 3afa4f1..e33e8f2 100644 --- a/static/js/ShakesCache.js +++ b/static/js/ShakesCache.js @@ -11,3 +11,5 @@ var ShakesCache = { this.result = result; }, }; + +export { ShakesCache }; diff --git a/static/js/SidebarStatsView.js b/static/js/SidebarStatsView.js index 27aae56..0e23c0e 100644 --- a/static/js/SidebarStatsView.js +++ b/static/js/SidebarStatsView.js @@ -1,3 +1,5 @@ +const DEFAULT_IMAGE_STATS = { save_count: 0, like_count: 0 }; + var SidebarStatsView = (function (scope) { // if we aren't on a permalink page, just expose a dummy public API if ($(scope).length == 0) { @@ -8,7 +10,7 @@ var SidebarStatsView = (function (scope) { }; } - var image_stats = new ImageStats(); + const image_stats = { ...DEFAULT_IMAGE_STATS }; $save_count = $(".save-count", scope); $like_count = $(".like-count", scope); image_stats.save_count = parseInt($save_count.html(), 10); @@ -205,3 +207,5 @@ var SidebarStatsView = (function (scope) { }, }; })("#sidebar-stats"); + +export { SidebarStatsView }; diff --git a/static/js/StreamStatsView.js b/static/js/StreamStatsView.js index ea684f5..7ad43d6 100644 --- a/static/js/StreamStatsView.js +++ b/static/js/StreamStatsView.js @@ -232,3 +232,5 @@ $.extend(StreamStatsView.prototype, { } }, }); + +export { StreamStatsView }; diff --git a/static/js/StreamStatsViewRegistry.js b/static/js/StreamStatsViewRegistry.js index f243d6c..f5b6d8e 100644 --- a/static/js/StreamStatsViewRegistry.js +++ b/static/js/StreamStatsViewRegistry.js @@ -19,3 +19,5 @@ var StreamStatsViewRegistry = { return this.files_on_page[share_key]; }, }; + +export { StreamStatsViewRegistry }; diff --git a/static/js/UserCounts.js b/static/js/UserCounts.js index 2cea8be..6a3bfe2 100644 --- a/static/js/UserCounts.js +++ b/static/js/UserCounts.js @@ -1,15 +1,18 @@ var UserCounts = (function () { - var $root = $user_counts, - name = $root.attr("name"); - $.get( - "/user/" + name + "/counts", - function (result) { - UserCounts.display_results(result); - }, - "json", - ); + var $root; return { + populate($user_counts) { + $root = $user_counts; + var name = $root.attr("name"); + $.get( + "/user/" + name + "/counts", + function (result) { + UserCounts.display_results(result); + }, + "json", + ); + }, display_results: function (result) { if ("views" in result) { $root @@ -86,3 +89,5 @@ var UserCounts = (function () { }, }; })(); + +export { UserCounts }; diff --git a/static/js/main.js b/static/js/main.js index fc99021..1b58c76 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,16 +1,21 @@ /* For now the core JS behavior needed accross the site */ -$(document).ready(function () { - // TODO moved to NewPostPanel.js +import { SaveThisView } from "./SaveThisView.js"; +import { SidebarStatsView } from "./SidebarStatsView.js"; +import { StreamStatsView } from "./StreamStatsView.js"; +import { StreamStatsViewRegistry } from "./StreamStatsViewRegistry.js"; +import { NSFWCover } from "./NSFWCover.js"; +import { NewPostPanel } from "./NewPostPanel.js"; +import { InviteMember } from "./InviteMember.js"; +import { UserCounts } from "./UserCounts.js"; +$(document).ready(function () { var to_text = function (num, base) { return num == 1 ? num + " " + "" + base + "" : num + " " + "" + base + "s" + ""; }; - // TODO moved to ShakesCache.js - function screen_reader_focus(el) { el.setAttribute("tabindex", "0"); el.blur(); @@ -337,13 +342,8 @@ $(document).ready(function () { return false; }); - // TODO moved to ImageStats.js - - // TODO moved to SidebarStatsView.js SidebarStatsView.init(); - // TODO moved to StreamStatsViewRegistry.js - function apply_hover_for_video(sel) { sel.hover(function () { if (this.hasAttribute("controls")) { @@ -841,7 +841,7 @@ $(document).ready(function () { var $user_counts = $("#user-counts"); if ($user_counts.length > 0) { - // TODO moved to UserCounts.js + UserCounts.populate($user_counts); } /* Shake Page: Request invitation to shake */ @@ -865,9 +865,6 @@ $(document).ready(function () { "/incoming"; }); - // Invite Member widget for the Shake administrator. - // TODO moved to InviteMember.js - /* Shake Page: Remove Members From Shake */ var $shake_member_list = $("#shake-members-list"); if ($shake_member_list.length > 0) { diff --git a/templates/base.html b/templates/base.html index 541203d..7fc0b38 100644 --- a/templates/base.html +++ b/templates/base.html @@ -205,7 +205,7 @@
    - + {% block included_scripts %} {% end %} From 91a78fe24b6d0ee1d60d32cc56525063f19da696 Mon Sep 17 00:00:00 2001 From: James Beard Date: Mon, 6 Apr 2026 17:00:46 +1000 Subject: [PATCH 5/7] Removed IIFE wrapper, now redundant given we're including the JS as a module. No other code changes. --- static/js/main.js | 1580 ++++++++++++++++++++++----------------------- 1 file changed, 774 insertions(+), 806 deletions(-) diff --git a/static/js/main.js b/static/js/main.js index 1b58c76..0e87d7e 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -9,916 +9,884 @@ import { NewPostPanel } from "./NewPostPanel.js"; import { InviteMember } from "./InviteMember.js"; import { UserCounts } from "./UserCounts.js"; -$(document).ready(function () { - var to_text = function (num, base) { - return num == 1 - ? num + " " + "" + base + "" - : num + " " + "" + base + "s" + ""; - }; - - function screen_reader_focus(el) { - el.setAttribute("tabindex", "0"); - el.blur(); - el.focus(); - } - - $(".save-this").each(function () { - var save_this_view = new SaveThisView(this); - }); - - // when we hit enter on a form, we want to submit it - // even though we don't have an type="submit" input - // available, since we're using a styled button. - const $sign_in_form = $("#sign-in-form"); - $("input", $sign_in_form).keydown(function (e) { - if (e.keyCode == 13) { - $sign_in_form.submit(); - return false; - } - }); +var to_text = function (num, base) { + return num == 1 + ? num + " " + "" + base + "" + : num + " " + "" + base + "s" + ""; +}; + +function screen_reader_focus(el) { + el.setAttribute("tabindex", "0"); + el.blur(); + el.focus(); +} + +$(".save-this").each(function () { + var save_this_view = new SaveThisView(this); +}); - $(".btn", $sign_in_form).click(function () { +// when we hit enter on a form, we want to submit it even though we don't have +// an type="submit" input available, since we're using a styled button. +const $sign_in_form = $("#sign-in-form"); +$("input", $sign_in_form).keydown(function (e) { + if (e.keyCode == 13) { $sign_in_form.submit(); return false; - }); + } +}); - // Prompt user to confirm before flagging something as NSFW. - $("#flag-image-permalink").click(function () { - return confirm("Are you sure you want to flag this as NSFW?"); - }); +$(".btn", $sign_in_form).click(function () { + $sign_in_form.submit(); + return false; +}); - // Prompt user to confirm before quitting a shake. - $("#quit-shake-page").click(function () { - return confirm( - "Are you sure you want to quit this shake?\n(If you are following this shake you will also have to unfollow with the button above.)", - ); - }); +// Prompt user to confirm before flagging something as NSFW. +$("#flag-image-permalink").click(function () { + return confirm("Are you sure you want to flag this as NSFW?"); +}); - // Prompt user to confirm before deleting a sharedfile. - $("#delete-post-text").click(function () { - return confirm("Are you sure you want to delete this post?"); - }); +// Prompt user to confirm before quitting a shake. +$("#quit-shake-page").click(function () { + return confirm( + "Are you sure you want to quit this shake?\n(If you are following this shake you will also have to unfollow with the button above.)", + ); +}); - // Inline editing of the title. - $(".image-edit-title-form .cancel").click(function () { - $(this).closest(".image-title").find(".image-edit-title").show(); - $(this).closest(".image-edit-title-form").removeClass("is-active"); - return false; - }); +// Prompt user to confirm before deleting a sharedfile. +$("#delete-post-text").click(function () { + return confirm("Are you sure you want to delete this post?"); +}); - $(".image-edit-title").hover( - function () { - $(this).addClass("image-edit-title-hover"); - }, - function () { - $(this).removeClass("image-edit-title-hover"); +// Inline editing of the title. +$(".image-edit-title-form .cancel").click(function () { + $(this).closest(".image-title").find(".image-edit-title").show(); + $(this).closest(".image-edit-title-form").removeClass("is-active"); + return false; +}); + +$(".image-edit-title").hover( + function () { + $(this).addClass("image-edit-title-hover"); + }, + function () { + $(this).removeClass("image-edit-title-hover"); + }, +); + +$(".image-edit-title").click(function () { + var $title_container = $(this).closest(".image-title"); + var url = $title_container.find("form").attr("action"); + var that = this; + + $.get( + url, + function (result) { + if ("title_raw" in result) { + $(that).hide(); + $title_container.find(".title-input").val(result["title_raw"]); + var $input = $title_container.find(".title-input"); + $(that).next(".image-edit-title-form").addClass("is-active"); + screen_reader_focus($input[0]); + } }, + "json", ); +}); - $(".image-edit-title").click(function () { - var $title_container = $(this).closest(".image-title"); - var url = $title_container.find("form").attr("action"); - var that = this; - - $.get( - url, - function (result) { - if ("title_raw" in result) { - $(that).hide(); - $title_container - .find(".title-input") - .val(result["title_raw"]); - var $input = $title_container.find(".title-input"); - $(that) - .next(".image-edit-title-form") - .addClass("is-active"); - screen_reader_focus($input[0]); - } - }, - "json", - ); - }); - - $(".image-edit-title-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("title" in result && "title_raw" in result) { - var $title_container = $(that).closest(".image-title"); +$(".image-edit-title-form").submit(function () { + var data = $(this).serialize(); + var url = $(this).attr("action"); + var that = this; + $.post( + url, + data, + function (result) { + if ("title" in result && "title_raw" in result) { + var $title_container = $(that).closest(".image-title"); + $title_container + .find(".image-edit-title") + .html(result["title"]) + .show(); + $title_container.find(".title-input").val(result["title_raw"]); + $title_container + .find(".image-edit-title-form") + .removeClass("is-active"); + if (result["title_raw"] === "") { $title_container - .find(".image-edit-title") - .html(result["title"]) + .find(".the-title") + .html("click here to edit title") .show(); $title_container - .find(".title-input") - .val(result["title_raw"]); + .find(".the-title") + .addClass("the-title-blank"); + } else { $title_container - .find(".image-edit-title-form") - .removeClass("is-active"); - if (result["title_raw"] === "") { - $title_container - .find(".the-title") - .html("click here to edit title") - .show(); - $title_container - .find(".the-title") - .addClass("the-title-blank"); - } else { - $title_container - .find(".the-title") - .removeClass("the-title-blank"); - } + .find(".the-title") + .removeClass("the-title-blank"); } - }, - "json", - ); - return false; - }); + } + }, + "json", + ); + return false; +}); - // Inline editing of the description. - $(".description-edit-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("description" in result && "description_raw" in result) { - var $description_container = - $(that).closest(".description-edit"); +// Inline editing of the description. +$(".description-edit-form").submit(function () { + var data = $(this).serialize(); + var url = $(this).attr("action"); + var that = this; + $.post( + url, + data, + function (result) { + if ("description" in result && "description_raw" in result) { + var $description_container = + $(that).closest(".description-edit"); + $description_container + .find("textarea") + .val(result["description_raw"]); + $description_container.find(".description-edit-form").hide(); + if (result["description"]) { $description_container - .find("textarea") - .val(result["description_raw"]); + .find(".the-description") + .html(result["description"]) + .show(); $description_container - .find(".description-edit-form") - .hide(); - if (result["description"]) { - $description_container - .find(".the-description") - .html(result["description"]) - .show(); - $description_container - .find(".the-description") - .removeClass("the-description-blank"); - } else { - $description_container - .find(".the-description") - .html("click here to edit description") - .show(); - $description_container - .find(".the-description") - .addClass("the-description-blank"); - } + .find(".the-description") + .removeClass("the-description-blank"); + } else { + $description_container + .find(".the-description") + .html("click here to edit description") + .show(); + $description_container + .find(".the-description") + .addClass("the-description-blank"); } - }, - "json", - ); - return false; - }); - - $(".description-edit .the-description").hover( - function () { - $(this).addClass("the-description-hover"); - }, - function () { - $(this).removeClass("the-description-hover"); + } }, + "json", ); + return false; +}); - $(".description-edit .the-description").click(function () { - var $description_container = $(this).closest(".description-edit"); - var url = $description_container.find("form").attr("action"); - var that = this; - $.get( - url, - function (result) { - if ("description_raw" in result) { - $(that).hide(); - let $textarea = $description_container.find( - ".description-edit-textarea", - ); - $textarea.val(result["description_raw"]); - $(that).next(".description-edit-form").show(); - screen_reader_focus($textarea[0]); - } - }, - "json", - ); - }); +$(".description-edit .the-description").hover( + function () { + $(this).addClass("the-description-hover"); + }, + function () { + $(this).removeClass("the-description-hover"); + }, +); + +$(".description-edit .the-description").click(function () { + var $description_container = $(this).closest(".description-edit"); + var url = $description_container.find("form").attr("action"); + var that = this; + $.get( + url, + function (result) { + if ("description_raw" in result) { + $(that).hide(); + let $textarea = $description_container.find( + ".description-edit-textarea", + ); + $textarea.val(result["description_raw"]); + $(that).next(".description-edit-form").show(); + screen_reader_focus($textarea[0]); + } + }, + "json", + ); +}); - $(".description-edit .cancel").click(function () { - $(this).closest(".description-edit").find(".the-description").show(); - $(this).closest(".description-edit-form").hide(); - return false; - }); +$(".description-edit .cancel").click(function () { + $(this).closest(".description-edit").find(".the-description").show(); + $(this).closest(".description-edit-form").hide(); + return false; +}); - // Inline editing of the alt text. - $(".alt-text-edit-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("alt_text" in result && "alt_text_raw" in result) { - var $alt_text_container = $(that).closest(".alt-text-edit"); - if (result["alt_text"]) { - $alt_text_container.removeClass("alt-text--blank"); - $alt_text_container - .find(".the-alt-text") - .html(result["alt_text"]); - } else { - $alt_text_container.addClass("alt-text--blank"); - $alt_text_container - .find(".the-alt-text") - .html("add some alt text"); - } - $alt_text_container.removeClass("alt-text--hidden"); - $alt_text_container.removeClass("alt-text--editing"); +// Inline editing of the alt text. +$(".alt-text-edit-form").submit(function () { + var data = $(this).serialize(); + var url = $(this).attr("action"); + var that = this; + $.post( + url, + data, + function (result) { + if ("alt_text" in result && "alt_text_raw" in result) { + var $alt_text_container = $(that).closest(".alt-text-edit"); + if (result["alt_text"]) { + $alt_text_container.removeClass("alt-text--blank"); $alt_text_container - .find("textarea") - .val(result["alt_text_raw"]); - screen_reader_focus( - $alt_text_container.find(".the-alt-text")[0], - ); + .find(".the-alt-text") + .html(result["alt_text"]); + } else { + $alt_text_container.addClass("alt-text--blank"); + $alt_text_container + .find(".the-alt-text") + .html("add some alt text"); } - }, - "json", - ); - return false; - }); - - $(".alt-text-edit .the-alt-text").hover( - function () { - $(this).addClass("the-alt-text-hover"); + $alt_text_container.removeClass("alt-text--hidden"); + $alt_text_container.removeClass("alt-text--editing"); + $alt_text_container + .find("textarea") + .val(result["alt_text_raw"]); + screen_reader_focus( + $alt_text_container.find(".the-alt-text")[0], + ); + } }, - function () { - $(this).removeClass("the-alt-text-hover"); + "json", + ); + return false; +}); + +$(".alt-text-edit .the-alt-text").hover( + function () { + $(this).addClass("the-alt-text-hover"); + }, + function () { + $(this).removeClass("the-alt-text-hover"); + }, +); + +$(".alt-text-edit .the-alt-text").click(function () { + var $alt_text_container = $(this).closest(".alt-text-edit"); + var url = $alt_text_container.find("form").attr("action"); + var that = this; + $.get( + url, + function (result) { + if ("alt_text_raw" in result) { + $(that).closest(".alt-text-edit").addClass("alt-text--editing"); + let $textarea = $alt_text_container.find( + ".alt-text-edit-textarea", + ); + $textarea.val(result["alt_text_raw"]); + screen_reader_focus($textarea[0]); + } }, + "json", ); +}); - $(".alt-text-edit .the-alt-text").click(function () { - var $alt_text_container = $(this).closest(".alt-text-edit"); - var url = $alt_text_container.find("form").attr("action"); - var that = this; - $.get( - url, - function (result) { - if ("alt_text_raw" in result) { - $(that) - .closest(".alt-text-edit") - .addClass("alt-text--editing"); - let $textarea = $alt_text_container.find( - ".alt-text-edit-textarea", - ); - $textarea.val(result["alt_text_raw"]); - screen_reader_focus($textarea[0]); +$(".alt-text-edit .cancel").click(function () { + $(this).closest(".alt-text-edit").removeClass("alt-text--hidden"); + $(this).closest(".alt-text-edit").removeClass("alt-text--editing"); + return false; +}); + +$(".alt-text-toggle").click(function () { + let $alt = $(this).closest(".alt-text"); + $alt.toggleClass("alt-text--hidden"); + if (!$alt.hasClass("alt-text--hidden")) { + screen_reader_focus($alt.find(".the-alt-text")[0]); + } +}); + +$(".delete-from-shakes-form").click(function () { + return confirm("Are you sure you want to remove it?"); +}); + +/* Like / Unlike button */ +$(".like-button, .unlike-button").click(function () { + var to_text = function (num, base) { + return num == 1 + ? num + " " + "" + base + "" + : num + " " + "" + base + "s" + ""; + }; + + var $form = $(this).parents("form"); + var $buttons = $form.children("button"); + var url = $form.attr("action"); + var data = $form.serialize() + "&json=1"; + + $.post( + url, + data, + function (response) { + if (response["error"]) { + return false; + } else { + var count = response["count"]; + var share_key = response["share_key"]; + var count_string = to_text(count, "Like"); + $("#like-count-amount-" + share_key).html(count_string); + if (response["like"] === true) { + $form.attr("action", "/p/" + share_key + "/unlike"); + } else { + $form.attr("action", "/p/" + share_key + "/like"); } - }, - "json", - ); - }); + $buttons.toggleClass("is-active"); + SidebarStatsView.refresh_likes(); + StreamStatsViewRegistry.refresh_likes(share_key); + } + }, + "json", + ); + return false; +}); - $(".alt-text-edit .cancel").click(function () { - $(this).closest(".alt-text-edit").removeClass("alt-text--hidden"); - $(this).closest(".alt-text-edit").removeClass("alt-text--editing"); - return false; - }); +SidebarStatsView.init(); - $(".alt-text-toggle").click(function () { - let $alt = $(this).closest(".alt-text"); - $alt.toggleClass("alt-text--hidden"); - if (!$alt.hasClass("alt-text--hidden")) { - screen_reader_focus($alt.find(".the-alt-text")[0]); +function apply_hover_for_video(sel) { + sel.hover(function () { + if (this.hasAttribute("controls")) { + this.removeAttribute("controls"); + } else { + this.setAttribute("controls", "controls"); } }); +} - $(".delete-from-shakes-form").click(function () { - return confirm("Are you sure you want to remove it?"); - }); +$(".image-content").each(function () { + var $image_content = $(this), + $image_footer = $image_content.find(".image-content-footer"), + $nsfw_cover = $image_content.find(".nsfw-cover"); + var stream_stats_view = new StreamStatsView($image_footer); + StreamStatsViewRegistry.register(stream_stats_view); + var nsfw_cover = new NSFWCover($nsfw_cover); +}); - /* Like / Unlike button */ - $(".like-button, .unlike-button").click(function () { - var to_text = function (num, base) { - return num == 1 - ? num + " " + "" + base + "" - : num + " " + "" + base + "s" + ""; - }; +apply_hover_for_video($(".image-content video.autoplay")); - var $form = $(this).parents("form"); - var $buttons = $form.children("button"); - var url = $form.attr("action"); - var data = $form.serialize() + "&json=1"; +/* Open / close notification boxes */ +$(document).on("click", ".notification-block-hd", function () { + $(this).next().toggle(); +}); - $.post( - url, - data, - function (response) { - if (response["error"]) { - return false; +/* User follow module */ +$(document).on("click", ".user-follow .submit-form", function () { + var $container = $(this).parents(".user-follow"); + var $form = $container.find("form"); + var url = $form.attr("action"); + var data = $form.serialize() + "&json=1"; + var that = this; + $.post( + url, + data, + function (response) { + if (response["error"]) { + return false; + } else { + if (response["subscription_status"] == true) { + $form.attr( + "action", + url.replace("subscribe", "unsubscribe"), + ); + $(that) + .text("- Unfollow") + .addClass("btn-warning") + .removeClass("btn-secondary"); } else { - var count = response["count"]; - var share_key = response["share_key"]; - var count_string = to_text(count, "Like"); - $("#like-count-amount-" + share_key).html(count_string); - if (response["like"] === true) { - $form.attr("action", "/p/" + share_key + "/unlike"); - } else { - $form.attr("action", "/p/" + share_key + "/like"); - } - $buttons.toggleClass("is-active"); - SidebarStatsView.refresh_likes(); - StreamStatsViewRegistry.refresh_likes(share_key); + $form.attr( + "action", + url.replace("unsubscribe", "subscribe"), + ); + $(that) + .text("+ Follow") + .addClass("btn-secondary") + .removeClass("btn-warning"); } - }, - "json", - ); - return false; - }); + } + }, + "json", + ); + return false; +}); - SidebarStatsView.init(); +$(document).on("click", ".notification-close", function () { + $notification = $(this).parent(".notification"); + var $notification_block = $(this).parents(".notification-block"); + var $notification_block_hd = $notification_block.find( + ".notification-block-hd", + ); + var id = $(this) + .attr("id") + .replace(/[^\d]+/, ""); + $.post( + "/account/clear-notification" + "?type=single&id=" + id, + {}, + function (response) { + $notification.remove(); + var html = $notification_block_hd.html(); + var count = html.replace(/[^\d]+/, ""); + var new_count = parseInt(count, 10) - 1; + if (new_count == 0) { + $notification_block_hd.html("You have 0 new followers"); + $notification_block.find(".clear-all").remove(); + } else { + $notification_block_hd.html(html.replace(/[\d]+/, new_count)); + } + }, + "json", + ); + return false; +}); - function apply_hover_for_video(sel) { - sel.hover(function () { - if (this.hasAttribute("controls")) { - this.removeAttribute("controls"); +$(document).on("click", ".notification-block .clear-all a", function () { + var url = $(this).attr("href"); + var $notification_block = $(this).parents(".notification-block"); + $.post( + url, + {}, + function (response) { + if (response["error"]) { + return false; } else { - this.setAttribute("controls", "controls"); + $notification_block + .find(".notification-block-hd") + .html(response["response"]); + $notification_block + .find(".notification-block-bd") + .html("") + .toggle(); } - }); - } + }, + "json", + ); - $(".image-content").each(function () { - var $image_content = $(this), - $image_footer = $image_content.find(".image-content-footer"), - $nsfw_cover = $image_content.find(".nsfw-cover"); - var stream_stats_view = new StreamStatsView($image_footer); - StreamStatsViewRegistry.register(stream_stats_view); - var nsfw_cover = new NSFWCover($nsfw_cover); - }); + return false; +}); - apply_hover_for_video($(".image-content video.autoplay")); +/* Notification block: invitations: */ +$(document).on("submit", "#notifcation-block-invitations form", function () { + var data = $(this).serialize(); + var url = $(this).attr("action"); + + var that = this; + $.post( + url, + data, + function (response) { + if (response["error"]) { + $(that) + .find(".main-message") + .html("

    " + response["error"] + "

    "); + } else { + if (response["count"] == 0) { + $(that).find("input").hide(); + $("#invitation-count-text").html( + response["count"] + " invitations", + ); + $(that).find(".main-message").html("

    Thanks!

    "); + } else { + var invitation_text = + response["count"] == 1 ? "invitation" : "invitations"; + $("#invitation-count-text").html( + response["count"] + " " + invitation_text, + ); + $(that).find(".main-message").html(response["message"]); + $("#email_address").val(""); + } + } + }, + "json", + ); - /* Open / close notification boxes */ - $(document).on("click", ".notification-block-hd", function () { - $(this).next().toggle(); - }); + return false; +}); + +/* Notification block: shake invitations: */ +$(document).on( + "submit", + "#notifcation-block-shakeinvitation form", + function () { + var data = $(this).serialize(); + var url = $(this).attr("action"); + var $block = $(this).parents(".notification"); + var $header = $( + "#notifcation-block-shakeinvitation .notification-block-hd", + ); - /* User follow module */ - $(document).on("click", ".user-follow .submit-form", function () { - var $container = $(this).parents(".user-follow"); - var $form = $container.find("form"); - var url = $form.attr("action"); - var data = $form.serialize() + "&json=1"; var that = this; $.post( url, data, function (response) { - if (response["error"]) { - return false; - } else { - if (response["subscription_status"] == true) { - $form.attr( - "action", - url.replace("subscribe", "unsubscribe"), - ); - $(that) - .text("- Unfollow") - .addClass("btn-warning") - .removeClass("btn-secondary"); + if (!response["error"]) { + $block.remove(); + // we update the header differently when presenting only one + // invitation on the shake page itself. + if ($header.hasClass("invitation-single")) { + $header.html("Got it."); } else { - $form.attr( - "action", - url.replace("unsubscribe", "subscribe"), + var invitation_text = + response["count"] == 1 + ? "invitation" + : "invitations"; + $header.html( + response["count"] + " new shake " + invitation_text, ); - $(that) - .text("+ Follow") - .addClass("btn-secondary") - .removeClass("btn-warning"); } } }, "json", ); - return false; - }); - - $(document).on("click", ".notification-close", function () { - $notification = $(this).parent(".notification"); - var $notification_block = $(this).parents(".notification-block"); - var $notification_block_hd = $notification_block.find( - ".notification-block-hd", - ); - var id = $(this) - .attr("id") - .replace(/[^\d]+/, ""); - $.post( - "/account/clear-notification" + "?type=single&id=" + id, - {}, - function (response) { - $notification.remove(); - var html = $notification_block_hd.html(); - var count = html.replace(/[^\d]+/, ""); - var new_count = parseInt(count, 10) - 1; - if (new_count == 0) { - $notification_block_hd.html("You have 0 new followers"); - $notification_block.find(".clear-all").remove(); - } else { - $notification_block_hd.html( - html.replace(/[\d]+/, new_count), - ); - } - }, - "json", - ); - return false; - }); - - $(document).on("click", ".notification-block .clear-all a", function () { - var url = $(this).attr("href"); - var $notification_block = $(this).parents(".notification-block"); - $.post( - url, - {}, - function (response) { - if (response["error"]) { - return false; - } else { - $notification_block - .find(".notification-block-hd") - .html(response["response"]); - $notification_block - .find(".notification-block-bd") - .html("") - .toggle(); - } - }, - "json", - ); return false; - }); + }, +); - /* Notification block: invitations: */ - $(document).on( - "submit", - "#notifcation-block-invitations form", - function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - - var that = this; - $.post( - url, - data, - function (response) { - if (response["error"]) { - $(that) - .find(".main-message") - .html("

    " + response["error"] + "

    "); - } else { - if (response["count"] == 0) { - $(that).find("input").hide(); - $("#invitation-count-text").html( - response["count"] + " invitations", - ); - $(that) - .find(".main-message") - .html("

    Thanks!

    "); - } else { - var invitation_text = - response["count"] == 1 - ? "invitation" - : "invitations"; - $("#invitation-count-text").html( - response["count"] + " " + invitation_text, - ); - $(that) - .find(".main-message") - .html(response["message"]); - $("#email_address").val(""); - } - } - }, - "json", - ); - - return false; - }, - ); - - /* Notification block: shake invitations: */ - $(document).on( - "submit", - "#notifcation-block-shakeinvitation form", - function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var $block = $(this).parents(".notification"); - var $header = $( - "#notifcation-block-shakeinvitation .notification-block-hd", - ); - - var that = this; - $.post( - url, - data, - function (response) { - if (!response["error"]) { - $block.remove(); - // we update the header differently when presenting only one - // invitation on the shake page itself. - if ($header.hasClass("invitation-single")) { - $header.html("Got it."); - } else { - var invitation_text = - response["count"] == 1 - ? "invitation" - : "invitations"; - $header.html( - response["count"] + - " new shake " + - invitation_text, - ); - } - } - }, - "json", - ); - - return false; - }, +var init_notification_invitation_request = function () { + const $notification_invitation_request = $( + "#notification-block-invitation-request", ); - - var init_notification_invitation_request = function () { - const $notification_invitation_request = $( - "#notification-block-invitation-request", + if ($notification_invitation_request.length > 0) { + var invitation_requests = new NotificationInvitationContainer( + $notification_invitation_request, ); - if ($notification_invitation_request.length > 0) { - var invitation_requests = new NotificationInvitationContainer( - $notification_invitation_request, - ); - } - }; - init_notification_invitation_request(); - - // Expand all notifications. - $("#notification-block-aggregate").click(function () { - $(this).find(".notification-block-hd").html("Loading..."); - $.get("/account/quick-notifications", function (response) { - $("#notification-block-aggregate").hide().after(response); - init_notification_invitation_request(); - }); + } +}; +init_notification_invitation_request(); + +// Expand all notifications. +$("#notification-block-aggregate").click(function () { + $(this).find(".notification-block-hd").html("Loading..."); + $.get("/account/quick-notifications", function (response) { + $("#notification-block-aggregate").hide().after(response); + init_notification_invitation_request(); }); +}); - /* Action Button in a Fun Form, should submit the form (exception here +/* Action Button in a Fun Form, should submit the form (exception here for a button with a g-recaptcha class which has a separate event handler). */ - $(".field-submit .btn:not(.g-recaptcha)").click(function () { - $(this).closest("form").submit(); - return false; - }); +$(".field-submit .btn:not(.g-recaptcha)").click(function () { + $(this).closest("form").submit(); + return false; +}); - /* Site Nav dropdown */ - const $site_nav = $("#site-nav"); - var site_nav_expanded = false; - $("#site-nav .site-nav--toggle").click(function (event) { - event.stopPropagation(); - if (site_nav_expanded == false) { - site_nav_expanded = true; - $site_nav.addClass("is-expanded"); - $("body").one("click", function () { - $site_nav.removeClass("is-expanded"); - site_nav_expanded = false; - }); - } else { +/* Site Nav dropdown */ +const $site_nav = $("#site-nav"); +var site_nav_expanded = false; +$("#site-nav .site-nav--toggle").click(function (event) { + event.stopPropagation(); + if (site_nav_expanded == false) { + site_nav_expanded = true; + $site_nav.addClass("is-expanded"); + $("body").one("click", function () { $site_nav.removeClass("is-expanded"); - $("body").unbind("click"); site_nav_expanded = false; - } - }); + }); + } else { + $site_nav.removeClass("is-expanded"); + $("body").unbind("click"); + site_nav_expanded = false; + } +}); - $("#site-nav .site-nav--list").click(function (event) { - event.stopPropagation(); - }); +$("#site-nav .site-nav--list").click(function (event) { + event.stopPropagation(); +}); - /* Choose a shake dropdown */ - const $choose_a_shake = $("#choose-a-shake"); - var shake_expanded = false; - $("#choose-a-shake .choose-a-shake--toggle").click(function (event) { - event.stopPropagation(); - if (shake_expanded == false) { - shake_expanded = true; - $choose_a_shake.addClass("is-expanded"); - $("body").one("click", function () { - $choose_a_shake.removeClass("is-expanded"); - shake_expanded = false; - }); - } else { +/* Choose a shake dropdown */ +const $choose_a_shake = $("#choose-a-shake"); +var shake_expanded = false; +$("#choose-a-shake .choose-a-shake--toggle").click(function (event) { + event.stopPropagation(); + if (shake_expanded == false) { + shake_expanded = true; + $choose_a_shake.addClass("is-expanded"); + $("body").one("click", function () { $choose_a_shake.removeClass("is-expanded"); - $("body").unbind("click"); shake_expanded = false; - } - }); - - $("#choose-a-shake .choose-a-shake--dropdown").click(function (event) { - event.stopPropagation(); - }); - - /* Conversations - mute this button */ - $(".mute-this-conversation").click(function () { - var $form = $(this).next(".mute-this-conversation-form"); - var url = $form.attr("action"); - var data = $form.serialize(); - var $conversation = $(this).parents(".conversation"); - $.post(url, data, function () { - $conversation.fadeOut("slow"); }); - return false; - }); - - // http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity - function setCaret(el) { - ctrl = el; - pos = ctrl.value.length; - if (ctrl.setSelectionRange) { - ctrl.focus(); - ctrl.setSelectionRange(pos, pos); - } else if (ctrl.createTextRange) { - var range = ctrl.createTextRange(); - range.collapse(true); - range.moveEnd("character", pos); - range.moveStart("character", pos); - range.select(); - } - } - - var $image_comments_permalink = $("#image-comments-permalink"); - if ($image_comments_permalink.length > 0) { - var new_comment = new PermalinkCommentsView($image_comments_permalink); + } else { + $choose_a_shake.removeClass("is-expanded"); + $("body").unbind("click"); + shake_expanded = false; } +}); - $("#nsfw-filter-button a").click(function () { - $(this).parents("form").submit(); - return false; - }); +$("#choose-a-shake .choose-a-shake--dropdown").click(function (event) { + event.stopPropagation(); +}); - $("#apps .disconnect").click(function () { - if (confirm("Are you sure you want to disconnect this app?")) { - var $form = $(this).parent().next("form"); - var url = $form.attr("action"); - var data = $form.serialize(); - var parent = $(this).parents("li"); - $.post( - url, - data, - function (response) { - parent.hide("slow"); - }, - "ajax", - ); - return false; - } else { - return false; - } +/* Conversations - mute this button */ +$(".mute-this-conversation").click(function () { + var $form = $(this).next(".mute-this-conversation-form"); + var url = $form.attr("action"); + var data = $form.serialize(); + var $conversation = $(this).parents(".conversation"); + $.post(url, data, function () { + $conversation.fadeOut("slow"); }); + return false; +}); - // Tools: Recommended group shakes - if ($("#shake-categories").length > 0) { - $("#shake-categories .shake-category").each(function () { - var new_category = new RecommendedShakeCategory(this); - }); +// http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity +function setCaret(el) { + ctrl = el; + pos = ctrl.value.length; + if (ctrl.setSelectionRange) { + ctrl.focus(); + ctrl.setSelectionRange(pos, pos); + } else if (ctrl.createTextRange) { + var range = ctrl.createTextRange(); + range.collapse(true); + range.moveEnd("character", pos); + range.moveStart("character", pos); + range.select(); } +} - // Shake Page - change image. - $("#shake-image-edit").hover( - function () { - $(this).addClass("shake-image-hover"); - }, - function () { - $(this).removeClass("shake-image-hover"); - }, - ); - - // Shake Page: choosing file to upload. - $("#shake-image-edit input").change(function () { - $(this).closest("form").submit(); - }); - - // Shake Page: inline editing title & description: - $(".shake-edit-title-form .cancel").click(function () { - $(this).parents(".shake-details").find(".shake-edit-title").show(); - $(this).closest(".shake-edit-title-form").hide(); - return false; - }); - - $(".shake-edit-title").hover( - function () { - $(this).addClass("shake-edit-title-hover"); - }, - function () { - $(this).removeClass("shake-edit-title-hover"); - }, - ); - - $(".shake-edit-title").click(function () { - var $title_container = $(this).closest(".shake-details"); - var url = $title_container.find("form").attr("action"); - var that = this; +var $image_comments_permalink = $("#image-comments-permalink"); +if ($image_comments_permalink.length > 0) { + var new_comment = new PermalinkCommentsView($image_comments_permalink); +} - $.get( - url, - function (result) { - if ("title_raw" in result) { - $(that).hide(); - $title_container - .find(".shake-edit-title-input") - .val(result["title_raw"]); - $(that).next(".shake-edit-title-form").show(); - } - }, - "json", - ); - }); +$("#nsfw-filter-button a").click(function () { + $(this).parents("form").submit(); + return false; +}); - $(".shake-edit-title-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; +$("#apps .disconnect").click(function () { + if (confirm("Are you sure you want to disconnect this app?")) { + var $form = $(this).parent().next("form"); + var url = $form.attr("action"); + var data = $form.serialize(); + var parent = $(this).parents("li"); $.post( url, data, - function (result) { - if ("title" in result && "title_raw" in result) { - var $title_container = $(that).closest(".shake-details"); - $title_container - .find(".shake-edit-title") - .html(result["title"]) - .show(); - $title_container - .find(".shake-edit-title-input") - .val(result["title_raw"]); - $title_container.find(".shake-edit-title-form").hide(); - } + function (response) { + parent.hide("slow"); }, - "json", + "ajax", ); return false; - }); - - // Shake Page: Edit Description - $(".shake-edit-description-form .cancel").click(function () { - $(this) - .parents(".shake-details") - .find(".shake-edit-description") - .show(); - $(this).closest(".shake-edit-description-form").hide(); + } else { return false; - }); - - $(".shake-edit-description").hover( - function () { - $(this).addClass("shake-edit-description-hover"); - }, - function () { - $(this).removeClass("shake-edit-description-hover"); - }, - ); + } +}); - $(".shake-edit-description").click(function () { - var $title_container = $(this).closest(".shake-details"); - var url = $title_container.find("form").attr("action"); - var that = this; +// Tools: Recommended group shakes +if ($("#shake-categories").length > 0) { + $("#shake-categories .shake-category").each(function () { + var new_category = new RecommendedShakeCategory(this); + }); +} + +// Shake Page - change image. +$("#shake-image-edit").hover( + function () { + $(this).addClass("shake-image-hover"); + }, + function () { + $(this).removeClass("shake-image-hover"); + }, +); + +// Shake Page: choosing file to upload. +$("#shake-image-edit input").change(function () { + $(this).closest("form").submit(); +}); - $.get( - url, - function (result) { - if ("description_raw" in result) { - $(that).hide(); - $title_container - .find(".shake-edit-description-input") - .val(result["description_raw"]); - $(that).next(".shake-edit-description-form").show(); - } - }, - "json", - ); - }); +// Shake Page: inline editing title & description: +$(".shake-edit-title-form .cancel").click(function () { + $(this).parents(".shake-details").find(".shake-edit-title").show(); + $(this).closest(".shake-edit-title-form").hide(); + return false; +}); - $(".shake-edit-description-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("description" in result && "description_raw" in result) { - var $title_container = $(that).closest(".shake-details"); - $title_container - .find(".shake-edit-description") - .html(result["description"]) - .show(); - $title_container - .find(".shake-edit-description-input") - .val(result["description_raw"]); - $title_container - .find(".shake-edit-description-form") - .hide(); - } - }, - "json", - ); - return false; - }); +$(".shake-edit-title").hover( + function () { + $(this).addClass("shake-edit-title-hover"); + }, + function () { + $(this).removeClass("shake-edit-title-hover"); + }, +); + +$(".shake-edit-title").click(function () { + var $title_container = $(this).closest(".shake-details"); + var url = $title_container.find("form").attr("action"); + var that = this; + + $.get( + url, + function (result) { + if ("title_raw" in result) { + $(that).hide(); + $title_container + .find(".shake-edit-title-input") + .val(result["title_raw"]); + $(that).next(".shake-edit-title-form").show(); + } + }, + "json", + ); +}); - var $user_counts = $("#user-counts"); - if ($user_counts.length > 0) { - UserCounts.populate($user_counts); - } +$(".shake-edit-title-form").submit(function () { + var data = $(this).serialize(); + var url = $(this).attr("action"); + var that = this; + $.post( + url, + data, + function (result) { + if ("title" in result && "title_raw" in result) { + var $title_container = $(that).closest(".shake-details"); + $title_container + .find(".shake-edit-title") + .html(result["title"]) + .show(); + $title_container + .find(".shake-edit-title-input") + .val(result["title_raw"]); + $title_container.find(".shake-edit-title-form").hide(); + } + }, + "json", + ); + return false; +}); - /* Shake Page: Request invitation to shake */ - var $request_invitation = $("#request-invitation"); - if ($request_invitation.length > 0) { - request_invitation = new RequestInvitation($request_invitation); - } +// Shake Page: Edit Description +$(".shake-edit-description-form .cancel").click(function () { + $(this).parents(".shake-details").find(".shake-edit-description").show(); + $(this).closest(".shake-edit-description-form").hide(); + return false; +}); - // Button to remove from shake. - $(".remove-from-shake").click(function () { - $form = $(this).find("form").submit(); - return false; - }); +$(".shake-edit-description").hover( + function () { + $(this).addClass("shake-edit-description-hover"); + }, + function () { + $(this).removeClass("shake-edit-description-hover"); + }, +); + +$(".shake-edit-description").click(function () { + var $title_container = $(this).closest(".shake-details"); + var url = $title_container.find("form").attr("action"); + var that = this; + + $.get( + url, + function (result) { + if ("description_raw" in result) { + $(that).hide(); + $title_container + .find(".shake-edit-description-input") + .val(result["description_raw"]); + $(that).next(".shake-edit-description-form").show(); + } + }, + "json", + ); +}); - //make incoming clickable (I know.) - $(".incoming-header").click(function () { - document.location = - document.location.protocol + - "//" + - document.location.host + - "/incoming"; - }); +$(".shake-edit-description-form").submit(function () { + var data = $(this).serialize(); + var url = $(this).attr("action"); + var that = this; + $.post( + url, + data, + function (result) { + if ("description" in result && "description_raw" in result) { + var $title_container = $(that).closest(".shake-details"); + $title_container + .find(".shake-edit-description") + .html(result["description"]) + .show(); + $title_container + .find(".shake-edit-description-input") + .val(result["description_raw"]); + $title_container.find(".shake-edit-description-form").hide(); + } + }, + "json", + ); + return false; +}); - /* Shake Page: Remove Members From Shake */ - var $shake_member_list = $("#shake-members-list"); - if ($shake_member_list.length > 0) { - var shake_member_list = new ShakeMemberList($shake_member_list); - } +var $user_counts = $("#user-counts"); +if ($user_counts.length > 0) { + UserCounts.populate($user_counts); +} + +/* Shake Page: Request invitation to shake */ +var $request_invitation = $("#request-invitation"); +if ($request_invitation.length > 0) { + request_invitation = new RequestInvitation($request_invitation); +} + +// Button to remove from shake. +$(".remove-from-shake").click(function () { + $form = $(this).find("form").submit(); + return false; +}); - // support for dismissable "Vote" banner; - // cookie naturally expires the day after the election - var alertVote = $("#alert-vote"); - var alertVoteCookieVal = "dismiss-alert-vote=1"; - var alertVoteExpires = new Date("2024-11-06T00:00:00"); - if ( - document.cookie.indexOf(alertVoteCookieVal) === -1 && - new Date() < alertVoteExpires - ) { - alertVote.css({ display: "block" }); - alertVote.find("button").click(function () { - document.cookie = [ - alertVoteCookieVal, - "expires=" + alertVoteExpires.toGMTString(), - "path=/", - ].join("; "); - alertVote.css({ display: "none" }); - }); - } +//make incoming clickable (I know.) +$(".incoming-header").click(function () { + document.location = + document.location.protocol + + "//" + + document.location.host + + "/incoming"; +}); - // Support for sticky site header - const $siteHeader = $(".site-header"); - if ($siteHeader.length > 0) { - let lastScrollY = window.scrollY; - const scrollHandler = () => { - if (!$siteHeader.hasClass("docked")) { - if (window.scrollY > 120) { - $siteHeader.addClass("docked"); - } - } else { - if (window.scrollY <= 120) { - $siteHeader.removeClass("docked visible hidden"); - } +/* Shake Page: Remove Members From Shake */ +var $shake_member_list = $("#shake-members-list"); +if ($shake_member_list.length > 0) { + var shake_member_list = new ShakeMemberList($shake_member_list); +} + +// support for dismissable "Vote" banner; +// cookie naturally expires the day after the election +var alertVote = $("#alert-vote"); +var alertVoteCookieVal = "dismiss-alert-vote=1"; +var alertVoteExpires = new Date("2024-11-06T00:00:00"); +if ( + document.cookie.indexOf(alertVoteCookieVal) === -1 && + new Date() < alertVoteExpires +) { + alertVote.css({ display: "block" }); + alertVote.find("button").click(function () { + document.cookie = [ + alertVoteCookieVal, + "expires=" + alertVoteExpires.toGMTString(), + "path=/", + ].join("; "); + alertVote.css({ display: "none" }); + }); +} + +// Support for sticky site header +const $siteHeader = $(".site-header"); +if ($siteHeader.length > 0) { + let lastScrollY = window.scrollY; + const scrollHandler = () => { + if (!$siteHeader.hasClass("docked")) { + if (window.scrollY > 120) { + $siteHeader.addClass("docked"); } - if ($siteHeader.hasClass("docked")) { - const isVisible = $siteHeader.hasClass("visible"); - if (!isVisible && window.scrollY < lastScrollY) { - // triggering delta can be 1px - $siteHeader.addClass("visible"); - $siteHeader.removeClass("hidden"); - } else if (isVisible && window.scrollY > lastScrollY + 20) { - // triggering delta must be ~20px - $siteHeader.removeClass("visible"); - $siteHeader.addClass("hidden"); - } + } else { + if (window.scrollY <= 120) { + $siteHeader.removeClass("docked visible hidden"); } - lastScrollY = window.scrollY; - }; - $(window).on("scroll", scrollHandler); - } -}); + } + if ($siteHeader.hasClass("docked")) { + const isVisible = $siteHeader.hasClass("visible"); + if (!isVisible && window.scrollY < lastScrollY) { + // triggering delta can be 1px + $siteHeader.addClass("visible"); + $siteHeader.removeClass("hidden"); + } else if (isVisible && window.scrollY > lastScrollY + 20) { + // triggering delta must be ~20px + $siteHeader.removeClass("visible"); + $siteHeader.addClass("hidden"); + } + } + lastScrollY = window.scrollY; + }; + $(window).on("scroll", scrollHandler); +} From e7daabd169143d0771243071d8ddd854536c6cb4 Mon Sep 17 00:00:00 2001 From: James Beard Date: Wed, 8 Apr 2026 18:32:14 +1000 Subject: [PATCH 6/7] Converted any jQuery extended objects into plain JS modules. Depending on whether the associated client code is run once, multiple times, or generates concrete objet instances, went with plain module, closures over context variables, or full class definitions. Pulled setCaret and toText out into a common file for reuse. --- static/js/InviteMember.js | 233 +++++----- static/js/NSFWCover.js | 102 ++--- static/js/NewPostPanel.js | 339 ++++++++------- static/js/NotificationInvitationContainer.js | 52 +-- static/js/NotificationInvitationRequest.js | 76 ++-- static/js/PermalinkCommentsView.js | 49 ++- static/js/RecommendedShakeCategory.js | 70 +-- static/js/RequestInvitation.js | 43 +- static/js/SaveThisView.js | 206 ++++----- static/js/ShakeMemberList.js | 65 +-- static/js/SidebarStatsView.js | 428 ++++++++++--------- static/js/StreamStatsView.js | 154 +++---- static/js/StreamStatsViewRegistry.js | 24 +- static/js/UserCounts.js | 155 ++++--- static/js/common.js | 27 ++ static/js/main.js | 61 +-- 16 files changed, 1076 insertions(+), 1008 deletions(-) create mode 100644 static/js/common.js diff --git a/static/js/InviteMember.js b/static/js/InviteMember.js index 47c001d..a62d850 100644 --- a/static/js/InviteMember.js +++ b/static/js/InviteMember.js @@ -1,134 +1,131 @@ -// Invite Member widget for the Shake administrator. -var InviteMember = (function () { - var $main_module = $("#shake-invite-member"); - var $input_field = $main_module.find(".input-text"); - var $invite_button = $main_module.find(".invite-button"); - var $shake_results = $main_module.find(".shake-results"); - var $form = $main_module.find("form"); - var $title = $main_module.find("h3"); - var search_results = []; - var last_search = ""; - - $input_field.keyup(function (ev) { - InviteMember.search_names(ev); - }); +/** + * Functionality for "Invite A New Member" form shown in the sidebar of a shake + * the current user is an editor of. Lets the user type in a few characters, see + * an autocompleted list of matching usernames to pick from, and finally sends + * an invitation via the backend. + */ - $form.submit(function (ev) { - return InviteMember.submit_form(); - }); +// Invite Member widget for the Shake administrator. +var $main_module = $("#shake-invite-member"); +var $input_field = $main_module.find(".input-text"); +var $invite_button = $main_module.find(".invite-button"); +var $shake_results = $main_module.find(".shake-results"); +var $form = $main_module.find("form"); +var $title = $main_module.find("h3"); +var search_results = []; +var last_search = ""; + +const InviteMember = { + attachEvents: function () { + $input_field.keyup((ev) => this.search_names(ev)); + $form.submit((ev) => this.submit_form()); + $shake_results.click((ev) => this.select_user($(ev.target).text())); + $invite_button.click(() => { + this.send_invite(); + return false; + }); + }, - $shake_results.click(function (ev) { - InviteMember.select_user($(ev.target).text()); - }); + search_names: function () { + if ($input_field.val() == "") { + this.clear_results(); + this.clear_input(); + return false; + } - $invite_button.click(function () { - InviteMember.send_invite(); + // don't search again if field hasn't changed. + if ($input_field.val() == last_search) { + return false; + } + + last_search = $input_field.val(); + var data = $form.serialize(); + $.post( + "/account/quick_name_search", + data, + (response) => { + if ("users" in response) { + this.update_results(response["users"]); + } + }, + "json", + ); + }, + + update_results: function (users) { + search_results = users; + if (search_results.length == 0) { + this.clear_results(); + } else { + this.render_results(); + } + }, + + render_results: function () { + $shake_results.html("").show(); + for (var i = 0; i < search_results.length; i++) { + $shake_results.append( + '
  • ' + + search_results[i].name + + "
  • ", + ); + } + }, + + select_user: function (user_name) { + this.clear_results(); + $input_field.val(user_name); + $invite_button.removeAttr("disabled"); + }, + + submit_form: function (ev) { + if ( + search_results.length == 1 && + search_results[0].name == $input_field.val() + ) { + this.select_user(search_results[0].name); + this.send_invite(); + this.clear_results(); + } return false; - }); + }, - return { - search_names: function () { - if ($input_field.val() == "") { - this.clear_results(); - this.clear_input(); - return false; - } + clear_results: function () { + last_search = ""; + $shake_results.hide().html(""); + }, - // don't search again if field hasn't changed. - if ($input_field.val() == last_search) { - return false; - } + clear_input: function () { + $input_field.val(""); + $invite_button.attr("disabled", "disabled"); + }, - last_search = $input_field.val(); + send_invite: function () { + if ($invite_button.disabled) { + return false; + } else { + var url = $form.attr("action"); var data = $form.serialize(); - var that = this; $.post( - "/account/quick_name_search", + url, data, - function (response) { - if ("users" in response) { - that.update_results(response["users"]); - } + () => { + this.data_sent(); + return false; }, "json", ); - }, - - update_results: function (users) { - search_results = users; - if (search_results.length == 0) { - this.clear_results(); - } else { - this.render_results(); - } - }, - - render_results: function () { - $shake_results.html("").show(); - for (var i = 0; i < search_results.length; i++) { - $shake_results.append( - '
  • ' + - search_results[i].name + - "
  • ", - ); - } - }, - - select_user: function (user_name) { - this.clear_results(); - $input_field.val(user_name); - $invite_button.removeAttr("disabled"); - }, - - submit_form: function (ev) { - if ( - search_results.length == 1 && - search_results[0].name == $input_field.val() - ) { - this.select_user(search_results[0].name); - this.send_invite(); - this.clear_results(); - } return false; - }, - - clear_results: function () { - last_search = ""; - $shake_results.hide().html(""); - }, - - clear_input: function () { - $input_field.val(""); - $invite_button.attr("disabled", "disabled"); - }, - - send_invite: function () { - if ($invite_button.disabled) { - return false; - } else { - var url = $form.attr("action"); - var data = $form.serialize(); - $.post( - url, - data, - function () { - InviteMember.data_sent(); - return false; - }, - "json", - ); - return false; - } - }, - - data_sent: function () { - $title.html("Your invitation has been sent"); - this.clear_input(); - this.clear_results(); - }, - }; -})(); + } + }, + + data_sent: function () { + $title.html("Your invitation has been sent"); + this.clear_input(); + this.clear_results(); + }, +}; export { InviteMember }; diff --git a/static/js/NSFWCover.js b/static/js/NSFWCover.js index 602e300..642596e 100644 --- a/static/js/NSFWCover.js +++ b/static/js/NSFWCover.js @@ -1,57 +1,57 @@ -var NSFWCover = function ($root) { - this.$root = $root; - this.init(); -}; - -$.extend(NSFWCover.prototype, { - init: function () { - this.$root.delegate("a", "click", $.proxy(this.click_show_image, this)); - }, - - click_show_image: function (ev) { - var location = document.location, - host = location.host, - protocol = location.protocol, - base_path = location.protocol + "//" + location.host, - file_path = $(ev.target).attr("href"); - $.get( - base_path + - "/services/oembed?include_embed=1&url=" + - escape(base_path + file_path), - $.proxy(this.load_image, this), - "json", - ); - return false; - }, +const NSFWCover = { + attachEvents($root) { + // Per invocation functions that will close over the context dependent + // variables defined above. + function click_show_image(ev) { + var location = document.location, + host = location.host, + protocol = location.protocol, + base_path = location.protocol + "//" + location.host, + file_path = $(ev.target).attr("href"); + $.get( + base_path + + "/services/oembed?include_embed=1&url=" + + escape(base_path + file_path), + (resp) => load_image(resp), + "json", + ); + return false; + } - load_image: function (response) { - var parent = this.$root.parent(), - parent_height = parent.height(); + function load_image(response) { + var parent = $root.parent(), + parent_height = parent.height(); - if (response["type"] === "photo") { - parent - .css("min-height", parent_height + "px") - .html(''); - } else if (response["embed_html"]) { - parent - .css("min-height", parent_height + "px") - .html( - '
    ' + - response["embed_html"] + - "
    ", - ); - } else if (response["type"] === "video") { - var content = parent - .css("min-height", parent_height + "px") - .html( - response["html"].replace( - /', + ); + } else if (response["embed_html"]) { + parent + .css("min-height", parent_height + "px") + .html( + '
    ' + + response["embed_html"] + + "
    ", + ); + } else if (response["type"] === "video") { + var content = parent + .css("min-height", parent_height + "px") + .html( + response["html"].replace( + / click_show_image(ev)); }, -}); +}; export { NSFWCover }; diff --git a/static/js/NewPostPanel.js b/static/js/NewPostPanel.js index 107621d..8fb5a2d 100644 --- a/static/js/NewPostPanel.js +++ b/static/js/NewPostPanel.js @@ -1,181 +1,194 @@ -var NewPostPanel = (function () { - var panel_expanded = false; - - var $new_post_panel; - var $new_post_panel_inner; - var $new_post_button; - var $save_video_form; - var $save_video_form_button; - var $post_video_form; - var $post_video_form_button; - let $upload_image_input; - let $link_to_video; - let $video_shake_id; - let $shake_selector; - - var init_dom = function () { - $new_post_panel = $("#new-post-panel"); - $new_post_panel_inner = $("#new-post-panel .new-post-panel--inner"); - $new_post_button = $("#new-post-button"); - // upload image - $upload_image_input = $("#upload-image-input"); - // link to video - $link_to_video = $("#link-to-video"); - $video_shake_id = $("#video-shake-id"); - // video preview screen - $save_video_form = $("#new-post-panel .save-video-form"); - $save_video_form_button = $("#new-post-panel .save-video-form .btn"); - $post_video_form = $("#new-post-panel .post-video-form"); - $post_video_form_button = $("#new-post-panel .post-video-form .btn"); - // shake selector - $shake_selector = $(".shake-selector"); - }; - init_dom(); - - $new_post_button.click(function () { - NewPostPanel.load_new_post(); - return false; - }); +/** + * Functionality associated with the new post panel that pops up on any page + * allowing the user to post a new image or video to one of their shakes. + * + * Takes care of adding event listeners to the new post button, as well as to + * the shake dropdowns on both the image and video sides of the dialog. The new + * post dropdown slides in from the top of the screen. + */ +var $new_post_panel; +var $new_post_panel_inner; +var $new_post_button; +var $save_video_form; +var $save_video_form_button; +var $post_video_form; +var $post_video_form_button; +let $upload_image_input; +let $link_to_video; +let $video_shake_id; +let $shake_selector; +let panel_expanded = false; // unused? - // We don't want click event on panel to bubble up to body - // since a click to body closes the panel. - $new_post_panel.click(function (ev) { - ev.stopPropagation(); - }); +function remove_events() { + $save_video_form_button.unbind(); + $post_video_form_button.unbind(); + $shake_selector.unbind(); + $link_to_video.unbind(); +} +function init_dom() { + $new_post_panel = $("#new-post-panel"); + $new_post_panel_inner = $("#new-post-panel .new-post-panel--inner"); + $new_post_button = $("#new-post-button"); + // upload image + $upload_image_input = $("#upload-image-input"); + // link to video + $link_to_video = $("#link-to-video"); + $video_shake_id = $("#video-shake-id"); + // video preview screen + $save_video_form = $("#new-post-panel .save-video-form"); + $save_video_form_button = $("#new-post-panel .save-video-form .btn"); + $post_video_form = $("#new-post-panel .post-video-form"); + $post_video_form_button = $("#new-post-panel .post-video-form .btn"); + // shake selector + $shake_selector = $(".shake-selector"); +} + +function init_events() { // The events that are inside the panel that we want to initialize // when the panel loads. These are the events that are subject // to change depending on content that is loaded. - var init_events = function () { - $link_to_video.click(function () { - NewPostPanel.load_post_video(); + $link_to_video.click(function () { + NewPostPanel.load_post_video(); + return false; + }); + + $save_video_form_button.click(function (e) { + NewPostPanel.submit_save_video(); + return false; + }); + + $post_video_form_button.click(function (e) { + NewPostPanel.submit_post_video(); + return false; + }); + + $upload_image_input.change(function () { + $(this).closest("form").submit(); + }); + + $shake_selector.click(NewPostPanel.toggle_shake_selector); + $shake_selector.find("ul a").click(NewPostPanel.choose_shake); +} + +const NewPostPanel = { + attachEvents: function () { + init_dom(); + + $new_post_button.click(function () { + NewPostPanel.load_new_post(); return false; }); - $save_video_form_button.click(function (e) { - NewPostPanel.submit_save_video(); - return false; + // We don't want click event on panel to bubble up to body + // since a click to body closes the panel. + $new_post_panel.click(function (ev) { + ev.stopPropagation(); }); + }, - $post_video_form_button.click(function (e) { - NewPostPanel.submit_post_video(); - return false; + toggle_shake_selector: function (ev) { + $(this).toggleClass("is-active").find("ul").toggle(); + ev.stopPropagation(); + ev.preventDefault(); + }, + + // Sets the text of the shake to the chosen one and + // sets a hidden input field with the proper shake id. + choose_shake: function () { + var $shake_selector = $(this).parents(".shake-selector"); + var $selected_shake = $shake_selector.find(".green"); + var $selected_shake_input = $shake_selector.find("input"); + var name = $(this).html(); + var id = $(this) + .attr("id") + .replace(/[^0-9]+/, ""); + $selected_shake.html(name); + $selected_shake_input.val(id); + }, + + load_new_post: function () { + var url = "/tools/new-post"; + var that = this; + $.get(url, function (response) { + that.refresh_panel(response); + that.expand_panel(); }); + return false; + }, - $upload_image_input.change(function () { - $(this).closest("form").submit(); + load_post_video: function () { + if ($video_shake_id.length > 0) { + var shake_suffix = "?shake_id=" + $video_shake_id.val(); + } else { + var shake_suffix = ""; + } + var url = "/tools/save-video" + shake_suffix; + var that = this; + $.get(url, function (response) { + that.refresh_panel(response); + that.expand_panel(); }); + }, - $shake_selector.click(NewPostPanel.toggle_shake_selector); - $shake_selector.find("ul a").click(NewPostPanel.choose_shake); - }; + expand_panel: function () { + panel_expanded = true; + $new_post_panel.slideDown(); + var that = this; + $("body").one("click", $.proxy(this.close_panel, this)); + // we want to hide anything with a video since we can't + // overlap things like youtube embeds, which is an iframe + // that has an absolutely positioned flash element inside. + $(".the-image iframe").each(function () { + $(this).parent().css("height", $(this).height()); + $(this).parent().css("width", $(this).width()); + $(this).hide(); + }); + }, - var remove_events = function () { - $save_video_form_button.unbind(); - $post_video_form_button.unbind(); - $shake_selector.unbind(); - $link_to_video.unbind(); - }; + close_panel: function () { + panel_expanded = false; + $new_post_panel.hide(); + remove_events(); + // show the videos again. + $(".the-image iframe").show(); + }, - return { - toggle_shake_selector: function (ev) { - $(this).toggleClass("is-active").find("ul").toggle(); - ev.stopPropagation(); - ev.preventDefault(); - }, - // Sets the text of the shake to the chosen one and - // sets a hidden input field with the proper shake id. - choose_shake: function () { - var $shake_selector = $(this).parents(".shake-selector"); - var $selected_shake = $shake_selector.find(".green"); - var $selected_shake_input = $shake_selector.find("input"); - var name = $(this).html(); - var id = $(this) - .attr("id") - .replace(/[^0-9]+/, ""); - $selected_shake.html(name); - $selected_shake_input.val(id); - }, - load_new_post: function () { - var url = "/tools/new-post"; - var that = this; - $.get(url, function (response) { - that.refresh_panel(response); - that.expand_panel(); - }); - return false; - }, - load_post_video: function () { - if ($video_shake_id.length > 0) { - var shake_suffix = "?shake_id=" + $video_shake_id.val(); - } else { - var shake_suffix = ""; - } - var url = "/tools/save-video" + shake_suffix; - var that = this; - $.get(url, function (response) { - that.refresh_panel(response); - that.expand_panel(); - }); - }, - expand_panel: function () { - panel_expanded = true; - $new_post_panel.slideDown(); - var that = this; - $("body").one("click", $.proxy(this.close_panel, this)); - // we want to hide anything with a video since we can't - // overlap things like youtube embeds, which is an iframe - // that has an absolutely positioned flash element inside. - $(".the-image iframe").each(function () { - $(this).parent().css("height", $(this).height()); - $(this).parent().css("width", $(this).width()); - $(this).hide(); - }); - }, - close_panel: function () { - panel_expanded = false; - $new_post_panel.hide(); - remove_events(); - // show the videos again. - $(".the-image iframe").show(); - }, - submit_save_video: function () { - var url = $save_video_form.attr("action"); - var data = $save_video_form.serialize(); - var that = this; - $.get(url, data, function (response) { - that.refresh_panel(response); - }); - }, - submit_post_video: function () { - var url = $post_video_form.attr("action"); - var data = $post_video_form.serialize(); - var that = this; - $post_video_form_button - .unbind("click") - .find("span") - .html("Posting..."); - $.post( - url, - data, - function (response) { - document.location = - document.location.protocol + - "//" + - document.location.host + - response["path"]; - }, - "json", - ); - }, - refresh_panel: function (response) { - $new_post_panel_inner.html(response); - $new_post_panel_inner.html(); - remove_events(); - init_dom(); - init_events(); - }, - }; -})(); + submit_save_video: function () { + var url = $save_video_form.attr("action"); + var data = $save_video_form.serialize(); + var that = this; + $.get(url, data, function (response) { + that.refresh_panel(response); + }); + }, + + submit_post_video: function () { + var url = $post_video_form.attr("action"); + var data = $post_video_form.serialize(); + var that = this; + $post_video_form_button.unbind("click").find("span").html("Posting..."); + $.post( + url, + data, + function (response) { + document.location = + document.location.protocol + + "//" + + document.location.host + + response["path"]; + }, + "json", + ); + }, + + refresh_panel: function (response) { + $new_post_panel_inner.html(response); + $new_post_panel_inner.html(); + remove_events(); + init_dom(); + init_events(); + }, +}; export { NewPostPanel }; diff --git a/static/js/NotificationInvitationContainer.js b/static/js/NotificationInvitationContainer.js index 21cda31..5733102 100644 --- a/static/js/NotificationInvitationContainer.js +++ b/static/js/NotificationInvitationContainer.js @@ -1,29 +1,31 @@ -var NotificationInvitationContainer = function ($root) { - this.$root = $root; - this.init(); -}; +import { NotificationInvitationRequest } from "./NotificationInvitationRequest.js"; + +let on_shake_page; +let $hd; +let $bd; + +function update_count(count) { + if (!on_shake_page) { + var request_text = count == 1 ? " request" : " requests"; + var html = count + request_text + " to join a shake"; + $hd.html(html); + } else { + $hd.html("Got it!"); + } +} -$.extend(NotificationInvitationContainer.prototype, { - init: function () { - this.$hd = this.$root.find(".notification-block-hd"); - this.$bd = this.$root.find(".notification-block-bd"); - this.on_shake_page = this.$hd.hasClass("on-shake-page"); - var that = this; - this.$root.find(".notification").each(function () { - var new_invitation_request = new NotificationInvitationRequest( - $(this), - that, - ); +const NotificationInvitationContainer = { + populate: function ($root) { + $hd = $root.find(".notification-block-hd"); + $bd = $root.find(".notification-block-bd"); + on_shake_page = $hd.hasClass("on-shake-page"); + $root.find(".notification").each(function () { + // Attach events to each invitation, passing the update_count + // function as a callback for approval / disapproval clicks to + // update the container header. + NotificationInvitationRequest.attachEvents($(this), update_count); }); }, +}; - update_count: function (count) { - if (!this.on_shake_page) { - var request_text = count == 1 ? " request" : " requests"; - var html = count + request_text + " to join a shake"; - this.$hd.html(html); - } else { - this.$hd.html("Got it!"); - } - }, -}); +export { NotificationInvitationContainer }; diff --git a/static/js/NotificationInvitationRequest.js b/static/js/NotificationInvitationRequest.js index bcc2bf1..d590583 100644 --- a/static/js/NotificationInvitationRequest.js +++ b/static/js/NotificationInvitationRequest.js @@ -1,50 +1,40 @@ -var NotificationInvitationRequest = function ($root, container) { - this.$root = $root; - this.container = container; - this.init_dom(); - this.init_events(); -}; +const NotificationInvitationRequest = { + attachEvents: function ($root, updateFn) { + // const $form = $root.find("form"); + const $form_approve_invitation = $root.find(".approve-invitation"); + const $form_decline_invitation = $root.find(".decline-invitation"); -$.extend(NotificationInvitationRequest.prototype, { - init_dom: function () { - this.$form = this.$root.find("form"); - this.$form_approve_invitation = this.$root.find(".approve-invitation"); - this.$form_decline_invitation = this.$root.find(".decline-invitation"); - }, + function submit_approve_invitation(ev) { + ev.preventDefault(); + submit_form($form_approve_invitation); + } - init_events: function () { - this.$root.delegate( - ".approve-invitation", - "submit", - $.proxy(this.submit_approve_invitation, this), - ); - this.$root.delegate( - ".decline-invitation", - "submit", - $.proxy(this.submit_decline_invitation, this), - ); - }, + function submit_decline_invitation(ev) { + ev.preventDefault(); + submit_form($form_decline_invitation); + } - submit_approve_invitation: function (ev) { - ev.preventDefault(); - this.submit_form(this.$form_approve_invitation); - }, + function submit_form($form) { + var url = $form.attr("action"); + var data = $form.serialize(); + $.post(url, data, (resp) => clear_notification(resp), "json"); + } - submit_decline_invitation: function (ev) { - ev.preventDefault(); - this.submit_form(this.$form_decline_invitation); - }, + function clear_notification(response) { + if (response["status"] == "ok") { + $root.remove(); + updateFn(response["count"]); + } + } - submit_form: function ($form) { - var url = $form.attr("action"); - var data = $form.serialize(); - $.post(url, data, $.proxy(this.clear_notification, this), "json"); - }, + $root.delegate(".approve-invitation", "submit", (ev) => + submit_approve_invitation(ev), + ); - clear_notification: function (response) { - if (response["status"] == "ok") { - this.$root.remove(); - this.container.update_count(response["count"]); - } + $root.delegate(".decline-invitation", "submit", (ev) => + submit_decline_invitation(ev), + ); }, -}); +}; + +export { NotificationInvitationRequest }; diff --git a/static/js/PermalinkCommentsView.js b/static/js/PermalinkCommentsView.js index be26255..240e099 100644 --- a/static/js/PermalinkCommentsView.js +++ b/static/js/PermalinkCommentsView.js @@ -1,24 +1,27 @@ -var PermalinkCommentsView = function ($root) { - this.$root = $root; - this.init(); - this.init_events(); -}; +import { setCaret } from "./common.js"; -$.extend(PermalinkCommentsView.prototype, { - init: function () { - this.$post_comment_body = $("#post-comment-body"); - }, +/** + * Functionality associated with replying to or deleting a comment from a + * permalink page e.g. https://mltshp.com/p/1RNJS#post-comment + * + * Attaches event handlers to "reply" and "delete" (if owned by the current + * user) links on each existing comment. Only initialised if the + * #post-comment-body has some children. + */ + +let $root; +let $post_comment_body; - init_events: function () { - this.$root.delegate( - ".reply-to", - "click", - $.proxy(this.click_reply_to, this), +const PermalinkCommentsView = { + addEvents: function ($image_comments_permalink) { + $root = $image_comments_permalink; + $post_comment_body = $("#post-comment-body"); + + $root.delegate(".reply-to", "click", (ev) => + PermalinkCommentsView.click_reply_to(ev), ); - this.$root.delegate( - ".delete", - "click", - $.proxy(this.click_delete, this), + $root.delegate(".delete", "click", (ev) => + PermalinkCommentsView.click_delete(ev), ); }, @@ -27,9 +30,9 @@ $.extend(PermalinkCommentsView.prototype, { var $meta = $target.parent(); var username = $meta.find(".username").html(); var username_clean = username.replace(/[^a-zA-Z0-9_\-]+/g, ""); - var current_text = this.$post_comment_body.val(); - this.$post_comment_body.val(current_text + "@" + username_clean + " "); - setCaret(this.$post_comment_body.get(0)); + var current_text = $post_comment_body.val(); + $post_comment_body.val(current_text + "@" + username_clean + " "); + setCaret($post_comment_body.get(0)); window.location.hash = "post-comment"; return false; }, @@ -41,4 +44,6 @@ $.extend(PermalinkCommentsView.prototype, { } return false; }, -}); +}; + +export { PermalinkCommentsView }; diff --git a/static/js/RecommendedShakeCategory.js b/static/js/RecommendedShakeCategory.js index 2db084b..4384198 100644 --- a/static/js/RecommendedShakeCategory.js +++ b/static/js/RecommendedShakeCategory.js @@ -1,36 +1,46 @@ -var RecommendedShakeCategory = function (root) { - this.root = root; - this.$root = $(root); - this.fetched = false; - this.init_events(); -}; +/** + * Functionality associated with the shake categories accordian control on the + * find shakes page e.g. https://mltshp.com/tools/find-shakes + * + * Adds event listener to toggle category open and closed, and load shakes from + * the server upon first opening of a category. + */ -$.extend(RecommendedShakeCategory.prototype, { - init_events: function () { - this.$toggle = this.$root.find(".shake-category-toggle"); - this.$body = this.$root.find(".shake-category-body"); - this.$toggle.click($.proxy(this.click_toggle, this)); - }, +const RecommendedShakeCategory = { + attachEvents: function (root) { + const $root = $(root); + const $toggle = $root.find(".shake-category-toggle"); + const $body = $root.find(".shake-category-body"); + + let fetched = false; - click_toggle: function () { - if (!this.fetched) { - var url = - "/tools/find-shakes/quick-fetch-category/" + - this.$toggle.attr("href").replace("#", ""); - $.get(url, $.proxy(this.populate_results, this)); - } else { - this.toggle(); + // Per invocation functions that will close over the context dependent + // variables defined above. + function click_toggle() { + if (!fetched) { + var url = + "/tools/find-shakes/quick-fetch-category/" + + $toggle.attr("href").replace("#", ""); + $.get(url, (resp) => populate_results(resp)); + } else { + toggle(); + } + return false; } - return false; - }, - populate_results: function (results) { - this.fetched = true; - this.$body.html(results); - this.toggle(); - }, + function populate_results(results) { + fetched = true; + $body.html(results); + toggle(); + } + + function toggle(result) { + $root.toggleClass("shake-category-selected"); + } - toggle: function (result) { - this.$root.toggleClass("shake-category-selected"); + // Attach any event handlers. + $toggle.click(() => click_toggle()); }, -}); +}; + +export { RecommendedShakeCategory }; diff --git a/static/js/RequestInvitation.js b/static/js/RequestInvitation.js index a7fcc26..56a4323 100644 --- a/static/js/RequestInvitation.js +++ b/static/js/RequestInvitation.js @@ -1,30 +1,23 @@ -var RequestInvitation = function ($root) { - this.$root = $root; - this.init_dom(); - this.init_events(); -}; +const RequestInvitation = { + attachEvents: function ($root) { + const $form = $root.find("form"); -$.extend(RequestInvitation.prototype, { - init_dom: function () { - this.$form = this.$root.find("form"); - }, + // Per invocation functions that will close over the context dependent + // variables defined above. + function submit_request() { + var url = $form.attr("action"); + var data = $form.serialize(); + $.post(url, data, () => process_response()); + return false; + } - init_events: function () { - this.$root.delegate( - "form", - "submit", - $.proxy(this.submit_request, this), - ); - }, + function process_response() { + $root.html("Ok! Request sent."); + } - submit_request: function () { - var url = this.$form.attr("action"); - var data = this.$form.serialize(); - $.post(url, data, $.proxy(this.process_response, this)); - return false; + // Attach any event handlers. + $root.delegate("form", "submit", () => submit_request()); }, +}; - process_response: function () { - this.$root.html("Ok! Request sent."); - }, -}); +export { RequestInvitation }; diff --git a/static/js/SaveThisView.js b/static/js/SaveThisView.js index b7aedf1..5ecaee8 100644 --- a/static/js/SaveThisView.js +++ b/static/js/SaveThisView.js @@ -1,125 +1,125 @@ import { ShakesCache } from "./ShakesCache.js"; +import { SidebarStatsView } from "./SidebarStatsView.js"; +import { StreamStatsViewRegistry } from "./StreamStatsViewRegistry.js"; +import { toText } from "./common.js"; -var SaveThisView = function (container) { - this.$save_this = $(container); - this.init(); -}; - -$.extend(SaveThisView.prototype, { - init: function () { - this.init_dom(); - this.init_events(); - }, +/** + * Functionality related to the "save this" button on posts. Sets up event + * handlers for responding to buttons, dynamically generating and displaying + * the drop down box to select shakes from, and handles submitting the save. + * + * Although a global module the attachEvents function is called once per post, + * passing per post context as a parameter. + */ - init_dom: function () { - this.$save_this_link = this.$save_this.find(".save-this-link"); - this.$form = this.$save_this.find("form"); - this.$shake_id_input = this.$save_this.find(".shake-id-input"); - this.$shake_selector = $( +const SaveThisView = { + attachEvents: function (container) { + const $save_this = $(container); + const $save_this_link = $save_this.find(".save-this-link"); + const $form = $save_this.find("form"); + const $shake_id_input = $save_this.find(".shake-id-input"); + const $shake_selector = $( "
    ", ); - }, - init_events: function () { - this.$save_this_link.click($.proxy(this.click_save_this, this)); - this.$save_this.delegate( - ".shake-link", - "click", - $.proxy(this.click_choose_shake, this), - ); - this.$save_this.delegate( - ".close", - "click", - $.proxy(this.click_close_selector, this), - ); - }, + // Per invocation functions that will close over the context dependent + // variables defined above. + function click_save_this(ev) { + ev.stopPropagation(); + if ($save_this_link.hasClass("save-this-link-multiple")) { + show_shake_selector(); + } else { + submit_image_save(); + } + return false; + } - click_save_this: function (ev) { - ev.stopPropagation(); - if (this.$save_this_link.hasClass("save-this-link-multiple")) { - this.show_shake_selector(); - } else { - this.submit_image_save(); + function click_choose_shake(ev) { + ev.stopPropagation(); + var shake_id = ev.target.id.replace(/[^\d]+/, ""); + $shake_id_input.val(shake_id); + submit_image_save(); + return false; } - return false; - }, - click_choose_shake: function (ev) { - ev.stopPropagation(); - var shake_id = ev.target.id.replace(/[^\d]+/, ""); - this.$shake_id_input.val(shake_id); - this.submit_image_save(); - return false; - }, + function click_close_selector(ev) { + ev.stopPropagation(); + $shake_selector.remove(); + } - click_close_selector: function (ev) { - ev.stopPropagation(); - this.$shake_selector.remove(); - }, + function show_shake_selector() { + $save_this.append($shake_selector); + $("body").one("click", (ev) => click_close_selector(ev)); + + // Only query once per page. + if (ShakesCache.fetch() !== false) { + fetch_available_shakes(ShakesCache.fetch()); + } else { + $.get( + "/account/shakes", + (resp) => fetch_available_shakes(resp), + "json", + ); + } + } - show_shake_selector: function () { - this.$save_this.append(this.$shake_selector); - $("body").one("click", $.proxy(this.click_close_selector, this)); + function fetch_available_shakes(response) { + ShakesCache.store(response); + var html = '"; + $shake_selector + .removeClass("save-this-shake-selector-loading") + .html(html); + } - // Only query once per page. - if (ShakesCache.fetch() !== false) { - this.fetch_available_shakes(ShakesCache.fetch()); - } else { - $.get( - "/account/shakes", - $.proxy(this.fetch_available_shakes, this), + function submit_image_save(ev) { + var url = $form.attr("action"); + var data = $form.serialize(); + $.post( + url, + data, + (resp) => process_image_save_response(resp), "json", ); } - }, - fetch_available_shakes: function (response) { - ShakesCache.store(response); - var html = '
      '; - for (var i = 0; i < response["result"].length; i++) { - html += - '
    • ' + - response["result"][i]["name"] + - "
    • "; + function process_image_save_response(response) { + if (response["share_key"]) { + var count = response["count"]; + var share_key = response["share_key"]; + var new_share_key = response["new_share_key"]; + var count_string = toText(count, "Save"); + $("#save-count-amount-" + share_key).html(count_string); + var output = + ''; + $shake_selector.remove(); + $save_this.html(output); + SidebarStatsView.refresh_saves(); + StreamStatsViewRegistry.refresh_saves(share_key); + } else { + return false; + } } - html += "
    "; - this.$shake_selector - .removeClass("save-this-shake-selector-loading") - .html(html); - }, - submit_image_save: function (ev) { - var url = this.$form.attr("action"); - var data = this.$form.serialize(); - $.post( - url, - data, - $.proxy(this.process_image_save_response, this), - "json", + // Attach any event handlers. + $save_this_link.click((ev) => click_save_this(ev)); + $save_this.delegate(".shake-link", "click", (ev) => + click_choose_shake(ev), + ); + $save_this.delegate(".close", "click", (ev) => + click_close_selector(ev), ); }, - - process_image_save_response: function (response) { - if (response["share_key"]) { - var count = response["count"]; - var share_key = response["share_key"]; - var new_share_key = response["new_share_key"]; - var count_string = to_text(count, "Save"); - $("#save-count-amount-" + share_key).html(count_string); - var output = - ''; - this.$shake_selector.remove(); - this.$save_this.html(output); - SidebarStatsView.refresh_saves(); - StreamStatsViewRegistry.refresh_saves(share_key); - } else { - return false; - } - }, -}); +}; export { SaveThisView }; diff --git a/static/js/ShakeMemberList.js b/static/js/ShakeMemberList.js index 6b48d8d..746b084 100644 --- a/static/js/ShakeMemberList.js +++ b/static/js/ShakeMemberList.js @@ -1,35 +1,42 @@ -var ShakeMemberList = function ($root) { - this.$root = $root; - this.init_events(); -}; +/** + * Functionality associated with the edit shake members list, found on each + * shake page e.g. https://mltshp.com/AMearworm + * + * Attaches an event handler to each person in the list, allowing them to be + * removed as a member by the shake owner. + */ -$.extend(ShakeMemberList.prototype, { - init_events: function () { - this.$root.delegate( - ".remove-from-shake-button-link", - "click", - $.proxy(this.remove_from_shake, this), - ); - }, +function process_remove(elem) { + // li element of this user in the list. + elem.remove(); +} - remove_from_shake: function (ev) { - var $target = $(ev.target), - $li = $target.parents("li"), - $form = $target.next(), - url = $form.attr("action"); - data = $form.serialize(); +const ShakeMemberList = { + attachEvents: function ($root) { + // Per invocation functions that will close over the context dependent + // variable defined above. + function remove_from_shake(ev) { + var $target = $(ev.target), + $li = $target.parents("li"), + $form = $target.next(), + url = $form.attr("action"); + const data = $form.serialize(); - if ( - confirm( - "Are you sure you want to remove this user from a shake? If they have notifications on an email will be sent informing them of the change.", - ) - ) { - $.post(url, data, $.proxy(this.process_remove, $li)); + if ( + confirm( + "Are you sure you want to remove this user from a shake? If they have notifications on an email will be sent informing them of the change.", + ) + ) { + $.post(url, data, () => process_remove($li)); + } + return false; } - return false; - }, - process_remove: function (response) { - this.remove(); + // Attach any event handlers. + $root.delegate(".remove-from-shake-button-link", "click", (ev) => + remove_from_shake(ev), + ); }, -}); +}; + +export { ShakeMemberList }; diff --git a/static/js/SidebarStatsView.js b/static/js/SidebarStatsView.js index 0e23c0e..4d732d4 100644 --- a/static/js/SidebarStatsView.js +++ b/static/js/SidebarStatsView.js @@ -1,211 +1,237 @@ +import { toText } from "./common.js"; + +/** + * Funcionality associated with the stats shown in the sidebar of a permalink + * page e.g. https://mltshp.com/p/1RNNC + * + * Defines and attaches event handlers. + */ const DEFAULT_IMAGE_STATS = { save_count: 0, like_count: 0 }; -var SidebarStatsView = (function (scope) { - // if we aren't on a permalink page, just expose a dummy public API - if ($(scope).length == 0) { - return { - init: function () {}, - refresh_likes: function () {}, - refresh_saves: function () {}, - }; - } - - const image_stats = { ...DEFAULT_IMAGE_STATS }; - $save_count = $(".save-count", scope); - $like_count = $(".like-count", scope); - image_stats.save_count = parseInt($save_count.html(), 10); - image_stats.like_count = parseInt($like_count.html(), 10); - - var $save_button = $(".sidebar-stats-saves", scope); - var $like_button = $(".sidebar-stats-hearts", scope); - var $content = $(".sidebar-stats-content", scope); - - var saves_expanded = false; - var likes_expanded = false; - - return { - init: function () { - if (image_stats.save_count > 0) { - this.bind_saves(); - } else { - this.unbind_saves(); - } - if (image_stats.like_count > 0) { - this.bind_likes(); - this.toggle_likes(); - } else { - this.unbind_likes(); - } - }, - - refresh_likes: function () { - $like_count = $(".like-count", scope); - image_stats.like_count = parseInt($like_count.html(), 10); - if (likes_expanded) { - this.get_likes(); - } - if (image_stats.like_count > 0) { - this.bind_likes(); - } else { - likes_expanded = false; - this.unbind_likes(); - } - }, - - refresh_saves: function () { - $save_count = $(".save-count", scope); - image_stats.save_count = parseInt($save_count.html(), 10); - if (saves_expanded) { - this.get_saves(); - } - if (image_stats.save_count > 0) { - this.bind_saves(); - } else { - saves_expanded = false; - this.unbind_saves(); - } - }, - - bind_saves: function () { - $save_button.unbind("click"); - $save_button.addClass("enable-cursor"); - $save_button.click(function () { - SidebarStatsView.toggle_saves(); - }); - }, - - unbind_saves: function () { - $save_button.removeClass("enable-cursor"); - $save_button.unbind("click"); - this.collapse(); - }, - - bind_likes: function () { - $like_button.unbind("click"); - $like_button.addClass("enable-cursor"); - $like_button.click(function () { - SidebarStatsView.toggle_likes(); - }); - }, - - unbind_likes: function () { - $like_button.removeClass("enable-cursor"); - $like_button.unbind("click"); - this.collapse(); - }, - - toggle_saves: function () { +let scope; + +const image_stats = { ...DEFAULT_IMAGE_STATS }; + +// Initialise these once init() has been called with a valid scope. If never +// initialised, never referred to. +let $save_count; +let $like_count; + +let $save_button; +let $like_button; +let $content; + +let saves_expanded; +let likes_expanded; + +// if we aren't on a permalink page, just expose a dummy public API +function noScope() { + return scope === undefined || $(scope).length === 0; +} + +const SidebarStatsView = { + init: function (_scope) { + scope = _scope; + + if (noScope()) { + return; + } + + // Set all these now that we have been provided a meaningful scope. + $save_count = $(".save-count", scope); + $like_count = $(".like-count", scope); + image_stats.save_count = parseInt($save_count.html(), 10); + image_stats.like_count = parseInt($like_count.html(), 10); + + $save_button = $(".sidebar-stats-saves", scope); + $like_button = $(".sidebar-stats-hearts", scope); + $content = $(".sidebar-stats-content", scope); + + saves_expanded = false; + likes_expanded = false; + + if (image_stats.save_count > 0) { + this.bind_saves(); + } else { + this.unbind_saves(); + } + if (image_stats.like_count > 0) { + this.bind_likes(); + this.toggle_likes(); + } else { + this.unbind_likes(); + } + }, + + refresh_likes: function () { + if (noScope()) { + return; + } + + $like_count = $(".like-count", scope); + image_stats.like_count = parseInt($like_count.html(), 10); + if (likes_expanded) { + this.get_likes(); + } + if (image_stats.like_count > 0) { + this.bind_likes(); + } else { likes_expanded = false; - saves_expanded = !saves_expanded; - if (saves_expanded) { - $like_button.removeClass("selected"); - $content.addClass("loading").show(); - $save_button.addClass("selected"); - this.get_saves(); - } else { - this.collapse(); - } - }, - - get_saves: function () { - $.get( - document.location.pathname + "/saves", - function (response) { - if (response["result"]) { - SidebarStatsView.process_save(response); - } - }, - "json", - ); - }, - - toggle_likes: function () { + this.unbind_likes(); + } + }, + + refresh_saves: function () { + if (noScope()) { + return; + } + + $save_count = $(".save-count", scope); + image_stats.save_count = parseInt($save_count.html(), 10); + if (saves_expanded) { + this.get_saves(); + } + if (image_stats.save_count > 0) { + this.bind_saves(); + } else { saves_expanded = false; - likes_expanded = !likes_expanded; - if (likes_expanded) { - $save_button.removeClass("selected"); - $content.addClass("loading").show(); - $like_button.addClass("selected"); - this.get_likes(); - } else { - this.collapse(); - } - }, - - get_likes: function () { - $.get( - document.location.pathname + "/likes", - function (response) { - if (response["result"]) { - SidebarStatsView.process_like(response); - } - }, - "json", - ); - }, - - process_save: function (response) { - if (response["count"] == 0) { - this.disable_saves(); - } else { - $save_count.html(this.to_text(response["count"], "Save")); - this.render_content(response); - } - }, - - process_like: function (response) { - if (response["count"] == 0) { - this.unbind_likes(); - } else { - $like_count.html(this.to_text(response["count"], "Like")); - this.render_content(response); - } - }, - - to_text: function (num, base) { - return num == 1 - ? num + " " + "" + base + "" - : num + " " + "" + base + "s" + ""; - }, - - collapse: function (repsponse) { + this.unbind_saves(); + } + }, + + bind_saves: function () { + $save_button.unbind("click"); + $save_button.addClass("enable-cursor"); + $save_button.click(function () { + SidebarStatsView.toggle_saves(); + }); + }, + + unbind_saves: function () { + $save_button.removeClass("enable-cursor"); + $save_button.unbind("click"); + this.collapse(); + }, + + bind_likes: function () { + $like_button.unbind("click"); + $like_button.addClass("enable-cursor"); + $like_button.click(function () { + SidebarStatsView.toggle_likes(); + }); + }, + + unbind_likes: function () { + $like_button.removeClass("enable-cursor"); + $like_button.unbind("click"); + this.collapse(); + }, + + toggle_saves: function () { + likes_expanded = false; + saves_expanded = !saves_expanded; + if (saves_expanded) { $like_button.removeClass("selected"); + $content.addClass("loading").show(); + $save_button.addClass("selected"); + this.get_saves(); + } else { + this.collapse(); + } + }, + + get_saves: function () { + $.get( + document.location.pathname + "/saves", + function (response) { + if (response["result"]) { + SidebarStatsView.process_save(response); + } + }, + "json", + ); + }, + + toggle_likes: function () { + saves_expanded = false; + likes_expanded = !likes_expanded; + if (likes_expanded) { $save_button.removeClass("selected"); - $content.hide(); - }, - - render_content: function (response) { - var html = ""; - for (var i = 0, len = response["result"].length; i < len; i++) { - var result = response["result"][i]; - var link; - if (result["action"] == "save") { - // for saves, we link to the saved post - link = result["post_url"]; - } else { - link = "/user/" + result["user_name"]; + $content.addClass("loading").show(); + $like_button.addClass("selected"); + this.get_likes(); + } else { + this.collapse(); + } + }, + + get_likes: function () { + $.get( + document.location.pathname + "/likes", + function (response) { + if (response["result"]) { + SidebarStatsView.process_like(response); } - html += '
    '; - html += ''; - html += - ''; - html += - '' + - result["user_name"] + - ""; - html += - '' + - result["posted_at_friendly"] + - ""; - html += "
    "; + }, + "json", + ); + }, + + process_save: function (response) { + if (response["count"] == 0) { + this.disable_saves(); + } else { + $save_count.html(toText(response["count"], "Save")); + this.render_content(response); + } + }, + + process_like: function (response) { + if (response["count"] == 0) { + this.unbind_likes(); + } else { + $like_count.html(toText(response["count"], "Like")); + this.render_content(response); + } + }, + + collapse: function (repsponse) { + $like_button.removeClass("selected"); + $save_button.removeClass("selected"); + $content.hide(); + }, + + render_content: function (response) { + var html = ""; + for (var i = 0, len = response["result"].length; i < len; i++) { + var result = response["result"][i]; + var link; + if (result["action"] == "save") { + // for saves, we link to the saved post + link = result["post_url"]; + } else { + link = "/user/" + result["user_name"]; } - $content.removeClass("loading").html(html); - }, - }; -})("#sidebar-stats"); + html += '
    '; + html += ''; + html += + ''; + html += + '' + + result["user_name"] + + ""; + html += + '' + + result["posted_at_friendly"] + + ""; + html += "
    "; + } + $content.removeClass("loading").html(html); + }, +}; export { SidebarStatsView }; diff --git a/static/js/StreamStatsView.js b/static/js/StreamStatsView.js index 7ad43d6..2ad2576 100644 --- a/static/js/StreamStatsView.js +++ b/static/js/StreamStatsView.js @@ -1,24 +1,37 @@ -var StreamStatsView = function ($image_content_footer) { - this.$image_content_footer = $image_content_footer; - this.share_key = this.$image_content_footer - .attr("id") - .replace("image-content-footer-", ""); - this.can_submit_comments = true; - this.init_dom(); - this.init_events(); -}; - -$.extend(StreamStatsView.prototype, { - init_dom: function () { +import { setCaret } from "./common.js"; + +/** + * Functionality associated with the stats and comments section of each post on + * a list page e.g. https://mltshp.com/incoming, + * https://mltshp.com/CurrentListening, or https://mltshp.com/user/LocalStain + * + * Attaches event handlers to "likes", "comments", and "saves" tabs, as well as + * event handlers and processing for replaying, deleting, and adding commetns. + * One instance created per post on the page, and managed by the global + * StreamStatsViewRegistry module. + */ + +class StreamStatsView { + constructor($image_content_footer) { + this.$image_content_footer = $image_content_footer; + this.share_key = this.$image_content_footer + .attr("id") + .replace("image-content-footer-", ""); + this.can_submit_comments = true; + this.init_dom(); + this.init_events(); + } + + init_dom() { this.$likes_button = this.$image_content_footer.find(".likes"); this.$saves_button = this.$image_content_footer.find(".saves"); this.$comments_button = this.$image_content_footer.find(".comments"); this.$inline_details = this.$image_content_footer.find(".inline-details"); this.init_comment_dom(); - }, + } - init_comment_dom: function () { + init_comment_dom() { this.$post_comment_inline = this.$inline_details.find( ".post-comment-inline", ); @@ -33,43 +46,41 @@ $.extend(StreamStatsView.prototype, { this.$comment = this.$inline_details.find(".comment"); this.$reply_to = this.$inline_details.find(".reply-to"); this.$delete = this.$inline_details.find(".delete"); - }, + } - init_events: function () { - this.$likes_button.click($.proxy(this.click_like, this)); - this.$saves_button.click($.proxy(this.click_saves, this)); - this.$comments_button.click($.proxy(this.click_comments, this)); + init_events() { + this.$likes_button.click((ev) => this.click_like(ev)); + this.$saves_button.click((ev) => this.click_saves(ev)); + this.$comments_button.click((ev) => this.click_comments(ev)); this.init_comment_events(); - }, + } - init_comment_events: function () { - this.$comment_textarea.click( - $.proxy(this.click_comment_textarea, this), - ); - this.$show_more_comments.click($.proxy(this.click_more_comments, this)); - this.$reply_to.click($.proxy(this.click_reply_to, this)); - this.$delete.click($.proxy(this.click_delete, this)); + init_comment_events() { + this.$comment_textarea.click((ev) => this.click_comment_textarea(ev)); + this.$show_more_comments.click((ev) => this.click_more_comments(ev)); + this.$reply_to.click((ev) => this.click_reply_to(ev)); + this.$delete.click((ev) => this.click_delete(ev)); // Fix for Webkit bug where textarea looses focus incorrectly on mouseup. // http://code.google.com/p/chromium/issues/detail?id=4505 this.$comment_textarea.mouseup(function (e) { e.preventDefault(); }); - this.$comment_form.submit($.proxy(this.submit_comment, this)); - }, + this.$comment_form.submit((ev) => this.submit_comment(ev)); + } // Removes selected state from all tabs. - clear_tab_selection: function () { + clear_tab_selection() { this.$likes_button.removeClass("selected"); this.$saves_button.removeClass("selected"); this.$comments_button.removeClass("selected"); - }, + } // Start the "loading" state transition. - start_loading: function () { + start_loading() { this.$inline_details.addClass("inline-details-loading").html("").show(); - }, + } - user_html: function (data) { + user_html(data) { var html = ""; for (var i = 0; i < data.result.length; i++) { var result = data.result[i]; @@ -91,9 +102,9 @@ $.extend(StreamStatsView.prototype, { ""; } return html; - }, + } - click_like: function () { + click_like() { if (this.$likes_button.hasClass("selected")) { this.clear_tab_selection(); this.$inline_details.hide(); @@ -104,20 +115,20 @@ $.extend(StreamStatsView.prototype, { this.start_loading(); this.load_likes(); return false; - }, + } - load_likes: function () { + load_likes() { var url = "/p/" + this.share_key + "/likes"; - $.get(url, $.proxy(this.process_like_response, this), "json"); - }, + $.get(url, (ev) => this.process_like_response(ev), "json"); + } - process_like_response: function (data) { + process_like_response(data) { var html = '
    ' + this.user_html(data) + "
    "; this.$inline_details.html(html); - }, + } - click_saves: function () { + click_saves() { if (this.$saves_button.hasClass("selected")) { this.clear_tab_selection(); this.$inline_details.hide(); @@ -129,14 +140,14 @@ $.extend(StreamStatsView.prototype, { this.start_loading(); this.load_saves(); return false; - }, + } - load_saves: function () { + load_saves() { var url = "/p/" + this.share_key + "/saves"; - $.get(url, $.proxy(this.process_like_response, this), "json"); - }, + $.get(url, (ev) => this.process_like_response(ev), "json"); + } - click_comments: function () { + click_comments() { if (this.$comments_button.hasClass("selected")) { this.clear_tab_selection(); this.$inline_details.hide(); @@ -147,17 +158,17 @@ $.extend(StreamStatsView.prototype, { this.$comments_button.addClass("selected"); this.start_loading(); var url = "/p/" + this.share_key + "/quick-comments"; - $.get(url, $.proxy(this.process_comments_response, this), "json"); + $.get(url, (ev) => this.process_comments_response(ev), "json"); return false; - }, + } - click_more_comments: function () { + click_more_comments() { this.$comment.show(); this.$show_more_comments.hide(); return false; - }, + } - process_comments_response: function (data) { + process_comments_response(data) { this.can_submit_comments = true; if (data["result"] == "ok") { this.$comments_button.find("a").html(data["count"]); @@ -165,9 +176,9 @@ $.extend(StreamStatsView.prototype, { this.init_comment_dom(); this.init_comment_events(); } - }, + } - click_reply_to: function (ev) { + click_reply_to(ev) { this.click_comment_textarea(); var username = $(ev.target) .parents(".comment") @@ -178,9 +189,9 @@ $.extend(StreamStatsView.prototype, { this.$comment_textarea.val(current_text + "@" + username_clean + " "); setCaret(this.$comment_textarea.get(0)); return false; - }, + } - click_delete: function (ev) { + click_delete(ev) { var $delete_form = $("#" + ev.target.id + "-form"), url = $delete_form.attr("action"), data = $delete_form.serialize(); @@ -188,49 +199,44 @@ $.extend(StreamStatsView.prototype, { $.post( url, data, - $.proxy(this.process_comments_response, this), + (ev) => this.process_comments_response(ev), "json", ); } return false; - }, + } - submit_comment: function () { + submit_comment() { if (this.can_submit_comments === false) { return false; } this.can_submit_comments = false; var url = this.$comment_form.attr("action"); var data = this.$comment_form.serialize(); - $.post( - url, - data, - $.proxy(this.process_comments_response, this), - "json", - ); + $.post(url, data, (ev) => this.process_comments_response(ev), "json"); return false; - }, + } - click_comment_textarea: function (e) { + click_comment_textarea(e) { this.$post_comment_inline.addClass("post-comment-inline-expanded"); if (this.$comment_textarea.val().indexOf("Write a comment") === 0) { this.$comment_textarea.val(""); } this.click_more_comments(); this.$comment_textarea.css("min-height", "60px"); - }, + } - refresh_likes: function () { + refresh_likes() { if (this.$likes_button.hasClass("selected")) { this.load_likes(); } - }, + } - refresh_saves: function () { + refresh_saves() { if (this.$saves_button.hasClass("selected")) { this.load_saves(); } - }, -}); + } +} export { StreamStatsView }; diff --git a/static/js/StreamStatsViewRegistry.js b/static/js/StreamStatsViewRegistry.js index f5b6d8e..009cc1d 100644 --- a/static/js/StreamStatsViewRegistry.js +++ b/static/js/StreamStatsViewRegistry.js @@ -1,23 +1,33 @@ +/** + * A singleton registry of StreamStatsView objects. These objects are created + * one per post on a page, and contain logic and event handlers for stats and + * comments of each individual post. + */ + +const files_on_page = {}; + +const get_view = function (share_key) { + return files_on_page[share_key]; +}; + var StreamStatsViewRegistry = { - files_on_page: {}, register: function (view) { - this.files_on_page[view.share_key] = view; + files_on_page[view.share_key] = view; }, + refresh_likes: function (share_key) { - view = this.get_view(share_key); + var view = get_view(share_key); if (view !== undefined) { view.refresh_likes(); } }, + refresh_saves: function (share_key) { - view = this.get_view(share_key); + var view = get_view(share_key); if (view !== undefined) { view.refresh_saves(); } }, - get_view: function (share_key) { - return this.files_on_page[share_key]; - }, }; export { StreamStatsViewRegistry }; diff --git a/static/js/UserCounts.js b/static/js/UserCounts.js index 6a3bfe2..da19164 100644 --- a/static/js/UserCounts.js +++ b/static/js/UserCounts.js @@ -1,93 +1,92 @@ -var UserCounts = (function () { - var $root; +/** + * Functionality to update the statistics (views, saves, likes) in the sidebar + * of a user page e.g. https://mltshp.com/user/epski + * + * Populates latest values from a backend API call on page load. Unsure why not + * just server side generated. + */ - return { - populate($user_counts) { - $root = $user_counts; - var name = $root.attr("name"); - $.get( - "/user/" + name + "/counts", - function (result) { - UserCounts.display_results(result); - }, - "json", - ); - }, - display_results: function (result) { +function format(str_num) { + return Number.parseInt(str_num).toLocaleString(); +} + +function formatBrief(str_num) { + // Format number in a way that won't need excessive space to + // display. Abbreviate with suffixes and limit to one + // decimal place. + + const n = parseInt(str_num, 10); + + // Handle garbage input (somewhat) gracefully. + if (Number.isNaN(n)) { + return "0"; + } + + // Anything up to and including 9,999 return verbatim as + // we've got four characters minimum to play with. + if (n < 10000) { + return n.toLocaleString(); + } + + const suffixes = [ + { threshold: 1e12, suffix: "T" }, + { threshold: 1e9, suffix: "B" }, + { threshold: 1e6, suffix: "M" }, + { threshold: 1e3, suffix: "K" }, + ]; + + for (const { threshold, suffix } of suffixes) { + // Iterate until we find a suffix that can handle this + // value. + if (n >= threshold) { + // Truncate to 1 decimal place. + const truncated = Math.floor((n / threshold) * 10) / 10; + + // If the decimal part is 0 trim it. + if (truncated % 1 === 0) { + return truncated.toFixed(0) + suffix; + } else { + return truncated.toFixed(1) + suffix; + } + } + } + + return n.toLocaleString(); +} + +var $root; + +const UserCounts = { + populate: function ($user_counts) { + $root = $user_counts; + var name = $root.attr("name"); + + function display_results(result) { if ("views" in result) { $root .find(".views") - .attr( - "title", - UserCounts.format(result["views"]) + " views", - ) + .attr("title", format(result["views"]) + " views") .find(".num") - .html(UserCounts.formatBrief(result["views"])); + .html(formatBrief(result["views"])); $root .find(".saves") - .attr( - "title", - UserCounts.format(result["saves"]) + " saves", - ) + .attr("title", format(result["saves"]) + " saves") .find(".num") - .html(UserCounts.formatBrief(result["saves"])); + .html(formatBrief(result["saves"])); $root .find(".likes") - .attr( - "title", - UserCounts.format(result["likes"]) + " likes", - ) + .attr("title", format(result["likes"]) + " likes") .find(".num") - .html(UserCounts.formatBrief(result["likes"])); - } - }, - format: function (str_num) { - return Number.parseInt(str_num).toLocaleString(); - }, - formatBrief: function (str_num) { - // Format number in a way that won't need excessive space to - // display. Abbreviate with suffixes and limit to one - // decimal place. - - const n = parseInt(str_num, 10); - - // Handle garbage input (somewhat) gracefully. - if (Number.isNaN(n)) { - return "0"; - } - - // Anything up to and including 9,999 return verbatim as - // we've got four characters minimum to play with. - if (n < 10000) { - return n.toLocaleString(); - } - - const suffixes = [ - { threshold: 1e12, suffix: "T" }, - { threshold: 1e9, suffix: "B" }, - { threshold: 1e6, suffix: "M" }, - { threshold: 1e3, suffix: "K" }, - ]; - - for (const { threshold, suffix } of suffixes) { - // Iterate until we find a suffix that can handle this - // value. - if (n >= threshold) { - // Truncate to 1 decimal place. - const truncated = Math.floor((n / threshold) * 10) / 10; - - // If the decimal part is 0 trim it. - if (truncated % 1 === 0) { - return truncated.toFixed(0) + suffix; - } else { - return truncated.toFixed(1) + suffix; - } - } + .html(formatBrief(result["likes"])); } + } - return n.toLocaleString(); - }, - }; -})(); + $.get( + "/user/" + name + "/counts", + (result) => display_results(result), + "json", + ); + }, +}; export { UserCounts }; diff --git a/static/js/common.js b/static/js/common.js new file mode 100644 index 0000000..e8547ed --- /dev/null +++ b/static/js/common.js @@ -0,0 +1,27 @@ +/** + * Common utility functions. + */ + +// http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity +function setCaret(el) { + const ctrl = el; + const pos = ctrl.value.length; + if (ctrl.setSelectionRange) { + ctrl.focus(); + ctrl.setSelectionRange(pos, pos); + } else if (ctrl.createTextRange) { + var range = ctrl.createTextRange(); + range.collapse(true); + range.moveEnd("character", pos); + range.moveStart("character", pos); + range.select(); + } +} + +function toText(num, base) { + return num == 1 + ? num + " " + "" + base + "" + : num + " " + "" + base + "s" + ""; +} + +export { setCaret, toText }; diff --git a/static/js/main.js b/static/js/main.js index 0e87d7e..60901e7 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,19 +1,22 @@ /* For now the core JS behavior needed accross the site */ +import { InviteMember } from "./InviteMember.js"; +import { NewPostPanel } from "./NewPostPanel.js"; +import { NotificationInvitationContainer } from "./NotificationInvitationContainer.js"; +import { NSFWCover } from "./NSFWCover.js"; +import { PermalinkCommentsView } from "./PermalinkCommentsView.js"; +import { RecommendedShakeCategory } from "./RecommendedShakeCategory.js"; +import { RequestInvitation } from "./RequestInvitation.js"; import { SaveThisView } from "./SaveThisView.js"; +import { ShakeMemberList } from "./ShakeMemberList.js"; import { SidebarStatsView } from "./SidebarStatsView.js"; import { StreamStatsView } from "./StreamStatsView.js"; import { StreamStatsViewRegistry } from "./StreamStatsViewRegistry.js"; -import { NSFWCover } from "./NSFWCover.js"; -import { NewPostPanel } from "./NewPostPanel.js"; -import { InviteMember } from "./InviteMember.js"; import { UserCounts } from "./UserCounts.js"; +import { toText } from "./common.js"; -var to_text = function (num, base) { - return num == 1 - ? num + " " + "" + base + "" - : num + " " + "" + base + "s" + ""; -}; +NewPostPanel.attachEvents(); +InviteMember.attachEvents(); function screen_reader_focus(el) { el.setAttribute("tabindex", "0"); @@ -22,7 +25,7 @@ function screen_reader_focus(el) { } $(".save-this").each(function () { - var save_this_view = new SaveThisView(this); + SaveThisView.attachEvents(this); }); // when we hit enter on a form, we want to submit it even though we don't have @@ -293,12 +296,6 @@ $(".delete-from-shakes-form").click(function () { /* Like / Unlike button */ $(".like-button, .unlike-button").click(function () { - var to_text = function (num, base) { - return num == 1 - ? num + " " + "" + base + "" - : num + " " + "" + base + "s" + ""; - }; - var $form = $(this).parents("form"); var $buttons = $form.children("button"); var url = $form.attr("action"); @@ -313,7 +310,7 @@ $(".like-button, .unlike-button").click(function () { } else { var count = response["count"]; var share_key = response["share_key"]; - var count_string = to_text(count, "Like"); + var count_string = toText(count, "Like"); $("#like-count-amount-" + share_key).html(count_string); if (response["like"] === true) { $form.attr("action", "/p/" + share_key + "/unlike"); @@ -330,7 +327,7 @@ $(".like-button, .unlike-button").click(function () { return false; }); -SidebarStatsView.init(); +SidebarStatsView.init("#sidebar-stats"); function apply_hover_for_video(sel) { sel.hover(function () { @@ -348,7 +345,9 @@ $(".image-content").each(function () { $nsfw_cover = $image_content.find(".nsfw-cover"); var stream_stats_view = new StreamStatsView($image_footer); StreamStatsViewRegistry.register(stream_stats_view); - var nsfw_cover = new NSFWCover($nsfw_cover); + if ($nsfw_cover.length > 0) { + NSFWCover.attachEvents($nsfw_cover); + } }); apply_hover_for_video($(".image-content video.autoplay")); @@ -536,7 +535,7 @@ var init_notification_invitation_request = function () { "#notification-block-invitation-request", ); if ($notification_invitation_request.length > 0) { - var invitation_requests = new NotificationInvitationContainer( + NotificationInvitationContainer.populate( $notification_invitation_request, ); } @@ -618,25 +617,9 @@ $(".mute-this-conversation").click(function () { return false; }); -// http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity -function setCaret(el) { - ctrl = el; - pos = ctrl.value.length; - if (ctrl.setSelectionRange) { - ctrl.focus(); - ctrl.setSelectionRange(pos, pos); - } else if (ctrl.createTextRange) { - var range = ctrl.createTextRange(); - range.collapse(true); - range.moveEnd("character", pos); - range.moveStart("character", pos); - range.select(); - } -} - var $image_comments_permalink = $("#image-comments-permalink"); if ($image_comments_permalink.length > 0) { - var new_comment = new PermalinkCommentsView($image_comments_permalink); + PermalinkCommentsView.addEvents($image_comments_permalink); } $("#nsfw-filter-button a").click(function () { @@ -667,7 +650,7 @@ $("#apps .disconnect").click(function () { // Tools: Recommended group shakes if ($("#shake-categories").length > 0) { $("#shake-categories .shake-category").each(function () { - var new_category = new RecommendedShakeCategory(this); + RecommendedShakeCategory.attachEvents(this); }); } @@ -816,7 +799,7 @@ if ($user_counts.length > 0) { /* Shake Page: Request invitation to shake */ var $request_invitation = $("#request-invitation"); if ($request_invitation.length > 0) { - request_invitation = new RequestInvitation($request_invitation); + RequestInvitation.attachEvents($request_invitation); } // Button to remove from shake. @@ -837,7 +820,7 @@ $(".incoming-header").click(function () { /* Shake Page: Remove Members From Shake */ var $shake_member_list = $("#shake-members-list"); if ($shake_member_list.length > 0) { - var shake_member_list = new ShakeMemberList($shake_member_list); + ShakeMemberList.attachEvents($shake_member_list); } // support for dismissable "Vote" banner; From 5c5402a01721b34022591ce51b97e7d0e382187c Mon Sep 17 00:00:00 2001 From: James Beard Date: Wed, 15 Apr 2026 22:05:46 +1000 Subject: [PATCH 7/7] General conversion to more modern JS e.g. var -> let.+ const, template strings. Removed quite a few jQuery .post() and .get() in favour of built in fetch(). Renamed variables to more widely used JS scheme. Switched to async event handlers as a flow on from that in many places. --- static/js/InviteMember.js | 149 ++- static/js/NSFWCover.js | 38 +- static/js/NewPostPanel.js | 218 +++-- static/js/NotificationInvitationContainer.js | 22 +- static/js/NotificationInvitationRequest.js | 36 +- static/js/PermalinkCommentsView.js | 34 +- static/js/RecommendedShakeCategory.js | 12 +- static/js/RequestInvitation.js | 26 +- static/js/SaveThisView.js | 124 ++- static/js/ShakeMemberList.js | 12 +- static/js/ShakesCache.js | 2 +- static/js/SidebarStatsView.js | 222 ++--- static/js/StreamStatsView.js | 314 +++--- static/js/StreamStatsViewRegistry.js | 22 +- static/js/UserCounts.js | 27 +- static/js/common.js | 12 +- static/js/main.js | 964 +++++++++---------- 17 files changed, 1104 insertions(+), 1130 deletions(-) diff --git a/static/js/InviteMember.js b/static/js/InviteMember.js index a62d850..b7f0cf5 100644 --- a/static/js/InviteMember.js +++ b/static/js/InviteMember.js @@ -6,125 +6,122 @@ */ // Invite Member widget for the Shake administrator. -var $main_module = $("#shake-invite-member"); -var $input_field = $main_module.find(".input-text"); -var $invite_button = $main_module.find(".invite-button"); -var $shake_results = $main_module.find(".shake-results"); -var $form = $main_module.find("form"); -var $title = $main_module.find("h3"); -var search_results = []; -var last_search = ""; +const $mainModule = $("#shake-invite-member"); +const $inputField = $mainModule.find(".input-text"); +const $inviteButton = $mainModule.find(".invite-button"); +const $shakeResults = $mainModule.find(".shake-results"); +const $form = $mainModule.find("form"); +const $title = $mainModule.find("h3"); +let searchResults = []; +let lastSearch = ""; const InviteMember = { attachEvents: function () { - $input_field.keyup((ev) => this.search_names(ev)); - $form.submit((ev) => this.submit_form()); - $shake_results.click((ev) => this.select_user($(ev.target).text())); - $invite_button.click(() => { - this.send_invite(); + $inputField.keyup((ev) => this.searchNames(ev)); + $form.submit((ev) => this.submitForm()); + $shakeResults.click((ev) => this.selectUser($(ev.target).text())); + $inviteButton.click(() => { + this.sendInvite(); return false; }); }, - search_names: function () { - if ($input_field.val() == "") { - this.clear_results(); - this.clear_input(); + searchNames: async function () { + if ($inputField.val() == "") { + this.clearResults(); + this.clearInput(); return false; } // don't search again if field hasn't changed. - if ($input_field.val() == last_search) { + if ($inputField.val() == lastSearch) { return false; } + lastSearch = $inputField.val(); - last_search = $input_field.val(); - var data = $form.serialize(); - $.post( - "/account/quick_name_search", - data, - (response) => { - if ("users" in response) { - this.update_results(response["users"]); - } - }, - "json", - ); + const data = $form.serialize(); + const resp = await fetch("/account/quick_name_search", { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + + if ("users" in json) { + this.updateResults(json["users"]); + } }, - update_results: function (users) { - search_results = users; - if (search_results.length == 0) { - this.clear_results(); + updateResults: function (users) { + searchResults = users; + if (searchResults.length == 0) { + this.clearResults(); } else { - this.render_results(); + this.renderResults(); } }, - render_results: function () { - $shake_results.html("").show(); - for (var i = 0; i < search_results.length; i++) { - $shake_results.append( - '
  • ' + - search_results[i].name + - "
  • ", + renderResults: function () { + $shakeResults.html("").show(); + for (let i = 0; i < searchResults.length; i++) { + $shakeResults.append( + `
  • + + ${searchResults[i].name} +
  • `, ); } }, - select_user: function (user_name) { - this.clear_results(); - $input_field.val(user_name); - $invite_button.removeAttr("disabled"); + selectUser: function (userName) { + this.clearResults(); + $inputField.val(userName); + $inviteButton.removeAttr("disabled"); }, - submit_form: function (ev) { + submitForm: function (ev) { if ( - search_results.length == 1 && - search_results[0].name == $input_field.val() + searchResults.length == 1 && + searchResults[0].name == $inputField.val() ) { - this.select_user(search_results[0].name); - this.send_invite(); - this.clear_results(); + this.selectUser(searchResults[0].name); + this.sendInvite(); + this.clearResults(); } return false; }, - clear_results: function () { - last_search = ""; - $shake_results.hide().html(""); + clearResults: function () { + lastSearch = ""; + $shakeResults.hide().html(""); }, - clear_input: function () { - $input_field.val(""); - $invite_button.attr("disabled", "disabled"); + clearInput: function () { + $inputField.val(""); + $inviteButton.attr("disabled", "disabled"); }, - send_invite: function () { - if ($invite_button.disabled) { + sendInvite: async function () { + if ($inviteButton.disabled) { return false; } else { - var url = $form.attr("action"); - var data = $form.serialize(); - $.post( - url, - data, - () => { - this.data_sent(); - return false; - }, - "json", - ); + const url = $form.attr("action"); + const data = $form.serialize(); + + await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + + this.dataSent(); return false; } }, - data_sent: function () { + dataSent: function () { $title.html("Your invitation has been sent"); - this.clear_input(); - this.clear_results(); + this.clearInput(); + this.clearResults(); }, }; diff --git a/static/js/NSFWCover.js b/static/js/NSFWCover.js index 642596e..bc693d1 100644 --- a/static/js/NSFWCover.js +++ b/static/js/NSFWCover.js @@ -1,36 +1,44 @@ +import { applyHoverForVideo } from "./common.js"; + +/** + * Functionality associated with NSFW covers. Called on to attach event + * handlers to any posts that the server has generated a NSFW cover for. + */ + const NSFWCover = { attachEvents($root) { // Per invocation functions that will close over the context dependent // variables defined above. - function click_show_image(ev) { + function clickShowImage(ev) { var location = document.location, - host = location.host, - protocol = location.protocol, - base_path = location.protocol + "//" + location.host, - file_path = $(ev.target).attr("href"); + basePath = location.protocol + "//" + location.host, + filePath = $(ev.target).attr("href"); + // Going to leave this as a jquery get rather than migrate to fetch + // right away. The two implementations behave differently and only + // the existing method seems to work with /services/oembed $.get( - base_path + + basePath + "/services/oembed?include_embed=1&url=" + - escape(base_path + file_path), - (resp) => load_image(resp), + escape(basePath + filePath), + (resp) => loadImage(resp), "json", ); return false; } - function load_image(response) { + function loadImage(response) { var parent = $root.parent(), - parent_height = parent.height(); + parentHeight = parent.height(); if (response["type"] === "photo") { parent - .css("min-height", parent_height + "px") + .css("min-height", parentHeight + "px") .html( '', ); } else if (response["embed_html"]) { parent - .css("min-height", parent_height + "px") + .css("min-height", parentHeight + "px") .html( '
    ' + response["embed_html"] + @@ -38,19 +46,19 @@ const NSFWCover = { ); } else if (response["type"] === "video") { var content = parent - .css("min-height", parent_height + "px") + .css("min-height", parentHeight + "px") .html( response["html"].replace( / click_show_image(ev)); + $root.delegate("a", "click", (ev) => clickShowImage(ev)); }, }; diff --git a/static/js/NewPostPanel.js b/static/js/NewPostPanel.js index 8fb5a2d..77bc5d9 100644 --- a/static/js/NewPostPanel.js +++ b/static/js/NewPostPanel.js @@ -6,88 +6,98 @@ * the shake dropdowns on both the image and video sides of the dialog. The new * post dropdown slides in from the top of the screen. */ -var $new_post_panel; -var $new_post_panel_inner; -var $new_post_button; -var $save_video_form; -var $save_video_form_button; -var $post_video_form; -var $post_video_form_button; -let $upload_image_input; -let $link_to_video; -let $video_shake_id; -let $shake_selector; -let panel_expanded = false; // unused? - -function remove_events() { - $save_video_form_button.unbind(); - $post_video_form_button.unbind(); - $shake_selector.unbind(); - $link_to_video.unbind(); +let $newPostPanel; +let $newPostPanelInner; +let $newPostButton; +let $saveVideoForm; +let $saveVideoFormButton; +let $postVideoForm; +let $postVideoFormButton; +let $uploadImageInput; +let $linkToVideo; +let $videoShakeId; +let $shakeSelector; + +function removeEvents() { + $saveVideoFormButton.unbind(); + $postVideoFormButton.unbind(); + $shakeSelector.unbind(); + $linkToVideo.unbind(); } -function init_dom() { - $new_post_panel = $("#new-post-panel"); - $new_post_panel_inner = $("#new-post-panel .new-post-panel--inner"); - $new_post_button = $("#new-post-button"); +function initDom() { + $newPostPanel = $("#new-post-panel"); + $newPostPanelInner = $("#new-post-panel .new-post-panel--inner"); + $newPostButton = $("#new-post-button"); // upload image - $upload_image_input = $("#upload-image-input"); + $uploadImageInput = $("#upload-image-input"); // link to video - $link_to_video = $("#link-to-video"); - $video_shake_id = $("#video-shake-id"); + $linkToVideo = $("#link-to-video"); + $videoShakeId = $("#video-shake-id"); // video preview screen - $save_video_form = $("#new-post-panel .save-video-form"); - $save_video_form_button = $("#new-post-panel .save-video-form .btn"); - $post_video_form = $("#new-post-panel .post-video-form"); - $post_video_form_button = $("#new-post-panel .post-video-form .btn"); + $saveVideoForm = $("#new-post-panel .save-video-form"); + $saveVideoFormButton = $("#new-post-panel .save-video-form .btn"); + $postVideoForm = $("#new-post-panel .post-video-form"); + $postVideoFormButton = $("#new-post-panel .post-video-form .btn"); // shake selector - $shake_selector = $(".shake-selector"); + $shakeSelector = $(".shake-selector"); } -function init_events() { +function initEvents() { // The events that are inside the panel that we want to initialize // when the panel loads. These are the events that are subject // to change depending on content that is loaded. - $link_to_video.click(function () { - NewPostPanel.load_post_video(); + + // Video upload step 1. + // Called when user clicks "video" link and takes them to a new form where + // they can enter the video url. + $linkToVideo.click(function () { + NewPostPanel.loadPostVideo(); return false; }); - $save_video_form_button.click(function (e) { - NewPostPanel.submit_save_video(); + // Video upload step 2. + // Called when the user clicks "Go get it!" button which takes the user to a + // new form containing a preview of the video. + $saveVideoFormButton.click(function (e) { + NewPostPanel.submitSaveVideo(); return false; }); - $post_video_form_button.click(function (e) { - NewPostPanel.submit_post_video(); + // Video upload step 3. + // Called when the user clicks "Yes! Post it please!" which uploads the + // video via submitPostVideo(). + $postVideoFormButton.click(function (e) { + NewPostPanel.submitPostVideo(); return false; }); - $upload_image_input.change(function () { + // Uploads the image file upon selection by the file upload dialog. + $uploadImageInput.change(function () { $(this).closest("form").submit(); }); - $shake_selector.click(NewPostPanel.toggle_shake_selector); - $shake_selector.find("ul a").click(NewPostPanel.choose_shake); + $shakeSelector.click(NewPostPanel.toggleShakeSelector); + $shakeSelector.find("ul a").click(NewPostPanel.chooseShake); } const NewPostPanel = { attachEvents: function () { - init_dom(); + initDom(); - $new_post_button.click(function () { - NewPostPanel.load_new_post(); + $newPostButton.click(function () { + NewPostPanel.loadNewPost(); return false; }); // We don't want click event on panel to bubble up to body // since a click to body closes the panel. - $new_post_panel.click(function (ev) { + $newPostPanel.click(function (ev) { ev.stopPropagation(); }); }, - toggle_shake_selector: function (ev) { + toggleShakeSelector: function (ev) { $(this).toggleClass("is-active").find("ul").toggle(); ev.stopPropagation(); ev.preventDefault(); @@ -95,45 +105,44 @@ const NewPostPanel = { // Sets the text of the shake to the chosen one and // sets a hidden input field with the proper shake id. - choose_shake: function () { - var $shake_selector = $(this).parents(".shake-selector"); - var $selected_shake = $shake_selector.find(".green"); - var $selected_shake_input = $shake_selector.find("input"); - var name = $(this).html(); - var id = $(this) + chooseShake: function () { + const $shakeSelector = $(this).parents(".shake-selector"); + const $selectedShake = $shakeSelector.find(".green"); + const $selectedShakeInput = $shakeSelector.find("input"); + const name = $(this).html(); + const id = $(this) .attr("id") .replace(/[^0-9]+/, ""); - $selected_shake.html(name); - $selected_shake_input.val(id); + $selectedShake.html(name); + $selectedShakeInput.val(id); }, - load_new_post: function () { + // Renders step 1 of image / video upload process - shake choice and file + // type. + loadNewPost: async function () { var url = "/tools/new-post"; - var that = this; - $.get(url, function (response) { - that.refresh_panel(response); - that.expand_panel(); - }); + const resp = await fetch(url); + + this.refreshPanel(await resp.text()); + this.expandPanel(); return false; }, - load_post_video: function () { - if ($video_shake_id.length > 0) { - var shake_suffix = "?shake_id=" + $video_shake_id.val(); - } else { - var shake_suffix = ""; + // Renders step 2 of the video upload process - entering the url. + loadPostVideo: async function () { + let shakeSuffix = ""; + if ($videoShakeId.length > 0) { + shakeSuffix = "?shake_id=" + $videoShakeId.val(); } - var url = "/tools/save-video" + shake_suffix; - var that = this; - $.get(url, function (response) { - that.refresh_panel(response); - that.expand_panel(); - }); + const url = `/tools/save-video${shakeSuffix}`; + + const resp = await fetch(url); + this.refreshPanel(await resp.text()); + this.expandPanel(); }, - expand_panel: function () { - panel_expanded = true; - $new_post_panel.slideDown(); + expandPanel: function () { + $newPostPanel.slideDown(); var that = this; $("body").one("click", $.proxy(this.close_panel, this)); // we want to hide anything with a video since we can't @@ -147,47 +156,44 @@ const NewPostPanel = { }, close_panel: function () { - panel_expanded = false; - $new_post_panel.hide(); - remove_events(); + $newPostPanel.hide(); + removeEvents(); // show the videos again. $(".the-image iframe").show(); }, - submit_save_video: function () { - var url = $save_video_form.attr("action"); - var data = $save_video_form.serialize(); - var that = this; - $.get(url, data, function (response) { - that.refresh_panel(response); - }); + // Renders step 3 of the video upload process - previewing the video. + submitSaveVideo: async function () { + const url = $saveVideoForm.attr("action"); + const data = $saveVideoForm.serialize(); + + const resp = await fetch(`${url}?${new URLSearchParams(data)}`); + this.refreshPanel(await resp.text()); }, - submit_post_video: function () { - var url = $post_video_form.attr("action"); - var data = $post_video_form.serialize(); - var that = this; - $post_video_form_button.unbind("click").find("span").html("Posting..."); - $.post( - url, - data, - function (response) { - document.location = - document.location.protocol + - "//" + - document.location.host + - response["path"]; - }, - "json", - ); + // Final step of video upload - submitting the post details to the server. + submitPostVideo: async function () { + const url = $postVideoForm.attr("action"); + const data = $postVideoForm.serialize(); + $postVideoFormButton.unbind("click").find("span").html("Posting..."); + + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + + // Redirect to the new post permalink page. + document.location = + document.location.protocol + + `//${document.location.host}${json["path"]}`; }, - refresh_panel: function (response) { - $new_post_panel_inner.html(response); - $new_post_panel_inner.html(); - remove_events(); - init_dom(); - init_events(); + refreshPanel: function (html) { + $newPostPanelInner.html(html); + removeEvents(); + initDom(); + initEvents(); }, }; diff --git a/static/js/NotificationInvitationContainer.js b/static/js/NotificationInvitationContainer.js index 5733102..1c5b8d1 100644 --- a/static/js/NotificationInvitationContainer.js +++ b/static/js/NotificationInvitationContainer.js @@ -1,24 +1,24 @@ import { NotificationInvitationRequest } from "./NotificationInvitationRequest.js"; -let on_shake_page; -let $hd; -let $bd; +let onShakePage; +let $header; +let $body; function update_count(count) { - if (!on_shake_page) { - var request_text = count == 1 ? " request" : " requests"; - var html = count + request_text + " to join a shake"; - $hd.html(html); + if (!onShakePage) { + const requestText = count === 1 ? "request" : "requests"; + const html = `${count} ${requestText} to join a shake`; + $header.html(html); } else { - $hd.html("Got it!"); + $header.html("Got it!"); } } const NotificationInvitationContainer = { populate: function ($root) { - $hd = $root.find(".notification-block-hd"); - $bd = $root.find(".notification-block-bd"); - on_shake_page = $hd.hasClass("on-shake-page"); + $header = $root.find(".notification-block-hd"); + $body = $root.find(".notification-block-bd"); + onShakePage = $header.hasClass("on-shake-page"); $root.find(".notification").each(function () { // Attach events to each invitation, passing the update_count // function as a callback for approval / disapproval clicks to diff --git a/static/js/NotificationInvitationRequest.js b/static/js/NotificationInvitationRequest.js index d590583..cd9c88c 100644 --- a/static/js/NotificationInvitationRequest.js +++ b/static/js/NotificationInvitationRequest.js @@ -1,38 +1,42 @@ const NotificationInvitationRequest = { - attachEvents: function ($root, updateFn) { - // const $form = $root.find("form"); - const $form_approve_invitation = $root.find(".approve-invitation"); - const $form_decline_invitation = $root.find(".decline-invitation"); + attachEvents: function ($root, updateCallbackFn) { + const $formApproveInvitation = $root.find(".approve-invitation"); + const $formDeclineInvitation = $root.find(".decline-invitation"); - function submit_approve_invitation(ev) { + function submitApproveInvitation(ev) { ev.preventDefault(); - submit_form($form_approve_invitation); + submitForm($formApproveInvitation); } - function submit_decline_invitation(ev) { + function submitDeclineInvitation(ev) { ev.preventDefault(); - submit_form($form_decline_invitation); + submitForm($formDeclineInvitation); } - function submit_form($form) { - var url = $form.attr("action"); - var data = $form.serialize(); - $.post(url, data, (resp) => clear_notification(resp), "json"); + async function submitForm($form) { + const url = $form.attr("action"); + const data = $form.serialize(); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + clearNotification(json); } - function clear_notification(response) { + function clearNotification(response) { if (response["status"] == "ok") { $root.remove(); - updateFn(response["count"]); + updateCallbackFn(response["count"]); } } $root.delegate(".approve-invitation", "submit", (ev) => - submit_approve_invitation(ev), + submitApproveInvitation(ev), ); $root.delegate(".decline-invitation", "submit", (ev) => - submit_decline_invitation(ev), + submitDeclineInvitation(ev), ); }, }; diff --git a/static/js/PermalinkCommentsView.js b/static/js/PermalinkCommentsView.js index 240e099..1e380ae 100644 --- a/static/js/PermalinkCommentsView.js +++ b/static/js/PermalinkCommentsView.js @@ -10,37 +10,37 @@ import { setCaret } from "./common.js"; */ let $root; -let $post_comment_body; +let $postCommentBody; const PermalinkCommentsView = { - addEvents: function ($image_comments_permalink) { - $root = $image_comments_permalink; - $post_comment_body = $("#post-comment-body"); + addEvents: function ($imageCommentsPermalink) { + $root = $imageCommentsPermalink; + $postCommentBody = $("#post-comment-body"); $root.delegate(".reply-to", "click", (ev) => - PermalinkCommentsView.click_reply_to(ev), + PermalinkCommentsView.clickReplyTo(ev), ); $root.delegate(".delete", "click", (ev) => - PermalinkCommentsView.click_delete(ev), + PermalinkCommentsView.clickDelete(ev), ); }, - click_reply_to: function (ev) { - var $target = $(ev.target); - var $meta = $target.parent(); - var username = $meta.find(".username").html(); - var username_clean = username.replace(/[^a-zA-Z0-9_\-]+/g, ""); - var current_text = $post_comment_body.val(); - $post_comment_body.val(current_text + "@" + username_clean + " "); - setCaret($post_comment_body.get(0)); + clickReplyTo: function (ev) { + const $target = $(ev.target); + const $meta = $target.parent(); + const username = $meta.find(".username").html(); + const usernameClean = username.replace(/[^a-zA-Z0-9_\-]+/g, ""); + const currentText = $postCommentBody.val(); + $postCommentBody.val(currentText + "@" + usernameClean + " "); + setCaret($postCommentBody.get(0)); window.location.hash = "post-comment"; return false; }, - click_delete: function (ev) { - var $delete_form = $("#" + ev.target.id + "-form"); + clickDelete: function (ev) { + const $deleteForm = $("#" + ev.target.id + "-form"); if (confirm("Are you sure you want to delete this?")) { - $delete_form.submit(); + $deleteForm.submit(); } return false; }, diff --git a/static/js/RecommendedShakeCategory.js b/static/js/RecommendedShakeCategory.js index 4384198..884bc31 100644 --- a/static/js/RecommendedShakeCategory.js +++ b/static/js/RecommendedShakeCategory.js @@ -16,19 +16,21 @@ const RecommendedShakeCategory = { // Per invocation functions that will close over the context dependent // variables defined above. - function click_toggle() { + async function clickToggle() { if (!fetched) { - var url = + const url = "/tools/find-shakes/quick-fetch-category/" + $toggle.attr("href").replace("#", ""); - $.get(url, (resp) => populate_results(resp)); + + const resp = await fetch(url); + populateResults(await resp.text()); } else { toggle(); } return false; } - function populate_results(results) { + function populateResults(results) { fetched = true; $body.html(results); toggle(); @@ -39,7 +41,7 @@ const RecommendedShakeCategory = { } // Attach any event handlers. - $toggle.click(() => click_toggle()); + $toggle.click(() => clickToggle()); }, }; diff --git a/static/js/RequestInvitation.js b/static/js/RequestInvitation.js index 56a4323..b544d2e 100644 --- a/static/js/RequestInvitation.js +++ b/static/js/RequestInvitation.js @@ -1,22 +1,40 @@ +/** + * Functionality related to a user requesting an invitation to join a shake. + * This module attaches an event handler to the "Join this shake" button, and + * persists a request for invitation to the server for the shaken owner to sed. + */ + const RequestInvitation = { attachEvents: function ($root) { const $form = $root.find("form"); // Per invocation functions that will close over the context dependent // variables defined above. - function submit_request() { + async function submitRequest() { var url = $form.attr("action"); var data = $form.serialize(); - $.post(url, data, () => process_response()); + $.post(url, data, () => processResponse()); + + // Work in progress. Seems to be some difference in behaviour + // between the two techniques. + // const url = $form.attr("action"); + // const data = $form.serialize(); + // console.log(url, data); + // await fetch(url, { + // method: "POST", + // body: new URLSearchParams(data), + // }); + // processResponse(); + return false; } - function process_response() { + function processResponse() { $root.html("Ok! Request sent."); } // Attach any event handlers. - $root.delegate("form", "submit", () => submit_request()); + $root.delegate("form", "submit", () => submitRequest()); }, }; diff --git a/static/js/SaveThisView.js b/static/js/SaveThisView.js index 5ecaee8..d429165 100644 --- a/static/js/SaveThisView.js +++ b/static/js/SaveThisView.js @@ -14,111 +14,105 @@ import { toText } from "./common.js"; const SaveThisView = { attachEvents: function (container) { - const $save_this = $(container); - const $save_this_link = $save_this.find(".save-this-link"); - const $form = $save_this.find("form"); - const $shake_id_input = $save_this.find(".shake-id-input"); - const $shake_selector = $( + const $saveThis = $(container); + const $saveThisLink = $saveThis.find(".save-this-link"); + const $form = $saveThis.find("form"); + const $shakeIdInput = $saveThis.find(".shake-id-input"); + const $shakeSelector = $( "
    ", ); // Per invocation functions that will close over the context dependent // variables defined above. - function click_save_this(ev) { + function clickSaveThis(ev) { ev.stopPropagation(); - if ($save_this_link.hasClass("save-this-link-multiple")) { - show_shake_selector(); + if ($saveThisLink.hasClass("save-this-link-multiple")) { + showShakeSelector(); } else { - submit_image_save(); + submitImageSave(); } return false; } - function click_choose_shake(ev) { + function clickChooseShake(ev) { ev.stopPropagation(); - var shake_id = ev.target.id.replace(/[^\d]+/, ""); - $shake_id_input.val(shake_id); - submit_image_save(); + const shakeId = ev.target.id.replace(/[^\d]+/, ""); + $shakeIdInput.val(shakeId); + submitImageSave(); return false; } - function click_close_selector(ev) { + function clickCloseSelector(ev) { ev.stopPropagation(); - $shake_selector.remove(); + $shakeSelector.remove(); } - function show_shake_selector() { - $save_this.append($shake_selector); - $("body").one("click", (ev) => click_close_selector(ev)); + async function showShakeSelector() { + $saveThis.append($shakeSelector); + $("body").one("click", (ev) => clickCloseSelector(ev)); // Only query once per page. if (ShakesCache.fetch() !== false) { - fetch_available_shakes(ShakesCache.fetch()); + fetchAvailableShakes(ShakesCache.fetch()); } else { - $.get( - "/account/shakes", - (resp) => fetch_available_shakes(resp), - "json", - ); + const resp = await fetch("/account/shakes"); + const json = await resp.json(); + fetchAvailableShakes(json); } } - function fetch_available_shakes(response) { + function fetchAvailableShakes(response) { ShakesCache.store(response); - var html = '
      '; - for (var i = 0; i < response["result"].length; i++) { - html += - '
    • ' + - response["result"][i]["name"] + - "
    • "; + let html = '"; - $shake_selector + $shakeSelector .removeClass("save-this-shake-selector-loading") .html(html); } - function submit_image_save(ev) { - var url = $form.attr("action"); - var data = $form.serialize(); - $.post( - url, - data, - (resp) => process_image_save_response(resp), - "json", - ); + async function submitImageSave(ev) { + const url = $form.attr("action"); + const data = $form.serialize(); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + processImageSaveResponse(json); } - function process_image_save_response(response) { + function processImageSaveResponse(response) { if (response["share_key"]) { - var count = response["count"]; - var share_key = response["share_key"]; - var new_share_key = response["new_share_key"]; - var count_string = toText(count, "Save"); - $("#save-count-amount-" + share_key).html(count_string); - var output = - ''; - $shake_selector.remove(); - $save_this.html(output); - SidebarStatsView.refresh_saves(); - StreamStatsViewRegistry.refresh_saves(share_key); - } else { - return false; + const count = response["count"]; + const shareKey = response["share_key"]; + const newShareKey = response["new_share_key"]; + const countString = toText(count, "Save"); + $("#save-count-amount-" + shareKey).html(countString); + const output = ` + + + `; + $shakeSelector.remove(); + $saveThis.html(output); + SidebarStatsView.refreshSaves(); + StreamStatsViewRegistry.refreshSaves(shareKey); } } // Attach any event handlers. - $save_this_link.click((ev) => click_save_this(ev)); - $save_this.delegate(".shake-link", "click", (ev) => - click_choose_shake(ev), - ); - $save_this.delegate(".close", "click", (ev) => - click_close_selector(ev), + $saveThisLink.click((ev) => clickSaveThis(ev)); + $saveThis.delegate(".shake-link", "click", (ev) => + clickChooseShake(ev), ); + $saveThis.delegate(".close", "click", (ev) => clickCloseSelector(ev)); }, }; diff --git a/static/js/ShakeMemberList.js b/static/js/ShakeMemberList.js index 746b084..87cf418 100644 --- a/static/js/ShakeMemberList.js +++ b/static/js/ShakeMemberList.js @@ -15,8 +15,8 @@ const ShakeMemberList = { attachEvents: function ($root) { // Per invocation functions that will close over the context dependent // variable defined above. - function remove_from_shake(ev) { - var $target = $(ev.target), + async function removeFromShake(ev) { + const $target = $(ev.target), $li = $target.parents("li"), $form = $target.next(), url = $form.attr("action"); @@ -27,14 +27,18 @@ const ShakeMemberList = { "Are you sure you want to remove this user from a shake? If they have notifications on an email will be sent informing them of the change.", ) ) { - $.post(url, data, () => process_remove($li)); + await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + process_remove($li); } return false; } // Attach any event handlers. $root.delegate(".remove-from-shake-button-link", "click", (ev) => - remove_from_shake(ev), + removeFromShake(ev), ); }, }; diff --git a/static/js/ShakesCache.js b/static/js/ShakesCache.js index e33e8f2..732b68c 100644 --- a/static/js/ShakesCache.js +++ b/static/js/ShakesCache.js @@ -1,4 +1,4 @@ -var ShakesCache = { +const ShakesCache = { fetch: function () { if (this.result !== undefined) { return this.result; diff --git a/static/js/SidebarStatsView.js b/static/js/SidebarStatsView.js index 4d732d4..d55aaa9 100644 --- a/static/js/SidebarStatsView.js +++ b/static/js/SidebarStatsView.js @@ -6,23 +6,23 @@ import { toText } from "./common.js"; * * Defines and attaches event handlers. */ -const DEFAULT_IMAGE_STATS = { save_count: 0, like_count: 0 }; +const DEFAULT_IMAGE_STATS = { saveCount: 0, likeCount: 0 }; let scope; -const image_stats = { ...DEFAULT_IMAGE_STATS }; +const imageStats = { ...DEFAULT_IMAGE_STATS }; // Initialise these once init() has been called with a valid scope. If never // initialised, never referred to. -let $save_count; -let $like_count; +let $saveCount; +let $likeCount; -let $save_button; -let $like_button; +let $saveButton; +let $likeButton; let $content; -let saves_expanded; -let likes_expanded; +let savesExpanded; +let likesExpanded; // if we aren't on a permalink page, just expose a dummy public API function noScope() { @@ -38,170 +38,162 @@ const SidebarStatsView = { } // Set all these now that we have been provided a meaningful scope. - $save_count = $(".save-count", scope); - $like_count = $(".like-count", scope); - image_stats.save_count = parseInt($save_count.html(), 10); - image_stats.like_count = parseInt($like_count.html(), 10); + $saveCount = $(".save-count", scope); + $likeCount = $(".like-count", scope); + imageStats.saveCount = parseInt($saveCount.html(), 10); + imageStats.likeCount = parseInt($likeCount.html(), 10); - $save_button = $(".sidebar-stats-saves", scope); - $like_button = $(".sidebar-stats-hearts", scope); + $saveButton = $(".sidebar-stats-saves", scope); + $likeButton = $(".sidebar-stats-hearts", scope); $content = $(".sidebar-stats-content", scope); - saves_expanded = false; - likes_expanded = false; + savesExpanded = false; + likesExpanded = false; - if (image_stats.save_count > 0) { - this.bind_saves(); + if (imageStats.saveCount > 0) { + this.bindSaves(); } else { - this.unbind_saves(); + this.unbindSaves(); } - if (image_stats.like_count > 0) { - this.bind_likes(); - this.toggle_likes(); + if (imageStats.likeCount > 0) { + this.bindLikes(); + this.toggleLikes(); } else { - this.unbind_likes(); + this.unbindLikes(); } }, - refresh_likes: function () { + refreshLikes: function () { if (noScope()) { return; } - $like_count = $(".like-count", scope); - image_stats.like_count = parseInt($like_count.html(), 10); - if (likes_expanded) { - this.get_likes(); + $likeCount = $(".like-count", scope); + imageStats.likeCount = parseInt($likeCount.html(), 10); + if (likesExpanded) { + this.getLikes(); } - if (image_stats.like_count > 0) { - this.bind_likes(); + if (imageStats.likeCount > 0) { + this.bindLikes(); } else { - likes_expanded = false; - this.unbind_likes(); + likesExpanded = false; + this.unbindLikes(); } }, - refresh_saves: function () { + refreshSaves: async function () { if (noScope()) { return; } - $save_count = $(".save-count", scope); - image_stats.save_count = parseInt($save_count.html(), 10); - if (saves_expanded) { - this.get_saves(); + $saveCount = $(".save-count", scope); + imageStats.saveCount = parseInt($saveCount.html(), 10); + if (savesExpanded) { + await this.getSaves(); } - if (image_stats.save_count > 0) { - this.bind_saves(); + if (imageStats.saveCount > 0) { + this.bindSaves(); } else { - saves_expanded = false; - this.unbind_saves(); + savesExpanded = false; + this.unbindSaves(); } }, - bind_saves: function () { - $save_button.unbind("click"); - $save_button.addClass("enable-cursor"); - $save_button.click(function () { - SidebarStatsView.toggle_saves(); + bindSaves: function () { + $saveButton.unbind("click"); + $saveButton.addClass("enable-cursor"); + $saveButton.click(function () { + SidebarStatsView.toggleSaves(); }); }, - unbind_saves: function () { - $save_button.removeClass("enable-cursor"); - $save_button.unbind("click"); + unbindSaves: function () { + $saveButton.removeClass("enable-cursor"); + $saveButton.unbind("click"); this.collapse(); }, - bind_likes: function () { - $like_button.unbind("click"); - $like_button.addClass("enable-cursor"); - $like_button.click(function () { - SidebarStatsView.toggle_likes(); + bindLikes: function () { + $likeButton.unbind("click"); + $likeButton.addClass("enable-cursor"); + $likeButton.click(function () { + SidebarStatsView.toggleLikes(); }); }, - unbind_likes: function () { - $like_button.removeClass("enable-cursor"); - $like_button.unbind("click"); + unbindLikes: function () { + $likeButton.removeClass("enable-cursor"); + $likeButton.unbind("click"); this.collapse(); }, - toggle_saves: function () { - likes_expanded = false; - saves_expanded = !saves_expanded; - if (saves_expanded) { - $like_button.removeClass("selected"); + toggleSaves: function () { + likesExpanded = false; + savesExpanded = !savesExpanded; + if (savesExpanded) { + $likeButton.removeClass("selected"); $content.addClass("loading").show(); - $save_button.addClass("selected"); - this.get_saves(); + $saveButton.addClass("selected"); + this.getSaves(); } else { this.collapse(); } }, - get_saves: function () { - $.get( - document.location.pathname + "/saves", - function (response) { - if (response["result"]) { - SidebarStatsView.process_save(response); - } - }, - "json", - ); + getSaves: async function () { + const resp = await fetch(`${document.location.pathname}/saves`); + const json = await resp.json(); + if (response["result"]) { + SidebarStatsView.processSave(json); + } }, - toggle_likes: function () { - saves_expanded = false; - likes_expanded = !likes_expanded; - if (likes_expanded) { - $save_button.removeClass("selected"); + toggleLikes: function () { + savesExpanded = false; + likesExpanded = !likesExpanded; + if (likesExpanded) { + $saveButton.removeClass("selected"); $content.addClass("loading").show(); - $like_button.addClass("selected"); - this.get_likes(); + $likeButton.addClass("selected"); + this.getLikes(); } else { this.collapse(); } }, - get_likes: function () { - $.get( - document.location.pathname + "/likes", - function (response) { - if (response["result"]) { - SidebarStatsView.process_like(response); - } - }, - "json", - ); + getLikes: async function () { + const resp = await fetch(document.location.pathname + "/likes"); + const json = await resp.json(); + if (json["result"]) { + SidebarStatsView.processLike(json); + } }, - process_save: function (response) { + processSave: function (response) { if (response["count"] == 0) { this.disable_saves(); } else { - $save_count.html(toText(response["count"], "Save")); - this.render_content(response); + $saveCount.html(toText(response["count"], "Save")); + this.renderContent(response); } }, - process_like: function (response) { + processLike: function (response) { if (response["count"] == 0) { - this.unbind_likes(); + this.unbindLikes(); } else { - $like_count.html(toText(response["count"], "Like")); - this.render_content(response); + $likeCount.html(toText(response["count"], "Like")); + this.renderContent(response); } }, collapse: function (repsponse) { - $like_button.removeClass("selected"); - $save_button.removeClass("selected"); + $likeButton.removeClass("selected"); + $saveButton.removeClass("selected"); $content.hide(); }, - render_content: function (response) { + renderContent: function (response) { var html = ""; for (var i = 0, len = response["result"].length; i < len; i++) { var result = response["result"][i]; @@ -210,25 +202,17 @@ const SidebarStatsView = { // for saves, we link to the saved post link = result["post_url"]; } else { - link = "/user/" + result["user_name"]; + link = `/user/${result["user_name"]}`; } - html += '
      '; - html += ''; - html += - ''; - html += - '' + - result["user_name"] + - ""; - html += - '' + - result["posted_at_friendly"] + - ""; - html += "
      "; + html += ` +
      + + + + ${result["user_name"]} + ${result["posted_at_friendly"]} +
      + `; } $content.removeClass("loading").html(html); }, diff --git a/static/js/StreamStatsView.js b/static/js/StreamStatsView.js index 2ad2576..ffe0125 100644 --- a/static/js/StreamStatsView.js +++ b/static/js/StreamStatsView.js @@ -12,229 +12,245 @@ import { setCaret } from "./common.js"; */ class StreamStatsView { - constructor($image_content_footer) { - this.$image_content_footer = $image_content_footer; - this.share_key = this.$image_content_footer + constructor($imageContentFooter) { + this.$imageContentFooter = $imageContentFooter; + this.shareKey = this.$imageContentFooter .attr("id") .replace("image-content-footer-", ""); - this.can_submit_comments = true; - this.init_dom(); - this.init_events(); + this.canSubmitComments = true; + this.initDom(); + this.initEvents(); } - init_dom() { - this.$likes_button = this.$image_content_footer.find(".likes"); - this.$saves_button = this.$image_content_footer.find(".saves"); - this.$comments_button = this.$image_content_footer.find(".comments"); - this.$inline_details = - this.$image_content_footer.find(".inline-details"); - this.init_comment_dom(); + initDom() { + this.$likesButton = this.$imageContentFooter.find(".likes"); + this.$savesButton = this.$imageContentFooter.find(".saves"); + this.$commentsButton = this.$imageContentFooter.find(".comments"); + this.$inlineDetails = this.$imageContentFooter.find(".inline-details"); + this.initCommentDom(); } - init_comment_dom() { - this.$post_comment_inline = this.$inline_details.find( + initCommentDom() { + this.$postCommentInline = this.$inlineDetails.find( ".post-comment-inline", ); - this.$comment_form = this.$inline_details.find(".post-comment-form"); - this.$comment_textarea = this.$inline_details.find("textarea"); - this.$submit_comment_button = this.$inline_details.find( + this.$commentForm = this.$inlineDetails.find(".post-comment-form"); + this.$commentTextarea = this.$inlineDetails.find("textarea"); + this.$submitCommentButton = this.$inlineDetails.find( ".submit-comment-button", ); - this.$show_more_comments = this.$inline_details.find( + this.$showMoreComments = this.$inlineDetails.find( ".show-more-comments", ); - this.$comment = this.$inline_details.find(".comment"); - this.$reply_to = this.$inline_details.find(".reply-to"); - this.$delete = this.$inline_details.find(".delete"); + this.$comment = this.$inlineDetails.find(".comment"); + this.$replyTo = this.$inlineDetails.find(".reply-to"); + this.$delete = this.$inlineDetails.find(".delete"); } - init_events() { - this.$likes_button.click((ev) => this.click_like(ev)); - this.$saves_button.click((ev) => this.click_saves(ev)); - this.$comments_button.click((ev) => this.click_comments(ev)); - this.init_comment_events(); + initEvents() { + this.$likesButton.click((ev) => this.clickLike(ev)); + this.$savesButton.click((ev) => this.clickSaves(ev)); + this.$commentsButton.click((ev) => this.clickComments(ev)); + this.initCommentEvents(); } - init_comment_events() { - this.$comment_textarea.click((ev) => this.click_comment_textarea(ev)); - this.$show_more_comments.click((ev) => this.click_more_comments(ev)); - this.$reply_to.click((ev) => this.click_reply_to(ev)); - this.$delete.click((ev) => this.click_delete(ev)); + initCommentEvents() { + this.$commentTextarea.click((ev) => this.clickCommentTextarea(ev)); + this.$showMoreComments.click((ev) => this.clickMoreComments(ev)); + this.$replyTo.click((ev) => this.clickReplyTo(ev)); + this.$delete.click((ev) => this.clickDelete(ev)); // Fix for Webkit bug where textarea looses focus incorrectly on mouseup. // http://code.google.com/p/chromium/issues/detail?id=4505 - this.$comment_textarea.mouseup(function (e) { - e.preventDefault(); + this.$commentTextarea.mouseup(function (ev) { + ev.preventDefault(); }); - this.$comment_form.submit((ev) => this.submit_comment(ev)); + this.$commentForm.submit((ev) => this.submitComment(ev)); } // Removes selected state from all tabs. - clear_tab_selection() { - this.$likes_button.removeClass("selected"); - this.$saves_button.removeClass("selected"); - this.$comments_button.removeClass("selected"); + clearTabSelection() { + this.$likesButton.removeClass("selected"); + this.$savesButton.removeClass("selected"); + this.$commentsButton.removeClass("selected"); } // Start the "loading" state transition. - start_loading() { - this.$inline_details.addClass("inline-details-loading").html("").show(); + startLoading() { + this.$inlineDetails.addClass("inline-details-loading").html("").show(); } user_html(data) { - var html = ""; - for (var i = 0; i < data.result.length; i++) { - var result = data.result[i]; - var link; - if (result["action"] == "save") { - link = result["post_url"]; - } else { - link = "/user/" + result["user_name"]; - } - html += - '' + - '' + - '' + - result["user_name"] + - ""; + let html = ""; + for (let i = 0; i < data.result.length; i++) { + const result = data.result[i]; + const link = + result["action"] == "save" + ? result["post_url"] + : `/user/${result["user_name"]}`; + + html += ` + + + ${result["user_name"]} + + `; } return html; } - click_like() { - if (this.$likes_button.hasClass("selected")) { - this.clear_tab_selection(); - this.$inline_details.hide(); - return false; + clickLike(ev) { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.$likesButton.hasClass("selected")) { + this.clearTabSelection(); + this.$inlineDetails.hide(); } - this.clear_tab_selection(); - this.$likes_button.addClass("selected"); - this.start_loading(); - this.load_likes(); - return false; + + this.clearTabSelection(); + this.$likesButton.addClass("selected"); + this.startLoading(); + this.loadLikes(); } - load_likes() { - var url = "/p/" + this.share_key + "/likes"; - $.get(url, (ev) => this.process_like_response(ev), "json"); + async loadLikes() { + const url = `/p/${this.shareKey}/likes`; + const resp = await fetch(url); + const json = await resp.json(); + this.processLikeResponse(json); } - process_like_response(data) { - var html = - '
      ' + this.user_html(data) + "
      "; - this.$inline_details.html(html); + processLikeResponse(data) { + const html = `
      ${this.user_html(data)}
      `; + this.$inlineDetails.html(html); } - click_saves() { - if (this.$saves_button.hasClass("selected")) { - this.clear_tab_selection(); - this.$inline_details.hide(); - return false; + clickSaves(ev) { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.$savesButton.hasClass("selected")) { + this.clearTabSelection(); + this.$inlineDetails.hide(); } - this.clear_tab_selection(); - this.$saves_button.addClass("selected"); - this.start_loading(); - this.load_saves(); - return false; + this.clearTabSelection(); + this.$savesButton.addClass("selected"); + this.startLoading(); + this.loadSaves(); } - load_saves() { - var url = "/p/" + this.share_key + "/saves"; - $.get(url, (ev) => this.process_like_response(ev), "json"); + async loadSaves() { + const url = `/p/${this.shareKey}/saves`; + const resp = await fetch(url); + const json = await resp.json(); + this.processLikeResponse(json); } - click_comments() { - if (this.$comments_button.hasClass("selected")) { - this.clear_tab_selection(); - this.$inline_details.hide(); - return false; + async clickComments(ev) { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.$commentsButton.hasClass("selected")) { + this.clearTabSelection(); + this.$inlineDetails.hide(); } - this.clear_tab_selection(); - this.$comments_button.addClass("selected"); - this.start_loading(); - var url = "/p/" + this.share_key + "/quick-comments"; - $.get(url, (ev) => this.process_comments_response(ev), "json"); - return false; + this.clearTabSelection(); + this.$commentsButton.addClass("selected"); + this.startLoading(); + const url = `/p/${this.shareKey}/quick-comments`; + const resp = await fetch(url); + const json = await resp.json(); + this.processCommentsResponse(json); } - click_more_comments() { + clickMoreComments(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.$comment.show(); - this.$show_more_comments.hide(); - return false; + this.$showMoreComments.hide(); } - process_comments_response(data) { - this.can_submit_comments = true; + processCommentsResponse(data) { + this.canSubmitComments = true; if (data["result"] == "ok") { - this.$comments_button.find("a").html(data["count"]); - this.$inline_details.html(data["html"]); - this.init_comment_dom(); - this.init_comment_events(); + this.$commentsButton.find("a").html(data["count"]); + this.$inlineDetails.html(data["html"]); + this.initCommentDom(); + this.initCommentEvents(); } } - click_reply_to(ev) { - this.click_comment_textarea(); - var username = $(ev.target) + clickReplyTo(ev) { + ev.preventDefault(); + ev.stopPropagation(); + + this.clickCommentTextarea(ev); + const username = $(ev.target) .parents(".comment") .find(".username") .html(); - var username_clean = username.replace(/[^a-zA-Z0-9_\-]+/g, ""); - var current_text = this.$comment_textarea.val(); - this.$comment_textarea.val(current_text + "@" + username_clean + " "); - setCaret(this.$comment_textarea.get(0)); - return false; + const usernameClean = username.replace(/[^a-zA-Z0-9_\-]+/g, ""); + const currentText = this.$commentTextarea.val(); + this.$commentTextarea.val(currentText + "@" + usernameClean + " "); + setCaret(this.$commentTextarea.get(0)); } - click_delete(ev) { - var $delete_form = $("#" + ev.target.id + "-form"), - url = $delete_form.attr("action"), - data = $delete_form.serialize(); + async clickDelete(ev) { + ev.preventDefault(); + ev.stopPropagation(); + + const $deleteForm = $(`#${ev.target.id}-form`), + url = $deleteForm.attr("action"), + data = $deleteForm.serialize(); if (confirm("Are you sure you want to delete this?")) { - $.post( - url, - data, - (ev) => this.process_comments_response(ev), - "json", - ); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + this.processCommentsResponse(json); } - return false; } - submit_comment() { - if (this.can_submit_comments === false) { - return false; + async submitComment(ev) { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.canSubmitComments === false) { + return; } - this.can_submit_comments = false; - var url = this.$comment_form.attr("action"); - var data = this.$comment_form.serialize(); - $.post(url, data, (ev) => this.process_comments_response(ev), "json"); - return false; + this.canSubmitComments = false; + const url = this.$commentForm.attr("action"); + const data = this.$commentForm.serialize(); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + this.processCommentsResponse(json); } - click_comment_textarea(e) { - this.$post_comment_inline.addClass("post-comment-inline-expanded"); - if (this.$comment_textarea.val().indexOf("Write a comment") === 0) { - this.$comment_textarea.val(""); + clickCommentTextarea(ev) { + this.$postCommentInline.addClass("post-comment-inline-expanded"); + if (this.$commentTextarea.val().indexOf("Write a comment") === 0) { + this.$commentTextarea.val(""); } - this.click_more_comments(); - this.$comment_textarea.css("min-height", "60px"); + this.clickMoreComments(ev); + this.$commentTextarea.css("min-height", "60px"); } - refresh_likes() { - if (this.$likes_button.hasClass("selected")) { - this.load_likes(); + refreshLikes() { + if (this.$likesButton.hasClass("selected")) { + this.loadLikes(); } } - refresh_saves() { - if (this.$saves_button.hasClass("selected")) { - this.load_saves(); + refreshSaves() { + if (this.$savesButton.hasClass("selected")) { + this.loadSaves(); } } } diff --git a/static/js/StreamStatsViewRegistry.js b/static/js/StreamStatsViewRegistry.js index 009cc1d..70214c0 100644 --- a/static/js/StreamStatsViewRegistry.js +++ b/static/js/StreamStatsViewRegistry.js @@ -4,28 +4,28 @@ * comments of each individual post. */ -const files_on_page = {}; +const filesOnPage = {}; -const get_view = function (share_key) { - return files_on_page[share_key]; +const getView = function (shareKey) { + return filesOnPage[shareKey]; }; -var StreamStatsViewRegistry = { +const StreamStatsViewRegistry = { register: function (view) { - files_on_page[view.share_key] = view; + filesOnPage[view.shareKey] = view; }, - refresh_likes: function (share_key) { - var view = get_view(share_key); + refreshLikes: function (shareKey) { + const view = getView(shareKey); if (view !== undefined) { - view.refresh_likes(); + view.refreshLikes(); } }, - refresh_saves: function (share_key) { - var view = get_view(share_key); + refreshSaves: function (shareKey) { + const view = getView(shareKey); if (view !== undefined) { - view.refresh_saves(); + view.refreshSaves(); } }, }; diff --git a/static/js/UserCounts.js b/static/js/UserCounts.js index da19164..eb9e5ba 100644 --- a/static/js/UserCounts.js +++ b/static/js/UserCounts.js @@ -10,12 +10,12 @@ function format(str_num) { return Number.parseInt(str_num).toLocaleString(); } -function formatBrief(str_num) { +function formatBrief(strNum) { // Format number in a way that won't need excessive space to // display. Abbreviate with suffixes and limit to one // decimal place. - const n = parseInt(str_num, 10); + const n = parseInt(strNum, 10); // Handle garbage input (somewhat) gracefully. if (Number.isNaN(n)) { @@ -54,26 +54,23 @@ function formatBrief(str_num) { return n.toLocaleString(); } -var $root; - const UserCounts = { - populate: function ($user_counts) { - $root = $user_counts; - var name = $root.attr("name"); + populate: async function ($userCountsPanel) { + const name = $userCountsPanel.attr("name"); - function display_results(result) { + function displayResults(result) { if ("views" in result) { - $root + $userCountsPanel .find(".views") .attr("title", format(result["views"]) + " views") .find(".num") .html(formatBrief(result["views"])); - $root + $userCountsPanel .find(".saves") .attr("title", format(result["saves"]) + " saves") .find(".num") .html(formatBrief(result["saves"])); - $root + $userCountsPanel .find(".likes") .attr("title", format(result["likes"]) + " likes") .find(".num") @@ -81,11 +78,9 @@ const UserCounts = { } } - $.get( - "/user/" + name + "/counts", - (result) => display_results(result), - "json", - ); + const resp = await fetch(`/user/${name}/counts`); + const json = await resp.json(); + displayResults(json); }, }; diff --git a/static/js/common.js b/static/js/common.js index e8547ed..7611afe 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -24,4 +24,14 @@ function toText(num, base) { : num + " " + "" + base + "s" + ""; } -export { setCaret, toText }; +function applyHoverForVideo(sel) { + sel.hover(function () { + if (this.hasAttribute("controls")) { + this.removeAttribute("controls"); + } else { + this.setAttribute("controls", "controls"); + } + }); +} + +export { applyHoverForVideo, setCaret, toText }; diff --git a/static/js/main.js b/static/js/main.js index 60901e7..4130208 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -13,12 +13,12 @@ import { SidebarStatsView } from "./SidebarStatsView.js"; import { StreamStatsView } from "./StreamStatsView.js"; import { StreamStatsViewRegistry } from "./StreamStatsViewRegistry.js"; import { UserCounts } from "./UserCounts.js"; -import { toText } from "./common.js"; +import { applyHoverForVideo, toText } from "./common.js"; NewPostPanel.attachEvents(); InviteMember.attachEvents(); -function screen_reader_focus(el) { +function screenReaderFocus(el) { el.setAttribute("tabindex", "0"); el.blur(); el.focus(); @@ -67,6 +67,7 @@ $(".image-edit-title-form .cancel").click(function () { return false; }); +// TODO this could probably be a CSS :hover $(".image-edit-title").hover( function () { $(this).addClass("image-edit-title-hover"); @@ -76,104 +77,93 @@ $(".image-edit-title").hover( }, ); -$(".image-edit-title").click(function () { - var $title_container = $(this).closest(".image-title"); - var url = $title_container.find("form").attr("action"); - var that = this; - - $.get( - url, - function (result) { - if ("title_raw" in result) { - $(that).hide(); - $title_container.find(".title-input").val(result["title_raw"]); - var $input = $title_container.find(".title-input"); - $(that).next(".image-edit-title-form").addClass("is-active"); - screen_reader_focus($input[0]); - } - }, - "json", - ); +$(".image-edit-title").click(async (ev) => { + const $label = $(ev.currentTarget); + const $container = $label.closest(".image-title"); + const url = $container.find("form").attr("action"); + + const resp = await fetch(url); + const json = await resp.json(); + + if ("title_raw" in json) { + $label.hide(); + const $input = $container.find(".title-input"); + $input.val(json["title_raw"]); + $label.next(".image-edit-title-form").addClass("is-active"); + screenReaderFocus($input[0]); + } }); -$(".image-edit-title-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("title" in result && "title_raw" in result) { - var $title_container = $(that).closest(".image-title"); - $title_container - .find(".image-edit-title") - .html(result["title"]) - .show(); - $title_container.find(".title-input").val(result["title_raw"]); - $title_container - .find(".image-edit-title-form") - .removeClass("is-active"); - if (result["title_raw"] === "") { - $title_container - .find(".the-title") - .html("click here to edit title") - .show(); - $title_container - .find(".the-title") - .addClass("the-title-blank"); - } else { - $title_container - .find(".the-title") - .removeClass("the-title-blank"); - } - } - }, - "json", - ); - return false; +$(".image-edit-title-form").submit(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget); + const data = $form.serialize(); + const url = $form.attr("action"); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + if ("title" in json && "title_raw" in json) { + const $container = $form.closest(".image-title"); + $container.find(".image-edit-title").html(json["title"]).show(); + $container.find(".title-input").val(json["title_raw"]); + $container.find(".image-edit-title-form").removeClass("is-active"); + if (json["title_raw"] === "") { + $container + .find(".the-title") + .html("click here to edit title") + .show(); + $container.find(".the-title").addClass("the-title-blank"); + } else { + $container.find(".the-title").removeClass("the-title-blank"); + } + } }); // Inline editing of the description. -$(".description-edit-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("description" in result && "description_raw" in result) { - var $description_container = - $(that).closest(".description-edit"); - $description_container - .find("textarea") - .val(result["description_raw"]); - $description_container.find(".description-edit-form").hide(); - if (result["description"]) { - $description_container - .find(".the-description") - .html(result["description"]) - .show(); - $description_container - .find(".the-description") - .removeClass("the-description-blank"); - } else { - $description_container - .find(".the-description") - .html("click here to edit description") - .show(); - $description_container - .find(".the-description") - .addClass("the-description-blank"); - } - } - }, - "json", - ); - return false; +$(".description-edit-form").submit(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget); + const data = $form.serialize(); + const url = $form.attr("action"); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + + console.log(json); + + if ("description" in json && "description_raw" in json) { + const $container = $form.closest(".description-edit"); + $container.find("textarea").val(json["description_raw"]); + $container.find(".description-edit-form").hide(); + if (json["description"]) { + $container + .find(".the-description") + .html(json["description"]) + .show(); + $container + .find(".the-description") + .removeClass("the-description-blank"); + } else { + $container + .find(".the-description") + .html("click here to edit description") + .show(); + $container + .find(".the-description") + .addClass("the-description-blank"); + } + } }); +// TODO this could probably be a CSS :hover $(".description-edit .the-description").hover( function () { $(this).addClass("the-description-hover"); @@ -183,25 +173,20 @@ $(".description-edit .the-description").hover( }, ); -$(".description-edit .the-description").click(function () { - var $description_container = $(this).closest(".description-edit"); - var url = $description_container.find("form").attr("action"); - var that = this; - $.get( - url, - function (result) { - if ("description_raw" in result) { - $(that).hide(); - let $textarea = $description_container.find( - ".description-edit-textarea", - ); - $textarea.val(result["description_raw"]); - $(that).next(".description-edit-form").show(); - screen_reader_focus($textarea[0]); - } - }, - "json", - ); +$(".description-edit .the-description").click(async (ev) => { + const $label = $(ev.currentTarget); + const $container = $label.closest(".description-edit"); + const url = $container.find("form").attr("action"); + const resp = await fetch(url); + const json = await resp.json(); + + if ("description_raw" in json) { + $label.hide(); + const $textarea = $container.find(".description-edit-textarea"); + $textarea.val(json["description_raw"]); + $label.next(".description-edit-form").show(); + screenReaderFocus($textarea[0]); + } }); $(".description-edit .cancel").click(function () { @@ -211,42 +196,41 @@ $(".description-edit .cancel").click(function () { }); // Inline editing of the alt text. -$(".alt-text-edit-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("alt_text" in result && "alt_text_raw" in result) { - var $alt_text_container = $(that).closest(".alt-text-edit"); - if (result["alt_text"]) { - $alt_text_container.removeClass("alt-text--blank"); - $alt_text_container - .find(".the-alt-text") - .html(result["alt_text"]); - } else { - $alt_text_container.addClass("alt-text--blank"); - $alt_text_container - .find(".the-alt-text") - .html("add some alt text"); - } - $alt_text_container.removeClass("alt-text--hidden"); - $alt_text_container.removeClass("alt-text--editing"); - $alt_text_container - .find("textarea") - .val(result["alt_text_raw"]); - screen_reader_focus( - $alt_text_container.find(".the-alt-text")[0], - ); - } - }, - "json", - ); - return false; +$(".alt-text-edit-form").submit(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget); + const data = $form.serialize(); + const url = $form.attr("action"); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + if ("alt_text" in json && "alt_text_raw" in json) { + var $container = $form.closest(".alt-text-edit"); + if (json["alt_text"]) { + $container + .removeClass("alt-text--blank") + .find(".the-alt-text") + .html(json["alt_text"]); + } else { + $container + .addClass("alt-text--blank") + .find(".the-alt-text") + .html("add some alt text"); + } + $container + .removeClass("alt-text--hidden") + .removeClass("alt-text--editing") + .find("textarea") + .val(json["alt_text_raw"]); + screenReaderFocus($container.find(".the-alt-text")[0]); + } }); +// TODO this could probably be a CSS :hover $(".alt-text-edit .the-alt-text").hover( function () { $(this).addClass("the-alt-text-hover"); @@ -256,29 +240,26 @@ $(".alt-text-edit .the-alt-text").hover( }, ); -$(".alt-text-edit .the-alt-text").click(function () { - var $alt_text_container = $(this).closest(".alt-text-edit"); - var url = $alt_text_container.find("form").attr("action"); - var that = this; - $.get( - url, - function (result) { - if ("alt_text_raw" in result) { - $(that).closest(".alt-text-edit").addClass("alt-text--editing"); - let $textarea = $alt_text_container.find( - ".alt-text-edit-textarea", - ); - $textarea.val(result["alt_text_raw"]); - screen_reader_focus($textarea[0]); - } - }, - "json", - ); +$(".alt-text-edit .the-alt-text").click(async (ev) => { + const $label = $(ev.currentTarget); + const $container = $label.closest(".alt-text-edit"); + const url = $container.find("form").attr("action"); + const resp = await fetch(url); + const json = await resp.json(); + + if ("alt_text_raw" in json) { + $label.closest(".alt-text-edit").addClass("alt-text--editing"); + const $textarea = $container.find(".alt-text-edit-textarea"); + $textarea.val(json["alt_text_raw"]); + screenReaderFocus($textarea[0]); + } }); $(".alt-text-edit .cancel").click(function () { - $(this).closest(".alt-text-edit").removeClass("alt-text--hidden"); - $(this).closest(".alt-text-edit").removeClass("alt-text--editing"); + $(this) + .closest(".alt-text-edit") + .removeClass("alt-text--hidden") + .removeClass("alt-text--editing"); return false; }); @@ -286,7 +267,7 @@ $(".alt-text-toggle").click(function () { let $alt = $(this).closest(".alt-text"); $alt.toggleClass("alt-text--hidden"); if (!$alt.hasClass("alt-text--hidden")) { - screen_reader_focus($alt.find(".the-alt-text")[0]); + screenReaderFocus($alt.find(".the-alt-text")[0]); } }); @@ -295,62 +276,52 @@ $(".delete-from-shakes-form").click(function () { }); /* Like / Unlike button */ -$(".like-button, .unlike-button").click(function () { - var $form = $(this).parents("form"); - var $buttons = $form.children("button"); - var url = $form.attr("action"); - var data = $form.serialize() + "&json=1"; - - $.post( - url, - data, - function (response) { - if (response["error"]) { - return false; - } else { - var count = response["count"]; - var share_key = response["share_key"]; - var count_string = toText(count, "Like"); - $("#like-count-amount-" + share_key).html(count_string); - if (response["like"] === true) { - $form.attr("action", "/p/" + share_key + "/unlike"); - } else { - $form.attr("action", "/p/" + share_key + "/like"); - } - $buttons.toggleClass("is-active"); - SidebarStatsView.refresh_likes(); - StreamStatsViewRegistry.refresh_likes(share_key); - } - }, - "json", - ); - return false; +$(".like-button, .unlike-button").click(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget).parents("form"); + const $buttons = $form.children("button"); + const url = $form.attr("action"); + const data = $form.serialize() + "&json=1"; // TODO json suffix required? + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + + if (json["error"]) { + return; + } + + const count = json["count"]; + const shareKey = json["share_key"]; + const countString = toText(count, "Like"); + $("#like-count-amount-" + shareKey).html(countString); + if (json["like"] === true) { + $form.attr("action", `/p/${shareKey}/unlike`); + } else { + $form.attr("action", `/p/${shareKey}/like`); + } + $buttons.toggleClass("is-active"); + SidebarStatsView.refreshLikes(); + StreamStatsViewRegistry.refreshLikes(shareKey); }); SidebarStatsView.init("#sidebar-stats"); -function apply_hover_for_video(sel) { - sel.hover(function () { - if (this.hasAttribute("controls")) { - this.removeAttribute("controls"); - } else { - this.setAttribute("controls", "controls"); - } - }); -} - $(".image-content").each(function () { - var $image_content = $(this), + const $image_content = $(this), $image_footer = $image_content.find(".image-content-footer"), $nsfw_cover = $image_content.find(".nsfw-cover"); - var stream_stats_view = new StreamStatsView($image_footer); + const stream_stats_view = new StreamStatsView($image_footer); StreamStatsViewRegistry.register(stream_stats_view); if ($nsfw_cover.length > 0) { NSFWCover.attachEvents($nsfw_cover); } }); -apply_hover_for_video($(".image-content video.autoplay")); +applyHoverForVideo($(".image-content video.autoplay")); /* Open / close notification boxes */ $(document).on("click", ".notification-block-hd", function () { @@ -358,196 +329,174 @@ $(document).on("click", ".notification-block-hd", function () { }); /* User follow module */ -$(document).on("click", ".user-follow .submit-form", function () { - var $container = $(this).parents(".user-follow"); - var $form = $container.find("form"); - var url = $form.attr("action"); - var data = $form.serialize() + "&json=1"; - var that = this; - $.post( - url, - data, - function (response) { - if (response["error"]) { - return false; - } else { - if (response["subscription_status"] == true) { - $form.attr( - "action", - url.replace("subscribe", "unsubscribe"), - ); - $(that) - .text("- Unfollow") - .addClass("btn-warning") - .removeClass("btn-secondary"); - } else { - $form.attr( - "action", - url.replace("unsubscribe", "subscribe"), - ); - $(that) - .text("+ Follow") - .addClass("btn-secondary") - .removeClass("btn-warning"); - } - } - }, - "json", - ); - return false; +$(document).on("click", ".user-follow .submit-form", async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $button = $(ev.currentTarget); + const $container = $(button).parents(".user-follow"); + const $form = $container.find("form"); + const url = $form.attr("action"); + const data = $form.serialize() + "&json=1"; // TODO json suffix required? + const resp = await fetch(url, { + method: "POST", + body: URLSearchParams(data), + }); + const json = await resp.json(); + + if (json["error"]) { + return false; + } + + if (json["subscription_status"] == true) { + $form.attr("action", url.replace("subscribe", "unsubscribe")); + $button + .text("- Unfollow") + .addClass("btn-warning") + .removeClass("btn-secondary"); + } else { + $form.attr("action", url.replace("unsubscribe", "subscribe")); + $button + .text("+ Follow") + .addClass("btn-secondary") + .removeClass("btn-warning"); + } }); -$(document).on("click", ".notification-close", function () { - $notification = $(this).parent(".notification"); - var $notification_block = $(this).parents(".notification-block"); - var $notification_block_hd = $notification_block.find( +$(document).on("click", ".notification-close", async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $button = $(ev.currentTarget); + const $notification = $button.parent(".notification"); + const $notificationBlock = $button.parents(".notification-block"); + const $notificationBlockHd = $notificationBlock.find( ".notification-block-hd", ); - var id = $(this) - .attr("id") - .replace(/[^\d]+/, ""); - $.post( - "/account/clear-notification" + "?type=single&id=" + id, - {}, - function (response) { - $notification.remove(); - var html = $notification_block_hd.html(); - var count = html.replace(/[^\d]+/, ""); - var new_count = parseInt(count, 10) - 1; - if (new_count == 0) { - $notification_block_hd.html("You have 0 new followers"); - $notification_block.find(".clear-all").remove(); - } else { - $notification_block_hd.html(html.replace(/[\d]+/, new_count)); - } - }, - "json", - ); - return false; + const id = $button.attr("id").replace(/[^\d]+/, ""); + const url = `/account/clear-notification?type=single&id=${id}`; + // TODO data should be sent as body for POST? + const resp = await fetch(url, { method: "POST" }); + + $notification.remove(); + const html = $notificationBlockHd.html(); + const count = html.replace(/[^\d]+/, ""); + var newCount = parseInt(count, 10) - 1; + if (newCount == 0) { + $notificationBlockHd.html("You have 0 new followers"); + $notificationBlock.find(".clear-all").remove(); + } else { + $notificationBlockHd.html(html.replace(/[\d]+/, newCount)); + } }); -$(document).on("click", ".notification-block .clear-all a", function () { - var url = $(this).attr("href"); - var $notification_block = $(this).parents(".notification-block"); - $.post( - url, - {}, - function (response) { - if (response["error"]) { - return false; - } else { - $notification_block - .find(".notification-block-hd") - .html(response["response"]); - $notification_block - .find(".notification-block-bd") - .html("") - .toggle(); - } - }, - "json", - ); +$(document).on("click", ".notification-block .clear-all a", async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $link = $(ev.currentTarget); + const url = $link.attr("href"); + const $notificationBlock = $link.parents(".notification-block"); + const resp = await fetch(url, { method: "POST" }); + const json = await resp.json(); + + if (json["error"]) { + return false; + } + + $notificationBlock.find(".notification-block-hd").html(resp["response"]); + $notificationBlock.find(".notification-block-bd").html("").toggle(); return false; }); /* Notification block: invitations: */ -$(document).on("submit", "#notifcation-block-invitations form", function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - - var that = this; - $.post( - url, - data, - function (response) { - if (response["error"]) { - $(that) - .find(".main-message") - .html("

      " + response["error"] + "

      "); - } else { - if (response["count"] == 0) { - $(that).find("input").hide(); - $("#invitation-count-text").html( - response["count"] + " invitations", - ); - $(that).find(".main-message").html("

      Thanks!

      "); - } else { - var invitation_text = - response["count"] == 1 ? "invitation" : "invitations"; - $("#invitation-count-text").html( - response["count"] + " " + invitation_text, - ); - $(that).find(".main-message").html(response["message"]); - $("#email_address").val(""); - } - } - }, - "json", - ); +$(document).on("submit", "#notifcation-block-invitations form", async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget); + const data = $form.serialize(); + const url = $form.attr("action"); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); - return false; + if (json["error"]) { + $form.find(".main-message").html(`

      ${response["error"]}

      `); + } else { + if (json["count"] == 0) { + $form.find("input").hide(); + $("#invitation-count-text").html(`${json["count"]} invitations`); + $form.find(".main-message").html("

      Thanks!

      "); + } else { + const invitation_text = + json["count"] == 1 ? "invitation" : "invitations"; + $("#invitation-count-text").html( + `${json["count"]} ${invitation_text}`, + ); + $form.find(".main-message").html(json["message"]); + $("#email_address").val(""); + } + } }); /* Notification block: shake invitations: */ $(document).on( "submit", "#notifcation-block-shakeinvitation form", - function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var $block = $(this).parents(".notification"); - var $header = $( + async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget); + const data = $form.serialize(); + const url = $form.attr("action"); + const $block = $form.parents(".notification"); + const $header = $( "#notifcation-block-shakeinvitation .notification-block-hd", ); - - var that = this; - $.post( - url, - data, - function (response) { - if (!response["error"]) { - $block.remove(); - // we update the header differently when presenting only one - // invitation on the shake page itself. - if ($header.hasClass("invitation-single")) { - $header.html("Got it."); - } else { - var invitation_text = - response["count"] == 1 - ? "invitation" - : "invitations"; - $header.html( - response["count"] + " new shake " + invitation_text, - ); - } - } - }, - "json", - ); - - return false; + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + + if (!json["error"]) { + $block.remove(); + // we update the header differently when presenting only one + // invitation on the shake page itself. + if ($header.hasClass("invitation-single")) { + $header.html("Got it."); + } else { + const invitationText = + json["count"] == 1 ? "invitation" : "invitations"; + $header.html(`${json["count"]} new shake ${invitationText}`); + } + } }, ); -var init_notification_invitation_request = function () { - const $notification_invitation_request = $( +const initNotificationInvitationRequest = function () { + const $notificationInvitationRequest = $( "#notification-block-invitation-request", ); - if ($notification_invitation_request.length > 0) { + if ($notificationInvitationRequest.length > 0) { NotificationInvitationContainer.populate( - $notification_invitation_request, + $notificationInvitationRequest, ); } }; -init_notification_invitation_request(); +initNotificationInvitationRequest(); // Expand all notifications. $("#notification-block-aggregate").click(function () { $(this).find(".notification-block-hd").html("Loading..."); $.get("/account/quick-notifications", function (response) { $("#notification-block-aggregate").hide().after(response); - init_notification_invitation_request(); + initNotificationInvitationRequest(); }); }); @@ -560,21 +509,21 @@ $(".field-submit .btn:not(.g-recaptcha)").click(function () { }); /* Site Nav dropdown */ -const $site_nav = $("#site-nav"); -var site_nav_expanded = false; +const $siteNav = $("#site-nav"); +let siteNavExpanded = false; $("#site-nav .site-nav--toggle").click(function (event) { event.stopPropagation(); - if (site_nav_expanded == false) { - site_nav_expanded = true; - $site_nav.addClass("is-expanded"); + if (siteNavExpanded == false) { + siteNavExpanded = true; + $siteNav.addClass("is-expanded"); $("body").one("click", function () { - $site_nav.removeClass("is-expanded"); - site_nav_expanded = false; + $siteNav.removeClass("is-expanded"); + siteNavExpanded = false; }); } else { - $site_nav.removeClass("is-expanded"); + $siteNav.removeClass("is-expanded"); $("body").unbind("click"); - site_nav_expanded = false; + siteNavExpanded = false; } }); @@ -583,21 +532,21 @@ $("#site-nav .site-nav--list").click(function (event) { }); /* Choose a shake dropdown */ -const $choose_a_shake = $("#choose-a-shake"); -var shake_expanded = false; +const $chooseAShake = $("#choose-a-shake"); +let shakeExpanded = false; $("#choose-a-shake .choose-a-shake--toggle").click(function (event) { event.stopPropagation(); - if (shake_expanded == false) { - shake_expanded = true; - $choose_a_shake.addClass("is-expanded"); + if (shakeExpanded == false) { + shakeExpanded = true; + $chooseAShake.addClass("is-expanded"); $("body").one("click", function () { - $choose_a_shake.removeClass("is-expanded"); - shake_expanded = false; + $chooseAShake.removeClass("is-expanded"); + shakeExpanded = false; }); } else { - $choose_a_shake.removeClass("is-expanded"); + $chooseAShake.removeClass("is-expanded"); $("body").unbind("click"); - shake_expanded = false; + shakeExpanded = false; } }); @@ -606,20 +555,22 @@ $("#choose-a-shake .choose-a-shake--dropdown").click(function (event) { }); /* Conversations - mute this button */ -$(".mute-this-conversation").click(function () { - var $form = $(this).next(".mute-this-conversation-form"); - var url = $form.attr("action"); - var data = $form.serialize(); - var $conversation = $(this).parents(".conversation"); - $.post(url, data, function () { - $conversation.fadeOut("slow"); - }); - return false; -}); - -var $image_comments_permalink = $("#image-comments-permalink"); -if ($image_comments_permalink.length > 0) { - PermalinkCommentsView.addEvents($image_comments_permalink); +$(".mute-this-conversation").click(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $button = $(ev.currentTarget); + const $form = $button.next(".mute-this-conversation-form"); + const url = $form.attr("action"); + const data = $form.serialize(); + const $conversation = $button.parents(".conversation"); + await fetch(url, { method: "POST", body: new URLSearchParams(data) }); + $conversation.fadeOut("slow"); +}); + +const $imageCommentsPermalink = $("#image-comments-permalink"); +if ($imageCommentsPermalink.length > 0) { + PermalinkCommentsView.addEvents($imageCommentsPermalink); } $("#nsfw-filter-button a").click(function () { @@ -627,23 +578,18 @@ $("#nsfw-filter-button a").click(function () { return false; }); -$("#apps .disconnect").click(function () { +$("#apps .disconnect").click(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + if (confirm("Are you sure you want to disconnect this app?")) { - var $form = $(this).parent().next("form"); - var url = $form.attr("action"); - var data = $form.serialize(); - var parent = $(this).parents("li"); - $.post( - url, - data, - function (response) { - parent.hide("slow"); - }, - "ajax", - ); - return false; - } else { - return false; + const $button = $(ev.currentTarget); + const $form = $button.parent().next("form"); + const url = $form.attr("action"); + const data = $form.serialize(); + const parent = $button.parents("li"); + await fetch(url, { method: "POST", body: new URLSearchParams(data) }); + parent.hide("slow"); } }); @@ -655,6 +601,7 @@ if ($("#shake-categories").length > 0) { } // Shake Page - change image. +// TODO this could probably be a CSS :hover $("#shake-image-edit").hover( function () { $(this).addClass("shake-image-hover"); @@ -676,6 +623,7 @@ $(".shake-edit-title-form .cancel").click(function () { return false; }); +// TODO this could probably be a CSS :hover $(".shake-edit-title").hover( function () { $(this).addClass("shake-edit-title-hover"); @@ -685,58 +633,50 @@ $(".shake-edit-title").hover( }, ); -$(".shake-edit-title").click(function () { - var $title_container = $(this).closest(".shake-details"); - var url = $title_container.find("form").attr("action"); - var that = this; - - $.get( - url, - function (result) { - if ("title_raw" in result) { - $(that).hide(); - $title_container - .find(".shake-edit-title-input") - .val(result["title_raw"]); - $(that).next(".shake-edit-title-form").show(); - } - }, - "json", - ); +$(".shake-edit-title").click(async (ev) => { + const $label = $(ev.currentTarget); + const $container = $label.closest(".shake-details"); + const url = $container.find("form").attr("action"); + const resp = await fetch(url); + const json = await resp.json(); + + if ("title_raw" in json) { + $label.hide(); + $container.find(".shake-edit-title-input").val(json["title_raw"]); + $label.next(".shake-edit-title-form").show(); + } }); -$(".shake-edit-title-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("title" in result && "title_raw" in result) { - var $title_container = $(that).closest(".shake-details"); - $title_container - .find(".shake-edit-title") - .html(result["title"]) - .show(); - $title_container - .find(".shake-edit-title-input") - .val(result["title_raw"]); - $title_container.find(".shake-edit-title-form").hide(); - } - }, - "json", - ); - return false; +$(".shake-edit-title-form").submit(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget); + const data = $form.serialize(); + const url = $form.attr("action"); + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + + if ("title" in json && "title_raw" in json) { + const $container = $form.closest(".shake-details"); + $container.find(".shake-edit-title").html(json["title"]).show(); + $container.find(".shake-edit-title-input").val(json["title_raw"]); + $container.find(".shake-edit-title-form").hide(); + } }); // Shake Page: Edit Description -$(".shake-edit-description-form .cancel").click(function () { - $(this).parents(".shake-details").find(".shake-edit-description").show(); - $(this).closest(".shake-edit-description-form").hide(); +$(".shake-edit-description-form .cancel").click((ev) => { + const $form = $(ev.currentTarget); + $form.parents(".shake-details").find(".shake-edit-description").show(); + $form.closest(".shake-edit-description-form").hide(); return false; }); +// TODO this could probably be a CSS :hover $(".shake-edit-description").hover( function () { $(this).addClass("shake-edit-description-hover"); @@ -746,63 +686,63 @@ $(".shake-edit-description").hover( }, ); -$(".shake-edit-description").click(function () { - var $title_container = $(this).closest(".shake-details"); - var url = $title_container.find("form").attr("action"); - var that = this; - - $.get( - url, - function (result) { - if ("description_raw" in result) { - $(that).hide(); - $title_container - .find(".shake-edit-description-input") - .val(result["description_raw"]); - $(that).next(".shake-edit-description-form").show(); - } - }, - "json", - ); +$(".shake-edit-description").click(async (ev) => { + const $label = $(ev.currentTarget); + // Form is sibling of label, so find the enclosing container ... + const $container = $label.closest(".shake-details"); + + // ... then navigate down to the form + const url = $container.find("form").attr("action"); + const resp = await fetch(url); + const json = await resp.json(); + + if ("description_raw" in json) { + $label.hide(); + $container + .find(".shake-edit-description-input") + .val(json["description_raw"]); + $label.next(".shake-edit-description-form").show(); + } }); -$(".shake-edit-description-form").submit(function () { - var data = $(this).serialize(); - var url = $(this).attr("action"); - var that = this; - $.post( - url, - data, - function (result) { - if ("description" in result && "description_raw" in result) { - var $title_container = $(that).closest(".shake-details"); - $title_container - .find(".shake-edit-description") - .html(result["description"]) - .show(); - $title_container - .find(".shake-edit-description-input") - .val(result["description_raw"]); - $title_container.find(".shake-edit-description-form").hide(); - } - }, - "json", - ); - return false; +$(".shake-edit-description-form").submit(async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + const $form = $(ev.currentTarget); + const data = $form.serialize(); + const url = $form.attr("action"); + + const resp = await fetch(url, { + method: "POST", + body: new URLSearchParams(data), + }); + const json = await resp.json(); + if ("description" in json && "description_raw" in json) { + const $container = $form.closest(".shake-details"); + $container + .find(".shake-edit-description") + .html(json["description"]) + .show(); + $container + .find(".shake-edit-description-input") + .val(json["description_raw"]); + $container.find(".shake-edit-description-form").hide(); + } }); -var $user_counts = $("#user-counts"); -if ($user_counts.length > 0) { - UserCounts.populate($user_counts); +const $userCountsPanel = $("#user-counts"); +if ($userCountsPanel.length > 0) { + UserCounts.populate($userCountsPanel); } -/* Shake Page: Request invitation to shake */ -var $request_invitation = $("#request-invitation"); -if ($request_invitation.length > 0) { - RequestInvitation.attachEvents($request_invitation); +/* Shake Page: Request invitation to join shake */ +const $requestInvitationPanel = $("#request-invitation"); +if ($requestInvitationPanel.length > 0) { + RequestInvitation.attachEvents($requestInvitationPanel); } -// Button to remove from shake. +// Button to remove user from shake membership. $(".remove-from-shake").click(function () { $form = $(this).find("form").submit(); return false; @@ -810,17 +750,13 @@ $(".remove-from-shake").click(function () { //make incoming clickable (I know.) $(".incoming-header").click(function () { - document.location = - document.location.protocol + - "//" + - document.location.host + - "/incoming"; + document.location = `${document.location.protocol}//${document.location.host}/incoming`; }); /* Shake Page: Remove Members From Shake */ -var $shake_member_list = $("#shake-members-list"); -if ($shake_member_list.length > 0) { - ShakeMemberList.attachEvents($shake_member_list); +const $shakeMembersList = $("#shake-members-list"); +if ($shakeMembersList.length > 0) { + ShakeMemberList.attachEvents($shakeMembersList); } // support for dismissable "Vote" banner;