Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions components/ILIAS/UI/UI.php
Original file line number Diff line number Diff line change
Expand Up @@ -584,8 +584,6 @@ public function init(
new Component\Resource\ComponentJS($this, "js/Input/Field/file.js");
$contribute[Component\Resource\PublicAsset::class] = fn() =>
new Component\Resource\ComponentJS($this, "js/Input/Field/input.js");
$contribute[Component\Resource\PublicAsset::class] = fn() =>
new Component\Resource\ComponentJS($this, "js/Input/Field/tagInput.js");
$contribute[Component\Resource\PublicAsset::class] = fn() =>
new Component\Resource\ComponentJS($this, "js/Item/dist/notification.js");
$contribute[Component\Resource\PublicAsset::class] = fn() =>
Expand Down

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions components/ILIAS/UI/resources/js/Input/Field/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import terser from '@rollup/plugin-terser';
import nodeResolve from '@rollup/plugin-node-resolve';
import copyright from '../../../../../../../scripts/Copyright-Checker/copyright.js';
import preserveCopyright from '../../../../../../../scripts/Copyright-Checker/preserveCopyright.js';

Expand Down Expand Up @@ -42,4 +43,7 @@ export default {
}),
],
},
plugins: [
nodeResolve(),
]
};
171 changes: 171 additions & 0 deletions components/ILIAS/UI/resources/js/Input/Field/src/Tag/tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* 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
*/

/**
*
* @type {undefined|AbortController}
*/
let abortController;

/**
*
* @type {undefined|number}
*/
let timeout;

/**
* @param {string} inputId
* @param {Object} config
* @returns {Object}
*/
function buildSettings(inputId, config) {
return {
id: inputId,
whitelist: config.options,
enforceWhitelist: !config.userInput,
duplicates: config.allowDuplicates,
maxTags: config.maxItems,
delimiters: null,
originalInputValueFormat: (valuesArr) => valuesArr.map((item) => item.value),
dropdown: {
enabled: config.dropdownSuggestionsStartAfter,
maxItems: config.dropdownMaxItems,
closeOnSelect: config.dropdownCloseOnSelect,
highlightFirst: config.highlight,
},
transformTag(tagData) {
if (!tagData.display) {
tagData.display = tagData.value;
tagData.value = encodeURIComponent(tagData.value);
}
tagData.display = tagData.display
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
},
templates: {
wrapper(input, _s) {
return `<div class="${_s.classNames.namespace} ${_s.mode ? `${_s.classNames[`${_s.mode}Mode`]}` : ''} ${input.className}"
${_s.readonly ? 'readonly' : ''}
${_s.disabled ? 'disabled' : ''}
${_s.required ? 'required' : ''}
${_s.mode === 'select' ? "spellcheck='false'" : ''}
tabIndex="-1">
${this.settings.templates.input.call(this)}
\u200B
</div>`;
},
tag(tagData) {
return `<div contenteditable='false'
spellcheck="false" class='tagify__tag'
value="${tagData.value}"
tabindex="0">
<span title='remove tag' class='tagify__tag__removeBtn'></span>
<div>
<span class='tagify__tag-text'>${tagData.display}</span>
</div>
</div>`;
},
dropdownItem(tagData) {
return `<div class='tagify__dropdown__item' tagifySuggestionIdx="${tagData.tagifySuggestionIdx}" value="${tagData.value}">
<span>${tagData.display}</span>
</div>`;
},
},
};
}

/**
* @param {Tagify} instance
* @param {number} suggestionsStartAfter
* @param {URLBuilder} autocompleteEndpoint
* @param {URLBuilderToken} autocompleteToken
* @param {InputEvent} event
* @param {number} tagAutocompleteTriggerTimeout
*/
function retrieveAutocomplete(
instance,
suggestionsStartAfter,
autocompleteEndpoint,
autocompleteToken,
event,
tagAutocompleteTriggerTimeout,
) {
if (abortController instanceof AbortController) {
abortController.abort();
}
abortController = new AbortController();

instance.whitelist = null;

if (timeout !== undefined) {
instance.DOM.scope.ownerDocument.defaultView.clearTimeout(timeout);
timeout = undefined;
}

if (event.detail.value.length < suggestionsStartAfter) {
return;
}

timeout = instance.DOM.scope.ownerDocument.defaultView.setTimeout(
() => {
const searchTerm = event.detail.value;
autocompleteEndpoint.writeParameter(autocompleteToken, searchTerm);
instance.loading(true);
fetch(autocompleteEndpoint.getUrl().toString(), { signal: abortController.signal })
.then((answer) => answer.json())
.catch(() => {})
.then((options) => {
instance.whitelist = options;
instance.loading(false).dropdown.show(searchTerm);
});
},
tagAutocompleteTriggerTimeout,
);
}

/**
* @param {Tagify} Tagify
* @param {HTMLElement} input
* @param {Object} config
* @param {Array} value
* @param {undefined|URLBuilder} autocompleteEndpoint
* @param {undefined|URLBuilderToken} autocompleteToken
*/
export default function init(
Tagify,
input,
config,
value,
autocompleteEndpoint,
autocompleteToken,
) {
const instance = new Tagify(
input,
buildSettings(input.id, config),
);
instance.addTags(value);
if (autocompleteEndpoint !== undefined) {
instance.on('input', (event) => {
retrieveAutocomplete(
instance,
config.suggestionStarts,
autocompleteEndpoint,
autocompleteToken,
event,
config.autocompleteTriggerTimeout,
);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import TextareaFactory from './Textarea/textarea.factory.js';
import MarkdownFactory from './Markdown/markdown.factory.js';
import TreeSelectFactory from './TreeSelect/TreeSelectFactory.js';
import JQueryEventListener from '../../../Core/src/JQueryEventListener.js';
import Tagify from '@yaireo/tagify';
import tag from './Tag/tag.js';

il.UI = il.UI || {};
il.UI.Input = il.UI.Input || {};
Expand All @@ -45,4 +47,7 @@ il.UI.Input = il.UI.Input || {};
{txt: (s) => il.Language.txt(s)},
document,
);
Input.tagInput = Input.tag || {};
Input.tagInput.init = (input, config, value, autocompleteEndpoint, autocompleteToken) => tag(
Tagify, input, config, value, autocompleteEndpoint, autocompleteToken);
}(il.UI.Input));
96 changes: 0 additions & 96 deletions components/ILIAS/UI/resources/js/Input/Field/tagInput.js

This file was deleted.

47 changes: 15 additions & 32 deletions components/ILIAS/UI/src/Component/Input/Field/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
use ILIAS\UI\Component\Input\Container\Form\FormInput;
use ILIAS\UI\Component\Signal;
use InvalidArgumentException;
use ILIAS\UI\URLBuilder;
use ILIAS\UI\URLBuilderToken;

/**
* Interface Tag
Expand All @@ -33,23 +35,11 @@
*/
interface Tag extends FormInput
{
/**
* @return string[] of tags such as [ 'Interesting', 'Boring', 'Animating', 'Repetitious' ]
*/
public function getTags(): array;

/**
* Get an input like this, but decide whether the user can provide own
* tags or not. (Default: Allowed)
*/
public function withUserCreatedTagsAllowed(bool $extendable): Tag;

/**
* @see withUserCreatedTagsAllowed
* @return bool Whether the user is allowed to input more
* options than the given.
*/
public function areUserCreatedTagsAllowed(): bool;
public function withUserCreatedTagsAllowed(bool $extendable): self;

/**
* Get an input like this, but change the amount of characters the
Expand All @@ -58,38 +48,31 @@ public function areUserCreatedTagsAllowed(): bool;
* @param int $characters defaults to 1
* @throws InvalidArgumentException
*/
public function withSuggestionsStartAfter(int $characters): Tag;

/**
* @see withSuggestionsStartAfter
*/
public function getSuggestionsStartAfter(): int;
public function withSuggestionsStartAfter(int $characters): self;

/**
* Get an input like this, but limit the amount of characters one tag can be. (Default: unlimited)
*/
public function withTagMaxLength(int $max_length): Tag;

/**
* @see withTagMaxLength
*/
public function getTagMaxLength(): int;
public function withTagMaxLength(int $max_length): self;

/**
* Get an input like this, but limit the amount of tags a user can select or provide. (Default: unlimited)
*/
public function withMaxTags(int $max_tags): Tag;

public function withMaxTags(int $max_tags): self;

/**
* @see withMaxTags
* Get an input like this, but add an endpoint to get a list of possible options.
* The $autocomplete_endpoint MUST answer to a query with the provided text
* handed over in the parameter defined in $term_token.
* It MUST answer with a json array containing the options in the form of objects
* containing three properties "value", "display", and "searchBy". The property
* "value" MUST be save to transmit as url-parameter.
*/
public function getMaxTags(): int;

public function withAsyncAutocomplete(URLBuilder $autocomplete_endpoint, URLBuilderToken $term_token): self;

// Events

public function withAdditionalOnTagAdded(Signal $signal): Tag;
public function withAdditionalOnTagAdded(Signal $signal): self;

public function withAdditionalOnTagRemoved(Signal $signal): Tag;
public function withAdditionalOnTagRemoved(Signal $signal): self;
}
Loading