WME UI

UI Library for Waze Map Editor Greasy Fork scripts

当前为 2023-11-17 提交的版本,查看 最新版本

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

  1. // ==UserScript==
  2. // @name WME UI
  3. // @version 0.2.4
  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. this.image = attributes.image
  436. }
  437.  
  438. async inject () {
  439. const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(this.uid)
  440.  
  441. tabLabel.innerText = this.title
  442. tabLabel.title = this.title
  443.  
  444. tabPane.append(this.html())
  445. }
  446.  
  447. toHTML () {
  448. // Label of the panel
  449. let header = document.createElement('div')
  450. header.className = 'panel-header-component settings-header'
  451. header.style.alignItems = 'center'
  452. header.style.display = 'flex'
  453. header.style.gap = '9px'
  454. header.style.justifyContent = 'stretch'
  455. header.style.padding = '8px'
  456. header.style.width = '100%'
  457.  
  458. if (this.icon) {
  459. let icon = document.createElement('i')
  460. icon.className = 'w-icon panel-header-component-icon w-icon-' + this.icon
  461. icon.style.fontSize = '24px'
  462. header.append(icon)
  463. }
  464.  
  465. if (this.image) {
  466. let img = document.createElement('img')
  467. img.style.height = '42px'
  468. img.src = this.image
  469. header.append(img)
  470. }
  471.  
  472. let title = document.createElement('div')
  473. title.className = 'feature-id-container'
  474. title.innerHTML = unsafePolicy.createHTML(
  475. '<div class="feature-id-container"><wz-overline>' + this.title + '</wz-overline></div>'
  476. )
  477. header.append(title)
  478.  
  479. // Container for buttons
  480. let controls = document.createElement('div')
  481. controls.className = 'button-toolbar'
  482.  
  483. // Append buttons to container
  484. this.elements.forEach(element => controls.append(element.html()))
  485.  
  486. // Build form group
  487. let group = document.createElement('div')
  488. group.className = 'form-group'
  489. group.append(header)
  490. group.append(controls)
  491.  
  492. return group
  493. }
  494. }
  495.  
  496. class WMEUIHelperModal extends WMEUIHelperContainer {
  497. container () {
  498. return document.getElementById('panel-container')
  499. }
  500.  
  501. inject () {
  502. this.container().append(this.html())
  503. }
  504.  
  505. toHTML () {
  506. // Header and close button
  507. let close = document.createElement('a')
  508. close.className = 'close-panel'
  509. close.onclick = function () {
  510. panel.remove()
  511. }
  512.  
  513. let title = document.createElement('h5')
  514. title.innerHTML = unsafePolicy.createHTML(this.title)
  515.  
  516. let header = document.createElement('div')
  517. header.className = 'header'
  518. header.prepend(title)
  519. header.prepend(close)
  520.  
  521. // Body
  522. let body = document.createElement('div')
  523. body.className = 'body'
  524.  
  525. // Append buttons to panel
  526. this.elements.forEach(element => body.append(element.html()))
  527.  
  528. // Container
  529. let archivePanel = document.createElement('div')
  530. archivePanel.className = 'archive-panel'
  531. archivePanel.append(header)
  532. archivePanel.append(body)
  533.  
  534. let panel = document.createElement('div')
  535. panel.className = 'panel panel--to-be-deprecated show'
  536. panel.append(archivePanel)
  537.  
  538. return panel
  539. }
  540. }
  541.  
  542. /**
  543. * Just div, can be with text
  544. */
  545. class WMEUIHelperDiv extends WMEUIHelperElement {
  546. toHTML () {
  547. let div = document.createElement('div')
  548. div = this.applyAttributes(div)
  549. div.id = this.uid + '-' + this.id
  550. if (this.title) {
  551. div.innerHTML = unsafePolicy.createHTML(this.title)
  552. }
  553. return div
  554. }
  555. }
  556.  
  557. /**
  558. * Just paragraph with text
  559. */
  560. class WMEUIHelperText extends WMEUIHelperElement {
  561. toHTML () {
  562. let p = document.createElement('p')
  563. p = this.applyAttributes(p)
  564. p.innerHTML = unsafePolicy.createHTML(this.title)
  565. return p
  566. }
  567. }
  568.  
  569. /**
  570. * Base class for controls
  571. */
  572. class WMEUIHelperControl extends WMEUIHelperElement {
  573. constructor (uid, id, title, attributes = {}) {
  574. super(uid, id, title, attributes)
  575. if (!attributes.name) {
  576. this.attributes.name = this.id
  577. }
  578. }
  579. }
  580.  
  581. /**
  582. * Input with label inside the div
  583. */
  584. class WMEUIHelperControlInput extends WMEUIHelperControl {
  585. toHTML () {
  586. let input = document.createElement('input')
  587. input = this.applyAttributes(input)
  588.  
  589. let label = document.createElement('label')
  590. label.htmlFor = input.id
  591. label.innerHTML = unsafePolicy.createHTML(this.title)
  592.  
  593. let container = document.createElement('div')
  594. container.className = 'controls-container'
  595. container.append(input)
  596. container.append(label)
  597. return container
  598. }
  599. }
  600.  
  601. /**
  602. * Button with shortcut if neeeded
  603. */
  604. class WMEUIHelperControlButton extends WMEUIHelperControl {
  605. constructor (uid, id, title, description, callback, shortcut = null) {
  606. super(uid, id, title)
  607. this.description = description
  608. this.callback = callback
  609. if (shortcut) {
  610. /* name, desc, group, title, shortcut, callback, scope */
  611. new WMEUIShortcut(
  612. this.uid + '-' + this.id,
  613. this.description,
  614. this.uid,
  615. title,
  616. shortcut,
  617. this.callback
  618. ).register()
  619. }
  620. }
  621.  
  622. toHTML () {
  623. let button = document.createElement('button')
  624. button.className = 'waze-btn waze-btn-small waze-btn-white'
  625. button.innerHTML = unsafePolicy.createHTML(this.title)
  626. button.title = this.description
  627. button.onclick = this.callback
  628. return button
  629. }
  630. }
  631.  
  632. /**
  633. * Based on the code from the WazeWrap library
  634. */
  635. class WMEUIShortcut {
  636. /**
  637. * @param {String} name
  638. * @param {String} desc
  639. * @param {String} group
  640. * @param {String} title
  641. * @param {String} shortcut
  642. * @param {Function} callback
  643. * @param {Object} scope
  644. * @return {WMEUIShortcut}
  645. */
  646. constructor (name, desc, group, title, shortcut, callback, scope = null) {
  647. this.name = name
  648. this.desc = desc
  649. this.group = WMEUI.normalize(group) || 'default'
  650. this.title = title
  651. this.shortcut = null
  652. this.callback = callback
  653. this.scope = ('object' === typeof scope) ? scope : null
  654.  
  655. /* Setup shortcut */
  656. if (shortcut && shortcut.length > 0) {
  657. this.shortcut = { [shortcut]: name }
  658. }
  659. }
  660.  
  661. /**
  662. * @param {String} group name
  663. * @param {String} title of the shortcut section
  664. */
  665. static setGroupTitle (group, title) {
  666. group = WMEUI.normalize(group)
  667.  
  668. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group]) {
  669. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group] = {
  670. description: title,
  671. members: {}
  672. }
  673. } else {
  674. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group].description = title
  675. }
  676. }
  677.  
  678. /**
  679. * Add translation for shortcut
  680. */
  681. addTranslation () {
  682. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group]) {
  683. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group] = {
  684. description: this.title,
  685. members: {
  686. [this.name]: this.desc
  687. }
  688. }
  689. }
  690. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group].members[this.name] = this.desc
  691. }
  692.  
  693. /**
  694. * Register group/action/event/shortcut
  695. */
  696. register () {
  697. /* Try to initialize new group */
  698. this.addGroup()
  699.  
  700. /* Clear existing actions with same name and create new */
  701. this.addAction()
  702.  
  703. /* Try to register new event */
  704. this.addEvent()
  705.  
  706. /* Finally, register the shortcut */
  707. this.registerShortcut()
  708. }
  709.  
  710. /**
  711. * Determines if the shortcut's action already exists.
  712. * @private
  713. */
  714. doesGroupExist () {
  715. return 'undefined' !== typeof W.accelerators.Groups[this.group]
  716. && 'undefined' !== typeof W.accelerators.Groups[this.group].members
  717. }
  718.  
  719. /**
  720. * Determines if the shortcut's action already exists.
  721. * @private
  722. */
  723. doesActionExist () {
  724. return 'undefined' !== typeof W.accelerators.Actions[this.name]
  725. }
  726.  
  727. /**
  728. * Determines if the shortcut's event already exists.
  729. * @private
  730. */
  731. doesEventExist () {
  732. return 'undefined' !== typeof W.accelerators.events.dispatcher._events[this.name]
  733. && W.accelerators.events.dispatcher._events[this.name].length > 0
  734. && this.callback === W.accelerators.events.dispatcher._events[this.name][0].func
  735. && this.scope === W.accelerators.events.dispatcher._events[this.name][0].obj
  736. }
  737.  
  738. /**
  739. * Creates the shortcut's group.
  740. * @private
  741. */
  742. addGroup () {
  743. if (this.doesGroupExist()) return
  744.  
  745. W.accelerators.Groups[this.group] = []
  746. W.accelerators.Groups[this.group].members = []
  747. }
  748.  
  749. /**
  750. * Registers the shortcut's action.
  751. * @private
  752. */
  753. addAction () {
  754. if (this.doesActionExist()) {
  755. W.accelerators.Actions[this.name] = null
  756. }
  757. W.accelerators.addAction(this.name, { group: this.group })
  758. }
  759.  
  760. /**
  761. * Registers the shortcut's event.
  762. * @private
  763. */
  764. addEvent () {
  765. if (this.doesEventExist()) return
  766. W.accelerators.events.register(this.name, this.scope, this.callback)
  767. }
  768.  
  769. /**
  770. * Registers the shortcut's keyboard shortcut.
  771. * @private
  772. */
  773. registerShortcut () {
  774. if (this.shortcut) {
  775. /* Setup translation for shortcut */
  776. this.addTranslation()
  777. W.accelerators._registerShortcuts(this.shortcut)
  778. }
  779. }
  780. }