accessibility.vue 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341
  1. <template>
  2. <header
  3. v-if="ariaSettings.isCompActive"
  4. class="accessibility"
  5. >
  6. <template
  7. v-if="ariaSettings.isCursorCrosshair"
  8. >
  9. <div
  10. class="crosshair-h aria-control-target aria-inverse-theme"
  11. :style="{top: crosshairPosition.y + 'px'}"
  12. />
  13. <div
  14. class="crosshair-v aria-control-target aria-inverse-theme"
  15. :style="{left: crosshairPosition.x + 'px'}"
  16. />
  17. </template>
  18. <div
  19. v-if="ariaSettings.isMagnifying"
  20. class="mignify-area"
  21. >
  22. <div class="text-wrapper">
  23. <p>
  24. {{ elemType + ((elemType && elemDisc) ? ': ' : '') + elemDisc }}
  25. </p>
  26. </div>
  27. <button
  28. tabindex="0"
  29. aria-description="Close"
  30. type="button"
  31. @click="onClickMagnifier"
  32. >
  33. <img
  34. :src="assetUrls.closeMagnifyArea"
  35. alt="close magnify area"
  36. >
  37. </button>
  38. </div>
  39. <menu
  40. v-show="ariaSettings.menuMode === 'old'"
  41. class="old-mode-menu"
  42. >
  43. <li>
  44. <button
  45. tabindex="0"
  46. aria-description="Reset"
  47. type="button"
  48. @click="onClickReset"
  49. >
  50. <img
  51. :src="assetUrls.reset"
  52. alt="reset"
  53. >
  54. <span>Reset</span>
  55. </button>
  56. </li>
  57. <li>
  58. <button
  59. tabindex="0"
  60. aria-description="Sound"
  61. type="button"
  62. @click="onClickMute"
  63. >
  64. <img
  65. :src="ariaSettings.isMuted ? assetUrls.muteActive : assetUrls.mute"
  66. alt="mute"
  67. >
  68. <span>Mute</span>
  69. </button>
  70. </li>
  71. <li>
  72. <button
  73. tabindex="0"
  74. aria-description="Speech Rate"
  75. type="button"
  76. @click="onClickSpeechRate"
  77. >
  78. <img
  79. :src="(ariaSettings.speechRateLevel === 3) ? assetUrls.speechRate : assetUrls.speechRateActive"
  80. alt="speech rate"
  81. >
  82. <span>speech <br> rate</span>
  83. </button>
  84. </li>
  85. <li>
  86. <button
  87. tabindex="0"
  88. aria-description="Screen Reader"
  89. type="button"
  90. @click="onClickScreenReaderMode"
  91. >
  92. <img
  93. :src="(ariaSettings.readMode === 'point') ? assetUrls.screenReaderMode : assetUrls.screenReaderModeActive"
  94. alt="screen reader"
  95. >
  96. <span>screen <br> reader</span>
  97. </button>
  98. </li>
  99. <li>
  100. <button
  101. tabindex="0"
  102. aria-description="Color Modification"
  103. type="button"
  104. @click="onClickColorModification"
  105. >
  106. <img
  107. :src="assetUrls.colorTheme"
  108. alt="color modification"
  109. >
  110. <span>color <br> modification</span>
  111. </button>
  112. </li>
  113. <li>
  114. <button
  115. tabindex="0"
  116. aria-description="Zoom In"
  117. type="button"
  118. @click="onClickZoomIn"
  119. >
  120. <img
  121. :src="assetUrls.zoomIn"
  122. alt="zoom in"
  123. >
  124. <span>zoom in</span>
  125. </button>
  126. </li>
  127. <li>
  128. <button
  129. tabindex="0"
  130. aria-description="Zoom Out"
  131. type="button"
  132. @click="onClickZoomOut"
  133. >
  134. <img
  135. :src="assetUrls.zoomOut"
  136. alt="zoom out"
  137. >
  138. <span>zoom out</span>
  139. </button>
  140. </li>
  141. <li>
  142. <button
  143. tabindex="0"
  144. aria-description="Cursor Style"
  145. type="button"
  146. @click="onClickCursorStyle"
  147. >
  148. <img
  149. :src="assetUrls.cursorStyle"
  150. alt="cursor style"
  151. >
  152. <span>cursor <br> style</span>
  153. </button>
  154. </li>
  155. <li>
  156. <button
  157. tabindex="0"
  158. aria-description="Cross Cursor"
  159. type="button"
  160. @click="onClickCrossCursor"
  161. >
  162. <img
  163. :src="assetUrls.crossCursor"
  164. alt="cross cursor"
  165. >
  166. <span>cross <br> cursor</span>
  167. </button>
  168. </li>
  169. <li>
  170. <button
  171. tabindex="0"
  172. aria-description="Magnifier"
  173. type="button"
  174. @click="onClickMagnifier"
  175. >
  176. <img
  177. :src="assetUrls.magnifier"
  178. alt="magnifier"
  179. >
  180. <span>magnifier</span>
  181. </button>
  182. </li>
  183. <li>
  184. <button
  185. tabindex="0"
  186. aria-description="Help"
  187. type="button"
  188. @click="onClickHelp"
  189. >
  190. <img
  191. :src="assetUrls.help"
  192. alt="help"
  193. >
  194. <span>help</span>
  195. </button>
  196. </li>
  197. <li>
  198. <a
  199. tabindex="0"
  200. aria-label="Button"
  201. aria-description="Download Shortcut"
  202. ref="shortcutBtnRef"
  203. :href="shortcutFileText"
  204. download="CapitalMuseum.url"
  205. @click="onClickDownloadShortcut"
  206. >
  207. <img
  208. :src="assetUrls.shotcut"
  209. alt="shotcut"
  210. >
  211. <span style="margin-top: 3px;">shortcut</span>
  212. </a>
  213. </li>
  214. <li>
  215. <button
  216. class="special-color"
  217. tabindex="0"
  218. aria-description="Screen Reading Accessibility"
  219. type="button"
  220. @click="onClickScreenReaderAreaEntry"
  221. >
  222. <img
  223. :src="assetUrls.screenReaderAreaEntry"
  224. alt="screen reading accessibility"
  225. >
  226. <span>Screen Reading <br> Accessibility</span>
  227. </button>
  228. </li>
  229. <li>
  230. <button
  231. tabindex="0"
  232. aria-description="Quit"
  233. type="button"
  234. @click="onClickQuit"
  235. >
  236. <img
  237. :src="assetUrls.quit"
  238. alt="quit"
  239. >
  240. <span>quit</span>
  241. </button>
  242. </li>
  243. </menu>
  244. <menu
  245. v-show="ariaSettings.menuMode === 'blind'"
  246. class="blind-mode-menu"
  247. >
  248. <div class="blind-mode-title">
  249. <h5 tabindex="0">Intelligent blind guide</h5>
  250. <div class="splitter-line" />
  251. <h5 tabindex="0">Regional guidelines</h5>
  252. </div>
  253. <li
  254. class="text-button"
  255. >
  256. <button
  257. tabindex="0"
  258. aria-description="Navigation area. Please use the shortcut key to select the area."
  259. @mousedown="onMouseDownNavigationArea"
  260. @click="onClickNavigationArea"
  261. type="button"
  262. >
  263. <div class="button-name">
  264. Navigation
  265. <br>
  266. area ({{navigationAreaNum}})
  267. </div>
  268. <div class="button-shortcut">
  269. Alt+1
  270. </div>
  271. </button>
  272. </li>
  273. <li
  274. class="text-button"
  275. >
  276. <button
  277. tabindex="0"
  278. aria-description="Window area. Please use the shortcut key to select the area."
  279. @mousedown="onMouseDownWindowArea"
  280. @click="onClickWindowArea"
  281. type="button"
  282. >
  283. <div class="button-name">
  284. Window
  285. <br>
  286. area ({{viewportAreaNum}})
  287. </div>
  288. <div class="button-shortcut">
  289. Alt+2
  290. </div>
  291. </button>
  292. </li>
  293. <li
  294. class="text-button"
  295. >
  296. <button
  297. tabindex="0"
  298. aria-description="Interaction area. Please use the shortcut key to select the area."
  299. @mousedown="onMouseDownInteractionArea"
  300. @click="onClickInteractionArea"
  301. type="button"
  302. >
  303. <div class="button-name">
  304. Interaction
  305. <br>
  306. area ({{interactionAreaNum}})
  307. </div>
  308. <div class="button-shortcut">
  309. Alt+3
  310. </div>
  311. </button>
  312. </li>
  313. <li
  314. class="image-button"
  315. >
  316. <button
  317. tabindex="0"
  318. aria-description="Mute"
  319. type="button"
  320. @click="onClickMute"
  321. >
  322. <img
  323. :src="ariaSettings.isMuted ? assetUrls.muteActive : assetUrls.mute"
  324. alt="mute"
  325. >
  326. <span>Mute</span>
  327. </button>
  328. </li>
  329. <li
  330. class="image-button"
  331. >
  332. <button
  333. tabindex="0"
  334. aria-description="Help"
  335. type="button"
  336. @click="onClickHelp"
  337. >
  338. <img
  339. :src="assetUrls.help"
  340. alt="help"
  341. >
  342. <span>Help</span>
  343. </button>
  344. </li>
  345. <li
  346. class="image-button"
  347. >
  348. <button
  349. tabindex="0"
  350. aria-description="Magnifier"
  351. type="button"
  352. @click="onClickMagnifier"
  353. >
  354. <img
  355. :src="assetUrls.magnifier"
  356. alt="magnifier"
  357. >
  358. <span>Magnifier</span>
  359. </button>
  360. </li>
  361. <li
  362. class="image-button"
  363. >
  364. <button
  365. class="special-color"
  366. tabindex="0"
  367. aria-description="Elerly services"
  368. type="button"
  369. @click="onClickElderlyServicesAreaEntry"
  370. >
  371. <img
  372. :src="assetUrls.elderlyServicesAreaEntry"
  373. alt="elderly services"
  374. >
  375. <span>Elderly services</span>
  376. </button>
  377. </li>
  378. <li
  379. class="image-button"
  380. >
  381. <button
  382. tabindex="0"
  383. aria-description="Quit"
  384. type="button"
  385. @click="onClickQuit"
  386. >
  387. <img
  388. :src="assetUrls.quit"
  389. alt="quit"
  390. >
  391. <span>Quit</span>
  392. </button>
  393. </li>
  394. </menu>
  395. </header>
  396. </template>
  397. <script>
  398. import utils from "/src/utils.js"
  399. import bigCursor from '/src/assets/images/accessibility/big-cursor.cur'
  400. import "/src/assets/css/ariaGlobalStyle.less"
  401. import assetUrls from '/src/assets/images/accessibility/index.js'
  402. const config = require('/src/config.js')
  403. const speechRateFactors = [
  404. 0.75,
  405. 1,
  406. 1.25,
  407. ]
  408. const themeList = [
  409. 'default',
  410. 'white',
  411. 'blue',
  412. 'yellow',
  413. 'black',
  414. ]
  415. const zoomFactors = [
  416. 1,
  417. 1.1,
  418. 1.2,
  419. 1.3,
  420. 1.4,
  421. 1.5,
  422. 1.6,
  423. 1.7,
  424. 1.8,
  425. 1.9,
  426. 2,
  427. ]
  428. const defaultAriaSettings = {
  429. isCompActive: false, //是否显示无障碍功能菜单
  430. menuMode: 'old', // 'old', 'blind'
  431. isMuted: false,
  432. speechRateLevel: 1,
  433. readMode: 'point', // 'point'指读, 'continue'连读
  434. themeIdx: 0,
  435. zoomLevel: 0,
  436. isBigCursor: false,
  437. isCursorCrosshair: false,
  438. isMagnifying: false,
  439. }
  440. export default {
  441. data() {
  442. return {
  443. assetUrls,
  444. ariaSettings: defaultAriaSettings,
  445. crosshairPosition: {
  446. x: -100,
  447. y: -100,
  448. },
  449. elemType: '',
  450. elemDisc: '',
  451. continueReadTimeoutId: null,
  452. continueReadTaskId: null,
  453. continueReadIteratorStoper: null,
  454. shortcutFileText: 'data:text/plain;charset=utf-8,' + encodeURIComponent(`
  455. [{000214A0-0000-0000-C000-000000000046}]
  456. Prop3=19,2
  457. [InternetShortcut]
  458. IDList=
  459. URL=${this.$homePageUrl}
  460. `),
  461. audioPlayer: null,
  462. navigationAreaNum: null,
  463. viewportAreaNum: null,
  464. interactionAreaNum: null,
  465. // 为了避免多余的朗读行为
  466. isJustMouseDown: false,
  467. domChangeLastTime: null,
  468. mutationObserver: null,
  469. requestReadLastTime: null,
  470. isJustWindowFocus: false,
  471. }
  472. },
  473. watch: {
  474. $route: {
  475. handler(v) {
  476. this.$nextTick(() => {
  477. this.navigationAreaNum = document.querySelectorAll('*[data-aria-navigation-area]').length
  478. this.viewportAreaNum = document.querySelectorAll('*[data-aria-viewport-area]').length
  479. this.interactionAreaNum = document.querySelectorAll('*[data-aria-interaction-area]').length
  480. })
  481. },
  482. immediate: true,
  483. },
  484. ariaSettings: {
  485. handler(v) {
  486. let storedSettings = localStorage.getItem('ariaSettings')
  487. if (storedSettings) {
  488. storedSettings = JSON.parse(storedSettings)
  489. if (utils.isSameObject(storedSettings, v)) {
  490. return
  491. }
  492. }
  493. localStorage.setItem('ariaSettings', JSON.stringify(v))
  494. },
  495. deep: true
  496. },
  497. 'ariaSettings.isCompActive': {
  498. handler(v) {
  499. if (v) {
  500. document.body.classList.add('aria-active')
  501. document.documentElement.classList.add('aria-active')
  502. this.$emit('show')
  503. this.$eventBus.$emit('aria-show')
  504. } else {
  505. this.reset()
  506. for (const iterator of document.body.classList) {
  507. if (iterator === 'aria-active') {
  508. document.body.classList.remove(iterator)
  509. }
  510. }
  511. for (const iterator of document.documentElement.classList) {
  512. if (iterator === 'aria-active') {
  513. document.documentElement.classList.remove(iterator)
  514. }
  515. }
  516. this.$emit('hide')
  517. this.$eventBus.$emit('aria-hide')
  518. }
  519. },
  520. immediate: true,
  521. },
  522. 'ariaSettings.readMode': {
  523. handler(v) {
  524. if (v === 'point') {
  525. this.continueReadTaskId = null
  526. document.removeEventListener('mouseover', this.onMouseOverForContinueRead, {
  527. passive: true,
  528. })
  529. document.addEventListener('mouseover', this.onMouseOverForPointRead, {
  530. passive: true,
  531. })
  532. } else if (v === 'continue') {
  533. document.removeEventListener('mouseover', this.onMouseOverForPointRead, {
  534. passive: true,
  535. })
  536. document.addEventListener('mouseover', this.onMouseOverForContinueRead, {
  537. passive: true,
  538. })
  539. }
  540. },
  541. immediate: true
  542. },
  543. 'ariaSettings.themeIdx': {
  544. handler() {
  545. this.$eventBus.$emit('color-modification', this.ariaSettings.themeIdx)
  546. this.updateThemeClass()
  547. },
  548. immediate: true,
  549. },
  550. 'ariaSettings.zoomLevel': {
  551. handler() {
  552. this.zoomPage()
  553. },
  554. immediate: true,
  555. },
  556. 'ariaSettings.isBigCursor': {
  557. handler(v) {
  558. if (v) {
  559. const styleNode = document.createElement("style")
  560. styleNode.type = 'text/css'
  561. styleNode.id = 'aria-big-cursor-style-node'
  562. styleNode.innerHTML = `* {cursor: url(${bigCursor}), auto !important}`
  563. document.head.appendChild(styleNode)
  564. } else {
  565. const toRemove = document.getElementById('aria-big-cursor-style-node')
  566. if (toRemove) {
  567. document.head.removeChild(toRemove)
  568. }
  569. }
  570. },
  571. immediate: true,
  572. },
  573. 'ariaSettings.isCursorCrosshair': {
  574. handler(v) {
  575. if (v) {
  576. document.addEventListener('mousemove', this.onMouseMoveForCrosshair, {
  577. passive: true,
  578. })
  579. this.$nextTick(() => {
  580. this.updateThemeClass()
  581. })
  582. } else {
  583. document.removeEventListener('mousemove', this.onMouseMoveForCrosshair, {
  584. passive: true,
  585. })
  586. }
  587. },
  588. immediate: true,
  589. },
  590. 'ariaSettings.isMagnifying': {
  591. handler(v) {
  592. if (v) {
  593. document.body.classList.add('aria-magnifying')
  594. this.$eventBus.$emit('aria-show-magnify-area')
  595. } else {
  596. for (const iterator of document.body.classList) {
  597. if (iterator === 'aria-magnifying') {
  598. document.body.classList.remove(iterator)
  599. this.$eventBus.$emit('aria-hide-magnify-area')
  600. }
  601. }
  602. }
  603. },
  604. immediate: true,
  605. },
  606. },
  607. created() {
  608. this.loadStoredSettings()
  609. // 在同一个域的多个页面间做同步
  610. window.addEventListener('storage', this.loadStoredSettings, {
  611. passive: true,
  612. })
  613. window.addEventListener('focus', this.onWindowFocus, {
  614. passive: true,
  615. })
  616. document.addEventListener('keydown', this.keyEventHandler, {
  617. passive: true,
  618. })
  619. document.addEventListener('focusin', this.onFocusIn, {
  620. passive: true,
  621. })
  622. document.addEventListener('mousedown', this.onMouseDown, {
  623. passive: true,
  624. })
  625. document.addEventListener('mouseover', this.onMouseOver, {
  626. passive: true,
  627. })
  628. document.addEventListener("visibilitychange", this.onPageVisibilityChange, {
  629. passive: true
  630. })
  631. this.$eventBus.$on('request-read', (text) => {
  632. console.log('无障碍组件收到request-read消息:', text);
  633. if (this.ariaSettings.isCompActive) {
  634. this.requestReadLastTime = Date.now()
  635. this.planToPlayAudio('', text)
  636. }
  637. })
  638. this.$eventBus.$on('request-magnify', (textObj) => {
  639. console.log('无障碍组件收到request-magnify消息:', textObj);
  640. if (this.ariaSettings.isCompActive) {
  641. this.elemType = textObj.elemType
  642. this.elemDisc = textObj.elemDisc
  643. }
  644. })
  645. this.$eventBus.$on('request-process-text-element', (rootElement) => {
  646. console.log('无障碍组件收到request-process-text-element消息:', rootElement);
  647. const tagNameList = [
  648. 'span', 'em', 'i', 'small', 'b', 'strong', 'del', 'q', 'sub',
  649. 'div', 'pre', 'p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
  650. 'td', 'th',
  651. ]
  652. for (const tagName of tagNameList) {
  653. const elemList = rootElement.getElementsByTagName(tagName)
  654. for (const elemItem of elemList) {
  655. elemItem.setAttribute('tabindex', '0')
  656. }
  657. }
  658. })
  659. this.$eventBus.$on('request-process-image-element', (rootElement) => {
  660. console.log('无障碍组件收到request-process-image-element消息:', rootElement);
  661. const tagNameList = [
  662. 'img',
  663. ]
  664. for (const tagName of tagNameList) {
  665. const elemList = rootElement.getElementsByTagName(tagName)
  666. for (const elemItem of elemList) {
  667. elemItem.setAttribute('tabindex', '0')
  668. }
  669. }
  670. })
  671. const config = { attributes: true, childList: true, subtree: true };
  672. const callback = (mutationsList, observer) => {
  673. this.domChangeLastTime = Date.now()
  674. };
  675. this.mutationObserver = new MutationObserver(callback);
  676. this.mutationObserver.observe(document.body, config);
  677. },
  678. mounted() {
  679. },
  680. destroyed() {
  681. window.removeEventListener('storage', this.loadStoredSettings, {
  682. passive: true,
  683. }),
  684. window.removeEventListener('focus', this.onWindowFocus, {
  685. passive: true,
  686. }),
  687. document.removeEventListener('keydown', this.keyEventHandler, {
  688. passive: true,
  689. })
  690. document.removeEventListener('focusin', this.onFocusIn, {
  691. passive: true,
  692. })
  693. document.removeEventListener('mouseover', this.onMouseOverForContinueRead, {
  694. passive: true,
  695. })
  696. document.removeEventListener('mouseover', this.onMouseOverForPointRead, {
  697. passive: true,
  698. })
  699. document.removeEventListener('mousedown', this.onMouseDown, {
  700. passive: true,
  701. })
  702. document.removeEventListener("visibilitychange", this.onPageVisibilityChange, {
  703. passive: true
  704. })
  705. this.$eventBus.$off('request-read')
  706. this.$eventBus.$off('request-magnify')
  707. this.$eventBus.$off('request-process-text-element')
  708. this.$eventBus.$off('request-process-image-element')
  709. this.mutationObserver.disconnect();
  710. },
  711. methods: {
  712. onWindowFocus() {
  713. this.isJustWindowFocus = true
  714. setTimeout(() => {
  715. this.isJustWindowFocus = false
  716. }, 0);
  717. },
  718. onPageVisibilityChange() {
  719. if (document.visibilityState === 'hidden') {
  720. if (this.audioPlayer && !this.audioPlayer.ended) {
  721. this.audioPlayer.pause()
  722. }
  723. }
  724. },
  725. onMouseDown() {
  726. this.isJustMouseDown = true
  727. setTimeout(() => {
  728. this.isJustMouseDown = false
  729. }, 0);
  730. },
  731. planToPlayAudio(taskId, text = '') {
  732. let XHR = new XMLHttpRequest()
  733. const that = this
  734. XHR.onreadystatechange = function() {
  735. if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
  736. const res = JSON.parse(this.response)
  737. if (that.audioPlayer && !that.audioPlayer.ended) {
  738. that.audioPlayer.pause()
  739. }
  740. that.audioPlayer = new Audio('http://192.168.0.245:8008' + res.msg)
  741. that.audioPlayer.muted = that.ariaSettings.isMuted
  742. that.audioPlayer.playbackRate = speechRateFactors[that.ariaSettings.speechRateLevel]
  743. that.audioPlayer.oncanplaythrough = () => {
  744. that.audioPlayer.play()
  745. }
  746. that.audioPlayer.onended = () => {
  747. that.$emit('audio-end', taskId)
  748. }
  749. that.audioPlayer.onerror = (e) => {
  750. console.error('audio error!', e)
  751. that.$emit('audio-error', taskId)
  752. }
  753. that.audioPlayer.onabort = () => {
  754. that.$emit('audio-abort', taskId)
  755. }
  756. }
  757. }
  758. XHR.open("POST", "http://192.168.0.245:8008/api/tts/toMp3")
  759. XHR.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
  760. XHR.send(JSON.stringify({
  761. content: text || this.elemType + (this.elemType ? ': ' : '') + this.elemDisc
  762. }))
  763. },
  764. keyEventHandler(e) {
  765. if (e.repeat) {
  766. return
  767. }
  768. if (e.key === "?" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  769. if (this.ariaSettings.isCompActive) {
  770. this.onClickHelp()
  771. }
  772. } else if (e.key === "Q" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  773. if (this.ariaSettings.isCompActive) {
  774. this.onClickQuit()
  775. }
  776. } else if (e.key === "!" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  777. if (this.ariaSettings.isCompActive) {
  778. this.onClickReset()
  779. }
  780. } else if (e.key === "@" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  781. if (this.ariaSettings.isCompActive) {
  782. this.onClickMute()
  783. }
  784. } else if (e.key === "#" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  785. if (this.ariaSettings.isCompActive) {
  786. this.onClickSpeechRate()
  787. }
  788. } else if (e.key === "$" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  789. if (this.ariaSettings.isCompActive) {
  790. this.onClickScreenReaderMode()
  791. }
  792. } else if (e.key === "%" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  793. if (this.ariaSettings.isCompActive) {
  794. this.onClickColorModification()
  795. }
  796. } else if (e.key === "^" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  797. if (this.ariaSettings.isCompActive) {
  798. this.onClickZoomIn()
  799. }
  800. } else if (e.key === "&" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  801. if (this.ariaSettings.isCompActive) {
  802. this.onClickZoomOut()
  803. }
  804. } else if (e.key === "*" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  805. if (this.ariaSettings.isCompActive) {
  806. this.onClickCursorStyle()
  807. }
  808. } else if (e.key === "(" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  809. if (this.ariaSettings.isCompActive) {
  810. this.onClickCrossCursor()
  811. }
  812. } else if (e.key === ")" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  813. if (this.ariaSettings.isCompActive) {
  814. if (this.ariaSettings.menuMode === 'old') {
  815. this.onClickScreenReaderAreaEntry()
  816. } else if (this.ariaSettings.menuMode === 'blind') {
  817. this.onClickElderlyServicesAreaEntry()
  818. }
  819. }
  820. } else if (e.key === "D" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  821. if (this.ariaSettings.isCompActive) {
  822. this.$refs['shortcutBtnRef'].click()
  823. }
  824. } else if (e.key === "N" && !e.altKey && !e.ctrlKey && e.shiftKey) {
  825. if (this.ariaSettings.isCompActive) {
  826. this.onClickMagnifier()
  827. }
  828. } else if (e.key === "1" && e.altKey && !e.ctrlKey && !e.shiftKey) {
  829. this.onClickNavigationArea()
  830. } else if (e.key === "2" && e.altKey && !e.ctrlKey && !e.shiftKey) {
  831. this.onClickWindowArea()
  832. } else if (e.key === "3" && e.altKey && !e.ctrlKey && !e.shiftKey) {
  833. this.onClickInteractionArea()
  834. }
  835. },
  836. loadStoredSettings() {
  837. const settings = localStorage.getItem('ariaSettings')
  838. if (settings) {
  839. this.ariaSettings = JSON.parse(settings)
  840. }
  841. },
  842. zoomPage() {
  843. let styleNode = document.getElementById('aria-zoom-style-node')
  844. if (!styleNode) {
  845. styleNode = document.createElement("style")
  846. styleNode.type = 'text/css'
  847. styleNode.id = 'aria-zoom-style-node'
  848. document.head.appendChild(styleNode)
  849. }
  850. if (zoomFactors[this.ariaSettings.zoomLevel] === 1) {
  851. styleNode.innerHTML = ''
  852. } else {
  853. styleNode.innerHTML = `
  854. .aria-control-target {
  855. transform: scale(${zoomFactors[this.ariaSettings.zoomLevel]});
  856. transform-origin: left top;
  857. }
  858. .aria-no-zoom {
  859. transform: scale(${1 / zoomFactors[this.ariaSettings.zoomLevel]});
  860. transform-origin: left top;
  861. }
  862. `
  863. }
  864. document.documentElement.scrollLeft = (document.documentElement.scrollWidth - document.documentElement.clientWidth) / 2
  865. },
  866. updateThemeClass() {
  867. this.$nextTick(() => {
  868. const controlTargetNodeList = [...document.getElementsByClassName('aria-control-target')]
  869. for (const node of controlTargetNodeList) {
  870. for (const iterator of node.classList) {
  871. if (iterator.indexOf('aria-theme-') > -1) {
  872. node.classList.remove(iterator)
  873. }
  874. }
  875. node.classList.add(`aria-theme-${themeList[this.ariaSettings.themeIdx]}`)
  876. }
  877. })
  878. },
  879. onMouseMoveForCrosshair(e) {
  880. this.crosshairPosition.x = e.clientX
  881. this.crosshairPosition.y = e.clientY
  882. },
  883. onMouseOverForPointRead: utils.debounce(function(e) {
  884. if (!this.ariaSettings.isCompActive) {
  885. return
  886. }
  887. const curTime = Date.now()
  888. if (curTime - this.domChangeLastTime <= 500 + 100) {
  889. console.log('DOM刚改变,忽略hover。');
  890. return
  891. }
  892. if (curTime - this.requestReadLastTime <= 500 + 100) {
  893. console.log('刚被要求朗读,忽略hover。');
  894. return
  895. }
  896. const extractedText = utils.extractTextForMagnify(e)
  897. if (extractedText) {
  898. this.elemType = extractedText.elemType
  899. this.elemDisc = extractedText.elemDisc
  900. this.planToPlayAudio()
  901. }
  902. }, 500),
  903. onMouseOverForContinueRead(e) {
  904. if (!this.ariaSettings.isCompActive) {
  905. return
  906. }
  907. const curTime = Date.now()
  908. if (curTime - this.domChangeLastTime <= 100) {
  909. console.log('DOM刚改变,忽略hover。');
  910. return
  911. }
  912. if (curTime - this.requestReadLastTime <= 100) {
  913. console.log('刚被要求朗读,忽略hover。');
  914. return
  915. }
  916. const extractedText = utils.extractTextForMagnify(e)
  917. if (extractedText) {
  918. this.elemType = extractedText.elemType
  919. this.elemDisc = extractedText.elemDisc
  920. clearTimeout(this.continueReadTimeoutId)
  921. this.continueReadTimeoutId = setTimeout(() => {
  922. this.continueReadIteratorStoper && this.continueReadIteratorStoper()
  923. if (this.ariaSettings.readMode !== 'continue') {
  924. return
  925. }
  926. const continueReadTaskId = (new Date).getTime()
  927. this.continueReadTaskId = continueReadTaskId
  928. utils.iterateOnFocusableNode(e.target, (node) => {
  929. return new Promise((resolve, reject) => {
  930. this.continueReadIteratorStoper = reject
  931. this.$once('audio-end', (taskId) => {
  932. if (taskId === continueReadTaskId) {
  933. resolve()
  934. }
  935. })
  936. this.$once('audio-error', (taskId) => {
  937. if (taskId === continueReadTaskId) {
  938. resolve()
  939. }
  940. })
  941. this.$once('audio-abort', (taskId) => {
  942. if (taskId === continueReadTaskId) {
  943. resolve()
  944. }
  945. })
  946. })
  947. })
  948. }, 1000)
  949. }
  950. },
  951. onFocusIn(e) {
  952. if (!this.ariaSettings.isCompActive) {
  953. return
  954. }
  955. // 如果刚刚切换到此浏览器此页面,虽然会导致上次离开前focus到的元素的再次focus,但不能重复朗读。
  956. if (this.isJustWindowFocus) {
  957. console.log('刚刚切换到此浏览器此页面,导致上次离开前focus到的元素的再次focus,忽略之。')
  958. return
  959. }
  960. // 如果是点击鼠标引起的focus
  961. if (this.isJustMouseDown) {
  962. if (
  963. document.activeElement.dataset.ariaNavigationArea !== undefined ||
  964. document.activeElement.dataset.ariaViewportArea !== undefined ||
  965. document.activeElement.dataset.ariaInteractionArea !== undefined
  966. ) {
  967. document.activeElement.blur()
  968. }
  969. console.log('刚按下鼠标,忽略focus。');
  970. return
  971. }
  972. const extractedText = utils.extractTextForMagnify(e, true)
  973. if (extractedText) {
  974. this.elemType = extractedText.elemType
  975. this.elemDisc = extractedText.elemDisc
  976. this.planToPlayAudio(this.continueReadTaskId)
  977. }
  978. },
  979. reset() {
  980. const copy = { ...defaultAriaSettings }
  981. copy.isCompActive = this.ariaSettings.isCompActive
  982. this.ariaSettings = copy
  983. },
  984. onClickReset() {
  985. this.$eventBus.$emit('request-read', "You've reset the feature settings")
  986. this.reset()
  987. },
  988. onClickMute() {
  989. this.ariaSettings.isMuted = !this.ariaSettings.isMuted
  990. if (this.ariaSettings.isMuted) {
  991. // this.$eventBus.$emit('request-read', "Sound off")
  992. } else {
  993. this.$eventBus.$emit('request-read', "Sound on")
  994. }
  995. if (this.audioPlayer) {
  996. this.audioPlayer.muted = this.ariaSettings.isMuted
  997. }
  998. },
  999. onClickSpeechRate() {
  1000. this.ariaSettings.speechRateLevel++
  1001. if (this.ariaSettings.speechRateLevel === speechRateFactors.length) {
  1002. this.ariaSettings.speechRateLevel = 0
  1003. }
  1004. if (this.ariaSettings.speechRateLevel === 0) {
  1005. this.$eventBus.$emit('request-read', "Speak slowly")
  1006. } else if (this.ariaSettings.speechRateLevel === 1) {
  1007. this.$eventBus.$emit('request-read', "Speak normally")
  1008. } else if (this.ariaSettings.speechRateLevel === 2) {
  1009. this.$eventBus.$emit('request-read', "Speak fast")
  1010. }
  1011. if (this.audioPlayer) {
  1012. this.audioPlayer.playbackRate = speechRateFactors[this.ariaSettings.speechRateLevel]
  1013. }
  1014. },
  1015. onClickScreenReaderMode() {
  1016. if (this.ariaSettings.readMode === 'point') {
  1017. this.$eventBus.$emit('request-read', "You've enabled continuous reading mode. Please move the mouse over on the text you need to read,it'll start reading the screen in 1 second. When reading the link, tap the Enter key to enter the corresponding page.")
  1018. this.ariaSettings.readMode = 'continue'
  1019. } else if (this.ariaSettings.readMode === 'continue') {
  1020. this.$eventBus.$emit('request-read', "You've enabled read-only mode. Please move the mouse over on the text you need to read,Blind users can operate the keyboard directly.")
  1021. this.ariaSettings.readMode = 'point'
  1022. }
  1023. },
  1024. onClickColorModification() {
  1025. this.ariaSettings.themeIdx++
  1026. if (this.ariaSettings.themeIdx === themeList.length) {
  1027. this.ariaSettings.themeIdx = 0
  1028. }
  1029. if (this.ariaSettings.themeIdx === 0) {
  1030. this.$eventBus.$emit('request-read', "Ajust to standard color.")
  1031. } else if (this.ariaSettings.themeIdx === 1) {
  1032. this.$eventBus.$emit('request-read', "Adjust to black lettering on white background.")
  1033. } else if (this.ariaSettings.themeIdx === 2) {
  1034. this.$eventBus.$emit('request-read', "Adjust to yellow lettering on blue background.")
  1035. } else if (this.ariaSettings.themeIdx === 3) {
  1036. this.$eventBus.$emit('request-read', "Adjust to black lettering on yellow background.")
  1037. } else if (this.ariaSettings.themeIdx === 4) {
  1038. this.$eventBus.$emit('request-read', "Adjust to yellow lettering on black background.")
  1039. }
  1040. },
  1041. onClickZoomIn() {
  1042. if (this.ariaSettings.zoomLevel === zoomFactors.length - 1) {
  1043. return
  1044. }
  1045. this.$eventBus.$emit('request-read', "Zooming in on page")
  1046. this.ariaSettings.zoomLevel++
  1047. },
  1048. onClickZoomOut() {
  1049. if (this.ariaSettings.zoomLevel === 0) {
  1050. return
  1051. }
  1052. this.$eventBus.$emit('request-read', "Zooming out on page")
  1053. this.ariaSettings.zoomLevel--
  1054. },
  1055. onClickCursorStyle() {
  1056. this.ariaSettings.isBigCursor = !this.ariaSettings.isBigCursor
  1057. if (this.ariaSettings.isBigCursor) {
  1058. this.$eventBus.$emit('request-read', "You've enabled the large cursor")
  1059. } else {
  1060. this.$eventBus.$emit('request-read', "You've disabled the large cursor")
  1061. }
  1062. },
  1063. onClickCrossCursor() {
  1064. this.ariaSettings.isCursorCrosshair = !this.ariaSettings.isCursorCrosshair
  1065. if (this.ariaSettings.isCursorCrosshair) {
  1066. this.$eventBus.$emit('request-read', "You've enabled the cross cursor")
  1067. } else {
  1068. this.$eventBus.$emit('request-read', "You've disabled the cross cursor")
  1069. }
  1070. },
  1071. onClickMagnifier() {
  1072. this.ariaSettings.isMagnifying = !this.ariaSettings.isMagnifying
  1073. if (this.ariaSettings.isMagnifying) {
  1074. this.$eventBus.$emit('request-read', "You've enabled the magnifier")
  1075. } else {
  1076. this.$eventBus.$emit('request-read', "You've disabled the magnifier")
  1077. }
  1078. },
  1079. onClickHelp() {
  1080. window.open(config.publicPath + 'help.html')
  1081. },
  1082. onClickDownloadShortcut() {
  1083. this.$eventBus.$emit('request-read', "You are downloading the shortcut. Double click the shortcut to reach the website.")
  1084. },
  1085. onClickElderlyServicesAreaEntry() {
  1086. this.ariaSettings.menuMode = 'old'
  1087. this.$eventBus.$emit('request-read', "You've switched to the elderly services mode.")
  1088. },
  1089. onClickScreenReaderAreaEntry() {
  1090. this.ariaSettings.menuMode = 'blind'
  1091. this.$eventBus.$emit('request-read', "You've switched to screen the reading accessibility mode.")
  1092. },
  1093. onMouseDownNavigationArea(e) {
  1094. e.preventDefault()
  1095. },
  1096. onClickNavigationArea() {
  1097. utils.getAndFocusNextNodeWithCustomAttribute('ariaNavigationArea')
  1098. },
  1099. onMouseDownWindowArea(e) {
  1100. e.preventDefault()
  1101. },
  1102. onClickWindowArea() {
  1103. utils.getAndFocusNextNodeWithCustomAttribute('ariaViewportArea')
  1104. },
  1105. onMouseDownInteractionArea(e) {
  1106. e.preventDefault()
  1107. },
  1108. onClickInteractionArea() {
  1109. utils.getAndFocusNextNodeWithCustomAttribute('ariaInteractionArea')
  1110. },
  1111. onClickQuit() {
  1112. this.ariaSettings.isCompActive = false
  1113. },
  1114. // 供外界调用
  1115. requestToShowMenu() {
  1116. !this.ariaSettings.isCompActive && (this.ariaSettings.isCompActive = true)
  1117. },
  1118. requestToHideMenu() {
  1119. this.ariaSettings.isCompActive && (this.ariaSettings.isCompActive = false)
  1120. },
  1121. requestToSwitchMenuShowHide() {
  1122. this.ariaSettings.isCompActive = !this.ariaSettings.isCompActive
  1123. },
  1124. }
  1125. }
  1126. </script>
  1127. <style lang="less" scoped>
  1128. @import '/src/assets/css/common.less';
  1129. li {
  1130. list-style: none;
  1131. }
  1132. button {
  1133. cursor: pointer;
  1134. border: none;
  1135. background: transparent;
  1136. padding: 0;
  1137. &:focus {
  1138. outline: 3px solid red;
  1139. }
  1140. }
  1141. a {
  1142. color: inherit;
  1143. text-decoration: none;
  1144. &:focus {
  1145. outline: 3px solid red;
  1146. }
  1147. }
  1148. .accessibility {
  1149. color: #fff;
  1150. font-size: 16px;
  1151. font-family: SourceHanSansCN-Bold-GBpc-EUC-H;
  1152. line-height: 19px;
  1153. background-color: #36584C;
  1154. height: @accessibility-menu-height;
  1155. position: fixed;
  1156. top: 0;
  1157. width: 100%;
  1158. z-index: 1000;
  1159. .crosshair-h {
  1160. position: fixed;
  1161. width: 100%;
  1162. height: 3px;
  1163. background: blue;
  1164. transform: translateY(-50%);
  1165. pointer-events: none;
  1166. z-index: 10;
  1167. }
  1168. .crosshair-v {
  1169. position: fixed;
  1170. height: 100vh;
  1171. width: 3px;
  1172. background: blue;
  1173. transform: translateX(-50%);
  1174. pointer-events: none;
  1175. z-index: 10;
  1176. }
  1177. .mignify-area {
  1178. position: fixed;
  1179. height: @magnify-area-height;
  1180. width: 100%;
  1181. bottom: 0;
  1182. background: #fff;
  1183. z-index: 1;
  1184. display: flex;
  1185. .text-wrapper {
  1186. height: @magnify-area-height;
  1187. line-height: @magnify-area-height;
  1188. width: 1px;
  1189. flex: 1 0 auto;
  1190. overflow: auto;
  1191. text-align: center;
  1192. white-space: break-spaces;
  1193. p {
  1194. vertical-align: middle;
  1195. display: inline-block;
  1196. color: #000;
  1197. font-size: 72px;
  1198. font-family: Source Han Sans CN;
  1199. font-weight: 800;
  1200. line-height: 86px;
  1201. }
  1202. }
  1203. button {
  1204. width: 199px;
  1205. height: 100%;
  1206. flex: 0 0 auto;
  1207. }
  1208. }
  1209. .old-mode-menu {
  1210. display: flex;
  1211. justify-content: center;
  1212. height: 100%;
  1213. li {
  1214. button, a {
  1215. box-sizing: border-box;
  1216. color: white;
  1217. height: 100%;
  1218. padding-top: 10px;
  1219. width: @accessibility-menu-height;
  1220. display: flex;
  1221. flex-direction: column;
  1222. align-items: center;
  1223. &:hover {
  1224. background-color: #4D2128;
  1225. }
  1226. &.special-color:not(:hover) {
  1227. background-color: #701c12;
  1228. }
  1229. img {
  1230. width: 50px;
  1231. height: 50px;
  1232. }
  1233. span {
  1234. display: block;
  1235. margin-top: 5px;
  1236. }
  1237. }
  1238. }
  1239. }
  1240. .blind-mode-menu {
  1241. display: flex;
  1242. justify-content: center;
  1243. height: 100%;
  1244. .blind-mode-title {
  1245. width: 231px;
  1246. height: 80px;
  1247. background: #753641;
  1248. border-radius: 20px;
  1249. border: 2px solid rgb(136, 67, 79);
  1250. text-align: center;
  1251. margin-top: 12px;
  1252. margin-right: 30px;
  1253. h5 {
  1254. font-size: 18px;
  1255. line-height: 36px;
  1256. }
  1257. .splitter-line {
  1258. width: 208px;
  1259. height: 0px;
  1260. border: 1px solid rgb(136, 67, 79);
  1261. margin: 0 auto;
  1262. }
  1263. }
  1264. li.text-button:nth-of-type(3) {
  1265. margin-right: 100px;
  1266. }
  1267. li.text-button {
  1268. button {
  1269. width: 125px;
  1270. color: #fff;
  1271. display: flex;
  1272. flex-direction: column;
  1273. justify-content: center;
  1274. align-items: center;
  1275. height: 100%;
  1276. &:hover {
  1277. background-color: #4D2128;
  1278. }
  1279. .button-name {
  1280. font-size: 14px;
  1281. line-height: 21px;
  1282. margin-bottom: 6px;
  1283. }
  1284. .button-shortcut {
  1285. font-size: 16px;
  1286. }
  1287. }
  1288. }
  1289. li.image-button {
  1290. button {
  1291. color: white;
  1292. height: 100%;
  1293. padding-top: 10px;
  1294. width: @accessibility-menu-height;
  1295. display: flex;
  1296. flex-direction: column;
  1297. align-items: center;
  1298. &:hover {
  1299. background-color: #4D2128;
  1300. }
  1301. &.special-color:not(:hover) {
  1302. background-color: #701c12;
  1303. }
  1304. img {
  1305. width: 50px;
  1306. height: 50px;
  1307. }
  1308. span {
  1309. display: block;
  1310. margin-top: 5px;
  1311. }
  1312. }
  1313. }
  1314. }
  1315. }
  1316. </style>