diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 7d00cb671..5b84edba0 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -38,6 +38,7 @@ import pageDisplay from "./page-display.js" import pageEditor from "./page-editor.js" import pagePicker from "./page-picker.js" import permissionsTable from "./permissions-table.js" +import pointer from "./pointer.js"; import popup from "./popup.js" import settingAppColorPicker from "./setting-app-color-picker.js" import settingColorPicker from "./setting-color-picker.js" @@ -95,6 +96,7 @@ const componentMapping = { "page-editor": pageEditor, "page-picker": pagePicker, "permissions-table": permissionsTable, + "pointer": pointer, "popup": popup, "setting-app-color-picker": settingAppColorPicker, "setting-color-picker": settingColorPicker, diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js index 88254fd3a..b4f1cca4f 100644 --- a/resources/js/components/page-display.js +++ b/resources/js/components/page-display.js @@ -1,4 +1,3 @@ -import Clipboard from "clipboard/dist/clipboard.min"; import * as DOM from "../services/dom"; import {scrollAndHighlightElement} from "../services/util"; @@ -9,7 +8,6 @@ class PageDisplay { this.pageId = elem.getAttribute('page-display'); window.importVersioned('code').then(Code => Code.highlight()); - this.setupPointer(); this.setupNavHighlighting(); this.setupDetailsCodeBlockRefresh(); @@ -50,108 +48,6 @@ class PageDisplay { } } - setupPointer() { - let pointer = document.getElementById('pointer'); - if (!pointer) { - return; - } - - // Set up pointer - pointer = pointer.parentNode.removeChild(pointer); - const pointerInner = pointer.querySelector('div.pointer'); - - // Instance variables - let pointerShowing = false; - let isSelection = false; - let pointerModeLink = true; - let pointerSectionId = ''; - - // Select all contents on input click - DOM.onChildEvent(pointer, 'input', 'click', (event, input) => { - input.select(); - event.stopPropagation(); - }); - - // Prevent closing pointer when clicked or focused - DOM.onEvents(pointer, ['click', 'focus'], event => { - event.stopPropagation(); - }); - - // Pointer mode toggle - DOM.onChildEvent(pointer, 'span.icon', 'click', (event, icon) => { - event.stopPropagation(); - pointerModeLink = !pointerModeLink; - icon.querySelector('[data-icon="include"]').style.display = (!pointerModeLink) ? 'inline' : 'none'; - icon.querySelector('[data-icon="link"]').style.display = (pointerModeLink) ? 'inline' : 'none'; - updatePointerContent(); - }); - - // Set up clipboard - new Clipboard(pointer.querySelector('button')); - - // Hide pointer when clicking away - DOM.onEvents(document.body, ['click', 'focus'], event => { - if (!pointerShowing || isSelection) return; - pointer = pointer.parentElement.removeChild(pointer); - pointerShowing = false; - }); - - let updatePointerContent = (element) => { - let inputText = pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${pointerSectionId}`) : `{{@${this.pageId}#${pointerSectionId}}}`; - if (pointerModeLink && !inputText.startsWith('http')) { - inputText = window.location.protocol + "//" + window.location.host + inputText; - } - - pointer.querySelector('input').value = inputText; - - // Update anchor if present - const editAnchor = pointer.querySelector('#pointer-edit'); - if (editAnchor && element) { - const editHref = editAnchor.dataset.editHref; - const elementId = element.id; - - // get the first 50 characters. - const queryContent = element.textContent && element.textContent.substring(0, 50); - editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`; - } - }; - - // Show pointer when selecting a single block of tagged content - DOM.forEach('.page-content [id^="bkmrk"]', bookMarkElem => { - DOM.onEvents(bookMarkElem, ['mouseup', 'keyup'], event => { - event.stopPropagation(); - let selection = window.getSelection(); - if (selection.toString().length === 0) return; - - // Show pointer and set link - pointerSectionId = bookMarkElem.id; - updatePointerContent(bookMarkElem); - - bookMarkElem.parentNode.insertBefore(pointer, bookMarkElem); - pointer.style.display = 'block'; - pointerShowing = true; - isSelection = true; - - // Set pointer to sit near mouse-up position - requestAnimationFrame(() => { - const bookMarkBounds = bookMarkElem.getBoundingClientRect(); - let pointerLeftOffset = (event.pageX - bookMarkBounds.left - 164); - if (pointerLeftOffset < 0) { - pointerLeftOffset = 0 - } - const pointerLeftOffsetPercent = (pointerLeftOffset / bookMarkBounds.width) * 100; - - pointerInner.style.left = pointerLeftOffsetPercent + '%'; - - setTimeout(() => { - isSelection = false; - }, 100); - }); - - }); - }); - } - setupNavHighlighting() { // Check if support is present for IntersectionObserver if (!('IntersectionObserver' in window) || diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.js new file mode 100644 index 000000000..a74422ce4 --- /dev/null +++ b/resources/js/components/pointer.js @@ -0,0 +1,127 @@ +import * as DOM from "../services/dom"; +import Clipboard from "clipboard/dist/clipboard.min"; + +/** + * @extends Component + */ +class Pointer { + + setup() { + this.container = this.$el; + this.pageId = this.$opts.pageId; + + // Instance variables + this.showing = false; + this.isSelection = false; + this.pointerModeLink = true; + this.pointerSectionId = ''; + + this.init(); + this.setupListeners(); + } + + init() { + // Set up pointer by removing it + this.container.parentNode.removeChild(this.container); + + // Set up clipboard + new Clipboard(this.container.querySelector('button')); + } + + setupListeners() { + // Select all contents on input click + DOM.onChildEvent(this.container, 'input', 'click', (event, input) => { + input.select(); + event.stopPropagation(); + }); + + // Prevent closing pointer when clicked or focused + DOM.onEvents(this.container, ['click', 'focus'], event => { + event.stopPropagation(); + }); + + // Pointer mode toggle + DOM.onChildEvent(this.container, 'span.icon', 'click', (event, icon) => { + event.stopPropagation(); + this.pointerModeLink = !this.pointerModeLink; + icon.querySelector('[data-icon="include"]').style.display = (!this.pointerModeLink) ? 'inline' : 'none'; + icon.querySelector('[data-icon="link"]').style.display = (this.pointerModeLink) ? 'inline' : 'none'; + this.updateForTarget(); + }); + + // Hide pointer when clicking away + DOM.onEvents(document.body, ['click', 'focus'], event => { + if (!this.showing || this.isSelection) return; + this.container.parentElement.removeChild(this.container); + this.showing = false; + }); + + // Show pointer when selecting a single block of tagged content + const pageContent = document.querySelector('.page-content'); + DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => { + event.stopPropagation(); + const targetEl = event.target.closest('[id^="bkmrk"]'); + if (targetEl) { + this.showPointerAtTarget(targetEl, event.pageX); + } + }); + } + + /** + * Move and display the pointer at the given element, targeting the given screen x-position if possible. + * @param {Element} element + * @param {Number} xPosition + */ + showPointerAtTarget(element, xPosition) { + const selection = window.getSelection(); + if (selection.toString().length === 0) return; + + // Show pointer and set link + this.pointerSectionId = element.id; + this.updateForTarget(element); + + element.parentNode.insertBefore(this.container, element); + this.container.style.display = 'block'; + this.showing = true; + this.isSelection = true; + + // Set pointer to sit near mouse-up position + requestAnimationFrame(() => { + const bookMarkBounds = element.getBoundingClientRect(); + const pointerLeftOffset = Math.max((xPosition - bookMarkBounds.left - 164), 0); + const pointerLeftOffsetPercent = (pointerLeftOffset / bookMarkBounds.width) * 100; + + this.container.children[0].style.left = pointerLeftOffsetPercent + '%'; + + setTimeout(() => { + this.isSelection = false; + }, 100); + }); + } + + /** + * Update the pointer inputs/content for the given target element. + * @param {?Element} element + */ + updateForTarget(element) { + let inputText = this.pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${this.pointerSectionId}`) : `{{@${this.pageId}#${this.pointerSectionId}}}`; + if (this.pointerModeLink && !inputText.startsWith('http')) { + inputText = window.location.protocol + "//" + window.location.host + inputText; + } + + this.container.querySelector('input').value = inputText; + + // Update anchor if present + const editAnchor = this.container.querySelector('#pointer-edit'); + if (editAnchor && element) { + const editHref = editAnchor.dataset.editHref; + const elementId = element.id; + + // get the first 50 characters. + const queryContent = element.textContent && element.textContent.substring(0, 50); + editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`; + } + } +} + +export default Pointer; \ No newline at end of file diff --git a/resources/views/pages/parts/pointer.blade.php b/resources/views/pages/parts/pointer.blade.php index b4b28eaba..b8fe62fa4 100644 --- a/resources/views/pages/parts/pointer.blade.php +++ b/resources/views/pages/parts/pointer.blade.php @@ -1,4 +1,7 @@ -
+
@icon('link') @icon('include', ['style' => 'display:none;'])