- // ==UserScript==
- // @name WME UI
- // @version 0.2.7
- // @description UI Library for Waze Map Editor Greasy Fork scripts
- // @license MIT License
- // @author Anton Shevchuk
- // @namespace https://greasyfork.org/users/227648-anton-shevchuk
- // @supportURL https://github.com/AntonShevchuk/wme-ui/issues
- // @match https://*.waze.com/editor*
- // @match https://*.waze.com/*/editor*
- // @exclude https://*.waze.com/user/editor*
- // @icon https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://anton.shevchuk.name&size=64
- // @grant none
- // ==/UserScript==
-
- /* jshint esversion: 8 */
- /* global W, I18n */
-
- // WARNING: this is unsafe!
- let unsafePolicy = {
- createHTML: string => string
- }
-
- // Feature testing
- if (window.trustedTypes && window.trustedTypes.createPolicy) {
- unsafePolicy = window.trustedTypes.createPolicy('unsafe', {
- createHTML: string => string,
- })
- }
-
- class WMEUI {
- /**
- * Normalize title or UID
- * @param string
- * @returns {string}
- */
- static normalize (string) {
- return string.replace(/\W+/gi, '-').toLowerCase()
- }
-
- /**
- * Inject CSS styles
- * @param {String} css
- * @return void
- */
- static addStyle (css) {
- let style = document.createElement('style')
- style.type = 'text/css' // is required
- style.innerHTML = unsafePolicy.createHTML(css)
- document.querySelector('head').appendChild(style)
- }
-
- /**
- * Add translation for the I18n object
- * @param {String} uid
- * @param {Object} data
- * @return void
- */
- static addTranslation (uid, data) {
- if (!data.en) {
- console.error('Default translation `en` is required')
- }
- let locale = I18n.currentLocale()
- I18n.translations[locale][uid] = data[locale] || data.en
- }
-
- /**
- * Create and register shortcut
- * @param {String} name
- * @param {String} desc
- * @param {String} group
- * @param {String} title
- * @param {String} shortcut
- * @param {Function} callback
- * @param {Object} scope
- * @return void
- */
- static addShortcut (name, desc, group, title, shortcut, callback, scope = null) {
- new WMEUIShortcut(name, desc, group, title, shortcut, callback, scope).register()
- }
- }
-
- /**
- * God class, create it once
- */
- class WMEUIHelper {
- constructor (uid) {
- this.uid = WMEUI.normalize(uid)
- this.index = 0
- }
-
- /**
- * Generate unque ID
- * @return {string}
- */
- generateId () {
- this.index++
- return this.uid + '-' + this.index
- }
-
- /**
- * Create a panel for the sidebar
- * @param {String} title
- * @param {Object} attributes
- * @return {WMEUIHelperPanel}
- */
- createPanel (title, attributes = {}) {
- return new WMEUIHelperPanel(this.uid, this.generateId(), title, attributes)
- }
-
- /**
- * Create a tab for the sidebar
- * @param {String} title
- * @param {Object} attributes
- * @return {WMEUIHelperTab}
- */
- createTab (title, attributes = {}) {
- return new WMEUIHelperTab(this.uid, this.generateId(), title, attributes)
- }
-
- /**
- * Create a modal window
- * @param {String} title
- * @param {Object} attributes
- * @return {WMEUIHelperModal}
- */
- createModal (title, attributes = {}) {
- return new WMEUIHelperModal(this.uid, this.generateId(), title, attributes)
- }
-
- /**
- * Create a field set
- * @param {String} title
- * @param {Object} attributes
- * @return {WMEUIHelperFieldset}
- */
- createFieldset (title, attributes = {}) {
- return new WMEUIHelperFieldset(this.uid, this.generateId(), title, attributes)
- }
- }
-
- /**
- * Basic for all UI elements
- */
- class WMEUIHelperElement {
- constructor (uid, id, title = null, attributes = {}) {
- this.uid = uid
- this.id = id
- this.title = title
- // HTML attributes
- this.attributes = attributes
- // DOM element
- this.element = null
- // Children
- this.elements = []
- }
-
- /**
- * Add WMEUIHelperElement to container
- * @param {WMEUIHelperElement} element
- * @return {WMEUIHelperElement} element
- */
- addElement (element) {
- this.elements.push(element)
- return element
- }
-
- /**
- * @param {HTMLElement} element
- * @return {HTMLElement}
- */
- applyAttributes (element) {
- for (let attr in this.attributes) {
- if (this.attributes.hasOwnProperty(attr)) {
- element[attr] = this.attributes[attr]
- }
- }
- return element
- }
-
- /**
- * @return {HTMLElement}
- */
- html () {
- if (!this.element) {
- this.element = this.toHTML()
- this.element.className += ' ' + this.uid + ' ' + this.uid + '-' + this.id
- }
- return this.element
- }
-
- /**
- * Build and return HTML elements for injection
- * @return {HTMLElement}
- */
- toHTML () {
- throw new Error('Abstract method')
- }
- }
-
- /**
- * Basic for all UI containers
- */
- class WMEUIHelperContainer extends WMEUIHelperElement {
- /**
- * Create and add button
- * For Tab Panel Modal Fieldset
- * @param {String} id
- * @param {String} title
- * @param {String} description
- * @param {Function} callback
- * @param {String} shortcut
- * @return {WMEUIHelperElement} element
- */
- addButton (id, title, description, callback, shortcut = null) {
- return this.addElement(new WMEUIHelperControlButton(this.uid, id, title, description, callback, shortcut))
- }
-
- /**
- * Create buttons
- * @param {Object} buttons
- */
- addButtons (buttons) {
- for (let btn in buttons) {
- if (buttons.hasOwnProperty(btn)) {
- this.addButton(
- btn,
- buttons[btn].title,
- buttons[btn].description,
- buttons[btn].callback,
- buttons[btn].shortcut,
- )
- }
- }
- }
-
- /**
- * Create checkbox
- * For Tab, Panel, Modal, or Fieldset
- * @param {String} id
- * @param {String} title
- * @param {Function} callback
- * @param {Boolean} checked
- * @return {WMEUIHelperElement} element
- */
- addCheckbox (id, title, callback, checked = false) {
- return this.addElement(
- new WMEUIHelperControlInput(this.uid, id, title, {
- 'id': this.uid + '-' + id,
- 'onclick': callback,
- 'type': 'checkbox',
- 'value': 1,
- 'checked': checked,
- })
- )
- }
-
- /**
- * Create and add WMEUIHelperDiv element
- * @param {String} id
- * @param {String} innerHTML
- * @param {Object} attributes
- * @return {WMEUIHelperElement} element
- */
- addDiv (id, innerHTML = null, attributes = {}) {
- return this.addElement(new WMEUIHelperDiv(this.uid, id, innerHTML, attributes))
- }
-
- /**
- * Create and add WMEUIHelperFieldset element
- * For Tab, Panel, Modal
- * @param {String} id
- * @param {String} title
- * @return {WMEUIHelperElement} element
- */
- addFieldset (id, title) {
- return this.addElement(new WMEUIHelperFieldset(this.uid, id, title))
- }
-
- /**
- * Create text input
- * @param {String} id
- * @param {String} title
- * @param {Function} callback
- * @param {String} value
- * @return {WMEUIHelperElement} element
- */
- addInput (id, title, callback, value = '') {
- return this.addElement(
- new WMEUIHelperControlInput(this.uid, id, title, {
- 'id': this.uid + '-' + id,
- 'onchange': callback,
- 'type': 'text',
- 'value': value,
- })
- )
- }
-
- /**
- * Create number input
- * @param {String} id
- * @param {String} title
- * @param {Function} callback
- * @param {Number} value
- * @param {Number} min
- * @param {Number} max
- * @param {Number} step
- * @return {WMEUIHelperElement} element
- */
- addNumber (id, title, callback, value = 0, min, max, step = 10) {
- return this.addElement(
- new WMEUIHelperControlInput(this.uid, id, title, {
- 'id': this.uid + '-' + id,
- 'onchange': callback,
- 'type': 'number',
- 'value': value,
- 'min': min,
- 'max': max,
- 'step': step,
- })
- )
- }
-
- /**
- * Create radiobutton
- * @param {String} id
- * @param {String} title
- * @param {Function} callback
- * @param {String} name
- * @param {String} value
- * @param {Boolean} checked
- * @return {WMEUIHelperElement} element
- */
- addRadio (id, title, callback, name, value, checked = false) {
- return this.addElement(
- new WMEUIHelperControlInput(this.uid, id, title, {
- 'id': this.uid + '-' + id,
- 'name': name,
- 'onclick': callback,
- 'type': 'radio',
- 'value': value,
- 'checked': checked,
- })
- )
- }
-
- /**
- * Create range input
- * @param {String} id
- * @param {String} title
- * @param {Function} callback
- * @param {Number} value
- * @param {Number} min
- * @param {Number} max
- * @param {Number} step
- * @return {WMEUIHelperElement} element
- */
- addRange (id, title, callback, value, min, max, step = 10) {
- return this.addElement(
- new WMEUIHelperControlInput(this.uid, id, title, {
- 'id': this.uid + '-' + id,
- 'onchange': callback,
- 'type': 'range',
- 'value': value,
- 'min': min,
- 'max': max,
- 'step': step,
- })
- )
- }
-
- /**
- * Create and add WMEUIHelperText element
- * @param {String} id
- * @param {String} text
- * @return {WMEUIHelperElement} element
- */
- addText (id, text) {
- return this.addElement(new WMEUIHelperText(this.uid, id, text))
- }
- }
-
- class WMEUIHelperFieldset extends WMEUIHelperContainer {
- toHTML () {
- // Fieldset legend
- let legend = document.createElement('legend')
- legend.innerHTML = unsafePolicy.createHTML(this.title)
-
- // Container for buttons
- let controls = document.createElement('div')
- controls.className = 'controls'
- // Append buttons to container
- this.elements.forEach(element => controls.append(element.html()))
-
- let fieldset = document.createElement('fieldset')
- fieldset = this.applyAttributes(fieldset)
- fieldset.append(legend)
- fieldset.append(controls)
- return fieldset
- }
- }
-
- class WMEUIHelperPanel extends WMEUIHelperContainer {
- container () {
- return document.getElementById('edit-panel')
- }
-
- inject () {
- this.container().append(this.html())
- }
-
- toHTML () {
- // Label of the panel
- let label = document.createElement('label')
- label.className = 'control-label'
- label.innerHTML = unsafePolicy.createHTML(this.title)
- // Container for buttons
- let controls = document.createElement('div')
- controls.className = 'controls'
- // Append buttons to panel
- this.elements.forEach(element => controls.append(element.html()))
- // Build the panel
- let group = document.createElement('div')
- group.className = 'form-group'
- group.append(label)
- group.append(controls)
- return group
- }
- }
-
- class WMEUIHelperTab extends WMEUIHelperContainer {
- constructor (uid, id, title, attributes = {}) {
- super(uid, id, title, attributes)
- this.icon = attributes.icon
- this.image = attributes.image
- }
-
- async inject () {
- const { tabLabel, tabPane } = W.userscripts.registerSidebarTab(this.uid)
-
- tabLabel.innerText = this.title
- tabLabel.title = this.title
-
- tabPane.append(this.html())
- }
-
- toHTML () {
- // Label of the panel
- let header = document.createElement('div')
- header.className = 'panel-header-component settings-header'
- header.style.alignItems = 'center'
- header.style.display = 'flex'
- header.style.gap = '9px'
- header.style.justifyContent = 'stretch'
- header.style.padding = '8px'
- header.style.width = '100%'
-
- if (this.icon) {
- let icon = document.createElement('i')
- icon.className = 'w-icon panel-header-component-icon w-icon-' + this.icon
- icon.style.fontSize = '24px'
- header.append(icon)
- }
-
- if (this.image) {
- let img = document.createElement('img')
- img.style.height = '42px'
- img.src = this.image
- header.append(img)
- }
-
- let title = document.createElement('div')
- title.className = 'feature-id-container'
- title.innerHTML = unsafePolicy.createHTML(
- '<div class="feature-id-container"><wz-overline>' + this.title + '</wz-overline></div>'
- )
- header.append(title)
-
- // Container for buttons
- let controls = document.createElement('div')
- controls.className = 'button-toolbar'
-
- // Append buttons to container
- this.elements.forEach(element => controls.append(element.html()))
-
- // Build a form group
- let group = document.createElement('div')
- group.className = 'form-group'
- group.append(header)
- group.append(controls)
-
- return group
- }
- }
-
- class WMEUIHelperModal extends WMEUIHelperContainer {
- container () {
- return document.getElementById('tippy-container')
- }
-
- inject () {
- this.container().append(this.html())
- }
-
- toHTML () {
- // Header and close button
- let close = document.createElement('button')
- close.className = 'wme-ui-close-panel'
- close.style.background = '#fff'
- close.style.border = '1px solid #ececec'
- close.style.borderRadius = '100%'
- close.style.cursor = 'pointer'
- close.style.fontSize = '20px'
- close.style.height = '20px'
- close.style.lineHeight = '16px'
- close.style.position = 'absolute'
- close.style.right = '14px'
- close.style.textIndent = '-2px'
- close.style.top = '14px'
- close.style.transition = 'all 150ms'
- close.style.width = '20px'
- close.style.zIndex = '99'
- close.innerText = '×'
- close.onclick = function () {
- panel.remove()
- }
-
- let title = document.createElement('h5')
- title.style.padding = '16px 16px 0'
- title.innerHTML = unsafePolicy.createHTML(this.title)
-
- let header = document.createElement('div')
- header.className = 'wme-ui-header'
- header.style.position = 'relative'
- header.prepend(title)
- header.prepend(close)
-
- // Body
- let body = document.createElement('div')
- body.className = 'wme-ui-body'
-
- // Append buttons to panel
- this.elements.forEach(element => body.append(element.html()))
-
- // Container
- let container = document.createElement('div')
- container.className = 'wme-ui-panel-container'
- container.append(header)
- container.append(body)
-
- // Panel
- let panel = document.createElement('div')
- panel.style.width = '320px'
- panel.style.background = '#fff'
- panel.style.margin = '15px'
- panel.style.borderRadius = '5px'
- panel.className = 'wme-ui-panel'
- panel.append(container)
-
- return panel
- }
- }
-
- /**
- * Just div, can be with text
- */
- class WMEUIHelperDiv extends WMEUIHelperElement {
- toHTML () {
- let div = document.createElement('div')
- div = this.applyAttributes(div)
- div.id = this.uid + '-' + this.id
- if (this.title) {
- div.innerHTML = unsafePolicy.createHTML(this.title)
- }
- return div
- }
- }
-
- /**
- * Just a paragraph with text
- */
- class WMEUIHelperText extends WMEUIHelperElement {
- toHTML () {
- let p = document.createElement('p')
- p = this.applyAttributes(p)
- p.innerHTML = unsafePolicy.createHTML(this.title)
- return p
- }
- }
-
- /**
- * Base class for controls
- */
- class WMEUIHelperControl extends WMEUIHelperElement {
- constructor (uid, id, title, attributes = {}) {
- super(uid, id, title, attributes)
- if (!attributes.name) {
- this.attributes.name = this.id
- }
- }
- }
-
- /**
- * Input with label inside the div
- */
- class WMEUIHelperControlInput extends WMEUIHelperControl {
- toHTML () {
- let input = document.createElement('input')
- input = this.applyAttributes(input)
-
- let label = document.createElement('label')
- label.htmlFor = input.id
- label.innerHTML = unsafePolicy.createHTML(this.title)
-
- let container = document.createElement('div')
- container.className = 'controls-container'
- container.append(input)
- container.append(label)
- return container
- }
- }
-
- /**
- * Button with shortcut if needed
- */
- class WMEUIHelperControlButton extends WMEUIHelperControl {
- constructor (uid, id, title, description, callback, shortcut = null) {
- super(uid, id, title)
- this.description = description
- this.callback = callback
- if (shortcut) {
- /* name, desc, group, title, shortcut, callback, scope */
- new WMEUIShortcut(
- this.uid + '-' + this.id,
- this.description,
- this.uid,
- title,
- shortcut,
- this.callback
- ).register()
- }
- }
-
- toHTML () {
- let button = document.createElement('button')
- button.className = 'waze-btn waze-btn-small waze-btn-white'
- button.innerHTML = unsafePolicy.createHTML(this.title)
- button.title = this.description
- button.onclick = this.callback
- return button
- }
- }
-
- /**
- * Based on the code from the WazeWrap library
- */
- class WMEUIShortcut {
- /**
- * @param {String} name
- * @param {String} desc
- * @param {String} group
- * @param {String} title
- * @param {String} shortcut
- * @param {Function} callback
- * @param {Object} scope
- * @return {WMEUIShortcut}
- */
- constructor (name, desc, group, title, shortcut, callback, scope = null) {
- this.name = name
- this.desc = desc
- this.group = WMEUI.normalize(group) || 'default'
- this.title = title
- this.shortcut = null
- this.callback = callback
- this.scope = ('object' === typeof scope) ? scope : null
-
- /* Setup shortcut */
- if (shortcut && shortcut.length > 0) {
- this.shortcut = { [shortcut]: name }
- }
- }
-
- /**
- * @param {String} group name
- * @param {String} title of the shortcut section
- */
- static setGroupTitle (group, title) {
- group = WMEUI.normalize(group)
-
- if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group]) {
- I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group] = {
- description: title,
- members: {}
- }
- } else {
- I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group].description = title
- }
- }
-
- /**
- * Add translation for shortcut
- */
- addTranslation () {
- if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group]) {
- I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group] = {
- description: this.title,
- members: {
- [this.name]: this.desc
- }
- }
- }
- I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[this.group].members[this.name] = this.desc
- }
-
- /**
- * Register group/action/event/shortcut
- */
- register () {
- /* Try to initialize a new group */
- this.addGroup()
-
- /* Clear existing actions with the same name and create new */
- this.addAction()
-
- /* Try to register new event */
- this.addEvent()
-
- /* Finally, register the shortcut */
- this.registerShortcut()
- }
-
- /**
- * Determines if the shortcut's action already exists.
- * @private
- */
- doesGroupExist () {
- return 'undefined' !== typeof W.accelerators.Groups[this.group]
- && 'undefined' !== typeof W.accelerators.Groups[this.group].members
- }
-
- /**
- * Determines if the shortcut's action already exists.
- * @private
- */
- doesActionExist () {
- return 'undefined' !== typeof W.accelerators.Actions[this.name]
- }
-
- /**
- * Determines if the shortcut's event already exists.
- * @private
- */
- doesEventExist () {
- return 'undefined' !== typeof W.accelerators.events.dispatcher._events[this.name]
- && W.accelerators.events.dispatcher._events[this.name].length > 0
- && this.callback === W.accelerators.events.dispatcher._events[this.name][0].func
- && this.scope === W.accelerators.events.dispatcher._events[this.name][0].obj
- }
-
- /**
- * Creates the shortcut's group.
- * @private
- */
- addGroup () {
- if (this.doesGroupExist()) return
-
- W.accelerators.Groups[this.group] = []
- W.accelerators.Groups[this.group].members = []
- }
-
- /**
- * Registers the shortcut's action.
- * @private
- */
- addAction () {
- if (this.doesActionExist()) {
- W.accelerators.Actions[this.name] = null
- }
- W.accelerators.addAction(this.name, { group: this.group })
- }
-
- /**
- * Registers the shortcut's event.
- * @private
- */
- addEvent () {
- if (this.doesEventExist()) return
- W.accelerators.events.register(this.name, this.scope, this.callback)
- }
-
- /**
- * Registers the shortcut's keyboard shortcut.
- * @private
- */
- registerShortcut () {
- if (this.shortcut) {
- /* Setup translation for shortcut */
- this.addTranslation()
- W.accelerators._registerShortcuts(this.shortcut)
- }
- }
- }