diff --git a/app/assets/javascripts/keyboard_tools.js b/app/assets/javascripts/keyboard_tools.js new file mode 100644 index 0000000000000000000000000000000000000000..990dfdc8c7bd68904f42407ed927a9267128cf1f --- /dev/null +++ b/app/assets/javascripts/keyboard_tools.js @@ -0,0 +1,263 @@ +$(() => { + _CodidactKeyboard = { + state: 'home', + selectedItem: null, + user_id: parseInt($('.header--item.is-complex.is-visible-on-mobile[href^="/users/"').attr('href').split("/").pop()), + is_mod: !!$('.header--item[href="/mod/flags"]').length, + categories: function () { + category_elements = $("a.category-header--tab"); + return_obj = {}; + category_elements.each(function () { + return_obj[this.innerText] = this.getAttribute('href'); + }); + return return_obj; + }, + dialog: function (msg) { + _CodidactKeyboard.dialogClose(); + d = document.createElement("div") + d.classList.add("__keyboard_help"); + d.innerText = msg; + document.body.appendChild(d); + }, + dialogClose: function () { + $(".__keyboard_help").remove(); + _CodidactKeyboard.state = 'home'; + }, + updateSelected: function () { + $(".__keyboard_selected").removeClass('__keyboard_selected'); + if (_CodidactKeyboard.selectedItem) { + _CodidactKeyboard.selectedItem.classList.add('__keyboard_selected'); + _CodidactKeyboard.selectedItem.scrollIntoView({ behavior: 'smooth' }); + _CodidactKeyboard.selectedItem.focus(); + + _CodidactKeyboard.selectedItemData = { + type: _CodidactKeyboard.selectedItem.getAttribute("data-ckb-item-type"), + post_id: _CodidactKeyboard.selectedItem.getAttribute("data-ckb-post-id") + }; + } + } + } + + // Use html, so that all prior attempts to access keyup event have priority + $("html").on("keyup", function (e) { + if (e.target != document.body) return; + if (e.key == "Escape") { + _CodidactKeyboard.dialogClose(); + } else if (_CodidactKeyboard.state == 'home') { + homeMenu(e); + } else if (_CodidactKeyboard.state == 'goto') { + gotoMenu(e); + } else if (_CodidactKeyboard.state == 'goto/category') { + categoryMenu(e); + } else if (_CodidactKeyboard.state == 'goto/category-tags') { + categoryTagsMenu(e); + } else if (_CodidactKeyboard.state == 'tools') { + toolsMenu(e); + } else if (_CodidactKeyboard.state == 'tools/vote') { + voteMenu(e); + } + }); + + function homeMenu(e) { + if (e.key == "?") { + _CodidactKeyboard.dialog('Codidact Keyboard Shortcuts\n' + + '===========================\n' + + '? Open this help\n' + + 'n New post\n' + + ' (in current category)\n' + + 's Search for something\n' + + 'g Go to a page...\n\n' + + 'a Go to answer field\n\n' + + 'Selection shortcuts:\n\n' + + 'j Move one item down\n' + + 'k Move one item up\n' + + 't Use a tool (on selection)\n\n' + + '(Selection shortcuts will select\n' + + 'first post, if none selected)' + + ); + } else if (e.key == 'n') { + new_post_link = $('a.category-header--nav-item.is-button').attr('href'); + if (new_post_link) + window.location.href = new_post_link; + } else if (e.key == 'g') { + _CodidactKeyboard.dialog('Go to ...\n' + + '=========\n' + + 'm Main page\n' + + 'u User list\n' + + 'h Help\n' + + 'p Your profile page\n' + + 'c Category ...\n' + + 't Tags of category ...' + + (_CodidactKeyboard.is_mod ? '\nf Flags (mod only)' : '') + ); + _CodidactKeyboard.state = 'goto'; + } else if (e.key == 'k') { + if (_CodidactKeyboard.selectedItem == null) _CodidactKeyboard.selectedItem = $("[data-ckb-list-item]:first-of-type")[0]; + else { + _CodidactKeyboard.selectedItem = $(_CodidactKeyboard.selectedItem).nextAll('[data-ckb-list-item]')[0] || _CodidactKeyboard.selectedItem; + } + _CodidactKeyboard.updateSelected(); + } else if (e.key == 'j') { + if (_CodidactKeyboard.selectedItem == null) _CodidactKeyboard.selectedItem = $("[data-ckb-list-item]:first-of-type")[0]; + else { + _CodidactKeyboard.selectedItem = $(_CodidactKeyboard.selectedItem).prevAll('[data-ckb-list-item]')[0] || _CodidactKeyboard.selectedItem; + } + _CodidactKeyboard.updateSelected(); + } else if (e.key == 't') { + if (_CodidactKeyboard.selectedItem == null) _CodidactKeyboard.selectedItem = $("[data-ckb-list-item]:first-of-type")[0]; + _CodidactKeyboard.updateSelected(); + + if (_CodidactKeyboard.selectedItemData.type == "post") { + _CodidactKeyboard.dialog('Use tool ...\n' + + '============\n' + + 'f Flag\n' + + 'e Edit\n' + + 'c Comment\n' + + 'l Get permalink\n' + + 'h View history\n' + + 'v Vote ...' + + (_CodidactKeyboard.is_mod ? '\nt Use tools' : '') + ); + _CodidactKeyboard.state = 'tools'; + } + } else if (e.key == 'a') { + cl = $('#answer_body_markdown'); + cl[0].scrollIntoView({ behavior: "smooth" }); + cl.focus(); + _CodidactKeyboard.dialogClose(); + } else if (e.key == 'Enter') { + if (_CodidactKeyboard.selectedItemData.type == "link") { + link = $(_CodidactKeyboard.selectedItem).find("[data-ckb-item-link]").attr("href"); + window.location.href = link; + } + } + } + + function gotoMenu(e) { + if (e.key == 'm') { + window.location.href = '/'; + } else if (e.key == 'u') { + window.location.href = '/users'; + } else if (e.key == 'h') { + window.location.href = '/help'; + } else if (e.key == 'p') { + window.location.href = '/users/' + _CodidactKeyboard.user_id; + } else if (e.key == 'f') { + window.location.href = '/mod/flags'; + } else if (e.key == 'f') { + window.location.href = '/mod/flags'; + } else if (e.key == "t") { + data = _CodidactKeyboard.categories(); + data = Object.entries(data); + string_response = ""; + for (var i = 0; i < data.length; i++) { + entry = data[i]; + string_response += (i + 1) + " " + entry[0] + "\n" + } + _CodidactKeyboard.dialog('Go to tags of category ...\n' + + '==================\n' + + string_response.trim() + ); + _CodidactKeyboard.state = 'goto/category-tags'; + window.location.href = tlink; + } else if (e.key == 'c') { + data = _CodidactKeyboard.categories(); + data = Object.entries(data); + string_response = ""; + for (var i = 0; i < data.length; i++) { + entry = data[i]; + string_response += (i + 1) + " " + entry[0] + "\n" + } + _CodidactKeyboard.dialog('Go to category ...\n' + + '==================\n' + + "c Category List\n" + + string_response.trim() + ); + _CodidactKeyboard.state = 'goto/category'; + } + + } + + function categoryMenu(e) { + if (e.key == "c") { + window.location.href = "/categories"; + } else { + number = parseInt(e.key); + if (number != NaN) { + data = _CodidactKeyboard.categories(); + data = Object.entries(data); + + category = data[number - 1]; + window.location.href = category[1]; + } + } + } + + function categoryTagsMenu(e) { + number = parseInt(e.key); + if (number != NaN) { + data = _CodidactKeyboard.categories(); + data = Object.entries(data); + + category = data[number - 1]; + window.location.href = category[1] + "/tags"; + } + } + + function toolsMenu(e) { + if (e.key == 'e') { + window.location.href = $(_CodidactKeyboard.selectedItem).find('.tools--item i.fa.fa-pencil-alt').parent().attr("href"); + } else if (e.key == 'h') { + window.location.href = $(_CodidactKeyboard.selectedItem).find('.tools--item i.fa.fa-history').parent().attr("href"); + } else if (e.key == 'l') { + window.location.href = $(_CodidactKeyboard.selectedItem).find('.tools--item i.fa.fa-link').parent().attr("href"); + } else if (e.key == 'c') { + cl = $(_CodidactKeyboard.selectedItem).find('.js-add-comment'); + cl.nextAll("form").css("display", "block"); + cl.nextAll("form")[0].scrollIntoView({ behavior: "smooth" }); + cl.nextAll("form").find(".js-comment-content").focus(); + _CodidactKeyboard.dialogClose(); + } else if (e.key == 'f') { + cl = $(_CodidactKeyboard.selectedItem).find('.post--action-dialog.js-flag-box'); + cl.addClass("is-active"); + cl[0].scrollIntoView({ behavior: "smooth" }); + cl.find(".js-flag-comment").focus(); + _CodidactKeyboard.dialogClose(); + } else if (e.key == 'v') { + _CodidactKeyboard.dialog('Vote ...\n' + + '========\n' + + 'u Up\n' + + 'd Down\n' + + 'c Close' + ); + _CodidactKeyboard.state = 'tools/vote'; + } else if (e.key == 't') { + cl = $(_CodidactKeyboard.selectedItem).find('a.tools--item i.fa.fa-wrench').parent(); + cl = $(cl.attr("data-modal")); + cl.toggleClass("is-active"); + cl.focus(); + _CodidactKeyboard.dialogClose(); + } + + } + + function voteMenu(e) { + if (e.key == 'u') { + cl = $(_CodidactKeyboard.selectedItem).find('.vote-button[data-vote-type="1"]'); + cl.click(); + _CodidactKeyboard.dialogClose(); + } else if (e.key == 'd') { + cl = $(_CodidactKeyboard.selectedItem).find('.vote-button[data-vote-type="-1"]'); + cl.click(); + _CodidactKeyboard.dialogClose(); + } else if (e.key == 'c') { + cl = $(_CodidactKeyboard.selectedItem).find('.post--action-dialog.js-close-box'); + cl.addClass("is-active"); + cl[0].scrollIntoView({ behavior: "smooth" }); + cl.focus(); + _CodidactKeyboard.dialogClose(); + } + + } +}); \ No newline at end of file diff --git a/app/assets/stylesheets/keyboard_tools.scss b/app/assets/stylesheets/keyboard_tools.scss new file mode 100644 index 0000000000000000000000000000000000000000..385a5517fe2196847b7810dc8eb255b68beb37a8 --- /dev/null +++ b/app/assets/stylesheets/keyboard_tools.scss @@ -0,0 +1,15 @@ +.__keyboard_help { + padding: 1rem; + font-family: monospace; + white-space: pre-wrap; + width: 350px; + background-color: rgba(0,0,0,0.8); + color: white; + border-radius: 0.25rem; + position: fixed; + left: 1rem; + bottom: 1rem; +} +.__keyboard_selected { + outline: 0.25rem solid red; +} \ No newline at end of file diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index 4d278900e26b22c2fce7f072ce03045b341c028a..513208b5ce6a8216062af3e80b8b5f492b69e702 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -7,93 +7,93 @@ <div class="grid"> <% if current_user.is_global_admin %> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-globe has-color-red-700"></i> <i class="fas fa-cogs"></i> - <%= link_to 'Global Site Settings', global_settings_path %> + <%= link_to 'Global Site Settings', global_settings_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-globe has-color-red-700"></i> <i class="fas fa-tags"></i> - <%= link_to 'Global Tag Sets', global_tag_sets_path %> + <%= link_to 'Global Tag Sets', global_tag_sets_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-globe has-color-red-700"></i> <i class="fas fa-envelope"></i> - <%= link_to 'Email Moderators', moderator_email_path %> + <%= link_to 'Email Moderators', moderator_email_path, 'data-ckb-item-link' => '' %> </div> </div> </div> <% end %> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-cogs"></i> - <%= link_to 'Site Settings', site_settings_path %> + <%= link_to 'Site Settings', site_settings_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-tags"></i> - <%= link_to 'Tag Sets', tag_sets_path %> + <%= link_to 'Tag Sets', tag_sets_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-sliders-h"></i> - <%= link_to 'Privileges', admin_privileges_path %> + <%= link_to 'Privileges', admin_privileges_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-exclamation-triangle"></i> - <%= link_to 'Error Reports', admin_error_reports_path %> + <%= link_to 'Error Reports', admin_error_reports_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-hand-paper"></i> - <%= link_to 'Close Reasons', close_reasons_path %> + <%= link_to 'Close Reasons', close_reasons_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-balance-scale"></i> - <%= link_to 'Licenses', licenses_path %> + <%= link_to 'Licenses', licenses_path, 'data-ckb-item-link' => '' %> </div> </div> </div> - <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="grid--cell is-4-lg is-6-md is-12-sm" data-ckb-list-item data-ckb-item-type="link"> <div class="widget"> <div class="widget--body"> <i class="fas fa-user-secret"></i> - <%= link_to 'Audit Log', audit_log_path %> + <%= link_to 'Audit Log', audit_log_path, 'data-ckb-item-link' => '' %> </div> </div> </div> diff --git a/app/views/posts/_article_list.html.erb b/app/views/posts/_article_list.html.erb index 4c75f1adfe3b7ee3fc7a98d1c91abec4ba74fd50..c9dbed4f4e3e48fe4bcbd40b4708d1f0ab10f7db 100644 --- a/app/views/posts/_article_list.html.erb +++ b/app/views/posts/_article_list.html.erb @@ -1,7 +1,7 @@ <% active_user = post.last_activity_by || post.user %> <% show_type_tag ||= false %> <% show_category_tag ||= false %> -<div class="item-list--item <%= post.deleted ? 'deleted-content' : '' %>"> +<div class="item-list--item <%= post.deleted ? 'deleted-content' : '' %>" data-ckb-list-item data-ckb-item-type="link"> <div class="item-list--number-value"> <div class="meter is-question-score"> <div class="meter--bar is-<%= (post.score * 100).to_i %>%"><%= (post.score * 100).to_i %>%</div> @@ -18,7 +18,7 @@ <% if show_category_tag %> <span class="badge is-tag is-filled"><%= defined?(@category) ? @category.name : post.category.name %></span> <% end %> - <%= link_to post.title, share_article_path(post) %> + <%= link_to post.title, share_article_path(post), 'data-ckb-item-link' => '' %> </div> <p class="has-color-tertiary-600 has-float-right post-list--meta"> last activity <%= time_ago_in_words(post.last_activity) %> ago by <%= link_to active_user.username, user_path(active_user) %> diff --git a/app/views/posts/_expanded.html.erb b/app/views/posts/_expanded.html.erb index ff18d9da58109cd3ec817b0eb9b253540296c6a1..4414071db0b5bb0402d4453af1322f6606ccd6bc 100644 --- a/app/views/posts/_expanded.html.erb +++ b/app/views/posts/_expanded.html.erb @@ -2,7 +2,7 @@ <% is_top_level = post.parent_id.nil? %> <% has_tags = is_top_level && !post.tag_ids.empty? %> -<div class="post <%= post.meta? ? 'is-meta' : '' %> <%= is_top_level ? '' : 'has-border-bottom-style-solid has-border-bottom-width-1 has-border-color-tertiary-100' %>" data-post-id="<%= post.id %>" id="<%= (is_question ? 'question-' : 'answer-') + post.id.to_s %>"> +<div class="post <%= post.meta? ? 'is-meta' : '' %> <%= is_top_level ? '' : 'has-border-bottom-style-solid has-border-bottom-width-1 has-border-color-tertiary-100' %>" data-post-id="<%= post.id %>" id="<%= (is_question ? 'question-' : 'answer-') + post.id.to_s %>" data-ckb-list-item data-ckb-item-type="post" data-ckb-post-id="<%= post.id %>"> <% if is_top_level %> <h1 class="post--title has-border-top-width-4 has-border-top-style-solid has-border-color-<%= post.meta? ? 'tertiary' : 'primary' %>-400 has-padding-2"> <%= post.title %> diff --git a/app/views/posts/_list.html.erb b/app/views/posts/_list.html.erb index 836e0f8feabc68153d50a2752c0e2b458da22480..22e2d0ec88416c9b854c3915bcc318243c9dee89 100644 --- a/app/views/posts/_list.html.erb +++ b/app/views/posts/_list.html.erb @@ -3,7 +3,7 @@ <% active_user = post.last_activity_by || post.user %> <% show_type_tag ||= false %> <% show_category_tag ||= false %> -<div class="item-list--item <%= is_meta ? 'post__meta' : '' %> <%= post.deleted ? 'deleted-content' : '' %>"> +<div class="item-list--item <%= is_meta ? 'post__meta' : '' %> <%= post.deleted ? 'deleted-content' : '' %>" data-ckb-list-item data-ckb-item-type="link"> <div class="item-list--number-value"> <div class="meter is-question-score"> <div class="meter--bar is-<%= (post.score * 100).to_i %>%"><%= (post.score * 100).to_i %>%</div> @@ -17,7 +17,7 @@ </div> <div class="item-list--text-value is-primary"> <div class="post-list--title"> - <%= link_to generic_share_link(post) do %> + <%= link_to generic_share_link(post), 'data-ckb-item-link' => '' do %> <% if show_category_tag %> <span class="badge is-tag is-filled"><%= defined?(@category) ? @category.name : post.category.name %></span> <% end %> diff --git a/app/views/tags/_tag.html.erb b/app/views/tags/_tag.html.erb index 750f4633a985ce4c767190ff704eeea317ac2f1b..ccb2d56441b9d25af2439c7edcb2fdc86307d82f 100644 --- a/app/views/tags/_tag.html.erb +++ b/app/views/tags/_tag.html.erb @@ -1,4 +1,4 @@ -<div class="grid--cell tag-cell <%= params[:hierarchical].present? ? 'is-12-sm is-6' : 'is-4-lg is-6-md is-12-sm' %>"> +<div class="grid--cell tag-cell <%= params[:hierarchical].present? ? 'is-12-sm is-6' : 'is-4-lg is-6-md is-12-sm' %>" data-ckb-list-item data-ckb-item-type="link"> <% if tag.respond_to?(:path) && tag.path.present? %> <span class="has-font-size-caption"> <% tag.path.split(' > ')[0..-2].each do |tag| %> @@ -6,7 +6,7 @@ <% end %> </span> <% end %> - <%= link_to tag.name, tag_path(id: category.id, tag_id: tag.id), class: classes %> + <%= link_to tag.name, tag_path(id: category.id, tag_id: tag.id), class: classes, 'data-ckb-item-link' => '' %> <span class="has-color-tertiary-900">× <%= tag.post_count %></span> <% if tag.excerpt.present? %> <p class="has-font-size-caption has-color-tertiary-900"> diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb index 1001e0caf675dcacea13d2c51e2189c634970901..088553c2a61d067866eb7eae2f8ac20f133a0c62 100644 --- a/app/views/users/_user.html.erb +++ b/app/views/users/_user.html.erb @@ -1,9 +1,9 @@ -<div class="user-list--user"> +<div class="user-list--user" data-ckb-list-item data-ckb-item-type="link"> <div class="flex-row-always"> <div><img src="<%= avatar_url(user) %>" alt="user avatar" height="32" width="32" /></div> <div class="user--meta"> <% user_posts = defined?(@post_counts) ? @post_counts[user.id] : user.posts.count %> - <span class="username"><%= link_to user_path(user), class: "is-not-underlined" do %> + <span class="username"><%= link_to user_path(user), class: "is-not-underlined", 'data-ckb-item-link' => '' do %> <%= user.username %> <% if user.is_admin && SiteSetting['AdminBadgeCharacter'] %> <span class="badge is-user-role"><%= SiteSetting['AdminBadgeCharacter'] %></span>