WME UI

UI Library for Waze Map Editor Greasy Fork scripts

目前為 2022-09-06 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/450320/1090460/WME%20UI.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WME UI
// @version      0.0.2
// @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://www.waze.com/editor*
// @match        https://www.waze.com/*/editor*
// @match        https://beta.waze.com/editor*
// @match        https://beta.waze.com/*/editor*
// @exclude      https://www.waze.com/user/editor*
// @exclude      https://beta.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()
  }

  /**
   * Apply CSS styles
   */
  static addStyle (css) {
    let style = document.createElement('style')
    style.type = 'text/css' // is required
    style.innerHTML = unsafePolicy.createHTML(css)
    document.querySelector('head').appendChild(style)
  }

  /**
   * @param {String} uid
   * @param {Object} data
   */
  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
  }
}

/**
 * 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 HTMLElement 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 fieldset 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 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(
      '<div class="panel-header-component-main">' + this.icon + '<div class="feature-id-container"><wz-overline>' + this.title + '</wz-overline></div></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 show'
    panel.append(archivePanel)

    return panel
  }
}

class WMEUIHelperText extends WMEUIHelperElement {
  toHTML () {
    let p = document.createElement('p')
    p.innerHTML = unsafePolicy.createHTML(this.title)
    return p
  }
}

class WMEUIHelperControl extends WMEUIHelperElement {
  constructor (uid, id, title, description, attributes = {}) {
    super(uid, id, title, description, attributes)
    this.attributes.name = this.id
  }
}

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
  }
}

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,
        this.uid,
        shortcut,
        this.callback
      )
    }
  }

  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 = group || 'default'
    this.title = title
    this.shortcut = {}
    this.callback = callback
    this.scope = ('object' === typeof scope) ? scope : null

    /* Setup translation for shortcut */
    if (shortcut.length > 0) {
      this.shortcut = { [shortcut]: name }
      WMEUIShortcut.addTranslation(this.group, this.title, this.name, this.desc)
    }

    /* 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()
  }

  /**
   * @param {String} group name
   * @param {String} title of the shortcut section
   * @param {String} name of the shortcut
   * @param {String} description of the shortcut
   */
  static addTranslation (group, title, name, description) {
    if (!I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group]) {
      I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group] = {
        description: title,
        members: {
          [name]: description
        }
      }
    }
    I18n.translations[I18n.currentLocale()].keyboard_shortcuts.groups[group].members[name] = description
  }

  /**
   * 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 () {
    W.accelerators._registerShortcuts(this.shortcut)
  }
}