You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

635 lines
20 KiB

  1. /*******************************************************************************
  2. * jquery.ui-contextmenu.js plugin.
  3. *
  4. * jQuery plugin that provides a context menu (based on the jQueryUI menu widget).
  5. *
  6. * @see https://github.com/mar10/jquery-ui-contextmenu
  7. *
  8. * Copyright (c) 2013-2018, Martin Wendt (http://wwWendt.de). Licensed MIT.
  9. */
  10. (function( factory ) {
  11. "use strict";
  12. if ( typeof define === "function" && define.amd ) {
  13. // AMD. Register as an anonymous module.
  14. define([ "jquery", "jquery-ui/ui/widgets/menu" ], factory );
  15. } else {
  16. // Browser globals
  17. factory( jQuery );
  18. }
  19. }(function( $ ) {
  20. "use strict";
  21. var supportSelectstart = "onselectstart" in document.createElement("div"),
  22. match = $.ui.menu.version.match(/^(\d)\.(\d+)/),
  23. uiVersion = {
  24. major: parseInt(match[1], 10),
  25. minor: parseInt(match[2], 10)
  26. },
  27. isLTE110 = ( uiVersion.major < 2 && uiVersion.minor <= 10 ),
  28. isLTE111 = ( uiVersion.major < 2 && uiVersion.minor <= 11 );
  29. $.widget("moogle.contextmenu", {
  30. version: "@VERSION",
  31. options: {
  32. addClass: "ui-contextmenu", // Add this class to the outer <ul>
  33. closeOnWindowBlur: true, // Close menu when window loses focus
  34. appendTo: "body", // Set keyboard focus to first entry on open
  35. autoFocus: false, // Set keyboard focus to first entry on open
  36. autoTrigger: true, // open menu on browser's `contextmenu` event
  37. delegate: null, // selector
  38. hide: { effect: "fadeOut", duration: "fast" },
  39. ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents
  40. menu: null, // selector or jQuery pointing to <UL>, or a definition hash
  41. position: null, // popup positon
  42. preventContextMenuForPopup: false, // prevent opening the browser's system
  43. // context menu on menu entries
  44. preventSelect: false, // disable text selection of target
  45. show: { effect: "slideDown", duration: "fast" },
  46. taphold: false, // open menu on taphold events (requires external plugins)
  47. uiMenuOptions: {}, // Additional options, used when UI Menu is created
  48. // Events:
  49. beforeOpen: $.noop, // menu about to open; return `false` to prevent opening
  50. blur: $.noop, // menu option lost focus
  51. close: $.noop, // menu was closed
  52. create: $.noop, // menu was initialized
  53. createMenu: $.noop, // menu was initialized (original UI Menu)
  54. focus: $.noop, // menu option got focus
  55. open: $.noop, // menu was opened
  56. select: $.noop // menu option was selected; return `false` to prevent closing
  57. },
  58. /** Constructor */
  59. _create: function() {
  60. var cssText, eventNames, targetId,
  61. opts = this.options;
  62. this.$headStyle = null;
  63. this.$menu = null;
  64. this.menuIsTemp = false;
  65. this.currentTarget = null;
  66. this.extraData = {};
  67. this.previousFocus = null;
  68. if (opts.delegate == null) {
  69. $.error("ui-contextmenu: Missing required option `delegate`.");
  70. }
  71. if (opts.preventSelect) {
  72. // Create a global style for all potential menu targets
  73. // If the contextmenu was bound to `document`, we apply the
  74. // selector relative to the <body> tag instead
  75. targetId = ($(this.element).is(document) ? $("body")
  76. : this.element).uniqueId().attr("id");
  77. cssText = "#" + targetId + " " + opts.delegate + " { " +
  78. "-webkit-user-select: none; " +
  79. "-khtml-user-select: none; " +
  80. "-moz-user-select: none; " +
  81. "-ms-user-select: none; " +
  82. "user-select: none; " +
  83. "}";
  84. this.$headStyle = $("<style class='moogle-contextmenu-style' />")
  85. .prop("type", "text/css")
  86. .appendTo("head");
  87. try {
  88. this.$headStyle.html(cssText);
  89. } catch ( e ) {
  90. // issue #47: fix for IE 6-8
  91. this.$headStyle[0].styleSheet.cssText = cssText;
  92. }
  93. // TODO: the selectstart is not supported by FF?
  94. if (supportSelectstart) {
  95. this.element.on("selectstart" + this.eventNamespace, opts.delegate,
  96. function(event) {
  97. event.preventDefault();
  98. });
  99. }
  100. }
  101. this._createUiMenu(opts.menu);
  102. eventNames = "contextmenu" + this.eventNamespace;
  103. if (opts.taphold) {
  104. eventNames += " taphold" + this.eventNamespace;
  105. }
  106. this.element.on(eventNames, opts.delegate, $.proxy(this._openMenu, this));
  107. },
  108. /** Destructor, called on $().contextmenu("destroy"). */
  109. _destroy: function() {
  110. this.element.off(this.eventNamespace);
  111. this._createUiMenu(null);
  112. if (this.$headStyle) {
  113. this.$headStyle.remove();
  114. this.$headStyle = null;
  115. }
  116. },
  117. /** (Re)Create jQuery UI Menu. */
  118. _createUiMenu: function(menuDef) {
  119. var ct, ed,
  120. opts = this.options;
  121. // Remove temporary <ul> if any
  122. if (this.isOpen()) {
  123. // #58: 'replaceMenu' in beforeOpen causing select: to lose ui.target
  124. ct = this.currentTarget;
  125. ed = this.extraData;
  126. // close without animation, to force async mode
  127. this._closeMenu(true);
  128. this.currentTarget = ct;
  129. this.extraData = ed;
  130. }
  131. if (this.menuIsTemp) {
  132. this.$menu.remove(); // this will also destroy ui.menu
  133. } else if (this.$menu) {
  134. this.$menu
  135. .menu("destroy")
  136. .removeClass(opts.addClass)
  137. .hide();
  138. }
  139. this.$menu = null;
  140. this.menuIsTemp = false;
  141. // If a menu definition array was passed, create a hidden <ul>
  142. // and generate the structure now
  143. if ( !menuDef ) {
  144. return;
  145. } else if ($.isArray(menuDef)) {
  146. this.$menu = $.moogle.contextmenu.createMenuMarkup(menuDef, null, opts);
  147. this.menuIsTemp = true;
  148. }else if ( typeof menuDef === "string" ) {
  149. this.$menu = $(menuDef);
  150. } else {
  151. this.$menu = menuDef;
  152. }
  153. // Create - but hide - the jQuery UI Menu widget
  154. this.$menu
  155. .hide()
  156. .addClass(opts.addClass)
  157. // Create a menu instance that delegates events to our widget
  158. .menu($.extend(true, {}, opts.uiMenuOptions, {
  159. items: "> :not(.ui-widget-header)",
  160. blur: $.proxy(opts.blur, this),
  161. create: $.proxy(opts.createMenu, this),
  162. focus: $.proxy(opts.focus, this),
  163. select: $.proxy(function(event, ui) {
  164. // User selected a menu entry
  165. var retval,
  166. isParent = $.moogle.contextmenu.isMenu(ui.item),
  167. actionHandler = ui.item.data("actionHandler");
  168. ui.cmd = ui.item.attr("data-command");
  169. ui.target = $(this.currentTarget);
  170. ui.extraData = this.extraData;
  171. // ignore clicks, if they only open a sub-menu
  172. if ( !isParent || !opts.ignoreParentSelect) {
  173. retval = this._trigger.call(this, "select", event, ui);
  174. if ( actionHandler ) {
  175. retval = actionHandler.call(this, event, ui);
  176. }
  177. if ( retval !== false ) {
  178. this._closeMenu.call(this);
  179. }
  180. event.preventDefault();
  181. }
  182. }, this)
  183. }));
  184. },
  185. /** Open popup (called on 'contextmenu' event). */
  186. _openMenu: function(event, recursive) {
  187. var res, promise, ui,
  188. opts = this.options,
  189. posOption = opts.position,
  190. self = this,
  191. manualTrigger = !!event.isTrigger;
  192. if ( !opts.autoTrigger && !manualTrigger ) {
  193. // ignore browser's `contextmenu` events
  194. return;
  195. }
  196. // Prevent browser from opening the system context menu
  197. event.preventDefault();
  198. this.currentTarget = event.target;
  199. this.extraData = event._extraData || {};
  200. ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData,
  201. originalEvent: event, result: null };
  202. if ( !recursive ) {
  203. res = this._trigger("beforeOpen", event, ui);
  204. promise = (ui.result && $.isFunction(ui.result.promise)) ? ui.result : null;
  205. ui.result = null;
  206. if ( res === false ) {
  207. this.currentTarget = null;
  208. return false;
  209. } else if ( promise ) {
  210. // Handler returned a Deferred or Promise. Delay menu open until
  211. // the promise is resolved
  212. promise.done(function() {
  213. self._openMenu(event, true);
  214. });
  215. this.currentTarget = null;
  216. return false;
  217. }
  218. ui.menu = this.$menu; // Might have changed in beforeOpen
  219. }
  220. // Register global event handlers that close the dropdown-menu
  221. $(document).on("keydown" + this.eventNamespace, function(event) {
  222. if ( event.which === $.ui.keyCode.ESCAPE ) {
  223. self._closeMenu();
  224. }
  225. }).on("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace,
  226. function(event) {
  227. // Close menu when clicked outside menu
  228. if ( !$(event.target).closest(".ui-menu-item").length ) {
  229. self._closeMenu();
  230. }
  231. });
  232. $(window).on("blur" + this.eventNamespace, function(event) {
  233. if ( opts.closeOnWindowBlur ) {
  234. self._closeMenu();
  235. }
  236. });
  237. // required for custom positioning (issue #18 and #13).
  238. if ($.isFunction(posOption)) {
  239. posOption = posOption(event, ui);
  240. }
  241. posOption = $.extend({
  242. my: "left top",
  243. at: "left bottom",
  244. // if called by 'open' method, event does not have pageX/Y
  245. of: (event.pageX === undefined) ? event.target : event,
  246. collision: "fit"
  247. }, posOption);
  248. // Update entry statuses from callbacks
  249. this._updateEntries(this.$menu);
  250. // Finally display the popup
  251. this.$menu
  252. .show() // required to fix positioning error
  253. .css({
  254. position: "absolute",
  255. left: 0,
  256. top: 0
  257. }).position(posOption)
  258. .hide(); // hide again, so we can apply nice effects
  259. if ( opts.preventContextMenuForPopup ) {
  260. this.$menu.on("contextmenu" + this.eventNamespace, function(event) {
  261. event.preventDefault();
  262. });
  263. }
  264. this._show(this.$menu, opts.show, function() {
  265. var $first;
  266. // Set focus to first active menu entry
  267. if ( opts.autoFocus ) {
  268. self.previousFocus = $(event.target);
  269. // self.$menu.focus();
  270. $first = self.$menu
  271. .children("li.ui-menu-item")
  272. .not(".ui-state-disabled")
  273. .first();
  274. self.$menu.menu("focus", null, $first).focus();
  275. }
  276. self._trigger.call(self, "open", event, ui);
  277. });
  278. },
  279. /** Close popup. */
  280. _closeMenu: function(immediately) {
  281. var self = this,
  282. hideOpts = immediately ? false : this.options.hide,
  283. ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
  284. // Note: we don't want to unbind the 'contextmenu' event
  285. $(document)
  286. .off("mousedown" + this.eventNamespace)
  287. .off("touchstart" + this.eventNamespace)
  288. .off("keydown" + this.eventNamespace);
  289. $(window)
  290. .off("blur" + this.eventNamespace);
  291. self.currentTarget = null; // issue #44 after hide animation is too late
  292. self.extraData = {};
  293. if ( this.$menu ) { // #88: widget might have been destroyed already
  294. this.$menu
  295. .off("contextmenu" + this.eventNamespace);
  296. this._hide(this.$menu, hideOpts, function() {
  297. if ( self.previousFocus ) {
  298. self.previousFocus.focus();
  299. self.previousFocus = null;
  300. }
  301. self._trigger("close", null, ui);
  302. });
  303. } else {
  304. self._trigger("close", null, ui);
  305. }
  306. },
  307. /** Handle $().contextmenu("option", key, value) calls. */
  308. _setOption: function(key, value) {
  309. switch (key) {
  310. case "menu":
  311. this.replaceMenu(value);
  312. break;
  313. }
  314. $.Widget.prototype._setOption.apply(this, arguments);
  315. },
  316. /** Return ui-menu entry (<LI> tag). */
  317. _getMenuEntry: function(cmd) {
  318. return this.$menu.find("li[data-command=" + cmd + "]");
  319. },
  320. /** Close context menu. */
  321. close: function() {
  322. if (this.isOpen()) {
  323. this._closeMenu();
  324. }
  325. },
  326. /* Apply status callbacks when menu is opened. */
  327. _updateEntries: function() {
  328. var self = this,
  329. ui = {
  330. menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
  331. $.each(this.$menu.find(".ui-menu-item"), function(i, o) {
  332. var $entry = $(o),
  333. fn = $entry.data("disabledHandler"),
  334. res = fn ? fn({ type: "disabled" }, ui) : null;
  335. ui.item = $entry;
  336. ui.cmd = $entry.attr("data-command");
  337. // Evaluate `disabled()` callback
  338. if ( res != null ) {
  339. self.enableEntry(ui.cmd, !res);
  340. self.showEntry(ui.cmd, res !== "hide");
  341. }
  342. // Evaluate `title()` callback
  343. fn = $entry.data("titleHandler"),
  344. res = fn ? fn({ type: "title" }, ui) : null;
  345. if ( res != null ) {
  346. self.setTitle(ui.cmd, "" + res);
  347. }
  348. // Evaluate `tooltip()` callback
  349. fn = $entry.data("tooltipHandler"),
  350. res = fn ? fn({ type: "tooltip" }, ui) : null;
  351. if ( res != null ) {
  352. $entry.attr("title", "" + res);
  353. }
  354. });
  355. },
  356. /** Enable or disable the menu command. */
  357. enableEntry: function(cmd, flag) {
  358. this._getMenuEntry(cmd).toggleClass("ui-state-disabled", (flag === false));
  359. },
  360. /** Return ui-menu entry (LI tag) as jQuery object. */
  361. getEntry: function(cmd) {
  362. return this._getMenuEntry(cmd);
  363. },
  364. /** Return ui-menu entry wrapper as jQuery object.
  365. UI 1.10: this is the <a> tag inside the LI
  366. UI 1.11: this is the LI istself
  367. UI 1.12: this is the <div> tag inside the LI
  368. */
  369. getEntryWrapper: function(cmd) {
  370. return this._getMenuEntry(cmd).find(">[role=menuitem]").addBack("[role=menuitem]");
  371. },
  372. /** Return Menu element (UL). */
  373. getMenu: function() {
  374. return this.$menu;
  375. },
  376. /** Return true if menu is open. */
  377. isOpen: function() {
  378. // return this.$menu && this.$menu.is(":visible");
  379. return !!this.$menu && !!this.currentTarget;
  380. },
  381. /** Open context menu on a specific target (must match options.delegate)
  382. * Optional `extraData` is passed to event handlers as `ui.extraData`.
  383. */
  384. open: function(targetOrEvent, extraData) {
  385. // Fake a 'contextmenu' event
  386. extraData = extraData || {};
  387. var isEvent = (targetOrEvent && targetOrEvent.type && targetOrEvent.target),
  388. event = isEvent ? targetOrEvent : {},
  389. target = isEvent ? targetOrEvent.target : targetOrEvent,
  390. e = jQuery.Event("contextmenu", {
  391. target: $(target).get(0),
  392. pageX: event.pageX,
  393. pageY: event.pageY,
  394. originalEvent: isEvent ? targetOrEvent : undefined,
  395. _extraData: extraData
  396. });
  397. return this.element.trigger(e);
  398. },
  399. /** Replace the menu altogether. */
  400. replaceMenu: function(data) {
  401. this._createUiMenu(data);
  402. },
  403. /** Redefine a whole menu entry. */
  404. setEntry: function(cmd, entry) {
  405. var $ul,
  406. $entryLi = this._getMenuEntry(cmd);
  407. if (typeof entry === "string") {
  408. window.console && window.console.warn(
  409. "setEntry(cmd, t) with a plain string title is deprecated since v1.18." +
  410. "Use setTitle(cmd, '" + entry + "') instead.");
  411. return this.setTitle(cmd, entry);
  412. }
  413. $entryLi.empty();
  414. entry.cmd = entry.cmd || cmd;
  415. $.moogle.contextmenu.createEntryMarkup(entry, $entryLi);
  416. if ($.isArray(entry.children)) {
  417. $ul = $("<ul/>").appendTo($entryLi);
  418. $.moogle.contextmenu.createMenuMarkup(entry.children, $ul);
  419. }
  420. // #110: jQuery UI 1.12: refresh only works when this class is not set:
  421. $entryLi.removeClass("ui-menu-item");
  422. this.getMenu().menu("refresh");
  423. },
  424. /** Set icon (pass null to remove). */
  425. setIcon: function(cmd, icon) {
  426. return this.updateEntry(cmd, { uiIcon: icon });
  427. },
  428. /** Set title. */
  429. setTitle: function(cmd, title) {
  430. return this.updateEntry(cmd, { title: title });
  431. },
  432. // /** Set tooltip (pass null to remove). */
  433. // setTooltip: function(cmd, tooltip) {
  434. // this._getMenuEntry(cmd).attr("title", tooltip);
  435. // },
  436. /** Show or hide the menu command. */
  437. showEntry: function(cmd, flag) {
  438. this._getMenuEntry(cmd).toggle(flag !== false);
  439. },
  440. /** Redefine selective attributes of a menu entry. */
  441. updateEntry: function(cmd, entry) {
  442. var $icon, $wrapper,
  443. $entryLi = this._getMenuEntry(cmd);
  444. if ( entry.title !== undefined ) {
  445. $.moogle.contextmenu.updateTitle($entryLi, "" + entry.title);
  446. }
  447. if ( entry.tooltip !== undefined ) {
  448. if ( entry.tooltip === null ) {
  449. $entryLi.removeAttr("title");
  450. } else {
  451. $entryLi.attr("title", entry.tooltip);
  452. }
  453. }
  454. if ( entry.uiIcon !== undefined ) {
  455. $wrapper = this.getEntryWrapper(cmd),
  456. $icon = $wrapper.find("span.ui-icon").not(".ui-menu-icon");
  457. $icon.remove();
  458. if ( entry.uiIcon ) {
  459. $wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
  460. }
  461. }
  462. if ( entry.hide !== undefined ) {
  463. $entryLi.toggle(!entry.hide);
  464. } else if ( entry.show !== undefined ) {
  465. // Note: `show` is an undocumented variant. `hide: false` is preferred
  466. $entryLi.toggle(!!entry.show);
  467. }
  468. // if ( entry.isHeader !== undefined ) {
  469. // $entryLi.toggleClass("ui-widget-header", !!entry.isHeader);
  470. // }
  471. if ( entry.data !== undefined ) {
  472. $entryLi.data(entry.data);
  473. }
  474. // Set/clear class names, but handle ui-state-disabled separately
  475. if ( entry.disabled === undefined ) {
  476. entry.disabled = $entryLi.hasClass("ui-state-disabled");
  477. }
  478. if ( entry.setClass ) {
  479. if ( $entryLi.hasClass("ui-menu-item") ) {
  480. entry.setClass += " ui-menu-item";
  481. }
  482. $entryLi.removeClass();
  483. $entryLi.addClass(entry.setClass);
  484. } else if ( entry.addClass ) {
  485. $entryLi.addClass(entry.addClass);
  486. }
  487. $entryLi.toggleClass("ui-state-disabled", !!entry.disabled);
  488. // // #110: jQuery UI 1.12: refresh only works when this class is not set:
  489. // $entryLi.removeClass("ui-menu-item");
  490. // this.getMenu().menu("refresh");
  491. }
  492. });
  493. /*
  494. * Global functions
  495. */
  496. $.extend($.moogle.contextmenu, {
  497. /** Convert a menu description into a into a <li> content. */
  498. createEntryMarkup: function(entry, $parentLi) {
  499. var $wrapper = null;
  500. $parentLi.attr("data-command", entry.cmd);
  501. if ( !/[^\-\u2014\u2013\s]/.test( entry.title ) ) {
  502. // hyphen, em dash, en dash: separator as defined by UI Menu 1.10
  503. $parentLi.text(entry.title);
  504. } else {
  505. if ( isLTE110 ) {
  506. // jQuery UI Menu 1.10 or before required an `<a>` tag
  507. $wrapper = $("<a/>", {
  508. html: "" + entry.title,
  509. href: "#"
  510. }).appendTo($parentLi);
  511. } else if ( isLTE111 ) {
  512. // jQuery UI Menu 1.11 preferes to avoid `<a>` tags or <div> wrapper
  513. $parentLi.html("" + entry.title);
  514. $wrapper = $parentLi;
  515. } else {
  516. // jQuery UI Menu 1.12 introduced `<div>` wrappers
  517. $wrapper = $("<div/>", {
  518. html: "" + entry.title
  519. }).appendTo($parentLi);
  520. }
  521. if ( entry.uiIcon ) {
  522. $wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
  523. }
  524. // Store option callbacks in entry's data
  525. $.each( [ "action", "disabled", "title", "tooltip" ], function(i, attr) {
  526. if ( $.isFunction(entry[attr]) ) {
  527. $parentLi.data(attr + "Handler", entry[attr]);
  528. }
  529. });
  530. if ( entry.disabled === true ) {
  531. $parentLi.addClass("ui-state-disabled");
  532. }
  533. if ( entry.isHeader ) {
  534. $parentLi.addClass("ui-widget-header");
  535. }
  536. if ( entry.addClass ) {
  537. $parentLi.addClass(entry.addClass);
  538. }
  539. if ( $.isPlainObject(entry.data) ) {
  540. $parentLi.data(entry.data);
  541. }
  542. if ( typeof entry.tooltip === "string" ) {
  543. $parentLi.attr("title", entry.tooltip);
  544. }
  545. }
  546. },
  547. /** Convert a nested array of command objects into a <ul> structure. */
  548. createMenuMarkup: function(options, $parentUl, opts) {
  549. var i, menu, $ul, $li,
  550. appendTo = (opts && opts.appendTo) ? opts.appendTo : "body";
  551. if ( $parentUl == null ) {
  552. $parentUl = $("<ul class='ui-helper-hidden' />").appendTo(appendTo);
  553. }
  554. for (i = 0; i < options.length; i++) {
  555. menu = options[i];
  556. $li = $("<li/>").appendTo($parentUl);
  557. $.moogle.contextmenu.createEntryMarkup(menu, $li);
  558. if ( $.isArray(menu.children) ) {
  559. $ul = $("<ul/>").appendTo($li);
  560. $.moogle.contextmenu.createMenuMarkup(menu.children, $ul);
  561. }
  562. }
  563. return $parentUl;
  564. },
  565. /** Returns true if the menu item has child menu items */
  566. isMenu: function(item) {
  567. if ( isLTE110 ) {
  568. return item.has(">a[aria-haspopup='true']").length > 0;
  569. } else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
  570. return item.is("[aria-haspopup='true']");
  571. } else {
  572. return item.has(">div[aria-haspopup='true']").length > 0;
  573. }
  574. },
  575. /** Replace the title of elem', but retain icons andchild entries. */
  576. replaceFirstTextNodeChild: function(elem, html) {
  577. var $icons = elem.find(">span.ui-icon,>ul.ui-menu").detach();
  578. elem
  579. .empty()
  580. .html(html)
  581. .append($icons);
  582. },
  583. /** Updates the menu item's title */
  584. updateTitle: function(item, title) {
  585. if ( isLTE110 ) { // jQuery UI 1.10 and before used <a> tags
  586. $.moogle.contextmenu.replaceFirstTextNodeChild($("a", item), title);
  587. } else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
  588. $.moogle.contextmenu.replaceFirstTextNodeChild(item, title);
  589. } else { // jQuery UI 1.12+ introduced <div> tag wrappers
  590. $.moogle.contextmenu.replaceFirstTextNodeChild($("div", item), title);
  591. }
  592. }
  593. });
  594. }));