Drag & DropZones +

[userChromeJS] Drag selected character strings or image and drop to the semitransparent box displayed on web page to open search result.

  1. // ==UserScript==
  2. // @name Drag & DropZones +
  3. // @name:ja Drag & DropZones +
  4. // @description [userChromeJS] Drag selected character strings or image and drop to the semitransparent box displayed on web page to open search result.
  5. // @description:ja 【userChromeJS】選択した文字列などをドラッグし、ページ上に表示される半透明の枠内にドロップすることで、Web検索などを実行します。
  6. // @namespace https://userscripts.org/users/347021
  7. // @version 4.9.0
  8. // @include main
  9. // @license MPL-2.0
  10. // @contributionURL https://www.amazon.co.jp/registry/wishlist/E7PJ5C3K7AM2
  11. // @incompatible Edge
  12. // @compatible Firefox userChromeJS用スクリプト です (※GreasemonkeyスクリプトでもuserChromeES用スクリプトでもありません) / This script is for userChromeJS (* neither Greasemonkey nor userChromeES)
  13. // @incompatible Opera
  14. // @incompatible Chrome
  15. // @charset UTF-8
  16. // @author 100の人
  17. // @contributor HADAA
  18. // @homepageURL https://greasyfork.org/scripts/264
  19. // ==/UserScript==
  20.  
  21. (async function () {
  22. 'use strict';
  23.  
  24. /**
  25. * L10N
  26. * @type {LocalizedTexts}
  27. */
  28. const localizedTexts = {
  29. /*eslint-disable quote-props, max-len */
  30. 'en': {
  31. '次のパスへ設定ファイルを作成しました。': 'Created a configuration file to the following location.',
  32. 'JSONファイルとしてのパースに失敗しました。': 'Failed to parse the settings file as JSON.',
  33. 'ルートがオブジェクトではありません。': 'The root is not an object.',
  34. '「providers」プロパティが存在しません。': 'The “providers” property does not exist.',
  35. '「providers」プロパティは配列ではありません。': ' The “providers” property is not an array.',
  36. '「where」プロパティは %s のいずれかを設定します。': 'Set one of %s into the “where” property.',
  37. '「providers」プロパティの %i 番目の要素はオブジェクトではありません。': 'The %i-th element of the “providers” property is not an object.',
  38. '「providers」プロパティの %i 番目の要素には「search_url」「image_url」が重複して設定されています。':
  39. 'The %i-th element of the “providers” property has duplicate “search_url” and “image_url” set.',
  40. '「providers」プロパティの %i 番目の要素の「%s」プロパティは文字列ではありません。': 'The “%s” property of the %i-th element of the “providers” property is not a string.',
  41. '「providers」プロパティの %i 番目の要素の「%1s」プロパティは、%2s で始まる妥当なURLではありません。':
  42. 'The “%1s” property of the %i-th element of the “providers” property is not a valid URL beginning with %2s.',
  43. '「providers」プロパティの %i 番目の要素に「name」プロパティが存在しません。': ' The “name” property does not exist for the %i-th element of the “providers” property.',
  44. '「providers」プロパティの %i 番目で指定されている「%s」という名前のブラウザ検索プロバイダは存在しません。':
  45. 'There is no browser search provider named “%s” specified in the %i-th element of the “providers” property.',
  46. '「providers」プロパティの %i 番目の要素の「search_url」「search_url_post_params」プロパティのいずれにも、{searchTerms} が含まれません。':
  47. 'Neither the “search_url” nor the “search_url_post_params” properties of the %i-th element of the “providers” property contain {searchTerms}.',
  48. '「providers」プロパティの %i 番目の要素には「image_url_post_params」プロパティが存在しません。':
  49. 'The “image_url_post_params” property does not exist on the %i-th element of the “providers” property.',
  50. '「providers」プロパティの %i 番目の要素の「image_url_post_params」プロパティには、{searchTerms} に一致するクエリ値が存在しません。':
  51. 'The “image_url_post_params” property for the %i-th element of the “providers” property does not have a query value that matches {searchTerms}.',
  52. '検索プロバイダが1つも指定されていません。': 'None of the search providers have been specified.',
  53.  
  54. 'Google 画像で検索': 'Google search by image',
  55. },
  56. /*eslint-enable */
  57. };
  58.  
  59.  
  60.  
  61. const { FileUtils } = ChromeUtils.importESModule('resource://gre/modules/FileUtils.sys.mjs');
  62.  
  63. const ScriptableUnicodeConverter
  64. = Cc['@mozilla.org/intl/scriptableunicodeconverter'].createInstance(Ci.nsIScriptableUnicodeConverter);
  65.  
  66. const TextToSubURI = Cc['@mozilla.org/intl/texttosuburi;1'].getService(Ci.nsITextToSubURI);
  67.  
  68. const StringInputStream
  69. = Components.Constructor('@mozilla.org/io/string-input-stream;1', 'nsIStringInputStream', 'setByteStringData');
  70. const FileInputStream
  71. = Components.Constructor('@mozilla.org/network/file-input-stream;1', 'nsIFileInputStream', 'init');
  72. const ConverterOutputStream
  73. = Components.Constructor('@mozilla.org/intl/converter-output-stream;1', 'nsIConverterOutputStream', 'init');
  74.  
  75.  
  76.  
  77. const Cr = new Proxy(window.Cr, {
  78. get(target, name)
  79. {
  80. if (name in target) {
  81. return target[name];
  82. } else if (name === 'NS_ERROR_UCONV_NOCONV') {
  83. return 0x80500001;
  84. } else {
  85. return undefined;
  86. }
  87. },
  88. });
  89.  
  90.  
  91.  
  92. /**
  93. * HTML、XML、DOMに関するメソッド等。
  94. */
  95. const MarkupUtils = {
  96. /**
  97. * XMLの特殊文字と文字参照の変換テーブル。
  98. * @constant {Object.<string>}
  99. */
  100. CHARACTER_REFERENCES_TRANSLATION_TABLE: {
  101. '&': '&amp;',
  102. '<': '&lt;',
  103. '>': '&gt;',
  104. '"': '&quot;',
  105. "'": '&apos;',
  106. },
  107.  
  108. /**
  109. * XMLの特殊文字を文字参照に置換します。
  110. * @see {@link https://stackoverflow.com/a/4835406 html - HtmlSpecialChars equivalent in Javascript? - Stack Overflow}
  111. * @param {string} str - プレーンな文字列。
  112. * @returns {string} HTMLとして扱われる文字列。
  113. */
  114. convertSpecialCharactersToCharacterReferences(str) {
  115. return String(str).replace(
  116. /[&<>"']/g,
  117. specialCharcter => this.CHARACTER_REFERENCES_TRANSLATION_TABLE[specialCharcter],
  118. );
  119. },
  120.  
  121. /**
  122. * テンプレート文字列のタグとして用いることで、式内にあるXMLの特殊文字を文字参照に置換します。
  123. * @param {string[]} htmlTexts
  124. * @param {...string} plainText
  125. * @returns {string} HTMLとして扱われる文字列。
  126. */
  127. escapeTemplateStrings(htmlTexts, ...plainTexts) {
  128. return String.raw(
  129. htmlTexts,
  130. ...plainTexts.map(plainText => this.convertSpecialCharactersToCharacterReferences(plainText)),
  131. );
  132. },
  133. };
  134.  
  135. /**
  136. * {@link MarkupUtils.escapeTemplateStrings}、
  137. * または {@link MarkupUtils.convertSpecialCharactersToCharacterReferences} の短縮表記。
  138. * @example
  139. * // returns "<code>&lt;a href=&quot;https://example.com/&quot;link text&lt;/a&gt;</code>"
  140. * h`<code>${'<a href="https://example.com/">link text</a>'}</code>`;
  141. * @example
  142. * // returns "&lt;a href=&quot;https://example.com/&quot;link text&lt;/a&gt;"
  143. * h('<a href="https://example.com/">link text</a>');
  144. * @returns {string}
  145. */
  146. function h(...args)
  147. {
  148. return Array.isArray(args[0])
  149. ? MarkupUtils.escapeTemplateStrings(...args)
  150. : MarkupUtils.convertSpecialCharactersToCharacterReferences(args[0]);
  151. }
  152.  
  153. // i18n
  154. let _, setlang, setLocalizedTexts;
  155. {
  156. /**
  157. * 翻訳対象文字列 (msgid) の言語。
  158. * @constant {string}
  159. */
  160. const ORIGINAL_LOCALE = 'ja';
  161.  
  162. /**
  163. * クライアントの言語の翻訳リソースが存在しないとき、どの言語に翻訳するか。
  164. * @constant {string}
  165. */
  166. const DEFAULT_LOCALE = 'en';
  167.  
  168. /**
  169. * 以下のような形式の翻訳リソース。
  170. * {
  171. * 'IETF言語タグ': {
  172. * '翻訳前 (msgid)': '翻訳後 (msgstr)',
  173. * ……
  174. * },
  175. * ……
  176. * }
  177. * @typedef {Object} LocalizedTexts
  178. */
  179.  
  180. /**
  181. * クライアントの言語。{@link setlang}から変更される。
  182. * @type {string}
  183. * @access private
  184. */
  185. let langtag = 'ja';
  186.  
  187. /**
  188. * クライアントの言語のlanguage部分。{@link setlang}から変更される。
  189. * @type {string}
  190. * @access private
  191. */
  192. let language = 'ja';
  193.  
  194. /**
  195. * 翻訳リソース。{@link setLocalizedTexts}から変更される。
  196. * @type {LocalizedTexts}
  197. * @access private
  198. */
  199. const multilingualLocalizedTexts = {};
  200. multilingualLocalizedTexts[ORIGINAL_LOCALE] = {};
  201.  
  202. /**
  203. * テキストをクライアントの言語に変換する。
  204. * @param {string} message - 翻訳前。
  205. * @returns {string} 翻訳後。
  206. */
  207. _ = function (message) {
  208. // クライアントの言語の翻訳リソースが存在すれば、それを返す
  209. return langtag in multilingualLocalizedTexts && multilingualLocalizedTexts[langtag][message]
  210. // 地域下位タグを取り除いた言語タグの翻訳リソースが存在すれば、それを返す
  211. || language in multilingualLocalizedTexts && multilingualLocalizedTexts[language][message]
  212. // デフォルト言語の翻訳リソースが存在すれば、それを返す
  213. || DEFAULT_LOCALE in multilingualLocalizedTexts && multilingualLocalizedTexts[DEFAULT_LOCALE][message]
  214. // そのまま返す
  215. || message;
  216. };
  217.  
  218. /**
  219. * {@link gettext}から参照されるクライアントの言語を設定する。
  220. * @param {string} lang - IETF言語タグ。(「language」と「language-REGION」にのみ対応)
  221. */
  222. setlang = function (lang) {
  223. lang = lang.split('-', 2);
  224. language = lang[0].toLowerCase();
  225. langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');
  226. };
  227.  
  228. /**
  229. * {@link gettext}から参照される翻訳リソースを追加する。
  230. * @param {LocalizedTexts} localizedTexts
  231. */
  232. setLocalizedTexts = function (localizedTexts) {
  233. for (let lang in localizedTexts) {
  234. const localizedText = localizedTexts[lang];
  235. lang = lang.split('-');
  236. const language = lang[0].toLowerCase();
  237. const langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');
  238.  
  239. if (langtag in multilingualLocalizedTexts) {
  240. // すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば上書き)
  241. for (const msgid in localizedText) {
  242. multilingualLocalizedTexts[langtag][msgid] = localizedText[msgid];
  243. }
  244. } else {
  245. multilingualLocalizedTexts[langtag] = localizedText;
  246. }
  247.  
  248. if (language !== langtag) {
  249. // 言語タグに地域下位タグが含まれていれば
  250. // 地域下位タグを取り除いた言語タグも翻訳リソースとして追加する
  251. if (language in multilingualLocalizedTexts) {
  252. // すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば無視)
  253. for (const msgid in localizedText) {
  254. if (!(msgid in multilingualLocalizedTexts[language])) {
  255. multilingualLocalizedTexts[language][msgid] = localizedText[msgid];
  256. }
  257. }
  258. } else {
  259. multilingualLocalizedTexts[language] = localizedText;
  260. }
  261. }
  262.  
  263. // msgidの言語の翻訳リソースを生成
  264. for (const msgid in localizedText) {
  265. multilingualLocalizedTexts[ORIGINAL_LOCALE][msgid] = msgid;
  266. }
  267. }
  268. };
  269. }
  270.  
  271. setLocalizedTexts(localizedTexts);
  272.  
  273. setlang(window.navigator.language);
  274.  
  275.  
  276.  
  277.  
  278. /**
  279. * id属性値などに利用する識別子。
  280. * @constant {string}
  281. */
  282. const ID = 'drag-and-drop-search-347021';
  283.  
  284.  
  285.  
  286. /**
  287. * DOM関連のメソッド。
  288. */
  289. const DOMUtils = {
  290. /**
  291. * HTML名前空間。
  292. * @constant {string}
  293. */
  294. HTML_NS: 'http://www.w3.org/1999/xhtml',
  295.  
  296. /**
  297. * 属性値を{@link DOMTokenList}として取得する。
  298. * @param {Element} element - 要素。
  299. * @param {string} attributeName - 属性値名。
  300. * @returns {DOMTokenList}
  301. * @see {@link https://dom.spec.whatwg.org/#interface-domtokenlist 7.1. Interface DOMTokenList | DOM Standard}
  302. */
  303. getAttributeAsDOMTokenList(element, attributeName)
  304. {
  305. const tokenList = document.createElementNS(this.HTML_NS, 'div').classList;
  306. tokenList.value = element.getAttribute(attributeName) || '';
  307. return tokenList;
  308. },
  309.  
  310. /**
  311. * ノードに対応するfigcaption要素を取得する。
  312. * @param {Node} node
  313. * @returns {?HTMLElement}
  314. */
  315. getFigcaption(node)
  316. {
  317. let figcaption = null;
  318.  
  319. const parent = node.parentElement;
  320. if (parent && parent.localName === 'figure') {
  321. const first = parent.firstElementChild;
  322. if (first) {
  323. if (first.localName === 'figcaption') {
  324. figcaption = first;
  325. } else {
  326. const last = parent.lastElementChild;
  327. if (last && last.localName === 'figcaption') {
  328. figcaption = last;
  329. }
  330. }
  331. }
  332. }
  333.  
  334. return figcaption;
  335. },
  336. };
  337.  
  338.  
  339.  
  340. /**
  341. * 文字列操作。
  342. */
  343. const StringUtils = {
  344. /**
  345. * [Encoding Standard]{@link https://encoding.spec.whatwg.org/}が要求する標準の文字符号化方式。
  346. * @constant {string}
  347. */
  348. THE_ENCODING: 'UTF-8',
  349.  
  350. /**
  351. * フォームデータを multipart/form-data として、HTTPヘッダが前に結合された{@link Ci.nsIInputStream}に変換する。
  352. * @param {FormData} formData
  353. * @returns {Promise.<Ci.nsIStringInputStream>}
  354. */
  355. async encodeMultipartFormData(formData)
  356. {
  357. const response = new Response(formData);
  358. const blob = await response.blob();
  359. const binary = await new Promise(function (resolve) {
  360. const reader = new FileReader();
  361. reader.addEventListener('load', event => resolve(event.target.result));
  362. reader.readAsBinaryString(blob);
  363. });
  364. const headers = response.headers;
  365. headers.set('content-length', binary.length);
  366. const bodyWithHeaders = Array.from(headers).map(([name, value]) => `${name}: ${value}`).join('\r\n')
  367. + '\r\n\r\n' + binary;
  368. return new StringInputStream(bodyWithHeaders);
  369. },
  370.  
  371. /**
  372. * 文字列を指定した符号化方式の{@link nsIInputStream}として返す。
  373. * @param {string} str
  374. * @param {string} [encoding]
  375. * @returns {nsIInputStream}
  376. */
  377. convertToInputStream(str, encoding = this.THE_ENCODING)
  378. {
  379. try {
  380. ScriptableUnicodeConverter.charset = encoding;
  381. } catch (e) {
  382. if (e.result === Cr.NS_ERROR_UCONV_NOCONV) {
  383. ScriptableUnicodeConverter.charset = this.THE_ENCODING;
  384. } else {
  385. throw e;
  386. }
  387. }
  388. return ScriptableUnicodeConverter.convertToInputStream(str);
  389. },
  390. };
  391.  
  392.  
  393.  
  394. /**
  395. * ユーザー設定。
  396. * @typedef {Object} UserSettings
  397. * @property {string} [where] - 検索結果を開く場所。{@link SettingsUtils.WHERES} のいずれか。
  398. * @property {SearchProvider[]} [providers] - 検索プロバイダの一覧。
  399. */
  400.  
  401. /**
  402. * 一つの検索プロバイダを表します。
  403. *
  404. * 「search_url」「image_url」のいずれも存在しない場合、ブラウザに登録されている検索エンジンであることを表します。
  405. * そのとき「search_url_post_params」が存在する場合、POSTメソッドの検索エンジンであることを表します。
  406. * @see {@link https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_settings_overrides chrome_settings_overrides - Mozilla | MDN}
  407. * @typedef {Object} SearchProvider
  408. * @property {string} name - 検索プロバイダ名。
  409. * @property {string} [search_url] - テキスト検索のURL。`{searchTerms}` が検索対象に置き換えられます。
  410. * @property {string} [search_url_post_params] - テキスト検索でPOSTメソッドを使用する場合に指定。`{searchTerms}` が検索対象に置き換えられます。
  411. * @property {string} [encoding] - 検索プロバイダが受け入れる文字コード文字符号化方式。
  412. * @property {string} [favicon_url] - 検索プロバイダを表す16px×16pxのアイコンのURL。ユーザー定義の検索プロバイダの場合は、data URL。
  413. * @property {function(): Promise.<void>} [reloadFaviconURL]
  414. * - ユーザー定義の検索プロバイダの場合、Ci.nsISearchEngineへのアイコン設定に遅延があるため、使用直前に `favicon_url` プロパティを更新するためのメソッド。
  415. * @property {string} [image_url] - 画像検索に使用するURL。
  416. * @property {string} [image_url_post_params] - 画像検索のPOSTパラメータ。`{searchTerms}` が検索対象に置き換えられます。
  417. */
  418.  
  419.  
  420.  
  421. /**
  422. * ユーザー設定。
  423. */
  424. const SettingsUtils = {
  425. /**
  426. * 設定ファイル名。
  427. * @constant {string}
  428. */
  429. FILENAME: 'drag-and-dropzones-plus.json',
  430.  
  431. /**
  432. * {@link UserSettings.where} で使用可能な値。最初の値が既定値。
  433. * @constant {string[]}
  434. */
  435. WHERES: [ 'tab', 'current', 'window' ],
  436.  
  437. /**
  438. * {@link Ci.nsISearchEngine}を{@link SearchProvider}に変換する。
  439. * @param {Ci.nsISearchEngine} browserEngine
  440. * @returns {SearchProvider}
  441. */
  442. convertToSearchProviderFromBrowserEngine(browserEngine)
  443. {
  444. const provider = {
  445. name: browserEngine.name,
  446. };
  447.  
  448. provider.reloadFaviconURL = async function () {
  449. this.favicon_url = await browserEngine.getIconURL();
  450. };
  451.  
  452. const submission = browserEngine.getSubmission('dummy');
  453. if (submission.postData) {
  454. // POSTメソッドなら
  455. provider.search_url_post_params = '';
  456. }
  457.  
  458. return provider;
  459. },
  460.  
  461. /**
  462. * 設定ファイルを読み込みます。
  463. * @returns {Promise.<UserSettings>}
  464. */
  465. async load()
  466. {
  467. const file = FileUtils.getDir('UChrm', [ this.FILENAME ]);
  468. if (!file.exists()) {
  469. const settings = await this.preset();
  470.  
  471. const stream = FileUtils.openSafeFileOutputStream(file);
  472. const converterOutputStream = new ConverterOutputStream(stream, StringUtils.THE_ENCODING, 0, 0x0000);
  473. converterOutputStream.writeString(JSON.stringify(settings, null, '\t'));
  474. FileUtils.closeSafeFileOutputStream(stream);
  475. converterOutputStream.close();
  476.  
  477. showPopupNotification(_('次のパスへ設定ファイルを作成しました。') + ' / ' + file.path);
  478. return settings;
  479. }
  480.  
  481. const stream = new FileInputStream(file, -1, -1, 0);
  482. const json = NetUtil.readInputStreamToString(stream, stream.available(), { charset: StringUtils.THE_ENCODING });
  483. try {
  484. return JSON.parse(json);
  485. } catch (exception) {
  486. showPopupNotification(`${_('JSONファイルとしてのパースに失敗しました。')} / Path: ${file.path} / Error message: ${exception}`);
  487. } finally {
  488. stream.close();
  489. }
  490. },
  491.  
  492. /**
  493. * 読み込んだ設定ファイルを検証し、フィルタリングして返します。
  494. * @param {object} obj
  495. * @returns {Object.<(?UserSettings|string[])>} 「settings」プロパティにUserSettings、「messages」プロパティにエラーメッセージの一覧。
  496. */
  497. filter(obj)
  498. {
  499. const messages = [];
  500. if (typeof obj !== 'object' || obj === null) {
  501. messages.push(_('ルートがオブジェクトではありません。'));
  502. } else if (!('providers' in obj)) {
  503. messages.push(_('「providers」プロパティが存在しません。'));
  504. } else if (!Array.isArray(obj.providers)) {
  505. messages.push(_('「providers」プロパティは配列ではありません。'));
  506. }
  507.  
  508. if (messages.length > 0) {
  509. return { settings: null, messages };
  510. }
  511.  
  512. const settings = { providers: [] };
  513.  
  514. if (!('where' in obj)) {
  515. settings.where = this.WHERES[0];
  516. } else if (!this.WHERES.includes(obj.where)) {
  517. messages.push(_('「where」プロパティは %s のいずれかを設定します。').replace('%s', this.WHERES.join(', ')));
  518. settings.where = this.WHERES[0];
  519. } else {
  520. settings.where = obj.where;
  521. }
  522.  
  523. let i = 0;
  524. for (const p of obj.providers) {
  525. i++;
  526.  
  527. if (typeof p !== 'object' || p === null) {
  528. messages.push(_('「providers」プロパティの %i 番目の要素はオブジェクトではありません。').replace('%i', i));
  529. continue;
  530. }
  531.  
  532. if ('search_url' in p && 'image_url' in p) {
  533. messages.push(_('「providers」プロパティの %i 番目の要素には「search_url」「image_url」が重複して設定されています。').replace('%i', i));
  534. continue;
  535. }
  536.  
  537. const provider = {};
  538. for (const propertyName of [
  539. 'name',
  540. 'search_url',
  541. 'search_url_post_params',
  542. 'image_url',
  543. 'image_url_post_params',
  544. 'encoding',
  545. 'favicon_url',
  546. ]) {
  547. if (!(propertyName in p)) {
  548. continue;
  549. }
  550.  
  551. if (typeof p[propertyName] !== 'string') {
  552. messages.push(_('「providers」プロパティの %i 番目の要素の「%s」プロパティは文字列ではありません。')
  553. .replace('%i', i).replace('%s', propertyName));
  554. continue;
  555. }
  556.  
  557. if ([ 'search_url', 'favicon_url', 'image_url' ].includes(propertyName)) {
  558. let url;
  559. try {
  560. url = new URL(p[propertyName]);
  561. } catch (exception) {
  562. if (!(exception instanceof TypeError)) {
  563. throw exception;
  564. }
  565. }
  566. if (!url) {
  567. const schemas = propertyName === 'favicon_url' ? [ 'data' ] : [ 'https', 'http' ];
  568. if (schemas.includes(url.protocol)) {
  569. messages.push(_('「providers」プロパティの %i 番目の要素の「%1s」プロパティは、%2s で始まる妥当なURLではありません。')
  570. .replace('%i', i).replace('%1s', propertyName).replace('%2s', schemas.join(', ')));
  571. continue;
  572. }
  573. }
  574. }
  575.  
  576. provider[propertyName] = p[propertyName];
  577. }
  578.  
  579. if (!('name' in provider)) {
  580. messages.push(_('「providers」プロパティの %i 番目の要素に「name」プロパティが存在しません。').replace('%i', i));
  581. continue;
  582. }
  583.  
  584. if (!('search_url' in provider) && !('image_url' in provider)) {
  585. // 「search_url」「image_url」プロパティがいずれも存在しない場合は、ブラウザの検索エンジンの指定として扱う
  586. const browserSearchEngine = Services.search.getEngineByName(provider.name);
  587. if (!browserSearchEngine) {
  588. messages.push(_('「providers」プロパティの %i 番目で指定されている「%s」という名前のブラウザ検索プロバイダは存在しません。')
  589. .replace('%i', i).replace('%s', provider.name));
  590. continue;
  591. }
  592.  
  593. settings.providers.push(this.convertToSearchProviderFromBrowserEngine(browserSearchEngine));
  594. continue;
  595. }
  596.  
  597. if ('search_url' in provider) {
  598. if (!provider.search_url.includes('{searchTerms}')
  599. && (!('search_url_post_params' in provider)
  600. || !provider.search_url_post_params.includes('{searchTerms}'))) {
  601. messages.push(
  602. _('「providers」プロパティの %i 番目の要素の「search_url」「search_url_post_params」プロパティのいずれにも、{searchTerms} が含まれません。') //eslint-disable-line max-len
  603. .replace('%i', i),
  604. );
  605. }
  606. } else {
  607. if (!('image_url_post_params' in provider)) {
  608. messages.push(_('「providers」プロパティの %i 番目の要素には「image_url_post_params」プロパティが存在しません。')
  609. .replace('%i', i));
  610. continue;
  611. }
  612.  
  613. if (!Array.from(new URLSearchParams(provider.image_url_post_params))
  614. .some(([ , value]) => value === '{searchTerms}')) {
  615. messages.push(
  616. _('「providers」プロパティの %i 番目の要素の「image_url_post_params」プロパティには、{searchTerms} に一致するクエリ値が存在しません。')
  617. .replace('%i', i),
  618. );
  619. continue;
  620. }
  621. }
  622.  
  623. settings.providers.push(provider);
  624. }
  625.  
  626. return { settings, messages };
  627. },
  628.  
  629. /**
  630. * プリセットのユーザー設定を返します。
  631. * @returns {Promise.<UserSettings>} POSTメソッドのブラウザ検索エンジンでも、「search_url_post_params」プロパティを含みません。
  632. */
  633. async preset()
  634. {
  635. const providers = (await Services.search.getVisibleEngines()).map(engine => ({ name: engine.name }));
  636.  
  637. // 画像検索例
  638. providers.push({
  639. name: _('Google 画像で検索'),
  640. image_url: 'https://lens.google.com/v3/upload',
  641. image_url_post_params: 'encoded_image={searchTerms}',
  642. encoding: StringUtils.THE_ENCODING,
  643. favicon_url: '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==', //eslint-disable-line max-len
  644. });
  645.  
  646. return {
  647. where: this.WHERES[0],
  648. providers,
  649. };
  650. },
  651. };
  652.  
  653.  
  654.  
  655. /**
  656. * ドロップゾーンの作成やドロップされたデータの検索などを行う。
  657. * @type {Object}
  658. */
  659. const DropzoneUtils = {
  660. /**
  661. * 設定されていない場合に表示するアイコンのURL。
  662. * @constant {string}
  663. */
  664. DEFAULT_ICON: 'chrome://global/skin/icons/defaultFavicon.svg',
  665.  
  666. /**
  667. * @type {UserSettings}
  668. */
  669. settings: null,
  670.  
  671. /**
  672. * ドロップゾーン専用のスタイルシートを設定するための親要素。
  673. * @type {HTMLDivElement}
  674. */
  675. wrapper: null,
  676.  
  677. /**
  678. * 各ドロップゾーンを作成。
  679. */
  680. create()
  681. {
  682. document.head.insertAdjacentHTML('beforeend', h`
  683. <style>
  684. /*------------------------------------
  685. 位置決め用
  686. */
  687. #${CSS.escape(ID)} {
  688. position: relative;
  689. }
  690.  
  691. /*------------------------------------
  692. ドロップゾーン全体
  693. */
  694. #${CSS.escape(ID)} ul {
  695. position: absolute;
  696. top: 1.5em;
  697. left: 1.5em;
  698. right: 1.5em;
  699. height: 8em;
  700. display: flex;
  701. border: solid #A0A0A0 1px;
  702. background-color: rgba(100, 200, 255, 0.5);
  703. padding-left: 0;
  704. z-index: 1;
  705. }
  706.  
  707. /*------------------------------------
  708. 各ドロップゾーン
  709. */
  710. #${CSS.escape(ID)} li {
  711. flex: 1;
  712. font-weight: bold;
  713. padding-left: 0.5em;
  714. overflow: hidden;
  715. white-space: nowrap;
  716. line-height: 2em;
  717. position: relative;
  718. z-index: 1;
  719. }
  720.  
  721. #${CSS.escape(ID)} li:not(:first-of-type) {
  722. border-left: inherit;
  723. }
  724.  
  725. #${CSS.escape(ID)} img {
  726. width: 16px;
  727. height: 16px;
  728. vertical-align: middle;
  729. margin-right: 0.3em;
  730. }
  731.  
  732. /*------------------------------------
  733. ドロップゾーン上部の背景色
  734. */
  735. #${CSS.escape(ID)} li::before {
  736. display: block;
  737. content: "";
  738. position: absolute;
  739. top: 0;
  740. left: 0;
  741. right: 0;
  742. height: 2em;
  743. background-color: rgba(50, 100, 200, 0.7);
  744. z-index: -1;
  745. }
  746.  
  747. /*------------------------------------
  748. 各ドロップゾーンにポインタが載っている時
  749. */
  750. #${CSS.escape(ID)} li.drop-active-valid::before {
  751. height: initial;
  752. bottom: 0;
  753. }
  754. </style>
  755. `);
  756. const tabbox = document.getElementById('tabbrowser-tabbox');
  757. tabbox.insertAdjacentHTML('afterbegin', h`
  758. <div xmlns="${DOMUtils.HTML_NS}" id="${ID}" hidden="">
  759. <ul></ul>
  760. </div>
  761. `);
  762. this.wrapper = tabbox.firstElementChild;
  763.  
  764. // 構築
  765. this.wrapper.getElementsByTagName('ul')[0]
  766. .append(...this.settings.providers.map(this.convertFromSearchProvider));
  767. },
  768.  
  769. /**
  770. * アイコンが読み込まれていないエンジンがあれば、再読み込みを行い、ドロップゾーンへ設定し直す。
  771. * @returns {Promise.<void>}
  772. */
  773. reloadIcons()
  774. {
  775. return Promise.all(this.settings.providers.map(async provider => {
  776. if (provider.favicon_url || !provider.reloadFaviconURL) {
  777. return;
  778. }
  779.  
  780. await provider.reloadFaviconURL();
  781. if (!provider.favicon_url) {
  782. return;
  783. }
  784.  
  785. this.wrapper.querySelector(`li[data-name="${CSS.escape(provider.name)}"] img`).src = provider.favicon_url;
  786. }));
  787. },
  788.  
  789. /**
  790. * ドロップゾーンを初期状態に戻す。
  791. * @param {boolean} [forced] - {@link DropzoneUtils.itemTypesDuringDrag}の確認を行わずに実行するなら真。
  792. */
  793. resetDropzones(forced = false)
  794. {
  795. if (forced || this.itemTypesDuringDrag) {
  796. const activeValidDropzone = this.getActiveValidDropzone();
  797. if (activeValidDropzone) {
  798. activeValidDropzone.classList.remove('drop-active-valid');
  799. }
  800. this.wrapper.hidden = true;
  801. this.itemTypesDuringDrag = null;
  802. this.dragoverEventAlreadyFired = true;
  803. }
  804. },
  805.  
  806. /**
  807. * ドロップゾーンに関するスタイルシート、イベントリスナー、およびメッセージリスナーを設定する。
  808. */
  809. setEventListeners()
  810. {
  811. // dropzone属性の代替
  812. // Bug 723008 – Implement dropzone content attribute <https://bugzilla.mozilla.org/show_bug.cgi?id=723008>
  813. this.wrapper.addEventListener('dragover', event => {
  814. const activeValidDropzone = this.getActiveValidDropzone();
  815. if (activeValidDropzone && activeValidDropzone.contains(event.target)) {
  816. event.preventDefault();
  817. }
  818. });
  819.  
  820. // イベントリスナーの追加
  821. for (const type of this.eventTypesForWindow) {
  822. window.addEventListener(type, this, true);
  823. }
  824.  
  825. // メッセージリスナーの追加
  826. window.messageManager.addMessageListener(`${ID}:dragstart`, this);
  827. window.messageManager.addMessageListener(`${ID}:drop-data`, this);
  828. },
  829.  
  830. /**
  831. * :drop(active valid)な要素にdrop-active-validクラスを追加する。
  832. * @param {HTMLElement} target - :drop(active valid)か否か調べる要素。
  833. */
  834. setActiveValidDropzone(target)
  835. {
  836. if (target.nodeType === Node.ELEMENT_NODE && this.wrapper.contains(target)) {
  837. // ドロップゾーンなら
  838. const dropzone = DOMUtils.getAttributeAsDOMTokenList(target, 'dropzone');
  839. if (dropzone.contains('link')
  840. && Array.prototype.some.call(dropzone, type => this.itemTypesDuringDrag.indexOf(type) !== -1)) {
  841. // 各ドロップゾーンにポインタが載った時、
  842. // ドロップゾーンが受け取ることができるデータをドラッグしていれば
  843. target.classList.add('drop-active-valid');
  844. }
  845. }
  846. },
  847.  
  848. /**
  849. * イベントハンドラ。
  850. * @param {Event} event
  851. */
  852. handleEvent(event)
  853. {
  854. const target = event.target;
  855.  
  856. switch (event.type) {
  857. case 'dragover':
  858. if (!this.dragoverEventAlreadyFired) {
  859. this.dragoverEventAlreadyFired = true;
  860.  
  861. // ドラッグ開始時、すでにドロップゾーン内にカーソルがあった場合、dragenterイベントが発生しないため
  862. if (target.nodeType === Node.ELEMENT_NODE) {
  863. this.setActiveValidDropzone(target);
  864. }
  865. }
  866. break;
  867.  
  868. case 'dragenter':
  869. /*if (event.relatedTarget) {
  870. const activeValidDropzone = this.getActiveValidDropzone();
  871. if (activeValidDropzone && !activeValidDropzone.contains(target)) {
  872. // 各ドロップゾーンからポインタが外れた時
  873. activeValidDropzone.classList.remove('drop-active-valid');
  874. }
  875.  
  876. this.setActiveValidDropzone(target);
  877. } else {
  878. // ウィンドウ外からのドラッグなら
  879. if (this.itemTypesDuringDrag) {
  880. if (this.itemTypesDuringDrag.length > 0) {
  881. this.reloadIcons();
  882. this.wrapper.hidden = false;
  883. }
  884. } else {
  885. // ドラッグ開始なら
  886. if (event.isTrusted) {
  887. this.itemTypesDuringDrag
  888. = ['string:text/plain', 'file:text/*', 'file:image/*'];
  889.  
  890. // ドロップゾーンを表示
  891. this.reloadIcons();
  892. this.wrapper.hidden = false;
  893. }
  894. }
  895. }*/
  896. // Firefox 54 におけるリグレッション (event.relatedTargetが常にnullを返すようになった) への対処
  897. if (this.itemTypesDuringDrag) {
  898. if (this.itemTypesDuringDrag.length > 0) {
  899. this.reloadIcons();
  900. this.wrapper.hidden = false;
  901.  
  902. const activeValidDropzone = this.getActiveValidDropzone();
  903. if (activeValidDropzone && !activeValidDropzone.contains(target)) {
  904. // 各ドロップゾーンからポインタが外れた時
  905. activeValidDropzone.classList.remove('drop-active-valid');
  906. }
  907.  
  908. this.setActiveValidDropzone(target);
  909. }
  910. } else if (this.wrapper.hidden && event.isTrusted
  911. && (!event.dataTransfer.mozSourceNode
  912. || event.dataTransfer.mozSourceNode.nodeType !== Node.ELEMENT_NODE
  913. || !event.dataTransfer.mozSourceNode.classList.contains('tabbrowser-tab'))) {
  914. // ウィンドウ外からのドラッグなら
  915. // ドラッグ開始なら
  916. this.itemTypesDuringDrag = ['string:text/plain', 'file:text/*', 'file:image/*'];
  917.  
  918. // ドロップゾーンを表示
  919. this.reloadIcons();
  920. this.wrapper.hidden = false;
  921. }
  922. break;
  923.  
  924. case 'dragleave':
  925. if (this.itemTypesDuringDrag && !event.relatedTarget && !this.wrapper.hidden) {
  926. // ウィンドウ外へドラッグされたとき
  927. if (target.ownerDocument) {
  928. // Firefox 54 におけるリグレッション (event.relatedTargetが常にnullを返すようになった) への対処
  929. const win = target.ownerDocument.defaultView;
  930. const x = event.clientX;
  931. const y = event.clientY;
  932. /*eslint-disable yoda */
  933. if (0 < x && x < win.innerWidth && 0 < y && y < win.innerHeight) {
  934. break;
  935. }
  936. /*eslint-enable */
  937. }
  938.  
  939. this.wrapper.hidden = true;
  940. const activeValidDropzone = this.getActiveValidDropzone();
  941. if (activeValidDropzone) {
  942. activeValidDropzone.classList.remove('drop-active-valid');
  943. }
  944. }
  945. break;
  946.  
  947. case 'dragend':
  948. this.resetDropzones();
  949. gBrowser.selectedBrowser.messageManager.sendAsyncMessage(`${ID}:dragend`);
  950. break;
  951.  
  952. case 'drop':
  953. if (this.wrapper.contains(target)) {
  954. // 各ドロップゾーンにドロップされた時
  955. event.preventDefault();
  956. event.dataTransfer; // 後から参照できるようにdataTransferを参照しておく
  957. this.dropEvent = event;
  958. gBrowser.selectedBrowser.messageManager.sendAsyncMessage(
  959. `${ID}:drop`,
  960. {asImage: DOMUtils.getAttributeAsDOMTokenList(target, 'dropzone').contains('file:image/*')},
  961. );
  962. } else {
  963. this.resetDropzones();
  964. }
  965. break;
  966. }
  967. },
  968.  
  969. /**
  970. * @param {Object} message
  971. */
  972. async receiveMessage(message)
  973. {
  974. if (message.name.startsWith(`${ID}:`)) {
  975. switch (message.name.replace(`${ID}:`, '')) {
  976. case 'dragstart':
  977. if (this.itemTypesDuringDrag) {
  978. // ドロップゾーンが表示されたままなら
  979. this.resetDropzones();
  980. }
  981.  
  982. this.itemTypesDuringDrag = message.data.itemTypes;
  983. if (this.itemTypesDuringDrag.length > 0) {
  984. // ドロップゾーンを表示
  985. this.reloadIcons();
  986. this.wrapper.hidden = false;
  987. this.dragoverEventAlreadyFired = false;
  988. }
  989. break;
  990.  
  991. case 'drop-data': {
  992. let data = null;
  993. if (message.data.imageURL) {
  994. // 画像としてドロップしたとき
  995. data = await this.fetchBlobFromURL(message.data.imageURL);
  996. } else if (message.data.text !== undefined) {
  997. // 文字列としてドロップしたとき
  998. data = message.data.text;
  999. } else {
  1000. // ウィンドウ外からのドロップ
  1001. const dropzone = DOMUtils.getAttributeAsDOMTokenList(this.dropEvent.target, 'dropzone');
  1002. if (dropzone.contains('file:image/*')) {
  1003. // 画像ファイルとしてドロップしたとき
  1004. const file = this.dropEvent.dataTransfer.files[0];
  1005. if (file.type.startsWith('image/')) {
  1006. // ドロップゾーンが受け取ることができる形式のファイルなら
  1007. data = file;
  1008. }
  1009. } else {
  1010. // 文字列としてドロップしたとき
  1011. data = this.getTextFromDropEvent(this.dropEvent, !dropzone.contains('file:text/*'));
  1012. }
  1013. }
  1014. if (data !== null) {
  1015. this.searchDropData(
  1016. data,
  1017. Array.from(this.dropEvent.target.parentElement.children).indexOf(this.dropEvent.target),
  1018. this.dropEvent,
  1019. );
  1020. }
  1021. this.dropEvent = null;
  1022. this.resetDropzones();
  1023. break;
  1024. }
  1025. }
  1026. }
  1027. },
  1028.  
  1029. /**
  1030. * ユーザー設定を元に、ドロップゾーンを作成する。
  1031. * @param {SearchProvider} provider
  1032. * @returns {HTMLLIElement}
  1033. */
  1034. convertFromSearchProvider(provider)
  1035. {
  1036. const li = document.createElementNS(DOMUtils.HTML_NS, 'li');
  1037.  
  1038. // dropzone属性
  1039. const dropzone = DOMUtils.getAttributeAsDOMTokenList(li, 'dropzone');
  1040. dropzone.add('link');
  1041. if (provider.image_url) {
  1042. dropzone.add('file:image/*');
  1043. } else {
  1044. dropzone.add('string:text/plain');
  1045. if ('search_url_post_params' in provider) {
  1046. dropzone.add('file:text/*');
  1047. }
  1048. }
  1049. li.setAttribute('dropzone', dropzone);
  1050.  
  1051. // アイコン
  1052. const icon = new Image(16, 16);
  1053. icon.src = provider.favicon_url || DropzoneUtils.DEFAULT_ICON;
  1054. li.appendChild(icon);
  1055.  
  1056. // 表示名
  1057. li.appendChild(new Text(provider.name));
  1058. li.dataset.name = provider.name;
  1059.  
  1060. return li;
  1061. },
  1062.  
  1063. /**
  1064. * contentプロセスで実行するスクリプトを設定する。
  1065. */
  1066. setContentScript()
  1067. {
  1068. this.contentScriptURL = 'data:application/ecmascript;charset=UTF-8,' + encodeURIComponent(
  1069. gatherTextUnder.toString().replace(/Node\.|HTMLImageElement/g, 'content.$&')
  1070. + `new ${this.contentScript.toString()}(${JSON.stringify(ID)});`,
  1071. );
  1072. Services.mm.loadFrameScript(this.contentScriptURL, true);
  1073. },
  1074.  
  1075. /**
  1076. * contentプロセスで実行するクラス。
  1077. * @type {Function}
  1078. */
  1079. contentScript: class ContentScript {
  1080. /**
  1081. * XML Binding Language (XBL) の名前空間。
  1082. * @access private
  1083. * @constant {string}
  1084. */
  1085. static get XBL_NS() {return 'http://www.mozilla.org/xbl';}
  1086.  
  1087. /**
  1088. * @param {string} id
  1089. */
  1090. constructor(id)
  1091. {
  1092. /**
  1093. * @type {string}
  1094. */
  1095. this.id = id;
  1096.  
  1097. addMessageListener(`${this.id}:drop`, this);
  1098. addMessageListener(`${this.id}:dragend`, this);
  1099. addEventListener('dragstart', this, true);
  1100. }
  1101.  
  1102. /**
  1103. * @param {DragEvent} event
  1104. */
  1105. handleEvent(event)
  1106. {
  1107. switch (event.type) {
  1108. case 'dragstart':
  1109. if (event.isTrusted) {
  1110. // ユーザーによるドラッグなら
  1111. /**
  1112. * @access private
  1113. * @type {DragEvent}
  1114. */
  1115. this.dragstartEvent = event;
  1116. sendAsyncMessage(`${this.id}:dragstart`, {itemTypes: this.getItemTypes(event)});
  1117. }
  1118. break;
  1119. }
  1120. }
  1121.  
  1122. /**
  1123. * @param {Object} message
  1124. */
  1125. receiveMessage(message)
  1126. {
  1127. if (message.name.startsWith(`${this.id}:`)) {
  1128. switch (message.name.replace(`${this.id}:`, '')) {
  1129. case 'drop': {
  1130. const obj = {};
  1131. if (this.dragstartEvent) {
  1132. if (message.data.asImage) {
  1133. // 画像としてドロップしたとき
  1134. obj.imageURL = this.getImageURLFromDragstartEvent(this.dragstartEvent);
  1135. } else {
  1136. // 文字列としてドロップしたとき
  1137. obj.text = this.getTextFromDragstartEvent(this.dragstartEvent);
  1138. }
  1139. }
  1140. sendAsyncMessage(`${this.id}:drop-data`, obj);
  1141. this.dragstartEvent = null;
  1142. break;
  1143. }
  1144.  
  1145. case 'dragend':
  1146. this.dragstartEvent = null;
  1147. break;
  1148. }
  1149. }
  1150. }
  1151.  
  1152. /**
  1153. * ドラッグしようとしているアイテムの種類を取得する。
  1154. * @access private
  1155. * @param {DragEvent} event - dragstartイベント。
  1156. * @returns {string[]}
  1157. * @access protected
  1158. */
  1159. getItemTypes(event)
  1160. {
  1161. const types = [];
  1162.  
  1163. const target = event.target;
  1164. const name = target.localName || target.nodeName;
  1165. if (['a', 'img', '#text'].indexOf(name) !== -1
  1166. || ['input', 'textarea'].indexOf(name) !== -1 && !target.draggable
  1167. || target === document.documentElement && target.id === 'placesTreeBindings'
  1168. && target.namespaceURI === ContentScript.XBL_NS && target.localName === 'bindings') {
  1169. // ソースノードがリンク・画像・文字列、ドラッグ不可のテキスト入力欄、またはツリー表示されているXML文書なら
  1170. types.push('string:text/plain');
  1171. }
  1172.  
  1173. if (name === 'img' || name === 'a' && target.getElementsByTagName('img')[0]) {
  1174. // ソースノードが画像、または画像を含むリンクなら
  1175. types.push('file:image/*');
  1176. }
  1177.  
  1178. return types;
  1179. }
  1180.  
  1181. /**
  1182. * dragstartイベントから、対象の画像URLを取得する。
  1183. * @param {DragEvent} event
  1184. * @returns {?string}
  1185. */
  1186. getImageURLFromDragstartEvent(event)
  1187. {
  1188. let url = null;
  1189.  
  1190. const target = event.target;
  1191. switch (target.localName) {
  1192. case 'img':
  1193. url = target.src;
  1194. break;
  1195.  
  1196. case 'a': {
  1197. const images = target.getElementsByTagName('img');
  1198. if (images.length === 1) {
  1199. url = images[0].src;
  1200. } else {
  1201. const image = target.ownerDocument.elementFromPoint(event.clientX, event.clientY);
  1202. url = image.localName === 'img' && target.contains(image) ? image.src : images[0].src;
  1203. }
  1204. break;
  1205. }
  1206. }
  1207.  
  1208. return url;
  1209. }
  1210.  
  1211. /**
  1212. * dragstartイベントから、対象の文字列を取得する。
  1213. * @access private
  1214. * @param {DragEvent} event
  1215. * @returns {string}
  1216. */
  1217. getTextFromDragstartEvent(event)
  1218. {
  1219. let text = '';
  1220. let selection;
  1221. let selectedString = '';
  1222.  
  1223. const target = event.target;
  1224. const localName = target.localName;
  1225. const doc = target.ownerDocument;
  1226.  
  1227. if ('getSelection' in doc) {
  1228. selection = doc.getSelection();
  1229. if (selection) {
  1230. selectedString = selection.toString();
  1231. if (selectedString && (localName === 'a' || target.nodeType === content.Node.TEXT_NODE)) {
  1232. // リンクか選択範囲をドラッグしていれば
  1233. const x = event.clientX, y = event.clientY;
  1234. if (!this.isSuperposedCoordinateOnSelection(selection, x, y)) {
  1235. // ドラッグ開始位置が選択範囲外なら
  1236. let element = doc.elementFromPoint(x, y);
  1237. if (element && (element.localName === 'a' || (element = element.closest('a')))) {
  1238. // 選択範囲が重なったリンクの、選択範囲でない部分をドラッグしていれば
  1239. text = gatherTextUnder(element);
  1240. }
  1241. }
  1242. }
  1243. }
  1244. }
  1245.  
  1246. if (!text && ['a', 'img'].indexOf(localName) !== -1) {
  1247. // リンクか画像をドラッグしていれば
  1248. if (selectedString && selection && selection.containsNode(target, true)) {
  1249. // ソースノードが選択範囲と重なっており、
  1250. // リンクの一部分だけが選択されている場合は、選択範囲とドラッグ開始位置が重なっていれば
  1251. text = selectedString;
  1252. }
  1253.  
  1254. if (!text) {
  1255. if (localName === 'img') {
  1256. // 画像をドラッグしていれば
  1257. text = selectedString || target.alt || target.title;
  1258. if (!text) {
  1259. const figcaption = DOMUtils.getFigcaption(target);
  1260. if (figcaption) {
  1261. text = gatherTextUnder(figcaption);
  1262. }
  1263. }
  1264. } else {
  1265. // リンクをドラッグしていれば
  1266. text = gatherTextUnder(target);
  1267. }
  1268. }
  1269. }
  1270.  
  1271. return text.trim() || event.dataTransfer.getData('text/plain').trim();
  1272. }
  1273.  
  1274. /**
  1275. * 選択範囲と指定した座標が重なるか調べる。
  1276. * @access private
  1277. * @param {Selection} selection
  1278. * @param {number} x
  1279. * @param {number} y
  1280. * @returns {boolean}
  1281. */
  1282. isSuperposedCoordinateOnSelection(selection, x, y)
  1283. {
  1284. for (let i = 0, l = selection.rangeCount; i < l; i++) {
  1285. if (this.isSuperposedCoordinateOnRange(selection.getRangeAt(i), x, y)) {
  1286. return true;
  1287. }
  1288. }
  1289. return false;
  1290. }
  1291.  
  1292. /**
  1293. * rangeと指定した座標が重なるか調べる。
  1294. * @access private
  1295. * @param {Range} range
  1296. * @param {number} x
  1297. * @param {number} y
  1298. * @returns {boolean}
  1299. */
  1300. isSuperposedCoordinateOnRange(range, x, y)
  1301. {
  1302. // Firefox 53時点で、Range#getClientRects() が DOMRect[] ではなく DOMRectList を返すバグを確認
  1303. return Array.from(range.getClientRects()).some(rect => this.isSuperposedCoordinateOnRect(rect, x, y));
  1304. }
  1305.  
  1306. /**
  1307. * 長方形と指定した座標が重なるか調べる。
  1308. * @access private
  1309. * @param {DOMRect} rect
  1310. * @param {number} x
  1311. * @param {number} y
  1312. * @returns {boolean}
  1313. */
  1314. isSuperposedCoordinateOnRect(rect, x, y)
  1315. {
  1316. return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
  1317. }
  1318. },
  1319.  
  1320. /**
  1321. * contentプロセスで実行するスクリプトのURL。
  1322. * @returns {string}
  1323. * @access protected
  1324. */
  1325. contentScriptURL: '',
  1326.  
  1327. /**
  1328. * windowに追加するイベントリスナーが補足するイベントの種類。
  1329. * @type {string[]}
  1330. * @access protected
  1331. */
  1332. eventTypesForWindow: ['dragover', 'dragenter', 'dragleave', 'dragend', 'drop'],
  1333.  
  1334. /**
  1335. * ドラッグ中のアイテムの種類。
  1336. * ドラッグ中でなければnull。
  1337. * @type {?string[]}
  1338. * @access protected
  1339. */
  1340. itemTypesDuringDrag: null,
  1341.  
  1342. /**
  1343. * ドラッグ開始後、dragoverイベントが既に発生していれば真。
  1344. * @type {booelan}
  1345. * @access protected
  1346. */
  1347. dragoverEventAlreadyFired: true,
  1348.  
  1349. /**
  1350. * dropイベント。
  1351. * @type {?DragEvent}
  1352. * @access private
  1353. */
  1354. dropEvent: null,
  1355.  
  1356. /**
  1357. * drop-active-validクラスが付いた要素を返す。
  1358. * @returns {?HTMLLIElement}
  1359. * @access protected
  1360. */
  1361. getActiveValidDropzone()
  1362. {
  1363. return this.wrapper.getElementsByClassName('drop-active-valid')[0];
  1364. },
  1365.  
  1366. /**
  1367. * ウィンドウ外からドロップされた文字列情報を取得する。
  1368. * @param {DragEvent} event - dropイベント。
  1369. * @param {boolean} [forceString] - 真が指定されていれば、常にFileインスタンスの代わりにファイル名を返す。
  1370. * @returns {?(string|File)}
  1371. * @access protected
  1372. */
  1373. getTextFromDropEvent(event, forceString = false)
  1374. {
  1375. let dropFile = null, dropText = '';
  1376.  
  1377. const files = event.dataTransfer.files;
  1378. if (files.length > 0) {
  1379. // ファイルをドロップしていれば
  1380. if (!forceString) {
  1381. for (const file of files) {
  1382. if (BrowserUtils.mimeTypeIsTextBased(file.type)) {
  1383. // テキストファイルなら
  1384. dropFile = file;
  1385. break;
  1386. }
  1387. }
  1388. }
  1389.  
  1390. if (!dropFile) {
  1391. // テキスト形式でないファイルがドロップされているかforceStringが指定されていれば、ファイル名を取得する
  1392. dropText = files[0].name;
  1393. }
  1394. } else {
  1395. dropText = event.dataTransfer.getData('text/plain');
  1396. }
  1397.  
  1398. return dropFile ? dropFile : dropText.trim() || null;
  1399. },
  1400.  
  1401. /**
  1402. * URLからファイルを取得する。
  1403. * @param {string} url - ファイルのURL。
  1404. * @returns {Promise.<Blob>}
  1405. * @access protected
  1406. */
  1407. fetchBlobFromURL(url)
  1408. {
  1409. return fetch(url, {credentials: 'include', cache: 'force-cache'}).then(response => response.blob());
  1410. },
  1411.  
  1412. /**
  1413. * ドロップされたデータを、ドロップゾーンに結びつけられたプロバイダで検索する。
  1414. * @param {(string|Blob)} data - 検索する文字列、またはファイル。
  1415. * @param {number} providerIndex - {@link DropzoneUtils.settings.providers}内の0から始まるインデックス。
  1416. * @param {DragEvent} event - どのキーを押しているか取得するためのdropイベント。
  1417. * @access protected
  1418. */
  1419. async searchDropData(data, providerIndex, event)
  1420. {
  1421. const mimeType = data.type;
  1422. if (mimeType && BrowserUtils.mimeTypeIsTextBased(mimeType) && !/^image\//.test(mimeType)) {
  1423. // ドロップされたデータがテキストファイルなら、文字列に変換しておく
  1424. const fileReader = new FileReader();
  1425. fileReader.addEventListener('load', () => {
  1426. this.searchDropData(fileReader.result, providerIndex, event);
  1427. });
  1428. fileReader.readAsText(data);
  1429. return;
  1430. }
  1431.  
  1432. const provider = this.settings.providers[providerIndex];
  1433.  
  1434. let url = provider.search_url || provider.image_url;
  1435. let postData;
  1436. if (url) {
  1437. // ユーザー定義の検索プロバイダ
  1438. const params = provider.search_url_post_params || provider.image_url_post_params;
  1439. if (params) {
  1440. // POST
  1441. const formData = new FormData();
  1442. for (const [name, value] of new URLSearchParams(params)) {
  1443. formData.append(name, value.includes('{searchTerms}') ? data : value);
  1444. }
  1445. postData = await StringUtils.encodeMultipartFormData(formData);
  1446. } else {
  1447. // GET
  1448. let encodedString;
  1449. try {
  1450. encodedString = TextToSubURI.ConvertAndEscape(provider.encoding, data);
  1451. } catch (e) {
  1452. if (e.result === Cr.NS_ERROR_UCONV_NOCONV) {
  1453. encodedString = TextToSubURI.ConvertAndEscape(StringUtils.THE_ENCODING, data);
  1454. } else {
  1455. throw e;
  1456. }
  1457. }
  1458. url = url.replace(/{searchTerms}/g, encodedString);
  1459. }
  1460. } else {
  1461. // ブラウザに登録されている検索エンジン
  1462. const browserSearchEngine = Services.search.getEngineByName(provider.name);
  1463. const submission = browserSearchEngine.getSubmission(data);
  1464. url = submission.uri.spec;
  1465. postData = submission.postData;
  1466. }
  1467.  
  1468. this.openSearchResult(url, event, postData);
  1469. },
  1470.  
  1471. /**
  1472. * ユーザー設定に基づき、適切な場所で検索結果を開く。
  1473. * @param {string} url
  1474. * @param {DragEvent} event - どのキーを押しているか取得するためのdropイベント。
  1475. * @param {nsIInputStream} [postData]
  1476. * @access protected
  1477. */
  1478. openSearchResult(url, event, postData = null)
  1479. {
  1480. const where = this.settings.where;
  1481. if (where === 'current') {
  1482. openUILink(
  1483. url,
  1484. event,
  1485. { postData, triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}) },
  1486. );
  1487. } else {
  1488. openWebLinkIn(url, where, { postData });
  1489. }
  1490. },
  1491. };
  1492.  
  1493.  
  1494.  
  1495. /**
  1496. * ポップアップ通知を表示する。
  1497. * @param {string} message - 表示するメッセージ。
  1498. * @param {XULElement} tab - メッセージを表示するタブ。
  1499. * @param {string} [type] - メッセージの前に表示するアイコンの種類。「info」「warning」「error」のいずれか。
  1500. */
  1501. function showPopupNotification(message, type = 'info')
  1502. {
  1503. PopupNotifications
  1504. .show(gBrowser.getBrowserForTab(gBrowser.selectedTab), ID, '【Drag & Dropzones +】' + message, null, null, null, {
  1505. persistWhileVisible: true,
  1506. removeOnDismissal: true,
  1507. popupIconURL: `chrome://global/skin/icons/${type}.svg`,
  1508. });
  1509. }
  1510.  
  1511.  
  1512.  
  1513. DropzoneUtils.setContentScript();
  1514.  
  1515. const obj = await SettingsUtils.load();
  1516.  
  1517. // 検索エンジンサービスの初期化を待機
  1518. await Services.search.init();
  1519.  
  1520. let { settings, messages } = SettingsUtils.filter(obj);
  1521.  
  1522. if (settings && settings.providers.length === 0) {
  1523. messages.push(_('検索プロバイダが1つも指定されていません。'));
  1524. }
  1525.  
  1526. if (messages.length > 0) {
  1527. showPopupNotification(messages.join(' / '), settings && settings.providers > 0 ? 'warning' : 'error');
  1528. }
  1529.  
  1530. if (!settings || settings.providers === 0) {
  1531. const obj = await SettingsUtils.preset();
  1532. if (settings) {
  1533. obj.where = settings.where;
  1534. }
  1535. settings = SettingsUtils.filter(obj).settings;
  1536. }
  1537.  
  1538. DropzoneUtils.settings = settings;
  1539.  
  1540. // ドロップゾーンの作成
  1541. DropzoneUtils.create();
  1542. DropzoneUtils.setEventListeners();
  1543.  
  1544. })();