Skip to content

Commit

Permalink
Footer: Copy permalink to clipboard (#7498)
Browse files Browse the repository at this point in the history
* Footer: Copy permalink to clipboard

* Fix cut off permanent tooltip
  • Loading branch information
lscharmer authored Dec 5, 2024
1 parent cd42dc6 commit 28de4d2
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 12 deletions.
2 changes: 2 additions & 0 deletions lang/ilias_de.lang
Original file line number Diff line number Diff line change
Expand Up @@ -3772,6 +3772,7 @@ common#:#copy_all#:#Alle kopieren
common#:#copy_n_of_suffix#:#- Kopie (%1$s)
common#:#copy_of#:#Kopie von
common#:#copy_of_suffix#:#- Kopie
common#:#copy_perma_link#:#Link in Zwischenablage kopieren
common#:#copy_selected_items#:#Kopieren
common#:#count#:#Anzahl
common#:#counter_novelty#:#Neuigkeiten
Expand Down Expand Up @@ -5254,6 +5255,7 @@ common#:#pd_items_news#:#Einschließlich Neuigkeiten der ausgewählten Objekte
common#:#pdf_export#:#PDF-Export
common#:#perm_settings#:#Rechte
common#:#perma_link#:#Link zu dieser Seite
common#:#perma_link_copied#:#Link zu dieser Seite wurde in die Zwischenablage kopiert.
common#:#permission#:#Recht
common#:#permission_denied#:#Kein Zugriffsrecht
common#:#permission_settings#:#Rechteeinstellungen
Expand Down
2 changes: 2 additions & 0 deletions lang/ilias_en.lang
Original file line number Diff line number Diff line change
Expand Up @@ -3772,6 +3772,7 @@ common#:#copy_all#:#Copy all
common#:#copy_n_of_suffix#:#- Copy (%1$s)
common#:#copy_of#:#Copy of
common#:#copy_of_suffix#:#- Copy
common#:#copy_perma_link#:#Copy link to clipboard
common#:#copy_selected_items#:#Copy
common#:#count#:#Count
common#:#counter_novelty#:#News
Expand Down Expand Up @@ -5254,6 +5255,7 @@ common#:#pd_items_news#:#Include News of Personal Items
common#:#pdf_export#:#PDF Export
common#:#perm_settings#:#Permissions
common#:#perma_link#:#Permanent Link
common#:#perma_link_copied#:#Link to this page has been copied to the clipboard.
common#:#permission#:#Permission
common#:#permission_denied#:#Permission Denied
common#:#permission_settings#:#Object Permission Settings
Expand Down
21 changes: 16 additions & 5 deletions src/UI/Implementation/Component/MainControls/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,16 @@ protected function renderFooter(Footer $component, RendererInterface $default_re

$perm_url = $component->getPermanentURL();
if ($perm_url instanceof URI) {
$url = $perm_url->__toString();
$link = $this->getUIFactory()
->link()
->standard($this->txt('perma_link'), $url);
$tpl->setVariable('PERMANENT', $default_renderer->render($link));
$code = function (string $id) use ($perm_url): string {
$id = $this->jsonEncode($id);
$perm_url = $this->jsonEncode((string) $perm_url);

return "document.getElementById($id).addEventListener('click', e => il.Footer.permalink.copyText($perm_url)
.then(() => il.Footer.permalink.showTooltip(e.target.nextElementSibling, 5000)));";
};
$button = $this->getUIFactory()->button()->shy($this->txt('copy_perma_link'), '')->withAdditionalOnLoadCode($code);
$tpl->setVariable('PERMANENT', $default_renderer->render($button));
$tpl->setVariable('PERMANENT_TOOLTIP', $this->txt('perma_link_copied'));
}
return $tpl->get();
}
Expand All @@ -460,6 +465,7 @@ public function registerResources(ResourceRegistry $registry): void
$registry->register('./src/UI/templates/js/MainControls/dist/maincontrols.min.js');
$registry->register('./src/GlobalScreen/Client/dist/GS.js');
$registry->register('./src/UI/templates/js/MainControls/system_info.js');
$registry->register('./src/UI/templates/js/MainControls/dist/footer.min.js');
}

/**
Expand All @@ -475,4 +481,9 @@ protected function getComponentInterfaceName(): array
Component\MainControls\SystemInfo::class
);
}

private function jsonEncode($value): string
{
return json_encode($value, JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_THROW_ON_ERROR);
}
}
5 changes: 4 additions & 1 deletion src/UI/templates/default/MainControls/tpl.footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
<div class="il-footer-content">

<!-- BEGIN footer_perm_url -->
<div class="il-footer-permanent-url">
<div class="il-footer-permanent-url c-tooltip__container c-tooltip--top" aria-live="polite">
{PERMANENT}
<div class="c-tooltip c-tooltip--hidden" role="tooltip">
{PERMANENT_TOOLTIP}
</div>
</div>
<!-- END footer_perm_url -->

Expand Down
15 changes: 15 additions & 0 deletions src/UI/templates/js/MainControls/dist/footer.min.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*/
!function(e){"use strict";function t(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var o=t(e);var n=Object.freeze({__proto__:null,copyText:e=>{if(window.navigator.clipboard)return window.navigator.clipboard.writeText(e);const t=document.createElement("span"),o=document.createRange(),n=window.getSelection();t.textContent=e,document.body.appendChild(t),o.selectNodeContents(t),n.addRange(o);const r=document.execCommand("copy");return n.removeAllRanges(),t.remove(),r?Promise.resolve():Promise.reject(new Error("Unable to copy text."))},showTooltip:(e,t)=>{const o=(Array.from(document.getElementsByTagName("main")).find((e=>!e.hidden))||document.body).getBoundingClientRect();e.parentNode.classList.add("c-tooltip--visible");const n=e.getBoundingClientRect();o.left>n.left?e.style.transform="translateX(calc("+(o.left-n.left)+"px - 50%))":o.right<n.right&&(e.style.transform="translateX(calc("+(o.right-n.right)+"px - 50%))"),setTimeout((()=>{e.parentNode.classList.remove("c-tooltip--visible")}),t)}});o.default.Footer={permalink:n}}(il);
19 changes: 19 additions & 0 deletions src/UI/templates/js/MainControls/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,23 @@ export default [
external: ['il', 'jquery'],
},

{
input: './src/footer.js',
output: {
file: './dist/footer.min.js',
format: 'iife',
banner: copyright,
plugins: [
terser({
format: {
comments: preserveCopyright,
},
}),
],
globals: {
il: 'il',
},
},
external: ['il'],
},
];
19 changes: 19 additions & 0 deletions src/UI/templates/js/MainControls/src/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*/

import il from 'il';
import * as permalink from './footer/permalink';

il.Footer = { permalink };
55 changes: 55 additions & 0 deletions src/UI/templates/js/MainControls/src/footer/permalink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*/

/**
* @param {string} text
* @returns {Promise}
*/
export const copyText = text => {
if (window.navigator.clipboard) {
return window.navigator.clipboard.writeText(text);
}

const node = document.createElement('span');
const range = document.createRange();
const selection = window.getSelection();

node.textContent = text;
document.body.appendChild(node);
range.selectNodeContents(node);
selection.addRange(range);

const success = document.execCommand('copy');
selection.removeAllRanges();
node.remove();

return success ? Promise.resolve() : Promise.reject(new Error('Unable to copy text.'));
};

export const showTooltip = (node, delay) => {
const main = (Array.from(document.getElementsByTagName('main')).find(n => !n.hidden) || document.body).getBoundingClientRect();
node.parentNode.classList.add('c-tooltip--visible');
const r = node.getBoundingClientRect();

if (main.left > r.left) {
node.style.transform = 'translateX(calc(' + (main.left - r.left) + 'px - 50%))';
} else if (main.right < r.right) {
node.style.transform = 'translateX(calc(' + (main.right - r.right) + 'px - 50%))';
}

setTimeout(() => {
node.parentNode.classList.remove('c-tooltip--visible');
}, delay);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ footer{
font-size: $il-font-size-small;
padding: $il-padding-small-vertical $il-padding-large-horizontal;

.il-footer-content div {
display: inline-block;
.il-footer-content div:not(.c-tooltip) {
display: inline-block;
}

.il-footer-links {
Expand Down
2 changes: 1 addition & 1 deletion templates/default/delos.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 160 additions & 0 deletions tests/UI/Client/MainControls/permalink.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*/

import { describe, it, beforeEach, afterEach } from 'mocha';
import { expect } from 'chai';
import { copyText, showTooltip } from '../../../../src/UI/templates/js/MainControls/src/footer/permalink';

const expectOneCall = () => {
const expected = [];
const called = [];

return {
callOnce: (proc = () => {}) => {
const f = (...args) => {
if (called.includes(f)) {
throw new Error('Called more than once.');
}
called.push(f);
return proc(...args);
};
expected.push(f);

return f;
},
finish: () => expected.forEach(proc => {
if (!called.includes(proc)) {
throw new Error('Never called.');
}
}),
};
};

describe('Test permalink copy to clipboard', () => {
const saved = {};
beforeEach(() => {
saved.window = globalThis.window;
saved.document = globalThis.document;
});
afterEach(() => {
globalThis.window = saved.window;
globalThis.document = saved.document;
});

it('Clipboard API', () => {
let written = null;
const response = {};
const writeText = s => {
written = s;
return response;
};
globalThis.window = { navigator: { clipboard: { writeText } } };
expect(copyText('foo')).to.be.equal(response);
expect(written).to.be.equal('foo');
});

it('Legacy Clipboard API', () => {
const {callOnce, finish} = expectOneCall();
const node = { remove: callOnce() };
const range = {
selectNodeContents: callOnce(n => expect(n).to.be.equal(node))
};
const selection = {
addRange: callOnce(x => expect(x).to.be.equal(range)),
removeAllRanges: callOnce(),
};

globalThis.window = {
navigator: {},
getSelection: callOnce(() => selection),
};

globalThis.document = {
createRange: callOnce(() => range),

createElement: callOnce(text => {
expect(text).to.be.equal('span');
return node;
}),

execCommand: callOnce(s => {
expect(s).to.be.equal('copy');
return true;
}),

body: {
appendChild: callOnce(n => {
expect(n).to.be.equal(node);
expect(n.textContent).to.be.equal('foo');
}),
},
};

return copyText('foo').then(finish);
});
});

describe('Test permanentlink show tooltip', () => {
const saved = {};
beforeEach(() => {
saved.setTimeout = globalThis.setTimeout;
saved.document = globalThis.document;
});
afterEach(() => {
globalThis.setTimeout = saved.setTimeout;
globalThis.document = saved.document;
});

const testTooltip = (mainRect, nodeRect, expectTransform = null) => () => {
const {callOnce, finish} = expectOneCall();
let callTimeout = null;
globalThis.document = {
getElementsByTagName: callOnce(tag => {
expect(tag).to.be.equal('main');
return [
{getBoundingClientRect: callOnce(() => mainRect)}
];
}),
};

globalThis.setTimeout = callOnce((proc, delay) => {
callTimeout = proc;
expect(delay).to.be.equal(4321);
});

const isTooltipClass = name => expect(name).to.be.equal('c-tooltip--visible');
const node = {
parentNode: {
classList: {
add: callOnce(isTooltipClass),
remove: callOnce(isTooltipClass),
},
},
getBoundingClientRect: callOnce(() => nodeRect),
style: {transform: null},
};
showTooltip(node, 4321);

expect(callTimeout).not.to.be.equal(null);
expect(node.style.transform).to.be.equal(expectTransform);

callTimeout();
finish();
};

it('Show tooltip', testTooltip({left: 0, right: 10}, {left: 1, right: 9}));
it('Show tooltip left aligned', testTooltip({left: 5, right: 10}, {left: 3, right: 9}, 'translateX(calc(2px - 50%))'));
it('Show tooltip right aligned', testTooltip({left: 0, right: 7}, {left: 1, right: 9}, 'translateX(calc(-2px - 50%))'));
});
Loading

0 comments on commit 28de4d2

Please sign in to comment.