setup() {
this.container = this.$el;
- this.input = this.$refs.input;
- this.button = this.$refs.button;
+ this.pointer = this.$refs.pointer;
+ this.linkInput = this.$refs.linkInput;
+ this.linkButton = this.$refs.linkButton;
+ this.includeInput = this.$refs.includeInput;
+ this.includeButton = this.$refs.includeButton;
+ this.sectionModeButton = this.$refs.sectionModeButton;
+ this.modeToggles = this.$manyRefs.modeToggle;
+ this.modeSections = this.$manyRefs.modeSection;
this.pageId = this.$opts.pageId;
// Instance variables
this.showing = false;
this.isSelection = false;
- this.pointerModeLink = true;
- this.pointerSectionId = '';
this.setupListeners();
}
setupListeners() {
// Copy on copy button click
- this.button.addEventListener('click', () => {
- copyTextToClipboard(this.input.value);
- });
+ this.includeButton.addEventListener('click', () => copyTextToClipboard(this.includeInput.value));
+ this.linkButton.addEventListener('click', () => copyTextToClipboard(this.linkInput.value));
// Select all contents on input click
- this.input.addEventListener('click', event => {
- this.input.select();
+ DOM.onSelect([this.includeInput, this.linkInput], event => {
+ event.target.select();
event.stopPropagation();
});
// Prevent closing pointer when clicked or focused
- DOM.onEvents(this.container, ['click', 'focus'], event => {
+ DOM.onEvents(this.pointer, ['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'], () => {
if (!this.showing || this.isSelection) return;
this.hidePointer();
});
+ // Hide pointer on escape press
+ DOM.onEscapePress(this.pointer, this.hidePointer.bind(this));
+
// 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);
+ if (targetEl && window.getSelection().toString().length > 0) {
+ this.showPointerAtTarget(targetEl, event.pageX, false);
}
});
+
+ // Start section selection mode on button press
+ DOM.onSelect(this.sectionModeButton, this.enterSectionSelectMode.bind(this));
+
+ // Toggle between pointer modes
+ DOM.onSelect(this.modeToggles, event => {
+ for (const section of this.modeSections) {
+ const show = !section.contains(event.target);
+ section.toggleAttribute('hidden', !show);
+ }
+
+ this.modeToggles.find(b => b !== event.target).focus();
+ });
}
hidePointer() {
- this.container.style.display = null;
+ this.pointer.style.display = null;
this.showing = false;
}
* Move and display the pointer at the given element, targeting the given screen x-position if possible.
* @param {Element} element
* @param {Number} xPosition
+ * @param {Boolean} keyboardMode
*/
- showPointerAtTarget(element, xPosition) {
- const selection = window.getSelection();
- if (selection.toString().length === 0) return;
-
- // Show pointer and set link
- this.pointerSectionId = element.id;
+ showPointerAtTarget(element, xPosition, keyboardMode) {
this.updateForTarget(element);
- this.container.style.display = 'block';
+ this.pointer.style.display = 'block';
const targetBounds = element.getBoundingClientRect();
- const pointerBounds = this.container.getBoundingClientRect();
+ const pointerBounds = this.pointer.getBoundingClientRect();
const xTarget = Math.min(Math.max(xPosition, targetBounds.left), targetBounds.right);
const xOffset = xTarget - (pointerBounds.width / 2);
const yOffset = (targetBounds.top - pointerBounds.height) - 16;
- this.container.style.left = `${xOffset}px`;
- this.container.style.top = `${yOffset}px`;
+ this.pointer.style.left = `${xOffset}px`;
+ this.pointer.style.top = `${yOffset}px`;
this.showing = true;
this.isSelection = true;
this.hidePointer();
window.removeEventListener('scroll', scrollListener, {passive: true});
};
- window.addEventListener('scroll', scrollListener, {passive: true});
+
+ element.parentElement.insertBefore(this.pointer, element);
+ if (!keyboardMode) {
+ window.addEventListener('scroll', scrollListener, {passive: true});
+ }
}
/**
* @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}`;
- }
+ const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`);
+ const includeTag = `{{@${this.pageId}#${element.id}}}`;
- this.input.value = inputText;
+ this.linkInput.value = permaLink;
+ this.includeInput.value = includeTag;
// Update anchor if present
- const editAnchor = this.container.querySelector('#pointer-edit');
+ const editAnchor = this.pointer.querySelector('#pointer-edit');
if (editAnchor && element) {
const {editHref} = editAnchor.dataset;
const elementId = element.id;
- // get the first 50 characters.
+ // Get the first 50 characters.
const queryContent = element.textContent && element.textContent.substring(0, 50);
editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
}
}
+ enterSectionSelectMode() {
+ const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]'));
+ for (const section of sections) {
+ section.setAttribute('tabindex', '0');
+ }
+
+ sections[0].focus();
+
+ DOM.onEnterPress(sections, event => {
+ this.showPointerAtTarget(event.target, 0, true);
+ this.pointer.focus();
+ });
+ }
+
}
}
/**
- * Listen to enter press on the given element(s).
+ * Listen to key press on the given element(s).
+ * @param {String} key
* @param {HTMLElement|Array} elements
* @param {function} callback
*/
-export function onEnterPress(elements, callback) {
+function onKeyPress(key, elements, callback) {
if (!Array.isArray(elements)) {
elements = [elements];
}
const listener = event => {
- if (event.key === 'Enter') {
+ if (event.key === key) {
callback(event);
}
};
- elements.forEach(e => e.addEventListener('keypress', listener));
+ elements.forEach(e => e.addEventListener('keydown', listener));
+}
+
+/**
+ * Listen to enter press on the given element(s).
+ * @param {HTMLElement|Array} elements
+ * @param {function} callback
+ */
+export function onEnterPress(elements, callback) {
+ onKeyPress('Enter', elements, callback);
+}
+
+/**
+ * Listen to escape press on the given element(s).
+ * @param {HTMLElement|Array} elements
+ * @param {function} callback
+ */
+export function onEscapePress(elements, callback) {
+ onKeyPress('Escape', elements, callback);
}
/**
+
<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">
- <input refs="pointer@input" readonly="readonly" type="text" id="pointer-url" placeholder="url">
- <button refs="pointer@button" class="button outline icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
+ option:pointer:page-id="{{ $page->id }}">
+ <div id="pointer"
+ refs="pointer@pointer"
+ tabindex="-1"
+ aria-label="{{ trans('entities.pages_pointer_label') }}"
+ class="pointer-container">
+ <div class="pointer flex-container-row items-center justify-space-between p-s anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
+ <div refs="pointer@mode-section" class="flex-container-row items-center gap-s">
+ <button refs="pointer@mode-toggle"
+ title="{{ trans('entities.pages_pointer_toggle_link') }}"
+ class="text-button icon px-xs">@icon('link')</button>
+ <div class="input-group">
+ <input refs="pointer@link-input" aria-label="{{ trans('entities.pages_pointer_permalink') }}" readonly="readonly" type="text" id="pointer-url" placeholder="url">
+ <button refs="pointer@link-button" class="button outline icon" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
+ </div>
+ </div>
+ <div refs="pointer@mode-section" hidden class="flex-container-row items-center gap-s">
+ <button refs="pointer@mode-toggle"
+ title="{{ trans('entities.pages_pointer_toggle_include') }}"
+ class="text-button icon px-xs">@icon('include')</button>
+ <div class="input-group">
+ <input refs="pointer@include-input" aria-label="{{ trans('entities.pages_pointer_include_tag') }}" readonly="readonly" type="text" id="pointer-include" placeholder="include">
+ <button refs="pointer@include-button" class="button outline icon" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
+ </div>
+ </div>
+ @if(userCan('page-update', $page))
+ <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
+ class="button primary outline icon heading-edit-icon ml-s px-s" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
+ @endif
</div>
- @if(userCan('page-update', $page))
- <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
- class="button primary outline icon heading-edit-icon ml-s px-s" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
- @endif
</div>
-</div>
\ No newline at end of file
+
+ <button refs="pointer@section-mode-button" class="screen-reader-only">{{ trans('entities.pages_pointer_enter_mode') }}</button>
+</div>