linkui.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module link/linkui
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
  10. import { isLinkElement } from './utils';
  11. import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
  12. import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
  13. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  14. import LinkFormView from './ui/linkformview';
  15. import LinkActionsView from './ui/linkactionsview';
  16. import linkIcon from '../theme/icons/link.svg';
  17. const linkKeystroke = 'Ctrl+K';
  18. const protocolRegExp = /^((\w+:(\/{2,})?)|(\W))/i;
  19. const emailRegExp = /[\w-]+@[\w-]+\.+[\w-]+/i;
  20. const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
  21. /**
  22. * The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
  23. *
  24. * It uses the
  25. * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
  26. *
  27. * @extends module:core/plugin~Plugin
  28. */
  29. export default class LinkUI extends Plugin {
  30. /**
  31. * @inheritDoc
  32. */
  33. static get requires() {
  34. return [ ContextualBalloon ];
  35. }
  36. /**
  37. * @inheritDoc
  38. */
  39. static get pluginName() {
  40. return 'LinkUI';
  41. }
  42. /**
  43. * @inheritDoc
  44. */
  45. init() {
  46. const editor = this.editor;
  47. editor.editing.view.addObserver( ClickObserver );
  48. /**
  49. * The actions view displayed inside of the balloon.
  50. *
  51. * @member {module:link/ui/linkactionsview~LinkActionsView}
  52. */
  53. this.actionsView = this._createActionsView();
  54. /**
  55. * The form view displayed inside the balloon.
  56. *
  57. * @member {module:link/ui/linkformview~LinkFormView}
  58. */
  59. this.formView = this._createFormView();
  60. /**
  61. * The contextual balloon plugin instance.
  62. *
  63. * @private
  64. * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
  65. */
  66. this._balloon = editor.plugins.get( ContextualBalloon );
  67. // Create toolbar buttons.
  68. this._createToolbarLinkButton();
  69. // Attach lifecycle actions to the the balloon.
  70. this._enableUserBalloonInteractions();
  71. // Renders a fake visual selection marker on an expanded selection.
  72. editor.conversion.for( 'downcast' ).markerToHighlight( {
  73. model: VISUAL_SELECTION_MARKER_NAME,
  74. view: {
  75. classes: [ 'ck-fake-link-selection' ]
  76. }
  77. } );
  78. // Renders a fake visual selection marker on a collapsed selection.
  79. editor.conversion.for( 'downcast' ).markerToElement( {
  80. model: VISUAL_SELECTION_MARKER_NAME,
  81. view: {
  82. name: 'span',
  83. classes: [ 'ck-fake-link-selection', 'ck-fake-link-selection_collapsed' ]
  84. }
  85. } );
  86. }
  87. /**
  88. * @inheritDoc
  89. */
  90. destroy() {
  91. super.destroy();
  92. // Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
  93. this.formView.destroy();
  94. }
  95. /**
  96. * Creates the {@link module:link/ui/linkactionsview~LinkActionsView} instance.
  97. *
  98. * @private
  99. * @returns {module:link/ui/linkactionsview~LinkActionsView} The link actions view instance.
  100. */
  101. _createActionsView() {
  102. const editor = this.editor;
  103. const actionsView = new LinkActionsView( editor.locale );
  104. const linkCommand = editor.commands.get( 'link' );
  105. const unlinkCommand = editor.commands.get( 'unlink' );
  106. actionsView.bind( 'href' ).to( linkCommand, 'value' );
  107. actionsView.editButtonView.bind( 'isEnabled' ).to( linkCommand );
  108. actionsView.unlinkButtonView.bind( 'isEnabled' ).to( unlinkCommand );
  109. // Execute unlink command after clicking on the "Edit" button.
  110. this.listenTo( actionsView, 'edit', () => {
  111. this._addFormView();
  112. } );
  113. // Execute unlink command after clicking on the "Unlink" button.
  114. this.listenTo( actionsView, 'unlink', () => {
  115. editor.execute( 'unlink' );
  116. this._hideUI();
  117. } );
  118. // Close the panel on esc key press when the **actions have focus**.
  119. actionsView.keystrokes.set( 'Esc', ( data, cancel ) => {
  120. this._hideUI();
  121. cancel();
  122. } );
  123. // Open the form view on Ctrl+K when the **actions have focus**..
  124. actionsView.keystrokes.set( linkKeystroke, ( data, cancel ) => {
  125. this._addFormView();
  126. cancel();
  127. } );
  128. return actionsView;
  129. }
  130. /**
  131. * Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
  132. *
  133. * @private
  134. * @returns {module:link/ui/linkformview~LinkFormView} The link form view instance.
  135. */
  136. _createFormView() {
  137. const editor = this.editor;
  138. const linkCommand = editor.commands.get( 'link' );
  139. const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
  140. const formView = new LinkFormView( editor.locale, linkCommand, defaultProtocol );
  141. formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );
  142. // Form elements should be read-only when corresponding commands are disabled.
  143. formView.urlInputView.bind( 'isReadOnly' ).to( linkCommand, 'isEnabled', value => !value );
  144. formView.saveButtonView.bind( 'isEnabled' ).to( linkCommand );
  145. // Execute link command after clicking the "Save" button.
  146. this.listenTo( formView, 'submit', () => {
  147. const { value } = formView.urlInputView.fieldView.element;
  148. // The regex checks for the protocol syntax ('xxxx://' or 'xxxx:')
  149. // or non-word characters at the beginning of the link ('/', '#' etc.).
  150. const isProtocolNeeded = !!defaultProtocol && !protocolRegExp.test( value );
  151. const isEmail = emailRegExp.test( value );
  152. const protocol = isEmail ? 'mailto:' : defaultProtocol;
  153. const parsedValue = value && isProtocolNeeded ? protocol + value : value;
  154. editor.execute( 'link', parsedValue, formView.getDecoratorSwitchesState() );
  155. this._closeFormView();
  156. } );
  157. // Hide the panel after clicking the "Cancel" button.
  158. this.listenTo( formView, 'cancel', () => {
  159. this._closeFormView();
  160. } );
  161. // Close the panel on esc key press when the **form has focus**.
  162. formView.keystrokes.set( 'Esc', ( data, cancel ) => {
  163. this._closeFormView();
  164. cancel();
  165. } );
  166. return formView;
  167. }
  168. /**
  169. * Creates a toolbar Link button. Clicking this button will show
  170. * a {@link #_balloon} attached to the selection.
  171. *
  172. * @private
  173. */
  174. _createToolbarLinkButton() {
  175. const editor = this.editor;
  176. const linkCommand = editor.commands.get( 'link' );
  177. const t = editor.t;
  178. // Handle the `Ctrl+K` keystroke and show the panel.
  179. editor.keystrokes.set( linkKeystroke, ( keyEvtData, cancel ) => {
  180. // Prevent focusing the search bar in FF, Chrome and Edge. See https://github.com/ckeditor/ckeditor5/issues/4811.
  181. cancel();
  182. this._showUI( true );
  183. } );
  184. editor.ui.componentFactory.add( 'link', locale => {
  185. const button = new ButtonView( locale );
  186. button.isEnabled = true;
  187. button.label = t( 'Link' );
  188. button.icon = linkIcon;
  189. button.keystroke = linkKeystroke;
  190. button.tooltip = true;
  191. button.isToggleable = true;
  192. // Bind button to the command.
  193. button.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
  194. button.bind( 'isOn' ).to( linkCommand, 'value', value => !!value );
  195. // Show the panel on button click.
  196. this.listenTo( button, 'execute', () => this._showUI( true ) );
  197. return button;
  198. } );
  199. }
  200. /**
  201. * Attaches actions that control whether the balloon panel containing the
  202. * {@link #formView} is visible or not.
  203. *
  204. * @private
  205. */
  206. _enableUserBalloonInteractions() {
  207. const viewDocument = this.editor.editing.view.document;
  208. // Handle click on view document and show panel when selection is placed inside the link element.
  209. // Keep panel open until selection will be inside the same link element.
  210. this.listenTo( viewDocument, 'click', () => {
  211. const parentLink = this._getSelectedLinkElement();
  212. if ( parentLink ) {
  213. // Then show panel but keep focus inside editor editable.
  214. this._showUI();
  215. }
  216. } );
  217. // Focus the form if the balloon is visible and the Tab key has been pressed.
  218. this.editor.keystrokes.set( 'Tab', ( data, cancel ) => {
  219. if ( this._areActionsVisible && !this.actionsView.focusTracker.isFocused ) {
  220. this.actionsView.focus();
  221. cancel();
  222. }
  223. }, {
  224. // Use the high priority because the link UI navigation is more important
  225. // than other feature's actions, e.g. list indentation.
  226. // https://github.com/ckeditor/ckeditor5-link/issues/146
  227. priority: 'high'
  228. } );
  229. // Close the panel on the Esc key press when the editable has focus and the balloon is visible.
  230. this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
  231. if ( this._isUIVisible ) {
  232. this._hideUI();
  233. cancel();
  234. }
  235. } );
  236. // Close on click outside of balloon panel element.
  237. clickOutsideHandler( {
  238. emitter: this.formView,
  239. activator: () => this._isUIInPanel,
  240. contextElements: [ this._balloon.view.element ],
  241. callback: () => this._hideUI()
  242. } );
  243. }
  244. /**
  245. * Adds the {@link #actionsView} to the {@link #_balloon}.
  246. *
  247. * @protected
  248. */
  249. _addActionsView() {
  250. if ( this._areActionsInPanel ) {
  251. return;
  252. }
  253. this._balloon.add( {
  254. view: this.actionsView,
  255. position: this._getBalloonPositionData()
  256. } );
  257. }
  258. /**
  259. * Adds the {@link #formView} to the {@link #_balloon}.
  260. *
  261. * @protected
  262. */
  263. _addFormView() {
  264. if ( this._isFormInPanel ) {
  265. return;
  266. }
  267. const editor = this.editor;
  268. const linkCommand = editor.commands.get( 'link' );
  269. this._balloon.add( {
  270. view: this.formView,
  271. position: this._getBalloonPositionData()
  272. } );
  273. // Select input when form view is currently visible.
  274. if ( this._balloon.visibleView === this.formView ) {
  275. this.formView.urlInputView.fieldView.select();
  276. }
  277. // Make sure that each time the panel shows up, the URL field remains in sync with the value of
  278. // the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
  279. // unaltered) and re-opened it without changing the value of the link command (e.g. because they
  280. // clicked the same link), they would see the old value instead of the actual value of the command.
  281. // https://github.com/ckeditor/ckeditor5-link/issues/78
  282. // https://github.com/ckeditor/ckeditor5-link/issues/123
  283. this.formView.urlInputView.fieldView.element.value = linkCommand.value || '';
  284. }
  285. /**
  286. * Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
  287. * decided upon the link command value (which has a value if the document selection is in the link).
  288. *
  289. * Additionally, if any {@link module:link/link~LinkConfig#decorators} are defined in the editor configuration, the state of
  290. * switch buttons responsible for manual decorator handling is restored.
  291. *
  292. * @private
  293. */
  294. _closeFormView() {
  295. const linkCommand = this.editor.commands.get( 'link' );
  296. // Restore manual decorator states to represent the current model state. This case is important to reset the switch buttons
  297. // when the user cancels the editing form.
  298. linkCommand.restoreManualDecoratorStates();
  299. if ( linkCommand.value !== undefined ) {
  300. this._removeFormView();
  301. } else {
  302. this._hideUI();
  303. }
  304. }
  305. /**
  306. * Removes the {@link #formView} from the {@link #_balloon}.
  307. *
  308. * @protected
  309. */
  310. _removeFormView() {
  311. if ( this._isFormInPanel ) {
  312. // Blur the input element before removing it from DOM to prevent issues in some browsers.
  313. // See https://github.com/ckeditor/ckeditor5/issues/1501.
  314. this.formView.saveButtonView.focus();
  315. this._balloon.remove( this.formView );
  316. // Because the form has an input which has focus, the focus must be brought back
  317. // to the editor. Otherwise, it would be lost.
  318. this.editor.editing.view.focus();
  319. this._hideFakeVisualSelection();
  320. }
  321. }
  322. /**
  323. * Shows the correct UI type. It is either {@link #formView} or {@link #actionsView}.
  324. *
  325. * @param {Boolean} forceVisible
  326. * @private
  327. */
  328. _showUI( forceVisible = false ) {
  329. // When there's no link under the selection, go straight to the editing UI.
  330. if ( !this._getSelectedLinkElement() ) {
  331. this._addActionsView();
  332. // Be sure panel with link is visible.
  333. if ( forceVisible ) {
  334. this._balloon.showStack( 'main' );
  335. }
  336. this._addFormView();
  337. // Show visual selection on a text without a link when the contextual balloon is displayed.
  338. // See https://github.com/ckeditor/ckeditor5/issues/4721.
  339. this._showFakeVisualSelection();
  340. }
  341. // If there's a link under the selection...
  342. else {
  343. // Go to the editing UI if actions are already visible.
  344. if ( this._areActionsVisible ) {
  345. this._addFormView();
  346. }
  347. // Otherwise display just the actions UI.
  348. else {
  349. this._addActionsView();
  350. }
  351. // Be sure panel with link is visible.
  352. if ( forceVisible ) {
  353. this._balloon.showStack( 'main' );
  354. }
  355. }
  356. // Begin responding to ui#update once the UI is added.
  357. this._startUpdatingUI();
  358. }
  359. /**
  360. * Removes the {@link #formView} from the {@link #_balloon}.
  361. *
  362. * See {@link #_addFormView}, {@link #_addActionsView}.
  363. *
  364. * @protected
  365. */
  366. _hideUI() {
  367. if ( !this._isUIInPanel ) {
  368. return;
  369. }
  370. const editor = this.editor;
  371. this.stopListening( editor.ui, 'update' );
  372. this.stopListening( this._balloon, 'change:visibleView' );
  373. // Make sure the focus always gets back to the editable _before_ removing the focused form view.
  374. // Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
  375. editor.editing.view.focus();
  376. // Remove form first because it's on top of the stack.
  377. this._removeFormView();
  378. // Then remove the actions view because it's beneath the form.
  379. this._balloon.remove( this.actionsView );
  380. this._hideFakeVisualSelection();
  381. }
  382. /**
  383. * Makes the UI react to the {@link module:core/editor/editorui~EditorUI#event:update} event to
  384. * reposition itself when the editor UI should be refreshed.
  385. *
  386. * See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
  387. *
  388. * @protected
  389. */
  390. _startUpdatingUI() {
  391. const editor = this.editor;
  392. const viewDocument = editor.editing.view.document;
  393. let prevSelectedLink = this._getSelectedLinkElement();
  394. let prevSelectionParent = getSelectionParent();
  395. const update = () => {
  396. const selectedLink = this._getSelectedLinkElement();
  397. const selectionParent = getSelectionParent();
  398. // Hide the panel if:
  399. //
  400. // * the selection went out of the EXISTING link element. E.g. user moved the caret out
  401. // of the link,
  402. // * the selection went to a different parent when creating a NEW link. E.g. someone
  403. // else modified the document.
  404. // * the selection has expanded (e.g. displaying link actions then pressing SHIFT+Right arrow).
  405. //
  406. // Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
  407. // when fully selected.
  408. if ( ( prevSelectedLink && !selectedLink ) ||
  409. ( !prevSelectedLink && selectionParent !== prevSelectionParent ) ) {
  410. this._hideUI();
  411. }
  412. // Update the position of the panel when:
  413. // * link panel is in the visible stack
  414. // * the selection remains in the original link element,
  415. // * there was no link element in the first place, i.e. creating a new link
  416. else if ( this._isUIVisible ) {
  417. // If still in a link element, simply update the position of the balloon.
  418. // If there was no link (e.g. inserting one), the balloon must be moved
  419. // to the new position in the editing view (a new native DOM range).
  420. this._balloon.updatePosition( this._getBalloonPositionData() );
  421. }
  422. prevSelectedLink = selectedLink;
  423. prevSelectionParent = selectionParent;
  424. };
  425. function getSelectionParent() {
  426. return viewDocument.selection.focus.getAncestors()
  427. .reverse()
  428. .find( node => node.is( 'element' ) );
  429. }
  430. this.listenTo( editor.ui, 'update', update );
  431. this.listenTo( this._balloon, 'change:visibleView', update );
  432. }
  433. /**
  434. * Returns `true` when {@link #formView} is in the {@link #_balloon}.
  435. *
  436. * @readonly
  437. * @protected
  438. * @type {Boolean}
  439. */
  440. get _isFormInPanel() {
  441. return this._balloon.hasView( this.formView );
  442. }
  443. /**
  444. * Returns `true` when {@link #actionsView} is in the {@link #_balloon}.
  445. *
  446. * @readonly
  447. * @protected
  448. * @type {Boolean}
  449. */
  450. get _areActionsInPanel() {
  451. return this._balloon.hasView( this.actionsView );
  452. }
  453. /**
  454. * Returns `true` when {@link #actionsView} is in the {@link #_balloon} and it is
  455. * currently visible.
  456. *
  457. * @readonly
  458. * @protected
  459. * @type {Boolean}
  460. */
  461. get _areActionsVisible() {
  462. return this._balloon.visibleView === this.actionsView;
  463. }
  464. /**
  465. * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
  466. *
  467. * @readonly
  468. * @protected
  469. * @type {Boolean}
  470. */
  471. get _isUIInPanel() {
  472. return this._isFormInPanel || this._areActionsInPanel;
  473. }
  474. /**
  475. * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
  476. * currently visible.
  477. *
  478. * @readonly
  479. * @protected
  480. * @type {Boolean}
  481. */
  482. get _isUIVisible() {
  483. const visibleView = this._balloon.visibleView;
  484. return visibleView == this.formView || this._areActionsVisible;
  485. }
  486. /**
  487. * Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
  488. * to the target element or selection.
  489. *
  490. * If the selection is collapsed and inside a link element, the panel will be attached to the
  491. * entire link element. Otherwise, it will be attached to the selection.
  492. *
  493. * @private
  494. * @returns {module:utils/dom/position~Options}
  495. */
  496. _getBalloonPositionData() {
  497. const view = this.editor.editing.view;
  498. const viewDocument = view.document;
  499. const targetLink = this._getSelectedLinkElement();
  500. const target = targetLink ?
  501. // When selection is inside link element, then attach panel to this element.
  502. view.domConverter.mapViewToDom( targetLink ) :
  503. // Otherwise attach panel to the selection.
  504. view.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
  505. return { target };
  506. }
  507. /**
  508. * Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
  509. * the {@link module:engine/view/document~Document editing view's} selection or `null`
  510. * if there is none.
  511. *
  512. * **Note**: For a non–collapsed selection, the link element is only returned when **fully**
  513. * selected and the **only** element within the selection boundaries.
  514. *
  515. * @private
  516. * @returns {module:engine/view/attributeelement~AttributeElement|null}
  517. */
  518. _getSelectedLinkElement() {
  519. const view = this.editor.editing.view;
  520. const selection = view.document.selection;
  521. if ( selection.isCollapsed ) {
  522. return findLinkElementAncestor( selection.getFirstPosition() );
  523. } else {
  524. // The range for fully selected link is usually anchored in adjacent text nodes.
  525. // Trim it to get closer to the actual link element.
  526. const range = selection.getFirstRange().getTrimmed();
  527. const startLink = findLinkElementAncestor( range.start );
  528. const endLink = findLinkElementAncestor( range.end );
  529. if ( !startLink || startLink != endLink ) {
  530. return null;
  531. }
  532. // Check if the link element is fully selected.
  533. if ( view.createRangeIn( startLink ).getTrimmed().isEqual( range ) ) {
  534. return startLink;
  535. } else {
  536. return null;
  537. }
  538. }
  539. }
  540. /**
  541. * Displays a fake visual selection when the contextual balloon is displayed.
  542. *
  543. * This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
  544. *
  545. * @private
  546. */
  547. _showFakeVisualSelection() {
  548. const model = this.editor.model;
  549. model.change( writer => {
  550. if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
  551. writer.updateMarker( VISUAL_SELECTION_MARKER_NAME, {
  552. range: model.document.selection.getFirstRange()
  553. } );
  554. } else {
  555. writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
  556. usingOperation: false,
  557. affectsData: false,
  558. range: model.document.selection.getFirstRange()
  559. } );
  560. }
  561. } );
  562. }
  563. /**
  564. * Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
  565. *
  566. * @private
  567. */
  568. _hideFakeVisualSelection() {
  569. const model = this.editor.model;
  570. if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
  571. model.change( writer => {
  572. writer.removeMarker( VISUAL_SELECTION_MARKER_NAME );
  573. } );
  574. }
  575. }
  576. }
  577. // Returns a link element if there's one among the ancestors of the provided `Position`.
  578. //
  579. // @private
  580. // @param {module:engine/view/position~Position} View position to analyze.
  581. // @returns {module:engine/view/attributeelement~AttributeElement|null} Link element at the position or null.
  582. function findLinkElementAncestor( position ) {
  583. return position.getAncestors().find( ancestor => isLinkElement( ancestor ) );
  584. }