WME UI

UI Library for Waze Map Editor Greasy Fork scripts

当前为 2023-01-10 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/450320/1137009/WME%20UI.js

  1. // ==UserScript==
  2. // @name WME UI
  3. // @version 0.2.2
  4. // @description UI Library for Waze Map Editor Greasy Fork scripts
  5. // @license MIT License
  6. // @author Anton Shevchuk
  7. // @namespace https://greasyfork.org/users/227648-anton-shevchuk
  8. // @supportURL https://github.com/AntonShevchuk/wme-ui/issues
  9. // @match https://*.waze.com/editor*
  10. // @match https://*.waze.com/*/editor*
  11. // @exclude https://*.waze.com/user/editor*
  12. // @icon https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://anton.shevchuk.name&size=64
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. /* jshint esversion: 8 */
  17. /* global W, I18n */
  18.  
  19. // WARNING: this is unsafe!
  20. let unsafePolicy = {
  21. createHTML: string => string
  22. }
  23.  
  24. // Feature testing
  25. if (window.trustedTypes && window.trustedTypes.createPolicy) {
  26. unsafePolicy = window.trustedTypes.createPolicy('unsafe', {
  27. createHTML: string => string,
  28. })
  29. }
  30.  
  31. class WMEUI {
  32. /**
  33. * Normalize title or UID
  34. * @param string
  35. * @returns {string}
  36. */
  37. static normalize (string) {
  38. return string.replace(/\W+/gi, '-').toLowerCase()
  39. }
  40.  
  41. /**
  42. * Inject CSS styles
  43. * @param {String} css
  44. * @return void
  45. */
  46. static addStyle (css) {
  47. let style = document.createElement('style')
  48. style.type = 'text/css' // is required
  49. style.innerHTML = unsafePolicy.createHTML(css)
  50. document.querySelector('head').appendChild(style)
  51. }
  52.  
  53. /**
  54. * Add translation for I18n object
  55. * @param {String} uid
  56. * @param {Object} data
  57. * @return void
  58. */
  59. static addTranslation (uid, data) {
  60. if (!data.en) {
  61. console.error('Default translation `en` is required')
  62. }
  63. let locale = I18n.currentLocale()
  64. I18n.translations[locale][uid] = data[locale] || data.en
  65. }
  66.  
  67. /**
  68. * Create and register shortcut
  69. * @param {String} name
  70. * @param {String} desc
  71. * @param {String} group
  72. * @param {String} title
  73. * @param {String} shortcut
  74. * @param {Function} callback
  75. * @param {Object} scope
  76. * @return void
  77. */
  78. static addShortcut (name, desc, group, title, shortcut, callback, scope = null) {
  79. new WMEUIShortcut(name, desc, group, title, shortcut, callback, scope).register()
  80. }
  81. }
  82.  
  83. /**
  84. * God class, create it once
  85. */
  86. class WMEUIHelper {
  87. constructor (uid) {
  88. this.uid = WMEUI.normalize(uid)
  89. this.index = 0
  90. }
  91.  
  92. /**
  93. * Generate unque ID
  94. * @return {string}
  95. */
  96. generateId () {
  97. this.index++
  98. return this.uid + '-' + this.index
  99. }
  100.  
  101. /**
  102. * Create a panel for the sidebar
  103. * @param {String} title
  104. * @param {Object} attributes
  105. * @return {WMEUIHelperPanel}
  106. */
  107. createPanel (title, attributes = {}) {
  108. return new WMEUIHelperPanel(this.uid, this.generateId(), title, attributes)
  109. }
  110.  
  111. /**
  112. * Create a tab for the sidebar
  113. * @param {String} title
  114. * @param {Object} attributes
  115. * @return {WMEUIHelperTab}
  116. */
  117. createTab (title, attributes = {}) {
  118. return new WMEUIHelperTab(this.uid, this.generateId(), title, attributes)
  119. }
  120.  
  121. /**
  122. * Create a modal window
  123. * @param {String} title
  124. * @param {Object} attributes
  125. * @return {WMEUIHelperModal}
  126. */
  127. createModal (title, attributes = {}) {
  128. return new WMEUIHelperModal(this.uid, this.generateId(), title, attributes)
  129. }
  130.  
  131. /**
  132. * Create a field set
  133. * @param {String} title
  134. * @param {Object} attributes
  135. * @return {WMEUIHelperFieldset}
  136. */
  137. createFieldset (title, attributes = {}) {
  138. return new WMEUIHelperFieldset(this.uid, this.generateId(), title, attributes)
  139. }
  140. }
  141.  
  142. /**
  143. * Basic for all UI elements
  144. */
  145. class WMEUIHelperElement {
  146. constructor (uid, id, title = null, attributes = {}) {
  147. this.uid = uid
  148. this.id = id
  149. this.title = title
  150. // HTML attributes
  151. this.attributes = attributes
  152. // DOM element
  153. this.element = null
  154. // Children
  155. this.elements = []
  156. }
  157.  
  158. /**
  159. * Add WMEUIHelperElement to container
  160. * @param {WMEUIHelperElement} element
  161. * @return {WMEUIHelperElement} element
  162. */
  163. addElement (element) {
  164. this.elements.push(element)
  165. return element
  166. }
  167.  
  168. /**
  169. * @param {HTMLElement} element
  170. * @return {HTMLElement}
  171. */
  172. applyAttributes (element) {
  173. for (let attr in this.attributes) {
  174. if (this.attributes.hasOwnProperty(attr)) {
  175. element[attr] = this.attributes[attr]
  176. }
  177. }
  178. return element
  179. }
  180.  
  181. /**
  182. * @return {HTMLElement}
  183. */
  184. html () {
  185. if (!this.element) {
  186. this.element = this.toHTML()
  187. this.element.className += ' ' + this.uid + ' ' + this.uid + '-' + this.id
  188. }
  189. return this.element
  190. }
  191.  
  192. /**
  193. * Build and return HTML elements for injection
  194. * @return {HTMLElement}
  195. */
  196. toHTML () {
  197. throw new Error('Abstract method')
  198. }
  199. }
  200.  
  201. /**
  202. * Basic for all UI containers
  203. */
  204. class WMEUIHelperContainer extends WMEUIHelperElement {
  205. /**
  206. * Create and add button
  207. * For Tab Panel Modal Fieldset
  208. * @param {String} id
  209. * @param {String} title
  210. * @param {String} description
  211. * @param {Function} callback
  212. * @param {String} shortcut
  213. * @return {WMEUIHelperElement} element
  214. */
  215. addButton (id, title, description, callback, shortcut = null) {
  216. return this.addElement(new WMEUIHelperControlButton(this.uid, id, title, description, callback, shortcut))
  217. }
  218.  
  219. /**
  220. * Create buttons
  221. * @param {Object} buttons
  222. */
  223. addButtons (buttons) {
  224. for (let btn in buttons) {
  225. if (buttons.hasOwnProperty(btn)) {
  226. this.addButton(
  227. btn,
  228. buttons[btn].title,
  229. buttons[btn].description,
  230. buttons[btn].callback,
  231. buttons[btn].shortcut,
  232. )
  233. }
  234. }
  235. }
  236.  
  237. /**
  238. * Create checkbox
  239. * For Tab, Panel, Modal, or Fieldset
  240. * @param {String} id
  241. * @param {String} title
  242. * @param {Function} callback
  243. * @param {Boolean} checked
  244. * @return {WMEUIHelperElement} element
  245. */
  246. addCheckbox (id, title, callback, checked = false) {
  247. return this.addElement(
  248. new WMEUIHelperControlInput(this.uid, id, title, {
  249. 'id': this.uid + '-' + id,
  250. 'onclick': callback,
  251. 'type': 'checkbox',
  252. 'value': 1,
  253. 'checked': checked,
  254. })
  255. )
  256. }
  257.  
  258. /**
  259. * Create and add WMEUIHelperDiv element
  260. * @param {String} id
  261. * @param {String} innerHTML
  262. * @param {Object} attributes
  263. * @return {WMEUIHelperElement} element
  264. */
  265. addDiv (id, innerHTML = null, attributes = {}) {
  266. return this.addElement(new WMEUIHelperDiv(this.uid, id, innerHTML, attributes))
  267. }
  268.  
  269. /**
  270. * Create and add WMEUIHelperFieldset element
  271. * For Tab, Panel, Modal
  272. * @param {String} id
  273. * @param {String} title
  274. * @return {WMEUIHelperElement} element
  275. */
  276. addFieldset (id, title) {
  277. return this.addElement(new WMEUIHelperFieldset(this.uid, id, title))
  278. }
  279.  
  280. /**
  281. * Create text input
  282. * @param {String} id
  283. * @param {String} title
  284. * @param {Function} callback
  285. * @param {String} value
  286. * @return {WMEUIHelperElement} element
  287. */
  288. addInput (id, title, callback, value = '') {
  289. return this.addElement(
  290. new WMEUIHelperControlInput(this.uid, id, title, {
  291. 'id': this.uid + '-' + id,
  292. 'onchange': callback,
  293. 'type': 'text',
  294. 'value': value,
  295. })
  296. )
  297. }
  298.  
  299. /**
  300. * Create number input
  301. * @param {String} id
  302. * @param {String} title
  303. * @param {Function} callback
  304. * @param {Number} value
  305. * @param {Number} min
  306. * @param {Number} max
  307. * @param {Number} step
  308. * @return {WMEUIHelperElement} element
  309. */
  310. addNumber (id, title, callback, value = 0, min, max, step = 10) {
  311. return this.addElement(
  312. new WMEUIHelperControlInput(this.uid, id, title, {
  313. 'id': this.uid + '-' + id,
  314. 'onchange': callback,
  315. 'type': 'number',
  316. 'value': value,
  317. 'min': min,
  318. 'max': max,
  319. 'step': step,
  320. })
  321. )
  322. }
  323.  
  324. /**
  325. * Create radiobutton
  326. * @param {String} id
  327. * @param {String} title
  328. * @param {Function} callback
  329. * @param {String} name
  330. * @param {String} value
  331. * @param {Boolean} checked
  332. * @return {WMEUIHelperElement} element
  333. */
  334. addRadio (id, title, callback, name, value, checked = false) {
  335. return this.addElement(
  336. new WMEUIHelperControlInput(this.uid, id, title, {
  337. 'id': this.uid + '-' + id,
  338. 'name': name,
  339. 'onclick': callback,
  340. 'type': 'radio',
  341. 'value': value,
  342. 'checked': checked,
  343. })
  344. )
  345. }
  346.  
  347. /**
  348. * Create range input
  349. * @param {String} id
  350. * @param {String} title
  351. * @param {Function} callback
  352. * @param {Number} value
  353. * @param {Number} min
  354. * @param {Number} max
  355. * @param {Number} step
  356. * @return {WMEUIHelperElement} element
  357. */
  358. addRange (id, title, callback, value, min, max, step = 10) {
  359. return this.addElement(
  360. new WMEUIHelperControlInput(this.uid, id, title, {
  361. 'id': this.uid + '-' + id,
  362. 'onchange': callback,
  363. 'type': 'range',
  364. 'value': value,
  365. 'min': min,
  366. 'max': max,
  367. 'step': step,
  368. })
  369. )
  370. }
  371.  
  372. /**
  373. * Create and add WMEUIHelperText element
  374. * @param {String} id
  375. * @param {String} text
  376. * @return {WMEUIHelperElement} element
  377. */
  378. addText (id, text) {
  379. return this.addElement(new WMEUIHelperText(this.uid, id, text))
  380. }
  381. }
  382.  
  383. class WMEUIHelperFieldset extends WMEUIHelperContainer {
  384. toHTML () {
  385. // Fieldset legend
  386. let legend = document.createElement('legend')
  387. legend.innerHTML = unsafePolicy.createHTML(this.title)
  388.  
  389. // Container for buttons
  390. let controls = document.createElement('div')
  391. controls.className = 'controls'
  392. // Append buttons to container
  393. this.elements.forEach(element => controls.append(element.html()))
  394.  
  395. let fieldset = document.createElement('fieldset')
  396. fieldset = this.applyAttributes(fieldset)
  397. fieldset.append(legend)
  398. fieldset.append(controls)
  399. return fieldset
  400. }
  401. }
  402.  
  403. class WMEUIHelperPanel extends WMEUIHelperContainer {
  404. container () {
  405. return document.getElementById('edit-panel')
  406. }
  407.  
  408. inject () {
  409. this.container().append(this.html())
  410. }
  411.  
  412. toHTML () {
  413. // Label of the panel
  414. let label = document.createElement('label')
  415. label.className = 'control-label'
  416. label.innerHTML = unsafePolicy.createHTML(this.title)
  417. // Container for buttons
  418. let controls = document.createElement('div')
  419. controls.className = 'controls'
  420. // Append buttons to panel
  421. this.elements.forEach(element => controls.append(element.html()))
  422. // Build panel
  423. let group = document.createElement('div')
  424. group.className = 'form-group'
  425. group.append(label)
  426. group.append(controls)
  427. return group
  428. }
  429. }
  430.  
  431. class WMEUIHelperTab extends WMEUIHelperContainer {
  432. constructor (uid, id, title, attributes = {}) {
  433. super(uid, id, title, attributes)
  434. this.icon = attributes.icon
  435. }
  436.  
  437. async inject () {
  438. const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(this.uid)
  439.  
  440. tabLabel.innerText = this.title
  441. tabLabel.title = this.title
  442.  
  443. tabPane.append(this.html())
  444. }
  445.  
  446. toHTML () {
  447. // Label of the panel
  448. let header = document.createElement('div')
  449. header.className = 'panel-header-component settings-header'
  450. header.style.alignItems = 'center'
  451. header.style.display = 'flex'
  452. header.style.gap = '9px'
  453. header.style.justifyContent = 'stretch'
  454. header.style.padding = '8px'
  455. header.style.width = '100%'
  456.  
  457. if (this.icon) {
  458. let icon = document.createElement('i')
  459. icon.className = 'w-icon panel-header-component-icon w-icon-' + this.icon
  460. icon.style.fontSize = '24px'
  461. header.append(icon)
  462. }
  463.  
  464. let title = document.createElement('div')
  465. title.className = 'feature-id-container'
  466. title.innerHTML = unsafePolicy.createHTML(
  467. '<div class="feature-id-container"><wz-overline>' + this.title + '</wz-overline></div>'
  468. )
  469. header.append(title)
  470.  
  471. // Container for buttons
  472. let controls = document.createElement('div')
  473. controls.className = 'button-toolbar'
  474.  
  475. // Append buttons to container
  476. this.elements.forEach(element => controls.append(element.html()))
  477.  
  478. // Build form group
  479. let group = document.createElement('div')
  480. group.className = 'form-group'
  481. group.append(header)
  482. group.append(controls)
  483.  
  484. return group
  485. }
  486. }
  487.  
  488. class WMEUIHelperModal extends WMEUIHelperContainer {
  489. container () {
  490. return document.getElementById('panel-container')
  491. }
  492.  
  493. inject () {
  494. this.container().append(this.html())
  495. }
  496.  
  497. toHTML () {
  498. // Header and close button
  499. let close = document.createElement('a')
  500. close.className = 'close-panel'
  501. close.onclick = function () {
  502. panel.remove()
  503. }
  504.  
  505. let header = document.createElement('div')
  506. header.className = 'header'
  507. header.innerHTML = unsafePolicy.createHTML(this.title)
  508. header.prepend(close)
  509.  
  510. // Body
  511. let body = document.createElement('div')
  512. body.className = 'body'
  513.  
  514. // Append buttons to panel
  515. this.elements.forEach(element => body.append(element.html()))
  516.  
  517. // Container
  518. let archivePanel = document.createElement('div')
  519. archivePanel.className = 'archive-panel'
  520. archivePanel.append(header)
  521. archivePanel.append(body)
  522.  
  523. let panel = document.createElement('div')
  524. panel.className = 'panel panel--to-be-deprecated show'
  525. panel.append(archivePanel)
  526.  
  527. return panel
  528. }
  529. }
  530.  
  531. /**
  532. * Just div, can be with text
  533. */
  534. class WMEUIHelperDiv extends WMEUIHelperElement {
  535. toHTML () {
  536. let div = document.createElement('div')
  537. div = this.applyAttributes(div)
  538. div.id = this.uid + '-' + this.id
  539. if (this.title) {
  540. div.innerHTML = unsafePolicy.createHTML(this.title)
  541. }
  542. return div
  543. }
  544. }
  545.  
  546. /**
  547. * Just paragraph with text
  548. */
  549. class WMEUIHelperText extends WMEUIHelperElement {
  550. toHTML () {
  551. let p = document.createElement('p')
  552. p = this.applyAttributes(p)
  553. p.innerHTML = unsafePolicy.createHTML(this.title)
  554. return p
  555. }
  556. }
  557.  
  558. /**
  559. * Base class for controls
  560. */
  561. class WMEUIHelperControl extends WMEUIHelperElement {
  562. constructor (uid, id, title, attributes = {}) {
  563. super(uid, id, title, attributes)
  564. if (!attributes.name) {
  565. this.attributes.name = this.id
  566. }
  567. }
  568. }
  569.  
  570. /**
  571. * Input with label inside the div
  572. */
  573. class WMEUIHelperControlInput extends WMEUIHelperControl {
  574. toHTML () {
  575. let input = document.createElement('input')
  576. input = this.applyAttributes(input)
  577.  
  578. let label = document.createElement('label')
  579. label.htmlFor = input.id
  580. label.innerHTML = unsafePolicy.createHTML(this.title)
  581.  
  582. let container = document.createElement('div')
  583. container.className = 'controls-container'
  584. container.append(input)
  585. container.append(label)
  586. return container
  587. }
  588. }
  589.  
  590. /**
  591. * Button with shortcut if neeeded
  592. */
  593. class WMEUIHelperControlButton extends WMEUIHelperControl {
  594. constructor (uid, id, title, description, callback, shortcut = null) {
  595. super(uid, id, title)
  596. this.description = description
  597. this.callback = callback
  598. if (shortcut) {
  599. /* name, desc, group, title, shortcut, callback, scope */
  600. new WMEUIShortcut(
  601. this.uid + '-' + this.id,
  602. this.description,
  603. this.uid,
  604. title,
  605. shortcut,
  606. this.callback
  607. ).register()
  608. }
  609. }
  610.  
  611. toHTML () {
  612. let button = document.createElement('button')
  613. button.className = 'waze-btn waze-btn-small waze-btn-white'
  614. button.innerHTML = unsafePolicy.createHTML(this.title)
  615. button.title = this.description
  616. button.onclick = this.callback
  617. return button
  618. }
  619. }
  620.  
  621. /**
  622. * Based on the code from the WazeWrap library
  623. */
  624. class WMEUIShortcut {
  625. /**
  626. * @param {String} name
  627. * @param {String} desc
  628. * @param {String} group
  629. * @param {String} title
  630. * @param {String} shortcut
  631. * @param {Function} callback
  632. * @param {Object} scope
  633. * @return {WMEUIShortcut}
  634. */
  635. constructor (name, desc, group, title, shortcut, callback, scope = null) {
  636. this.name = name
  637. this.desc = desc
  638. this.group = WMEUI.normalize(group) || 'default'
  639. this.title = title
  640. this.shortcut = null
  641. this.callback = callback
  642. this.scope = ('object' === typeof scope) ? scope : null
  643.  
  644. /* Setup shortcut */
  645. if (shortcut && shortcut.length > 0) {
  646. this.shortcut = { [shortcut]: name }
  647. }
  648. }
  649.  
  650. /**
  651. * @param {String} group name
  652. * @param {String} title of the shortcut section
  653. */
  654. static setGroupTitle (group, title) {
  655. group = WMEUI.normalize(group)
  656.  
  657. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group]) {
  658. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group] = {
  659. description: title,
  660. members: {}
  661. }
  662. } else {
  663. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group].description = title
  664. }
  665. }
  666.  
  667. /**
  668. * Add translation for shortcut
  669. */
  670. addTranslation () {
  671. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group]) {
  672. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group] = {
  673. description: this.title,
  674. members: {
  675. [this.name]: this.desc
  676. }
  677. }
  678. }
  679. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group].members[this.name] = this.desc
  680. }
  681.  
  682. /**
  683. * Register group/action/event/shortcut
  684. */
  685. register () {
  686. /* Try to initialize new group */
  687. this.addGroup()
  688.  
  689. /* Clear existing actions with same name and create new */
  690. this.addAction()
  691.  
  692. /* Try to register new event */
  693. this.addEvent()
  694.  
  695. /* Finally, register the shortcut */
  696. this.registerShortcut()
  697. }
  698.  
  699. /**
  700. * Determines if the shortcut's action already exists.
  701. * @private
  702. */
  703. doesGroupExist () {
  704. return 'undefined' !== typeof W.accelerators.Groups[this.group]
  705. && 'undefined' !== typeof W.accelerators.Groups[this.group].members
  706. }
  707.  
  708. /**
  709. * Determines if the shortcut's action already exists.
  710. * @private
  711. */
  712. doesActionExist () {
  713. return 'undefined' !== typeof W.accelerators.Actions[this.name]
  714. }
  715.  
  716. /**
  717. * Determines if the shortcut's event already exists.
  718. * @private
  719. */
  720. doesEventExist () {
  721. return 'undefined' !== typeof W.accelerators.events.dispatcher._events[this.name]
  722. && W.accelerators.events.dispatcher._events[this.name].length > 0
  723. && this.callback === W.accelerators.events.dispatcher._events[this.name][0].func
  724. && this.scope === W.accelerators.events.dispatcher._events[this.name][0].obj
  725. }
  726.  
  727. /**
  728. * Creates the shortcut's group.
  729. * @private
  730. */
  731. addGroup () {
  732. if (this.doesGroupExist()) return
  733.  
  734. W.accelerators.Groups[this.group] = []
  735. W.accelerators.Groups[this.group].members = []
  736. }
  737.  
  738. /**
  739. * Registers the shortcut's action.
  740. * @private
  741. */
  742. addAction () {
  743. if (this.doesActionExist()) {
  744. W.accelerators.Actions[this.name] = null
  745. }
  746. W.accelerators.addAction(this.name, { group: this.group })
  747. }
  748.  
  749. /**
  750. * Registers the shortcut's event.
  751. * @private
  752. */
  753. addEvent () {
  754. if (this.doesEventExist()) return
  755. W.accelerators.events.register(this.name, this.scope, this.callback)
  756. }
  757.  
  758. /**
  759. * Registers the shortcut's keyboard shortcut.
  760. * @private
  761. */
  762. registerShortcut () {
  763. if (this.shortcut) {
  764. /* Setup translation for shortcut */
  765. this.addTranslation()
  766. W.accelerators._registerShortcuts(this.shortcut)
  767. }
  768. }
  769. }