UNPKG

medium-editor

Version:
307 lines (247 loc) 12.5 kB
(function () { 'use strict'; var AnchorPreview = MediumEditor.Extension.extend({ name: 'anchor-preview', // Anchor Preview Options /* hideDelay: [number] (previously options.anchorPreviewHideDelay) * time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag. */ hideDelay: 500, /* previewValueSelector: [string] * the default selector to locate where to put the activeAnchor value in the preview */ previewValueSelector: 'a', /* showWhenToolbarIsVisible: [boolean] * determines whether the anchor tag preview shows up when the toolbar is visible */ showWhenToolbarIsVisible: false, /* showOnEmptyLinks: [boolean] * determines whether the anchor tag preview shows up on links with href="" or href="#something" */ showOnEmptyLinks: true, init: function () { this.anchorPreview = this.createPreview(); this.getEditorOption('elementsContainer').appendChild(this.anchorPreview); this.attachToEditables(); }, getInteractionElements: function () { return this.getPreviewElement(); }, // TODO: Remove this function in 6.0.0 getPreviewElement: function () { return this.anchorPreview; }, createPreview: function () { var el = this.document.createElement('div'); el.id = 'medium-editor-anchor-preview-' + this.getEditorId(); el.className = 'medium-editor-anchor-preview'; el.innerHTML = this.getTemplate(); this.on(el, 'click', this.handleClick.bind(this)); return el; }, getTemplate: function () { return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' + ' <a class="medium-editor-toolbar-anchor-preview-inner"></a>' + '</div>'; }, destroy: function () { if (this.anchorPreview) { if (this.anchorPreview.parentNode) { this.anchorPreview.parentNode.removeChild(this.anchorPreview); } delete this.anchorPreview; } }, hidePreview: function () { if (this.anchorPreview) { this.anchorPreview.classList.remove('medium-editor-anchor-preview-active'); } this.activeAnchor = null; }, showPreview: function (anchorEl) { if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') || anchorEl.getAttribute('data-disable-preview')) { return true; } if (this.previewValueSelector) { this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value; this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value; } this.anchorPreview.classList.add('medium-toolbar-arrow-over'); this.anchorPreview.classList.remove('medium-toolbar-arrow-under'); if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) { this.anchorPreview.classList.add('medium-editor-anchor-preview-active'); } this.activeAnchor = anchorEl; this.positionPreview(); this.attachPreviewHandlers(); return this; }, positionPreview: function (activeAnchor) { activeAnchor = activeAnchor || this.activeAnchor; var containerWidth = this.window.innerWidth, buttonHeight = this.anchorPreview.offsetHeight, boundary = activeAnchor.getBoundingClientRect(), diffLeft = this.diffLeft, diffTop = this.diffTop, elementsContainer = this.getEditorOption('elementsContainer'), elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1, relativeBoundary = {}, halfOffsetWidth, defaultLeft, middleBoundary, elementsContainerBoundary, top; halfOffsetWidth = this.anchorPreview.offsetWidth / 2; var toolbarExtension = this.base.getExtensionByName('toolbar'); if (toolbarExtension) { diffLeft = toolbarExtension.diffLeft; diffTop = toolbarExtension.diffTop; } defaultLeft = diffLeft - halfOffsetWidth; // If container element is absolute / fixed, recalculate boundaries to be relative to the container if (elementsContainerAbsolute) { elementsContainerBoundary = elementsContainer.getBoundingClientRect(); ['top', 'left'].forEach(function (key) { relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key]; }); relativeBoundary.width = boundary.width; relativeBoundary.height = boundary.height; boundary = relativeBoundary; containerWidth = elementsContainerBoundary.width; // Adjust top position according to container scroll position top = elementsContainer.scrollTop; } else { // Adjust top position according to window scroll position top = this.window.pageYOffset; } middleBoundary = boundary.left + boundary.width / 2; top += buttonHeight + boundary.top + boundary.height - diffTop - this.anchorPreview.offsetHeight; this.anchorPreview.style.top = Math.round(top) + 'px'; this.anchorPreview.style.right = 'initial'; if (middleBoundary < halfOffsetWidth) { this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px'; this.anchorPreview.style.right = 'initial'; } else if ((containerWidth - middleBoundary) < halfOffsetWidth) { this.anchorPreview.style.left = 'auto'; this.anchorPreview.style.right = 0; } else { this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px'; this.anchorPreview.style.right = 'initial'; } }, attachToEditables: function () { this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this)); this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this)); }, handlePositionedToolbar: function () { // If the toolbar is visible and positioned, we don't need to hide the preview // when showWhenToolbarIsVisible is true if (!this.showWhenToolbarIsVisible) { this.hidePreview(); } }, handleClick: function (event) { var anchorExtension = this.base.getExtensionByName('anchor'), activeAnchor = this.activeAnchor; if (anchorExtension && activeAnchor) { event.preventDefault(); this.base.selectElement(this.activeAnchor); // Using setTimeout + delay because: // We may actually be displaying the anchor form, which should be controlled by delay this.base.delay(function () { if (activeAnchor) { var opts = { value: activeAnchor.attributes.href.value, target: activeAnchor.getAttribute('target'), buttonClass: activeAnchor.getAttribute('class') }; anchorExtension.showForm(opts); activeAnchor = null; } }.bind(this)); } this.hidePreview(); }, handleAnchorMouseout: function () { this.anchorToPreview = null; this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout); this.instanceHandleAnchorMouseout = null; }, handleEditableMouseover: function (event) { var target = MediumEditor.util.getClosestTag(event.target, 'a'); if (false === target) { return; } // Detect empty href attributes // The browser will make href="" or href="#top" // into absolute urls when accessed as event.target.href, so check the html if (!this.showOnEmptyLinks && (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML))) { return true; } // only show when toolbar is not present var toolbar = this.base.getExtensionByName('toolbar'); if (!this.showWhenToolbarIsVisible && toolbar && toolbar.isDisplayed && toolbar.isDisplayed()) { return true; } // detach handler for other anchor in case we hovered multiple anchors quickly if (this.activeAnchor && this.activeAnchor !== target) { this.detachPreviewHandlers(); } this.anchorToPreview = target; this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this); this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout); // Using setTimeout + delay because: // - We're going to show the anchor preview according to the configured delay // if the mouse has not left the anchor tag in that time this.base.delay(function () { if (this.anchorToPreview) { this.showPreview(this.anchorToPreview); } }.bind(this)); }, handlePreviewMouseover: function () { this.lastOver = (new Date()).getTime(); this.hovering = true; }, handlePreviewMouseout: function (event) { if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) { this.hovering = false; } }, updatePreview: function () { if (this.hovering) { return true; } var durr = (new Date()).getTime() - this.lastOver; if (durr > this.hideDelay) { // hide the preview 1/2 second after mouse leaves the link this.detachPreviewHandlers(); } }, detachPreviewHandlers: function () { // cleanup clearInterval(this.intervalTimer); if (this.instanceHandlePreviewMouseover) { this.off(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover); this.off(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout); if (this.activeAnchor) { this.off(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover); this.off(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout); } } this.hidePreview(); this.hovering = this.instanceHandlePreviewMouseover = this.instanceHandlePreviewMouseout = null; }, // TODO: break up method and extract out handlers attachPreviewHandlers: function () { this.lastOver = (new Date()).getTime(); this.hovering = true; this.instanceHandlePreviewMouseover = this.handlePreviewMouseover.bind(this); this.instanceHandlePreviewMouseout = this.handlePreviewMouseout.bind(this); this.intervalTimer = setInterval(this.updatePreview.bind(this), 200); this.on(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover); this.on(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout); this.on(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover); this.on(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout); } }); MediumEditor.extensions.anchorPreview = AnchorPreview; }());