зеркало из https://github.com/mozilla/gecko-dev.git
569 строки
21 KiB
JavaScript
569 строки
21 KiB
JavaScript
/* 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";
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"SelectParentHelper"
|
|
];
|
|
|
|
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
|
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
|
|
|
|
// Maximum number of rows to display in the select dropdown.
|
|
const MAX_ROWS = 20;
|
|
|
|
// Minimum elements required to show select search
|
|
const SEARCH_MINIMUM_ELEMENTS = 40;
|
|
|
|
// Make sure to clear these objects when the popup closes to avoid leaking.
|
|
var currentBrowser = null;
|
|
var currentMenulist = null;
|
|
var selectRect = null;
|
|
|
|
var currentZoom = 1;
|
|
var closedWithEnter = false;
|
|
var customStylingEnabled = Services.prefs.getBoolPref("dom.forms.select.customstyling");
|
|
|
|
var SelectParentHelper = {
|
|
/**
|
|
* `populate` takes the `menulist` element and a list of `items` and generates
|
|
* a popup list of options.
|
|
*
|
|
* If `customStylingEnabled` is set to `true`, the function will alse
|
|
* style the select and its popup trying to prevent the text
|
|
* and background to end up in the same color.
|
|
*
|
|
* All `ua*` variables represent the color values for the default colors
|
|
* for their respective form elements used by the user agent.
|
|
* The `select*` variables represent the color values defined for the
|
|
* particular <select> element.
|
|
*
|
|
* The `customoptionstyling` attribute controls the application of
|
|
* `-moz-appearance` on the elements and is disabled if the element is
|
|
* defining its own background-color.
|
|
*
|
|
* @param {Element} menulist
|
|
* @param {Array<Element>} items
|
|
* @param {Number} selectedIndex
|
|
* @param {Number} zoom
|
|
* @param {String} uaBackgroundColor
|
|
* @param {String} uaColor
|
|
* @param {String} uaSelectBackgroundColor
|
|
* @param {String} uaSelectColor
|
|
* @param {String} selectBackgroundColor
|
|
* @param {String} selectColor
|
|
* @param {String} selectTextShadow
|
|
*/
|
|
populate(menulist, items, selectedIndex, zoom, uaBackgroundColor, uaColor,
|
|
uaSelectBackgroundColor, uaSelectColor, selectBackgroundColor,
|
|
selectColor, selectTextShadow) {
|
|
// Clear the current contents of the popup
|
|
menulist.menupopup.textContent = "";
|
|
let stylesheet = menulist.querySelector("#ContentSelectDropdownStylesheet");
|
|
if (stylesheet) {
|
|
stylesheet.remove();
|
|
}
|
|
|
|
let doc = menulist.ownerDocument;
|
|
let sheet;
|
|
if (customStylingEnabled) {
|
|
stylesheet = doc.createElementNS("http://www.w3.org/1999/xhtml", "style");
|
|
stylesheet.setAttribute("id", "ContentSelectDropdownStylesheet");
|
|
stylesheet.hidden = true;
|
|
stylesheet = menulist.appendChild(stylesheet);
|
|
sheet = stylesheet.sheet;
|
|
}
|
|
|
|
let ruleBody = "";
|
|
let usedSelectBackgroundColor;
|
|
let usedSelectColor;
|
|
let selectBackgroundSet = false;
|
|
|
|
// Some webpages set the <select> backgroundColor to transparent,
|
|
// but they don't intend to change the popup to transparent.
|
|
if (customStylingEnabled &&
|
|
selectBackgroundColor != uaSelectBackgroundColor &&
|
|
selectBackgroundColor != "rgba(0, 0, 0, 0)") {
|
|
ruleBody = `background-image: linear-gradient(${selectBackgroundColor}, ${selectBackgroundColor});`;
|
|
usedSelectBackgroundColor = selectBackgroundColor;
|
|
selectBackgroundSet = true;
|
|
} else {
|
|
usedSelectBackgroundColor = uaSelectBackgroundColor;
|
|
}
|
|
|
|
if (customStylingEnabled &&
|
|
selectColor != uaSelectColor &&
|
|
selectColor != usedSelectBackgroundColor) {
|
|
ruleBody += `color: ${selectColor};`;
|
|
usedSelectColor = selectColor;
|
|
} else {
|
|
usedSelectColor = uaColor;
|
|
}
|
|
|
|
if (customStylingEnabled &&
|
|
selectTextShadow != "none") {
|
|
ruleBody += `text-shadow: ${selectTextShadow};`;
|
|
sheet.insertRule(`#ContentSelectDropdown > menupopup > [_moz-menuactive="true"] {
|
|
text-shadow: none;
|
|
}`, 0);
|
|
}
|
|
|
|
if (ruleBody) {
|
|
sheet.insertRule(`#ContentSelectDropdown > menupopup {
|
|
${ruleBody}
|
|
}`, 0);
|
|
sheet.insertRule(`#ContentSelectDropdown > menupopup > :not([_moz-menuactive="true"]) {
|
|
color: inherit;
|
|
}`, 0);
|
|
}
|
|
|
|
// We only set the `customoptionstyling` if the background has been
|
|
// manually set. This prevents the overlap between moz-appearance and
|
|
// background-color. `color` and `text-shadow` do not interfere with it.
|
|
if (selectBackgroundSet) {
|
|
menulist.menupopup.setAttribute("customoptionstyling", "true");
|
|
} else {
|
|
menulist.menupopup.removeAttribute("customoptionstyling");
|
|
}
|
|
|
|
currentZoom = zoom;
|
|
currentMenulist = menulist;
|
|
populateChildren(menulist, items, selectedIndex, zoom,
|
|
usedSelectBackgroundColor, usedSelectColor, selectTextShadow, selectBackgroundSet, sheet);
|
|
},
|
|
|
|
open(browser, menulist, rect, isOpenedViaTouch) {
|
|
menulist.hidden = false;
|
|
currentBrowser = browser;
|
|
closedWithEnter = false;
|
|
selectRect = rect;
|
|
this._registerListeners(browser, menulist.menupopup);
|
|
|
|
let win = browser.ownerGlobal;
|
|
|
|
// Set the maximum height to show exactly MAX_ROWS items.
|
|
let menupopup = menulist.menupopup;
|
|
let firstItem = menupopup.firstChild;
|
|
while (firstItem && firstItem.hidden) {
|
|
firstItem = firstItem.nextSibling;
|
|
}
|
|
|
|
if (firstItem) {
|
|
let itemHeight = firstItem.getBoundingClientRect().height;
|
|
|
|
// Include the padding and border on the popup.
|
|
let cs = win.getComputedStyle(menupopup);
|
|
let bpHeight = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) +
|
|
parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
|
|
menupopup.style.maxHeight = (itemHeight * MAX_ROWS + bpHeight) + "px";
|
|
}
|
|
|
|
menupopup.classList.toggle("isOpenedViaTouch", isOpenedViaTouch);
|
|
|
|
if (browser.getAttribute("selectmenuconstrained") != "false") {
|
|
let constraintRect = browser.getBoundingClientRect();
|
|
constraintRect = new win.DOMRect(constraintRect.left + win.mozInnerScreenX,
|
|
constraintRect.top + win.mozInnerScreenY,
|
|
constraintRect.width, constraintRect.height);
|
|
menupopup.setConstraintRect(constraintRect);
|
|
} else {
|
|
menupopup.setConstraintRect(new win.DOMRect(0, 0, 0, 0));
|
|
}
|
|
menupopup.openPopupAtScreenRect(AppConstants.platform == "macosx" ? "selection" : "after_start", rect.left, rect.top, rect.width, rect.height, false, false);
|
|
},
|
|
|
|
hide(menulist, browser) {
|
|
if (currentBrowser == browser) {
|
|
menulist.menupopup.hidePopup();
|
|
}
|
|
},
|
|
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "mouseup":
|
|
function inRect(rect, x, y) {
|
|
return x >= rect.left && x <= rect.left + rect.width && y >= rect.top && y <= rect.top + rect.height;
|
|
}
|
|
|
|
let x = event.screenX, y = event.screenY;
|
|
let onAnchor = !inRect(currentMenulist.menupopup.getOuterScreenRect(), x, y) &&
|
|
inRect(selectRect, x, y) && currentMenulist.menupopup.state == "open";
|
|
currentBrowser.messageManager.sendAsyncMessage("Forms:MouseUp", { onAnchor });
|
|
break;
|
|
|
|
case "mouseover":
|
|
currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOver", {});
|
|
break;
|
|
|
|
case "mouseout":
|
|
currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOut", {});
|
|
break;
|
|
|
|
case "keydown":
|
|
if (event.keyCode == event.DOM_VK_RETURN) {
|
|
closedWithEnter = true;
|
|
}
|
|
break;
|
|
|
|
case "command":
|
|
if (event.target.hasAttribute("value")) {
|
|
currentBrowser.messageManager.sendAsyncMessage("Forms:SelectDropDownItem", {
|
|
value: event.target.value,
|
|
closedWithEnter
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "fullscreen":
|
|
if (currentMenulist) {
|
|
currentMenulist.menupopup.hidePopup();
|
|
}
|
|
break;
|
|
|
|
case "popuphidden":
|
|
currentBrowser.messageManager.sendAsyncMessage("Forms:DismissedDropDown", {});
|
|
let popup = event.target;
|
|
this._unregisterListeners(currentBrowser, popup);
|
|
popup.parentNode.hidden = true;
|
|
currentBrowser = null;
|
|
currentMenulist = null;
|
|
selectRect = null;
|
|
currentZoom = 1;
|
|
break;
|
|
}
|
|
},
|
|
|
|
receiveMessage(msg) {
|
|
if (!currentBrowser) {
|
|
return;
|
|
}
|
|
|
|
if (msg.name == "Forms:UpdateDropDown") {
|
|
// Sanity check - we'd better know what the currently
|
|
// opened menulist is, and what browser it belongs to...
|
|
if (!currentMenulist) {
|
|
return;
|
|
}
|
|
|
|
let scrollBox = currentMenulist.menupopup.scrollBox;
|
|
let scrollTop = scrollBox.scrollTop;
|
|
|
|
let options = msg.data.options;
|
|
let selectedIndex = msg.data.selectedIndex;
|
|
let uaBackgroundColor = msg.data.uaBackgroundColor;
|
|
let uaColor = msg.data.uaColor;
|
|
let uaSelectBackgroundColor = msg.data.uaSelectBackgroundColor;
|
|
let uaSelectColor = msg.data.uaSelectColor;
|
|
let selectBackgroundColor = msg.data.selectBackgroundColor;
|
|
let selectColor = msg.data.selectColor;
|
|
let selectTextShadow = msg.data.selectTextShadow;
|
|
this.populate(currentMenulist, options, selectedIndex,
|
|
currentZoom, uaBackgroundColor, uaColor,
|
|
uaSelectBackgroundColor, uaSelectColor,
|
|
selectBackgroundColor, selectColor, selectTextShadow);
|
|
|
|
// Restore scroll position to what it was prior to the update.
|
|
scrollBox.scrollTop = scrollTop;
|
|
} else if (msg.name == "Forms:BlurDropDown-Ping") {
|
|
currentBrowser.messageManager.sendAsyncMessage("Forms:BlurDropDown-Pong", {});
|
|
}
|
|
},
|
|
|
|
_registerListeners(browser, popup) {
|
|
popup.addEventListener("command", this);
|
|
popup.addEventListener("popuphidden", this);
|
|
popup.addEventListener("mouseover", this);
|
|
popup.addEventListener("mouseout", this);
|
|
browser.ownerGlobal.addEventListener("mouseup", this, true);
|
|
browser.ownerGlobal.addEventListener("keydown", this, true);
|
|
browser.ownerGlobal.addEventListener("fullscreen", this, true);
|
|
browser.messageManager.addMessageListener("Forms:UpdateDropDown", this);
|
|
browser.messageManager.addMessageListener("Forms:BlurDropDown-Ping", this);
|
|
},
|
|
|
|
_unregisterListeners(browser, popup) {
|
|
popup.removeEventListener("command", this);
|
|
popup.removeEventListener("popuphidden", this);
|
|
popup.removeEventListener("mouseover", this);
|
|
popup.removeEventListener("mouseout", this);
|
|
browser.ownerGlobal.removeEventListener("mouseup", this, true);
|
|
browser.ownerGlobal.removeEventListener("keydown", this, true);
|
|
browser.ownerGlobal.removeEventListener("fullscreen", this, true);
|
|
browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this);
|
|
browser.messageManager.removeMessageListener("Forms:BlurDropDown-Ping", this);
|
|
},
|
|
|
|
};
|
|
|
|
/**
|
|
* `populateChildren` creates all <menuitem> elements for the popup menu
|
|
* based on the list of <option> elements from the <select> element.
|
|
*
|
|
* It attempts to intelligently add per-item CSS rules if the single
|
|
* item values differ from the parent menu values and attempting to avoid
|
|
* ending up with the same color of text and background.
|
|
*
|
|
* @param {Element} menulist
|
|
* @param {Array<Element>} options
|
|
* @param {Number} selectedIndex
|
|
* @param {Number} zoom
|
|
* @param {String} usedSelectBackgroundColor
|
|
* @param {String} usedSelectColor
|
|
* @param {String} selectTextShadow
|
|
* @param {String} selectBackgroundSet
|
|
* @param {CSSStyleSheet} sheet
|
|
* @param {Element} parentElement
|
|
* @param {Boolean} isGroupDisabled
|
|
* @param {Number} adjustedTextSize
|
|
* @param {Boolean} addSearch
|
|
* @param {Number} nthChildIndex
|
|
* @returns {Number}
|
|
*/
|
|
function populateChildren(menulist, options, selectedIndex, zoom,
|
|
usedSelectBackgroundColor, usedSelectColor,
|
|
selectTextShadow, selectBackgroundSet, sheet,
|
|
parentElement = null, isGroupDisabled = false,
|
|
adjustedTextSize = -1, addSearch = true,
|
|
nthChildIndex = 1) {
|
|
let element = menulist.menupopup;
|
|
let win = element.ownerGlobal;
|
|
|
|
// -1 just means we haven't calculated it yet. When we recurse through this function
|
|
// we will pass in adjustedTextSize to save on recalculations.
|
|
if (adjustedTextSize == -1) {
|
|
// Grab the computed text size and multiply it by the remote browser's fullZoom to ensure
|
|
// the popup's text size is matched with the content's. We can't just apply a CSS transform
|
|
// here as the popup's preferred size is calculated pre-transform.
|
|
let textSize = win.getComputedStyle(element).getPropertyValue("font-size");
|
|
adjustedTextSize = (zoom * parseFloat(textSize, 10)) + "px";
|
|
}
|
|
|
|
for (let option of options) {
|
|
let isOptGroup = (option.tagName == "OPTGROUP");
|
|
let item = element.ownerDocument.createElement(isOptGroup ? "menucaption" : "menuitem");
|
|
|
|
item.setAttribute("label", option.textContent);
|
|
item.style.direction = option.textDirection;
|
|
item.style.fontSize = adjustedTextSize;
|
|
item.hidden = option.display == "none" || (parentElement && parentElement.hidden);
|
|
// Keep track of which options are hidden by page content, so we can avoid showing
|
|
// them on search input
|
|
item.hiddenByContent = item.hidden;
|
|
item.setAttribute("tooltiptext", option.tooltip);
|
|
|
|
let ruleBody = "";
|
|
let usedBackgroundColor;
|
|
let optionBackgroundSet = false;
|
|
|
|
if (customStylingEnabled &&
|
|
option.backgroundColor &&
|
|
option.backgroundColor != "rgba(0, 0, 0, 0)" &&
|
|
option.backgroundColor != usedSelectBackgroundColor) {
|
|
ruleBody = `background-color: ${option.backgroundColor};`;
|
|
usedBackgroundColor = option.backgroundColor;
|
|
optionBackgroundSet = true;
|
|
} else {
|
|
usedBackgroundColor = usedSelectBackgroundColor;
|
|
}
|
|
|
|
if (customStylingEnabled &&
|
|
option.color &&
|
|
option.color != usedBackgroundColor &&
|
|
option.color != usedSelectColor) {
|
|
ruleBody += `color: ${option.color};`;
|
|
}
|
|
|
|
if (customStylingEnabled &&
|
|
option.textShadow &&
|
|
option.textShadow != selectTextShadow) {
|
|
ruleBody += `text-shadow: ${option.textShadow};`;
|
|
}
|
|
|
|
if (ruleBody) {
|
|
sheet.insertRule(`#ContentSelectDropdown > menupopup > :nth-child(${nthChildIndex}):not([_moz-menuactive="true"]) {
|
|
${ruleBody}
|
|
}`, 0);
|
|
|
|
if (option.textShadow && option.textShadow != selectTextShadow) {
|
|
// Need to explicitly disable the possibly inherited
|
|
// text-shadow rule when _moz-menuactive=true since
|
|
// _moz-menuactive=true disables custom option styling.
|
|
sheet.insertRule(`#ContentSelectDropdown > menupopup > :nth-child(${nthChildIndex})[_moz-menuactive="true"] {
|
|
text-shadow: none;
|
|
}`, 0);
|
|
}
|
|
}
|
|
|
|
if (customStylingEnabled &&
|
|
(optionBackgroundSet || selectBackgroundSet)) {
|
|
item.setAttribute("customoptionstyling", "true");
|
|
} else {
|
|
item.removeAttribute("customoptionstyling");
|
|
}
|
|
|
|
element.appendChild(item);
|
|
nthChildIndex++;
|
|
|
|
// A disabled optgroup disables all of its child options.
|
|
let isDisabled = isGroupDisabled || option.disabled;
|
|
if (isDisabled) {
|
|
item.setAttribute("disabled", "true");
|
|
}
|
|
|
|
if (isOptGroup) {
|
|
nthChildIndex =
|
|
populateChildren(menulist, option.children, selectedIndex, zoom,
|
|
usedSelectBackgroundColor, usedSelectColor,
|
|
selectTextShadow, selectBackgroundSet, sheet,
|
|
item, isDisabled, adjustedTextSize, false, nthChildIndex);
|
|
} else {
|
|
if (option.index == selectedIndex) {
|
|
// We expect the parent element of the popup to be a <xul:menulist> that
|
|
// has the popuponly attribute set to "true". This is necessary in order
|
|
// for a <xul:menupopup> to act like a proper <html:select> dropdown, as
|
|
// the <xul:menulist> does things like remember state and set the
|
|
// _moz-menuactive attribute on the selected <xul:menuitem>.
|
|
menulist.selectedItem = item;
|
|
|
|
// It's hack time. In the event that we've re-populated the menulist due
|
|
// to a mutation in the <select> in content, that means that the -moz_activemenu
|
|
// may have been removed from the selected item. Since that's normally only
|
|
// set for the initially selected on popupshowing for the menulist, and we
|
|
// don't want to close and re-open the popup, we manually set it here.
|
|
menulist.menuBoxObject.activeChild = item;
|
|
}
|
|
|
|
item.setAttribute("value", option.index);
|
|
|
|
if (parentElement) {
|
|
item.classList.add("contentSelectDropdown-ingroup");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if search pref is enabled, if this is the first time iterating through
|
|
// the dropdown, and if the list is long enough for a search element to be added.
|
|
if (Services.prefs.getBoolPref("dom.forms.selectSearch") && addSearch
|
|
&& element.childElementCount > SEARCH_MINIMUM_ELEMENTS) {
|
|
|
|
// Add a search text field as the first element of the dropdown
|
|
let searchbox = element.ownerDocument.createElement("textbox");
|
|
searchbox.setAttribute("type", "search");
|
|
searchbox.addEventListener("input", onSearchInput);
|
|
searchbox.addEventListener("focus", onSearchFocus);
|
|
searchbox.addEventListener("blur", onSearchBlur);
|
|
searchbox.addEventListener("command", onSearchInput);
|
|
|
|
// Handle special keys for exiting search
|
|
searchbox.addEventListener("keydown", function(event) {
|
|
if (event.defaultPrevented) {
|
|
return;
|
|
}
|
|
switch (event.key) {
|
|
case "Escape":
|
|
searchbox.parentElement.hidePopup();
|
|
break;
|
|
case "ArrowDown":
|
|
case "Enter":
|
|
case "Tab":
|
|
searchbox.blur();
|
|
if (searchbox.nextSibling.localName == "menuitem" &&
|
|
!searchbox.nextSibling.hidden) {
|
|
menulist.menuBoxObject.activeChild = searchbox.nextSibling;
|
|
} else {
|
|
var currentOption = searchbox.nextSibling;
|
|
while (currentOption && (currentOption.localName != "menuitem" ||
|
|
currentOption.hidden)) {
|
|
currentOption = currentOption.nextSibling;
|
|
}
|
|
if (currentOption) {
|
|
menulist.menuBoxObject.activeChild = currentOption;
|
|
} else {
|
|
searchbox.focus();
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
}, true);
|
|
|
|
element.insertBefore(searchbox, element.childNodes[0]);
|
|
}
|
|
|
|
return nthChildIndex;
|
|
}
|
|
|
|
function onSearchInput() {
|
|
let searchObj = this;
|
|
|
|
// Get input from search field, set to all lower case for comparison
|
|
let input = searchObj.value.toLowerCase();
|
|
// Get all items in dropdown (could be options or optgroups)
|
|
let menupopup = searchObj.parentElement;
|
|
let menuItems = menupopup.querySelectorAll("menuitem, menucaption");
|
|
|
|
// Flag used to detect any group headers with no visible options.
|
|
// These group headers should be hidden.
|
|
let allHidden = true;
|
|
// Keep a reference to the previous group header (menucaption) to go back
|
|
// and set to hidden if all options within are hidden.
|
|
let prevCaption = null;
|
|
|
|
for (let currentItem of menuItems) {
|
|
// Make sure we don't show any options that were hidden by page content
|
|
if (!currentItem.hiddenByContent) {
|
|
// Get label and tooltip (title) from option and change to
|
|
// lower case for comparison
|
|
let itemLabel = currentItem.getAttribute("label").toLowerCase();
|
|
let itemTooltip = currentItem.getAttribute("title").toLowerCase();
|
|
|
|
// If search input is empty, all options should be shown
|
|
if (!input) {
|
|
currentItem.hidden = false;
|
|
} else if (currentItem.localName == "menucaption") {
|
|
if (prevCaption != null) {
|
|
prevCaption.hidden = allHidden;
|
|
}
|
|
prevCaption = currentItem;
|
|
allHidden = true;
|
|
} else {
|
|
if (!currentItem.classList.contains("contentSelectDropdown-ingroup") &&
|
|
currentItem.previousSibling.classList.contains("contentSelectDropdown-ingroup")) {
|
|
if (prevCaption != null) {
|
|
prevCaption.hidden = allHidden;
|
|
}
|
|
prevCaption = null;
|
|
allHidden = true;
|
|
}
|
|
if (itemLabel.includes(input) || itemTooltip.includes(input)) {
|
|
currentItem.hidden = false;
|
|
allHidden = false;
|
|
} else {
|
|
currentItem.hidden = true;
|
|
}
|
|
}
|
|
if (prevCaption != null) {
|
|
prevCaption.hidden = allHidden;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function onSearchFocus() {
|
|
let searchObj = this;
|
|
let menupopup = searchObj.parentElement;
|
|
menupopup.parentElement.menuBoxObject.activeChild = null;
|
|
menupopup.setAttribute("ignorekeys", "true");
|
|
currentBrowser.messageManager.sendAsyncMessage("Forms:SearchFocused", {});
|
|
}
|
|
|
|
function onSearchBlur() {
|
|
let searchObj = this;
|
|
let menupopup = searchObj.parentElement;
|
|
menupopup.setAttribute("ignorekeys", "false");
|
|
}
|