WME UI

UI Library for Waze Map Editor Greasy Fork scripts

目前为 2023-01-06 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/450320/1135634/WME%20UI.js

  1. // ==UserScript==
  2. // @name WME UI
  3. // @version 0.2.1
  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 WMEUIHelperDiv element
  207. * @param {String} id
  208. * @param {String} innerHTML
  209. * @param {Object} attributes
  210. * @return {WMEUIHelperElement} element
  211. */
  212. addDiv (id, innerHTML = null, attributes = {}) {
  213. return this.addElement(new WMEUIHelperDiv(this.uid, id, innerHTML, attributes))
  214. }
  215.  
  216. /**
  217. * Create and add WMEUIHelperText element
  218. * @param {String} id
  219. * @param {String} text
  220. * @return {WMEUIHelperElement} element
  221. */
  222. addText (id, text) {
  223. return this.addElement(new WMEUIHelperText(this.uid, id, text))
  224. }
  225.  
  226. /**
  227. * Create and add WMEUIHelperFieldset element
  228. * For Tab, Panel, Modal
  229. * @param {String} id
  230. * @param {String} title
  231. * @param {String} description
  232. * @return {WMEUIHelperElement} element
  233. */
  234. addFieldset (id, title, description) {
  235. return this.addElement(new WMEUIHelperFieldset(this.uid, id, title, description))
  236. }
  237.  
  238. /**
  239. * Create text input
  240. * @param {String} id
  241. * @param {String} title
  242. * @param {Function} callback
  243. * @param {String} value
  244. * @return {WMEUIHelperElement} element
  245. */
  246. addInput (id, title, callback, value = '') {
  247. return this.addElement(
  248. new WMEUIHelperControlInput(this.uid, id, title, {
  249. 'id': this.uid + '-' + id,
  250. 'onchange': callback,
  251. 'type': 'text',
  252. 'value': value,
  253. })
  254. )
  255. }
  256.  
  257. /**
  258. * Create number input
  259. * @param {String} id
  260. * @param {String} title
  261. * @param {Function} callback
  262. * @param {Number} value
  263. * @param {Number} min
  264. * @param {Number} max
  265. * @param {Number} step
  266. * @return {WMEUIHelperElement} element
  267. */
  268. addNumber (id, title, callback, value = 0, min, max, step = 10) {
  269. return this.addElement(
  270. new WMEUIHelperControlInput(this.uid, id, title, {
  271. 'id': this.uid + '-' + id,
  272. 'onchange': callback,
  273. 'type': 'number',
  274. 'value': value,
  275. 'min': min,
  276. 'max': max,
  277. 'step': step,
  278. })
  279. )
  280. }
  281.  
  282. /**
  283. * Create checkbox
  284. * For Tab, Panel, Modal, or Fieldset
  285. * @param {String} id
  286. * @param {String} title
  287. * @param {Function} callback
  288. * @param {Boolean} checked
  289. * @return {WMEUIHelperElement} element
  290. */
  291. addCheckbox (id, title, callback, checked = false) {
  292. return this.addElement(
  293. new WMEUIHelperControlInput(this.uid, id, title, {
  294. 'id': this.uid + '-' + id,
  295. 'onclick': callback,
  296. 'type': 'checkbox',
  297. 'value': 1,
  298. 'checked': checked,
  299. })
  300. )
  301. }
  302.  
  303. /**
  304. * Create radiobutton
  305. * @param {String} id
  306. * @param {String} title
  307. * @param {Function} callback
  308. * @param {String} name
  309. * @param {String} value
  310. * @param {Boolean} checked
  311. * @return {WMEUIHelperElement} element
  312. */
  313. addRadio (id, title, callback, name, value, checked = false) {
  314. return this.addElement(
  315. new WMEUIHelperControlInput(this.uid, id, title, {
  316. 'id': this.uid + '-' + id,
  317. 'name': name,
  318. 'onclick': callback,
  319. 'type': 'radio',
  320. 'value': value,
  321. 'checked': checked,
  322. })
  323. )
  324. }
  325.  
  326. /**
  327. * Create range input
  328. * @param {String} id
  329. * @param {String} title
  330. * @param {Function} callback
  331. * @param {Number} value
  332. * @param {Number} min
  333. * @param {Number} max
  334. * @param {Number} step
  335. * @return {WMEUIHelperElement} element
  336. */
  337. addRange (id, title, callback, value, min, max, step = 10) {
  338. return this.addElement(
  339. new WMEUIHelperControlInput(this.uid, id, title, {
  340. 'id': this.uid + '-' + id,
  341. 'onchange': callback,
  342. 'type': 'range',
  343. 'value': value,
  344. 'min': min,
  345. 'max': max,
  346. 'step': step,
  347. })
  348. )
  349. }
  350.  
  351. /**
  352. * Create and add button
  353. * For Tab Panel Modal Fieldset
  354. * @param {String} id
  355. * @param {String} title
  356. * @param {String} description
  357. * @param {Function} callback
  358. * @param {String} shortcut
  359. * @return {WMEUIHelperElement} element
  360. */
  361. addButton (id, title, description, callback, shortcut = null) {
  362. return this.addElement(new WMEUIHelperControlButton(this.uid, id, title, description, callback, shortcut))
  363. }
  364.  
  365. /**
  366. * Create buttons
  367. * @param {Object} buttons
  368. */
  369. addButtons (buttons) {
  370. for (let btn in buttons) {
  371. if (buttons.hasOwnProperty(btn)) {
  372. this.addButton(
  373. btn,
  374. buttons[btn].title,
  375. buttons[btn].description,
  376. buttons[btn].callback,
  377. buttons[btn].shortcut,
  378. )
  379. }
  380. }
  381. }
  382. }
  383.  
  384. class WMEUIHelperFieldset extends WMEUIHelperContainer {
  385. toHTML () {
  386. // Fieldset legend
  387. let legend = document.createElement('legend')
  388. legend.innerHTML = unsafePolicy.createHTML(this.title)
  389.  
  390. // Container for buttons
  391. let controls = document.createElement('div')
  392. controls.className = 'controls'
  393. // Append buttons to container
  394. this.elements.forEach(element => controls.append(element.html()))
  395.  
  396. let fieldset = document.createElement('fieldset')
  397. fieldset = this.applyAttributes(fieldset)
  398. fieldset.append(legend)
  399. fieldset.append(controls)
  400. return fieldset
  401. }
  402. }
  403.  
  404. class WMEUIHelperPanel extends WMEUIHelperContainer {
  405. container () {
  406. return document.getElementById('edit-panel')
  407. }
  408. inject () {
  409. this.container().append(this.html())
  410. }
  411. toHTML () {
  412. // Label of the panel
  413. let label = document.createElement('label')
  414. label.className = 'control-label'
  415. label.innerHTML = unsafePolicy.createHTML(this.title)
  416. // Container for buttons
  417. let controls = document.createElement('div')
  418. controls.className = 'controls'
  419. // Append buttons to panel
  420. this.elements.forEach(element => controls.append(element.html()))
  421. // Build panel
  422. let group = document.createElement('div')
  423. group.className = 'form-group'
  424. group.append(label)
  425. group.append(controls)
  426. return group
  427. }
  428. }
  429.  
  430. class WMEUIHelperTab extends WMEUIHelperContainer {
  431. constructor (uid, id, title, attributes = {}) {
  432. super(uid, id, title, attributes)
  433. this.icon = attributes.icon
  434. }
  435.  
  436. async inject () {
  437. const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(this.uid)
  438.  
  439. tabLabel.innerText = this.title
  440. tabLabel.title = this.title
  441.  
  442. tabPane.append(this.html())
  443. }
  444.  
  445. toHTML () {
  446. // Label of the panel
  447. let header = document.createElement('div')
  448. header.className = 'panel-header-component settings-header'
  449. header.style.alignItems = 'center'
  450. header.style.display = 'flex'
  451. header.style.gap = '9px'
  452. header.style.justifyContent = 'stretch'
  453. header.style.padding = '8px'
  454. header.style.width = '100%'
  455.  
  456. if (this.icon) {
  457. let icon = document.createElement('i')
  458. icon.className = 'w-icon panel-header-component-icon w-icon-' + this.icon
  459. icon.style.fontSize = '24px'
  460. header.append(icon)
  461. }
  462.  
  463. let title = document.createElement('div')
  464. title.className = 'feature-id-container'
  465. title.innerHTML = unsafePolicy.createHTML(
  466. '<div class="feature-id-container"><wz-overline>' + this.title + '</wz-overline></div>'
  467. )
  468. header.append(title)
  469.  
  470. // Container for buttons
  471. let controls = document.createElement('div')
  472. controls.className = 'button-toolbar'
  473.  
  474. // Append buttons to container
  475. this.elements.forEach(element => controls.append(element.html()))
  476.  
  477. // Build form group
  478. let group = document.createElement('div')
  479. group.className = 'form-group'
  480. group.append(header)
  481. group.append(controls)
  482.  
  483. return group
  484. }
  485. }
  486.  
  487. class WMEUIHelperModal extends WMEUIHelperContainer {
  488. container () {
  489. return document.getElementById('panel-container')
  490. }
  491. inject () {
  492. this.container().append(this.html())
  493. }
  494. toHTML () {
  495. // Header and close button
  496. let close = document.createElement('a')
  497. close.className = 'close-panel'
  498. close.onclick = function () {
  499. panel.remove()
  500. }
  501.  
  502. let header = document.createElement('div')
  503. header.className = 'header'
  504. header.innerHTML = unsafePolicy.createHTML(this.title)
  505. header.prepend(close)
  506.  
  507. // Body
  508. let body = document.createElement('div')
  509. body.className = 'body'
  510.  
  511. // Append buttons to panel
  512. this.elements.forEach(element => body.append(element.html()))
  513.  
  514. // Container
  515. let archivePanel = document.createElement('div')
  516. archivePanel.className = 'archive-panel'
  517. archivePanel.append(header)
  518. archivePanel.append(body)
  519.  
  520. let panel = document.createElement('div')
  521. panel.className = 'panel panel--to-be-deprecated show'
  522. panel.append(archivePanel)
  523.  
  524. return panel
  525. }
  526. }
  527.  
  528. /**
  529. * Just div, can be with text
  530. */
  531. class WMEUIHelperDiv extends WMEUIHelperElement {
  532. toHTML () {
  533. let div = document.createElement('div')
  534. div = this.applyAttributes(div)
  535. div.id = this.uid + '-' + this.id
  536. if (this.title) {
  537. div.innerHTML = this.title
  538. }
  539. return div
  540. }
  541. }
  542.  
  543. /**
  544. * Just paragraph with text
  545. */
  546. class WMEUIHelperText extends WMEUIHelperElement {
  547. toHTML () {
  548. let p = document.createElement('p')
  549. p = this.applyAttributes(p)
  550. p.innerHTML = unsafePolicy.createHTML(this.title)
  551. return p
  552. }
  553. }
  554.  
  555. /**
  556. * Base class for controls
  557. */
  558. class WMEUIHelperControl extends WMEUIHelperElement {
  559. constructor (uid, id, title, attributes = {}) {
  560. super(uid, id, title, attributes)
  561. if (!attributes.name) {
  562. this.attributes.name = this.id
  563. }
  564. }
  565. }
  566.  
  567. /**
  568. * Input with label inside the div
  569. */
  570. class WMEUIHelperControlInput extends WMEUIHelperControl {
  571. toHTML () {
  572. let input = document.createElement('input')
  573. input = this.applyAttributes(input)
  574.  
  575. let label = document.createElement('label')
  576. label.htmlFor = input.id
  577. label.innerHTML = unsafePolicy.createHTML(this.title)
  578.  
  579. let container = document.createElement('div')
  580. container.className = 'controls-container'
  581. container.append(input)
  582. container.append(label)
  583. return container
  584. }
  585. }
  586.  
  587. /**
  588. * Button with shortcut if neeeded
  589. */
  590. class WMEUIHelperControlButton extends WMEUIHelperControl {
  591. constructor (uid, id, title, description, callback, shortcut = null) {
  592. super(uid, id, title)
  593. this.description = description
  594. this.callback = callback
  595. if (shortcut) {
  596. /* name, desc, group, title, shortcut, callback, scope */
  597. new WMEUIShortcut(
  598. this.uid + '-' + this.id,
  599. this.description,
  600. this.uid,
  601. title,
  602. shortcut,
  603. this.callback
  604. ).register()
  605. }
  606. }
  607.  
  608. toHTML () {
  609. let button = document.createElement('button')
  610. button.className = 'waze-btn waze-btn-small waze-btn-white'
  611. button.innerHTML = unsafePolicy.createHTML(this.title)
  612. button.title = this.description
  613. button.onclick = this.callback
  614. return button
  615. }
  616. }
  617.  
  618. /**
  619. * Based on the code from the WazeWrap library
  620. */
  621. class WMEUIShortcut {
  622. /**
  623. * @param {String} name
  624. * @param {String} desc
  625. * @param {String} group
  626. * @param {String} title
  627. * @param {String} shortcut
  628. * @param {Function} callback
  629. * @param {Object} scope
  630. * @return {WMEUIShortcut}
  631. */
  632. constructor (name, desc, group, title, shortcut, callback, scope = null) {
  633. this.name = name
  634. this.desc = desc
  635. this.group = WMEUI.normalize(group) || 'default'
  636. this.title = title
  637. this.shortcut = null
  638. this.callback = callback
  639. this.scope = ('object' === typeof scope) ? scope : null
  640.  
  641. /* Setup shortcut */
  642. if (shortcut && shortcut.length > 0) {
  643. this.shortcut = { [shortcut]: name }
  644. }
  645. }
  646.  
  647. /**
  648. * @param {String} group name
  649. * @param {String} title of the shortcut section
  650. */
  651. static setGroupTitle (group, title) {
  652. group = WMEUI.normalize(group)
  653.  
  654. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group]) {
  655. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group] = {
  656. description: title,
  657. members: {}
  658. }
  659. } else {
  660. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group].description = title
  661. }
  662. }
  663.  
  664. /**
  665. * Add translation for shortcut
  666. */
  667. addTranslation () {
  668. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group]) {
  669. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group] = {
  670. description: this.title,
  671. members: {
  672. [this.name]: this.desc
  673. }
  674. }
  675. }
  676. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group].members[this.name] = this.desc
  677. }
  678.  
  679. /**
  680. * Register group/action/event/shortcut
  681. */
  682. register () {
  683. /* Try to initialize new group */
  684. this.addGroup()
  685.  
  686. /* Clear existing actions with same name and create new */
  687. this.addAction()
  688.  
  689. /* Try to register new event */
  690. this.addEvent()
  691.  
  692. /* Finally, register the shortcut */
  693. this.registerShortcut()
  694. }
  695.  
  696. /**
  697. * Determines if the shortcut's action already exists.
  698. * @private
  699. */
  700. doesGroupExist () {
  701. return 'undefined' !== typeof W.accelerators.Groups[this.group]
  702. && 'undefined' !== typeof W.accelerators.Groups[this.group].members
  703. }
  704.  
  705. /**
  706. * Determines if the shortcut's action already exists.
  707. * @private
  708. */
  709. doesActionExist () {
  710. return 'undefined' !== typeof W.accelerators.Actions[this.name]
  711. }
  712.  
  713. /**
  714. * Determines if the shortcut's event already exists.
  715. * @private
  716. */
  717. doesEventExist () {
  718. return 'undefined' !== typeof W.accelerators.events.dispatcher._events[this.name]
  719. && W.accelerators.events.dispatcher._events[this.name].length > 0
  720. && this.callback === W.accelerators.events.dispatcher._events[this.name][0].func
  721. && this.scope === W.accelerators.events.dispatcher._events[this.name][0].obj
  722. }
  723.  
  724. /**
  725. * Creates the shortcut's group.
  726. * @private
  727. */
  728. addGroup () {
  729. if (this.doesGroupExist()) return
  730.  
  731. W.accelerators.Groups[this.group] = []
  732. W.accelerators.Groups[this.group].members = []
  733. }
  734.  
  735. /**
  736. * Registers the shortcut's action.
  737. * @private
  738. */
  739. addAction () {
  740. if (this.doesActionExist()) {
  741. W.accelerators.Actions[this.name] = null
  742. }
  743. W.accelerators.addAction(this.name, { group: this.group })
  744. }
  745.  
  746. /**
  747. * Registers the shortcut's event.
  748. * @private
  749. */
  750. addEvent () {
  751. if (this.doesEventExist()) return
  752. W.accelerators.events.register(this.name, this.scope, this.callback)
  753. }
  754.  
  755. /**
  756. * Registers the shortcut's keyboard shortcut.
  757. * @private
  758. */
  759. registerShortcut () {
  760. if (this.shortcut) {
  761. /* Setup translation for shortcut */
  762. this.addTranslation()
  763. W.accelerators._registerShortcuts(this.shortcut)
  764. }
  765. }
  766. }