UI Library for Waze Map Editor Greasy Fork scripts
Verzia zo dňa
Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greasyfork.org/scripts/450320/1127617/WME%20UI.js
// ==UserScript==
// @name WME UI
// @version 0.0.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 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 {String} description
* @param {Object} attributes
* @return {WMEUIHelperPanel}
*/
createPanel (title, description = null, attributes = {}) {
return new WMEUIHelperPanel(this.uid, this.generateId(), title, description, attributes)
}
/**
* Create a tab for the sidebar
* @param {String} title
* @param {String} description
* @param {Object} attributes
* @return {WMEUIHelperTab}
*/
createTab (title, description = null, attributes = {}) {
return new WMEUIHelperTab(this.uid, this.generateId(), title, description, attributes)
}
/**
* Create a modal window
* @param {String} title
* @param {String} description
* @param {Object} attributes
* @return {WMEUIHelperModal}
*/
createModal (title, description = null) {
return new WMEUIHelperModal(this.uid, this.generateId(), title, description)
}
/**
* Create a field set
* @param {String} title
* @param {String} description
* @param {Object} attributes
* @return {WMEUIHelperFieldset}
*/
createFieldset (title, description = null) {
return new WMEUIHelperFieldset(this.uid, this.generateId(), title, description)
}
}
/**
* Basic for all UI elements
*/
class WMEUIHelperElement {
constructor (uid, id, title, description = null, attributes = {}) {
this.uid = uid
this.id = id
this.title = title
this.description = description
this.attributes = attributes
this.domElement = null
}
/**
* @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.domElement) {
this.domElement = this.toHTML()
this.domElement.className += ' ' + this.uid + ' ' + this.uid + '-' + this.id
}
return this.domElement
}
/**
* @return {HTMLElement}
*/
toHTML () {
throw new Error('Abstract method')
}
}
/**
* Basic for all UI containers
*/
class WMEUIHelperContainer extends WMEUIHelperElement {
constructor (uid, id, title, description = null, attributes = {}) {
super(uid, id, title, description, attributes)
this.elements = []
if (description) {
this.addText('description', description)
}
}
/**
* Add WMEUIHelperElement to container
* @param {WMEUIHelperElement} element
*/
addElement (element) {
this.elements.push(element)
}
/**
* Create and add WMEUIHelperText element
* For Tab, Panel, Modal, or Fieldset
* @param {String} id
* @param {String} text
*/
addText (id, text) {
return this.addElement(new WMEUIHelperText(this.uid, id, text))
}
/**
* Create and add WMEUIHelperFieldset element
* For Tab, Panel, Modal
* @param {String} id
* @param {String} title
* @param {String} description
*/
addFieldset (id, title, description) {
return this.addElement(new WMEUIHelperFieldset(this.uid, id, title, description))
}
/**
* Create text input
* For Tab, Panel, Modal, or Fieldset
* @param {String} id
* @param {String} title
* @param {String} description
* @param {Function} callback
* @param {String} value
*/
addInput (id, title, description, callback, value = '') {
return this.addElement(
new WMEUIHelperControlInput(this.uid, id, title, description, {
'id': this.uid + '-' + id,
'onchange': callback,
'type': 'text',
'value': value,
})
)
}
/**
* Create number input
* For Tab, Panel, Modal, or Fieldset
* @param {String} id
* @param {String} title
* @param {String} description
* @param {Function} callback
* @param {String} value
*/
addNumber (id, title, description, callback, value = '') {
return this.addElement(
new WMEUIHelperControlInput(this.uid, id, title, description, {
'id': this.uid + '-' + id,
'onchange': callback,
'type': 'number',
'value': value,
})
)
}
/**
* Create checkbox
* For Tab, Panel, Modal, or Fieldset
* @param {String} id
* @param {String} title
* @param {String} description
* @param {Function} callback
* @param {Bool} checked
*/
addCheckbox (id, title, description, callback, checked = false) {
return this.addElement(
new WMEUIHelperControlInput(this.uid, id, title, description, {
'id': this.uid + '-' + id,
'onclick': callback,
'type': 'checkbox',
'value': 1,
'checked': checked,
})
)
}
/**
* Create radiobutton
* @param {String} id
* @param {String} title
* @param {String} description
* @param {Function} callback
* @param {String} value
* @param {Bool} checked
*/
addRadio (id, title, description, callback, value, checked = false) {
return this.addElement(
new WMEUIHelperControlInput(this.uid, id, title, description, {
'id': this.uid + '-' + id + '-' + value,
'onclick': callback,
'type': 'radio',
'value': value,
'checked': checked,
})
)
}
/**
* Create range input
* @param {String} id
* @param {String} title
* @param {String} description
* @param {Function} callback
* @param {Integer} min
* @param {Integer} max
* @param {Integer} value
* @param {Integer} step
*/
addRange (id, title, description, callback, min, max, value, step = 10) {
return this.addElement(
new WMEUIHelperControlInput(this.uid, id, title, description, {
'id': this.uid + '-' + id,
'onchange': callback,
'type': 'range',
'min': min,
'max': max,
'value': value,
'step': step,
})
)
}
/**
* Create and add button
* For Tab Panel Modal Fieldset
* @param {String} id
* @param {String} title
* @param {String} description
* @param {Function} callback
* @param {String} shortcut
*/
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,
)
}
}
}
}
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.append(legend, controls)
return fieldset
}
}
class WMEUIHelperPanel extends WMEUIHelperContainer {
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 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, description = null, attributes = {}) {
super(uid, id, title, description, attributes)
this.icon = attributes.icon ? attributes.icon : ''
}
container () {
return document.querySelector('.tab-content')
}
inject () {
this.container().append(this.html())
}
toHTML () {
// Create tab toggler
let li = document.createElement('li')
li.innerHTML = unsafePolicy.createHTML(
'<a href="#sidepanel-' + this.uid + '" id="' + this.uid + '" data-toggle="tab">' + this.title + '</a>'
)
document.querySelector('#user-tabs .nav-tabs').append(li)
// Label of the panel
let header = document.createElement('div')
header.className = 'panel-header-component settings-header'
header.innerHTML = unsafePolicy.createHTML(
this.icon + '<div class="feature-id-container"><wz-overline>' + this.title + '</wz-overline></div>'
)
// 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 form group
let group = document.createElement('div')
group.className = 'form-group'
group.append(header)
group.append(controls)
// Section
let pane = document.createElement('div')
pane.id = 'sidepanel-' + this.uid // required by tab toggle, see above
pane.className = 'tab-pane'
pane.append(group)
return pane
}
}
class WMEUIHelperModal extends WMEUIHelperContainer {
container () {
return document.getElementById('panel-container')
}
inject () {
this.container().append(this.html())
}
toHTML () {
// Header and close button
let close = document.createElement('a')
close.className = 'close-panel'
close.onclick = function () {
panel.remove()
}
let header = document.createElement('div')
header.className = 'header'
header.innerHTML = unsafePolicy.createHTML(this.title)
header.prepend(close)
// Body
let body = document.createElement('div')
body.className = 'body'
// Append buttons to panel
this.elements.forEach(element => body.append(element.html()))
// Container
let archivePanel = document.createElement('div')
archivePanel.className = 'archive-panel'
archivePanel.append(header)
archivePanel.append(body)
let panel = document.createElement('div')
panel.className = 'panel panel--to-be-deprecated show'
panel.append(archivePanel)
return panel
}
}
/**
* Just paragraph with text
*/
class WMEUIHelperText extends WMEUIHelperElement {
toHTML () {
let p = document.createElement('p')
p.innerHTML = unsafePolicy.createHTML(this.title)
return p
}
}
/**
* Base class for controls
*/
class WMEUIHelperControl extends WMEUIHelperElement {
constructor (uid, id, title, description, attributes = {}) {
super(uid, id, title, description, attributes)
this.attributes.name = this.id
}
}
/**
* Input with label inside the div
*/
class WMEUIHelperControlInput extends WMEUIHelperControl {
toHTML () {
let input = this.applyAttributes(document.createElement('input'))
let label = document.createElement('label')
label.htmlFor = input.id
label.innerHTML = unsafePolicy.createHTML(this.title)
let container = document.createElement('div')
container.title = this.description
container.className = 'controls-container'
container.append(input, label)
return container
}
}
/**
* Button with shortcut if neeeded
*/
class WMEUIHelperControlButton extends WMEUIHelperControl {
constructor (uid, id, title, description, callback, shortcut = null) {
super(uid, id, title, 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 new group */
this.addGroup()
/* Clear existing actions with 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)
}
}
}