/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /** * This is a CSS Filter Editor widget used * for Rule View's filter swatches */ const EventEmitter = require("resource://devtools/shared/event-emitter.js"); const XHTML_NS = "http://www.w3.org/1999/xhtml"; const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); const STRINGS_URI = "devtools/client/locales/filterwidget.properties"; const L10N = new LocalizationHelper(STRINGS_URI); const { cssTokenizer, } = require("resource://devtools/shared/css/parsing-utils.js"); const asyncStorage = require("resource://devtools/shared/async-storage.js"); const DEFAULT_FILTER_TYPE = "length"; const UNIT_MAPPING = { percentage: "%", length: "px", angle: "deg", string: "", }; const FAST_VALUE_MULTIPLIER = 10; const SLOW_VALUE_MULTIPLIER = 0.1; const DEFAULT_VALUE_MULTIPLIER = 1; const LIST_PADDING = 7; const LIST_ITEM_HEIGHT = 32; const filterList = [ { name: "blur", range: [0, Infinity], type: "length", }, { name: "brightness", range: [0, Infinity], type: "percentage", }, { name: "contrast", range: [0, Infinity], type: "percentage", }, { name: "drop-shadow", placeholder: L10N.getStr("dropShadowPlaceholder"), type: "string", }, { name: "grayscale", range: [0, 100], type: "percentage", }, { name: "hue-rotate", range: [0, Infinity], type: "angle", }, { name: "invert", range: [0, 100], type: "percentage", }, { name: "opacity", range: [0, 100], type: "percentage", }, { name: "saturate", range: [0, Infinity], type: "percentage", }, { name: "sepia", range: [0, 100], type: "percentage", }, { name: "url", placeholder: "example.svg#c1", type: "string", }, ]; // Valid values that shouldn't be parsed for filters. const SPECIAL_VALUES = new Set(["none", "unset", "initial", "inherit"]); /** * A CSS Filter editor widget used to add/remove/modify * filters. * * Normally, it takes a CSS filter value as input, parses it * and creates the required elements / bindings. * * You can, however, use add/remove/update methods manually. * See each method's comments for more details * * @param {Node} el * The widget container. * @param {String} value * CSS filter value */ function CSSFilterEditorWidget(el, value = "") { this.doc = el.ownerDocument; this.win = this.doc.defaultView; this.el = el; this._cssIsValid = (name, val) => { return this.win.CSS.supports(name, val); }; this._addButtonClick = this._addButtonClick.bind(this); this._removeButtonClick = this._removeButtonClick.bind(this); this._mouseMove = this._mouseMove.bind(this); this._mouseUp = this._mouseUp.bind(this); this._mouseDown = this._mouseDown.bind(this); this._keyDown = this._keyDown.bind(this); this._input = this._input.bind(this); this._presetClick = this._presetClick.bind(this); this._savePreset = this._savePreset.bind(this); this._togglePresets = this._togglePresets.bind(this); this._resetFocus = this._resetFocus.bind(this); // Passed to asyncStorage, requires binding this.renderPresets = this.renderPresets.bind(this); this._initMarkup(); this._buildFilterItemMarkup(); this._buildPresetItemMarkup(); this._addEventListeners(); EventEmitter.decorate(this); this.filters = []; this.setCssValue(value); this.renderPresets(); } exports.CSSFilterEditorWidget = CSSFilterEditorWidget; CSSFilterEditorWidget.prototype = { _initMarkup() { // The following structure is created: //
//
// //
//
//
// //
const content = this.doc.createDocumentFragment(); const filterListWrapper = this.doc.createElementNS(XHTML_NS, "div"); filterListWrapper.classList.add("filters-list"); content.appendChild(filterListWrapper); this.filterList = this.doc.createElementNS(XHTML_NS, "div"); this.filterList.setAttribute("id", "filters"); filterListWrapper.appendChild(this.filterList); const filterListFooter = this.doc.createElementNS(XHTML_NS, "div"); filterListFooter.classList.add("footer"); filterListWrapper.appendChild(filterListFooter); this.filterSelect = this.doc.createElementNS(XHTML_NS, "select"); this.filterSelect.setAttribute("value", ""); filterListFooter.appendChild(this.filterSelect); const filterListPlaceholder = this.doc.createElementNS(XHTML_NS, "option"); filterListPlaceholder.setAttribute("value", ""); filterListPlaceholder.textContent = L10N.getStr( "filterListSelectPlaceholder" ); this.filterSelect.appendChild(filterListPlaceholder); const addFilter = this.doc.createElementNS(XHTML_NS, "button"); addFilter.setAttribute("id", "add-filter"); addFilter.classList.add("add"); addFilter.textContent = L10N.getStr("addNewFilterButton"); filterListFooter.appendChild(addFilter); this.togglePresets = this.doc.createElementNS(XHTML_NS, "button"); this.togglePresets.setAttribute("id", "toggle-presets"); this.togglePresets.textContent = L10N.getStr("presetsToggleButton"); filterListFooter.appendChild(this.togglePresets); const presetListWrapper = this.doc.createElementNS(XHTML_NS, "div"); presetListWrapper.classList.add("presets-list"); content.appendChild(presetListWrapper); this.presetList = this.doc.createElementNS(XHTML_NS, "div"); this.presetList.setAttribute("id", "presets"); presetListWrapper.appendChild(this.presetList); const presetListFooter = this.doc.createElementNS(XHTML_NS, "div"); presetListFooter.classList.add("footer"); presetListWrapper.appendChild(presetListFooter); this.addPresetInput = this.doc.createElementNS(XHTML_NS, "input"); this.addPresetInput.setAttribute("value", ""); this.addPresetInput.classList.add("devtools-textinput"); this.addPresetInput.setAttribute( "placeholder", L10N.getStr("newPresetPlaceholder") ); presetListFooter.appendChild(this.addPresetInput); this.addPresetButton = this.doc.createElementNS(XHTML_NS, "button"); this.addPresetButton.classList.add("add"); this.addPresetButton.textContent = L10N.getStr("savePresetButton"); presetListFooter.appendChild(this.addPresetButton); this.el.appendChild(content); this._populateFilterSelect(); }, _destroyMarkup() { this._filterItemMarkup.remove(); this.el.remove(); this.el = this.filterList = this._filterItemMarkup = null; this.presetList = this.togglePresets = this.filterSelect = null; this.addPresetButton = null; }, destroy() { this._removeEventListeners(); this._destroyMarkup(); }, /** * Creates