WME-UI

UI Library for Waze Map Editor Greasy Fork scripts

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

  1. // ==UserScript==
  2. // @name WME UI
  3. // @version 0.2.7
  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 the 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 the 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 a 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('tippy-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('button')
  508. close.className = 'wme-ui-close-panel'
  509. close.style.background = '#fff'
  510. close.style.border = '1px solid #ececec'
  511. close.style.borderRadius = '100%'
  512. close.style.cursor = 'pointer'
  513. close.style.fontSize = '20px'
  514. close.style.height = '20px'
  515. close.style.lineHeight = '16px'
  516. close.style.position = 'absolute'
  517. close.style.right = '14px'
  518. close.style.textIndent = '-2px'
  519. close.style.top = '14px'
  520. close.style.transition = 'all 150ms'
  521. close.style.width = '20px'
  522. close.style.zIndex = '99'
  523. close.innerText = '×'
  524. close.onclick = function () {
  525. panel.remove()
  526. }
  527.  
  528. let title = document.createElement('h5')
  529. title.style.padding = '16px 16px 0'
  530. title.innerHTML = unsafePolicy.createHTML(this.title)
  531.  
  532. let header = document.createElement('div')
  533. header.className = 'wme-ui-header'
  534. header.style.position = 'relative'
  535. header.prepend(title)
  536. header.prepend(close)
  537.  
  538. // Body
  539. let body = document.createElement('div')
  540. body.className = 'wme-ui-body'
  541.  
  542. // Append buttons to panel
  543. this.elements.forEach(element => body.append(element.html()))
  544.  
  545. // Container
  546. let container = document.createElement('div')
  547. container.className = 'wme-ui-panel-container'
  548. container.append(header)
  549. container.append(body)
  550.  
  551. // Panel
  552. let panel = document.createElement('div')
  553. panel.style.width = '320px'
  554. panel.style.background = '#fff'
  555. panel.style.margin = '15px'
  556. panel.style.borderRadius = '5px'
  557. panel.className = 'wme-ui-panel'
  558. panel.append(container)
  559.  
  560. return panel
  561. }
  562. }
  563.  
  564. /**
  565. * Just div, can be with text
  566. */
  567. class WMEUIHelperDiv extends WMEUIHelperElement {
  568. toHTML () {
  569. let div = document.createElement('div')
  570. div = this.applyAttributes(div)
  571. div.id = this.uid + '-' + this.id
  572. if (this.title) {
  573. div.innerHTML = unsafePolicy.createHTML(this.title)
  574. }
  575. return div
  576. }
  577. }
  578.  
  579. /**
  580. * Just a paragraph with text
  581. */
  582. class WMEUIHelperText extends WMEUIHelperElement {
  583. toHTML () {
  584. let p = document.createElement('p')
  585. p = this.applyAttributes(p)
  586. p.innerHTML = unsafePolicy.createHTML(this.title)
  587. return p
  588. }
  589. }
  590.  
  591. /**
  592. * Base class for controls
  593. */
  594. class WMEUIHelperControl extends WMEUIHelperElement {
  595. constructor (uid, id, title, attributes = {}) {
  596. super(uid, id, title, attributes)
  597. if (!attributes.name) {
  598. this.attributes.name = this.id
  599. }
  600. }
  601. }
  602.  
  603. /**
  604. * Input with label inside the div
  605. */
  606. class WMEUIHelperControlInput extends WMEUIHelperControl {
  607. toHTML () {
  608. let input = document.createElement('input')
  609. input = this.applyAttributes(input)
  610.  
  611. let label = document.createElement('label')
  612. label.htmlFor = input.id
  613. label.innerHTML = unsafePolicy.createHTML(this.title)
  614.  
  615. let container = document.createElement('div')
  616. container.className = 'controls-container'
  617. container.append(input)
  618. container.append(label)
  619. return container
  620. }
  621. }
  622.  
  623. /**
  624. * Button with shortcut if needed
  625. */
  626. class WMEUIHelperControlButton extends WMEUIHelperControl {
  627. constructor (uid, id, title, description, callback, shortcut = null) {
  628. super(uid, id, title)
  629. this.description = description
  630. this.callback = callback
  631. if (shortcut) {
  632. /* name, desc, group, title, shortcut, callback, scope */
  633. new WMEUIShortcut(
  634. this.uid + '-' + this.id,
  635. this.description,
  636. this.uid,
  637. title,
  638. shortcut,
  639. this.callback
  640. ).register()
  641. }
  642. }
  643.  
  644. toHTML () {
  645. let button = document.createElement('button')
  646. button.className = 'waze-btn waze-btn-small waze-btn-white'
  647. button.innerHTML = unsafePolicy.createHTML(this.title)
  648. button.title = this.description
  649. button.onclick = this.callback
  650. return button
  651. }
  652. }
  653.  
  654. /**
  655. * Based on the code from the WazeWrap library
  656. */
  657. class WMEUIShortcut {
  658. /**
  659. * @param {String} name
  660. * @param {String} desc
  661. * @param {String} group
  662. * @param {String} title
  663. * @param {String} shortcut
  664. * @param {Function} callback
  665. * @param {Object} scope
  666. * @return {WMEUIShortcut}
  667. */
  668. constructor (name, desc, group, title, shortcut, callback, scope = null) {
  669. this.name = name
  670. this.desc = desc
  671. this.group = WMEUI.normalize(group) || 'default'
  672. this.title = title
  673. this.shortcut = null
  674. this.callback = callback
  675. this.scope = ('object' === typeof scope) ? scope : null
  676.  
  677. /* Setup shortcut */
  678. if (shortcut && shortcut.length > 0) {
  679. this.shortcut = { [shortcut]: name }
  680. }
  681. }
  682.  
  683. /**
  684. * @param {String} group name
  685. * @param {String} title of the shortcut section
  686. */
  687. static setGroupTitle (group, title) {
  688. group = WMEUI.normalize(group)
  689.  
  690. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group]) {
  691. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group] = {
  692. description: title,
  693. members: {}
  694. }
  695. } else {
  696. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group].description = title
  697. }
  698. }
  699.  
  700. /**
  701. * Add translation for shortcut
  702. */
  703. addTranslation () {
  704. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group]) {
  705. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group] = {
  706. description: this.title,
  707. members: {
  708. [this.name]: this.desc
  709. }
  710. }
  711. }
  712. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group].members[this.name] = this.desc
  713. }
  714.  
  715. /**
  716. * Register group/action/event/shortcut
  717. */
  718. register () {
  719. /* Try to initialize a new group */
  720. this.addGroup()
  721.  
  722. /* Clear existing actions with the same name and create new */
  723. this.addAction()
  724.  
  725. /* Try to register new event */
  726. this.addEvent()
  727.  
  728. /* Finally, register the shortcut */
  729. this.registerShortcut()
  730. }
  731.  
  732. /**
  733. * Determines if the shortcut's action already exists.
  734. * @private
  735. */
  736. doesGroupExist () {
  737. return 'undefined' !== typeof W.accelerators.Groups[this.group]
  738. && 'undefined' !== typeof W.accelerators.Groups[this.group].members
  739. }
  740.  
  741. /**
  742. * Determines if the shortcut's action already exists.
  743. * @private
  744. */
  745. doesActionExist () {
  746. return 'undefined' !== typeof W.accelerators.Actions[this.name]
  747. }
  748.  
  749. /**
  750. * Determines if the shortcut's event already exists.
  751. * @private
  752. */
  753. doesEventExist () {
  754. return 'undefined' !== typeof W.accelerators.events.dispatcher._events[this.name]
  755. && W.accelerators.events.dispatcher._events[this.name].length > 0
  756. && this.callback === W.accelerators.events.dispatcher._events[this.name][0].func
  757. && this.scope === W.accelerators.events.dispatcher._events[this.name][0].obj
  758. }
  759.  
  760. /**
  761. * Creates the shortcut's group.
  762. * @private
  763. */
  764. addGroup () {
  765. if (this.doesGroupExist()) return
  766.  
  767. W.accelerators.Groups[this.group] = []
  768. W.accelerators.Groups[this.group].members = []
  769. }
  770.  
  771. /**
  772. * Registers the shortcut's action.
  773. * @private
  774. */
  775. addAction () {
  776. if (this.doesActionExist()) {
  777. W.accelerators.Actions[this.name] = null
  778. }
  779. W.accelerators.addAction(this.name, { group: this.group })
  780. }
  781.  
  782. /**
  783. * Registers the shortcut's event.
  784. * @private
  785. */
  786. addEvent () {
  787. if (this.doesEventExist()) return
  788. W.accelerators.events.register(this.name, this.scope, this.callback)
  789. }
  790.  
  791. /**
  792. * Registers the shortcut's keyboard shortcut.
  793. * @private
  794. */
  795. registerShortcut () {
  796. if (this.shortcut) {
  797. /* Setup translation for shortcut */
  798. this.addTranslation()
  799. W.accelerators._registerShortcuts(this.shortcut)
  800. }
  801. }
  802. }