@use "sass:math"; // The suggestions list (dropdown) is appended to the document's body so the CSS variables should be defined at root-level :root { --tagify-dd-color-primary: rgb(53,149,246); // should be same as "$tags-focus-border-color" --tagify-dd-text-color: black; --tagify-dd-bg-color: white; --tagify-dd-item-pad: .3em .5em; // should be same as $tag-pad (below) --tagify-dd-max-height: 300px; } .tagify{ // SCSS "default" allows overriding variables BEFORE they are set in the below lines of code $self: &; $tags-border-color : #DDD !default; $tags-hover-border-color : #CCC !default; $tags-focus-border-color : #3595f6 !default; $tagMargin : 5px !default; $tag-pad : .3em .5em !default; $tag-min-width : 1ch !default; $tag-max-width : 100% !default; $tag-text-color : black !default; $tag-text-color--edit : black !default; $tag-bg : #E5E5E5 !default; $tag-hover : #D3E2E2 !default; $tag-remove : #D39494 !default; $tag-remove-btn-color : $tag-text-color !default; $tag-remove-btn-bg : none !default; $tag-remove-btn-bg--hover: darken($tag-remove, 8) !default; $tag-invalid-color : $tag-remove !default; $tag-invalid-bg : rgba($tag-remove, .5) !default; $tag-inset-shadow-size : 1.1em !default; $tag-hide-transition : .3s !default; $placeholder-color : rgba($tag-text-color, .4) !default; $placeholder-color-focus : rgba($tag-text-color, .25) !default; $input-color : inherit !default; $tagify-dd-bg-color : white !default; $tagify-dd-color-primary : rgb(53,149,246) !default; // CSS variables --tags-disabled-bg : #F1F1F1; --tags-border-color : #{$tags-border-color}; --tags-hover-border-color : #{$tags-hover-border-color}; --tags-focus-border-color : #{$tags-focus-border-color}; --tag-border-radius : 3px; --tag-bg : #{$tag-bg}; --tag-hover : #{$tag-hover}; --tag-text-color : #{$tag-text-color}; --tag-text-color--edit : #{$tag-text-color--edit}; --tag-pad : #{$tag-pad}; --tag-inset-shadow-size : #{$tag-inset-shadow-size}; --tag-invalid-color : #{$tag-invalid-color}; --tag-invalid-bg : #{$tag-invalid-bg}; --tag--min-width : #{$tag-min-width}; --tag--max-width : #{$tag-max-width}; --tag-hide-transition : #{$tag-hide-transition}; --tag-remove-bg : #{rgba($tag-remove, .3)}; --tag-remove-btn-color : #{$tag-remove-btn-color}; --tag-remove-btn-bg : #{$tag-remove-btn-bg}; --tag-remove-btn-bg--hover : #{$tag-remove-btn-bg--hover}; --input-color : #{$input-color}; --placeholder-color : #{$placeholder-color}; --placeholder-color-focus : #{$placeholder-color-focus}; --loader-size : .8em; --readonly-striped : 1; @mixin firefox { @at-root { @-moz-document url-prefix() { & { @content; } } } } @mixin placeholder( $show:true ){ transition: .2s ease-out; @if $show == true { opacity: 1; transform: none; } @else { opacity: 0; transform: translatex(6px); } } @mixin loader(){ content: ''; vertical-align: middle; opacity: 1; width: .7em; height: .7em; width: var(--loader-size); height: var(--loader-size); min-width: 0; border: 3px solid; border-color: #EEE #BBB #888 transparent; border-radius: 50%; animation: rotateLoader .4s infinite linear; } @mixin tagReadonlyBG($size:5px){ animation: readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused; @keyframes readonlyStyles { 0% { background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/#{$size} #{$size}; box-shadow: none; filter: brightness(.95); } } } @keyframes tags--bump{ 30% { transform: scale(1.2); } } @keyframes rotateLoader { to{ transform: rotate(1turn) } } display : inline-flex; align-items : flex-start; flex-wrap : wrap; border : 1px solid var(--tags-border-color); padding : 0; line-height : 0; outline : none; position : relative; box-sizing : border-box; transition : .1s; &:has([contenteditable='true']) { cursor: text; } &:hover:not(.tagify--focus):not(.tagify--invalid){ --tags-border-color : var(--tags-hover-border-color); } &[disabled]{ background: var(--tags-disabled-bg); filter: saturate(0); opacity: .5; pointer-events: none; } // Global "read-only" mode (no input button) &[readonly], &[disabled]{ &#{$self}--select{ pointer-events: none; } &:not(#{$self}--mix):not(#{$self}--select){ cursor: default; > #{$self}__input{ visibility: hidden; width: 0; margin: $tagMargin 0; } #{$self}__tag > div{ padding: var(--tag-pad); &::before{ @include tagReadonlyBG; } } } #{ $self }__tag__removeBtn{ display:none; } } &--loading{ #{ $self }__input{ > br:last-child{ display:none; } &::before{ content:none; } &::after{ @include loader; content: '' !important; margin: -2px 0 -2px .5em; } &:empty{ &::after{ margin-left:0; } } } } /////////////////////////////////////////// // Hides originals + input, + textarea{ position: absolute !important; left: -9999em !important; transform: scale(0) !important; } &__tag{ display : inline-flex; align-items : center; max-width : var(--tag--max-width); margin-inline: $tagMargin 0; margin-block : $tagMargin; position : relative; z-index : 1; outline : none; line-height : normal; cursor : default; transition : .13s ease-out; > div{ // :not([contenteditable]) display : flex; flex : 1; vertical-align : top; box-sizing : border-box; max-width : 100%; padding : var(--tag-pad); color : var(--tag-text-color); line-height : inherit; border-radius : var(--tag-border-radius); // user-select : none; // should allow selecting text if the user wishes to copy something white-space : nowrap; transition : .13s ease-out; > *{ white-space : pre-wrap; overflow : hidden; text-overflow : ellipsis; display : inline-block; vertical-align : top; min-width : var(--tag--min-width); max-width : var(--tag--max-width); transition : .8s ease, .1s color; &[contenteditable]{ display: block; // solves similar issue to this: https://stackoverflow.com/q/34354085/104380 (but with because of its default `inline-block` display) outline: none; user-select: text; cursor: text; // fix: sometimes the caret after the last character wasn't visible (when setting {backspace:"edit"}) margin: -2px; padding: 2px; max-width: 350px; } &:only-child{ width: 100%; } } &::before{ content: ''; position: absolute; border-radius: inherit; inset: var(--tag-bg-inset, 0); z-index: -1; pointer-events:none; transition: 120ms ease; animation : tags--bump .3s ease-out 1; box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset; } } &:hover:not([readonly]), &:focus{ div{ // :not([contenteditable]) &::before{ --tag-bg-inset: #{math.div(-$tagMargin, 2)}; --tag-bg: var(--tag-hover); } } } &--loading{ pointer-events: none; .tagify__tag__removeBtn{ display: none; } &::after{ --loader-size: .4em; @include loader; margin: 0 .5em 0 -.1em; } } &--flash{ div::before{ animation:none; } } &--hide{ width : 0 !important; padding-left : 0; padding-right : 0; margin-left : 0; margin-right : 0; opacity : 0; transform : scale(0); transition : var(--tag-hide-transition); pointer-events : none; > div > *{ white-space: nowrap; } } &#{ $self }{ &--noAnim{ > div::before{ animation:none; } } &--notAllowed:not(.tagify__tag--editable){ div{ > span{ opacity:.5; } // filter:blur(.2px); &::before{ --tag-bg: var(--tag-invalid-bg); transition: .2s; } } } } &[readonly]{ #{ $self }__tag__removeBtn{ display:none; } > div{// padding: $tag-pad; &::before{ @include tagReadonlyBG; } } } &--editable{ > div{ color : var(--tag-text-color--edit); &::before{ box-shadow: 0 0 0 2px var(--tag-hover) inset !important; } } > #{$self}__tag__removeBtn{ pointer-events: none; &::after{ opacity: 0; transform: translateX(100%) translateX(5px); } } &.tagify--invalid{ > div{ &::before{ box-shadow: 0 0 0 2px var(--tag-invalid-color) inset !important; } } } } &__removeBtn{ $size: 14px; order : 5; display : inline-flex; align-items : center; justify-content: center; border-radius : 50px; cursor : pointer; font : #{$size}/1 Arial; background : var(--tag-remove-btn-bg); color : var(--tag-remove-btn-color); width : $size; height : $size; margin-inline : auto math.div($size,3); overflow : hidden; transition : .2s ease-out; &::after{ content: "\00D7"; transition: .3s, color 0s; } &:hover{ color: white; background: var(--tag-remove-btn-bg--hover); // + span{ box-shadow: 0 0 0 2px $tag-remove inset; transition:.2s; } + div{ > span{ opacity:.5; } // filter:blur(.2px); &::before{ box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg, rgba($tag-remove, .3)) inset !important; transition: box-shadow .2s; } } } } } &:not(#{$self}--mix){ #{ $self }__input{ // https://stackoverflow.com/a/13470210/104380 br { display:none; } * { display:inline; white-space:nowrap; } } } /////////////////////////////////////////// // Holds the placeholder & the tags input &__input{ $placeholder-width : 110px; flex-grow: 1; display: inline-block; min-width: $placeholder-width; margin: $tagMargin; padding: var(--tag-pad); line-height: normal; position: relative; white-space: pre-wrap; // #160 Line break (\n) as delimeter color: var(--input-color); box-sizing: inherit; overflow: hidden; &:empty{ @include firefox { // clicking twice on the input (not fast) disallows typing (bug) only when the input has "display:flex". // disabled the below rule for the above reason: // display: flex; // https://bugzilla.mozilla.org/show_bug.cgi?id=904846#c45 } } &:focus{ outline:none; &::before{ @include placeholder(false); /* ALL MS BROWSERS: hide placeholder (on focus) otherwise the caret is placed after it, which is weird */ /* IE Edge 12+ CSS styles go here */ @supports ( -ms-ime-align:auto ) { display: none; } } &:empty{ &::before{ @include placeholder(true); // Seems to be fixed! no need for the below hack // @include firefox { // // remove ":after" pseudo element: https://bugzilla.mozilla.org/show_bug.cgi?id=904846#c45 // content: unset; // // display:inline-block; // } color: $placeholder-color-focus; color: var(--placeholder-color-focus); } &::after{ @include firefox { display: none; } } } } &::before{ content: attr(data-placeholder); width: 100%; height: 100%; margin: auto 0; z-index: 1; color: var(--placeholder-color); white-space: nowrap; text-overflow: ellipsis; overflow: hidden; pointer-events: none; opacity: 0; position: absolute; } /* Seems firefox newer versions don't need this any more @supports ( -moz-appearance:none ){ &::before{ line-height: inherit; position:relative; } } */ // tries to suggest the rest of the value from the first item in the whitelist which matches it &::after{ content: attr(data-suggest); display: inline-block; vertical-align: middle; position: absolute; min-width: calc(100% - 1.5em); text-overflow: ellipsis; overflow: hidden; white-space: pre; /* allows spaces at the beginning */ color: var(--tag-text-color); opacity: .3; pointer-events:none; max-width: 100px; } // &--invalid{ // // color: $invalid-input-color; // } // in "mix mode" the tags might be next to plain text, so spread things a bit #{ $self }__tag{ margin: 0 1px; } } &--mix { display: block; // display:flex makes Chrome generates

when pressing ENTER key #{ $self }__input{ padding: $tagMargin; margin: 0; width: 100%; height: 100%; line-height: 1.5; display: block; // needed to resolve this bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1182621 &::before{ height:auto; display: none; line-height: inherit; } // no suggested-complete are shown in mix-mode while higilighting dropdown options &::after{ content:none; } } } &--select{ cursor: default; &::after{ $size: 16px; content: '>'; opacity: .5; position: absolute; top: 50%; right: 0; bottom: 0; font: $size monospace; line-height: math.div($size,2); height: math.div($size,2); pointer-events: none; transform: translate(-150%, -50%) scaleX(1.2) rotate(90deg); transition: .2s ease-in-out; } &[aria-expanded=true]{ &::after{ transform: translate(-150%, -50%) rotate(270deg) scaleY(1.2); } } #{$self}__tag{ flex: 1; max-width: none; margin-inline-end: 2em; margin-block: 0; padding-block: $tagMargin; cursor: text; div{ &::before { display: none; } } + #{$self}__input{ display: none; } } } &--empty{ #{ $self }__input{ &::before{ @include placeholder; // position: static; display: inline-block; width: auto; #{ $self }--mix &{ display: inline-block; } } } } &--focus{ --tags-border-color: var(--tags-focus-border-color); transition: 0s; } &--invalid{ --tags-border-color : #{$tag-invalid-color}; } // Since the dropdown is an external element, which is positioned directly on the body element // it cannot ingerit the CSS variables applied on the ".Tagify" element &__dropdown{ $dropdown: &; $trans: .3s; $trans-curve: cubic-bezier(.5, 0, .3, 1); position: absolute; z-index: 9999; transform: translateY(-1px); border-top: 1px solid var(--tagify-dd-color-primary); overflow: hidden; &[dir='rtl'] { transform: translate(-100%, -1px); } &[placement="top"]{ margin-top: 0; transform: translateY(-100%); #{$dropdown}__wrapper{ border-top-width: 1.1px; // fixes - https://bugs.chromium.org/p/chromium/issues/detail?id=1147523 border-bottom-width: 0; } } // when the dropdown is shown next to the caret while typing &[position="text"]{ box-shadow: 0 0 0 3px rgba(var(--tagify-dd-color-primary), .1); font-size: .9em; #{$dropdown}__wrapper{ border-width: 1px; } } &__wrapper{ max-height: var(--tagify-dd-max-height); overflow: hidden; overflow-x: hidden; color: var(--tagify-dd-text-color); background: var(--tagify-dd-bg-color); border: 1px solid; border-color: var(--tagify-dd-color-primary); border-bottom-width: 1.5px; // fixes - https://bugs.chromium.org/p/chromium/issues/detail?id=1147523 border-top-width: 0; box-shadow: 0 2px 4px -2px rgba(black,.2); transition: $trans $trans-curve, transform math.div($trans, 2); animation: dd-wrapper-show 0s $trans forwards; } @keyframes dd-wrapper-show { to { overflow-y: auto; } } &__header{ &:empty{ display: none; } } &__footer{ display: inline-block; margin-top: .5em; padding: var(--tagify-dd-item-pad); font-size: 0.7em; font-style: italic; opacity: .5; &:empty{ display: none; } } // intial state, pre-rendered &--initial{ #{$dropdown}__wrapper{ max-height: 20px; transform: translateY(-1em); } &[placement="top"]{ #{$dropdown}__wrapper{ transform: translateY(2em); } } } &__item{ box-sizing: border-box; padding: var(--tagify-dd-item-pad); margin: 1px; white-space: pre-wrap; cursor: pointer; border-radius: 2px; position: relative; outline: none; max-height: 60px; max-width: 100%; line-height: normal; position: relative; &--active{ background: var(--tagify-dd-color-primary); color: white; } &:active{ filter: brightness(105%); } /* custom hidden transition effect is needed for horizontal-layout suggestions */ &--hidden{ padding-top: 0; padding-bottom: 0; margin: 0 1px; pointer-events: none; overflow: hidden; max-height: 0; transition: var(--tagify-dd-item--hidden-duration, .3s) !important; > * { transform: translateY(-100%); opacity: 0; transition: inherit; } } &--selected{ &::before{ content: "✓"; // checkmark font-family: monospace; position: absolute; inset-inline-start: 6px; text-indent: 0; line-height: 1.1; } } } &:has(.tagify__dropdown__item--selected) .tagify__dropdown__item { text-indent: 1em; } } }