/* 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",
...InspectorUtils.getCSSWideKeywords(),
]);
/**
* 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
*/
class CSSFilterEditorWidget extends EventEmitter {
/**
* @param {Node} el
* The widget container.
* @param {string} value
* CSS filter value
*/
constructor(el, value = "") {
super();
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();
this.filters = [];
this.setCssValue(value);
this.renderPresets();
}
_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