WME UI

UI Library for Waze Map Editor Greasy Fork scripts

目前为 2022-12-12 提交的版本。查看 最新版本

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

  1. // ==UserScript==
  2. // @name WME UI
  3. // @version 0.0.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 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 {String} description
  105. * @param {Object} attributes
  106. * @return {WMEUIHelperPanel}
  107. */
  108. createPanel (title, description = null, attributes = {}) {
  109. return new WMEUIHelperPanel(this.uid, this.generateId(), title, description, attributes)
  110. }
  111.  
  112. /**
  113. * Create a tab for the sidebar
  114. * @param {String} title
  115. * @param {String} description
  116. * @param {Object} attributes
  117. * @return {WMEUIHelperTab}
  118. */
  119. createTab (title, description = null, attributes = {}) {
  120. return new WMEUIHelperTab(this.uid, this.generateId(), title, description, attributes)
  121. }
  122.  
  123. /**
  124. * Create a modal window
  125. * @param {String} title
  126. * @param {String} description
  127. * @param {Object} attributes
  128. * @return {WMEUIHelperModal}
  129. */
  130. createModal (title, description = null) {
  131. return new WMEUIHelperModal(this.uid, this.generateId(), title, description)
  132. }
  133.  
  134. /**
  135. * Create a field set
  136. * @param {String} title
  137. * @param {String} description
  138. * @param {Object} attributes
  139. * @return {WMEUIHelperFieldset}
  140. */
  141. createFieldset (title, description = null) {
  142. return new WMEUIHelperFieldset(this.uid, this.generateId(), title, description)
  143. }
  144. }
  145.  
  146. /**
  147. * Basic for all UI elements
  148. */
  149. class WMEUIHelperElement {
  150. constructor (uid, id, title, description = null, attributes = {}) {
  151. this.uid = uid
  152. this.id = id
  153. this.title = title
  154. this.description = description
  155. this.attributes = attributes
  156. this.domElement = null
  157. }
  158.  
  159. /**
  160. * @param {HTMLElement} element
  161. * @return {HTMLElement}
  162. */
  163. applyAttributes (element) {
  164. for (let attr in this.attributes) {
  165. if (this.attributes.hasOwnProperty(attr)) {
  166. element[attr] = this.attributes[attr]
  167. }
  168. }
  169. return element
  170. }
  171.  
  172. /**
  173. * @return {HTMLElement}
  174. */
  175. html () {
  176. if (!this.domElement) {
  177. this.domElement = this.toHTML()
  178. this.domElement.className += ' ' + this.uid + ' ' + this.uid + '-' + this.id
  179. }
  180. return this.domElement
  181. }
  182.  
  183. /**
  184. * @return {HTMLElement}
  185. */
  186. toHTML () {
  187. throw new Error('Abstract method')
  188. }
  189. }
  190.  
  191. /**
  192. * Basic for all UI containers
  193. */
  194. class WMEUIHelperContainer extends WMEUIHelperElement {
  195. constructor (uid, id, title, description = null, attributes = {}) {
  196. super(uid, id, title, description, attributes)
  197. this.elements = []
  198. if (description) {
  199. this.addText('description', description)
  200. }
  201. }
  202.  
  203. /**
  204. * Add WMEUIHelperElement to container
  205. * @param {WMEUIHelperElement} element
  206. */
  207. addElement (element) {
  208. this.elements.push(element)
  209. }
  210.  
  211. /**
  212. * Create and add WMEUIHelperText element
  213. * For Tab, Panel, Modal, or Fieldset
  214. * @param {String} id
  215. * @param {String} text
  216. */
  217. addText (id, text) {
  218. return this.addElement(new WMEUIHelperText(this.uid, id, text))
  219. }
  220.  
  221. /**
  222. * Create and add WMEUIHelperFieldset element
  223. * For Tab, Panel, Modal
  224. * @param {String} id
  225. * @param {String} title
  226. * @param {String} description
  227. */
  228. addFieldset (id, title, description) {
  229. return this.addElement(new WMEUIHelperFieldset(this.uid, id, title, description))
  230. }
  231.  
  232. /**
  233. * Create text input
  234. * For Tab, Panel, Modal, or Fieldset
  235. * @param {String} id
  236. * @param {String} title
  237. * @param {String} description
  238. * @param {Function} callback
  239. * @param {String} value
  240. */
  241. addInput (id, title, description, callback, value = '') {
  242. return this.addElement(
  243. new WMEUIHelperControlInput(this.uid, id, title, description, {
  244. 'id': this.uid + '-' + id,
  245. 'onchange': callback,
  246. 'type': 'text',
  247. 'value': value,
  248. })
  249. )
  250. }
  251.  
  252. /**
  253. * Create number input
  254. * For Tab, Panel, Modal, or Fieldset
  255. * @param {String} id
  256. * @param {String} title
  257. * @param {String} description
  258. * @param {Function} callback
  259. * @param {String} value
  260. */
  261. addNumber (id, title, description, callback, value = '') {
  262. return this.addElement(
  263. new WMEUIHelperControlInput(this.uid, id, title, description, {
  264. 'id': this.uid + '-' + id,
  265. 'onchange': callback,
  266. 'type': 'number',
  267. 'value': value,
  268. })
  269. )
  270. }
  271.  
  272. /**
  273. * Create checkbox
  274. * For Tab, Panel, Modal, or Fieldset
  275. * @param {String} id
  276. * @param {String} title
  277. * @param {String} description
  278. * @param {Function} callback
  279. * @param {Bool} checked
  280. */
  281. addCheckbox (id, title, description, callback, checked = false) {
  282. return this.addElement(
  283. new WMEUIHelperControlInput(this.uid, id, title, description, {
  284. 'id': this.uid + '-' + id,
  285. 'onclick': callback,
  286. 'type': 'checkbox',
  287. 'value': 1,
  288. 'checked': checked,
  289. })
  290. )
  291. }
  292.  
  293. /**
  294. * Create radiobutton
  295. * @param {String} id
  296. * @param {String} title
  297. * @param {String} description
  298. * @param {Function} callback
  299. * @param {String} value
  300. * @param {Bool} checked
  301. */
  302. addRadio (id, title, description, callback, value, checked = false) {
  303. return this.addElement(
  304. new WMEUIHelperControlInput(this.uid, id, title, description, {
  305. 'id': this.uid + '-' + id + '-' + value,
  306. 'onclick': callback,
  307. 'type': 'radio',
  308. 'value': value,
  309. 'checked': checked,
  310. })
  311. )
  312. }
  313.  
  314. /**
  315. * Create range input
  316. * @param {String} id
  317. * @param {String} title
  318. * @param {String} description
  319. * @param {Function} callback
  320. * @param {Integer} min
  321. * @param {Integer} max
  322. * @param {Integer} value
  323. * @param {Integer} step
  324. */
  325. addRange (id, title, description, callback, min, max, value, step = 10) {
  326. return this.addElement(
  327. new WMEUIHelperControlInput(this.uid, id, title, description, {
  328. 'id': this.uid + '-' + id,
  329. 'onchange': callback,
  330. 'type': 'range',
  331. 'min': min,
  332. 'max': max,
  333. 'value': value,
  334. 'step': step,
  335. })
  336. )
  337. }
  338.  
  339. /**
  340. * Create and add button
  341. * For Tab Panel Modal Fieldset
  342. * @param {String} id
  343. * @param {String} title
  344. * @param {String} description
  345. * @param {Function} callback
  346. * @param {String} shortcut
  347. */
  348. addButton (id, title, description, callback, shortcut = null) {
  349. return this.addElement(new WMEUIHelperControlButton(this.uid, id, title, description, callback, shortcut))
  350. }
  351.  
  352. /**
  353. * Create buttons
  354. * @param {Object} buttons
  355. */
  356. addButtons (buttons) {
  357. for (let btn in buttons) {
  358. if (buttons.hasOwnProperty(btn)) {
  359. this.addButton(
  360. btn,
  361. buttons[btn].title,
  362. buttons[btn].description,
  363. buttons[btn].callback,
  364. buttons[btn].shortcut,
  365. )
  366. }
  367. }
  368. }
  369. }
  370.  
  371. class WMEUIHelperFieldset extends WMEUIHelperContainer {
  372. toHTML () {
  373. // Fieldset legend
  374. let legend = document.createElement('legend')
  375. legend.innerHTML = unsafePolicy.createHTML(this.title)
  376.  
  377. // Container for buttons
  378. let controls = document.createElement('div')
  379. controls.className = 'controls'
  380. // Append buttons to container
  381. this.elements.forEach(element => controls.append(element.html()))
  382.  
  383. let fieldset = document.createElement('fieldset')
  384. fieldset.append(legend, controls)
  385. return fieldset
  386. }
  387. }
  388.  
  389. class WMEUIHelperPanel extends WMEUIHelperContainer {
  390. toHTML () {
  391. // Label of the panel
  392. let label = document.createElement('label')
  393. label.className = 'control-label'
  394. label.innerHTML = unsafePolicy.createHTML(this.title)
  395. // Container for buttons
  396. let controls = document.createElement('div')
  397. controls.className = 'controls'
  398. // Append buttons to panel
  399. this.elements.forEach(element => controls.append(element.html()))
  400. // Build panel
  401. let group = document.createElement('div')
  402. group.className = 'form-group'
  403. group.append(label)
  404. group.append(controls)
  405. return group
  406. }
  407. }
  408.  
  409. class WMEUIHelperTab extends WMEUIHelperContainer {
  410. constructor (uid, id, title, description = null, attributes = {}) {
  411. super(uid, id, title, description, attributes)
  412. this.icon = attributes.icon ? attributes.icon : ''
  413. }
  414.  
  415. container () {
  416. return document.querySelector('.tab-content')
  417. }
  418.  
  419. inject () {
  420. this.container().append(this.html())
  421. }
  422.  
  423. toHTML () {
  424. // Create tab toggler
  425. let li = document.createElement('li')
  426. li.innerHTML = unsafePolicy.createHTML(
  427. '<a href="#sidepanel-' + this.uid + '" id="' + this.uid + '" data-toggle="tab">' + this.title + '</a>'
  428. )
  429. document.querySelector('#user-tabs .nav-tabs').append(li)
  430.  
  431. // Label of the panel
  432. let header = document.createElement('div')
  433. header.className = 'panel-header-component settings-header'
  434. header.innerHTML = unsafePolicy.createHTML(
  435. this.icon + '<div class="feature-id-container"><wz-overline>' + this.title + '</wz-overline></div>'
  436. )
  437.  
  438. // Container for buttons
  439. let controls = document.createElement('div')
  440. controls.className = 'button-toolbar'
  441.  
  442. // Append buttons to container
  443. this.elements.forEach(element => controls.append(element.html()))
  444.  
  445. // Build form group
  446. let group = document.createElement('div')
  447. group.className = 'form-group'
  448. group.append(header)
  449. group.append(controls)
  450.  
  451. // Section
  452. let pane = document.createElement('div')
  453. pane.id = 'sidepanel-' + this.uid // required by tab toggle, see above
  454. pane.className = 'tab-pane'
  455. pane.append(group)
  456. return pane
  457. }
  458. }
  459.  
  460. class WMEUIHelperModal extends WMEUIHelperContainer {
  461. container () {
  462. return document.getElementById('panel-container')
  463. }
  464.  
  465. inject () {
  466. this.container().append(this.html())
  467. }
  468.  
  469. toHTML () {
  470. // Header and close button
  471. let close = document.createElement('a')
  472. close.className = 'close-panel'
  473. close.onclick = function () {
  474. panel.remove()
  475. }
  476.  
  477. let header = document.createElement('div')
  478. header.className = 'header'
  479. header.innerHTML = unsafePolicy.createHTML(this.title)
  480. header.prepend(close)
  481.  
  482. // Body
  483. let body = document.createElement('div')
  484. body.className = 'body'
  485.  
  486. // Append buttons to panel
  487. this.elements.forEach(element => body.append(element.html()))
  488.  
  489. // Container
  490. let archivePanel = document.createElement('div')
  491. archivePanel.className = 'archive-panel'
  492. archivePanel.append(header)
  493. archivePanel.append(body)
  494.  
  495. let panel = document.createElement('div')
  496. panel.className = 'panel panel--to-be-deprecated show'
  497. panel.append(archivePanel)
  498.  
  499. return panel
  500. }
  501. }
  502.  
  503. /**
  504. * Just paragraph with text
  505. */
  506. class WMEUIHelperText extends WMEUIHelperElement {
  507. toHTML () {
  508. let p = document.createElement('p')
  509. p.innerHTML = unsafePolicy.createHTML(this.title)
  510. return p
  511. }
  512. }
  513.  
  514. /**
  515. * Base class for controls
  516. */
  517. class WMEUIHelperControl extends WMEUIHelperElement {
  518. constructor (uid, id, title, description, attributes = {}) {
  519. super(uid, id, title, description, attributes)
  520. this.attributes.name = this.id
  521. }
  522. }
  523.  
  524. /**
  525. * Input with label inside the div
  526. */
  527. class WMEUIHelperControlInput extends WMEUIHelperControl {
  528. toHTML () {
  529. let input = this.applyAttributes(document.createElement('input'))
  530. let label = document.createElement('label')
  531. label.htmlFor = input.id
  532. label.innerHTML = unsafePolicy.createHTML(this.title)
  533.  
  534. let container = document.createElement('div')
  535. container.title = this.description
  536. container.className = 'controls-container'
  537. container.append(input, label)
  538. return container
  539. }
  540. }
  541.  
  542. /**
  543. * Button with shortcut if neeeded
  544. */
  545. class WMEUIHelperControlButton extends WMEUIHelperControl {
  546. constructor (uid, id, title, description, callback, shortcut = null) {
  547. super(uid, id, title, description)
  548. this.callback = callback
  549. if (shortcut) {
  550. /* name, desc, group, title, shortcut, callback, scope */
  551. new WMEUIShortcut(
  552. this.uid + '-' + this.id,
  553. this.description,
  554. this.uid,
  555. title,
  556. shortcut,
  557. this.callback
  558. ).register()
  559. }
  560. }
  561.  
  562. toHTML () {
  563. let button = document.createElement('button')
  564. button.className = 'waze-btn waze-btn-small waze-btn-white'
  565. button.innerHTML = unsafePolicy.createHTML(this.title)
  566. button.title = this.description
  567. button.onclick = this.callback
  568. return button
  569. }
  570. }
  571.  
  572. /**
  573. * Based on the code from the WazeWrap library
  574. */
  575. class WMEUIShortcut {
  576. /**
  577. * @param {String} name
  578. * @param {String} desc
  579. * @param {String} group
  580. * @param {String} title
  581. * @param {String} shortcut
  582. * @param {Function} callback
  583. * @param {Object} scope
  584. * @return {WMEUIShortcut}
  585. */
  586. constructor (name, desc, group, title, shortcut, callback, scope = null) {
  587. this.name = name
  588. this.desc = desc
  589. this.group = WMEUI.normalize(group) || 'default'
  590. this.title = title
  591. this.shortcut = null
  592. this.callback = callback
  593. this.scope = ('object' === typeof scope) ? scope : null
  594.  
  595. /* Setup shortcut */
  596. if (shortcut && shortcut.length > 0) {
  597. this.shortcut = { [shortcut]: name }
  598. }
  599. }
  600.  
  601. /**
  602. * @param {String} group name
  603. * @param {String} title of the shortcut section
  604. */
  605. static setGroupTitle (group, title) {
  606. group = WMEUI.normalize(group)
  607.  
  608. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group]) {
  609. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group] = {
  610. description: title,
  611. members: {}
  612. }
  613. } else {
  614. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group].description = title
  615. }
  616. }
  617.  
  618. /**
  619. * Add translation for shortcut
  620. */
  621. addTranslation () {
  622. if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group]) {
  623. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group] = {
  624. description: this.title,
  625. members: {
  626. [this.name]: this.desc
  627. }
  628. }
  629. }
  630. I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group].members[this.name] = this.desc
  631. }
  632.  
  633. /**
  634. * Register group/action/event/shortcut
  635. */
  636. register () {
  637. /* Try to initialize new group */
  638. this.addGroup()
  639.  
  640. /* Clear existing actions with same name and create new */
  641. this.addAction()
  642.  
  643. /* Try to register new event */
  644. this.addEvent()
  645.  
  646. /* Finally, register the shortcut */
  647. this.registerShortcut()
  648. }
  649.  
  650. /**
  651. * Determines if the shortcut's action already exists.
  652. * @private
  653. */
  654. doesGroupExist () {
  655. return 'undefined' !== typeof W.accelerators.Groups[this.group]
  656. && 'undefined' !== typeof W.accelerators.Groups[this.group].members
  657. }
  658.  
  659. /**
  660. * Determines if the shortcut's action already exists.
  661. * @private
  662. */
  663. doesActionExist () {
  664. return 'undefined' !== typeof W.accelerators.Actions[this.name]
  665. }
  666.  
  667. /**
  668. * Determines if the shortcut's event already exists.
  669. * @private
  670. */
  671. doesEventExist () {
  672. return 'undefined' !== typeof W.accelerators.events.dispatcher._events[this.name]
  673. && W.accelerators.events.dispatcher._events[this.name].length > 0
  674. && this.callback === W.accelerators.events.dispatcher._events[this.name][0].func
  675. && this.scope === W.accelerators.events.dispatcher._events[this.name][0].obj
  676. }
  677.  
  678. /**
  679. * Creates the shortcut's group.
  680. * @private
  681. */
  682. addGroup () {
  683. if (this.doesGroupExist()) return
  684.  
  685. W.accelerators.Groups[this.group] = []
  686. W.accelerators.Groups[this.group].members = []
  687. }
  688.  
  689. /**
  690. * Registers the shortcut's action.
  691. * @private
  692. */
  693. addAction () {
  694. if (this.doesActionExist()) {
  695. W.accelerators.Actions[this.name] = null
  696. }
  697. W.accelerators.addAction(this.name, { group: this.group })
  698. }
  699.  
  700. /**
  701. * Registers the shortcut's event.
  702. * @private
  703. */
  704. addEvent () {
  705. if (this.doesEventExist()) return
  706. W.accelerators.events.register(this.name, this.scope, this.callback)
  707. }
  708.  
  709. /**
  710. * Registers the shortcut's keyboard shortcut.
  711. * @private
  712. */
  713. registerShortcut () {
  714. if (this.shortcut) {
  715. /* Setup translation for shortcut */
  716. this.addTranslation()
  717. W.accelerators._registerShortcuts(this.shortcut)
  718. }
  719. }
  720. }