diff --git a/.factory/index.css.scss b/.factory/index.css.scss index 6481c43..85fc6bd 100644 --- a/.factory/index.css.scss +++ b/.factory/index.css.scss @@ -1,5 +1,17 @@ $name: 'tag-picker'; +%tag { + align-items: center; + border: 1px solid transparent; + display: flex; + gap: 0.25em; + justify-content: center; + overflow: hidden; + padding: 0 0.25em; + position: relative; + text-decoration: none; +} + .#{$name} { &, & * { @@ -17,134 +29,117 @@ $name: 'tag-picker'; flex-wrap: wrap; position: relative; vertical-align: middle; -} - -.#{$name}__self { - left: -9999px; - position: fixed; - top: -9999px; - &:disabled { - + .#{$name} { - color: rgba(128, 128, 128); + &__self { + left: -9999px; + position: fixed; + top: -9999px; + &:disabled { + + .#{$name} { + color: rgba(128, 128, 128); + } + + .#{$name} { + &, + & * { + cursor: not-allowed; + } + } } - + .#{$name} { - &, - & * { - cursor: not-allowed; + &:read-only { + + .#{$name} { + .#{$name}__x { + cursor: not-allowed; + } } } } - &:read-only { - + .#{$name} { - .#{$name}__x { - cursor: not-allowed; + &__tag { + @extend %tag; + border: { + color: inherit; + } + cursor: pointer; + &:focus { + outline: 2px solid; + outline: { + offset: -2px; } } - } -} - -%tag { - align-items: center; - border: 1px solid transparent; - display: flex; - gap: 0.25em; - justify-content: center; - overflow: hidden; - padding: 0 0.25em; - position: relative; - text-decoration: none; -} - -.#{$name}__tag { - @extend %tag; - border: { - color: inherit; - } - cursor: pointer; - &:focus { - outline: 2px solid; - outline: { - offset: -2px; + &--selected { + background: rgba(0, 0, 255, 0.5); + } + // Keep selection (so that context menu can show the “Copy” command), but hide it visually + ::selection { + background-color: transparent; + color: inherit; + } + + .#{$name}__text { + span { + + span { + // Hide the placeholder if we have at least one tag + display: none; + } + } } } - &--selected { - background: rgba(0, 0, 255, 0.5); - } - // Keep selection (so that context menu can show the “Copy” command), but hide it visually - ::selection { - background-color: transparent; - color: inherit; + &__tags { + border-color: inherit; + display: flex; + flex-wrap: wrap; + flex: 1; + gap: 1px; + padding: 1px; } - + .#{$name}__text { + &__text { + @extend %tag; + flex: 1; span { - + span { - // Hide the placeholder if we have at least one tag + flex: 1; + position: relative; + z-index: 1; + // Ensure height even when tag(s) is empty + &::after { + content: '\200c'; + } + * { + display: inline; // Remove (hide) line-break in tag editor + } + br { display: none; } + + span { + bottom: 0; + left: 0; + opacity: 0.5; + padding: inherit; + position: absolute; + right: 0; + top: 0; + white-space: nowrap; + z-index: 0; + } } } -} - -.#{$name}__tags { - border-color: inherit; - display: flex; - flex-wrap: wrap; - flex: 1; - gap: 1px; - padding: 1px; -} - -.#{$name}__text { - @extend %tag; - flex: 1; - span { - flex: 1; + &__x { + height: 1em; + line-height: 0; + margin-right: -0.125em; position: relative; - z-index: 1; - // Ensure height even when tag(s) is empty - &::after { - content: '\200c'; - } - * { - display: inline; // Remove (hide) line-break in tag editor - } - br { - display: none; - } - + span { + width: 1em; + &::after, + &::before { + border: 1px solid; bottom: 0; - left: 0; - opacity: 0.5; - padding: inherit; + content: ""; + left: 50%; + margin-left: -1px; position: absolute; - right: 0; top: 0; - white-space: nowrap; - z-index: 0; } - } -} - -.#{$name}__x { - height: 1em; - line-height: 0; - margin-right: -0.125em; - position: relative; - width: 1em; - &::after, - &::before { - border: 1px solid; - bottom: 0; - content: ""; - left: 50%; - margin-left: -1px; - position: absolute; - top: 0; - } - &::after { - transform: rotate(45deg); - } - &::before { - transform: rotate(135deg); + &::after { + transform: rotate(45deg); + } + &::before { + transform: rotate(135deg); + } } } \ No newline at end of file diff --git a/.factory/index.html.pug b/.factory/index.html.pug index e26300a..1ecec4e 100644 --- a/.factory/index.html.pug +++ b/.factory/index.html.pug @@ -9,6 +9,9 @@ block state block script script | const picker = new TagPicker(document.forms[0].tags); + | picker.on('change', function () { + | console.log(this.value); + | }); | picker.on('has.tag', tag => { | alert('Tag “' + tag + '” already exists.'); | }); @@ -236,10 +239,12 @@ block content | } | | picker.on('change', onChange); - h4: code picker.set(name) - p Adds a new tag with the specified name. + h4: code picker.set(name, at = -1) + p Adds a new tag with the given name. pre: code - | picker.set('foo'); // Add (append) “foo” tag + | picker.set('foo'); // Append “foo” tag + | picker.set('bar', 0); // Prepend “bar” tag + | picker.set('baz', 2); // Insert “baz” tag at index 2 in the list h3 Static Methods p(role='status') This application does not have any static methods. h2#properties @@ -322,6 +327,7 @@ block content | }); h3 List of Extensions ul + li: a(aria-disabled='true') Clear Feature li: a(aria-disabled='true') History Feature li: a(aria-disabled='true') Options Feature li: a(aria-disabled='true') Sortable Feature diff --git a/.factory/index.js.mjs b/.factory/index.js.mjs index edcd5a8..bfd14ec 100644 --- a/.factory/index.js.mjs +++ b/.factory/index.js.mjs @@ -3,7 +3,7 @@ import {delay} from '@taufik-nurrohman/tick'; import {fromHTML, fromStates} from '@taufik-nurrohman/from'; import {hasValue} from '@taufik-nurrohman/has'; import {hook} from '@taufik-nurrohman/hook'; -import {isArray, isDefined, isFunction, isInstance, isObject, isSet, isString} from '@taufik-nurrohman/is'; +import {isArray, isDefined, isFunction, isInstance, isInteger, isObject, isSet, isString} from '@taufik-nurrohman/is'; import {offEvent, offEventDefault, offEventPropagation, onEvent} from '@taufik-nurrohman/event'; import {toCount, toObjectCount, toObjectKeys, toObjectValues} from '@taufik-nurrohman/to'; import {toPattern} from '@taufik-nurrohman/pattern'; @@ -157,9 +157,9 @@ defineProperty($$, 'value', { let $ = this, {_tags, state} = $; if ($.value) { - $.value.split(state.join).forEach(tag => $.let(tag, 0)); + $.value.split(state.join).forEach(tag => $.let(tag, 1)); } - value.split(state.join).forEach(tag => $.set(tag, 0)); + value.split(state.join).forEach(tag => $.set(tag, -1, 1)); $.fire('change'); } }); @@ -245,11 +245,11 @@ function onCutTag(e) { let selection = []; for (let k in _tags) { if (hasClass(_tags[k], n)) { - selection.push(k), picker.let(k); + selection.push(k), picker.let(k, 1); } } e.clipboardData.setData('text/plain', selection.join(state.join)); - picker.fire('cut', [e, selection]).focus(); + picker.fire('cut', [e, selection]).fire('change', [$.title]).focus(); offEventDefault(e); } @@ -302,7 +302,7 @@ function onKeyDownTag(e) { prevTag = getPrev($), nextTag = getNext($), firstTag, lastTag, - n = state.n + '__tag--selected'; + n = state.n + '__tag--selected', v; if (keyIsShift) { setClass($, n), selectTo(getChildFirst($)); if (KEY_ARROW_LEFT === key) { @@ -381,25 +381,27 @@ function onKeyDownTag(e) { nextTag && text !== nextTag ? focusTo(nextTag) : picker.focus(); exit = true; } else if (KEY_DELETE_LEFT === key) { - picker.let($.title); + picker.let(v = $.title, 1); if (toCount(selection) > 1) { let current; while (current = selection.pop()) { prevTag = _tags[current] && getPrev(_tags[current]); - picker.let(current); + picker.let(current, 1); } } + picker.fire('change', [v]); prevTag ? (focusTo(prevTag), selectTo(getChildFirst(prevTag))) : picker.focus(); exit = true; } else if (KEY_DELETE_RIGHT === key) { - picker.let($.title); + picker.let(v = $.title, 1); if (toCount(selection) > 1) { let current; while (current = selection.shift()) { nextTag = _tags[current] && getNext(_tags[current]); - picker.let(current); + picker.let(current, 1); } } + picker.fire('change', [v]); nextTag && text !== nextTag ? (focusTo(nextTag), selectTo(getChildFirst(nextTag))) : picker.focus(); exit = true; } else if (KEY_ESCAPE === key || KEY_TAB === key) { @@ -553,10 +555,10 @@ function onPasteTag(e) { } // Delete all tag(s) before paste if (isAllSelected && picker.value) { - picker.value.split(state.join).forEach(tag => picker.let(tag, 0)); + picker.value.split(state.join).forEach(tag => picker.let(tag, 1)); } let values = value.split(state.join); - values.forEach(tag => picker.set(tag, 0)); + values.forEach(tag => picker.set(tag, -1, 1)); picker.fire('paste', [e, values]).focus().fire('change'); offEventDefault(e); } @@ -573,7 +575,7 @@ function onPasteTextInput(e) { picker.text = ""; if (value) { let values = value.split(state.join); - values.forEach(tag => picker.set(tag, 0)); + values.forEach(tag => picker.set(tag, -1, 1)); picker.fire('paste', [e, values]).fire('change'); } }, 1)(); @@ -640,8 +642,7 @@ function onPointerDownTagX(e) { {_mask} = picker, {input} = _mask; offEvent('click', $, onPointerDownTagX); - picker.let(tag.title); - picker.focus(), offEventDefault(e); + picker.let(tag.title).focus(), offEventDefault(e); } function onResetForm(e) { @@ -731,7 +732,7 @@ $$.attach = function (self, state) { $._mask = _mask; // Attach the current tag(s) if ($._value) { - $._value.split(state.join).forEach(tag => $.set(tag, 0, 1)); + $._value.split(state.join).forEach(tag => $.set(tag, -1, 1, 1)); } // Attach extension(s) if (isSet(state) && isArray(state.with)) { @@ -831,7 +832,7 @@ $$.get = function (v) { return toObjectKeys(_tags).indexOf(v); }; -$$.let = function (v, _hookChange = true) { +$$.let = function (v, _skipHookChange) { let $ = this, {_active, _tags, _value, self, state} = $; if (!_active) { @@ -840,9 +841,9 @@ $$.let = function (v, _hookChange = true) { // Reset if (!isDefined(v)) { if ($.value) { - $.value.split(state.join).forEach(tag => $.let(tag, 0)); + $.value.split(state.join).forEach(tag => $.let(tag, 1)); } - return _value.split(state.join).forEach(tag => $.set(tag, 0)), $.fire('change'); + return _value.split(state.join).forEach(tag => $.set(tag, -1, 1)), $.fire('change'); } if (toObjectCount(_tags) < state.min) { return $.fire('min.tags', [v]); @@ -869,13 +870,13 @@ $$.let = function (v, _hookChange = true) { delete $._tags[v]; self.value = toObjectKeys($._tags).join(state.join); $.fire('let.tag', [v]); - if (_hookChange) { + if (!_skipHookChange) { $.fire('change', [v]); } return $; }; -$$.set = function (v, _hookChange = true, _attach) { +$$.set = function (v, at, _skipHookChange, _attach) { let $ = this, {_active, _filter, _mask, _tags, self, state} = $, {text} = _mask, @@ -925,11 +926,22 @@ $$.set = function (v, _hookChange = true, _attach) { } setChildLast(tag, tagText); setChildLast(tag, tagX); - setPrev(text, tag); - $._tags[v] = tag; + if (isInteger(at) && at >= 0) { + let tags = toObjectKeys(_tags); + tags.splice(at, 0, v); + $._tags = {}; + _tags[v] = tag; + tags.forEach(k => { + $._tags[k] = _tags[k]; + setPrev(text, _tags[k]); + }); + } else { + setPrev(text, tag); + $._tags[v] = tag; + } self.value = toObjectKeys($._tags).join(state.join); $.fire('set.tag', [v]); - if (_hookChange) { + if (!_skipHookChange) { $.fire('change', [v]); } return $; diff --git a/.factory/tweak/bootstrap5.html.pug b/.factory/tweak/bootstrap5.html.pug index 1725e78..f5bb9f8 100644 --- a/.factory/tweak/bootstrap5.html.pug +++ b/.factory/tweak/bootstrap5.html.pug @@ -17,7 +17,7 @@ block style | border-radius: var(--bs-border-radius); | border: var(--bs-border-width) solid var(--bs-border-color); | color: var(--bs-body-color); - | line-height: 1.5; + | line-height: 1.5em; | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | width: 100%; | } @@ -27,24 +27,50 @@ block style | box-shadow: var(--bs-focus-ring-blur, var(--bs-focus-ring-y, var(--bs-focus-ring-x, 0) 0) 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color); | color: var(--bs-body-color); | } + | .tag-picker__self.form-control-lg + .tag-picker { + | border-radius: var(--bs-border-radius-lg); + | font-size: 1.25rem; + | } + | .tag-picker__self.form-control-lg + .tag-picker .tag-picker__tag, + | .tag-picker__self.form-control-lg + .tag-picker .tag-picker__text { + | gap: 0.5rem; + | padding: 0 calc(0.5rem - 1px); + | } + | .tag-picker__self.form-control-lg + .tag-picker .tag-picker__tags { + | gap: 0.5rem; + | padding: 0.5rem; + | } + | .tag-picker__self.form-control-sm + .tag-picker { + | border-radius: var(--bs-border-radius-sm); + | font-size: 0.875rem; + | } + | .tag-picker__self.form-control-sm + .tag-picker .tag-picker__tag, + | .tag-picker__self.form-control-sm + .tag-picker .tag-picker__text { + | gap: 0.25rem; + | padding: 0 calc(0.25rem - 1px); + | } + | .tag-picker__self.form-control-sm + .tag-picker .tag-picker__tags { + | gap: 0.25rem; + | padding: 0.25rem; + | } | .tag-picker__self:disabled + .tag-picker, | .tag-picker__self:read-only + .tag-picker { | background-color: var(--bs-secondary-bg); | } | .tag-picker__self:disabled + .tag-picker .tag-picker__tag { - | opacity: 0.5 + | opacity: 0.5; | } | .tag-picker__tag, | .tag-picker__text { - | height: 1.5em; | line-height: calc(1.5em - 2px); - | padding: 0 0.375rem; + | padding: 0 calc(0.375rem - 1px); | } | .tag-picker__tag { | background-color: var(--bs-secondary-bg-subtle); | border-color: var(--bs-secondary-border-subtle); | border-radius: var(--bs-border-radius-sm); | color: var(--bs-secondary-text-emphasis); + | gap: 0.5rem; | } | .tag-picker__tag:focus { | border-color: var(--bs-secondary-text-emphasis); @@ -67,6 +93,16 @@ block style | color: var(--bs-secondary-color); | opacity: 1; | } + | .tag-picker__x { + | height: 0.75em; + | margin-right: 0; + | width: 0.75em; + | } + | .tag-picker__x::after, + | .tag-picker__x::before { + | border-left-width: 0; + | margin-left: -0.5px; + | } block content main.container.py-3 @@ -75,7 +111,12 @@ block content input.form-control(placeholder!='Text goes here…') div.col-lg-4 input.form-control(name='tags' placeholder!='Tags go here…' type='text' value='foo, bar, baz') - div.col-lg-4 + div.align-items-start.col-lg-4.d-flex.gap-2 + select.form-select(onchange='picker.self.classList.remove(\'form-control-lg\', \'form-control-sm\'); (this.value && picker.self.classList.add(\'form-control-\' + this.value)); picker.detach().attach();') + option(value='sm') Small + option(selected value="") Medium + option(value='lg') Large + = ' ' button.btn.btn-secondary(data-bs-toggle='button' onclick='picker.self.disabled = !picker.self.disabled; picker.detach().attach();' type='button') Disable = ' ' button.btn.btn-danger(onclick='picker.detach();' type='button') Destroy diff --git a/index.css b/index.css index c3149bd..3e6ef21 100644 --- a/index.css +++ b/index.css @@ -1,4 +1,17 @@ @charset "UTF-8"; +.tag-picker__text, +.tag-picker__tag { + align-items: center; + border: 1px solid transparent; + display: flex; + gap: 0.25em; + justify-content: center; + overflow: hidden; + padding: 0 0.25em; + position: relative; + text-decoration: none; +} + .tag-picker, .tag-picker * { box-sizing: border-box; @@ -15,7 +28,6 @@ position: relative; vertical-align: middle; } - .tag-picker__self { left: -9999px; position: fixed; @@ -31,20 +43,6 @@ .tag-picker__self:read-only + .tag-picker .tag-picker__x { cursor: not-allowed; } - -.tag-picker__text, -.tag-picker__tag { - align-items: center; - border: 1px solid transparent; - display: flex; - gap: 0.25em; - justify-content: center; - overflow: hidden; - padding: 0 0.25em; - position: relative; - text-decoration: none; -} - .tag-picker__tag { border-color: inherit; cursor: pointer; @@ -63,7 +61,6 @@ .tag-picker__tag + .tag-picker__text span + span { display: none; } - .tag-picker__tags { border-color: inherit; display: flex; @@ -72,7 +69,6 @@ gap: 1px; padding: 1px; } - .tag-picker__text { flex: 1; } @@ -101,7 +97,6 @@ white-space: nowrap; z-index: 0; } - .tag-picker__x { height: 1em; line-height: 0; diff --git a/index.html b/index.html index 9780a93..509d295 100644 --- a/index.html +++ b/index.html @@ -203,9 +203,11 @@

picker.on(name, event)

} picker.on('change', onChange); -

picker.set(name)

-

Adds a new tag with the specified name.

-
picker.set('foo'); // Add (append) “foo” tag
+

picker.set(name, at = -1)

+

Adds a new tag with the given name.

+
picker.set('foo'); // Append “foo” tag
+picker.set('bar', 0); // Prepend “bar” tag
+picker.set('baz', 2); // Insert “baz” tag at index 2 in the list

Static Methods

This application does not have any static methods.

# Properties

@@ -271,6 +273,7 @@

Usage of an Extension

});

List of Extensions