Drag & DropZones +

[userChromeJS] 選択した文字列などをドラッグし、ページ上に表示される半透明の枠内にドロップすることで、Web検索などを実行する / Drag selected character strings or image and drop to the semitransparent box displayed on web page to open search result.

À partir de 2014-06-24. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name           Drag & DropZones +
// @namespace      https://userscripts.org/users/347021
// @version        2.2.0
// @description    [userChromeJS] 選択した文字列などをドラッグし、ページ上に表示される半透明の枠内にドロップすることで、Web検索などを実行する / Drag selected character strings or image and drop to the semitransparent box displayed on web page to open search result.
// @include        main
// @author         100の人 https://greatest.deepsurf.us/users/137-100%E3%81%AE%E4%BA%BA
// @contributor    HADAA
// @license        Creative Commons Attribution 4.0 International Public License; http://creativecommons.org/licenses/by/4.0/
// ==/UserScript==

(function () {
'use strict';

/**
 * L10N
 * @type {LocalizedTexts}
 */
let localizedTexts = {
	'en': {
		'Drag & DropZones +': 'Drag & DropZones +',

		'検索エンジン名': 'Search engine name',
		'他と重複しないエンジン名を入力してください。': 'Please input search engine names without repetition.',
		'URL・POSTパラメータ': 'URL, POST parameters',
		'POSTパラメータの設定': 'Setting POST parameters',
		'名前': 'name',
		'値': 'value',
		'メソッド': 'Method',
		'データの種類': 'Data type',
		'文字列': 'String',
		'画像': 'Image',
		'音声': 'Audio',
		'文字符号化方式': 'Character encoding scheme',
		'キャンセル': 'Cancel',
		'OK': 'OK',

		'行を追加': 'Add row',
		'行を削除': 'Delete row',
		'上に新しい行を挿入': 'Insert new row above',
		'下に新しい行を挿入': 'Insert new row below',
		'上に新しい行を挿入します。': 'Insert new row above.',
		'下に新しい行を挿入します。': 'Insert new row below.',
		'上の行に移動します。': 'Move focus to above row.',
		'下の行に移動します。': 'Move focus to below row.',
		'行をドラッグ & ドロップで、順番を変更できます。': 'Drag and drop row to change order.',

		'アイコンボタンのポップアップメニューから、アイコンを変更できます。検索窓のエンジンのアイコンは変更できません。':
			'You can change icon via the icon button pop-up menu. You cannot modify the search engine icon in the search bar.',
		'アイコンを変更': 'Modify icon',
		'元のアイコンに戻す': 'Restore to default icon',
		'ローカルファイルからアイコンを設定': 'Set icon from local file',
		'画像ファイルを選択してください。': 'Please choose image file.',
		'Webページ、または画像ファイルのURLからアイコンを設定': 'Set icon by URL of Web page or image file',
		'Webページ、または画像ファイルのURLを入力してください。': 'Please input URL of Web page or image file.',
		'アイコンの設定に失敗しました。約 %s KiB までの画像を設定できます。': 'Setting icon failed. Image up to about %s KiB can be set.',
		'クリップボードのURL、または画像データからアイコンを設定': 'Set icon by URL or image data on clipboard',
		'クリップボードからデータを取得できませんでした。': 'Could not get data from clipboard.',
		'指定されたURLに接続できませんでした。': 'Connection to specified URL failed.',
		'http:// などで始まるURLを入力してください。': 'Specify URL starting with "http://" etc.',
		'アイコンを取得できませんでした。WebページのURLであれば、一度ブラウザでページを表示してみてください。':
			'Could not get icon. If you have input a URL of a Web page, please open that page in your browser once.',
		'アイコンを一括取得': 'Collectively get icons',
		'アイコン未取得の検索エンジンについて、URLを基にアイコンを取得します。アイコンボタンのポップアップメニューの「元のアイコンに戻す」から、個別に取得することもできます。':
			'Get icons from URL for search engines without an icon. You can choose "Restore to default icon" from the icon button pop-up menu to get individual ones.',
		'アイコンの取得が完了しました。': 'Completed getting icons.',

		'検索窓のエンジンの追加': 'Add engine in Search Bar',
		'選択してください': 'Choose',
		'検索結果を開く場所': 'Where to open search result',
		'現在のタブ。Ctrl、Shiftキーを押していれば、それぞれ新しいタブ、ウィンドウ': 'Current tab. If Ctrl or Shift key is pressed, it will open in a new tab or window, respectively',
		'新しいタブ': 'New tab',
		'新しいウィンドウ': 'New window',
		'検索窓に新しい検索エンジンが追加されたとき、自動的にドロップゾーンとしても追加する。':
			'When new engine is added to Search Bar, a dropzone will also be automatically created.',
		'テキスト入力欄のキーボードショートカット': 'Keyboard Shortcuts in input box',
		'または': 'or',

		'インポートとエクスポート': 'Import and export',
		'インポート': 'Import',
		'現在の設定をすべて削除し、XMLファイルから設定をインポートします。ブラウザの検索エンジンサービスに同名の検索エンジンが存在する場合は、そちらを優先します。':
			'Delete all settings, then import settings from XML file. If the browser search service with the same name already exists, the existing one takes priority.',
		'%s からのインポートに失敗しました。': 'Import from "%s" failed.', // %sはファイル名
		'XMLパースエラーです。': 'XML parse error occured.',
		'検索エンジンが一つも見つかりませんでした。': 'Not even one search engine was found.',
		'%s からのインポートが完了しました。': 'Import from "%s" completed.', // %sはファイル名
		'エクスポート': 'Export',
		'現在の設定をファイルへエクスポートします。保存していない設定は反映されません。': 'Export current settings to file. Not yet saved settings are not reflected.',
		'%s へ設定をエクスポートしました。': 'Export to "%s" completed.', // %sはファイルパス
		'追加インポート': 'Additional import',
		'XMLファイルから検索エンジンを追加します。同名の検索エンジンがすでに存在する場合は上書きします。':
			'Add search engine from XML file. If a search engine with the same name already exists, overwrite it.',
		'インポートした設定を保存するには、「OK」ボタンをクリックしてください。': 'Click "OK" button to save import data.',
		'JSON文字列から追加インポート': 'Additional import from JSON string',
		'本スクリプトのバージョン1でエクスポートしたJSON文字列から、検索エンジンを追加します。':
			'Add search engine from JSON string exported by version 1 of this script.',
		'JSON文字列を貼り付けてください。': 'Please paste JSON string.',
		'JSON文字列からのインポートに失敗しました。': 'Import from JSON string failed.',
		'JSONパースエラーです。': 'JSON parse error occured.',
		'JSON文字列からのインポートが完了しました。': 'Import form JSON string completed.',

		'その他': 'Others',
		'設定を初期化': 'Initialize the settings',
		'すべての設定を削除し、初回起動時の状態に戻します。': 'Delete all settings, then restore to first starting state.',
		'本当に、『%s』のすべての設定を削除してもよろしいですか?':
			'Are you sure you want to delete all settings of "%s" ?', // %sは当スクリプト名
		'設定の初期化が完了しました。': 'Settings initialization completed.',
		'すべての設定を削除': 'Delete all settings',
		'すべての設定を削除し、スクリプトを停止します。': 'Delete all settings, and stop this script.',
		'設定の削除が完了しました。当スクリプト自体を削除しなければ、次回のブラウザ起動時にまた設定が作成されます。':
			'Completed deleting all settings. If you don\'t delete this script, settings will be created again when you start your browser next time.',

		'Google 画像で検索': 'Google search by image',
	},
};



Cu.import('resource://gre/modules/FileUtils.jsm');

let ScriptableUnicodeConverter = Cc['@mozilla.org/intl/scriptableunicodeconverter'].createInstance(Ci.nsIScriptableUnicodeConverter);
let NativeJSON = Cc['@mozilla.org/dom/json;1'].createInstance(Ci.nsIJSON);
let DOMSerializer = Cc['@mozilla.org/xmlextras/xmlserializer;1'].createInstance(Ci.nsIDOMSerializer);

let TextToSubURI = Cc['@mozilla.org/intl/texttosuburi;1'].getService(Ci.nsITextToSubURI);
let MIMEService = Cc['@mozilla.org/mime;1'].getService(Ci.nsIMIMEService);
let FaviconService = Cc['@mozilla.org/browser/favicon-service;1'].getService(Ci.nsIFaviconService);
let ImgTools = Cc['@mozilla.org/image/tools;1'].getService(Ci.imgITools);

let StringInputStream = Components.Constructor('@mozilla.org/io/string-input-stream;1', 'nsIStringInputStream', 'setData');
let MultiplexInputStream = Components.Constructor('@mozilla.org/io/multiplex-input-stream;1', 'nsIMultiplexInputStream');
let MIMEInputStream = Components.Constructor('@mozilla.org/network/mime-input-stream;1', 'nsIMIMEInputStream');
let ArrayBufferInputStream = Components.Constructor('@mozilla.org/io/arraybuffer-input-stream;1', 'nsIArrayBufferInputStream', 'setData');
let FileInputStream = Components.Constructor('@mozilla.org/network/file-input-stream;1', 'nsIFileInputStream', 'init');
let BinaryInputStream = Components.Constructor('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
let FilePicker = Components.Constructor('@mozilla.org/filepicker;1', 'nsIFilePicker', 'init');
let Transferable = Components.Constructor('@mozilla.org/widget/transferable;1', 'nsITransferable', 'addDataFlavor');

let Cr = new Proxy(window.Cr, {
	get: function (target, name, receiver) {
		if (name in target) {
			return target[name];
		} else if (name === 'NS_ERROR_UCONV_NOCONV') {
			return 0x80500001;
		} else {
			return undefined;
		}
	},
});



// i18n
let _, gettext, setlang, setLocalizedTexts;
{
	/**
	 * 翻訳対象文字列 (msgid) の言語。
	 * @constant {string}
	 */
	let ORIGINAL_LOCALE = 'ja';

	/**
	 * クライアントの言語の翻訳リソースが存在しないとき、どの言語に翻訳するか。
	 * @constant {string}
	 */
	let DEFAULT_LOCALE = 'en';

	/**
	 * 以下のような形式の翻訳リソース。
	 * {
	 *     'IETF言語タグ': {
	 *         '翻訳前 (msgid)': '翻訳後 (msgstr)',
	 *         ……
	 *     },
	 *     ……
	 * }
	 * @typedef {Object} LocalizedTexts
	 */

	/**
	 * クライアントの言語。{@link setlang}から変更される。
	 * @type {string}
	 * @access private
	 */
	let langtag = 'ja';

	/**
	 * クライアントの言語のlanguage部分。{@link setlang}から変更される。
	 * @type {string}
	 * @access private
	 */
	let language = 'ja';

	/**
	 * 翻訳リソース。{@link setLocalizedTexts}から変更される。
	 * @type {LocalizedTexts}
	 * @access private
	 */
	let multilingualLocalizedTexts = {};
	multilingualLocalizedTexts[ORIGINAL_LOCALE] = {};

	/**
	 * テキストをクライアントの言語に変換する。
	 * @param {string} message - 翻訳前。
	 * @returns {string} 翻訳後。
	 */
	_ = gettext = function (message) {
		// クライアントの言語の翻訳リソースが存在すれば、それを返す
		return langtag in multilingualLocalizedTexts && multilingualLocalizedTexts[langtag][message]
				// 地域下位タグを取り除いた言語タグの翻訳リソースが存在すれば、それを返す
				|| language in multilingualLocalizedTexts && multilingualLocalizedTexts[language][message]
				// デフォルト言語の翻訳リソースが存在すれば、それを返す
				|| DEFAULT_LOCALE in multilingualLocalizedTexts && multilingualLocalizedTexts[DEFAULT_LOCALE][message]
				// そのまま返す
				|| message;
	};

	/**
	 * {@link gettext}から参照されるクライアントの言語を設定する。
	 * @param {string} lang - IETF言語タグ。(「language」と「language-REGION」にのみ対応)
	 */
	setlang = function (lang) {
		lang = lang.split('-', 2);
		language = lang[0].toLowerCase();
		langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');
	};

	/**
	 * {@link gettext}から参照される翻訳リソースを追加する。
	 * @param {LocalizedTexts} localizedTexts
	 */
	setLocalizedTexts = function (localizedTexts) {
		var localizedText, lang, language, langtag, msgid;
		for (lang in localizedTexts) {
			localizedText = localizedTexts[lang];
			lang = lang.split('-');
			language = lang[0].toLowerCase();
			langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');

			if (langtag in multilingualLocalizedTexts) {
				// すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば上書き)
				for (msgid in localizedText) {
					multilingualLocalizedTexts[langtag][msgid] = localizedText[msgid];
				}
			} else {
				multilingualLocalizedTexts[langtag] = localizedText;
			}

			if (language !== langtag) {
				// 言語タグに地域下位タグが含まれていれば
				// 地域下位タグを取り除いた言語タグも翻訳リソースとして追加する
				if (language in multilingualLocalizedTexts) {
					// すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば無視)
					for (msgid in localizedText) {
						if (!(msgid in multilingualLocalizedTexts[language])) {
							multilingualLocalizedTexts[language][msgid] = localizedText[msgid];
						}
					}
				} else {
					multilingualLocalizedTexts[language] = localizedText;
				}
			}

			// msgidの言語の翻訳リソースを生成
			for (msgid in localizedText) {
				multilingualLocalizedTexts[ORIGINAL_LOCALE][msgid] = msgid;
			}
		}
	};
}

setLocalizedTexts(localizedTexts);

setlang(window.navigator.language);



/**
 * スクリプトの中核。
 */
let DragAndDropZonesPlus = {
	/**
	 * id属性値などに利用する識別子。
	 * @constant {string}
	 */
	ID: 'drag-and-drop-search-347021',

	/**
	 * スクリプト名。
	 * @constant {string}
	 */
	NAME: _('Drag & DropZones +'),

	/**
	 * ウィンドウが開いたときに実行する処理。
	 */
	main: function () {
		// ドロップゾーンの作成
		let earliestWindow = FUELUtils.getChromeWindow(Application.windows[0]);
		if (earliestWindow === window) {
			// ブラウザ起動時
			// 検索エンジンサービスの初期化を待機
			Services.search.init(function () {
				if (SearchUtils.getDropzoneLength() < 1) {
					// 検索エンジンが1つも登録されていなければ(初回起動なら)、検索窓のエンジンを登録する
					SearchUtils.initializeDropzones();
				}

				DropzoneUtils.create();
				DropzoneUtils.setEventListeners();
			});

			SettingsScreen.addToMenu();

			BrowserSearchEngineModifiedObserver.init();
		} else {
			// 新しいウィンドウを開いたとき
			let originalWrapper = earliestWindow.document.getElementById(DragAndDropZonesPlus.ID);
			if (!originalWrapper) {
				// 最初に開かれたウィンドウでスクリプトが実行されていなければ、終了
				return;
			}
			DropzoneUtils.wrapper = originalWrapper.cloneNode(true);
			let appContent = document.getElementById('appcontent');
			appContent.insertBefore(DropzoneUtils.wrapper, appContent.firstChild);
			DropzoneUtils.setEventListeners();
			DropzoneUtils.resetDropzones(true);
			SettingsScreen.addToMenu();
		}

		ObserverUtils.init(this.ID);
		UninstallObserver.init();
	},

	/**
	 * 設定を初期化する。
	 */
	initialize: function () {
		gPrefService.getBranch(SettingsUtils.ROOT_BRANCH_NAME).deleteBranch('');
		SearchUtils.initializeDropzones();
		DropzoneUtils.update();
	},

	/**
	 * 設定を削除し、スクリプトを停止する。
	 */
	uninstall: function () {
		gPrefService.getBranch(SettingsUtils.ROOT_BRANCH_NAME).deleteBranch('');
		UninstallObserver.notify();
	},
};



/**
 * 一つの検索エンジンを表す。
 * @typedef {Object} SearchEngine
 * @property {number} [index] - prefs.jsに保存されている場合のインデックス。
 * @property {string} [icon] - 検索エンジンを表す16px×16pxのアイコンのDataURL。
 * @property {string} name - 検索エンジン名。
 * @property {boolean} browserSearchEngine - ブラウザの検索エンジンの情報ならtrue。
 * @property {string} url - 検索エンジンに結果をリクエストするときのURL。
 * @property {string} [method] - 検索エンジンが受け入れるHTTPメソッド。GETメソッドかPOSTメソッド。
 * @property {FormDataEntry[]} [params] - 検索エンジンがPOSTメソッドを受け入れる場合のPOSTパラメータ。
 * @property {string} [accept] - 検索エンジンが受け入れるデータの種類。text/*、image/*、audio/*のいずれか。
 * @property {string} [encoding] - 検索エンジンが受け入れる文字コード文字符号化方式。
 */

/**
 * 検索エンジンに関する操作群。
 */
let SearchUtils = {
	/**
	 * XMLパースエラーを示す要素の名前空間。
	 * @constant {string}
	 */
	PARSE_ERROR_NS: 'http://www.mozilla.org/newlayout/xml/parsererror.xml',

	/**
	 * 保存可能なドロップゾーンの最大数。
	 * @constant {number}
	 */
	MAX_DROPZONE_LENGTH: 32,

	/**
	 * prefs.jsからドロップゾーンの検索エンジン数を取得する。(歯抜けインデックスを含む)
	 * @returns {number}
	 */
	getDropzoneLength: function () {
		let branch = gPrefService.getBranch(SettingsUtils.ROOT_BRANCH_NAME + 'engines.');
		let indexes = [];
		for (let prefName of branch.getChildList('')) {
			let [index, property] = prefName.split('.');
			if (property !== undefined && /^(?:0|[1-9][0-9]*)$/.test(index) && index < this.MAX_DROPZONE_LENGTH) {
				indexes.push(index);
			} else {
				// 壊れた設定なら、削除する
				branch.clearUserPref(prefName);
			}
		}
		return indexes.length > 0 ? Math.max.apply(null, indexes) + 1 : 0;
	},

	/**
	 * prefs.jsに検索窓のエンジンを登録する。
	 */
	initializeDropzones: function () {
		let engines = Services.search.getVisibleEngines().map(function (engine) {
			return {
				name: engine.name,
			};
		});

		// POST検索例
		engines.push({
			icon: 'data:image/vnd.microsoft.icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD0hUJK9IVC5/SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQuT0hUJK9IVC5vSFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC5/SFQv/0hUL/9IVC//SFQv/1jU7/+sir//7v5//95df/+9S9//vPtf/3oW7/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/+9jC//3s4f/1lFn/9IVC//SFQv/0iEb//NvH//eibv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//3u5f/5u5b/9IVC//SFQv/0hUL/9IVC//m6lP/707r/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/5vpv//N7M//SIR//0hUL/9IVC//WSV//97OH/+8+0//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//epev/6yKr/+byW//nCoP/+9O7//e3j//WSVv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SHRv/+9vH//OLT//WPUf/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//aeaf/5uZL////+//iwhf/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//m4kf//+/n/96h5//WNT//7zbL/9p9q//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/+9fD/+86z//SFQv/0hUL/96Rx//3r4P/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL//vby//iwhf/0hUL/9IVC//izif//+/j/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//rIqf/5vJf/9IVC//SGRP/95NX/+9a///SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hkT/+bqU//m7lv/84dD///79//rLr//3p3f/9IVC//SFQv/0hUL/9IVC//SFQub0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQub0hUJJ9IVC5vSFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQub0hUJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==',
			name: _('Google 画像で検索'),
			url: 'https://www.google.com/searchbyimage/upload',
			method: 'POST',
			params: [['encoded_image', '{searchTerms}']],
			accept: 'image/*',
			encoding: StringUtils.THE_ENCODING,
		});

		SearchUtils.setEngines(engines);
	},

	/**
	 * 検索エンジン名からブラウザに登録されているエンジンを取得する。
	 * @param {string} name
	 * @returns {?SearchEngine} - encodingプロパティを含まない。
	 */
	getBrowserEngineByName: function (name) {
		let browserEngine = Services.search.getEngineByName(name);
		return browserEngine ? this.convertEngineFromBrowser(browserEngine) : null;
	},

	/**
	 * {@link Ci.nsISearchEngine}を{@link SearchEngine}に変換する。
	 * @param {Ci.nsISearchEngine} browserEngine
	 * @returns {SearchEngine} - encodingプロパティを含まない。
	 */
	convertEngineFromBrowser: function (browserEngine) {
		let engine = {
			browserSearchEngine: true,
			name: browserEngine.name,
			accept: 'text/plain',
		};
		if (browserEngine.iconURI) {
			engine.icon = browserEngine.iconURI.spec;
		}
		let searchTerms = String(Math.random()).replace('.', '');
		let submission = browserEngine.getSubmission(searchTerms);
		if (submission.postData) {
			// POSTメソッドなら
			engine.method = 'POST';
			engine.url = submission.uri.spec;
			let postData = NetUtil.readInputStreamToString(submission.postData, submission.postData.available());
			engine.params = StringUtils.parseXWwwFormUrlencoded(postData.split('\r\n\r\n')[1].replace(searchTerms, '{searchTerms}'));
		} else {
			// GETメソッドなら
			engine.method = 'GET';
			engine.url = submission.uri.spec.replace(new RegExp(searchTerms, 'g'), '{searchTerms}');
		}
		return engine;
	},

	/**
	 * インデックスからprefs.jsに保存されているユーザー定義のエンジンを取得する。
	 * @param {number} index
	 * @returns {?SearchEngine}
	 */
	getCustomEngineByIndex: function (index) {
		let engine = null;
		let branchName = SettingsUtils.ROOT_BRANCH_NAME + 'engines.' + index + '.';
		let name = Application.prefs.getValue(branchName + 'name', null);
		if (name) {
			engine = {
				index: index,
				browserSearchEngine: false,
				name: name,
				url: Application.prefs.getValue(branchName + 'url', ''),
				method: Application.prefs.getValue(branchName + 'method', 'GET'),
				accept: Application.prefs.getValue(branchName + 'accept', 'text/plain'),
				encoding: Application.prefs.getValue(branchName + 'encoding', StringUtils.THE_ENCODING),
			};
			let icon = Application.prefs.getValue(branchName + 'icon', null);
			if (icon) {
				engine.icon = icon;
			}
			if (engine.method === 'POST') {
				// POSTメソッドなら
				let params;
				try {
					params = JSON.parse(Application.prefs.getValue(branchName + 'params', '[]'));
				} catch (e) {
					if (!(e instanceof SyntaxError)) {
						throw e;
					}
				}
				engine.params = [];
				if (Array.isArray(params)) {
					for (let param of params) {
						if (Array.isArray(param)) {
							engine.params.push([param[0] || '', param[1] || '']);
						}
					}
				}
			}
		}
		return engine;
	},

	/**
	 * インデックスからエンジンを取得する。
	 * @param {number} index
	 * @returns {?SearchEngine}
	 */
	getEngineByIndex: function (index) {
		let branchName = SettingsUtils.ROOT_BRANCH_NAME + 'engines.' + index + '.';
		return Application.prefs.getValue(branchName + 'url', null)
				? this.getCustomEngineByIndex(index)
				: this.getBrowserEngineByName(Application.prefs.getValue(branchName + 'name', null));
	},

	/**
	 * prefs.jsに保存されている検索エンジンをすべて取得する。
	 * @returns {SearchEngine[]}
	 */
	getEngines: function () {
		let encodings = this.getBrowserEngineEncodings();
		let engines = [];
		for (let i = 0, l = this.getDropzoneLength(); i < l; i++) {
			let branchName = SettingsUtils.ROOT_BRANCH_NAME + 'engines.' + i + '.';
			if (Application.prefs.getValue(branchName + 'url', null)) {
				// ユーザー定義のエンジンなら
				engines.push(this.getCustomEngineByIndex(i));
			} else {
				// ブラウザのエンジンなら
				let name = Application.prefs.getValue(branchName + 'name', null);
				let engine = this.getBrowserEngineByName(name);
				if (engine) {
					engine.index = i;
					engine.encoding = encodings[name] || StringUtils.THE_ENCODING;
					engines.push(engine);
				}
			}
		}
		return engines;
	},

	/**
	 * ブラウザに登録されている検索エンジンをすべて取得する。
	 * @returns {SearchEngine[]}
	 */
	getBrowserEngines: function () {
		let encodings = this.getBrowserEngineEncodings();
		return Services.search.getVisibleEngines().map(browserEngine => {
			let engine = this.convertEngineFromBrowser(browserEngine);
			engine.encoding = encodings[engine.name] || StringUtils.THE_ENCODING;
			return engine;
		});
	},

	/**
	 * prefs.jsに保存されているユーザー定義のエンジンをすべて取得する。
	 * @returns {SearchEngine[]}
	 */
	getCustomEngines: function () {
		let engines = [];
		for (let i = 0, l = this.getDropzoneLength(); i < l; i++) {
			let branchName = SettingsUtils.ROOT_BRANCH_NAME + 'engines.' + i + '.';
			if (Application.prefs.getValue(branchName + 'url', null)) {
				// ユーザー定義のエンジンなら
				engines.push(this.getCustomEngineByIndex(i));
			}
		}
		return engines;
	},

	/**
	 * prefs.jsに保存されている検索エンジンをすべて削除し、指定したエンジンリストと置き換える。
	 * @param {SearchEngine[]} engines
	 */
	setEngines: function (engines) {
		let oldEngineLength = this.getDropzoneLength();
		engines.forEach((engine, index) => {
			this.setEngine(index, engine);
		});
		let branch = gPrefService.getBranch(SettingsUtils.ROOT_BRANCH_NAME + 'engines.');
		for (let i = engines.length; i < oldEngineLength; i++) {
			branch.deleteBranch(i + '.');
		}
	},

	/**
	 * prefs.jsのブランチの指定した位置に検索エンジンを追加する。
	 * @param {number} index
	 * @param {SearchEngine} engine
	 */
	setEngine: function (index, engine) {
		// 古い設定の削除
		gPrefService.getBranch(SettingsUtils.ROOT_BRANCH_NAME + 'engines.').deleteBranch(index + '.');

		let branchName = SettingsUtils.ROOT_BRANCH_NAME + 'engines.' + index + '.';

		Application.prefs.setValue(branchName + 'name', engine.name);

		if (!Services.search.getEngineByName(engine.name)) {
			// 同名の検索エンジンがブラウザに存在しなければ
			if (engine.icon) {
				Application.prefs.setValue(branchName + 'icon', engine.icon);
			}
			Application.prefs.setValue(branchName + 'url', engine.url);
			if (engine.method !== 'GET') {
				// POSTメソッドなら
				Application.prefs.setValue(branchName + 'method', engine.method);
				Application.prefs.setValue(branchName + 'params', JSON.stringify(engine.params));
				if (engine.accept !== 'text/plain') {
					Application.prefs.setValue(branchName + 'accept', engine.accept);
				}
			}
			if (engine.encoding !== StringUtils.THE_ENCODING) {
				Application.prefs.setValue(branchName + 'encoding', engine.encoding);
			}
		}
	},

	/**
	 * 長すぎる値を切り詰める。
	 * @param {SearchEngine} engine
	 * @returns {SearchEngine}
	 */
	trimValues: function (engine) {
		for (let name in engine) {
			switch (name) {
				case 'icon':
					delete engine[name];
					break;
				case 'params':
					while (JSON.stringify(engine[name]) > SettingsUtils.MAX_PREFERENCE_VALUE_LENGTH) {
						engine[name].pop();
					}
					break;
				default:
					if (typeof engine[name] === 'string' && engine[name] > SettingsUtils.MAX_PREFERENCE_VALUE_LENGTH) {
						engine[name] = engine[name].substr(0, SettingsUtils.MAX_PREFERENCE_VALUE_LENGTH);
					}
			}
		}
		return engine;
	},

	/**
	 * ファイル選択ダイアログを表示し、選択されたXMLファイルの検索エンジンを返す。
	 * ファイルが選択されなかった場合は何もしない。
	 * @param {Window} win - ダイアログの親となるウィンドウ。
	 * @param {Function} callback - 第1引数に{@link SearchEngine[]}、第2引数にファイルの文書。第3引数にファイル名。エラーが起きていれば、第4引数にエラーメッセージ。
	 */
	getSearchEnginesFromFile: function (win, callback) {
		let filePicker = new FilePicker(win, null, Ci.nsIFilePicker.modeOpen);
		filePicker.appendFilters(Ci.nsIFilePicker.filterXML);
		filePicker.appendFilters(Ci.nsIFilePicker.filterAll);
		filePicker.open(result => {
			if (result === Ci.nsIFilePicker.returnOK) {
				let client = new XMLHttpRequest();
				client.open('GET', NetUtil.newURI(filePicker.file).spec);
				client.responseType = 'document';
				client.addEventListener('load', event => {
					let doc = event.target.response;
					let root = doc.documentElement;
					if (root.namespaceURI === this.PARSE_ERROR_NS && root.localName === 'parseerror') {
						// パースエラーが起きていれば
						callback(null, null, filePicker.file.leafName, _('XMLパースエラーです。') + '\n\n' + root.textContent);
					} else {
						let engines = [];
						for (let description of doc.getElementsByTagNameNS(OpenSearchUtils.NS, 'OpenSearchDescription')) {
							let engine = OpenSearchUtils.convertEngineToObject(description);
							if (engine) {
								engines.push(engine);
							}
						}
						if (engines.length > 0) {
							callback(engines, doc, filePicker.file.leafName);
						} else {
							// 検索エンジンが一つも含まれていなければ
							callback(null, null, filePicker.file.leafName, _('検索エンジンが一つも見つかりませんでした。'));
						}
					}
				});
				client.send();
			}
		});
	},

	/**
	 * search.jsonから、ブラウザの検索エンジンの文字符号化方式を取得する。
	 * @returns {Object} プロパティ名に検索エンジン名、値に文字符号化方式をもつオブジェクト。
	 * @access protected
	 */
	getBrowserEngineEncodings: function () {
		let encodings = {};
		let file = FileUtils.getFile('ProfD', ['search.json']);
		if (file.exists()) {
			let stream = new FileInputStream(file, -1, -1, 0);
			try {
				let searchJson = NativeJSON.decodeFromStream(stream, -1);
				if (searchJson && searchJson.directories) {
					for (let directory in searchJson.directories) {
						if (directory && Array.isArray(directory.engines)) {
							for (let engine of directory.engines) {
								encodings[engine._name] = engine.queryCharset;
							}
						}
					}
				}
			} catch (e) {
				if (!(e instanceof SyntaxError)) {
					throw e;
				}
			} finally {
				stream.close();
			}
		}
		return encodings;
	},
};



/**
 * prefs.jsなどの設定を読み書きする。
 */
let SettingsUtils = {
	/**
	 * エクスポートするXMLファイルで使用する名前空間。
	 * @constant {string}
	 */
	NS: 'https://userscripts.org/scripts/show/130510',

	/**
	 * 設定を保存するブランチ名。(末尾にピリオドを含む)
	 * @constant {string}
	 */
	ROOT_BRANCH_NAME: 'extensions.' + DragAndDropZonesPlus.ID + '.',

	/**
	 * prefs.jsの一項目の容量制限。(UTF-16のコードユニット数)
	 * @constant {number}
	 */
	MAX_PREFERENCE_VALUE_LENGTH: 1 * 1024 * 1024,

	/**
	 * ファイルに設定をエクスポートする。
	 * @param {Window} win - ダイアログの親となるウィンドウ。
	 * @param {Function} [callback] - 第1引数にファイルのフルパス。
	 */
	exportToFile: function (win, callback = function () { }) {
		// ファイル保存ダイアログを開く
		let filePicker = new FilePicker(win, null, Ci.nsIFilePicker.modeSave);
		filePicker.appendFilters(Ci.nsIFilePicker.filterXML);
		filePicker.appendFilters(Ci.nsIFilePicker.filterAll);
		filePicker.defaultString = DragAndDropZonesPlus.ID + '.xml';
		filePicker.open(result => {
			if (result === Ci.nsIFilePicker.returnOK || result === Ci.nsIFilePicker.returnReplace) {
				let settingsDocument = new Document();
				let settings = settingsDocument.createElementNS(this.NS, 'settings');
				let engines = settingsDocument.createElementNS(this.NS, 'engines');

				for (let engine of SearchUtils.getEngines()) {
					engines.appendChild(OpenSearchUtils.convertEngineToElement(engine, settingsDocument));
				}
				settings.appendChild(engines);

				let where = Application.prefs.getValue(this.ROOT_BRANCH_NAME + 'where', 'tab');
				settings.appendChild(settingsDocument.createElementNS(this.NS, 'where')).textContent = where;

				let automaticallyReflect = Application.prefs.getValue(this.ROOT_BRANCH_NAME + 'automaticallyReflect', true);
				if (automaticallyReflect) {
					settings.appendChild(settingsDocument.createElementNS(this.NS, 'automatically-reflect')).textContent = 'on';
				}

				settingsDocument.appendChild(settings);

				// 保存
				let stream = FileUtils.openSafeFileOutputStream(filePicker.file);
				DOMSerializer.serializeToStream(DOMUtils.toPrettyXML(settings), stream, '');
				FileUtils.closeSafeFileOutputStream(stream);
				callback(filePicker.file.path);
			}
		});
	},

	/**
	 * ファイルから設定をインポートする。
	 * @param {Window} win - ダイアログの親となるウィンドウ。
	 * @param {Function} [callback] - 第1引数にファイル名。インポートに失敗していれば、第2引数にエラーメッセージ。
	 */
	importFromFile: function (win, callback = function () { }) {
		SearchUtils.getSearchEnginesFromFile(win, (engines, settingsDocument, fileName, errorMessage) => {
			if (engines) {
				SearchUtils.setEngines(engines);

				let where = settingsDocument.getElementsByTagNameNS(this.NS, 'where')[0];
				if (where) {
					let value = where.textContent.trim();
					if (value !== 'tab') {
						Application.prefs.setValue(this.ROOT_BRANCH_NAME + 'where', value);
					}
				}

				let automaticallyReflect = settingsDocument.getElementsByTagNameNS(this.NS, 'automatically-reflect')[0];
				if (!automaticallyReflect || automaticallyReflect.textContent.trim() !== 'on') {
					Application.prefs.setValue(this.ROOT_BRANCH_NAME + 'automaticallyReflect', false);
				}

				DropzoneUtils.update();

				callback(fileName);
			} else {
				callback(fileName, errorMessage);
			}
		});
	},
};



/**
 * バージョン1の設定値。
 */
let Version1Settings = {
	/**
	 * JSON文字列から検索エンジンを取得する。
	 * @param {Window} win - ダイアログの親となるウィンドウ。
	 * @param {Function} [callback] - 取得に成功していれば、第1引数に検索エンジンの配列。失敗していれば、第2引数にエラーメッセージ。
	 */
	getEnginesFromText: function (win, callback) {
		let jsonString = win.prompt(_('JSON文字列を貼り付けてください。'));
		if (jsonString && jsonString.trim()) {
			let errorMessage = _('JSON文字列からのインポートに失敗しました。');
			let oldSettings;
			try {
				oldSettings = JSON.parse(jsonString);
			} catch (e) {
				if (e instanceof SyntaxError) {
					callback(null, _('JSONパースエラーです。'));
					return;
				} else {
					throw e;
				}
			}
			if (Array.isArray(oldSettings)) {
				let doc = win.document;
				// ブラウザのエンジン名一覧を取得しておく
				let browserEngineNames = Array.prototype.map.call(doc.querySelectorAll('[name=add-browser-engine] > [label]'), function (option) {
					return option.label;
				});
				// 利用可能な文字コード一覧を取得しておく
				let encodings = Array.prototype.map.call(doc.getElementsByTagName('template')[0].content.querySelectorAll('[name=encoding] > option'), function (option) {
					return option.value;
				});
				// デフォルトのFaviconのDataURLを取得しておく
				let client = new XMLHttpRequest();
				client.open('GET', DropzoneUtils.DEFAULT_ICON);
				client.responseType = 'blob';
				client.addEventListener('load', function (event) {
					IconUtils.convertToDataURL(event.target.response, function (defaultIconDataURL) {
						let engines = [];
						for (let oldSetting of oldSettings) {
							if (typeof oldSetting === 'object' && oldSetting !== null && oldSetting.title) {
								let engine = {
									name: oldSetting.title,
								};
								if (browserEngineNames.indexOf(engine.name) !== -1) {
									// 同名の検索エンジンがブラウザに存在すれば
									engines.push(engine)
								} else if (oldSetting.query) {
									engine.url = oldSetting.query + '{searchTerms}';
									if (oldSetting.icon && oldSetting.icon !== defaultIconDataURL) {
										engine.icon = oldSetting.icon;
									}
									if (oldSetting.encoding && encodings.indexOf(oldSetting.encoding) !== -1) {
										engine.encoding = oldSetting.encoding;
									}
									engines.push(engine);
								}
							}
						}

						if (engines.length > 0) {
							callback(engines);
						} else {
							callback(null, _('検索エンジンが一つも見つかりませんでした。'));
						}
					});
				});
				client.send();
			}
		} else {
			callback(null, _('検索エンジンが一つも見つかりませんでした。'));
		}
	},
};



/**
 * OpenSearchに関する操作群。
 */
let OpenSearchUtils = {
	/**
	 * OpenSearchの名前空間。
	 * @constant {string}
	 */
	NS: 'http://a9.com/-/spec/opensearch/1.1/',

	/**
	 * OpenSearch Referrer extensionの名前空間。
	 * @constant {string}
	 */
	REFERRER_NS: 'http://a9.com/-/opensearch/extensions/referrer/1.0/',

	/**
	 * OpenSearch parameter extensionの名前空間。
	 * @constant {string}
	 */
	PARAMETER_NS: 'http://a9.com/-/spec/opensearch/extensions/parameters/1.0/',

	/**
	 * OpenSearch Suggestions extensionの名前空間。
	 * @constant {string}
	 */
	SUGGESTIONS_NS: 'http://www.opensearch.org/specifications/opensearch/extensions/suggestions/1.1',

	/**
	 * OpenSearch Geo extensionの名前空間。
	 * @constant {string}
	 */
	GEO_NS: 'http://a9.com/-/opensearch/extensions/geo/1.0/',

	/**
	 * OpenSearch Time Extensionの名前空間。
	 * @constant {string}
	 */
	TIME_NS: 'http://a9.com/-/opensearch/extensions/time/1.0/',

	/**
	 * OpenSearch Mobile Extensionの名前空間。
	 * @constant {string}
	 */
	M_NS: 'http://a9.com/-/opensearch/extensions/mobile/1.0/',

	/**
	 * OpenSearch SRU Extensionの名前空間。
	 * @constant {string}
	 */
	SRU_NS: 'http://a9.com/-/opensearch/extensions/sru/2.0/',

	/**
	 * OpenSearch Semantic Extensionの名前空間。
	 * @constant {string}
	 */
	SEMANTIC_NS: 'http://a9.com/-/opensearch/extensions/semantic/1.0/',

	/**
	 * InputEncoding要素の既定値。
	 * @constant {string}
	 */
	DEFAULT_ENCODING: 'UTF-8',

	/**
	 * itemsPerPage要素が存在しない場合の、countパラメータの既定値。
	 * @constant {number}
	 */
	DEFAULT_ITEMS_PER_PAGE: 20,

	/**
	 * {@link SearchEngine}をOpenSearchDescription要素に変換する。
	 * @param {SearchEngine} engine
	 * @param {XMLDocument} doc - 作成するOpenSearchDescription要素のノード文書。
	 * @returns {Element} OpenSearchDescription要素。
	 */
	convertEngineToElement: function (engine, doc) {
		let description = doc.createElementNS(this.NS, 'OpenSearchDescription');
		description.appendChild(doc.createElementNS(this.NS, 'ShortName')).textContent = engine.name;
		description.appendChild(doc.createElementNS(this.NS, 'Description'));
		if (engine.icon) {
			description.appendChild(doc.createElementNS(this.NS, 'Image')).textContent = engine.icon;
		}
		let url = doc.createElementNS(this.NS, 'Url');
		url.setAttribute('template', engine.url);
		url.setAttribute('type', 'text/html');
		if (engine.method === 'POST') {
			// POSTメソッドなら
			url.setAttributeNS(this.PARAMETER_NS, 'parameters:method', engine.method);
			for (let [name, value] of engine.params) {
				let parameter = doc.createElementNS(this.PARAMETER_NS, 'parameters:Parameter');
				parameter.setAttribute('name', name);
				parameter.setAttribute('value', value);
				url.appendChild(parameter);
			}
			if (engine.accept !== 'text/plain') {
				url.setAttributeNS(SettingsUtils.NS, 'dnd-search:accept', engine.accept);
			}
		}
		description.appendChild(url);
		if (engine.encoding !== this.DEFAULT_ENCODING) {
			description.appendChild(doc.createElementNS(OPEN_SEARCH_NS, 'InputEncoding')).textContent = encoding;
		}
		return description;
	},

	/**
	 * OpenSearchDescription要素を{@link SearchEngine}に変換する。
	 * @param {Element} description - OpenSearchDescription要素。
	 * @returns {?SearchEngine}
	 */
	convertEngineToObject: function (description) {
		let engine = null;
		let shortName = description.getElementsByTagNameNS(this.NS, 'ShortName')[0];
		let url = description.querySelector('Url[template][type="text/html"], Url[template][type="application/xhtml+xml"]') || description.querySelector('Url[template]');
		if (shortName && url) {
			let template = this.parseURLTemplate(url.getAttribute('template'), url);
			let nativeURL;
			try {
				nativeURL = NetUtil.newURI(template).QueryInterface(Ci.nsIURL);
			} catch (e) {
				if (e.result === Cr.NS_ERROR_MALFORMED_URI || e.result === Cr.NS_NOINTERFACE) {
					// 妥当なURLでなければ
					return null;
				} else {
					throw e;
				}
			}
			engine = {
				name: shortName.textContent,
			};
			let image = description.getElementsByTagNameNS(this.NS, 'Image')[0];
			if (image) {
				engine.icon = image.textContent;
			}

			let parameters = url.getElementsByTagNameNS(this.PARAMETER_NS, 'Parameter');

			let method = url.getAttributeNS(this.PARAMETER_NS, 'method');
			if (method && method.toUpperCase() === 'POST') {
				// POSTメソッドなら
				engine.method = 'POST';
				engine.url = template;
				engine.params = [];
				for (let parameter of parameters) {
					engine.params.push([
						parameter.getAttribute('name'),
						this.parseURLTemplate(parameter.getAttribute('value'), parameter)]);
				}
				engine.accept = url.getAttributeNS(SettingsUtils.NS, 'accept') || 'text/*';
			} else {
				// POSTメソッド以外はGETメソッドとして扱う
				engine.method = 'GET';
				let searchParams = new URLSearchParams(nativeURL.query);
				for (let parameter of parameters) {
					searchParams.append(
						parameter.getAttribute('name'),
						this.parseURLTemplate(parameter.getAttribute('value'), parameter));
				}
				nativeURL.query = searchParams.toString().replace(/%7BsearchTerms%7D/g, '{searchTerms}');
				engine.url = nativeURL.spec;
				engine.accept = 'text/*';
			}
			let inputEncoding = description.getElementsByTagNameNS(this.NS, 'InputEncoding')[0];
			engine.encoding = inputEncoding ? inputEncoding.textContent : this.DEFAULT_ENCODING;
			if (engine.encoding !== this.DEFAULT_ENCODING && engine.encoding.toUpperCase() === this.DEFAULT_ENCODING) {
				engine.encoding = this.DEFAULT_ENCODING;
			}
		}
		return engine || SearchUtils.trimValues(engine);
	},

	/**
	 * OpenSearch URLテンプレートに含まれるテンプレートパラメータのうち、{searchTerms}以外を置換する。
	 * @param {string} template - OpenSearch URLテンプレート。
	 * @param {Element} element - OpenSearch URLテンプレートが設定されているUrl要素、またはParameter要素。
	 * @returns {string}
	 * @see [OpenSearch URL template syntax]{@link http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax}
	 * @access protected
	 */
	parseURLTemplate: function (template, element) {
		let description = DOMUtils.getParentElementByTagName(element, 'OpenSearchDescription');
		let url = DOMUtils.getParentElementByTagName(element, 'Url');
		let searchValue = /{((?:[-a-zA-Z0-9._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})+:)?((?:[-a-zA-Z0-9._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)(\?)?}/g;
		return template.replace(searchValue, (parameter, encodedPrefix, encodedLname, modifier) => {
			let prefix = decodeURIComponent(encodedPrefix), lname = decodeURIComponent(encodedLname);
			let value = null;
			switch (prefix ? element.lookupNamespaceURI(prefix) || this.NS : this.NS) {
				case this.NS:
					switch (lname) {
						case 'searchTerms':
							return '{searchTerms}';
						case 'count':
							if (modifier) {
								value = '';
							} else {
								let itemsPerPage = description.getElementsByTagNameNS(this.NS, 'itemsPerPage');
								value = itemsPerPage ? itemsPerPage.textContent : this.DEFAULT_ITEMS_PER_PAGE;
							}
							break;
						case 'startIndex':
							value = url.getAttribute('indexOffset') || 1;
							break;
						case 'startPage':
							value = url.getAttribute('pageOffset') || 1;
							break;
						case 'language':
							value = window.navigator.language;
							break;
						case 'inputEncoding':
							let encoding = description.getElementsByTagNameNS(this.NS, 'InputEncoding');
							value = encoding ? encoding.textContent : this.DEFAULT_ENCODING;
							break;
						case 'outputEncoding':
							value = StringUtils.THE_ENCODING;
							break;
					}
					break;
				case this.REFERRER_NS:
					switch (lname) {
						case 'source':
							value = DragAndDropZonesPlus.ID;
							break;
					}
					break;
				case this.SUGGESTIONS_NS:
					switch (lname) {
						//case 'suggestionPrefix':
						//	break;
						case 'suggestionIndex':
							value = modifier ? '' : 0;
							break;
					}
					break;
				//case this.GEO_NS:
				//	break;
				case this.TIME_NS:
					switch (lname) {
						case 'start':
							value = modifier ? '' : '0000-01-01T00:00:00Z';
							break;
						case 'end':
							value = modifier ? '' : '9999-12-31T23:59:58Z';
							break;
					}
					break;
				case this.M_NS:
					switch (lname) {
						case 'userAgent':
							value = window.navigator.userAgent;
							break;
						//case 'subId':
						//	break;
						//case 'mcc':
						//	break;
						//case 'mnc':
						//	break;
					}
					break;
				case this.SRU_NS:
					switch (lname) {
						case 'queryType':
							value = modifier ? '' : 'searchTerms';
							break;
						case 'query':
							return modifier ? '' : '{searchTerms}';
						case 'startRecord':
							value = modifier ? '' : url.getAttribute('indexOffset') || 1;
							break;
						case 'maximumRecords':
							if (modifier) {
								value = '';
							} else {
								let itemsPerPage = description.getElementsByTagNameNS(this.NS, 'itemsPerPage');
								value = itemsPerPage ? itemsPerPage.textContent : this.DEFAULT_ITEMS_PER_PAGE;
							}
							break;
						case 'recordPacking':
							value = modifier ? '' : 'xml';
							break;
						//case 'recordSchema':
						//	break;
						//case 'resultSetTTL':
						//	break;
						//case 'sortKeys':
						//	break;
						//case 'stylesheet':
						//	break;
						case 'rendering':
							value = 'server';
							break;
						case 'httpAccept':
							value = 'text/html, application/xhtml+xml';
							break;
						case 'httpAcceptCharset':
							value = modifier ? '' : '*';
							break;
						case 'httpAcceptEncoding':
							value = modifier ? '' : '*';
							break;
						case 'httpAcceptLanguage':
							value = modifier ? '' : window.navigator.language + ', *';
							break;
						case 'httpAcceptRanges':
							value = modifier ? '' : 'none';
							break;
						case 'facetLimit':
							value = modifier ? '' : 1;
							break;
						case 'facetSort':
							value = modifier ? '' : 'recordCount';
							break;
						//case 'facetRangeField':
						//	break;
						//case 'facetLowValue':
						//	break;
						//case 'facetHighValue':
						//	break;
						//case 'facetCount':
						//	break;
						//case 'extension':
						//	break;
					}
					break;
				//case this.SEMANTIC_NS:
				//	break;
			}
			return value === null ? (modifier ? '' : parameter) : encodeURIComponent(value);
		});
	},
};



/**
 * アイコンに関する操作群。
 */
let IconUtils = {
	/**
	 * SQLiteのLIKE演算子におけるESCAPE文字。
	 * @constant {string}
	 */
	SQLITE_LIKE_ESCAPE_STRING: '@',

	/**
	 * DataURLに変換する。
	 * @param {(Blob|Array)} icon
	 * @param {Function} callback - 第1引数にDataURL。
	 * @param {string} [type] - iconが配列の場合の、アイコンのMIMEタイプ。
	 */
	convertToDataURL: function (icon, callback, type) {
		if (type) {
			icon = new Blob([new Uint8Array(icon)], { type: type });
		}
		let reader = new FileReader();
		reader.addEventListener('load', function (event) {
			callback(event.target.result);
		});
		reader.readAsDataURL(icon);
	},

	/**
	 * URLから、そのWebサイトのFaviconのDataURLを取得する。
	 * @param {string} url
	 * @param {Function} callback - 第1引数にDataURL。
	 */
	getFaviconFromSiteUrl: function (url, callback) {
		let nativeURL = NetUtil.newURI(url).QueryInterface(Ci.nsIURL);
		PlacesUtils.favicons.getFaviconDataForPage(nativeURL, (faviconURL, length, data, type) => {
			if (length > 0) {
				// Faviconが存在すれば
				this.convertToDataURL(data, callback, type);
			} else {
				// places.sqliteに接続する
				let places = Services.storage.openDatabase(FileUtils.getFile('ProfD', ['places.sqlite']));

				// 指定されたURLに似た履歴のFaviconを取得するSQL文を構築・実行
				let statement = places.createAsyncStatement('SELECT data, mime_type'
						+ ' FROM moz_places INNER JOIN moz_favicons ON favicon_id = moz_favicons.id'
						+ ' WHERE moz_places.url LIKE :url ESCAPE :escape ORDER BY last_visit_date DESC LIMIT 1');
				statement.params.url = statement.escapeStringForLIKE(nativeURL.prePath + nativeURL.directory, this.SQLITE_LIKE_ESCAPE_STRING) + '%';
				statement.params.escape = this.SQLITE_LIKE_ESCAPE_STRING;
				let favicon;
				statement.executeAsync({
					handleResult: resultSet => {
						let favicon = resultSet.getNextRow();
						statement.finalize();
					},
					handleError: error => {
						this.getFaviconIco(nativeURL.prePath, callback);
					},
					handleCompletion: () => {
						if (favicon) {
							this.convertToDataURL(favicon.getResultByName('data'), callback, favicon.getResultByName('mime_type'));
						} else {
							this.getFaviconIco(nativeURL.prePath, callback);
						}
					},
				});
				places.asyncClose();
			}
		});
	},

	/**
	 * Faviconを含むWebページのURL、または画像のURLからBlobインスタンスを取得する。
	 * @param {string} url
	 * @param {Function} callback - 第1引数にDataURL。エラーが起こった場合は、第2引数にエラーメッセージ。
	 */
	getFromUrl: function (url, callback) {
		let uri;
		try {
			uri = NetUtil.newURI(url);
		} catch (e) {
			if (e.result === Cr.NS_ERROR_MALFORMED_URI) {
				// 妥当なURLでなければ
				callback(null, _('http:// などで始まるURLを入力してください。'));
				return;
			} else {
				throw e;
			}
		}
		let client = new XMLHttpRequest();
		try {
			client.open('GET', url);
		} catch (e) {
			if (e.result === Cr.NS_ERROR_UNKNOWN_PROTOCOL) {
				callback(null, _('http:// などで始まるURLを入力してください。'));
				return;
			} else {
				throw e;
			}
		}
		client.responseType = 'blob';
		client.addEventListener('error', () => {
			this.getFaviconFromSiteUrl(url, callback, function () {
				callback(null, _('指定されたURLに接続できませんでした。'));
			});
		});
		client.addEventListener('load', event => {
			let client = event.target;
			if (client.status === 200) {
				if (client.response.type.startsWith('image/')) {
					this.convertToDataURL(client.response, callback);
				} else {
					this.getFaviconFromSiteUrl(url, callback, function () {
						callback(null, _('アイコンを取得できませんでした。WebページのURLであれば、一度ブラウザでページを表示してみてください。'));
					});
				}
			} else {
				this.getFaviconFromSiteUrl(url, callback, function () {
					callback(null, _('指定されたURLに接続できませんでした。') + '\n' + client.status + ' ' + client.statusText);
				});
			}
		});
		client.send();
	},

	/**
	 * ローカルファイルのDataURLを取得する。
	 * @param {Window} win - ダイアログの親となるウィンドウ。
	 * @param {Function} callback - 第1引数にDataURL。エラーが起こった場合は、第2引数にエラーメッセージ。
	 */
	getFromLocalFile: function (win, callback) {
		// ファイル選択ダイアログを開く
		let filePicker = new FilePicker(win, null, Ci.nsIFilePicker.modeOpen);
		filePicker.appendFilters(Ci.nsIFilePicker.filterImages);
		filePicker.open(result => {
			if (result === Ci.nsIFilePicker.returnOK) {
				let file = filePicker.file;
				let type;
				try {
					// 拡張子を元にMIMEタイプを取得
					type = MIMEService.getTypeFromFile(file);
				} catch(e) {
					if (e.result === Cr.NS_ERROR_NOT_AVAILABLE) {
						// 未知の拡張子
						callback(null, _('画像ファイルを選択してください。'));
					} else {
						throw e;
					}
				}
				if (type.startsWith('image/')) {
					// Blobインスタンスを取得
					let fileStream = new FileInputStream(file, -1, -1, 0);
					let stream = new BinaryInputStream(fileStream);
					this.convertToDataURL(stream.readByteArray(stream.available()), callback, type);
					stream.close();
					fileStream.close();
				} else {
					callback(null, _('画像ファイルを指定してください。'));
				}
			}
		});
	},

	/**
	 * クリップボードのURL、または画像からDataURLを取得する。
	 * @param {Function} callback - 第1引数にDataURL。エラーが起こった場合は、第2引数にエラーメッセージ。
	 */
	getIconFromClipboard: function (callback) {
		if (Services.clipboard.supportsSelectionClipboard()) {
			// 選択クリップボードが有効なOSなら
			let url = ClipboardUtils.getText(Services.clipboard.kSelectionClipboard);
			if (url) {
				// テキストデータが保持されていれば
				this.getFromUrl(url, (dataURL, errorMessage) => {
					if (dataURL) {
						callback(dataURL);
					} else {
						this.getIconFromGlobalClipboard(callback);
					}
				});
			} else {
				this.getIconFromGlobalClipboard(callback);
			}
		} else {
			this.getIconFromGlobalClipboard(callback);
		}
	},

	/**
	 * クリップボード(グローバル)のURL、または画像からDataURLを取得する。
	 * @param {Function} callback - 第1引数にDataURL。エラーが起こった場合は、第2引数にエラーメッセージ。
	 * @access protected
	 */
	getIconFromGlobalClipboard: function (callback) {
		let url = ClipboardUtils.getText(Services.clipboard.kGlobalClipboard);
		if (url) {
			// テキストデータが保持されていれば
			this.getFromUrl(url, callback);
			return;
		} else if (Services.clipboard.hasDataMatchingFlavors(['image/png'], 1, Services.clipboard.kGlobalClipboard)) {
			// 画像データが保持されていれば、PNG画像として取得する(Windowsでは透過部分が黒色になる)
			// <http://mxr.mozilla.org/mozilla-central/source/addon-sdk/source/lib/sdk/clipboard.js#259>を参考
			let transferable = new Transferable('image/png'), data = {};
			Services.clipboard.getData(transferable, Services.clipboard.kGlobalClipboard);
			transferable.getTransferData('image/png', data, {});
			let image = data.value;
			if (image instanceof Ci.nsISupportsInterfacePointer) {
				image = image.data;
			}
			if (image instanceof Ci.imgIContainer) {
				image = ImgTools.encodeImage(image, 'image/png');
			}
			if (image instanceof Ci.nsIInputStream) {
				this.convertToDataURL(new BinaryInputStream(image).readByteArray(image.available()), callback, 'image/png');
				return;
			}
		}
		callback(null, _('クリップボードからデータを取得できませんでした。'));
	},

	/**
	 * /favicon.ico を取得する。
	 * @param {string} origin
	 * @param {Function} callback - 第1引数にDataURL。
	 * @access protected
	 */
	getFaviconIco: function (origin, callback) {
		let client = new XMLHttpRequest();
		client.open('GET', origin + '/' + 'favicon.ico');
		client.responseType = 'blob';
		client.addEventListener('error', () => callback(null));
		client.addEventListener('load', event => {
			let client = event.target;
			if (client.status === 200 && client.response.type.startsWith('image/')) {
				this.convertToDataURL(client.response, callback);
			} else {
				callback(null);
			}
		});
		client.send();
	},
};



/**
 * ドロップゾーンの作成やドロップされたデータの検索などを行う。
 * @type {Object}
 */
let DropzoneUtils = {
	/**
	 * 設定されていない場合に表示するアイコンのURL。
	 * @constant {string}
	 * @see Ci.nsIFaviconService#defaultFavicon
	 */
	DEFAULT_ICON: 'resource://gre/chrome/toolkit/skin/classic/mozapps/places/defaultFavicon.png',

	/**
	 * ドロップゾーン専用のスタイルシートを設定するための親要素。
	 * @type {HTMLDivElement}
	 */
	wrapper: null,

	/**
	 * 各ドロップゾーンを作成。
	 */
	create: function () {
		this.wrapper = document.createElementNS(DOMUtils.HTML_NS, 'div');
		this.wrapper.id = DragAndDropZonesPlus.ID;
		this.wrapper.hidden = true;
		let style = document.createElementNS(DOMUtils.HTML_NS, 'style');
		style.scoped = true;
		this.wrapper.appendChild(style);
		this.wrapper.appendChild(document.createElementNS(DOMUtils.HTML_NS, 'ul'));
		let appContent = document.getElementById('appcontent');
		appContent.insertBefore(this.wrapper, appContent.firstChild);
		this.update();
	},

	/**
	 * ドロップゾーンを初期状態に戻す。
	 * @param {boolean} [forced] - {@link DropzoneUtils.itemTypesDuringDrag}の確認を行わずに実行するなら真。
	 */
	resetDropzones: function (forced = false) {
		if (forced || this.itemTypesDuringDrag) {
			let activeValidDropzone = this.getActiveValidDropzone();
			if (activeValidDropzone) {
				activeValidDropzone.classList.remove('drop-active-valid');
			}
			this.wrapper.hidden = true;
			this.itemTypesDuringDrag = null;
			this.dragstartEvent = null;
			this.dragoverEventAlreadyFired = true;
		}
	},

	/**
	 * ドロップゾーンに関するスタイルシートとイベントリスナーを設定する。
	 */
	setEventListeners: function () {
		let styleSheet = this.wrapper.getElementsByTagNameNS(DOMUtils.HTML_NS, 'style')[0].sheet;
		let cssRules = styleSheet.cssRules;
		[
			// 位置決め用
			'div {'
					+ 'position: relative;'
					+ '}',

			// ドロップゾーン全体
			'ul {'
					+ 'position: absolute;'
					+ 'top: 1.5em;'
					+ 'left: 1.5em;'
					+ 'right: 1.5em;'
					+ 'height: 8em;'
					+ 'display: flex;'
					+ 'border: solid #A0A0A0 1px;'
					+ 'background-color: rgba(100, 200, 255, 0.5);'
					+ 'padding-left: 0;'
					+ '}',

			// 各ドロップゾーン
			'li {'
					+ 'flex: 1;'
					+ 'font-weight: bold;'
					+ 'padding-left: 0.5em;'
					+ 'overflow: hidden;'
					+ 'white-space: nowrap;'
					+ 'line-height: 2em;'
					+ 'position: relative;'
					+ 'z-index: 1;'
					+ '}',

			'li:not(:first-of-type) {'
					+ 'border-left: inherit;'
					+ '}',

			'img {'
					+ 'width: 16px;'
					+ 'height: 16px;'
					+ 'vertical-align: middle;'
					+ 'margin-right: 0.3em;'
					+ '}',

			// ドロップゾーン上部の背景色
			'li::before {'
					+ 'display: block;'
					+ 'content: "";'
					+ 'position: absolute;'
					+ 'top: 0;'
					+ 'left: 0;'
					+ 'right: 0;'
					+ 'height: 2em;'
					+ 'background-color: rgba(50, 100, 200, 0.7);'
					+ 'z-index: -1;'
					+ '}',

			// 各ドロップゾーンにポインタが載っている時
			'li.drop-active-valid::before {'
					+ 'height: initial;'
					+ 'bottom: 0;'
					+ '}',
		].forEach(function (rule) {
			styleSheet.insertRule(rule, cssRules.length);
		});

		// dropzone属性の代替
		// Bug 723008 – Implement dropzone content attribute <https://bugzilla.mozilla.org/show_bug.cgi?id=723008>
		this.wrapper.addEventListener('dragover', event => {
			let activeValidDropzone = this.getActiveValidDropzone();
			if (activeValidDropzone && activeValidDropzone.contains(event.target)) {
				event.preventDefault();
			}
		});

		// イベントリスナーの追加
		for (let type of this.eventTypesForWindow) {
			window.addEventListener(type, this, true);
		}
	},

	/**
	 * windowに追加したイベントリスナーを取り除く。
	 */
	removeEventListeners: function () {
		for (let type of this.eventTypesForWindow) {
			window.removeEventListener(type, this, true);
		}
	},

	/**
	 * :drop(active valid)な要素にdrop-active-validクラスを追加する。
	 * @param {HTMLElement} target - :drop(active valid)か否か調べる要素。
	 */
	setActiveValidDropzone: function (target) {
		if (target.nodeType === Node.ELEMENT_NODE && this.wrapper.contains(target)) {
			// ドロップゾーンなら
			let dropzone = DOMUtils.getAttributeAsDOMSettableTokenList(target, 'dropzone');
			if (dropzone.contains('link')
					&& Array.prototype.some.call(dropzone, type => this.itemTypesDuringDrag.indexOf(type) !== -1)) {
				// 各ドロップゾーンにポインタが載った時、
				// ドロップゾーンが受け取ることができるデータをドラッグしていれば
				target.classList.add('drop-active-valid');
			}
		}
	},

	/**
	 * イベントハンドラ。
	 * @param {Event} event
	 */
	handleEvent: function (event) {
		let target = event.target;

		switch (event.type) {
			case 'dragstart':
				if (event.isTrusted) {
					// ユーザーによるドラッグなら
					if (this.itemTypesDuringDrag) {
						// ドロップゾーンが表示されたままなら
						this.resetDropzones();
					}

					this.itemTypesDuringDrag = this.getItemTypes(event);
					if (this.itemTypesDuringDrag.length > 0) {
						// ドロップゾーンを表示
						this.wrapper.hidden = false;
						this.dragstartEvent = event;
						this.dragoverEventAlreadyFired = false;
					}
				}
				break;

			case 'dragover':
				if (!this.dragoverEventAlreadyFired) {
					this.dragoverEventAlreadyFired = true;

					// ドラッグ開始時、すでにドロップゾーン内にカーソルがあった場合、dragenterイベントが発生しないため
					if (target.nodeType === Node.ELEMENT_NODE) {
						this.setActiveValidDropzone(target);
					}
				}
				break;

			case 'dragenter':
				if (event.relatedTarget) {
					let activeValidDropzone = this.getActiveValidDropzone();
					if (activeValidDropzone && !activeValidDropzone.contains(target)) {
						// 各ドロップゾーンからポインタが外れた時
						activeValidDropzone.classList.remove('drop-active-valid');
					}

					this.setActiveValidDropzone(target);
				} else {
					// ウィンドウ外からのドラッグなら
					if (this.itemTypesDuringDrag) {
						if (this.itemTypesDuringDrag.length > 0) {
							this.wrapper.hidden = false;
						}
					} else {
						// ドラッグ開始なら
						if (event.isTrusted) {
							this.itemTypesDuringDrag = ['string:text/plain', 'file:text/*', 'file:image/*', 'file:audio/*'];

							// ドロップゾーンを表示
							this.wrapper.hidden = false;
						}
					}
				}
				break;

			case 'dragleave':
				if (this.itemTypesDuringDrag && !event.relatedTarget && !this.wrapper.hidden) {
					// ウィンドウ外へドラッグされたとき
					this.wrapper.hidden = true;
					let activeValidDropzone = this.getActiveValidDropzone();
					if (activeValidDropzone) {
						activeValidDropzone.classList.remove('drop-active-valid');
					}
				}
				break;

			case 'dragend':
				this.resetDropzones();
				break;

			case 'drop':
				if (this.wrapper.contains(target)) {
					// 各ドロップゾーンにドロップされた時
					event.preventDefault();
					let dropzone = DOMUtils.getAttributeAsDOMSettableTokenList(target, 'dropzone');
					if (this.dragstartEvent) {
						if (dropzone.contains('file:image/*')) {
							// 画像としてドロップしたとき
							this.getMIMEInputStreamFromURL(this.getImageURLFromDragstartEvent(this.dragstartEvent),
									mimeInputStream => this.searchDropData(mimeInputStream, target.dataset.engineIndex, event));
						} else {
							// 文字列としてドロップしたとき
							this.searchDropData(this.getTextFromDragstartEvent(this.dragstartEvent), target.dataset.engineIndex, event);
						}
					} else {
						// ウィンドウ外からのドロップ
						if (dropzone.contains('file:image/*') || dropzone.contains('file:audio/*')) {
							// 画像、または音声ファイルとしてドロップしたとき
							let type = dropzone.contains('file:image/*') ? 'image' : 'audio';
							// Firefox 24 ESRにはArray#findが実装されていない
							for (let file of event.dataTransfer.files) {
								if (file.type.startsWith(type + '/')) {
									// ドロップゾーンが受け取ることができる形式のファイルなら
									this.searchDropData(file, target.dataset.engineIndex, event);
									break;
								}
							}
						} else {
							// 文字列としてドロップしたとき
							let text = this.getTextFromDropEvent(event, !dropzone.contains('file:text/*'));
							if (text) {
								this.searchDropData(text, target.dataset.engineIndex, event);
							}
						}
					}
				}
				this.resetDropzones();
				break;
		}
	},

	/**
	 * prefs.jsの設定値を元に、各ウィンドウのドロップゾーンを更新する。
	 */
	update: function() {
		// 構築
		let dropzones = new DocumentFragment();
		for (let engine of SearchUtils.getEngines()) {
			dropzones.appendChild(this.convertFromSearchEngine(engine));
		}

		// 置換
		for (let fuelWindow of Application.windows) {
			let ul = FUELUtils.getChromeWindow(fuelWindow).document.querySelector('#' + DragAndDropZonesPlus.ID + ' ul');

			// クリア
			while (ul.hasChildNodes()) {
				ul.firstChild.remove();
			}

			ul.appendChild(dropzones.cloneNode(true));
		}
	},

	/**
	 * ドロップゾーンを削除する。
	 */
	remove: function() {
		this.wrapper.remove();
	},

	/**
	 * prefs.jsの設定値を元に、ドロップゾーンを作成する。
	 * @param {SearchEngine} engine
	 * @returns	{HTMLLIElement}
	 */
	convertFromSearchEngine: function (engine) {
		let li = document.createElementNS(DOMUtils.HTML_NS, 'li');

		// インデックス
		li.dataset.engineIndex = engine.index;

		// dropzone属性
		let dropzone = DOMUtils.getAttributeAsDOMSettableTokenList(li, 'dropzone');
		dropzone.add('link');
		if (engine.accept === 'text/plain') {
			dropzone.add('string:text/plain');
			if (engine.method === 'POST') {
				dropzone.add('file:text/*');
			}
		} else {
			dropzone.add('file:' + engine.accept);
		}
		li.setAttribute('dropzone', dropzone);

		// アイコン
		let icon = new Image(16, 16);
		icon.src = engine.icon || DropzoneUtils.DEFAULT_ICON;
		li.appendChild(icon);

		// 表示名
		li.appendChild(new Text(engine.name));
		li.dataset.name = engine.name;

		return li;
	},

	/**
	 * windowに追加するイベントリスナーが補足するイベントの種類。
	 * @type {string[]}
	 * @access protected
	 */
	eventTypesForWindow: ['dragstart', 'dragover', 'dragenter', 'dragleave', 'dragend', 'drop'],

	/**
	 * ドラッグ中のアイテムの種類。
	 * ドラッグ中でなければnull。
	 * @type {?string[]}
	 * @access protected
	 */
	itemTypesDuringDrag: null,

	/**
	 * dragstartイベント。
	 * ウィンドウ外からドラッグしている場合はnull。
	 * @type {?DragEvent}
	 * @access protected
	 */
	dragstartEvent: null,

	/**
	 * ドラッグ開始後、dragoverイベントが既に発生していれば真。
	 * @type {booelan}
	 * @access protected
	 */
	dragoverEventAlreadyFired: true,

	/**
	 * ドラッグしようとしているアイテムの種類を取得する。
	 * @param {DragEvent} event - dragstartイベント。
	 * @returns {string[]}
	 * @access protected
	 */
	getItemTypes: function (event) {
		let types = [];

		let target = event.target;
		let name = target.localName || target.nodeName;
		if (target.ownerDocument === document && event.dataTransfer.getData('text/plain')
				|| ['a', 'img', '#text'].indexOf(name) !== -1
				|| ['input', 'textarea'].indexOf(name) !== -1 && !target.draggable) {
			// ロケーションバーや検索窓からのドラッグ、
			// またはソースノードがリンク・画像・文字列、ドラッグ不可のテキスト入力欄なら
			types.push('string:text/plain');
		}

		if (name === 'img' || name === 'a' && target.getElementsByTagName('img')[0]) {
			// ソースノードが画像、または画像を含むリンクなら
			types.push('file:image/*');
		}

		return types;
	},

	/**
	 * drop-active-validクラスが付いた要素を返す。
	 * @returns {?HTMLLIElement}
	 * @access protected
	 */
	getActiveValidDropzone: function () {
		return this.wrapper.getElementsByClassName('drop-active-valid')[0];
	},

	/**
	 * dragstartイベントから、対象の文字列を取得する。
	 * @param {DragEvent} event
	 * @returns {string}
	 * @access protected
	 */
	getTextFromDragstartEvent: function (event) {
		let text = '';
		let target = event.target;
		let localName = target.localName;
		let doc = target.ownerDocument;

		let selection = doc.getSelection();
		let selectedString = selection ? selection.toString() : '';
		if (selectedString && (localName === 'a' || target.nodeType === Node.TEXT_NODE)) {
			// リンクか選択範囲をドラッグしていれば
			let x = event.clientX, y = event.clientY;
			if (!DOMUtils.isSuperposedCoordinateOnSelection(selection, x, y)) {
				// ドラッグ開始位置が選択範囲外なら
				let element = doc.elementFromPoint(x, y);
				if (element && (element.localName === 'a' || (element = DOMUtils.getParentElementByTagName(element, 'a')))) {
					// 選択範囲が重なったリンクの、選択範囲でない部分をドラッグしていれば
					text = gatherTextUnder(element);
				}
			}
		}

		if (!text && ['a', 'img'].indexOf(localName) !== -1) {
			// リンクか画像をドラッグしていれば
			if (selectedString && selection.containsNode(target, true)) {
				// ソースノードが選択範囲と重なっており、
				// リンクの一部分だけが選択されている場合は、選択範囲とドラッグ開始位置が重なっていれば
				text = selectedString;
			}

			if (!text) {
				if (localName === 'img') {
					// 画像をドラッグしていれば
					text = selectedString || target.alt || target.title;
					if (!text) {
						let figcaption = DOMUtils.getFigcaption(target);
						if (figcaption) {
							text = gatherTextUnder(figcaption);
						}
					}
				} else {
					// リンクをドラッグしていれば
					text = gatherTextUnder(target);
				}
			}
		}

		return text.trim() || event.dataTransfer.getData('text/plain').trim();
	},

	/**
	 * ウィンドウ外からドロップされた文字列情報を取得する。
	 * @param {DragEvent} event - dropイベント。
	 * @prams {boolean} [forceString] - 真が指定されていれば、常にFileインスタンスの代わりにファイル名を返す。
	 * @returns {?(string|File)}
	 * @access protected
	 */
	getTextFromDropEvent: function (event, forceString = false) {
		let dropFile = null, dropText = '';

		let files = event.dataTransfer.files;
		if (files.length > 0) {
			// ファイルをドロップしていれば
			if (!forceString) {
				// Firefox 24 ESRにはArray#findが実装されていない
				for (let file of files) {
					if (mimeTypeIsTextBased(file.type)) {
						// テキストファイルなら
						dropFile = file;
						break;
					}
				}
			}

			if (!dropFile) {
				// テキスト形式でないファイルがドロップされているかforceStringが指定されていれば、ファイル名を取得する
				dropText = files[0].name;
			}
		} else {
			dropText = event.dataTransfer.getData('text/plain');
		}

		return dropFile ? dropFile : dropText.trim() || null;
	},

	/**
	 * dragstartイベントから、対象の画像URLを取得する。
	 * @param {DragEvent} event
	 * @returns {?string}
	 * @access protected
	 */
	getImageURLFromDragstartEvent: function (event) {
		let url = null;

		let target = event.target;
		switch (target.localName) {
			case 'img':
				url = target.src;
				break;

			case 'a':
				let images = target.getElementsByTagName('img');
				if (images.length === 1) {
					url = images[0].src;
				} else {
					let image = target.ownerDocument.elementFromPoint(event.clientX, event.clientY);
					url = image.localName === 'img' && target.contains(image) ? image.src : images[0].src;
				}
				break;
		}

		return url;
	},

	/**
	 * URLからファイルを取得する。
	 * @param {string} url - ファイルのURL。
	 * @param {Function} [callback] - 第1引数に{@link Ci.nsIMIMEInputStream}。
	 * @access protected
	 */
	getMIMEInputStreamFromURL: function (url, callback) {
		let channel = NetUtil.newChannel(url);
		channel.loadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
		channel.asyncOpen({
			onDataAvailable: (request, context, stream) => {
				let mimeStream = new MIMEInputStream();
				mimeStream.addHeader('content-type', channel.contentType);
				mimeStream.setData(stream);
				callback(mimeStream);
			},
			onStartRequest: function () { },
			onStopRequest: function () { },
		}, null);
	},

	/**
	 * ドロップされたデータを、ドロップゾーンに結びつけられたエンジンで検索する。
	 * @param {(string|Blob|nsIMIMEInputStream)} data - 検索する文字列、またはファイル。
	 * @param {number} engineIndex - prefs.jsに保存されているインデックス。
	 * @param {DragEvent} event - どのキーを押しているか取得するためのdropイベント。
	 * @access protected
	 */
	searchDropData: function (data, engineIndex, event) {
		let mimeType = data.type;
		if (mimeType && mimeTypeIsTextBased(mimeType) && !/^(?:image|audio)\//.test(mimeType)) {
			// ドロップされたデータがテキストファイルなら、文字列に変換しておく
			let fileReader = new FileReader();
			fileReader.addEventListener('load', () => {
				this.searchDropData(fileReader.result, engineIndex, event);
			});
			fileReader.readAsText(data);
			return;
		}

		let engine = SearchUtils.getEngineByIndex(engineIndex);
		if (engine) {
			if (engine.browserSearchEngine) {
				// ブラウザの検索窓のエンジンなら
				let browserSearchEngine = Services.search.getEngineByName(engine.name);
				if (browserSearchEngine) {
					let submission = browserSearchEngine.getSubmission(data);
					this.openSearchResult(submission.uri.spec, event, submission.postData);
				}
			} else {
				// ユーザー定義のエンジンなら
				if (engine.method === 'POST') {
					for (let i = 0, l = engine.params.length; i < l; i++) {
						if (engine.params[i][1].contains('{searchTerms}')) {
							engine.params[i][1] = data;
						}
					}
					StringUtils.encodeMultipartFormData(engine.params, postData => {
						this.openSearchResult(engine.url, event, postData);
					}, engine.encoding);
				} else {
					let encodedString;
					try {
						encodedString = TextToSubURI.ConvertAndEscape(engine.encoding, data);
					} catch(e) {
						if (e.result === Cr.NS_ERROR_UCONV_NOCONV) {
							encodedString = TextToSubURI.ConvertAndEscape(StringUtils.THE_ENCODING, data);
						} else {
							throw e;
						}
					}
					this.openSearchResult(engine.url.replace(/{searchTerms}/g, encodedString), event);
				}
			}
		}
	},

	/**
	 * ユーザー設定に基づき、適切な場所で検索結果を開く。
	 * @param {string}	url
	 * @param {DragEvent} event - どのキーを押しているか取得するためのdropイベント。
	 * @param {nsIInputStream} [postData]
	 * @access protected
	 */
	openSearchResult: function (url, event, postData = null) {
		let where = Application.prefs.getValue(this.ROOT_BRANCH_NAME + 'where', 'tab');
		if (where === 'current') {
			openUILink(url, event, { postData: postData });
		} else {
			openUILinkIn(url, where, { postData: postData });
		}
	},
};



/**
 * 検索窓のエンジンを追加・削除したときのオブザーバ。
 */
let BrowserSearchEngineModifiedObserver = {
	/**
	 * 監視する項目。
	 * @constant {string}
	 */
	SEARCH_ENGINE_TOPIC: 'browser-search-engine-modified',

	/**
	 * 検索エンジンが削除されたときの通知。
	 * @constant {string}
	 */
	SEARCH_ENGINE_REMOVED: 'engine-removed',

	/**
	 * 検索エンジンの情報が変更されたときの通知。
	 * @constant {string}
	 */
	SEARCH_ENGINE_CHNAGED: 'engine-changed',

	/**
	 * 検索エンジンが追加されたときの通知。
	 * @constant {string}
	 */
	SEARCH_ENGINE_ADDED: 'engine-added',

	/**
	 * オブザーバを追加する。
	 * ブラウザ起動時に呼び出し、新しいウィンドウが開かれたときは呼び出さない。
	 */
	init: function () {
		Services.obs.addObserver(this, this.SEARCH_ENGINE_TOPIC, false);
		Services.obs.addObserver(this, DragAndDropZonesPlus.ID, false);
	},

	/**
	 * オブザーバを削除する。
	 */
	stop: function () {
		Services.obs.removeObserver(this, this.SEARCH_ENGINE_TOPIC);
		Services.obs.removeObserver(this, DragAndDropZonesPlus.ID);
	},

	/**
	 * 通知を受け取るメソッド。
	 * @param {*} subject
	 * @param {string} topic
	 * @param {string} data
	 */
	observe: function (subject, topic, data) {
		switch (topic) {
			case this.SEARCH_ENGINE_TOPIC:
				let browserEngine = subject.QueryInterface(Ci.nsISearchEngine);
				switch (data) {
					case this.SEARCH_ENGINE_ADDED:
						// 検索窓にエンジンが追加されたとき
						if (Application.prefs.getValue(SettingsUtils.ROOT_BRANCH_NAME + 'automaticallyReflect', true)) {
							// ドロップゾーンの自動追加が有効なら
							let engine = SearchUtils.convertEngineFromBrowser(browserEngine);
							engine.index = SearchUtils.getDropzoneLength();
							SearchUtils.setEngine(engine.index, engine);
							DropzoneUtils.update();
						}
						break;

					case this.SEARCH_ENGINE_REMOVED:
						// 検索窓からエンジンが削除されたとき
						for (let i = 0, l = SearchUtils.getDropzoneLength(); i < l; i++) {
							let branchName = SettingsUtils.ROOT_BRANCH_NAME + 'engines.' + i + '.';
							if (Application.prefs.getValue(branchName + 'name', null) === browserEngine.name
									&& !Application.prefs.getValue(branchName + 'url', null)) {
								// 同名のエンジン、かつユーザー定義エンジンでなければ
								gPrefService.getBranch(SettingsUtils.ROOT_BRANCH_NAME + 'engines.').deleteBranch(i + '.');
								DropzoneUtils.update();
								break;
							}
						}
						break;
				}
				break;

			case DragAndDropZonesPlus.ID:
				switch (data) {
					case UninstallObserver.TYPE:
						// アンインストール時
						this.stop();
						break;
				}
				break;
		}
	},
};



/**
 * 設定画面。
 */
let SettingsScreen = {
	/**
	 * XUL名前空間。
	 * @constant {string}
	 */
	XUL_NS: 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul',

	/**
	 * Base64に変換したとき、データが何倍になるか。
	 * @constant {number}
	 */
	BASE64_SIZE_RATIO: 4 / 3,

	/**
	 * 設定画面タブのアイコン。
	 * @constant {string}
	 */
	ICON: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAACSVBMVEX////////////////////////////////////////////////r8PTu8vby9fju8fbw8/f09vn////////////////////8/f38/v79/v6kvdSkvtelyeClyeK0x9q0yd210eW10ebE1OPE1eXF3OvF3OzO2ubV4Orf5+/P2+jP2+nX4evX4e3g6PDg6PHU4Ova5e7j6/K4x9rE0ODR2+e5ydu5ytvF0eHF0+HS3OjS3ejAz+DK1+XW4Ou4zuC4z+K50uW51ObE1eXE1+bF2enF2urR3+vR4OzS4u7S4+9ykLWOpMGkt89HeahJgLBMjr5Mk8NukrdvmL1yo8lypsyIqMeJrcyLttWLudhGf7Btl72HrMw/cqVpjrWDpMUqa6QterNaiLRck8B1n8R3qc4dXpsid7NSf6xVkcBtl75wp84ALnQ4XY9SeKUAP4MAT5YAWp8AZKo7aZo9dKk/e7A/g7hVg69XjbtZlMFZm8gAMHUANXwAO4A2XpA2YpU3ZphQeaZQfKpRgK0APIU1Z5xPgbAAHGYtUYRFbJwAHGgAH2sAJXEAMH0qUYYqU4grV4wsXpZCbJ1Cbp9DcqNEeasAKHUpWZBBdKYAGmwANYgARpkfUIkiYp4jbas1a6A4fLI6h70AAAAABVgACV0AD2MAFmkAHHEAJHcAKn8AMIUANosAO5AAP5QbQnobRX4cSYIcTYcdUYwdVpEeWpcfXpwfY6AfZqQgaacvXZMvYJYwZJoxaJ4ybKMycaczdaw0ebA1fbQ1gLc2g7rS/SeDAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfeAwQBGiXWyX7TAAAAd0lEQVQY02NgwAEiKhdURiDxsw6BQBacH34wxZPBM+VgOEyg4oAHkPQ4UAETmL8fTO2fDxMo3+cOJN33lcMEovcmuzG4Je8Ng/INW6fsAYFMmIKm3dPjyuaVhcL4jbsm2yI7smHnJCtkfu6OiZYovvDpMGEgCQAArSEnE6lZL7YAAAAASUVORK5CYII=',

	/**
	 * メニューバーの「ツール」に、設定画面を開くオプションを追加。
	 */
	addToMenu: function () {
		let menuItem = document.createElementNS(this.XUL_NS, 'menuitem');
		menuItem.id = DragAndDropZonesPlus.ID + '-menuitem';
		menuItem.setAttribute('label', DragAndDropZonesPlus.NAME);
		menuItem.setAttribute('image', this.ICON);
		menuItem.classList.add('menuitem-iconic');
		menuItem.addEventListener('command', () => this.open());
		document.getElementById('menu_ToolsPopup').appendChild(menuItem);
	},

	/**
	 * 設定画面を開く。
	 */
	open: function () {
		if (this.tab) {
			// すでに同じウィンドウで開いていれば
			this.tab.focus();
		} else {
			let fuelTab = Application.storage.get(DragAndDropZonesPlus.ID + '_settingsTab', null);
			if (fuelTab) {
				// すでに別にウィンドウで開いていれば
				fuelTab.focus();
				FUELUtils.getChromeWindow(fuelTab.window).focus();
			} else {
				let fuelWindow = FUELUtils.getFUELWindow();
				this.tab = fuelWindow.open(NetUtil.newURI('about:blank'));
				this.tab.events.addListener('load', this);
				this.tab.focus();
				fuelWindow.events.addListener('TabClose', this);
			}
		}
	},

	/**
	 * イベントハンドラ。
	 * @param {Event} event
	 */
	handleEvent: function (event) {
		let target = event.target;

		switch (event.type) {
			case 'load':
				if (this.tab.uri.spec === 'about:blank') {
					this.show();
				} else {
					// 同じタブで別のページが開かれたら
					event.data.events.removeListener(event.type, this);
					this.tab = null;
					return;
				}
				break;

			case 'TabClose':
				let closedTab = event.data;
				if (closedTab.index === this.tab.index) {
					closedTab.window.events.removeListener(event.type, this);
					this.tab = null;
				}
				break;

			case 'beforeunload':
				Application.storage.set(DragAndDropZonesPlus.ID + '_settingsTab', null);
				break;

			case 'submit':
				// OKボタン
				target.querySelector('[type=submit],button:not([type])').disable = true;
				event.preventDefault();

				let engines = [];
				for (let row of target.getElementsByTagName('tbody')[0].rows) {
					let engine = {};
					let name = row.querySelector('[name=name]');
					if (name.readOnly) {
						// 検索窓のエンジンなら
						engine.browserSearchEngine = true;
					}
					engine.name = name.value;
					if (!engine.browserSearchEngine) {
						engine.url = row.querySelector('[name=url]').value;
					}
					if (engine.name && (engine.browserSearchEngine ? Services.search.getEngineByName(engine.name) : engine.url)) {
						// 検索エンジン名が空文字列でなければ、
						// かつブラウザのエンジンであればそれが存在するなら、ユーザー定義エンジンであればURLが空文字列でなければ
						if (!engine.browserSearchEngine) {
							// ユーザー定義エンジンであれば
							let icon = row.querySelector('[name=icon]').value;
							if (icon) {
								engine.icon = icon;
							}
							engine.method = row.querySelector('[name=method]').value;
							if (engine.method === 'GET') {
								// GETメソッドなら
								if (!engine.url.contains('{searchTerms}')) {
									engine.url += '{searchTerms}';
								}
							} else {
								// POSTメソッドなら
								engine.params = [];
								for (let row of row.querySelectorAll('tbody > tr')) {
									let name = row.querySelector('[name=post-param-name]').value;
									let value = row.querySelector('[name=post-param-value]').value;
									if (name || value) {
										// 名前と値どちらかが入力されていれば
										engine.params.push([name, value]);
									}
								}
							}
							engine.accept = row.querySelector('[name=accept]').value;
							engine.encoding = row.querySelector('[name=encoding]').value;
						}
						engines.push(engine);
					}
				}

				let prefBranch = gPrefService.getBranch(SettingsUtils.ROOT_BRANCH_NAME);

				let where = target.where.value;
				if (where === 'tab') {
					prefBranch.clearUserPref('where');
				} else {
					Application.prefs.setValue(SettingsUtils.ROOT_BRANCH_NAME + 'where', where);
				}

				if (target['automatically-reflect'].checked) {
					prefBranch.clearUserPref('automaticallyReflect');
				} else {
					Application.prefs.setValue(SettingsUtils.ROOT_BRANCH_NAME + 'automaticallyReflect', false);
				}

				SearchUtils.setEngines(engines);
				DropzoneUtils.update();
				this.close();
				break;

			case 'contextmenu':
				// 選択された要素を取得しておく
				this.selectedElement = target;

			case 'click':
				switch (target.name) {
					case 'icon':
						// 選択された要素を取得しておく
						this.selectedElement = target;
						if (target.type !== 'menu') {
							// button[type=menu] の代替
							// Bug 897102 – Update <menu> to spec <https://bugzilla.mozilla.org/show_bug.cgi?id=897102>
							event.preventDefault();
							let doc = this.tab.document;
							let menu = doc.getElementById('icon-menu');
							let iconMenupopup = document.createElementNS(this.XUL_NS, 'menupopup');
							for (let menuitem of menu.getElementsByTagName('menuitem')) {
								let xulMenuitem = document.createElementNS(this.XUL_NS, 'menuitem');
								xulMenuitem.setAttribute('label', menuitem.label);
								xulMenuitem.setAttribute('value', menuitem.id);
								iconMenupopup.appendChild(xulMenuitem);
							}
							iconMenupopup.addEventListener('click', function (event) {
								doc.getElementById(event.target.value).click();
							});
							iconMenupopup.addEventListener('popuphidden', function (event) {
								event.currentTarget.remove();
							});
							document.documentElement.appendChild(iconMenupopup).openPopup(target);
						}
						break;

					case 'delete':
						// 行の削除
						this.deleteEngine(target);
						break;

					case 'add-row':
						// 行の追加
						this.insertEmptyRow(target);
						break;

					case 'params':
						// POSTパラメータの開閉
						DOMUtils.getParentElementByTagName(target, 'tr').classList.toggle('displaying-post-params');
						break;

					case 'export':
						// エクスポート
						SettingsUtils.exportToFile(this.tab.document.defaultView, filePath => {
							showPopupNotification(_('%s へ設定をエクスポートしました。').replace('%s', filePath), this.tab);
						});
						break;

					case 'import':
						// インポート
						SettingsUtils.importFromFile(this.tab.document.defaultView, (fileName, errorMessage) => {
							if (errorMessage) {
								showPopupNotification(_('%s からのインポートに失敗しました。').replace('%s', fileName) + '\n' + errorMessage, this.tab, 'warning');
							} else {
								this.tab.document.defaultView.location.reload();
								showPopupNotification(_('%s からのインポートが完了しました。').replace('%s', fileName), this.tab);
							}
						});
						break;

					case 'additional-import':
						// 追加インポート
						this.addEnginesFromFile((fileName, errorMessage) => {
							if (errorMessage) {
								showPopupNotification(_('%s からのインポートに失敗しました。').replace('%s', fileName) + '\n' + errorMessage, this.tab, 'warning');
							} else {
								showPopupNotification(_('%s からのインポートが完了しました。').replace('%s', fileName) + '\n'
										+ _('インポートした設定を保存するには、「OK」ボタンをクリックしてください。'), this.tab);
							}
						});
						break;

					case 'import-from-text':
						// JSON文字列から追加インポート
						Version1Settings.getEnginesFromText(this.tab.document.defaultView, (engines, errorMessage) => {
							if (errorMessage) {
								showPopupNotification(_('JSON文字列からのインポートに失敗しました。') + '\n' + errorMessage, this.tab, 'warning');
							} else {
								this.addEngines(engines);
								showPopupNotification(_('JSON文字列からのインポートが完了しました。') + '\n'
										+ _('インポートした設定を保存するには、「OK」ボタンをクリックしてください。'), this.tab);
							}
						});
						break;

					case 'cancel':
						// キャンセル
						this.close();
						break;

					case 'get-icons':
						// 未取得アイコンの一括取得
						target.disabled = true;
						this.setIconsToEngineWithout(() => {
							target.disabled = false;
							showPopupNotification(_('アイコンの取得が完了しました。'), this.tab);
						});
						break;

					case 'initialize':
					case 'uninstall':
						// 設定の初期化、またはすべての設定の削除
						if (this.tab.document.defaultView.confirm('本当に、『%s』のすべての設定を削除してもよろしいですか?'.replace('%s', DragAndDropZonesPlus.NAME))) {
							if (target.name === 'initialize') {
								DragAndDropZonesPlus.initialize();
								this.tab.document.defaultView.location.reload();
								showPopupNotification(_('設定の初期化が完了しました。'), this.tab);
							} else {
								let fuelWindow = this.tab.window;
								this.close();
								DragAndDropZonesPlus.uninstall();
								showPopupNotification(_('設定の削除が完了しました。当スクリプト自体を削除しなければ、次回のブラウザ起動時にまた設定が作成されます。'), fuelWindow.activeTab);
							}
						}
						break;

					default:
						let parent = target.parentElement;
						if (parent) {
							switch (parent.id) {
								case 'row-contextmenu':
									// 行のコンテキストメニュー
									// 行の挿入
									this.insertEmptyRow(this.selectedElement, target.id === 'add-row-above');
									break;

								case 'icon-menu':
									// アイコンのメニュー
									let url;
									switch (target.id) {
										case 'set-icon-from-local-file':
											IconUtils.getFromLocalFile(this.tab.document.defaultView, (dataURL, errorMessage) => {
												if (dataURL) {
													this.showIcon(dataURL);
												} else {
													showPopupNotification(errorMessage, this.tab, 'warning');
												}
											});
											break;
										case 'set-icon-from-url':
											// 入力ダイアログを開く
											url = this.tab.document.defaultView.prompt(_('WebページのURL、または画像ファイルのURLを入力してください。'));
											if (url) {
												IconUtils.getFromUrl(url, (dataURL, errorMessage) => {
													if (dataURL) {
														this.showIcon(dataURL);
													} else {
														showPopupNotification(errorMessage, this.tab, 'warning');
													}
												});
											}
											break;
										case 'set-icon-from-clipboard':
											// クリップボードのデータを取得する
											IconUtils.getIconFromClipboard((dataURL, errorMessage) => {
												if (dataURL) {
													this.showIcon(dataURL);
												} else {
													showPopupNotification(errorMessage, this.tab, 'warning');
												}
											});
											break;
										case 'restore-default-icon':
											// Faviconを取得し設定
											url = DOMUtils.getParentElementByTagName(this.selectedElement, 'tr').querySelector('[name=url]').value;
											if (url) {
												IconUtils.getFaviconFromSiteUrl(url, (dataURL) => {
													if (dataURL) {
														this.showIcon(dataURL);
													} else {
														this.selectedElement.value = '';
														this.selectedElement.firstElementChild.src = DropzoneUtils.DEFAULT_ICON;
													}
												});
											} else {
												this.selectedElement.value = '';
												this.selectedElement.firstElementChild.src = DropzoneUtils.DEFAULT_ICON;
											}
											break;
									}
									break;
							}
						}
				}
				break;

			case 'keypress':
				let key = event.key;
				switch (key) {
					case 'Enter':
						// Enterキーが押されたとき
						// Bug 886308 – Implement Element.matches() <https://bugzilla.mozilla.org/show_bug.cgi?id=886308>
						if (target.mozMatchesSelector('tbody tr, tbody tr *')) {
							// 行内でキーが押されたとき
							let shiftKey = event.getModifierState('Shift');
							if (shiftKey || event.getModifierState('Alt') || event.getModifierState('Control')) {
								// Shiftキー、Altキー、Ctrlキーいずれかが押されていれば
								event.preventDefault();
								// 行を追加する
								this.insertEmptyRow(target, shiftKey, true);
							}
						}
						break;

					case 'ArrowUp':
					case 'ArrowDown':
					// Bug 900390 – Replace "Left", "Right", "Up" and "Down" with "ArrowLeft", "ArrowRight", "ArrowUp" and "ArrowDown" <https://bugzilla.mozilla.org/show_bug.cgi?id=900390>
					case 'Up':
					case 'Down':
						// 上矢印キー、または下矢印キーが押されたとき
						if (target.localName === 'input' && (event.getModifierState('Alt') || event.getModifierState('Control'))) {
							// input要素上でAltキーかCtrlキーが押されていれば
							let current = DOMUtils.getParentElementByTagName(target, 'tr');
							let currentIndex = current.sectionRowIndex + 1;
							let indexes = key === 'ArrowUp' || key === 'Up' ? '-n+' + (currentIndex - 1) : 'n+' + (currentIndex + 1);
							let rows = current.parentElement.querySelectorAll(':not(td) > table > tbody > tr:nth-of-type(' + indexes + '):not(.browser-search-engine)');
							let sibling = key === 'ArrowUp' || key === 'Up' ? rows[rows.length - 1] : rows[0];
							if (sibling) {
								event.preventDefault();
								sibling.querySelector('[name=' + target.name + ']').focus();
							}
						}
						break;
				}
				break;

			case 'change':
				switch (target.name) {
					case 'method':
						// メソッドが変更されたとき
						let row = DOMUtils.getParentElementByTagName(target, 'tr');
						if (target.value === 'POST') {
							// POSTメソッド
							row.classList.add('post');
						} else {
							// GETメソッド
							row.querySelector('[name=accept]').value = 'text/plain';
							// Firefox 24 ESR ではremoveメソッドの第2引数以降が無視される
							row.classList.remove('displaying-post-params');
							row.classList.remove('post');
						}
						break;

					case 'url':
						let url = target.value;
						if (url) {
							// URLのバリデート
							let validationMessage = '';
							try {
								NetUtil.newURI(url).QueryInterface(Ci.nsIURL);
							} catch (e) {
								if (e.result === Cr.NS_ERROR_MALFORMED_URI || e.result === Cr.NS_NOINTERFACE) {
									// 妥当なURLでなければ
									validationMessage = _('http:// などで始まるURLを入力してください。');
								} else {
									throw e;
								}
							}
							if (target.validationMessage !== validationMessage) {
								target.setCustomValidity(validationMessage);
							}
						}
						break;

					case 'add-browser-engine':
						// ブラウザの検索エンジンの追加
						this.insertEngine(JSON.parse(target.selectedOptions[0].dataset.engine));
						target[0].selected = true;
						break;
				}
				break;

			// 行の並び替え
			case 'dragstart':
				if (target.mozMatchesSelector('[draggable], [draggable] *')) {
					let row = DOMUtils.getParentElementByTagName(target, 'tr'), dataTransfer = event.dataTransfer;
					dataTransfer.setDragImage(row, 0, 0);
					dataTransfer.setData('application/x-sectionrowindex', row.sectionRowIndex);
					this.duringRowDrag = true;
				}
				break;

			case 'dragover':
				if (this.duringRowDrag) {
					let row = DOMUtils.getParentElementByTagName(target, 'tr');
					if (row) {
						event.preventDefault();
						this.resetDropzoneClasses();
						switch (row.parentElement.localName) {
							case 'thead':
								DOMUtils.getParentElementByTagName(target, 'table').tBodies[0].rows[0].classList.add('active-dropzone-above');
								break;
							case 'tbody':
								let rect = row.getBoundingClientRect();
								if (event.clientY - rect.top < rect.height / 2) {
									// ポインタが行の真ん中より上にあれば
									row.classList.add('active-dropzone-above');
								} else {
									// ポインタが行の真ん中より下にあれば
									row.classList.add('active-dropzone-below');
								}
								break;
							case 'tfoot':
								let tbody = DOMUtils.getParentElementByTagName(target, 'table').tBodies[0];
								tbody.rows[tbody.rows.length - 1].classList.add('active-dropzone-below');
								break;
						}
					}
				}
				break;

			case 'dragleave':
				if (this.duringRowDrag && !target.ownerDocument.getElementsByTagName('table')[0].contains(event.relatedTarget)) {
					this.resetDropzoneClasses();
				}
				break;

			case 'drop':
				if (this.duringRowDrag) {
					let doc = this.tab.document;
					let refChild = doc.getElementsByClassName('active-dropzone-above')[0];
					let tbody;
					if (refChild) {
						tbody = refChild.parentElement;
					} else {
						let targetRow = doc.getElementsByClassName('active-dropzone-below')[0];
						tbody = targetRow.parentElement;
						if (targetRow) {
							refChild = targetRow.nextElementSibling;
						} else {
							return;
						}
					}
					tbody.insertBefore(tbody.rows[event.dataTransfer.getData('application/x-sectionrowindex')], refChild);
					this.resetDropzones();
				}
				break;

			case 'dragend':
				if (this.duringRowDrag) {
					this.resetDropzones();
				}
				break;
		}
	},

	/**
	 * 行の並べ替え終了時の処理。
	 */
	resetDropzones: function () {
		this.duringRowDrag = false;
		this.resetDropzoneClasses();
	},

	/**
	 * 行の並べ替えに関するクラス名の削除。
	 */
	resetDropzoneClasses: function () {
		for (let row of this.tab.document.querySelectorAll('.active-dropzone-above, .active-dropzone-below')) {
			// Firefox 24 ESR ではremoveメソッドの第2引数以降が無視される
			row.classList.remove('active-dropzone-above');
			row.classList.remove('active-dropzone-below');
		}
	},

	/**
	 * アイコンが設定されていない行について、アイコンを一括取得し設定する。
	 * @param {Function} [callback]
	 * @access protected
	 */
	setIconsToEngineWithout: function (callback = function () { }) {
		let emptyIcons = this.tab.document.querySelectorAll('tbody > tr [name=icon]:not([value]), tbody > tr [name=icon][value=""]');

		// 進行状況
		let progress = doc.createElement('progress');
		progress.max = emptyIcons.length;
		progress.value = 0;
		target.parentElement.replaceChild(progress, target);

		(function getFavicon() {
			let icon = emptyIcons[progress.value];
			if (icon) {
				let url = DOMUtils.getParentElementByTagName(icon, 'tr').querySelector('[name=url]').value;
				if (url) {
					IconUtils.getFaviconFromSiteUrl(url, function (dataURL) {
						icon.firstElementChild.src = icon.value = dataURL;
						progress.value = Number(progress.value) + 1;
						getFavicon();
					}, function () {
						progress.value = Number(progress.value) + 1;
						getFavicon();
					});
				} else {
					progress.value = Number(progress.value) + 1;
					getFavicon();
				}
			} else {
				// すべてのアイコンを取得し終えたら
				progress.parentElement.replaceChild(target, progress);
				callback();
			}
		})();
	},

	/**
	 * 行を削除する。
	 * @param {HTMLButtonElement} deleteButton - 削除ボタン。
	 * @access protected
	 */
	deleteEngine: function (deleteButton) {
		let row = DOMUtils.getParentElementByTagName(deleteButton, 'tr');
		row.remove();
		let name = row.querySelector('[name=name]');
		if (name.readOnly) {
			// 検索窓の検索エンジンを削除していれば、検索窓の検索エンジンを追加するセレクトボックスで選択可能に
			deleteButton.ownerDocument.querySelector('[name=add-browser-engine] [label=' + StringUtils.quote(name.value) + ']').hidden = false;
		}
	},

	/**
	 * 指定した位置に空行を作成。
	 * @param {HTMLElement} target - 挿入位置の基準になる行に含まれる要素、または行を追加するtable要素。
	 * @param {boolean} [insertingBefore=false] - 基準になる行の上に挿入するならtrue。
	 * @param {boolean} [focus=false] - 新しい行にフォーカスを移すならtrue。
	 */
	insertEmptyRow: function(target, insertingBefore = false, focus = false) {
		let table = target.localName === 'table' ? target : DOMUtils.getParentElementByTagName(target, 'table');
		let targetRow = null;
		if (target.localName !== 'table' && target.name !== 'add-row') {
			let row = DOMUtils.getParentElementByTagName(target, 'tr');
			targetRow = insertingBefore ? row : row.nextElementSibling;
		}

		let row = table.parentElement.localName === 'form'
				? this.insertEngine(null, targetRow)
				: table.tBodies[0].insertBefore(table.getElementsByTagName('template')[0].content.firstChild.cloneNode(true), targetRow);

		if (focus && /^(?:input|select)$/.test(target.localName)) {
			// 追加した行にフォーカスを移す
			row.querySelector('[name=' + event.target.name + ']').focus();
		}
	},

	/**
	 * 指定された位置に行を挿入する。
	 * @param {SearchEngine} [engine] - 指定しなかった場合は空行を挿入する。
	 * @param {HTMLTableRowElement} [child] - 指定しなかった場合は表本体の末尾に追加する。
	 * @return {?HTMLTableRowElement} ブラウザの検索エンジンが壊れたURLを保持していた場合は行を挿入しない。
	 */
	insertEngine: function (engine = null, child = null) {
		let doc = this.tab.document;
		let tbody = doc.getElementsByTagName('tbody')[0];
		let row = tbody.getElementsByTagName('template')[0].content.firstChild.cloneNode(true);

		let paramsTbody = row.getElementsByTagName('tbody')[0];
		let paramTemplate = paramsTbody.getElementsByTagName('template')[0].content.firstChild;
		if (!engine || engine.method === 'GET') {
			// GETメソッド
			let row = paramTemplate.cloneNode(true);
			row.querySelector('[name=post-param-value]').value = '{searchTerms}';
			paramsTbody.appendChild(row);
		}

		if (engine) {
			if (engine.browserSearchEngine) {
				// ブラウザの検索エンジンなら
				row.classList.add('browser-search-engine');
				doc.querySelector('[name=add-browser-engine] [label=' + StringUtils.quote(engine.name) + ']').hidden = true;
			} else {
				if (engine.method === 'POST') {
					// POSTメソッド
					row.classList.add('post');
				}
			}
			for (let input of row.querySelectorAll('[name]')) {
				let name = input.name;
				if (engine[name]) {
					if (name === 'params') {
						// POSTパラメータなら
						if (!engine.browserSearchEngine) {
							let params = engine[name] || [];
							if (engine.browserSearchEngine) {
								// ブラウザの検索エンジンなら
								row.getElementsByTagName('tfoot')[0].remove();
							} else {
								params = params.concat([['', '']]);
							}
							params.forEach(function ([name, value]) {
								let row = paramTemplate.cloneNode(true);
								let nameInput = row.querySelector('[name=post-param-name]');
								let valueInput = row.querySelector('[name=post-param-value]');
								nameInput.value = name;
								valueInput.value = value;
								if (engine.browserSearchEngine) {
									// ブラウザの検索エンジンなら
									nameInput.readOnly = true;
									valueInput.readOnly = true;
									row.querySelector('[name=delete]').remove();
								}
								paramsTbody.appendChild(row);
							});
						}
					} else {
						if (engine[name]) {
							input.value = engine[name];
						}
						if (engine.browserSearchEngine) {
							// ブラウザの検索エンジンなら
							switch (name) {
								case 'name':
									input.readOnly = true;
									break;
								case 'url':
									input.readOnly = true;
									if (engine.method === 'POST') {
										let url;
										try {
											url = NetUtil.newURI(engine[name]).QueryInterface(Ci.nsIURL);
										} catch (e) {
											if (e.result === Cr.NS_ERROR_MALFORMED_URI || e.result === Cr.NS_NOINTERFACE) {
												// 妥当なURLでなければ
												return null;
											} else {
												throw e;
											}
										}
										let searchParams = new URLSearchParams(url.query);
										for (let [name, value] of engine.params) {
											searchParams.append(name, value);
										}
										url.query = searchParams.toString().replace(/%7BsearchTerms%7D/g, '{searchTerms}');
										input.value = url.spec;
									}
									break;
								case 'icon':
									let cell = DOMUtils.getParentElementByTagName(input, 'td');
									let img = input.firstElementChild;
									cell.replaceChild(img, input);
									img.src = engine[name] || DropzoneUtils.DEFAULT_ICON;
									break;
								default:
									let value;
									if (input.localName === 'select') {
										if (!('selectedOptions' in input)) {
											// Firefox 24 ESR
											Object.defineProperty(input, 'selectedOptions', {
												enumerable: false,
												configurable: true,
												get: function () {
													return this.querySelectorAll(':checked');
												},
											})
										}
										value = input.selectedOptions[0].text;
									} else {
										value = input.value;
									}
									DOMUtils.getParentElementByTagName(input, 'td').textContent = value;
							}
						}
						switch (name) {
							case 'icon':
								if (!engine.browserSearchEngine && engine[name]) {
									input.firstElementChild.src = engine[name];
								}
								break;
							case 'name':
								input.dataset.value = engine[name];
								break;
						}
					}
				}
			}
		}

		if (engine && !child) {
			// 空行の挿入ではない、かつ末尾への追加なら
			let rows = tbody.rows;
			// Firefox 24 ESRにはArray#findが実装されていない
			let previousRowSibling;
			for (let row of Array.prototype.slice.call(rows).reverse()) {
				// 行リストを末尾から走査
				if (row.querySelector('[name=name]').value.trim() !== '' || row.querySelector('[name=url]').value.trim() !== '') {
					// 空行でなければ
					previousRowSibling = row;
					break;
				}
			}
			child = previousRowSibling ? previousRowSibling.nextElementSibling : rows[0];
		}
		return tbody.insertBefore(row, child);
	},

	/**
	 * 行をドラッグ中ならtrue。
	 * @type {boolean}
	 * @access protected
	 */
	duringRowDrag: false,

	/**
	 * イベントが発生したタブに設定画面を描画する。
	 * @access protected
	 */
	show: function () {
		let doc = this.tab.document, win = doc.defaultView;

		Application.storage.set(DragAndDropZonesPlus.ID + '_settingsTab', this.tab);

		this.printStatic();

		// ブラウザの検索エンジンを追加するセレクトボックス
		let addingBrowserEngine = doc.getElementsByName('add-browser-engine')[0];
		for (let engine of SearchUtils.getBrowserEngines()) {
			let option = new Option(engine.name);
			option.label = engine.name;
			option.dataset.engine = JSON.stringify(engine);
			addingBrowserEngine.add(option);
		}

		let table = doc.getElementsByTagName('table')[0];
		let tbody = table.tBodies[0];

		doc.addEventListener('click', this);

		tbody.addEventListener('contextmenu', this);
		tbody.addEventListener('keypress', this);
		doc.addEventListener('submit', this);
		doc.addEventListener('change', this);
		win.addEventListener('beforeunload', this);

		tbody.addEventListener('dragstart', this);
		table.addEventListener('dragover', this);
		table.addEventListener('dragleave', this);
		table.addEventListener('drop', this);
		win.addEventListener('dragend', this);

		// 設定を表示
		for (let engine of SearchUtils.getEngines()) {
			this.insertEngine(engine);
		}

		// 空行を追加
		this.insertEngine();
	},

	/**
	 * 表に検索エンジンを追加する。
	 * @param {SearchEngine[]} engines
	 * @access protected
	 */
	addEngines: function (engines) {
		let doc = this.tab.document;

		let addingBrowserEngine = doc.getElementsByName('add-browser-engine')[0];
		for (let engine of engines) {
			let option = addingBrowserEngine.querySelector('[label=' + StringUtils.quote(engine.name) + ']');
			if (option) {
				// 同名の検索エンジンがブラウザに存在すれば
				if (!option.hidden) {
					// それが追加されていないエンジンなら
					this.insertEngine(JSON.parse(option.dataset.engine));
					option.hidden = true;
				}
			} else {
				this.insertEngine(engine);
			}
		}
	},

	/**
	 * 取得したアイコンをアイコン設定ボタンに表示する。
	 * @param {string} dataURL - アイコンのDataURL
	 * @access protected
	 */
	showIcon: function (dataURL) {
		if (dataURL.length > SettingsUtils.MAX_PREFERENCE_VALUE_LENGTH) {
			showPopupNotification(_('アイコンの設定に失敗しました。約 %s KiB までの画像を設定できます。').replace('%s', SettingsUtils.MAX_PREFERENCE_VALUE_LENGTH / this.BASE64_SIZE_RATIO / 1024), this.tab, 'warning');
		} else {
			this.selectedElement.firstElementChild.src = this.selectedElement.value = dataURL;
		}
	},

	/**
	 * ファイルから設定を追加する。
	 * @param {Function} [callback] - 第1引数にファイル名。インポートに失敗していれば、第2引数にエラーメッセージ。
	 * @access protected
	 */
	addEnginesFromFile: function (callback = function () { }) {
		SearchUtils.getSearchEnginesFromFile(this.tab.document.defaultView, (engines, settingsDocument, fileName, errorMessage) => {
			if (engines) {
				this.addEngines(engines);
				callback(fileName);
			} else {
				callback(fileName, errorMessage);
			}
		});
	},

	/**
	 * 設定画面を閉じる。
	 * @access protected
	 */
	close: function () {
		let fuelWindow = this.tab.window;
		let tabsLength = fuelWindow.tabs.length;
		if (tabsLength === 1) {
			// 設定画面以外のタブが存在しなければ
			if (Application.windows.length === 1) {
				// 他にウィンドウが存在しなければ、ホームページに移動する
				let homePage = gHomeButton.getHomePage();
				this.tab.document.defaultView.location.assign(homePage === 'about:blank' ? 'about:home' : homePage);
			} else {
				FUELUtils.getChromeWindow(fuelWindow).close();
			}
		} else {
			this.tab.close();
		}
	},

	/**
	 * 設定画面の静的部分を描画。
	 * @access protected
	 */
	printStatic: function () {
		let doc = this.tab.document;

		// Favicon
		let link = doc.createElement('link');
		link.rel = 'icon';
		link.href = this.ICON;
		doc.head.appendChild(link);

		// タイトル
		doc.title = DragAndDropZonesPlus.NAME;

		// スタイルシート
		let styleSheet = doc.head.appendChild(doc.createElement('style')).sheet;
		let cssRules = styleSheet.cssRules;
		// Bug 906353 – Add support for css4 selector :matches(), the standard of :-moz-any(). <https://bugzilla.mozilla.org/show_bug.cgi?id=906353>
		[
			':root {'
					+ 'height: 100%;'
					+ 'color: -moz-DialogText;'
					+ 'background: -moz-Dialog;'
					+ '}',

			// 行のドラッグ
			'[draggable=true],' +
			'[draggable=true] [readonly]:not([name=name]),' +
			'td table [readonly] {'
					+ 'cursor: move;'
					+ '}',

			'[name=name] {'
					+ 'width: 150px;'
					+ '}',

			'[name=url] {'
					+ 'width: 400px;'
					+ '}',

			'input:not([type]), [type=text], [type=url] {'
					+ 'width: 100%;'
					+ '}',

			// 検索窓のエンジン
			'.browser-search-engine {'
					+ 'font: -moz-field;'
					+ '}',
			'.browser-search-engine > :not(:first-child):not(:last-child) {'
					+ 'padding-left: 8px;'
					+ '}',
			'.browser-search-engine input {'
					+ 'margin-left: -2px;'
					+ 'background-color: transparent;'
					+ 'border: none;'
					+ 'text-overflow: ellipsis ellipsis;'
					+ '}',

			// 行の背景色・枠線
			'table {'
					+ 'border-collapse: collapse;'
					+ 'width: 100%;'
					+ '}',
			'thead th {'
					+ '-moz-appearance: treeheadercell;'
					+ 'font-weight: normal;'
					+ '}',
			'th, td {'
					+ 'padding: 3px;'
					+ '}',
			'tbody > tr {'
					+ 'background: whitesmoke;'
					+ '}',
			'tbody > tr:nth-child(2n) {'
					+ 'background: gainsboro;'
					+ '}',
			'thead {'
					+ 'border-top: solid 1px gray;'
					+ 'border-left: solid 1px gray;'
					+ 'border-right: solid 1px gray;'
					+ '}',
			'tbody {'
					+ 'border-left: solid 1px gray;'
					+ 'border-bottom: solid 1px gray;'
					+ 'border-right: solid 1px gray;'
					+ '}',

			// 行の追加ボタン
			'tfoot td {'
					+ 'padding: 0;'
					+ '}',
			'[name=add-row]::before {'
					+ 'content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAASCAYAAABSO15qAAAAtElEQVR42sXTvwuCQBjG8e7feAkaa2hUorEb+jF1DeJSqFBLiK1Bf/xTj8TLoQ6eIQ0fOE75cveiEwA/GT1gaGhAfKEB8yHTRQz6BkxwYGUdKCRgPGLdBaQB1R2Qpl1SgLqeacA/8my5xjza1GJ7RFJUxLXu8x09UTMQWYdDeq25rERevYhr3Y+8mXRdYe87358grhuk1xBP+QPUb4jtkLisAg3+DrbpDRQeaM9E/vIzqVEDb9gqPHy3H+TDAAAAAElFTkSuQmCC");'
					+ 'margin-right: 0.5em;'
					+ 'vertical-align: -4px;'
					+ '}',
			'[name=add-row] {'
					+ 'border-top: none;'
					+ 'border-left: solid 1px gray;'
					+ 'border-bottom: solid 1px gray;'
					+ 'border-right: solid 1px gray;'
					+ 'border-radius: 0 0 0.2em 0.2em;'
					+ 'background: linear-gradient(lightgrey, silver);'
					+ 'position: relative;'
					+ 'top: -1px;'
					+ 'left: -1px;'
					+ '}',
			'[name=add-row]:not([disabled]):-moz-any(:hover, :focus, :active) {'
					+ 'background: gainsboro;'
					+ '}',

			// キャンセル・OKボタン
			'#submit-buttons {'
					+ 'text-align: right;'
					+ '}',
			'#submit-buttons button {'
					+ 'margin-left: 1em;'
					+ '}',
			'button:not([type]), button[type=submit] {'
					+ 'font-size: 2em;'
					+ '}',

			// その他の操作
			':not(td) > select {'
					+ 'margin-left: 0.5em;'
					+ '}',
			'form > label {'
					+ 'display: block;'
					+ '}',

			// キーボードショートカット
			'#shortcut-keys ul {'
					+ 'list-style: none;'
					+ 'padding-left: 0;'
					+ '}',
			'#shortcut-keys :not(kbd) > kbd:last-of-type::after {'
					+ 'margin-right: 1em;'
					+ 'content: ":";'
					+ '}',
			'kbd kbd {'
					+ 'font-size: 0.75em;'
					+ 'border-radius: 0.5em;'
					+ 'border-style: solid;'
					+ 'border-width: 0.15em 0.3em 0.45em;'
					+ 'border-color: rgba(0,0,0,0.2) rgba(0,0,0,0.1) rgba(255,255,255,0.2);'
					+ 'background-origin: border-box;'
					+ 'background: gainsboro;'
					+ 'color: black;'
					+ 'display: inline-flex;'
					+ 'align-items: center;'
					+ '-moz-box-sizing: border-box;'
					+ 'box-sizing: border-box;'
					+ 'padding-left: 0.3em;'
					+ 'width: 3em;'
					+ 'height: 3em;'
					+ 'margin-left: 0.2em;'
					+ 'margin-right: 0.2em;'
					+ '}',
			'.control {'
					+ 'width: 4em;'
					+ 'color: transparent;'
					+ '}',
			'.control::before {'
					+ 'content: "Ctrl";'
					+ 'color: black;'
					+ '}',
			'.shift {'
					+ 'width: 5em;'
					+ '}',
			'.shift::before {'
					+ 'content: "⇧";'
					+ 'margin-right: 0.2em;'
					+ '}',
			'.alt {'
					+ 'color: darkcyan;'
					+ '}',
			'.enter {'
					+ 'width: 4.5em;'
					+ 'height: 5em;'
					+ '}',
			'.enter::after {'
					+ 'content: "⏎";'
					+ '}',

			'td {'
					+ 'height: 100%;'	// セル内で高さのパーセント指定が使えるように
					+ '}',

			// アイコン
			'td > img {'
					+ 'display: block;'
					+ 'margin-left: auto;'
					+ 'margin-right: auto;'
					+ '}',
			'[name=icon] {'
					+ 'width: 100%;'
					+ 'height: 100%;'
					+ '}',

			// 行の削除ボタン
			'[name=delete] {'
					+ 'border: none;'
					+ 'padding: 0;'
					+ 'background: transparent;'
					+ 'width: 100%;'
					+ 'height: 100%;'
					+ 'overflow: hidden;'
					+ 'border-radius: 0.5em;'
					+ '}',
			'[name=delete]:not([disabled]):-moz-any(:hover, :focus, :active) {'
					+ 'box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3) inset, -1px 0 2px rgba(0, 0, 0, 0.3) inset, 0 -1px 1px rgba(255, 255, 255, 0.3) inset;'
					+ '}',
			'[name=delete]:not([disabled]):active {'
					+ 'background-color: lightcoral;'
					+ '}',
			'tbody tr:only-of-type [name=delete] {'
					// 行が一つだけなら、削除ボタンは表示しない
					+ 'visibility: hidden;'
					+ '}',
			// Bug 895182 – [CSS Filters] Implement parsing for blur, brightness, contrast, grayscale, invert, opacity, saturate, sepia <https://bugzilla.mozilla.org/show_bug.cgi?id=895182>
			'[name=delete] img {'
					+ 'background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAABDklEQVQoz3WRv0oDQRCHv93bk4NgaxFtzhQWwUbb9CkECznUIlZp8i6+gY2NVZCAjZUpgg9gYR2DcPoACYTg3e5Y7J4X/82P3WJm+O3sN0oUsH4ApD4GUN8E8iXAhLQO8uGCqBw0UZB3sFgs4CoHTURMvNZQhKfQoWzSQWPS7pOQtPuNSTrAEKHR1ZB6Nj5t3ncPBF67Rw/DMRqFQpQYDDEbJLR6J7fbkL3djJiy4oMCMdSRL+dbmyWLOXmdNAGM4A7PF52zJ8uws796vq45hPLeRZEdP17ewdKMst3y5cq3KIn8L2iR8k4O7NBkxpQSC0o8wb9AWQ8KBBdu+wO1gPGj4BDcr2VJ5QDy37o/ASA8darOg/vVAAAAAElFTkSuQmCC");'
					+ '}',
			'[name=delete]:active img {'
					+ 'background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAABBklEQVQoz32RP0sDQRDFf/vnIAQObAQJgkQhRcBvELAMWEoIWlilyRfwy9hYqySgYGeKYK0WkiKFHsKhYBkICmZvLXYuZw5xHrPFzPD28Z5CwUoD+KItoFYAfgnAylgLQmUCcgaNEQQGh8MBWc6gMUREvw6+5Su0rG3Sn48nPSpUJr35OOljMWh0LlLXRxe1/faDh632zW13hEah8AYdGPi8fG+u7TXWN4bTowGvuCDTUlRanX3ElnhGWgytGOPJ7g/j1vmjodt6+to9K3yQ9fQ46lzfnVxB1R50Xhbbp+FEYUTDDnXeSIFNaiQ8s8Cx9PAvo1wu0pPJ60pW+3JQ5bDk4N+4fwD4m2u8dB+ciAAAAABJRU5ErkJggg==");'
					+ '}',

			// 行の移動
			'.active-dropzone-above {'
					+ 'border-top: solid 0.5em lightblue;'
					+ '}',
			'.active-dropzone-below {'
					+ 'border-bottom: solid 0.5em lightblue;'
					+ '}',

			// POSTパラメータの開閉ボタン
			'td > div {'
					+ 'display: flex;'
					+ 'align-items: flex-start;'
					+ '}',
			'[name=url] {'
					+ 'flex-grow: 1;'
					+ '}',
			'[name=params] {'
					+ 'display: none;'
					+ 'white-space: nowrap;'
					+ 'border: 1px solid lightblue;'
					+ 'box-shadow: 0px -2px 0px rgba(204, 223, 243, 0.3) inset, 0px 0px 1px rgba(0, 0, 0, 0.1);'
					+ 'border-radius: 5px;'
					+ 'background: linear-gradient(ghostwhite, aliceblue)  ghostwhite;'
					+ 'vertical-align: middle;'
					+ '}',
			'[name=params]::after {'
					+ 'content: "";'
					+ 'display: inline-block;'
					+ 'width: 20px;'
					+ 'height: 20px;'
					+ 'background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAADUCAYAAACcYQO5AAAEdElEQVR4nO2avWsbSRjGBQaDQWAwBAyuDAEXwa3BxQVlZwwGVy70F6g6N8YhiqUZCCoC7rawWM37QCAQcKPCYDA4JDj6mCGQ1lXAkMqVwGAwGAQBXeFdZbxaSZvzHtxx84MttDv7aD/ebR5+udx/BiI6JiL4vj+X9pwgCPJEBCI6TgosARgQ0Xel1Nq0MKXUGoDL8JxS4iIAZQADAH2llCwWizPxNcVicYaI3gD4Ga4tT/xnAAfhwgERfa3X68vRsXq9vkxE36LjAA6m3UkU2rBCb4moFG63VlgjVVh0WwCOrJPj21HS45hIrVabBXCaEHZaq9VmpwYQ0Uaj0Vix9/m+PwdAW2E6PlaNRmOFiDZHAgG0iegOwJ/2/iAI8kopCaAcDyOi3fCc9kig7/sLAC7CF3F2eHj4ZNzdKKWWiOgsvOoL3/cXxi6MhhVATym1nbBmG0AvXHOplFoa98e5XG44a1fW2CAIgnz4mb2z9l8FQfB0YlgEgFXrKgYAfoRb9PsawGqqMOvW1mKDPBz0NN/6uFAPQN8K7CulvL8VFgFgK7zSGyLaeFSYw+FwOBwOh8PhcDgc/3eY0MdMaKzvNVP3h4WdVp4JDSb0aH/IqqbEpRkwab57sju1U/Bkd40Lc8mlGbCqSe4PueiWuTQDLk3fE1rmis3RwqzYnGHV7hsu9E8uzYCL7uT+kEt9EIYOmDBfn7/qDPvD5686y0zob9FxLnW6/pAL0xiGSn3LqqbEqqbEpL4dhgmTvj/MFZszXOijX1cS24Q+Snwck3hWbM5yYU5Hw8zps2Jzen/IhNl48frLg/5wfa85x6XW1jPT8bF68frLCqvo0f6QC91mwtzxavdBf1jYaeU9oSUX3XI8jAm9y4S540K3RwLX9z4ucGkuwhdx9ke1O7Y/9OT5EpP6LLzyi/W9j8n9oSfPl6Jh5UL3PKlH+kNP6m0udC98npeePJ/cH97Pmrn6NYsahZ1WvrDTyjOp31kzelWotNL1h3y/szq8ivuX8eN+G77ta77f+b3+0JPdtQeDbA16mm89ObTS8bg0fSuw71U6j+sPuehuMalvuTA3TBjXHzocDofD4XA4HA6Hw/FPAGArdJR6jxZ4MlWMMpWgUmhavdSaVoJI9j4SyQB8sEUyW3ZMJAiCxZSq202kugVBsDjuNudtGW+SExfKeJ8iGQ/AfFJgm4juiGjX3h86iJUxuuDLsbogEW2OERrb1stox0MBrCYKjXFC5fIkQbk8SaVcxslUCs1UWwXw1gp7nFiLLNXfzOXkzPVph8PhcDgcDofD4XA4HOnhorvFpbnmQvceLfBkqhhlKkFN1bSE7qXWtEZFMvM+Esm41B9skcyWHRMplFuL6VQ3cxOpboVyK7k/5Puf520Zb5IT58nzJSb0p0jG4/ufR/vDSBdkQj/oD9f3mnO8aiqJumBVvxyrC7KK3kwUGoVuWy+jHQ/l+53VRKExTqhcniQolyeplMs4mUqhmWqrXOi3mYm1maq/mcvJmevT/1r+Ahtz/abguyZRAAAAAElFTkSuQmCC");'
					+ 'vertical-align: middle;'
					+ '}',
			'[name=params]:not([disabled]):-moz-any(:hover, :focus, :active)::after {'
					+ 'background-position: 0 -64px;'
					+ '}',

			// データの種類
			'[name=accept] :not([value="text/plain"]) {'
					+ 'display: none;'
					+ '}',

			// POSTメソッドが選択されているとき
			'.post [name=url] {'
					+ 'width: auto;'
					+ '}',
			'.post [name=params] {'
					+ 'display: inline-block;'
					+ '}',
			'.post [name=accept] option {'
					+ 'display: block;'
					+ '}',

			// POSTパラメータ
			'td table {'
					+ 'display: none;'
					+ 'border: 1px solid lightblue;'
					+ 'border-collapse: separate;'
					+ 'border-radius: 5px 0 5px 5px;'
					+ 'background: linear-gradient(aliceblue, lavender)  aliceblue;'
					+ 'margin-top: -2px;'
					+ '}',
			'td table tr {'
					+ 'background: transparent !important;'
					+ '}',
			'td table td {'
					+ 'padding-left: 0;'
					+ 'padding-right: 0;'
					+ '}',
			'td table [name=add-row] {'
					+ 'background: linear-gradient(aliceblue, lavender);'
					+ 'border: darkgray solid 1px;'
					+ 'border-radius: 5px;'
					+ '}',
			'td table [name=add-row]:not([disabled]):-moz-any(:hover, :focus, :active) {'
					+ 'background: aliceblue;'
					+ '}',

			// POSTパラメータ展開時
			'.displaying-post-params > * {'
					+ 'vertical-align: top;'
					+ '}',
			'.displaying-post-params > td > [name=delete] {'
					+ 'height: 1.6em;'
					+ '}',
			'.displaying-post-params [name=params] {'
					+ 'border-bottom: none;'
					+ 'border-bottom-left-radius: 0;'
					+ 'border-bottom-right-radius: 0;'
					+ 'box-shadow: none;'
					+ '}',
			'[name=params]:not([disabled]):-moz-any(:hover, :focus, :active)::after {'
					+ 'background-position: 0 -64px;'
					+ '}',
			'.displaying-post-params [name=params]::after {'
					+ 'background-position: 0 -128px;'
					+ '}',
			'.displaying-post-params [name=params]:not([disabled]):-moz-any(:hover, :focus, :active)::after {'
					+ 'background-position: 0 -192px;'
					+ '}',
			'.displaying-post-params table {'
					+ 'display: table;'
					+ '}',
		].forEach(function (rule) {
			styleSheet.insertRule(rule, cssRules.length);
		});

		let row, cell, input, select, option, menu, button, img, div, label, section, kbd, key, li, dl;

		let form = doc.createElement('form');
		let table = doc.createElement('table');
		if (!('createTBody' in table)) {
			// Firefox 24 ESR
			table.createTBody = function () {
				var tBodies = this.tBodies;
				var tBodiesLength = tBodies.length;
				return this.insertBefore(doc.createElement('tbody'), tBodiesLength > 0 ? tBodies[tBodiesLength - 1].nextSibling : null);
			}
		}

		// 見出し
		let thead = table.createTHead();
		let headRow = thead.insertRow();

		// 本体
		let tbody = table.createTBody();
		let template = doc.createElement('template');
		row = template.content.appendChild(doc.createElement('tr'));

		// アイコン
		headRow.appendChild(doc.createElement('th'));
		cell = row.insertCell();
		cell.draggable = true;
		input = doc.createElement('button');
		input.type = 'menu';
		input.name = 'icon';
		input.title = _('アイコンを変更');
		img = new Image(16, 16);
		img.src = DropzoneUtils.DEFAULT_ICON;
		img.alt = '';
		input.appendChild(img);
		cell.appendChild(input);

		menu = doc.body.appendChild(createMenu({
			'set-icon-from-local-file': _('ローカルファイルからアイコンを設定'),
			'set-icon-from-url': _('Webページ、または画像ファイルのURLからアイコンを設定'),
			'set-icon-from-clipboard': _('クリップボードのURL、または画像データからアイコンを設定'),
			'restore-default-icon': _('元のアイコンに戻す'),
		}, 'icon-menu'));
		input.setAttribute('menu', menu.id);

		// 検索エンジン名
		headRow.appendChild(doc.createElement('th')).textContent = _('検索エンジン名');
		cell = row.appendChild(doc.createElement('th'));
		input = doc.createElement('input');
		input.name = 'name';
		cell.appendChild(input);

		// URL
		headRow.appendChild(doc.createElement('th')).textContent = _('URL・POSTパラメータ');
		cell = row.insertCell();
		let urlWrapper = doc.createElement('div');
		input = doc.createElement('input');
		input.name = 'url';
		input.type = 'url';
		urlWrapper.appendChild(input);

		// POSTパラメータ開閉ボタン
		let params = doc.createElement('button');
		params.draggable = true;
		params.type = 'button';
		params.name = 'params';
		params.textContent = _('POSTパラメータの設定');
		urlWrapper.appendChild(params);
		cell.appendChild(urlWrapper);

		// POSTパラメータ
		let paramsTable = doc.createElement('table');
		if(!('createTBody' in paramsTable)){
			// Firefox 24 ESR
			paramsTable.createTBody = function () {
				var tBodies = this.tBodies;
				var tBodiesLength = tBodies.length;
				return this.insertBefore(doc.createElement('tbody'), tBodiesLength > 0 ? tBodies[tBodiesLength - 1].nextSibling : null);
			}
		}

		let paramsTbody = paramsTable.createTBody();
		let paramsTemplate = doc.createElement('template');
		let paramsRow = paramsTemplate.content.appendChild(doc.createElement('tr'));
		input = paramsRow.insertCell().appendChild(doc.createElement('input'));
		input.name = 'post-param-name';
		input.placeholder = _('名前');
		input = paramsRow.insertCell().appendChild(doc.createElement('input'));
		input.name = 'post-param-value';
		input.placeholder = _('値');

		// 行を削除するボタン
		let cellContainingDeleteRowButton = paramsRow.insertCell();
		button = doc.createElement('button');
		button.name = 'delete';
		button.type = 'button';
		img = new Image();
		button.title = img.alt = _('行を削除');
		img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjYCANAAAAMAABx6qFjgAAAABJRU5ErkJggg==';
		button.appendChild(img);
		cellContainingDeleteRowButton.appendChild(button);
		paramsRow.appendChild(cellContainingDeleteRowButton);

		// 行を追加するボタン
		let tfootContainingAddRowButton = paramsTable.createTFoot();
		button = doc.createElement('button');
		button.name = 'add-row';
		button.type = 'button';
		button.textContent = _('行を追加');
		let paramsCell = tfootContainingAddRowButton.insertRow().insertCell();
		paramsCell.colSpan = paramsRow.cells.length;
		paramsCell.appendChild(button);

		paramsTbody.appendChild(paramsTemplate);
		cell.appendChild(paramsTable);

		// メソッド
		headRow.appendChild(doc.createElement('th')).textContent = _('メソッド');
		cell = row.insertCell();
		cell.draggable = true;
		select = doc.createElement('select');
		select.name = 'method';
		select.add(new Option(_('GET'), 'GET'));
		select.add(new Option(_('POST'), 'POST'));
		cell.appendChild(select);

		// データの種類
		headRow.appendChild(doc.createElement('th')).textContent = _('データの種類');
		cell = row.insertCell();
		cell.draggable = true;
		select = doc.createElement('select');
		select.name = 'accept';
		select.add(new Option(_('文字列'), 'text/plain'), true);
		select.add(new Option(_('画像'), 'image/*'));
		select.add(new Option(_('音声'), 'audio/*'));
		cell.appendChild(select);

		// 文字符号化方式
		headRow.appendChild(doc.createElement('th')).textContent = _('文字符号化方式');
		cell = row.insertCell();
		cell.draggable = true;

		let encodingSelect = document.createElementNS(DOMUtils.HTML_NS, 'select');
		encodingSelect.hidden = true;
		encodingSelect.setAttribute('datasources', 'rdf:charset-menu');
		encodingSelect.setAttribute('ref', 'NC:DecodersRoot');
		let encodingTemplate = document.createElementNS(this.XUL_NS, 'template');
		let option = new Option('', '...');
		option.setAttribute('uri', '...');
		option.label = 'rdf:http://home.netscape.com/NC-rdf#Name';
		encodingTemplate.appendChild(option);
		encodingSelect.appendChild(encodingTemplate);
		document.documentElement.appendChild(encodingSelect);
		Services.obs.notifyObservers(null, 'charsetmenu-selected', 'other');

		select = encodingSelect.cloneNode(true);
		encodingSelect.remove();
		select.hidden = false;
		select.name = 'encoding';
		select.namedItem(StringUtils.THE_ENCODING).defaultSelected = true;
		for (let option of select.options) {
			option.removeAttribute('id');
			// Bug 40545 – (option-label) LABELs don't work for OPTIONs (<option label> in selects) <https://bugzilla.mozilla.org/show_bug.cgi?id=40545>
			option.text = option.label;
			option.removeAttribute('label');
		}
		cell.appendChild(select);

		// 行を削除するボタン
		headRow.appendChild(doc.createElement('th'));
		row.appendChild(cellContainingDeleteRowButton.cloneNode(true)).draggable = true;

		template.content.appendChild(row);
		tbody.appendChild(template);

		// 行のコンテキストメニュー
		row.setAttribute('contextmenu', doc.body.appendChild(createMenu({
			'add-row-above': _('上に新しい行を挿入'),
			'add-row-below': _('下に新しい行を挿入'),
		}, 'row-contextmenu')).id);

		// 行追加ボタン
		let tfoot = tfootContainingAddRowButton.cloneNode(true);
		tfoot.getElementsByTagName('td')[0].colSpan = paramsRow.cells.length;
		table.insertBefore(tfoot, table.tBodies[0]);

		form.appendChild(table);

		div = doc.createElement('div');
		div.id = 'submit-buttons';

		// アイコン一括取得ボタン
		button = doc.createElement('button');
		button.name = 'get-icons';
		button.type = 'button';
		button.textContent = _('アイコンを一括取得');
		button.title = _('アイコン未取得の検索エンジンについて、URLを基にアイコンを取得します。アイコンボタンのポップアップメニューの「元のアイコンに戻す」から、個別に取得することもできます。');
		div.appendChild(button);

		// キャンセルボタン
		button = doc.createElement('button');
		button.name = 'cancel';
		button.type = 'button';
		button.textContent = _('キャンセル');
		div.appendChild(button);

		// OKボタン
		button = doc.createElement('button');
		button.name = 'ok';
		button.textContent = _('OK');
		div.appendChild(button);

		form.appendChild(div);

		// ブラウザの検索エンジンの追加
		div = doc.createElement('div');
		div.textContent = _('検索窓のエンジンの追加') + ':';
		let addingBrowserEngine = doc.createElement('select');
		addingBrowserEngine.name = 'add-browser-engine';
		addingBrowserEngine.add(new Option(_('選択してください', '', true, true)));
		div.appendChild(addingBrowserEngine);
		form.appendChild(div);
		if (!('selectedOptions' in addingBrowserEngine)) {
			// Firefox 24 ESR
			Object.defineProperty(addingBrowserEngine, 'selectedOptions', {
				enumerable: false,
				configurable: true,
				get: function () {
					return this.querySelectorAll(':checked');
				},
			})
		}

		// 検索結果を開く場所
		label = doc.createElement('label');
		label.textContent = _('検索結果を開く場所') + ':';
		select = doc.createElement('select');
		select.name = 'where';
		select.add(new Option(_('現在のタブ。Ctrl、Shiftキーを押していれば、それぞれ新しいタブ、ウィンドウ'), 'current'));
		select.add(new Option(_('新しいタブ'), 'tab'));
		select.add(new Option(_('新しいウィンドウ'), 'window'));
		select.value = Application.prefs.getValue(SettingsUtils.ROOT_BRANCH_NAME + 'where', 'tab');
		label.appendChild(select);
		form.appendChild(label);

		// 検索エンジンの自動追加
		label = doc.createElement('label');
		let automaticallyReflect = doc.createElement('input');
		automaticallyReflect.name = 'automatically-reflect';
		automaticallyReflect.type = 'checkbox';
		automaticallyReflect.checked = Application.prefs.getValue(SettingsUtils.ROOT_BRANCH_NAME + 'automaticallyReflect', true);
		label.appendChild(automaticallyReflect);
		label.appendChild(new Text(_('検索窓に新しい検索エンジンが追加されたとき、自動的にドロップゾーンとしても追加する。')));
		form.appendChild(label);

		doc.body.appendChild(form);

		// 説明
		section = doc.createElement('section');
		let manual = doc.createElement('ul');
		manual.appendChild(doc.createElement('li')).textContent = _('行をドラッグ & ドロップで、順番を変更できます。');
		manual.appendChild(doc.createElement('li')).textContent = _('アイコンボタンのポップアップメニューから、アイコンを変更できます。検索窓のエンジンのアイコンは変更できません。');
		section.appendChild(manual);
		doc.body.appendChild(section);

		// ショートカットの説明
		section = doc.createElement('section');
		section.id = 'shortcut-keys';
		section.appendChild(doc.createElement('h1')).textContent = _('テキスト入力欄のキーボードショートカット');
		let shortcuts = doc.createElement('ul');

		li = doc.createElement('li');
		kbd = doc.createElement('kbd');
		key = doc.createElement('kbd');
		key.textContent = 'Shift';
		key.classList.add('shift');
		kbd.appendChild(key);
		kbd.appendChild(new Text('+'));
		key = doc.createElement('kbd');
		key.textContent = 'Enter';
		key.classList.add('enter');
		kbd.appendChild(key);
		li.appendChild(kbd);
		li.appendChild(new Text(_('上に新しい行を挿入します。')));
		shortcuts.appendChild(li);

		li = doc.createElement('li');
		kbd = doc.createElement('kbd');
		key = doc.createElement('kbd');
		key.textContent = 'Control';
		key.classList.add('control');
		kbd.appendChild(key);
		kbd.appendChild(new Text('+'));
		key = doc.createElement('kbd');
		key.textContent = 'Enter';
		key.classList.add('enter');
		kbd.appendChild(key);
		li.appendChild(kbd);
		li.appendChild(new Text(_('または')));
		kbd = doc.createElement('kbd');
		key = doc.createElement('kbd');
		key.textContent = 'Alt';
		key.classList.add('alt');
		kbd.appendChild(key);
		kbd.appendChild(new Text('+'));
		key = doc.createElement('kbd');
		key.textContent = 'Enter';
		key.classList.add('enter');
		kbd.appendChild(key);
		li.appendChild(kbd);
		li.appendChild(new Text(_('下に新しい行を挿入します。')));
		shortcuts.appendChild(li);

		li = doc.createElement('li');
		kbd = doc.createElement('kbd');
		key = doc.createElement('kbd');
		key.textContent = 'Control';
		key.classList.add('control');
		kbd.appendChild(key);
		kbd.appendChild(new Text('+'));
		kbd.appendChild(doc.createElement('kbd')).textContent = '↑';
		li.appendChild(kbd);
		li.appendChild(new Text(_('または')));
		kbd = doc.createElement('kbd');
		key = doc.createElement('kbd');
		key.textContent = 'Alt';
		key.classList.add('alt');
		kbd.appendChild(key);
		kbd.appendChild(new Text('+'));
		kbd.appendChild(doc.createElement('kbd')).textContent = '↑';
		li.appendChild(kbd);
		li.appendChild(new Text(_('上の行に移動します。')));
		shortcuts.appendChild(li);

		li = doc.createElement('li');
		kbd = doc.createElement('kbd');
		key = doc.createElement('kbd');
		key.textContent = 'Control';
		key.classList.add('control');
		kbd.appendChild(key);
		kbd.appendChild(new Text('+'));
		kbd.appendChild(doc.createElement('kbd')).textContent = '↓';
		li.appendChild(kbd);
		li.appendChild(new Text(_('または')));
		kbd = doc.createElement('kbd');
		key = doc.createElement('kbd');
		key.textContent = 'Alt';
		key.classList.add('alt');
		kbd.appendChild(key);
		kbd.appendChild(new Text('+'));
		kbd.appendChild(doc.createElement('kbd')).textContent = '↓';
		li.appendChild(kbd);
		li.appendChild(new Text(_('下の行に移動します。')));
		shortcuts.appendChild(li);

		section.appendChild(shortcuts);
		doc.body.appendChild(section);

		// インポートとエクスポート
		section = doc.createElement('section');
		section.appendChild(doc.createElement('h1')).textContent = _('インポートとエクスポート');
		dl = doc.createElement('dl');

		button = doc.createElement('button');
		button.name = 'export';
		button.type = 'button';
		button.textContent = _('エクスポート');
		dl.appendChild(doc.createElement('dt')).appendChild(button);
		dl.appendChild(doc.createElement('dd')).textContent = _('現在の設定をファイルへエクスポートします。保存していない設定は反映されません。');

		button = doc.createElement('button');
		button.name = 'import';
		button.type = 'button';
		button.textContent = _('インポート');
		dl.appendChild(doc.createElement('dt')).appendChild(button);
		dl.appendChild(doc.createElement('dd')).textContent = _('現在の設定をすべて削除し、XMLファイルから設定をインポートします。ブラウザの検索エンジンサービスに同名の検索エンジンが存在する場合は、そちらを優先します。');

		button = doc.createElement('button');
		button.name = 'additional-import';
		button.type = 'button';
		button.textContent = _('追加インポート');
		dl.appendChild(doc.createElement('dt')).appendChild(button);
		dl.appendChild(doc.createElement('dd')).textContent = _('XMLファイルから検索エンジンを追加します。同名の検索エンジンがすでに存在する場合は上書きします。');

		button = doc.createElement('button');
		button.name = 'import-from-text';
		button.type = 'button';
		button.textContent = _('JSON文字列から追加インポート');
		dl.appendChild(doc.createElement('dt')).appendChild(button);
		dl.appendChild(doc.createElement('dd')).textContent = _('本スクリプトのバージョン1でエクスポートしたJSON文字列から、検索エンジンを追加します。');

		section.appendChild(dl);
		doc.body.appendChild(section);

		// 初期化やアンインストールについて
		section = doc.createElement('section');
		section.appendChild(doc.createElement('h1')).textContent = _('その他');
		let dl = doc.createElement('dl');

		button = doc.createElement('button');
		button.name = 'initialize';
		button.type = 'button';
		button.textContent = _('設定を初期化');
		dl.appendChild(doc.createElement('dt')).appendChild(button);
		dl.appendChild(doc.createElement('dd')).textContent = _('すべての設定を削除し、初回起動時の状態に戻します。');

		button = doc.createElement('button');
		button.name = 'uninstall';
		button.type = 'button';
		button.textContent = _('すべての設定を削除');
		dl.appendChild(doc.createElement('dt')).appendChild(button);
		dl.appendChild(doc.createElement('dd')).textContent = _('すべての設定を削除し、スクリプトを停止します。');

		section.appendChild(dl);
		doc.body.appendChild(section);

		/**
		 * メニューを作成する
		 * @param {Object} commands - キーがmenuitem要素のid属性値、値がlabel属性値の連想配列
		 * @param {string} id - menu要素のid属性値
		 * @returns {HTMLMenuElement} menu要素
		 */
		function createMenu(commands, id) {
			let menu = document.createElementNS(DOMUtils.HTML_NS, 'menu');
			menu.id = id;
			for (let id in commands) {
				let menuitem = document.createElementNS(DOMUtils.HTML_NS, 'menuitem');
				menuitem.id = id;
				menuitem.label = commands[id];
				menu.appendChild(menuitem);
			}
			if (menu.type !== 'popup') {
				// Bug 897102 – Update <menu> to spec <https://bugzilla.mozilla.org/show_bug.cgi?id=897102>
				menu.type = 'context';
			}
			return menu;
		}
	},
};



/**
 * フォームデータ集合の各エントリ。
 * @typedef FormDataEntry
 * @type {Array}
 * @property {string} 0 - 名前。
 * @property {(string|Blob|nsIMIMEInputStream)} 1 - 値。
 * @see [Interface FormData - XMLHttpRequest Standard]{@link http://xhr.spec.whatwg.org/#interface-formdata}
 */

/**
 * 文字列操作。
 */
let StringUtils = {
	/**
	 * [Encoding Standard]{@link http://encoding.spec.whatwg.org/}が要求する標準の文字符号化方式。
	 * @constant {string}
	 */
	THE_ENCODING: 'UTF-8',

	/**
	 * 境界文字列の前半に用いるハイフンマイナスの数。
	 * @type {number}
	 */
	BOUNDARY_HYPHEN_LENGTH: 25,

	/**
	 * 境界文字列の後半に用いる乱数値の文字数。
	 * @type {number}
	 */
	BOUNDARY_RANDOM_STRING_LENGTH: 15,

	/**
	 * フォームデータ集合を multipart/form-data として{@link nsIInputStream}に変換する。
	 * @param {FormDataEntry[]} formDataSet
	 * @param {Function} callback - 第1引数に戻り値としての{@link nsIMIMEInputStream}を含むコールバック関数。
	 * @param {string} [encoding=StringUtils.THE_ENCODING]
	 */
	encodeMultipartFormData: function (formDataSet, callback, encoding = this.THE_ENCODING) {
		// 境界文字列を生成
		let boundary = '-'.repeat(this.BOUNDARY_HYPHEN_LENGTH) + (Math.random() * Math.pow(10, this.BOUNDARY_RANDOM_STRING_LENGTH)).toFixed();

		// 要求本体の生成
		let multipartBody = new MultiplexInputStream();
		(function createMultipartBody(i = 0) {
			for (let l = formDataSet.length; i < l; i++) {
				let [name, value] = formDataSet[i];
				multipartBody.appendStream(new StringInputStream((i > 0 ? '\r\n' : '') + '--' + boundary + '\r\n', -1));
				let isMIMEInputStream = value instanceof Ci.nsIMIMEInputStream;
				let isFile = isMIMEInputStream || /^\[object (?:Blob|File)]$/.test(Object.prototype.toString.call(value));
				let bodyPart = isMIMEInputStream ? value : new MIMEInputStream();
				bodyPart.addHeader('content-disposition', 'form-data; name=' + StringUtils.quote(StringUtils.convertEncoding(name, encoding))
						+ (isFile ? '; filename=' + StringUtils.quote(StringUtils.convertEncoding(value.name || 'blob', encoding)) : ''));
				if (isMIMEInputStream) {
					multipartBody.appendStream(bodyPart);
				} else if (isFile) {
					// エントリの値がファイルなら
					bodyPart.addHeader('content-type', value.type);
					// BlobをArrayBufferに変換する
					let fileReader = new FileReader();
					fileReader.addEventListener('load', function (event) {
						// ArrayBufferをnsIArrayBufferStreamに変換し追加する
						let buffer = event.target.result;
						bodyPart.setData(new ArrayBufferInputStream(buffer, 0, buffer.byteLength));
						multipartBody.appendStream(bodyPart);
						createMultipartBody(i + 1);
					});
					fileReader.readAsArrayBuffer(value);
					return;
				} else {
					bodyPart.setData(StringUtils.convertToInputStream(value, encoding));
					multipartBody.appendStream(bodyPart);
				}
			}
			multipartBody.appendStream(new StringInputStream('\r\n--' + boundary + '--\r\n', -1));

			// 要求ヘッダの設定
			let postData = new MIMEInputStream();
			postData.addHeader('content-type', 'multipart/form-data; boundary=' + boundary);
			postData.addContentLength = true;

			// 要求本体の設定
			postData.setData(multipartBody);

			callback(postData);
		})();
	},

	/**
	 * 文字列を指定した符号化方式の{@link nsIInputStream}として返す。
	 * @param {string} str
	 * @param {string} [encoding=StringUtils.THE_ENCODING]
	 * @returns {nsIInputStream}
	 */
	convertToInputStream: function (str, encoding = this.THE_ENCODING) {
		try {
			ScriptableUnicodeConverter.charset = encoding;
		} catch(e) {
			if (e.result === Cr.NS_ERROR_UCONV_NOCONV) {
				ScriptableUnicodeConverter.charset = this.THE_ENCODING;
			} else {
				throw e;
			}
		}
		return ScriptableUnicodeConverter.convertToInputStream(str);
	},

	/**
	 * 文字列を指定した符号化方式のバイナリ文字列に変換する。
	 * @param {string} str
	 * @param {string} [encoding=StringUtils.THE_ENCODING]
	 * @returns {string}
	 */
	convertEncoding: function (str, encoding = this.THE_ENCODING) {
		let stream = this.convertToInputStream(str, encoding);
		return NetUtil.readInputStreamToString(stream, stream.available());
	},

	/**
	 * 文字列をquoted-string形式に。(", \, CR, LF にバックスラッシュを前置)
	 * @param {string} str
	 * @returns {string}
	 */
	quote: function (str) {
		return '"' + str.replace(/["\\\r\n]/g, '\\$&') + '"';
	},

	/**
	 * application/x-www-form-urlencoded形式の文字列を解析する。
	 * @param {string} input
	 * @returns {Array[]} [['name', 'value'], ……] のような形式の二次元配列。
	 * @see {@link http://url.spec.whatwg.org/#concept-urlencoded-parser The application/x-www-form-urlencoded parser - URL Standard}
	 * @version polyfill-2013-12-26-uc-2013-12-30
	 */
	parseXWwwFormUrlencoded: function (input) {
		var strings, string, index, name, value, i, l;
		let _pairs = [];
		strings = input.split('&');
		if (!strings[0].contains('=')) {
			strings[0] = '=' + strings[0];
		}
		for (i = 0, l = strings.length; i < l; i++) {
			string = strings[i];
			if (string === '') {
				continue;
			}
			index = string.indexOf('=');
			if (index !== -1) {
				name = string.slice(0, index);
				value = string.slice(index + 1);
			} else {
				name = string;
				value = '';
			}
			_pairs.push([
				decodeURIComponent(name.replace(/\+/g, ' ')),
				decodeURIComponent(value.replace(/\+/g, ' '))
			]);
		}
		return _pairs;
	},
};



/**
 * アンインストール時に実行する処理。
 */
let UninstallObserver = {
	/**
	 * 通知の種類。
	 * @constant {string}
	 */
	TYPE: 'uninstall',

	/**
	 * スクリプトが停止していれば真。
	 * @type {booelan}
	 */
	uninstalled: false,

	/**
	 * オブザーバを登録。
	 */
	init: function () {
		ObserverUtils.register(this.TYPE, this);
	},

	/**
	 * オブザーバ。
	 */
	observe: function () {
		this.uninstalled = true;
		ObserverUtils.stop();
		document.getElementById(DragAndDropZonesPlus.ID + '-menuitem').remove();
		DropzoneUtils.remove();
		DropzoneUtils.removeEventListeners();
	},

	/**
	 * 通知。
	 */
	notify: function () {
		ObserverUtils.notify(this.TYPE);
	},
};



/**
 * クリップボードに関する操作。
 */
let ClipboardUtils = {
	/**
	 * クリップボードからテキスト情報を取得する。
	 * @param {number} whichClipboard {@link Services.clipboard.kSelectionClipboard}か{@link Services.clipboard.kGlobalClipboard}
	 * @returns {?string}
	 */
	getText: function (whichClipboard) {
		if (Services.clipboard.hasDataMatchingFlavors(['text/unicode'], 1, whichClipboard)) {
			// テキストデータが保持されていれば
			let transferable = new Transferable('text/unicode'), data = {};
			Services.clipboard.getData(transferable, whichClipboard);
			transferable.getTransferData('text/unicode', data, {});
			return data.value.QueryInterface(Ci.nsISupportsString).data;
		} else {
			return null;
		}
	},
};



/**
 * DOM関連のメソッド。
 */
let DOMUtils = {
	/**
	 * HTML名前空間。
	 * @constant {string}
	 */
	HTML_NS: 'http://www.w3.org/1999/xhtml',

	/**
	 * 属性値を{@link DOMSettableTokenList}として取得する。
	 * @param {Element} element - 要素。
	 * @param {string} attributeName - 属性値名。
	 * @returns {DOMSettableTokenList}
	 * @see {@link http://dom.spec.whatwg.org/#interface-domsettabletokenlist 9.2 Interface DOMSettableTokenList - DOM Standard}
	 */
	getAttributeAsDOMSettableTokenList: function(element, attributeName) {
		let htmlElement = document.createElementNS(this.HTML_NS, 'div');
		htmlElement.itemRef = element.getAttribute(attributeName) || '';
		return htmlElement.itemRef;
	},

	/**
	 * ノードに対応するfigcaption要素を取得する。
	 * @param {Node} node
	 * @returns {?HTMLElement}
	 */
	getFigcaption: function (node) {
		let figcaption = null;

		let parent = node.parentElement;
		if (parent && parent.localName === 'figure') {
			let first = parent.firstElementChild;
			if (first) {
				if (first.localName === 'figcaption') {
					figcaption = first;
				} else {
					let last = parent.lastElementChild;
					if (last && last.localName === 'figcaption') {
						figcaption = last;
					}
				}
			}
		}

		return figcaption;
	},

	/**
	 * 指定した局所名を持つ直近の親を返す。
	 * @param {Node} childNode
	 * @param {string} localName
	 * @returns {?Element}
	 */
	getParentElementByTagName: (function () {
		let _localName;
		let treeWalkers = new WeakMap();
		return function (childNode, localName) {
			if (childNode.localName === localName) {
				return childNode;
			} else {
				let doc = childNode.ownerDocument;
				let treeWalker = treeWalkers.get(doc);
				if (!treeWalker) {
					treeWalker = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT, function (node) {
						return node.localName === _localName ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
					});
					treeWalkers.set(doc, treeWalker);
				}
				_localName = localName;
				treeWalker.currentNode = childNode;
				return treeWalker.parentNode();
			}
		};
	})(),

	/**
	 * 指定されたノードの子孫要素に、適宜改行とインデントを挿入し読みやすくする。
	 * すでに改行やインデントが含まれていることは想定しない。
	 * @param {Node} root
	 * @returns {Node}
	 */
	toPrettyXML: function (root) {
		let walker = (root.ownerDocument || root).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
		let indent = 0;
		while (true) {
			if (walker.firstChild()) {
				// 子要素を操作し、存在すればインデントを増やす
				indent++;
			} else {
				// 子要素が存在しなければ、次の同胞要素が存在する親要素を走査する
				while (true) {
					if (walker.nextSibling()) {
						break;
					} else if (walker.parentNode()) {
						indent--;
					} else {
						// すべての要素を走査し終えていれば
						return root;
					}
				}
			}
			// 要素の前に改行とインデントを挿入する
			walker.currentNode.parentNode.insertBefore(new Text('\n' + '\t'.repeat(indent)), walker.currentNode);
			if (!walker.currentNode.nextElementSibling) {
				// 次の同胞要素が存在しなければ、要素の後ろに改行とインデントを追加する
				walker.currentNode.parentNode.appendChild(new Text('\n' + '\t'.repeat(indent - 1)));
			}
		}
	},

	/**
	 * 選択範囲と指定した座標が重なるか調べる。
	 * @param {Selection} selection
	 * @param {number} x
	 * @param {number} y
	 * @returns {boolean}
	 */
	isSuperposedCoordinateOnSelection: function (selection, x, y) {
		for (let i = 0, l = selection.rangeCount; i < l; i++) {
			if (this.isSuperposedCoordinateOnRange(selection.getRangeAt(i), x, y)) {
				return true;
			}
		}
		return false;
	},

	/**
	 * rangeと指定した座標が重なるか調べる。
	 * @param {Range} range
	 * @param {number} x
	 * @param {number} y
	 * @returns {boolean}
	 */
	isSuperposedCoordinateOnRange: function (range, x, y) {
		return Array.prototype.some.call(range.getClientRects(), rect => {
			return this.isSuperposedCoordinateOnRect(rect, x, y);
		});
	},

	/**
	 * 長方形と指定した座標が重なるか調べる。
	 * @param {DOMRect} rect
	 * @param {number} x
	 * @param {number} y
	 * @returns {boolean}
	 */
	isSuperposedCoordinateOnRect: function (rect, x, y) {
		return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
	},
};



/**
 * オブザーバサービスを利用して、各ウィンドウと通知を送受信する。
 * @version 2014-05-10
 */
let ObserverUtils = {
	/**
	 * オブザーバを追加する。
	 * @param {string} id - 当スクリプトに関する通知のID。
	 */
	init: function (id) {
		this.id = id;
		Services.obs.addObserver(this, this.id, false);
		window.addEventListener('unload', () => this.stop());
	},

	/**
	 * オブザーバを削除する。
	 */
	stop: function () {
		Services.obs.removeObserver(this, this.id);
	},

	/**
	 * 通知を受け取る関数を登録する。同じtypeの場合は上書きされる。
	 * @param {string} type
	 * @param {object} observer - observeメソッドを持つオブジェクト。
	 */
	register: function (type, observer) {
		this.observers[type] = observer;
	},

	/**
	 * 通知。
	 * @param {string} type
	 * @param {*} [data]
	 */
	notify: function (type, data = null) {
		Services.obs.notifyObservers(data, this.id, type);
	},

	/**
	 * オブザーバのリスト。
	 * @type {Object}
	 * @access protected
	 */
	observers: {},

	/**
	 * 当スクリプトに関する通知のID。
	 * @type {string}
	 * @access protected
	 */
	id: null,

	/**
	 * 通知を受け取るメソッド。
	 * @param {*} subject
	 * @param {string} topic
	 * @param {string} data
	 * @access protected
	 */
	observe: function (subject, topic, data) {
		if (data in this.observers) {
			// 対応するオブザーバが存在すれば
			this.observers[data].observe(subject);
		}
	},
};



/**
 * FUELオブジェクトとDOMオブジェクトの変換等を行う。
 * @version 2014-05-10
 */
let FUELUtils = {
	/**
	 * 対応するFUELウィンドウを取得する。
	 * @param {ChromeWindow} [win=window]
	 * @returns {?Ci.fuelIWindow}
	 */
	getFUELWindow: function (win = window) {
		// Firefox 24 ESRにはArray#findが実装されていない
		for (let fuelWindow of Application.windows) {
			if (this.getChromeWindow(fuelWindow) === win) {
				return fuelWindow;
			}
		}
		return null;
	},

	/**
	 * 対応するChromeウィンドウを取得する。
	 * @param {Ci.fuelIWindow} fuelWindow
	 * @returns {ChromeWindow}
	 */
	getChromeWindow: function (fuelWindow) {
		return fuelWindow._window || fuelWindow.tabs[0].window._window;
	},

	/**
	 * 対応するbrowser要素を取得する。
	 * @param {Ci.fuelIBrowserTab} fuelTab
	 * @returns {XULElement} browser要素。
	 */
	getBrowserElement: function (fuelTab) {
		return fuelTab._browser || fuelTab.window.tabs[fuelTab.index]._browser;
	},

	/**
	 * すべてのウィンドウから、指定した文書に対応するFUELブラウザタブを捜す。
	 * @param {Document} doc
	 * @returns {?Ci.fuelIBrowserTab}
	 */
	getFUELTab: function (doc) {
		for (let fuelWindow of Application.windows) {
			let index = this.getChromeWindow(fuelWindow).gBrowser.getBrowserIndexForDocument(doc);
			if (index !== -1) {
				return fuelWindow.tabs[index];
			}
		}
		return null;
	},

	/**
	 * 確実に開いている最前面のウィンドウを返す。
	 * @returns {Ci.fuelIWindow}
	 */
	getActiveFUELWindow: function () {
		let activeFUELWindow = Application.activeWindow;
		let activeChromeWindow = this.getChromeWindow(activeFUELWindow);
		if (activeChromeWindow.closed) {
			// {@link Ci.extIApplication#activeWindow}が既に閉じているウィンドウを返した場合
			// 別ウィンドウを最前面にしてそれを返す
			// Firefox 24 ESRにはArray#findIndexが実装されていない
			for (let fuelWindow of Application.windows) {
				let chromeWindow = this.getChromeWindow(fuelWindow);
				if (chromeWindow !== activeChromeWindow) {
					activeFUELWindow = fuelWindow;
					chromeWindow.focus();
					break;
				}
			}
		}
		return activeFUELWindow;
	}
};



/**
 * ポップアップ通知を表示する。
 * @param {string} message - 表示するメッセージ。
 * @param {Ci.fuelIBrowserTab} tab - メッセージを表示するタブ。
 * @param {string} [type=information] - メッセージの前に表示するアイコンの種類。"information"、"warning"、"error"、"question" のいずれか。
 * @version 2014-05-10
 */
function showPopupNotification(message, tab, type = 'information') {
	if (FUELUtils.getChromeWindow(tab.window).closed) {
		// 指定されたタブを含むウィンドウが既に閉じていれば、別ウィンドウの最前面のタブを取得
		tab = FUELUtils.getActiveFUELWindow().activeTab;
	} else if (tab.index === -1) {
		// 指定されたタブが既に閉じていれば、最前面のタブを取得
		tab = tab.window.activeTab;
	}
	FUELUtils.getChromeWindow(tab.window).PopupNotifications.show(FUELUtils.getBrowserElement(tab), DragAndDropZonesPlus.ID, message, null, null, null, {
		persistWhileVisible: true,
		removeOnDismissal: true,
		popupIconURL: 'chrome://global/skin/icons/' + type + '-64.png',
	});
}



// Bug 887836 – Implement URLSearchParams <https://bugzilla.mozilla.org/show_bug.cgi?id=887836>
let URLSearchParams;
if ('URLSearchParams' in window) {
	URLSearchParams = window.URLSearchParams;
} else {
	/**
	 * A URLSearchParams object has an associated list of name-value pairs, which is initially empty.
	 * @constructor
	 * @param {(string|URLSearchParams)} [init=""]
	 * @see {@link http://url.spec.whatwg.org/#interface-urlsearchparams Interface URLSearchParams - URL Standard}
	 * @version polyfill-2014-03-18-uc-2014-03-18
	 * @name URLSearchParams
	 */
	URLSearchParams = function (init) {
		var strings, string, index, name, value, i, l;
		this._pairs = [];
		if (init) {
			if (init instanceof URLSearchParams) {
				for (i = 0, l = init._pairs.length; i < l; i++) {
					this._pairs.push([init._pairs[i][0], init._pairs[i][1]]);
				}
			} else {
				strings = init.split('&');
				if (!strings[0].contains('=')) {
					strings[0] = '=' + strings[0];
				}
				for (i = 0, l = strings.length; i < l; i++) {
					string = strings[i];
					if (string === '') {
						continue;
					}
					index = string.indexOf('=');
					if (index !== -1) {
						name = string.slice(0, index);
						value = string.slice(index + 1);
					} else {
						name = string;
						value = '';
					}
					this._pairs.push([
						decodeURIComponent(name.replace(/\+/g, ' ')),
						decodeURIComponent(value.replace(/\+/g, ' '))
					]);
				}
			}
		}
	};
	/**
	 * Append a new name-value pair whose name is name and value is value, to the list of name-value pairs.
	 * @param {string} name
	 * @param {string} value
	 * @name URLSearchParams#append
	 */
	Object.defineProperty(URLSearchParams.prototype, 'append', {
		writable: true,
		enumerable: false,
		configurable: true,
		value: function (name, value) {
			this._pairs.push([String(name), String(value)]);
		}
	});
	/**
	 * Remove all name-value pairs whose name is name.
	 * @param {string} name
	 * @name URLSearchParams#delete
	 */
	Object.defineProperty(URLSearchParams.prototype, 'delete', {
		writable: true,
		enumerable: false,
		configurable: true,
		value: function (name) {
			var i;
			for (i = 0; i < this._pairs.length; i++) {
				if (this._pairs[i][0] === name) {
					this._pairs.splice(i, 1);
					i--;
				}
			}
		}
	});
	/**
	 * Return the value of the first name-value pair whose name is name, and null if there is no such pair.
	 * @param {string} name
	 * @name URLSearchParams#get
	 * @returns {?string}
	 */
	Object.defineProperty(URLSearchParams.prototype, 'get', {
		writable: true,
		enumerable: false,
		configurable: true,
		value: function (name) {
			var i, l;
			for (i = 0, l = this._pairs.length; i < l; i++) {
				if (this._pairs[i][0] === name) {
					return this._pairs[i][1];
				}
			}
			return null;
		}
	});
	/**
	 * Return the values of all name-value pairs whose name is name, in list order, and the empty sequence otherwise.
	 * @param {string} name
	 * @name URLSearchParams#getAll
	 * @returns {string[]}
	 */
	Object.defineProperty(URLSearchParams.prototype, 'getAll', {
		writable: true,
		enumerable: false,
		configurable: true,
		value: function (name) {
			var pairs = [], i, l;
			for (i = 0, l = this._pairs.length; i < l; i++) {
				if (this._pairs[i][0] === name) {
					pairs.push(this._pairs[i][1]);
				}
			}
			return pairs;
		}
	});
	/**
	 * If there are any name-value pairs whose name is name, set the value of the first such name-value pair to value and remove the others.
	 * Otherwise, append a new name-value pair whose name is name and value is value, to the list of name-value pairs.
	 * @param {string} name
	 * @param {string} value
	 * @name URLSearchParams#set
	 */
	Object.defineProperty(URLSearchParams.prototype, 'set', {
		writable: true,
		enumerable: false,
		configurable: true,
		value: function (name, value) {
			var flag, i;
			for (i = 0; i < this._pairs.length; i++) {
				if (this._pairs[i][0] === name) {
					if (flag) {
						this._pairs.splice(i, 1);
						i--;
					} else {
						this._pairs[i][1] = String(value);
						flag = true;
					}
				}
			}
			if (!flag) {
				this.append(name, value);
			}
		}
	});
	/**
	 * Return true if there is a name-value pair whose name is name, and false otherwise.
	 * @param {string} name
	 * @name URLSearchParams#has
	 * @returns {boolean}
	 */
	Object.defineProperty(URLSearchParams.prototype, 'has', {
		writable: true,
		enumerable: false,
		configurable: true,
		value: function (name) {
			var i, l;
			for (i = 0, l = this._pairs.length; i < l; i++) {
				if (this._pairs[i][0] === name) {
					return true;
				}
			}
			return false;
		}
	});
	/**
	 * Return the serialization of the URLSearchParams object's associated list of name-value pairs.
	 * @name URLSearchParams#toString
	 * @returns {string}
	 */
	Object.defineProperty(URLSearchParams.prototype, 'toString', {
		writable: true,
		enumerable: false,
		configurable: true,
		value: function () {
			return this._pairs.map(function (pair) {
				return encodeURIComponent(pair[0]) + '=' + encodeURIComponent(pair[1]);
			}).join('&');
		}
	});
}



DragAndDropZonesPlus.main();

})();