]> BookStack Code Mirror - bookstack/commitdiff
Extracted page pointer to its own compontent
authorDan Brown <redacted>
Tue, 18 Oct 2022 21:02:34 +0000 (22:02 +0100)
committerDan Brown <redacted>
Tue, 18 Oct 2022 21:02:34 +0000 (22:02 +0100)
resources/js/components/index.js
resources/js/components/page-display.js
resources/js/components/pointer.js [new file with mode: 0644]
resources/views/pages/parts/pointer.blade.php

index 7d00cb671130841ae9477f62d0af9f4a9df8d047..5b84edba035308aea25638927f35594dbbe5a5d3 100644 (file)
@@ -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,
index 88254fd3a4efa97719bd1d3028afda2971ea601a..b4f1cca4fbc5682fc124d0342ee3189c198a0c3a 100644 (file)
@@ -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 (file)
index 0000000..a74422c
--- /dev/null
@@ -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
index b4b28eaba1b1fcef7a2e56db7c37510dad40e756..b8fe62fa45cee2317d9d759b89e693d8e41aeda7 100644 (file)
@@ -1,4 +1,7 @@
-<div class="pointer-container" id="pointer">
+<div component="pointer"
+     option:pointer:page-id="{{ $page->id }}"
+     id="pointer"
+     class="pointer-container">
     <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
         <span class="icon mr-xxs">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
         <div class="input-group inline block">