Created
February 28, 2026 21:58
-
-
Save EncodeTheCode/2966b0564cdcc20655863365be8244a4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Mock ID — Names Letters Only + Editable Meta (Demo)</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=Roboto+Slab:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --card-w: 820px; | |
| --card-h: 420px; | |
| --muted: #5b6b70; | |
| --accent: #0a6fb1; | |
| --photo-w: 140px; | |
| --photo-h: 150px; | |
| --padding: 20px; | |
| } | |
| *{box-sizing:border-box} | |
| body{ | |
| font-family: 'Inter', system-ui, Roboto, Arial; | |
| margin:0; | |
| min-height:100vh; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| background: linear-gradient(180deg,#f6fbff 0%, #eef6fb 100%); | |
| padding:28px; | |
| color:#07202a; | |
| } | |
| .container{ display:flex; gap:28px; align-items:flex-start; max-width:1400px; width:100%; justify-content:center; flex-wrap:wrap; } | |
| /* Card */ | |
| .id-card{ width:var(--card-w); height:var(--card-h); border-radius:12px; padding:var(--padding); position:relative; | |
| background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(245,250,255,0.94)); | |
| box-shadow: 0 14px 44px rgba(8,30,40,0.10); border:1px solid rgba(6,32,40,0.06); overflow:hidden; } | |
| .id-card::after{ | |
| content:"DEMO — NOT A GOVERNMENT DOCUMENT"; | |
| position:absolute; left:-6%; top:54%; transform:rotate(-22deg); font-weight:900; font-size:26px; | |
| color:rgba(7,32,40,0.05); letter-spacing:6px; pointer-events:none; white-space:nowrap; | |
| } | |
| .id-top{ display:flex; align-items:center; gap:12px; } | |
| .brand{ width:110px; height:48px; border-radius:6px; background:linear-gradient(90deg,#0a6fb1,#39a7d8); | |
| color:white; display:flex; align-items:center; justify-content:center; font-weight:800; letter-spacing:1px; font-size:13px; } | |
| .title{ font-family: 'Roboto Slab', serif; font-size:18px; margin:0; } | |
| .subtitle{ font-size:12px; color:var(--muted); margin-top:4px; } | |
| .main{ display:flex; gap:18px; margin-top:16px; align-items:flex-start; padding-right: calc(var(--photo-w) + 36px); } | |
| .fields{ flex:1; display:flex; flex-direction:column; gap:12px; } | |
| .label{ font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.6px; margin-bottom:6px; } | |
| .name-row{ display:flex; gap:12px; align-items:center; } | |
| .name-field{ | |
| min-height:52px; border-radius:6px; padding:10px 12px; font-family:'Roboto Slab', serif; font-size:20px; font-weight:700; | |
| color:#05202a; line-height:1.05; outline:none; background:rgba(10,107,177,0.02); border:1px solid rgba(6,32,40,0.045); | |
| cursor:text; user-select:text; white-space:nowrap; overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch; text-overflow:clip; | |
| } | |
| .given{ flex: 0 0 46ch; max-width:70%; } /* visually large to show 30 chars */ | |
| .family{ flex: 1 1 auto; min-width:12ch; } | |
| .name-field[contenteditable="true"]:empty::before{ content:attr(data-placeholder); color:var(--muted); font-weight:400; } | |
| .meta-row{ margin-top:8px; display:flex; gap:12px; align-items:center; flex-wrap:wrap; } | |
| /* meta editable chips */ | |
| .meta-chip{ | |
| padding:8px 12px; border-radius:8px; font-size:13px; color:var(--muted); border:1px solid rgba(6,32,40,0.04); | |
| min-width:110px; text-align:center; background:rgba(250,250,250,0.6); | |
| } | |
| .meta-chip[contenteditable="true"]{ background: rgba(10,107,177,0.03); cursor:text; } | |
| .photo { width:var(--photo-w); height:var(--photo-h); border-radius:6px; background:linear-gradient(180deg,#f0f7fb,#dfeff8); | |
| border:1px dashed rgba(6,32,40,0.06); display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:12px; | |
| position:absolute; right:var(--padding); bottom:var(--padding); box-shadow: 0 6px 18px rgba(12,40,60,0.03); z-index:10; } | |
| /* Controls */ | |
| .controls{ width:360px; display:flex; flex-direction:column; gap:12px; } | |
| .hidden-input{ width:100%; padding:10px 12px; border-radius:8px; border:1px solid rgba(6,32,40,0.06); font-size:15px; box-sizing:border-box; } | |
| .hidden-input[type="date"]{ padding:8px 12px; } | |
| .counter{ font-size:13px; color:var(--muted); text-align:right; } | |
| .note{ font-size:12px; color:#7a8a90; } | |
| button{ padding:10px 12px; border-radius:8px; border:1px solid rgba(6,32,40,0.06); background:white; cursor:pointer; font-size:14px; } | |
| .name-field::-webkit-scrollbar { height:8px; } | |
| .name-field::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.08); border-radius:8px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container" role="main"> | |
| <section class="id-card" aria-label="Sample identification card (mock)"> | |
| <div class="id-top"> | |
| <div class="brand" aria-hidden="true">ID DEMO</div> | |
| <div style="flex:1;"> | |
| <h3 class="title">Sample Identification</h3> | |
| <div class="subtitle">Prototype — clearly labeled sample (not official)</div> | |
| </div> | |
| </div> | |
| <div class="main" style="align-items:flex-start;"> | |
| <div class="fields"> | |
| <div class="label">Name</div> | |
| <div class="name-row" aria-hidden="false"> | |
| <div id="givenNames" class="name-field given" contenteditable="true" role="textbox" | |
| aria-label="Given names editable" spellcheck="false" data-placeholder="Given names">Example</div> | |
| <div id="familyName" class="name-field family" contenteditable="true" role="textbox" | |
| aria-label="Family name editable" spellcheck="false" data-placeholder="Family name">Person</div> | |
| </div> | |
| <div class="meta-row" style="margin-top:14px;"> | |
| <div> | |
| <div class="label" style="margin:0 0 6px 0;">ID no.</div> | |
| <div id="idNumber" class="meta-chip" contenteditable="true" role="textbox" aria-label="ID number editable">DEMO-000-000</div> | |
| </div> | |
| <div> | |
| <div class="label" style="margin:0 0 6px 0;">DOB</div> | |
| <div id="dobCard" class="meta-chip" contenteditable="true" role="textbox" aria-label="Date of birth editable">YYYY-MM-DD</div> | |
| </div> | |
| <div> | |
| <div class="label" style="margin:0 0 6px 0;">Expires</div> | |
| <div id="expCard" class="meta-chip" contenteditable="true" role="textbox" aria-label="Expiration date editable">YYYY-MM-DD</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="photo" aria-hidden="true">Photo<br>Placeholder</div> | |
| <div style="position:absolute; right:22px; bottom:calc(var(--photo-h) + 32px); font-size:12px; color:rgba(7,32,40,0.28);"> | |
| DEMO • NOT A GOVERNMENT DOCUMENT | |
| </div> | |
| </section> | |
| <!-- Controls --> | |
| <aside class="controls" aria-label="Controls for editing"> | |
| <div> | |
| <div class="label" style="margin:0 0 6px 0;">Edit names and meta (click card or use inputs)</div> | |
| <!-- name mirrors --> | |
| <input id="givenInput" class="hidden-input" type="text" maxlength="30" aria-label="Given names input" placeholder="Given names (letters & spaces only, max 30)" /> | |
| <input id="familyInput" class="hidden-input" type="text" maxlength="30" aria-label="Family name input" placeholder="Family name (letters & spaces only, max 30)" style="margin-top:8px" /> | |
| <!-- editable meta --> | |
| <input id="idInput" class="hidden-input" type="text" aria-label="ID number input" placeholder="ID number (any chars allowed)" style="margin-top:8px" /> | |
| <label style="font-size:12px;color:var(--muted);margin-top:8px">Date of birth</label> | |
| <input id="dobInput" class="hidden-input" type="date" aria-label="Date of birth input" /> | |
| <label style="font-size:12px;color:var(--muted);margin-top:8px">Expires</label> | |
| <input id="expInput" class="hidden-input" type="date" aria-label="Expiration date input" /> | |
| <div class="counter" id="counter">Given: 0/30 • Family: 0/30</div> | |
| <div class="note" style="margin-top:8px;">Name fields accept **letters and spaces only**. No digits/symbols. Dates & ID no. are editable below.</div> | |
| </div> | |
| <div> | |
| <button id="resetBtn">Reset example</button> | |
| </div> | |
| </aside> | |
| </div> | |
| <script> | |
| /****************************** | |
| * Configuration | |
| ******************************/ | |
| const GIVEN_MAX = 30; | |
| const FAMILY_MAX = 30; | |
| /* Elements */ | |
| const givenEl = document.getElementById('givenNames'); | |
| const familyEl = document.getElementById('familyName'); | |
| const givenInput = document.getElementById('givenInput'); | |
| const familyInput = document.getElementById('familyInput'); | |
| const counter = document.getElementById('counter'); | |
| const idCard = document.getElementById('idNumber'); | |
| const idInput = document.getElementById('idInput'); | |
| const dobCard = document.getElementById('dobCard'); | |
| const expCard = document.getElementById('expCard'); | |
| const dobInput = document.getElementById('dobInput'); | |
| const expInput = document.getElementById('expInput'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| /****************************** | |
| * Helpers: character rules | |
| ******************************/ | |
| // Allow Unicode letters and space only (no numbers, no punctuation) | |
| // Use Unicode property \p{L} to allow letters from any language, with the 'u' flag. | |
| const allowedNameRegex = /^\p{L}+(?:[ ]\p{L}+)*$/u; // full-match for validation of full string (one or more names separated by single spaces) | |
| // For incremental checks we'll test proposed insert text: must contain only letters or spaces | |
| const allowedInsertedCharsRegex = /^[\p{L} ]+$/u; | |
| function collapseSpacesPreserveEdges(s){ | |
| return (s || '').replace(/\u00A0/g, ' ').replace(/\s+/g, ' '); | |
| } | |
| function collapseAndTrim(s){ | |
| return collapseSpacesPreserveEdges(s).trim(); | |
| } | |
| function visibleLength(s){ | |
| return collapseAndTrim(s).length; | |
| } | |
| function updateCounter(){ | |
| const givLen = visibleLength(givenEl.innerText); | |
| const famLen = visibleLength(familyEl.innerText); | |
| counter.textContent = `Given: ${givLen}/${GIVEN_MAX} • Family: ${famLen}/${FAMILY_MAX}`; | |
| counter.style.color = (givLen > GIVEN_MAX || famLen > FAMILY_MAX) ? 'crimson' : ''; | |
| } | |
| /****************************** | |
| * Selection / caret helpers | |
| ******************************/ | |
| function getSelectionRangeWithin(el){ | |
| const sel = window.getSelection(); | |
| if (!sel.rangeCount) return { start:0, end:0 }; | |
| const range = sel.getRangeAt(0); | |
| const pre = range.cloneRange(); | |
| pre.selectNodeContents(el); | |
| pre.setEnd(range.startContainer, range.startOffset); | |
| const start = pre.toString().length; | |
| const pre2 = range.cloneRange(); | |
| pre2.selectNodeContents(el); | |
| pre2.setEnd(range.endContainer, range.endOffset); | |
| const end = pre2.toString().length; | |
| return { start, end }; | |
| } | |
| function setCaretAt(el, chars){ | |
| if (chars < 0) chars = 0; | |
| el.focus(); | |
| const range = document.createRange(); | |
| const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); | |
| let node; | |
| let charsLeft = chars; | |
| let found = false; | |
| while ((node = walker.nextNode())) { | |
| if (node.nodeValue.length >= charsLeft){ | |
| range.setStart(node, charsLeft); | |
| range.collapse(true); | |
| found = true; | |
| break; | |
| } else { | |
| charsLeft -= node.nodeValue.length; | |
| } | |
| } | |
| if (!found){ | |
| range.selectNodeContents(el); | |
| range.collapse(false); | |
| } | |
| const sel = window.getSelection(); | |
| sel.removeAllRanges(); | |
| sel.addRange(range); | |
| } | |
| function insertTextAtCaret(el, text){ | |
| const sel = window.getSelection(); | |
| if (!sel.rangeCount){ | |
| el.focus(); | |
| el.innerText = (el.innerText || '') + text; | |
| setCaretAt(el, (el.innerText || '').length); | |
| return; | |
| } | |
| const range = sel.getRangeAt(0); | |
| if (!el.contains(range.commonAncestorContainer)){ | |
| setCaretAt(el, (el.innerText || '').length); | |
| } | |
| const r = window.getSelection().getRangeAt(0); | |
| r.deleteContents(); | |
| const node = document.createTextNode(text); | |
| r.insertNode(node); | |
| r.setStartAfter(node); | |
| r.collapse(true); | |
| sel.removeAllRanges(); | |
| sel.addRange(r); | |
| } | |
| /****************************** | |
| * Name field handlers | |
| ******************************/ | |
| // beforeinput: check insertion (single characters or insertion text) for allowed chars and max length | |
| function nameBeforeInput(e, ownEl, ownMax){ | |
| // Only intercept insertions with data (typing and some composition events) | |
| if (!e.inputType) return; | |
| if (e.inputType.startsWith('insert') && e.data !== null){ | |
| const inserted = e.data; | |
| // allow composition sequences (insertCompositionText) to pass through; final validation occurs in input | |
| if (e.inputType === 'insertCompositionText') return; | |
| // If inserted contains disallowed characters -> block | |
| if (!allowedInsertedCharsRegex.test(inserted)){ | |
| e.preventDefault(); | |
| return; | |
| } | |
| // Check length with selection replacement taken into account | |
| const { start, end } = getSelectionRangeWithin(ownEl); | |
| const current = ownEl.innerText || ''; | |
| // proposed visible string (collapse & trim for counting) | |
| const proposed = current.slice(0, start) + inserted + current.slice(end); | |
| if (visibleLength(proposed) > ownMax){ | |
| e.preventDefault(); | |
| return; | |
| } | |
| } | |
| // other inputTypes allowed; final validation will happen on input event | |
| } | |
| // paste: filter allowed chars and insert up to remaining length | |
| function namePaste(e, ownEl, ownMax){ | |
| e.preventDefault(); | |
| const raw = (e.clipboardData || window.clipboardData).getData('text') || ''; | |
| if (!raw) return; | |
| // collapse whitespace in pasted text to single spaces (keeps UX clean), preserve letters | |
| let pasted = raw.replace(/\u00A0/g, ' ').replace(/\s+/g, ' '); | |
| // filter out any character that isn't a letter or space | |
| pasted = Array.from(pasted).filter(ch => { | |
| return /^[\p{L} ]$/u.test(ch); | |
| }).join(''); | |
| if (!pasted) return; | |
| // insert only allowed portion given current visible length and selection | |
| const currentVisible = visibleLength(ownEl.innerText); | |
| const sel = getSelectionRangeWithin(ownEl); | |
| // selection replacement frees up selection length | |
| const selectionLen = sel.end - sel.start; | |
| const remaining = Math.max(0, ownMax - currentVisible + selectionLen); | |
| if (remaining <= 0) return; | |
| const allowedText = pasted.slice(0, remaining); | |
| insertTextAtCaret(ownEl, allowedText); | |
| // keep caret visible | |
| setTimeout(()=> { ownEl.scrollLeft = ownEl.scrollWidth; }, 0); | |
| syncNameToInput(ownEl); | |
| updateCounter(); | |
| } | |
| // input event: final safety; remove any disallowed chars (preserve caret as best as possible) and trim to max | |
| function nameInput(ownEl, ownMax, mirrorInput){ | |
| const raw = ownEl.innerText || ''; | |
| // preserve leading/trailing spaces while cleaning internal characters | |
| const edges = raw.match(/^(\s*)([\s\S]*?)(\s*)$/); | |
| const leading = edges ? edges[1] : ''; | |
| const middle = edges ? edges[2] : raw; | |
| const trailing = edges ? edges[3] : ''; | |
| // collapse multiple whitespace inside for tidy internal behavior while typing we allow them until blur, | |
| // but here we will only remove disallowed characters | |
| const cleanedMiddle = Array.from(middle).filter(ch => /^[\p{L} ]$/u.test(ch)).join(''); | |
| let newVal = (leading + cleanedMiddle + trailing) || '\u00A0'; | |
| // if visible length > ownMax, trim visible content (trimmed middle) to fit | |
| const visible = collapseAndTrim(newVal); | |
| if (visible.length > ownMax){ | |
| const allowed = ownMax; | |
| // produce a trimmed visible string | |
| const trimmedVisible = visible.slice(0, allowed); | |
| newVal = trimmedVisible || '\u00A0'; | |
| } | |
| // If changed, update element while attempting to keep caret near previous position | |
| if (newVal !== raw){ | |
| // compute previous caret index and attempt to map it | |
| const sel = getSelectionRangeWithin(ownEl); | |
| const prevPos = sel.end; | |
| ownEl.innerText = newVal; | |
| // place caret at min(prevPos, newVal.length) | |
| setCaretAt(ownEl, Math.min(prevPos, (ownEl.innerText || '').length)); | |
| } | |
| // mirror to input (trimmed) | |
| mirrorInput.value = collapseAndTrim(ownEl.innerText); | |
| updateCounter(); | |
| } | |
| function syncNameToInput(ownEl){ | |
| if (ownEl === givenEl) givenInput.value = collapseAndTrim(givenEl.innerText); | |
| else familyInput.value = collapseAndTrim(familyEl.innerText); | |
| } | |
| // mirror input -> contenteditable (enforce only letters/spaces and maxlength) | |
| function mirrorNameInput(mirrorEl, ownEl, ownMax){ | |
| let v = mirrorEl.value || ''; | |
| // collapse multiple spaces, then remove disallowed chars | |
| v = v.replace(/\u00A0/g, ' ').replace(/\s+/g, ' '); | |
| v = Array.from(v).filter(ch => /^[\p{L} ]$/u.test(ch)).join(''); | |
| if (v.trim().length > ownMax) v = v.trim().slice(0, ownMax); | |
| mirrorEl.value = v; | |
| ownEl.innerText = v || '\u00A0'; | |
| updateCounter(); | |
| } | |
| /****************************** | |
| * ID / date fields handlers | |
| ******************************/ | |
| // id number: free-form. Mirror both ways. | |
| function syncIdToInput(){ idInput.value = idCard.innerText.trim(); } | |
| function syncIdFromInput(){ idCard.innerText = idInput.value || '\u00A0'; } | |
| // dates: when date input changes, update card display with YYYY-MM-DD (if value set) | |
| function formatDateForCard(dateValue){ | |
| // dateValue is value from input[type=date] in YYYY-MM-DD or empty | |
| return dateValue || 'YYYY-MM-DD'; | |
| } | |
| function syncDobFromInput(){ dobCard.innerText = formatDateForCard(dobInput.value); } | |
| function syncExpFromInput(){ expCard.innerText = formatDateForCard(expInput.value); } | |
| // when user edits the card date text itself, if it matches YYYY-MM-DD update the date input | |
| function syncDobFromCard(){ | |
| const txt = dobCard.innerText.trim(); | |
| if (/^\d{4}-\d{2}-\d{2}$/.test(txt)){ | |
| // validate date correctness loosely by attempting Date parse | |
| const d = new Date(txt + 'T00:00:00'); | |
| if (!isNaN(d.getTime())){ | |
| dobInput.value = txt; | |
| } | |
| } | |
| } | |
| function syncExpFromCard(){ | |
| const txt = expCard.innerText.trim(); | |
| if (/^\d{4}-\d{2}-\d{2}$/.test(txt)){ | |
| const d = new Date(txt + 'T00:00:00'); | |
| if (!isNaN(d.getTime())){ | |
| expInput.value = txt; | |
| } | |
| } | |
| } | |
| /****************************** | |
| * Init & event wiring | |
| ******************************/ | |
| function init(){ | |
| givenEl.innerText = 'Example'; | |
| familyEl.innerText = 'Person'; | |
| givenInput.value = 'Example'; | |
| familyInput.value = 'Person'; | |
| idCard.innerText = 'DEMO-000-000'; | |
| idInput.value = 'DEMO-000-000'; | |
| dobCard.innerText = 'YYYY-MM-DD'; | |
| expCard.innerText = 'YYYY-MM-DD'; | |
| dobInput.value = ''; | |
| expInput.value = ''; | |
| updateCounter(); | |
| } | |
| // Name field events | |
| givenEl.addEventListener('beforeinput', (e)=> nameBeforeInput(e, givenEl, GIVEN_MAX)); | |
| familyEl.addEventListener('beforeinput', (e)=> nameBeforeInput(e, familyEl, FAMILY_MAX)); | |
| givenEl.addEventListener('paste', (e)=> namePaste(e, givenEl, GIVEN_MAX)); | |
| familyEl.addEventListener('paste', (e)=> namePaste(e, familyEl, FAMILY_MAX)); | |
| givenEl.addEventListener('input', ()=> nameInput(givenEl, GIVEN_MAX, givenInput)); | |
| familyEl.addEventListener('input', ()=> nameInput(familyEl, FAMILY_MAX, familyInput)); | |
| givenEl.addEventListener('blur', ()=> { givenEl.innerText = collapseAndTrim(givenEl.innerText) || '\u00A0'; syncNameToInput(givenEl); updateCounter(); }); | |
| familyEl.addEventListener('blur', ()=> { familyEl.innerText = collapseAndTrim(familyEl.innerText) || '\u00A0'; syncNameToInput(familyEl); updateCounter(); }); | |
| givenInput.addEventListener('input', ()=> mirrorNameInput(givenInput, givenEl, GIVEN_MAX)); | |
| familyInput.addEventListener('input', ()=> mirrorNameInput(familyInput, familyEl, FAMILY_MAX)); | |
| // ensure focusing mirror focuses card for UX | |
| givenInput.addEventListener('focus', ()=> { givenEl.focus(); setTimeout(()=> { givenEl.scrollLeft = givenEl.scrollWidth; }, 0); }); | |
| familyInput.addEventListener('focus', ()=> { familyEl.focus(); setTimeout(()=> { familyEl.scrollLeft = familyEl.scrollWidth; }, 0); }); | |
| // ID fields | |
| idCard.addEventListener('input', syncIdToInput); | |
| idInput.addEventListener('input', syncIdFromInput); | |
| // DOB / expires: wire both ways | |
| dobInput.addEventListener('input', syncDobFromInput); | |
| expInput.addEventListener('input', syncExpFromInput); | |
| dobCard.addEventListener('input', ()=> { syncDobFromCard(); }); | |
| expCard.addEventListener('input', ()=> { syncExpFromCard(); }); | |
| // reset | |
| resetBtn.addEventListener('click', init); | |
| // small helper for caret set (used by other code) | |
| function setCaretAt(el, chars){ | |
| if (chars < 0) chars = 0; | |
| el.focus(); | |
| const range = document.createRange(); | |
| const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); | |
| let node; | |
| let charsLeft = chars; | |
| let found = false; | |
| while ((node = walker.nextNode())) { | |
| if (node.nodeValue.length >= charsLeft){ | |
| range.setStart(node, charsLeft); | |
| range.collapse(true); | |
| found = true; | |
| break; | |
| } else { | |
| charsLeft -= node.nodeValue.length; | |
| } | |
| } | |
| if (!found){ | |
| range.selectNodeContents(el); | |
| range.collapse(false); | |
| } | |
| const sel = window.getSelection(); | |
| sel.removeAllRanges(); | |
| sel.addRange(range); | |
| } | |
| // init on load | |
| init(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment