Greasy Fork is available in English.

accessibility_知乎键盘访问优化

针对知乎的屏幕阅读器可访问性优化

  1. // ==UserScript==
  2. // @name accessibility_知乎键盘访问优化
  3. // @namespace https://www.zhihu.com/people/yin-xiao-bo-11
  4. // @require https://greasyfork.org/scripts/432103-az-%E5%BF%AB%E6%8D%B7%E9%94%AE/code/AZ%20%E5%BF%AB%E6%8D%B7%E9%94%AE.js?version=968636
  5. // @version 0.7.1
  6. // @description 针对知乎的屏幕阅读器可访问性优化
  7. // @author Veg
  8. // @include https://*.zhihu.com/*
  9. // @include https://zhuanlan.zhihu.com/*
  10. // @grant none
  11. // ==/UserScript==
  12. (function () {
  13. 'use strict';
  14. let exclude = ':not(.veg-mark)';
  15. let mo = new MutationObserver((mutationRecord) => {
  16. middleFunction();
  17. });
  18. mo.observe(document.body, {
  19. 'childList': true,
  20. 'subtree': true
  21. });
  22. function middleFunction() {
  23. globalShortcutKey();
  24. middlewareFunction();
  25. proc();
  26. }
  27. function middlewareFunction() {
  28. let token = window.location.href.substring(20).split('/');
  29. if (token[1] == 'follow' || token[1] == 'hot' || token[1] == '') {
  30. let aside = document.querySelector('div.TopstoryPageHeader-aside' + exclude);
  31. if (aside) {
  32. aside.classList.add('veg-mark');
  33. aside.parentNode.setAttribute('style', 'display:none');
  34. }
  35. }
  36. if (token[1] == 'topic') {
  37. //
  38. }
  39. if (token[1] == 'question') {
  40. let title = document.querySelector('h1.QuestionHeader-title');
  41. if (title) {
  42. title.setAttribute('style', 'display:none');
  43. }
  44. let expandable = document.querySelector('div.QuestionRichText');
  45. if (expandable) {
  46. expandable.setAttribute('tabindex', '-1');
  47. let more = expandable.querySelector('button.QuestionRichText-more');
  48. if (more) {
  49. more.addEventListener('click', () => {
  50. expandable.focus();
  51. }, null);
  52. }
  53. }
  54. }
  55. if (token[1] == 'people' || token[1] == 'org') {
  56. let profile = document.querySelector('ul.ProfileMain-tabs');
  57. if (profile) {
  58. profile.setAttribute('style', 'display:none');
  59. }
  60. }
  61. if (token[3] === 'log' || token[3] === 'logs' || token[3] === 'topic_logs') {
  62. insdel();
  63. }
  64. //endFunction
  65. }
  66. //内容快捷键
  67. function globalShortcutKey() {
  68. let timelineStr = 'div.TopstoryItem' + exclude + ', div.QuestionAnswer-content' + exclude + ', div.List-item' + exclude + ', section.HotItem' + exclude + ', div.TopstoryItem-isFollow' + exclude;
  69. let timeline = document.querySelectorAll(timelineStr);
  70. for (let i = 0, l = timeline.length; i < l; i++) {
  71. timeline[i].classList.add('veg-mark');
  72. if (!timeline[i].classList.contains('hotkey-AZ') && timeline[i].hasAttribute('tabindex')) {
  73. timeline[i].classList.add('hotkey-AZ');
  74. }
  75. let modalWrap = timeline[i].querySelector('div.ModalWrap');
  76. if (modalWrap) {
  77. modalWrap.style = 'display:none';
  78. }
  79. }
  80. // end
  81. }
  82. //细节优化
  83. function proc() {
  84. let videoPackage = document.querySelectorAll('div.ZVideo-video' + exclude + ', div.VideoCard-player' + exclude);
  85. for (let i = 0, l = videoPackage.length; i < l; i++) {
  86. if (!videoPackage[i].classList.contains('veg-mark')) {
  87. videoPackage[i].classList.add('veg-mark');
  88. let button = document.createElement('button');
  89. button.innerHTML = '播放';
  90. button.style = 'background:#FFA500;';
  91. button.addEventListener('click', function () {
  92. let iframe = this.parentNode.querySelector('iframe');
  93. if (iframe)
  94. window.open(iframe.src);
  95. }, null);
  96. videoPackage[i].insertBefore(button, videoPackage[i].firstChild);
  97. }
  98. }
  99. //选项卡
  100. let tab = document.querySelectorAll('[role="tab"]' + exclude + ', [role="tablist"]' + exclude);
  101. for (let i = 0, l = tab.length; i < l; i++) {
  102. tab[i].classList.add('veg-mark');
  103. tab[i].removeAttribute('role');
  104. }
  105. let tooltip = document.querySelectorAll('[data-tooltip]' + exclude);
  106. for (let i = 0, l = tooltip.length; i < l; i++) {
  107. tooltip[i].classList.add('veg-mark');
  108. let label = tooltip[i].getAttribute('data-tooltip');
  109. tooltip[i].setAttribute('aria-label', label);
  110. }
  111. //搜索
  112. let search = document.querySelector('label.SearchBar-input' + exclude);
  113. if (search) {
  114. search.classList.add('veg-mark')
  115. search.querySelector('input[placeholder]').setAttribute('aria-label', '知乎搜索');
  116. }
  117. //优化一些菜单项
  118. let hdxx = document.querySelectorAll('button[aria-haspopup]' + exclude);
  119. for (let i = 0, l = hdxx.length; i < l; i++) {
  120. hdxx[i].classList.add('veg-mark');
  121. hdxx[i].removeAttribute('role');
  122. hdxx[i].removeAttribute('aria-haspopup');
  123. }
  124. /*
  125. //补货通知
  126. let notice = document.querySelector('div.Notification-textSection' + exclude);
  127. if (notice) {
  128. notice.classList.add('veg-mark')
  129. let text = notice.innerText;
  130. chrome.runtime.sendMessage({
  131. ttsState: "speak",
  132. text: text,
  133. enqueue: false
  134. });
  135. }
  136. */
  137. //公共编辑理由、上传视频、上传文档
  138. //注册、登录
  139. //发视频、写想法
  140. let lyStr = 'div.css-stef5c' + exclude + ', div.SignFlow-tab' + exclude + ', div.Login-socialButton' + exclude + ', div.Editable-docModal-uploader-text' + exclude + ',div.Editable-videoModal-uploader-text' + exclude + ', div.NewGlobalWrite-topTitle' + exclude;
  141. let ly = document.querySelectorAll(lyStr);
  142. for (let i = 0, l = ly.length; i < l; i++) {
  143. ly[i].classList.add('veg-mark')
  144. ly[i].classList.add('zhihu-click');
  145. ly[i].setAttribute('role', 'button');
  146. ly[i].setAttribute('tabindex', '0');
  147. }
  148.  
  149. // 隐藏选择语言、用户头像、匿名用户头像、动态中的原点
  150. let hiddenElementStr = 'span.Footer-dot' + exclude + ', img[alt="匿名用户"]' + exclude + ', a.UserLink-link' + exclude + ', input[placeholder="选择语言"]' + exclude + ', span.Bull' + exclude;
  151. let hiddenElement = document.querySelectorAll(hiddenElementStr);
  152. for (let i = 0, l = hiddenElement.length; i < l; i++) {
  153. hiddenElement[i].classList.add('veg-mark')
  154. if (hiddenElement[i].hasAttribute('alt') ||
  155. hiddenElement[i].hasAttribute('placeholder') ||
  156. hiddenElement[i].querySelector('img[src]') ||
  157. hiddenElement[i].classList.contains('Bull') ||
  158. hiddenElement[i].classList.contains('Footer-dot')
  159. ) {
  160. hiddenElement[i].setAttribute('style', 'display:none');
  161. }
  162. }
  163. // 优化对话框访问
  164. (function () {
  165. setTimeout(function () {
  166. let dhk = document.querySelectorAll("div.Modal" + exclude);
  167. for (let i = 0, l = dhk.length; i < l; i++) {
  168. dhk[i].classList.add('veg-mark')
  169. dhk[i].setAttribute("role", "dialog");
  170. dhk[i].setAttribute("aria-labelledby", ":1");
  171. let dhks = dhk[i].querySelectorAll("h3.Modal-title,div.Topbar-title,.CommentTopbar-title");
  172. for (let i = 0, l = dhks.length; i < l; i++) {
  173. dhks[i].setAttribute("id", ":1");
  174. }
  175. let yc = dhk[i].querySelectorAll("button.Tag-remove");
  176. for (let i = 0, l = yc.length; i < l; i++) {
  177. yc[i].setAttribute('aria-label', '移除');
  178. }
  179. }
  180. }, 80);
  181. })();
  182. //给匿名用户文本增加焦点
  183. let users = document.querySelectorAll('span.UserLink' + exclude);
  184. for (let i = 0; i < users.length; i++) {
  185. users[i].classList.add('veg-mark')
  186. let names = users[i].innerText;
  187. if (names == '匿名用户') {
  188. users[i].setAttribute('tabindex', '0');
  189. users[i].setAttribute('role', 'link');
  190. }
  191. }
  192. //处理评论
  193. let plButton = document.querySelectorAll('div.CommentItem-footer' + exclude + ',div.CommentItemV2-footer' + exclude);
  194. for (let i = 0, l = plButton.length; i < l; i++) {
  195. plButton[i].classList.add('veg-mark')
  196. plButton[i].setAttribute('tabindex', '-1');
  197. plButton[i].setAttribute('role', 'link');
  198. }
  199. // endFunction
  200. }
  201. document.body.addEventListener("keydown", function (k) {
  202. shareShortcutKey(k);
  203. //键盘点击
  204. let t = k.target;
  205. if (t.classList.contains('hotkey-AZ')) {
  206. if (k.altKey && k.keyCode == 13) {
  207. let text = ClearBr(t.innerText);
  208. navigator.clipboard.writeText(text);
  209. }
  210. }
  211. if (t.classList.contains('zhihu-click')) {
  212. if (k.keyCode == 13 || k.keyCode == 32) {
  213. t.click();
  214. }
  215. }
  216. }, null);
  217. // 阅读全文和收起的焦点管理
  218. document.body.addEventListener('click', (e) => {
  219. let t = e.target;
  220. if (t.classList.contains('ContentItem-more') || t.classList.contains('ContentItem-expandButton') || t.classList.contains('ContentItem-action')) {
  221. let parent = t.parentNode;
  222. while (!parent.classList.contains('hotkey-AZ')) {
  223. parent = parent.parentNode;
  224. }
  225. if (parent.classList.contains('hotkey-AZ')) {
  226. let heading = parent.querySelector('h2.ContentItem-title');
  227. if (heading) {
  228. heading.querySelector('a').focus();
  229. }
  230. else {
  231. parent.focus();
  232. }
  233. }
  234. }
  235. }, null);
  236. //全局快捷键函数
  237. function shareShortcutKey(k) {
  238. let hotkey = document.querySelectorAll('.hotkey-AZ');
  239. for (let i=0, l=hotkey.length; i<l; i++) {
  240. if (k.altKey && k.keyCode == 65) {
  241. hotkey[0].focus();
  242. }
  243. if (k.altKey && k.keyCode == 90) {
  244. hotkey[l - 1].focus();
  245. }
  246. }
  247. //获取页面代码
  248. if (k.altKey && k.keyCode == 66) {
  249. console.log(document.body.innerHTML);
  250. var input = document.createElement('input');
  251. input.value = document.body.innerHTML;
  252. document.body.appendChild(input);
  253. }
  254. if (k.altKey && k.keyCode == 82) {
  255. document.querySelector('button[aria-label="更多"]').focus();
  256. }
  257. if (k.keyCode == 113) {
  258. let Comment = document.querySelector('div.CommentItemV2');
  259. if (Comment !== null) {
  260. Comment.setAttribute('tabindex', '-1');
  261. Comment.focus();
  262. } else {
  263. let pinComment = document.querySelector('div.CommentItem');
  264. if (pinComment !== null)
  265. pinComment.setAttribute('tabindex', '-1');
  266. pinComment.focus();
  267. }
  268. }
  269. if (k.altKey && k.keyCode == 81) {
  270. var gb = document.querySelectorAll("button.ContentItem-action");
  271. for (var i = 0; i < gb.length; i++) {
  272. var gbName = gb[i].innerText;
  273. var gbNames = gbName.substring(2, 6);
  274. if (gbName == '​收起评论' || gbNames == '收起评论') {
  275. gb[i].click();
  276. gb[i].focus();
  277. }
  278. }
  279. }
  280. if (k.altKey && k.keyCode == 49) {
  281. var f = document.querySelectorAll('a.QuestionMainAction,a.NumberBoard-item,a[href="/lives"],button.follow-button,button.NumberBoard-itemWrapper');
  282. for (var i = 0; i < f.length; i++) {
  283. //a.zg-btn-white,
  284. f[0].focus();
  285. }
  286. }
  287. if (k.altKey && k.keyCode == 50) {
  288. document.querySelector("button.PaginationButton-prev").focus();
  289. }
  290. if (k.altKey && k.keyCode == 51) {
  291. document.querySelector("button.PaginationButton-next").focus();
  292. }
  293. if (k.altKey && k.keyCode == 52) {
  294. var more = document.querySelector('a.zu-button-more'); {
  295. more.setAttribute('tabindex', '0');
  296. more.focus();
  297. }
  298. }
  299. if (k.ctrlKey && k.keyCode == 81) {
  300. document.querySelector('a[href="/community"]').focus();
  301. }
  302. if (k.altKey && k.keyCode == 88) {
  303. if (document.querySelector('button.QuestionHeader-edit') !== null) {
  304. document.querySelector('button.QuestionHeader-edit').focus();
  305. } else {
  306. document.querySelector('button.NumberBoard-item').focus();
  307. }
  308. }
  309. }
  310.  
  311. function insdel() {
  312. let ins = document.querySelectorAll('ins' + exclude);
  313. for (let i = 0, l = ins.length; i < l; i++) {
  314. if (!ins[i].classList.contains('veg-mark')) {
  315. ins[i].classList.add('veg-mark');
  316. let parent = ins[i].parentNode;
  317. let div1 = document.createElement("div");
  318. div1.innerHTML = "「已插入:";
  319. parent.insertBefore(div1, ins[i]);
  320. div1.appendChild(ins[i]);
  321. let div2 = document.createElement("span");
  322. div2.innerHTML = ":插入结束」";
  323. ins[i].appendChild(div2);
  324. }
  325. }
  326. let del = document.querySelectorAll('del' + exclude);
  327. for (let i = 0, l = del.length; i < l; i++) {
  328. if (!del[i].classList.contains('veg-mark')) {
  329. del[i].classList.add('veg-mark');
  330. let parent = del[i].parentNode;
  331. let div1 = document.createElement("div");
  332. div1.innerHTML = "「已删除:";
  333. parent.insertBefore(div1, del[i]);
  334. div1.appendChild(del[i]);
  335. let div2 = document.createElement("span");
  336. div2.innerHTML = ":删除结束」";
  337. del[i].appendChild(div2);
  338. }
  339. }
  340. let fh = document.querySelectorAll('span.zg-bull' + exclude);
  341. for (let i = 0, l = fh.length; i < l; i++) {
  342. fh[i].classList.add('veg-mark');
  343. let name = fh[i].innerText;
  344. if (name == '•') {
  345. fh[i].setAttribute('style', 'display:none');
  346. }
  347. }
  348. let gly = document.querySelectorAll('a' + exclude);
  349. for (let i = 0; i < gly.length; i++) {
  350. gly[i].classList.add('veg-mark');
  351. let name = gly[i].innerText;
  352. if (name == '知乎管理员') {
  353. gly[i].setAttribute('href', 'javascript:;');
  354. }
  355. }
  356. }
  357. function ClearBr(key) {
  358. key = key.replace(
  359. /\n(\n)*( )*(\n)*\n/g,
  360. "\n");
  361. return key;
  362. }
  363. //判断是否有可被 focus 的能力
  364. function abilityDetection(t) {
  365. if ((t.tabIndex >= 0 || t.hasAttribute && t.hasAttribute('tabindex') && t.getAttribute('tabindex') == '-1') &&
  366. !t.hasAttribute('disabled') &&
  367. t.getAttribute('aria-hidden') !== 'true' &&
  368. t.offsetParent !== null)
  369. return true;
  370. }
  371. })();