mb. AUTO-FOCUS + KEYBOARD-SELECT

musicbrainz.org: MOUSE-LESS EDITING! Cleverly focus and refocus fields in various MusicBrainz edit pages and tracklist Up Down key navigation

  1. // ==UserScript==
  2. // @name mb. AUTO-FOCUS + KEYBOARD-SELECT
  3. // @version 2024.10.2
  4. // @description musicbrainz.org: MOUSE-LESS EDITING! Cleverly focus and refocus fields in various MusicBrainz edit pages and tracklist Up Down key navigation
  5. // @namespace https://github.com/jesus2099/konami-command
  6. // @supportURL https://github.com/jesus2099/konami-command/labels/mb_AUTO-FOCUS-KEYBOARD-SELECT
  7. // @author jesus2099
  8. // @licence CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/
  9. // @licence GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
  10. // @since 2012-06-08; https://web.archive.org/web/20131103163357/userscripts.org/scripts/show/135547 / https://web.archive.org/web/20141011084007/userscripts-mirror.org/scripts/show/135547
  11. // @icon data:image/gif;base64,R0lGODlhEAAQAKEDAP+/3/9/vwAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh/glqZXN1czIwOTkAIfkEAQACAwAsAAAAABAAEAAAAkCcL5nHlgFiWE3AiMFkNnvBed42CCJgmlsnplhyonIEZ8ElQY8U66X+oZF2ogkIYcFpKI6b4uls3pyKqfGJzRYAACH5BAEIAAMALAgABQAFAAMAAAIFhI8ioAUAIfkEAQgAAwAsCAAGAAUAAgAAAgSEDHgFADs=
  12. // @grant none
  13. // @include /^https?:\/\/(\w+\.)?musicbrainz\.org\/(recording|release)\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\/delete$/
  14. // @match *://*.musicbrainz.org/*/*/add-alias
  15. // @match *://*.musicbrainz.org/*/*/alias/*/delete
  16. // @match *://*.musicbrainz.org/*/*/alias/*/edit
  17. // @match *://*.musicbrainz.org/*/*/edit_annotation*
  18. // @match *://*.musicbrainz.org/*/*/tags*
  19. // @match *://*.musicbrainz.org/artist/*/edit
  20. // @match *://*.musicbrainz.org/artist/create
  21. // @match *://*.musicbrainz.org/cdtoc/*/set-durations?tracklist=*
  22. // @match *://*.musicbrainz.org/cdtoc/attach*
  23. // @match *://*.musicbrainz.org/cdtoc/move*
  24. // @match *://*.musicbrainz.org/cdtoc/remove*
  25. // @match *://*.musicbrainz.org/edit/*/cancel*
  26. // @match *://*.musicbrainz.org/edit-note/*/modify
  27. // @match *://*.musicbrainz.org/isrc/delete*
  28. // @match *://*.musicbrainz.org/label/*/edit
  29. // @match *://*.musicbrainz.org/label/create
  30. // @match *://*.musicbrainz.org/recording/*/add-isrc
  31. // @match *://*.musicbrainz.org/recording/*/edit
  32. // @match *://*.musicbrainz.org/recording/*/remove-puid*
  33. // @match *://*.musicbrainz.org/recording/create*
  34. // @match *://*.musicbrainz.org/release/*/add-cover-art
  35. // @match *://*.musicbrainz.org/release/*/edit
  36. // @match *://*.musicbrainz.org/release/*/edit-cover-art/*
  37. // @match *://*.musicbrainz.org/release/*/remove-cover-art/*
  38. // @match *://*.musicbrainz.org/release/*/reorder-cover-art
  39. // @match *://*.musicbrainz.org/release/add*
  40. // @match *://*.musicbrainz.org/release-group/*/edit
  41. // @match *://*.musicbrainz.org/release-group/create*
  42. // @match *://*.musicbrainz.org/url/*/edit
  43. // @match *://*.musicbrainz.org/work/*/add-iswc
  44. // @match *://*.musicbrainz.org/work/*/edit
  45. // @match *://*.musicbrainz.org/work/create*
  46. // @run-at document-end
  47. // ==/UserScript==
  48. "use strict";
  49. // focus most clever field http://tickets.musicbrainz.org/browse/MBS-2213
  50. var input = getMostCleverInputToFocus();
  51. if (input) {
  52. input.focus();
  53. if (input.getAttribute("type") == "text") {
  54. // Put caret at the text end
  55. var value_length = input.value.length;
  56. if (value_length) {
  57. input.setSelectionRange(value_length, value_length);
  58. }
  59. }
  60. highlight(input);
  61. }
  62. // re‐focus input field after related tool click http://tickets.musicbrainz.org/browse/MBS-7321
  63. // TODO: Maybe remove if bad good idea https://tickets.metabrainz.org/browse/MBS-7321?focusedId=67924&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-67924
  64. var tools = document.querySelectorAll("input[class*='with-guesscase'] ~ button:not(.guesscase-options)");
  65. for (var t = 0; t < tools.length; t++) {
  66. tools[t].addEventListener("click", function(event) {
  67. var related_input = this.parentNode.querySelector("input");
  68. related_input.focus();
  69. highlight(related_input);
  70. });
  71. }
  72. function addTracklistUpDownKeyNavigation() {
  73. // press UP↓/↑DOWN keys to navigate through track positions, names and lengths, auto clean‐up and format track length
  74. var tracklist = document.querySelector("#tracklist");
  75. if (tracklist) {
  76. tracklist.addEventListener("keydown", function(event) {
  77. // detect UP ↑ / DOWN ↓ to push cursor to upper or lower text field
  78. var class_name_match = event.target.className.match(/(?:\s|^)(pos|track-(name|length))(?:\s|$)/);
  79. if (class_name_match && (event.key == "ArrowUp" || event.key == "ArrowDown")) {
  80. var same_class_inputs = tracklist.querySelectorAll("input." + class_name_match[1]);
  81. var index;
  82. for (index = 0; index < same_class_inputs.length; index++) {
  83. if (same_class_inputs[index] == event.target) {
  84. break;
  85. }
  86. }
  87. index += event.key == "ArrowUp" ? -1 : 1;
  88. if (index >= 0 && index < same_class_inputs.length) {
  89. event.preventDefault();
  90. event.stopPropagation();
  91. if (
  92. event.shiftKey
  93. || event.target.className.match(/pos|track-length/)
  94. || event.target.selectionEnd - event.target.selectionStart === event.target.value.length
  95. ) {
  96. // select all
  97. // input.select() is more straightforward, but it does not scroll the window
  98. same_class_inputs[index].setSelectionRange(0, same_class_inputs[index].value.length);
  99. } else {
  100. if (event.target.selectionStart == event.target.selectionEnd && event.target.selectionStart == event.target.value.length) {
  101. // place the caret at the end
  102. same_class_inputs[index].setSelectionRange(same_class_inputs[index].value.length, same_class_inputs[index].value.length);
  103. } else {
  104. // keep same selection or caret position
  105. same_class_inputs[index].setSelectionRange(event.target.selectionStart, event.target.selectionEnd);
  106. }
  107. }
  108. // activate the input (but does not scroll to make it visible)
  109. same_class_inputs[index].focus();
  110. }
  111. }
  112. });
  113. }
  114. }
  115. function getMostCleverInputToFocus() {
  116. var best_input;
  117. switch (location.pathname.replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/, "*").replace(/[0-9]+/, "*")) {
  118. case "/artist/*/add-alias":
  119. case "/work/*/add-alias":
  120. case "/label/*/add-alias":
  121. case "/artist/*/alias/*/edit":
  122. case "/label/*/alias/*/edit":
  123. case "/work/*/alias/*/edit":
  124. best_input = document.querySelector("input[id='id-edit-alias.name']");
  125. break;
  126. case "/artist/create":
  127. case "/artist/*/edit":
  128. best_input = document.querySelector("input[id='id-edit-artist.name']");
  129. break;
  130. case "/artist/*/edit_annotation":
  131. case "/label/*/edit_annotation":
  132. case "/recording/*/edit_annotation":
  133. case "/release/*/edit_annotation":
  134. case "/release-group/*/edit_annotation":
  135. case "/work/*/edit_annotation":
  136. best_input = document.querySelector("textarea[id='id-edit-annotation.text']");
  137. break;
  138. case "/cdtoc/attach":
  139. case "/cdtoc/move":
  140. best_input = (
  141. document.querySelector("input[id='id-filter-release.query']")
  142. || document.querySelector("input[id='id-filter-artist.query']")
  143. );
  144. break;
  145. case "/label/create":
  146. case "/label/*/edit":
  147. best_input = document.querySelector("input[id='id-edit-label.name']");
  148. break;
  149. case "/recording/create":
  150. case "/recording/*/edit":
  151. best_input = document.querySelector("input[id='id-edit-recording.name']");
  152. break;
  153. case "/recording/*/add-isrc":
  154. best_input = document.querySelector("input[id='id-add-isrc.isrc']");
  155. break;
  156. case "/release/add":
  157. case "/release/*/edit":
  158. best_input = (
  159. document.querySelector("input#name")
  160. );
  161. addTracklistUpDownKeyNavigation();
  162. break;
  163. case "/release/*/add-cover-art":
  164. best_input = document.querySelector("input[id='id-add-cover-art.comment']");
  165. break;
  166. case "/release/*/edit-cover-art/*":
  167. best_input = document.querySelector("input[id='id-edit-cover-art.comment']");
  168. break;
  169. case "/artist/*/tags":
  170. case "/label/*/tags":
  171. case "/recording/*/tags":
  172. case "/release/*/tags":
  173. case "/release-group/*/tags":
  174. case "/work/*/tags":
  175. best_input = document.querySelector("textarea[id='id-tag.tags']");
  176. break;
  177. case "/url/*/edit":
  178. best_input = document.querySelector("input[id='id-edit-url.url']");
  179. break;
  180. case "/release-group/create":
  181. case "/release-group/*/edit":
  182. best_input = document.querySelector("input[id='id-edit-release-group.name']");
  183. break;
  184. case "/work/create":
  185. case "/work/*/edit":
  186. best_input = document.querySelector("input[id='id-edit-work.name']");
  187. break;
  188. case "/work/*/add-iswc":
  189. best_input = document.querySelector("input[id='id-add-iswc.iswc']");
  190. break;
  191. }
  192. return best_input ? best_input : document.querySelector("textarea[id*='edit_note'], textarea[id*='edit-note']");
  193. }
  194. // the focus animation
  195. var interval, blue;
  196. function highlight(input) {
  197. blue = 0;
  198. interval = setInterval(function() { hl(input); }, 50);
  199. }
  200. function hl(input) {
  201. input.style.setProperty("background-color", "rgb(255, 255, " + blue + ")");
  202. blue += 50;
  203. if (blue >= 255) {
  204. clearInterval(interval);
  205. input.style.removeProperty("background-color");
  206. }
  207. }