plugin.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. CKEDITOR.plugins.add("floatpanel", {
  6. requires: "panel"
  7. });
  8. (function() {
  9. var panels = {};
  10. function getPanel(editor, doc, parentElement, definition, level) {
  11. // Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level]
  12. var key = CKEDITOR.tools.genKey(
  13. doc.getUniqueId(),
  14. parentElement.getUniqueId(),
  15. editor.lang.dir,
  16. editor.uiColor || "",
  17. definition.css || "",
  18. level || ""
  19. ),
  20. panel = panels[key];
  21. if (!panel) {
  22. panel = panels[key] = new CKEDITOR.ui.panel(doc, definition);
  23. panel.element = parentElement.append(
  24. CKEDITOR.dom.element.createFromHtml(panel.render(editor), doc)
  25. );
  26. panel.element.setStyles({
  27. display: "none",
  28. position: "absolute"
  29. });
  30. }
  31. return panel;
  32. }
  33. /**
  34. * Represents a floating panel UI element.
  35. *
  36. * It is reused by rich combos, color combos, menus, etc.
  37. * and it renders its content using {@link CKEDITOR.ui.panel}.
  38. *
  39. * @class
  40. * @todo
  41. */
  42. CKEDITOR.ui.floatPanel = CKEDITOR.tools.createClass({
  43. /**
  44. * Creates a floatPanel class instance.
  45. *
  46. * @constructor
  47. * @param {CKEDITOR.editor} editor
  48. * @param {CKEDITOR.dom.element} parentElement
  49. * @param {Object} definition Definition of the panel that will be floating.
  50. * @param {Number} level
  51. */
  52. $: function(editor, parentElement, definition, level) {
  53. definition.forceIFrame = 1;
  54. // In case of editor with floating toolbar append panels that should float
  55. // to the main UI element.
  56. if (
  57. definition.toolbarRelated &&
  58. editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE
  59. )
  60. parentElement = CKEDITOR.document.getById("cke_" + editor.name);
  61. var doc = parentElement.getDocument(),
  62. panel = getPanel(editor, doc, parentElement, definition, level || 0),
  63. element = panel.element,
  64. iframe = element.getFirst(),
  65. that = this;
  66. // Disable native browser menu. (https://dev.ckeditor.com/ticket/4825)
  67. element.disableContextMenu();
  68. this.element = element;
  69. this._ = {
  70. editor: editor,
  71. // The panel that will be floating.
  72. panel: panel,
  73. parentElement: parentElement,
  74. definition: definition,
  75. document: doc,
  76. iframe: iframe,
  77. children: [],
  78. dir: editor.lang.dir,
  79. showBlockParams: null,
  80. markFirst:
  81. definition.markFirst !== undefined ? definition.markFirst : true
  82. };
  83. editor.on("mode", hide);
  84. editor.on("resize", hide);
  85. // When resize of the window is triggered floatpanel should be repositioned according to new dimensions.
  86. // https://dev.ckeditor.com/ticket/11724. Fixes issue with undesired panel hiding on Android and iOS.
  87. doc.getWindow().on(
  88. "resize",
  89. function() {
  90. this.reposition();
  91. },
  92. this
  93. );
  94. // We need a wrapper because events implementation doesn't allow to attach
  95. // one listener more than once for the same event on the same object.
  96. // Remember that floatPanel#hide is shared between all instances.
  97. function hide() {
  98. that.hide();
  99. }
  100. },
  101. proto: {
  102. /**
  103. * @todo
  104. */
  105. addBlock: function(name, block) {
  106. return this._.panel.addBlock(name, block);
  107. },
  108. /**
  109. * @todo
  110. */
  111. addListBlock: function(name, multiSelect) {
  112. return this._.panel.addListBlock(name, multiSelect);
  113. },
  114. /**
  115. * @todo
  116. */
  117. getBlock: function(name) {
  118. return this._.panel.getBlock(name);
  119. },
  120. /**
  121. * Shows the panel block.
  122. *
  123. * @param {String} name
  124. * @param {CKEDITOR.dom.element} offsetParent Positioned parent.
  125. * @param {Number} corner
  126. *
  127. * * For LTR (left to right) oriented editor:
  128. * * `1` = top-left
  129. * * `2` = top-right
  130. * * `3` = bottom-right
  131. * * `4` = bottom-left
  132. * * For RTL (right to left):
  133. * * `1` = top-right
  134. * * `2` = top-left
  135. * * `3` = bottom-left
  136. * * `4` = bottom-right
  137. *
  138. * @param {Number} [offsetX=0]
  139. * @param {Number} [offsetY=0]
  140. * @param {Function} [callback] A callback function executed when block positioning is done.
  141. * @todo what do exactly these params mean (especially corner)?
  142. */
  143. showBlock: function(
  144. name,
  145. offsetParent,
  146. corner,
  147. offsetX,
  148. offsetY,
  149. callback
  150. ) {
  151. var panel = this._.panel,
  152. block = panel.showBlock(name);
  153. this._.showBlockParams = [].slice.call(arguments);
  154. this.allowBlur(false);
  155. // Record from where the focus is when open panel.
  156. var editable = this._.editor.editable();
  157. this._.returnFocus = editable.hasFocus
  158. ? editable
  159. : new CKEDITOR.dom.element(CKEDITOR.document.$.activeElement);
  160. this._.hideTimeout = 0;
  161. var element = this.element,
  162. iframe = this._.iframe,
  163. // Edge prefers iframe's window to the iframe, just like the rest of the browsers (https://dev.ckeditor.com/ticket/13143).
  164. focused =
  165. CKEDITOR.env.ie && !CKEDITOR.env.edge
  166. ? iframe
  167. : new CKEDITOR.dom.window(iframe.$.contentWindow),
  168. doc = element.getDocument(),
  169. positionedAncestor = this._.parentElement.getPositionedAncestor(),
  170. position = offsetParent.getDocumentPosition(doc),
  171. positionedAncestorPosition = positionedAncestor
  172. ? positionedAncestor.getDocumentPosition(doc)
  173. : { x: 0, y: 0 },
  174. rtl = this._.dir == "rtl",
  175. left = position.x + (offsetX || 0) - positionedAncestorPosition.x,
  176. top = position.y + (offsetY || 0) - positionedAncestorPosition.y;
  177. // Floating panels are off by (-1px, 0px) in RTL mode. (https://dev.ckeditor.com/ticket/3438)
  178. if (rtl && (corner == 1 || corner == 4))
  179. left += offsetParent.$.offsetWidth;
  180. else if (!rtl && (corner == 2 || corner == 3))
  181. left += offsetParent.$.offsetWidth - 1;
  182. if (corner == 3 || corner == 4) top += offsetParent.$.offsetHeight - 1;
  183. // Memorize offsetParent by it's ID.
  184. this._.panel._.offsetParentId = offsetParent.getId();
  185. element.setStyles({
  186. top: top + "px",
  187. left: 0,
  188. display: ""
  189. });
  190. // Don't use display or visibility style because we need to
  191. // calculate the rendering layout later and focus the element.
  192. element.setOpacity(0);
  193. // To allow the context menu to decrease back their width
  194. element.getFirst().removeStyle("width");
  195. // Report to focus manager.
  196. this._.editor.focusManager.add(focused);
  197. // Configure the IFrame blur event. Do that only once.
  198. if (!this._.blurSet) {
  199. // With addEventListener compatible browsers, we must
  200. // useCapture when registering the focus/blur events to
  201. // guarantee they will be firing in all situations. (https://dev.ckeditor.com/ticket/3068, https://dev.ckeditor.com/ticket/3222 )
  202. CKEDITOR.event.useCapture = true;
  203. focused.on(
  204. "blur",
  205. function(ev) {
  206. // As we are using capture to register the listener,
  207. // the blur event may get fired even when focusing
  208. // inside the window itself, so we must ensure the
  209. // target is out of it.
  210. if (
  211. !this.allowBlur() ||
  212. ev.data.getPhase() != CKEDITOR.EVENT_PHASE_AT_TARGET
  213. )
  214. return;
  215. if (this.visible && !this._.activeChild) {
  216. // [iOS] Allow hide to be prevented if touch is bound
  217. // to any parent of the iframe blur happens before touch (https://dev.ckeditor.com/ticket/10714).
  218. if (CKEDITOR.env.iOS) {
  219. if (!this._.hideTimeout)
  220. this._.hideTimeout = CKEDITOR.tools.setTimeout(
  221. doHide,
  222. 0,
  223. this
  224. );
  225. } else {
  226. doHide.call(this);
  227. }
  228. }
  229. function doHide() {
  230. // Panel close is caused by user's navigating away the focus, e.g. click outside the panel.
  231. // DO NOT restore focus in this case.
  232. delete this._.returnFocus;
  233. this.hide();
  234. }
  235. },
  236. this
  237. );
  238. focused.on(
  239. "focus",
  240. function() {
  241. this._.focused = true;
  242. this.hideChild();
  243. this.allowBlur(true);
  244. },
  245. this
  246. );
  247. // [iOS] if touch is bound to any parent of the iframe blur
  248. // happens twice before touchstart and before touchend (https://dev.ckeditor.com/ticket/10714).
  249. if (CKEDITOR.env.iOS) {
  250. // Prevent false hiding on blur.
  251. // We don't need to return focus here because touchend will fire anyway.
  252. // If user scrolls and pointer gets out of the panel area touchend will also fire.
  253. focused.on(
  254. "touchstart",
  255. function() {
  256. clearTimeout(this._.hideTimeout);
  257. },
  258. this
  259. );
  260. // Set focus back to handle blur and hide panel when needed.
  261. focused.on(
  262. "touchend",
  263. function() {
  264. this._.hideTimeout = 0;
  265. this.focus();
  266. },
  267. this
  268. );
  269. }
  270. CKEDITOR.event.useCapture = false;
  271. this._.blurSet = 1;
  272. }
  273. panel.onEscape = CKEDITOR.tools.bind(function(keystroke) {
  274. if (this.onEscape && this.onEscape(keystroke) === false) return false;
  275. }, this);
  276. CKEDITOR.tools.setTimeout(
  277. function() {
  278. var panelLoad = CKEDITOR.tools.bind(function() {
  279. var target = element;
  280. // Reset panel width as the new content can be narrower
  281. // than the old one. (https://dev.ckeditor.com/ticket/9355)
  282. target.removeStyle("width");
  283. if (block.autoSize) {
  284. var panelDoc = block.element.getDocument(),
  285. width = (CKEDITOR.env.webkit || CKEDITOR.env.edge
  286. ? block.element
  287. : panelDoc.getBody()
  288. ).$.scrollWidth;
  289. // Account for extra height needed due to IE quirks box model bug:
  290. // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
  291. // (https://dev.ckeditor.com/ticket/3426)
  292. if (CKEDITOR.env.ie && CKEDITOR.env.quirks && width > 0)
  293. width +=
  294. (target.$.offsetWidth || 0) -
  295. (target.$.clientWidth || 0) +
  296. 3;
  297. // Add some extra pixels to improve the appearance.
  298. width += 10;
  299. target.setStyle("width", width + "px");
  300. var height = block.element.$.scrollHeight;
  301. // Account for extra height needed due to IE quirks box model bug:
  302. // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
  303. // (https://dev.ckeditor.com/ticket/3426)
  304. if (CKEDITOR.env.ie && CKEDITOR.env.quirks && height > 0)
  305. height +=
  306. (target.$.offsetHeight || 0) -
  307. (target.$.clientHeight || 0) +
  308. 3;
  309. target.setStyle("height", height + "px");
  310. // Fix IE < 8 visibility.
  311. panel._.currentBlock.element
  312. .setStyle("display", "none")
  313. .removeStyle("display");
  314. } else {
  315. target.removeStyle("height");
  316. }
  317. // Flip panel layout horizontally in RTL with known width.
  318. if (rtl) left -= element.$.offsetWidth;
  319. // Pop the style now for measurement.
  320. element.setStyle("left", left + "px");
  321. /* panel layout smartly fit the viewport size. */
  322. var panelElement = panel.element,
  323. panelWindow = panelElement.getWindow(),
  324. rect = element.$.getBoundingClientRect(),
  325. viewportSize = panelWindow.getViewPaneSize();
  326. // Compensation for browsers that dont support "width" and "height".
  327. var rectWidth = rect.width || rect.right - rect.left,
  328. rectHeight = rect.height || rect.bottom - rect.top;
  329. // Check if default horizontal layout is impossible.
  330. var spaceAfter = rtl
  331. ? rect.right
  332. : viewportSize.width - rect.left,
  333. spaceBefore = rtl ? viewportSize.width - rect.right : rect.left;
  334. if (rtl) {
  335. if (spaceAfter < rectWidth) {
  336. // Flip to show on right.
  337. if (spaceBefore > rectWidth) left += rectWidth;
  338. // Align to window left.
  339. else if (viewportSize.width > rectWidth)
  340. left = left - rect.left;
  341. // Align to window right, never cutting the panel at right.
  342. else left = left - rect.right + viewportSize.width;
  343. }
  344. } else if (spaceAfter < rectWidth) {
  345. // Flip to show on left.
  346. if (spaceBefore > rectWidth) left -= rectWidth;
  347. // Align to window right.
  348. else if (viewportSize.width > rectWidth)
  349. left = left - rect.right + viewportSize.width;
  350. // Align to window left, never cutting the panel at left.
  351. else left = left - rect.left;
  352. }
  353. // Check if the default vertical layout is possible.
  354. var spaceBelow = viewportSize.height - rect.top,
  355. spaceAbove = rect.top;
  356. if (spaceBelow < rectHeight) {
  357. // Flip to show above.
  358. if (spaceAbove > rectHeight) top -= rectHeight;
  359. // Align to window bottom.
  360. else if (viewportSize.height > rectHeight)
  361. top = top - rect.bottom + viewportSize.height;
  362. // Align to top, never cutting the panel at top.
  363. else top = top - rect.top;
  364. }
  365. // If IE is in RTL, we have troubles with absolute
  366. // position and horizontal scrolls. Here we have a
  367. // series of hacks to workaround it. (https://dev.ckeditor.com/ticket/6146)
  368. if (CKEDITOR.env.ie && !CKEDITOR.env.edge) {
  369. var offsetParent = new CKEDITOR.dom.element(
  370. element.$.offsetParent
  371. ),
  372. scrollParent = offsetParent;
  373. // Quirks returns <body>, but standards returns <html>.
  374. if (scrollParent.getName() == "html") {
  375. scrollParent = scrollParent.getDocument().getBody();
  376. }
  377. if (scrollParent.getComputedStyle("direction") == "rtl") {
  378. // For IE8, there is not much logic on this, but it works.
  379. if (CKEDITOR.env.ie8Compat) {
  380. left -=
  381. element.getDocument().getDocumentElement().$.scrollLeft *
  382. 2;
  383. } else {
  384. left -=
  385. offsetParent.$.scrollWidth - offsetParent.$.clientWidth;
  386. }
  387. }
  388. }
  389. // Trigger the onHide event of the previously active panel to prevent
  390. // incorrect styles from being applied (https://dev.ckeditor.com/ticket/6170)
  391. var innerElement = element.getFirst(),
  392. activePanel;
  393. if ((activePanel = innerElement.getCustomData("activePanel")))
  394. activePanel.onHide && activePanel.onHide.call(this, 1);
  395. innerElement.setCustomData("activePanel", this);
  396. element.setStyles({
  397. top: top + "px",
  398. left: left + "px"
  399. });
  400. element.setOpacity(1);
  401. callback && callback();
  402. }, this);
  403. panel.isLoaded ? panelLoad() : (panel.onLoad = panelLoad);
  404. CKEDITOR.tools.setTimeout(
  405. function() {
  406. var scrollTop =
  407. CKEDITOR.env.webkit &&
  408. CKEDITOR.document.getWindow().getScrollPosition().y;
  409. // Focus the panel frame first, so blur gets fired.
  410. this.focus();
  411. // Focus the block now.
  412. block.element.focus();
  413. // https://dev.ckeditor.com/ticket/10623, https://dev.ckeditor.com/ticket/10951 - restore the viewport's scroll position after focusing list element.
  414. if (CKEDITOR.env.webkit)
  415. CKEDITOR.document.getBody().$.scrollTop = scrollTop;
  416. // We need this get fired manually because of unfired focus() function.
  417. this.allowBlur(true);
  418. // Ensure that the first item is focused (https://dev.ckeditor.com/ticket/16804).
  419. if (this._.markFirst) {
  420. if (CKEDITOR.env.ie) {
  421. CKEDITOR.tools.setTimeout(function() {
  422. block.markFirstDisplayed
  423. ? block.markFirstDisplayed()
  424. : block._.markFirstDisplayed();
  425. }, 0);
  426. } else {
  427. block.markFirstDisplayed
  428. ? block.markFirstDisplayed()
  429. : block._.markFirstDisplayed();
  430. }
  431. }
  432. this._.editor.fire("panelShow", this);
  433. },
  434. 0,
  435. this
  436. );
  437. },
  438. CKEDITOR.env.air ? 200 : 0,
  439. this
  440. );
  441. this.visible = 1;
  442. if (this.onShow) this.onShow.call(this);
  443. },
  444. /**
  445. * Repositions the panel with the same parameters that were used in the last {@link #showBlock} call.
  446. *
  447. * @since 4.5.4
  448. */
  449. reposition: function() {
  450. var blockParams = this._.showBlockParams;
  451. if (this.visible && this._.showBlockParams) {
  452. this.hide();
  453. this.showBlock.apply(this, blockParams);
  454. }
  455. },
  456. /**
  457. * Restores the last focused element or simply focuses the panel window.
  458. */
  459. focus: function() {
  460. // Webkit requires to blur any previous focused page element, in
  461. // order to properly fire the "focus" event.
  462. if (CKEDITOR.env.webkit) {
  463. var active = CKEDITOR.document.getActive();
  464. active && !active.equals(this._.iframe) && active.$.blur();
  465. }
  466. // Restore last focused element or simply focus panel window.
  467. var focus =
  468. this._.lastFocused || this._.iframe.getFrameDocument().getWindow();
  469. focus.focus();
  470. },
  471. /**
  472. * @todo
  473. */
  474. blur: function() {
  475. var doc = this._.iframe.getFrameDocument(),
  476. active = doc.getActive();
  477. active && active.is("a") && (this._.lastFocused = active);
  478. },
  479. /**
  480. * Hides the panel.
  481. *
  482. * @todo
  483. */
  484. hide: function(returnFocus) {
  485. if (this.visible && (!this.onHide || this.onHide.call(this) !== true)) {
  486. this.hideChild();
  487. // Blur previously focused element. (https://dev.ckeditor.com/ticket/6671)
  488. CKEDITOR.env.gecko &&
  489. this._.iframe.getFrameDocument().$.activeElement.blur();
  490. this.element.setStyle("display", "none");
  491. this.visible = 0;
  492. this.element.getFirst().removeCustomData("activePanel");
  493. // Return focus properly. (https://dev.ckeditor.com/ticket/6247)
  494. var focusReturn = returnFocus && this._.returnFocus;
  495. if (focusReturn) {
  496. // Webkit requires focus moved out panel iframe first.
  497. if (CKEDITOR.env.webkit && focusReturn.type)
  498. focusReturn.getWindow().$.focus();
  499. focusReturn.focus();
  500. }
  501. delete this._.lastFocused;
  502. this._.showBlockParams = null;
  503. this._.editor.fire("panelHide", this);
  504. }
  505. },
  506. /**
  507. * @todo
  508. */
  509. allowBlur: function(allow) {
  510. // Prevent editor from hiding the panel. (https://dev.ckeditor.com/ticket/3222)
  511. var panel = this._.panel;
  512. if (allow !== undefined) panel.allowBlur = allow;
  513. return panel.allowBlur;
  514. },
  515. /**
  516. * Shows the specified panel as a child of one block of this one.
  517. *
  518. * @param {CKEDITOR.ui.floatPanel} panel
  519. * @param {String} blockName
  520. * @param {CKEDITOR.dom.element} offsetParent Positioned parent.
  521. * @param {Number} corner
  522. *
  523. * * For LTR (left to right) oriented editor:
  524. * * `1` = top-left
  525. * * `2` = top-right
  526. * * `3` = bottom-right
  527. * * `4` = bottom-left
  528. * * For RTL (right to left):
  529. * * `1` = top-right
  530. * * `2` = top-left
  531. * * `3` = bottom-left
  532. * * `4` = bottom-right
  533. *
  534. * @param {Number} [offsetX=0]
  535. * @param {Number} [offsetY=0]
  536. * @todo
  537. */
  538. showAsChild: function(
  539. panel,
  540. blockName,
  541. offsetParent,
  542. corner,
  543. offsetX,
  544. offsetY
  545. ) {
  546. // Skip reshowing of child which is already visible.
  547. if (
  548. this._.activeChild == panel &&
  549. panel._.panel._.offsetParentId == offsetParent.getId()
  550. )
  551. return;
  552. this.hideChild();
  553. panel.onHide = CKEDITOR.tools.bind(function() {
  554. // Use a timeout, so we give time for this menu to get
  555. // potentially focused.
  556. CKEDITOR.tools.setTimeout(
  557. function() {
  558. if (!this._.focused) this.hide();
  559. },
  560. 0,
  561. this
  562. );
  563. }, this);
  564. this._.activeChild = panel;
  565. this._.focused = false;
  566. panel.showBlock(blockName, offsetParent, corner, offsetX, offsetY);
  567. this.blur();
  568. /* https://dev.ckeditor.com/ticket/3767 IE: Second level menu may not have borders */
  569. if (CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat) {
  570. setTimeout(function() {
  571. panel.element.getChild(0).$.style.cssText += "";
  572. }, 100);
  573. }
  574. },
  575. /**
  576. * @todo
  577. */
  578. hideChild: function(restoreFocus) {
  579. var activeChild = this._.activeChild;
  580. if (activeChild) {
  581. delete activeChild.onHide;
  582. delete this._.activeChild;
  583. activeChild.hide();
  584. // At this point focus should be moved back to parent panel.
  585. restoreFocus && this.focus();
  586. }
  587. }
  588. }
  589. });
  590. CKEDITOR.on("instanceDestroyed", function() {
  591. var isLastInstance = CKEDITOR.tools.isEmpty(CKEDITOR.instances);
  592. for (var i in panels) {
  593. var panel = panels[i];
  594. // Safe to destroy it since there're no more instances.(https://dev.ckeditor.com/ticket/4241)
  595. if (isLastInstance) panel.destroy();
  596. // Panel might be used by other instances, just hide them.(https://dev.ckeditor.com/ticket/4552)
  597. else panel.element.hide();
  598. }
  599. // Remove the registration.
  600. isLastInstance && (panels = {});
  601. });
  602. })();