((resolve) => {
113 | window.registerReactApp = (updateTheme) => {
114 | themeUpdateCallbacks.push(updateTheme)
115 | resolve()
116 | }
117 | })
118 |
119 | Promise.all([loadingVueApp, loadingReactApp]).then(() => {
120 | updateTheme(themeSelect.value)
121 | })
122 |
123 | mountVueApp()
124 | mountReactApp()
125 |
--------------------------------------------------------------------------------
/packages/site/src/rules.ts:
--------------------------------------------------------------------------------
1 | const supportLookbehind = (() => {
2 | try {
3 | new RegExp('(?<=a)b')
4 | return true
5 | } catch {
6 | return false
7 | }
8 | })()
9 |
10 | export const tweet = [
11 | {
12 | pattern: supportLookbehind
13 | ? new RegExp('(?<=^|\\s)@[a-z][\\da-z_]+', 'gi')
14 | : /@[a-z][\\da-z_]+/gi,
15 | class: 'link'
16 | },
17 | {
18 | pattern: supportLookbehind
19 | ? new RegExp('(?<=^|\\s)#[a-z][\\da-z_]+', 'gi')
20 | : /#[a-z][\\da-z_]+/gi,
21 | class: 'link'
22 | }
23 | ]
24 |
25 | export const variable = [
26 | {
27 | pattern: /\{\{([a-z_]+?)\}\}/gi,
28 | replacer: (_: string, name: string) => {
29 | console.log(_, name)
30 | return `{{${name}}}`
31 | }
32 | }
33 | ]
34 |
35 | const namedColors = [
36 | 'aliceblue',
37 | 'antiquewhite',
38 | 'aqua',
39 | 'aquamarine',
40 | 'azure',
41 | 'beige',
42 | 'bisque',
43 | 'black',
44 | 'blanchedalmond',
45 | 'blue',
46 | 'blueviolet',
47 | 'brown',
48 | 'burlywood',
49 | 'cadetblue',
50 | 'chartreuse',
51 | 'chocolate',
52 | 'coral',
53 | 'cornflowerblue',
54 | 'cornsilk',
55 | 'crimson',
56 | 'cyan',
57 | 'darkblue',
58 | 'darkcyan',
59 | 'darkgoldenrod',
60 | 'darkgray',
61 | 'darkgreen',
62 | 'darkgrey',
63 | 'darkkhaki',
64 | 'darkmagenta',
65 | 'darkolivegreen',
66 | 'darkorange',
67 | 'darkorchid',
68 | 'darkred',
69 | 'darksalmon',
70 | 'darkseagreen',
71 | 'darkslateblue',
72 | 'darkslategray',
73 | 'darkslategrey',
74 | 'darkturquoise',
75 | 'darkviolet',
76 | 'deeppink',
77 | 'deepskyblue',
78 | 'dimgray',
79 | 'dimgrey',
80 | 'dodgerblue',
81 | 'firebrick',
82 | 'floralwhite',
83 | 'forestgreen',
84 | 'fuchsia',
85 | 'gainsboro',
86 | 'ghostwhite',
87 | 'gold',
88 | 'goldenrod',
89 | 'gray',
90 | 'green',
91 | 'greenyellow',
92 | 'grey',
93 | 'honeydew',
94 | 'hotpink',
95 | 'indianred',
96 | 'indigo',
97 | 'ivory',
98 | 'khaki',
99 | 'lavender',
100 | 'lavenderblush',
101 | 'lawngreen',
102 | 'lemonchiffon',
103 | 'lightblue',
104 | 'lightcoral',
105 | 'lightcyan',
106 | 'lightgoldenrodyellow',
107 | 'lightgray',
108 | 'lightgreen',
109 | 'lightgrey',
110 | 'lightpink',
111 | 'lightsalmon',
112 | 'lightseagreen',
113 | 'lightskyblue',
114 | 'lightslategray',
115 | 'lightslategrey',
116 | 'lightsteelblue',
117 | 'lightyellow',
118 | 'lime',
119 | 'limegreen',
120 | 'linen',
121 | 'magenta',
122 | 'maroon',
123 | 'mediumaquamarine',
124 | 'mediumblue',
125 | 'mediumorchid',
126 | 'mediumpurple',
127 | 'mediumseagreen',
128 | 'mediumslateblue',
129 | 'mediumspringgreen',
130 | 'mediumturquoise',
131 | 'mediumvioletred',
132 | 'midnightblue',
133 | 'mintcream',
134 | 'mistyrose',
135 | 'moccasin',
136 | 'navajowhite',
137 | 'navy',
138 | 'oldlace',
139 | 'olive',
140 | 'olivedrab',
141 | 'orange',
142 | 'orangered',
143 | 'orchid',
144 | 'palegoldenrod',
145 | 'palegreen',
146 | 'paleturquoise',
147 | 'palevioletred',
148 | 'papayawhip',
149 | 'peachpuff',
150 | 'peru',
151 | 'pink',
152 | 'plum',
153 | 'powderblue',
154 | 'purple',
155 | 'red',
156 | 'rebeccapurple',
157 | 'rosybrown',
158 | 'royalblue',
159 | 'saddlebrown',
160 | 'salmon',
161 | 'sandybrown',
162 | 'seagreen',
163 | 'seashell',
164 | 'sienna',
165 | 'silver',
166 | 'skyblue',
167 | 'slateblue',
168 | 'slategray',
169 | 'slategrey',
170 | 'snow',
171 | 'springgreen',
172 | 'steelblue',
173 | 'tan',
174 | 'teal',
175 | 'thistle',
176 | 'tomato',
177 | 'turquoise',
178 | 'violet',
179 | 'wheat',
180 | 'white',
181 | 'whitesmoke',
182 | 'yellow',
183 | 'yellowgreen'
184 | ].sort((a, b) => b.length - a.length)
185 | const colorPattern = new RegExp(
186 | `currentColor|(?:rgba?|hsla?|hwb|lab|lch|oklch|color)\\([^)]+\\)|#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})|(?:${namedColors.join(
187 | '|'
188 | )})`,
189 | 'gi'
190 | )
191 | export const color = [
192 | {
193 | pattern: colorPattern,
194 | replacer: (match: string) =>
195 | `${match}`
196 | }
197 | ]
198 |
--------------------------------------------------------------------------------
/packages/highlightable-input/src/cursor.ts:
--------------------------------------------------------------------------------
1 | export interface SelectOptions {
2 | force?: boolean
3 | collapse?: 'start' | 'end' | false
4 | }
5 |
6 | export type SelectOffsets = readonly [number, number] | number | true
7 |
8 | export function getSelection(el: HTMLElement): readonly [number, number] {
9 | const s = window.getSelection()!
10 | const { anchorNode, anchorOffset, focusNode, focusOffset } = s
11 |
12 | // Selecting with "Select all (⌘ + A on macOS / Ctrl + A on Windows)"
13 | // in Firefox will cause the root element to be selected.
14 | if (anchorNode === el && focusNode === el) {
15 | return [0, s.getRangeAt(0).toString().length]
16 | }
17 |
18 | const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
19 | let start = 0
20 | let end = 0
21 | let current: Node | null = null
22 | let startOffset: number | null = null
23 | let endOffset: number | null = null
24 |
25 | while ((current = walker.nextNode())) {
26 | if (startOffset == null) {
27 | if (current === anchorNode) {
28 | start += anchorOffset
29 | startOffset = start
30 | } else {
31 | start += current.nodeValue!.length
32 | }
33 | }
34 |
35 | if (endOffset == null) {
36 | if (current === focusNode) {
37 | end += focusOffset
38 | endOffset = end
39 | } else {
40 | end += current.nodeValue!.length
41 | }
42 | }
43 |
44 | if (startOffset != null && endOffset != null) {
45 | return [startOffset, endOffset]
46 | }
47 | }
48 |
49 | return [0, 0]
50 | }
51 |
52 | export function setSelection(
53 | el: HTMLElement,
54 | offsets: SelectOffsets,
55 | { force = false, collapse = false }: SelectOptions = {}
56 | ) {
57 | if (document.activeElement !== el && !force) {
58 | return
59 | }
60 |
61 | if (offsets === true) {
62 | // select all
63 | selectAll(el)
64 | } else {
65 | selectByOffsets(
66 | el,
67 | typeof offsets === 'number' ? [offsets, offsets] : offsets
68 | )
69 | }
70 |
71 | if (!collapse) {
72 | return
73 | }
74 |
75 | const selection = window.getSelection()!
76 | if (collapse === 'start') {
77 | selection.collapseToStart()
78 | } else if (collapse === 'end') {
79 | selection.collapseToEnd()
80 | }
81 | }
82 |
83 | function selectByOffsets(el: HTMLElement, offsets: readonly [number, number]) {
84 | const selection = window.getSelection()!
85 | const [startOffset, endOffset] = offsets
86 | const collapsed = startOffset === endOffset
87 | let [remainingStart, remainingEnd] =
88 | startOffset > endOffset
89 | ? [endOffset, startOffset]
90 | : [startOffset, endOffset]
91 | let current: Node | null = null
92 | let startNode: Node | null = null
93 | let endNode: Node | null = null
94 |
95 | const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
96 |
97 | while ((current = walker.nextNode())) {
98 | if (startNode == null) {
99 | if (remainingStart > current.nodeValue!.length) {
100 | remainingStart -= current.nodeValue!.length
101 | } else {
102 | startNode = current
103 | }
104 | }
105 |
106 | if (endNode == null && !collapsed) {
107 | if (remainingEnd > current.nodeValue!.length) {
108 | remainingEnd -= current.nodeValue!.length
109 | } else {
110 | endNode = current
111 | }
112 | }
113 |
114 | if (startNode && (endNode || collapsed)) {
115 | const range = document.createRange()
116 | range.setStart(startNode, remainingStart)
117 |
118 | if (endNode) {
119 | range.setEnd(endNode, remainingEnd)
120 | }
121 |
122 | selection.removeAllRanges()
123 | selection.addRange(range)
124 |
125 | return
126 | }
127 | }
128 | }
129 |
130 | function selectAll(el: HTMLElement) {
131 | const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
132 |
133 | let current: Node | null = null
134 | let first: Node | null = null
135 | let last: Node | null = null
136 |
137 | while ((current = walker.nextNode())) {
138 | if (!first) {
139 | first = current
140 | }
141 | last = current
142 | }
143 |
144 | const selection = window.getSelection()!
145 | const range = document.createRange()
146 |
147 | if (!first || !last) {
148 | range.selectNodeContents(el)
149 | return
150 | }
151 |
152 | range.setStart(first, 0)
153 | range.setEnd(last, last.nodeValue!.length)
154 |
155 | selection.removeAllRanges()
156 | selection.addRange(range)
157 | }
158 |
--------------------------------------------------------------------------------
/packages/highlightable-input/src/styles/themes/lightning.css:
--------------------------------------------------------------------------------
1 | highlightable-input[data-theme="lightning"] {
2 | font-size: 13px;
3 | padding-top: 0;
4 | padding-right: var(
5 | --slds-c-input-spacing-horizontal-end,
6 | var(--sds-c-input-spacing-horizontal-end, 1rem)
7 | );
8 | padding-bottom: 0;
9 | padding-left: var(
10 | --slds-c-input-spacing-horizontal-start,
11 | var(--sds-c-input-spacing-horizontal-start, 0.75rem)
12 | );
13 | width: 100%;
14 | min-height: calc(1.875rem + 2px);
15 | line-height: 1.875rem;
16 | border: 1px solid
17 | var(--slds-c-input-color-border, var(--sds-c-input-color-border, #c9c9c9));
18 | border-radius: var(
19 | --slds-c-input-radius-border,
20 | var(--sds-c-input-radius-border, 0.25rem)
21 | );
22 | background-color: var(
23 | --slds-c-input-color-background,
24 | var(--sds-c-input-color-background, #fff)
25 | );
26 | color: var(--slds-c-input-text-color, var(--sds-c-input-text-color));
27 | box-shadow: var(--slds-c-input-shadow, var(--sds-c-input-shadow));
28 | transition: border 0.1s linear, background-color 0.1s linear;
29 | }
30 |
31 | highlightable-input[data-theme="lightning"][aria-multiline="true"] {
32 | line-height: 1.5;
33 | min-height: var(
34 | --slds-c-textarea-sizing-min-height,
35 | var(--sds-c-textarea-sizing-min-height)
36 | );
37 | width: 100%;
38 | padding: var(
39 | --slds-c-textarea-spacing-block-start,
40 | var(--sds-c-textarea-spacing-block-start, 0.5rem)
41 | )
42 | var(
43 | --slds-c-textarea-spacing-inline-end,
44 | var(--sds-c-textarea-spacing-inline-end, 0.75rem)
45 | )
46 | var(
47 | --slds-c-textarea-spacing-block-end,
48 | var(--sds-c-textarea-spacing-block-end, 0.5rem)
49 | )
50 | var(
51 | --slds-c-textarea-spacing-inline-start,
52 | var(--sds-c-textarea-spacing-inline-start, 0.75rem)
53 | );
54 | background-color: var(
55 | --slds-c-textarea-color-background,
56 | var(--sds-c-textarea-color-background, #fff)
57 | );
58 | color: var(--slds-c-textarea-text-color, var(--sds-c-textarea-text-color));
59 | border: 1px solid
60 | var(
61 | --slds-c-textarea-color-border,
62 | var(--sds-c-textarea-color-border, #c9c9c9)
63 | );
64 | border-radius: var(
65 | --slds-c-textarea-radius-border,
66 | var(--sds-c-textarea-radius-border, 0.25rem)
67 | );
68 | box-shadow: var(--slds-c-textarea-shadow, var(--sds-c-textarea-shadow));
69 | resize: vertical;
70 | transition: border 0.1s linear, background-color 0.1s linear;
71 | }
72 |
73 | highlightable-input[data-theme="lightning"][aria-placeholder]::before {
74 | color: transparent;
75 | }
76 |
77 | highlightable-input[data-theme="lightning"][aria-placeholder]:empty::before {
78 | color: #747474;
79 | }
80 |
81 | highlightable-input[data-theme="lightning"]:focus {
82 | --slds-c-input-color-border: var(
83 | --slds-c-input-color-border-focus,
84 | var(--sds-c-input-color-border-focus, #1b96ff)
85 | );
86 | --slds-c-input-background-color: var(
87 | --slds-c-input-color-background-focus,
88 | var(--sds-c-input-color-background-focus, #fff)
89 | );
90 | --slds-c-input-text-color: var(
91 | --slds-c-input-text-color-focus,
92 | var(--sds-c-input-text-color-focus)
93 | );
94 | --slds-c-input-shadow: var(
95 | --slds-c-input-shadow-focus,
96 | var(--sds-c-input-shadow-focus, 0 0 3px #0176d3)
97 | );
98 | outline: 0;
99 | }
100 |
101 | highlightable-input[data-theme="lightning"][aria-multiline="true"]:focus {
102 | outline: 0;
103 | color: var(
104 | --slds-c-textarea-text-color-focus,
105 | var(--sds-c-textarea-text-color-focus)
106 | );
107 | background-color: var(
108 | --slds-c-textarea-color-background-focus,
109 | var(--sds-c-textarea-color-background-focus, #fff)
110 | );
111 | border-color: var(
112 | --slds-c-textarea-color-border-focus,
113 | var(--sds-c-textarea-color-border-focus, #1b96ff)
114 | );
115 | -webkit-box-shadow: var(
116 | --slds-c-textarea-shadow-focus,
117 | var(--sds-c-textarea-shadow-focus, 0 0 3px #0176d3)
118 | );
119 | box-shadow: var(
120 | --slds-c-textarea-shadow-focus,
121 | var(--sds-c-textarea-shadow-focus, 0 0 3px #0176d3)
122 | );
123 | }
124 |
125 | highlightable-input[data-theme="lightning"][aria-readonly="true"] {
126 | --slds-c-input-spacing-horizontal-start: 0;
127 | --slds-c-input-color-border: transparent;
128 | --slds-c-input-color-background: transparent;
129 | font-size: 0.875rem;
130 | }
131 |
132 | highlightable-input[data-theme="lightning"][aria-multiline="true"][aria-readonly="true"] {
133 | margin: 0;
134 | padding: 0;
135 | min-height: 0;
136 | line-height: normal;
137 | border: none;
138 | border-radius: 0;
139 | background: none;
140 | color: inherit;
141 | box-shadow: none;
142 | transition: none;
143 | overflow: visible;
144 | resize: none;
145 | }
146 |
147 | highlightable-input[data-theme="lightning"][aria-disabled="true"],
148 | highlightable-input[data-theme="lightning"][aria-multiline="true"][aria-disabled="true"] {
149 | background-color: #f3f3f3;
150 | border-color: #c9c9c9;
151 | color: #444;
152 | cursor: not-allowed;
153 | user-select: none;
154 | }
155 |
--------------------------------------------------------------------------------
/packages/highlightable-input/src/browser.ts:
--------------------------------------------------------------------------------
1 | let registered: boolean | null = null
2 |
3 | export function registerCustomElement(): boolean {
4 | if (registered != null) {
5 | return registered
6 | }
7 |
8 | if (
9 | typeof HTMLElement === 'undefined' ||
10 | typeof customElements === 'undefined'
11 | ) {
12 | return (registered = false)
13 | }
14 |
15 | class HighlightableInput extends HTMLElement {
16 | static formAssociated = true
17 | }
18 |
19 | if (customElements.get('highlightable-input') == null) {
20 | customElements.define('highlightable-input', HighlightableInput)
21 | }
22 |
23 | return (registered = true)
24 | }
25 |
26 | let isPlainTextSupported: boolean | null = null
27 |
28 | export function supportsPlainText() {
29 | if (isPlainTextSupported == null) {
30 | isPlainTextSupported = CSS.supports(
31 | '-webkit-user-modify',
32 | 'read-write-plaintext-only'
33 | )
34 | }
35 |
36 | return isPlainTextSupported
37 | }
38 |
39 | let isFF: boolean | null = null
40 |
41 | export function isFirefox() {
42 | if (isFF == null) {
43 | isFF = navigator.userAgent.indexOf('Firefox') !== -1
44 | }
45 |
46 | return isFF
47 | }
48 |
49 | let isCr: boolean | null = null
50 |
51 | // includes Chromium based browsers like Edge
52 | export function isChrome() {
53 | if (isCr == null) {
54 | isCr = navigator.userAgent.indexOf('Chrome') !== -1
55 | }
56 |
57 | return isCr
58 | }
59 |
60 | let isMacOS: boolean | null = null
61 |
62 | export function isMac() {
63 | if (isMacOS == null) {
64 | isMacOS = navigator.platform.indexOf('Mac') !== -1
65 | }
66 |
67 | return isMacOS
68 | }
69 |
70 | export function isMetaKey(e: KeyboardEvent) {
71 | if (isMac()) {
72 | return e.metaKey && !e.ctrlKey
73 | }
74 |
75 | return e.ctrlKey && !e.metaKey
76 | }
77 |
78 | export function isUndoShortcut(e: KeyboardEvent) {
79 | // ⌘ + Z on macOS
80 | // Ctrl + Z on windows
81 | return e.key.toUpperCase() === 'Z' && isMetaKey(e) && !e.shiftKey && !e.altKey
82 | }
83 |
84 | export function isRedoShortcut(e: KeyboardEvent) {
85 | // ⇧ + ⌘ + Z on macOS
86 | // Ctrl + Y on windows
87 | return (
88 | (isMac() &&
89 | e.key.toUpperCase() === 'Z' &&
90 | isMetaKey(e) &&
91 | e.shiftKey &&
92 | !e.altKey) ||
93 | (!isMac() &&
94 | e.key.toUpperCase() === 'Y' &&
95 | isMetaKey(e) &&
96 | !e.shiftKey &&
97 | !e.altKey)
98 | )
99 | }
100 |
101 | export function isSelectAllShortcut(e: KeyboardEvent) {
102 | return e.key.toUpperCase() === 'A' && !e.shiftKey && !e.altKey && isMetaKey(e)
103 | }
104 |
105 | // Get the user-expected text content of the element instead of the
106 | // text content actually rendered by the browser. For single-line
107 | // inputs, line breaks are replaced with spaces. For multi-line
108 | // inputs, the last line break is removed.
109 | export function getValueFromElement(el: HTMLElement, multiLine: boolean) {
110 | // Use `innerText` instead of `textContent` to get the correct
111 | // text value. In Firefox, `textContent` is not sufficient to get
112 | // the correct line breaks while editing.
113 | const text = el.innerText || ''
114 | return multiLine ? text.replace(/\n$/, '') : text.replace(/\r?\n/g, ' ')
115 | }
116 |
117 | // The highlighted HTML need to be processed for it to be
118 | // correctly rendered in the DOM.
119 | export function getHTMLToRender(html: string, multiLine: boolean) {
120 | return !multiLine || html === '' ? html : html + '\n'
121 | }
122 |
123 | export function getScrollbarSize(
124 | exemplar: HTMLElement = document.documentElement
125 | ): { width: number; height: number } {
126 | const probe = exemplar.cloneNode(false) as HTMLElement
127 |
128 | Object.assign(probe.style, {
129 | width: '100px',
130 | height: '100px',
131 | position: 'absolute',
132 | top: '0',
133 | left: '0',
134 | overflow: 'scroll',
135 | visibility: 'hidden',
136 | pointerEvents: 'none',
137 | margin: '0',
138 | padding: '0'
139 | })
140 |
141 | document.body.appendChild(probe)
142 |
143 | const {
144 | borderLeftWidth,
145 | borderRightWidth,
146 | borderTopWidth,
147 | borderBottomWidth
148 | } = getComputedStyle(probe)
149 | const borderLeft = Number.parseFloat(borderLeftWidth) || 0
150 | const borderRight = Number.parseFloat(borderRightWidth) || 0
151 | const borderTop = Number.parseFloat(borderTopWidth) || 0
152 | const borderBottom = Number.parseFloat(borderBottomWidth) || 0
153 |
154 | const width = probe.offsetWidth - probe.clientWidth - borderLeft - borderRight
155 | const height =
156 | probe.offsetHeight - probe.clientHeight - borderTop - borderBottom
157 |
158 | document.body.removeChild(probe)
159 |
160 | return {
161 | width: Math.max(0, width),
162 | height: Math.max(0, height)
163 | }
164 | }
165 |
166 | export function restoreResizing(e: MouseEvent) {
167 | if (isFirefox()) {
168 | return
169 | }
170 |
171 | const { target, clientX, clientY } = e
172 | const t = target as HTMLElement
173 | if (t.tagName.toLowerCase() !== 'highlightable-input') {
174 | return
175 | }
176 |
177 | const { right, bottom } = t.getBoundingClientRect()
178 | const handleSize = getScrollbarSize(t)
179 | if (
180 | clientX > right - handleSize.width &&
181 | clientY > bottom - handleSize.height
182 | ) {
183 | t.style.height = ''
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/packages/site/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <highlightable-input>
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
35 |
36 |
37 |
44 |
45 |
74 |
75 |
76 |
77 | Hello, @Chase! #Shepherd
80 |
81 |
82 |
83 |
84 |
85 | Hello, @Rocky! #Mixed
92 |
93 |
94 |
95 | Hello, @Skye! #Cockapoo
102 |
103 |
104 |
105 |
106 |
107 | Hello, @Marshall! #Dalmatian
115 |
116 |
117 |
118 | Hello, @Zuma! #Labrador
124 |
125 |
126 |
127 | Hello, @Rubble! #Bulldog
133 |
134 |
135 |
136 | Hello, @Everest! #Husky
144 |
145 |
146 |
147 | Hello, @Tracker! #Chihuahua
155 |
156 |
157 |
158 |
159 |
160 | You are a professional translator. Please translate the following {{from}} text into {{target}}. If user provided {{target}} text, please translate it back to {{from}}.
166 |
167 |
168 |
169 |
170 | highlightable-input[aria-disabled] mark {
176 | opacity: 0.6;
177 | }
178 |
179 | highlightable-input .link {
180 | background-color: transparent;
181 | color: rgb(29, 155, 240);
182 | font-weight: 400;
183 | cursor: pointer;
184 | }
185 |
186 | highlightable-input .link:hover {
187 | text-decoration: underline;
188 | }
189 |
190 | highlightable-input[aria-disabled="true"] .link {
191 | pointer-events: none;
192 | }
193 |
194 | highlightable-input .variable {
195 | margin: 1px;
196 | background-color: rgba(21, 94, 239, 0.05);
197 | font-weight: 500;
198 | color: rgb(28, 100, 242);
199 | }
200 |
201 | highlightable-input .variable span {
202 | opacity: 0.6;
203 | }
204 |
205 |
206 |
207 |
208 |
215 |
216 |
217 |
218 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Highlightable Input
2 |
3 | A simple yet fully stylable text field that highlights the text as you type.
4 |
5 | Live Demo
6 |
7 | ---
8 |
9 | Motivation
10 |
11 | There are two main approaches to implement a highlightable text field:
12 |
13 | 1. Use a `
20 |
21 | ## Vanilla JS
22 |
23 | ### Basic usage
24 |
25 | ```ts
26 | import { setup } from 'highlightable-input'
27 | import 'highlightable-input/style.css' // or add to `` in HTML
28 |
29 | const el = document.querySelector('highlightable-input')
30 |
31 | const input = setup(el, {
32 | highlight: [
33 | /* highlight rules */
34 | ],
35 | onInput: ({ value }) => {
36 | console.log(value)
37 | }
38 | })
39 |
40 | // Please make sure to call `destroy` when leaving current view (eg. before route change in an SPA)
41 | input.dispose()
42 | ```
43 |
44 | ```html
45 | Hello, @Ryder
48 | ```
49 |
50 | ### API types
51 |
52 | ```ts
53 | type Replacer = Parameters[1]
54 |
55 | interface HighlightRule {
56 | pattern: RegExp | string
57 | class?: string
58 | tagName?: string // default: mark
59 | style?: string
60 | replacer?: Replacer // eg. (match) => `${match}`
61 | }
62 |
63 | interface SetupOptions {
64 | defaultValue?: string
65 | highlight: HighlightRule | Array | ((value: string) => string) // use a function to fully customize the highlighting
66 | patch?: (el: HTMLElement, html: string) => void // used to customize the patching process, set `innerHTML` by default
67 | onInput?: ({ value }: { value: string; position: number }) => void
68 | }
69 |
70 | interface SelectOptions {
71 | force?: boolean
72 | collapse?: 'start' | 'end' | false
73 | }
74 |
75 | type SelectOffsets = [number, number] | number | true
76 |
77 | declare function setup(
78 | el: HTMLElement,
79 | { onInput, highlight, patch }: SetupOptions
80 | ): {
81 | getValue(): string | null // get the value of the input
82 | setValue(value: string): void // set the text value of the input
83 | setSelection(offsets: SelectOffsets, options?: SelectOptions): void // set the selection offsets of the text content
84 | getSelection(): [number, number] // get the current selection offsets of the text content
85 | valueToRawHTML(value: string): string // convert text value to HTML according to the highlight rules
86 | dispose: () => void // to release global event listeners and inactivate the element
87 | refresh(): void // re-initialize the element
88 | }
89 | ```
90 |
91 | > **Note**
92 | >
93 | > The `highlight` function or the `replacer` option shouldn't change the length of the text (only wrap text with HTML tags).
94 |
95 | ### Attributes
96 |
97 | The `setup` function will respect certain attributes on the element. As we are not using the built-in `` or `