WME UI

UI Library for Waze Map Editor Greasy Fork scripts

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

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

// ==UserScript==
// @name         WME UI
// @version      0.0.3
// @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 {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 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)
  }
}