UNPKG

ckeditor5-image-upload-base64

Version:

The development environment of CKEditor 5 – the best browser-based rich text editor.

303 lines (263 loc) 10.7 kB
/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module utils/dom/scroll */ import isRange from './isrange'; import Rect from './rect'; import isText from './istext'; const utils = {}; /** * Makes any page `HTMLElement` or `Range` (`target`) visible inside the browser viewport. * This helper will scroll all `target` ancestors and the web browser viewport to reveal the target to * the user. If the `target` is already visible, nothing will happen. * * @param {HTMLElement|Range} options.target A target, which supposed to become visible to the user. * @param {Number} [options.viewportOffset] An offset from the edge of the viewport (in pixels) * the `target` will be moved by when the viewport is scrolled. It enhances the user experience * by keeping the `target` some distance from the edge of the viewport and thus making it easier to * read or edit by the user. */ export function scrollViewportToShowTarget( { target, viewportOffset = 0 } ) { const targetWindow = getWindow( target ); let currentWindow = targetWindow; let currentFrame = null; // Iterate over all windows, starting from target's parent window up to window#top. while ( currentWindow ) { let firstAncestorToScroll; // Let's scroll target's ancestors first to reveal it. Then, once the ancestor scrolls // settled down, the algorithm can eventually scroll the viewport of the current window. // // Note: If the current window is target's **original** window (e.g. the first one), // start scrolling the closest parent of the target. If not, scroll the closest parent // of an iframe that resides in the current window. if ( currentWindow == targetWindow ) { firstAncestorToScroll = getParentElement( target ); } else { firstAncestorToScroll = getParentElement( currentFrame ); } // Scroll the target's ancestors first. Once done, scrolling the viewport is easy. scrollAncestorsToShowRect( firstAncestorToScroll, () => { // Note: If the target does not belong to the current window **directly**, // i.e. it resides in an iframe belonging to the window, obtain the target's rect // in the coordinates of the current window. By default, a Rect returns geometry // relative to the current window's viewport. To make it work in a parent window, // it must be shifted. return getRectRelativeToWindow( target, currentWindow ); } ); // Obtain the rect of the target after it has been scrolled within its ancestors. // It's time to scroll the viewport. const targetRect = getRectRelativeToWindow( target, currentWindow ); scrollWindowToShowRect( currentWindow, targetRect, viewportOffset ); if ( currentWindow.parent != currentWindow ) { // Keep the reference to the <iframe> element the "previous current window" was // rendered within. It will be useful to re–calculate the rect of the target // in the parent window's relative geometry. The target's rect must be shifted // by it's iframe's position. currentFrame = currentWindow.frameElement; currentWindow = currentWindow.parent; // If the current window has some parent but frameElement is inaccessible, then they have // different domains/ports and, due to security reasons, accessing and scrolling // the parent window won't be possible. // See https://github.com/ckeditor/ckeditor5/issues/930. if ( !currentFrame ) { return; } } else { currentWindow = null; } } } /** * Makes any page `HTMLElement` or `Range` (target) visible within its scrollable ancestors, * e.g. if they have `overflow: scroll` CSS style. * * @param {HTMLElement|Range} target A target, which supposed to become visible to the user. */ export function scrollAncestorsToShowTarget( target ) { const targetParent = getParentElement( target ); scrollAncestorsToShowRect( targetParent, () => { return new Rect( target ); } ); } // TODO: Using a property value shorthand in the top of the file // causes JSDoc to throw errors. See https://github.com/cksource/docs-builder/issues/75. Object.assign( utils, { scrollViewportToShowTarget, scrollAncestorsToShowTarget } ); // Makes a given rect visible within its parent window. // // Note: Avoid the situation where the caret is still in the viewport, but totally // at the edge of it. In such situation, if it moved beyond the viewport in the next // action e.g. after paste, the scrolling would move it to the viewportOffset level // and it all would look like the caret visually moved up/down: // // 1. // | foo[] // | <--- N px of space below the caret // +---------------------------------... // // 2. *paste* // 3. // | // | // +-foo-----------------------------... // bar[] <--- caret below viewport, scrolling... // // 4. *scrolling* // 5. // | // | foo // | bar[] <--- caret precisely at the edge // +---------------------------------... // // To prevent this, this method checks the rects moved by the viewportOffset to cover // the upper/lower edge of the viewport. It makes sure if the action repeats, there's // no twitching – it's a purely visual improvement: // // 5. (after fix) // | // | foo // | bar[] // | <--- N px of space below the caret // +---------------------------------... // // @private // @param {Window} window A window which is scrolled to reveal the rect. // @param {module:utils/dom/rect~Rect} rect A rect which is to be revealed. // @param {Number} viewportOffset See scrollViewportToShowTarget. function scrollWindowToShowRect( window, rect, viewportOffset ) { const targetShiftedDownRect = rect.clone().moveBy( 0, viewportOffset ); const targetShiftedUpRect = rect.clone().moveBy( 0, -viewportOffset ); const viewportRect = new Rect( window ).excludeScrollbarsAndBorders(); const rects = [ targetShiftedUpRect, targetShiftedDownRect ]; if ( !rects.every( rect => viewportRect.contains( rect ) ) ) { let { scrollX, scrollY } = window; if ( isAbove( targetShiftedUpRect, viewportRect ) ) { scrollY -= viewportRect.top - rect.top + viewportOffset; } else if ( isBelow( targetShiftedDownRect, viewportRect ) ) { scrollY += rect.bottom - viewportRect.bottom + viewportOffset; } // TODO: Web browsers scroll natively to place the target in the middle // of the viewport. It's not a very popular case, though. if ( isLeftOf( rect, viewportRect ) ) { scrollX -= viewportRect.left - rect.left + viewportOffset; } else if ( isRightOf( rect, viewportRect ) ) { scrollX += rect.right - viewportRect.right + viewportOffset; } window.scrollTo( scrollX, scrollY ); } } // Recursively scrolls element ancestors to visually reveal a rect. // // @private // @param {HTMLElement} A parent The first ancestors to start scrolling. // @param {Function} getRect A function which returns the Rect, which is to be revealed. function scrollAncestorsToShowRect( parent, getRect ) { const parentWindow = getWindow( parent ); let parentRect, targetRect; while ( parent != parentWindow.document.body ) { targetRect = getRect(); parentRect = new Rect( parent ).excludeScrollbarsAndBorders(); if ( !parentRect.contains( targetRect ) ) { if ( isAbove( targetRect, parentRect ) ) { parent.scrollTop -= parentRect.top - targetRect.top; } else if ( isBelow( targetRect, parentRect ) ) { parent.scrollTop += targetRect.bottom - parentRect.bottom; } if ( isLeftOf( targetRect, parentRect ) ) { parent.scrollLeft -= parentRect.left - targetRect.left; } else if ( isRightOf( targetRect, parentRect ) ) { parent.scrollLeft += targetRect.right - parentRect.right; } } parent = parent.parentNode; } } // Determines if a given `Rect` extends beyond the bottom edge of the second `Rect`. // // @private // @param {module:utils/dom/rect~Rect} firstRect // @param {module:utils/dom/rect~Rect} secondRect function isBelow( firstRect, secondRect ) { return firstRect.bottom > secondRect.bottom; } // Determines if a given `Rect` extends beyond the top edge of the second `Rect`. // // @private // @param {module:utils/dom/rect~Rect} firstRect // @param {module:utils/dom/rect~Rect} secondRect function isAbove( firstRect, secondRect ) { return firstRect.top < secondRect.top; } // Determines if a given `Rect` extends beyond the left edge of the second `Rect`. // // @private // @param {module:utils/dom/rect~Rect} firstRect // @param {module:utils/dom/rect~Rect} secondRect function isLeftOf( firstRect, secondRect ) { return firstRect.left < secondRect.left; } // Determines if a given `Rect` extends beyond the right edge of the second `Rect`. // // @private // @param {module:utils/dom/rect~Rect} firstRect // @param {module:utils/dom/rect~Rect} secondRect function isRightOf( firstRect, secondRect ) { return firstRect.right > secondRect.right; } // Returns the closest window of an element or range. // // @private // @param {HTMLElement|Range} firstRect // @returns {Window} function getWindow( elementOrRange ) { if ( isRange( elementOrRange ) ) { return elementOrRange.startContainer.ownerDocument.defaultView; } else { return elementOrRange.ownerDocument.defaultView; } } // Returns the closest parent of an element or DOM range. // // @private // @param {HTMLElement|Range} firstRect // @returns {HTMLelement} function getParentElement( elementOrRange ) { if ( isRange( elementOrRange ) ) { let parent = elementOrRange.commonAncestorContainer; // If a Range is attached to the Text, use the closest element ancestor. if ( isText( parent ) ) { parent = parent.parentNode; } return parent; } else { return elementOrRange.parentNode; } } // Returns the rect of an element or range residing in an iframe. // The result rect is relative to the geometry of the passed window instance. // // @private // @param {HTMLElement|Range} target Element or range which rect should be returned. // @param {Window} relativeWindow A window the rect should be relative to. // @returns {module:utils/dom/rect~Rect} function getRectRelativeToWindow( target, relativeWindow ) { const targetWindow = getWindow( target ); const rect = new Rect( target ); if ( targetWindow === relativeWindow ) { return rect; } else { let currentWindow = targetWindow; while ( currentWindow != relativeWindow ) { const frame = currentWindow.frameElement; const frameRect = new Rect( frame ).excludeScrollbarsAndBorders(); rect.moveBy( frameRect.left, frameRect.top ); currentWindow = currentWindow.parent; } } return rect; }