#bookmark
(function() {
'use strict';
const sub = window.SITE_SUBDOMAIN;
// 1. UI INJECTION (All tools included)
const uiWrap = document.createElement('div');
uiWrap.id = "cms-editor-wrapper";
uiWrap.innerHTML = `
<div id="sel-tb" class="sel-tb">
<button data-cmd="bold"><span class="material-icons">format_bold</span></button>
<button data-cmd="italic"><span class="material-icons">format_italic</span></button>
<button data-cmd="underline"><span class="material-icons">format_underlined</span></button>
<button id="add-lnk"><span class="material-icons">link</span></button>
<button id="add-id"><span class="material-icons">fingerprint</span></button>
<div class="ctrl-grp"><input type="color" id="clr-txt" value="#000000"></div>
<div class="ctrl-grp"><input type="color" id="clr-bg" value="#ffff00"></div>
</div>
<div id="plus-btn"><span class="material-icons">add</span></div>
<div id="ins-m" class="ins-m">
<div class="group"><div class="g-title">Text Blocks</div><div class="g-row">
<div data-b="H1"><span class="material-icons">looks_one</span><label>H1</label></div>
<div data-b="H2"><span class="material-icons">looks_two</span><label>H2</label></div>
<div data-b="H3"><span class="material-icons">looks_3</span><label>H3</label></div>
<div data-b="H4"><span class="material-icons">looks_4</span><label>H4</label></div>
<div data-b="P"><span class="material-icons">subject</span><label>P</label></div>
<div id="do-ul"><span class="material-icons">format_list_bulleted</span><label>List</label></div>
</div></div>
<div class="group"><div class="g-title">Media</div><div class="g-row">
<div id="do-img"><span class="material-icons">image</span><label>Img/Sticker</label></div>
<div id="do-vid"><span class="material-icons">smart_display</span><label>Video</label></div>
</div></div>
<div class="group"><div class="g-title">Layout</div><div class="g-row">
<div data-g="2"><span class="material-icons">view_week</span><label>2 Col</label></div>
<div data-g="3"><span class="material-icons">view_column</span><label>3 Col</label></div>
<div data-g="4"><span class="material-icons">view_module</span><label>4 Col</label></div>
<div data-g="6"><span class="material-icons">grid_view</span><label>6 Col</label></div>
<div data-g="8"><span class="material-icons">grid_on</span><label>8 Col</label></div>
</div></div>
</div>
<input type="file" id="f-sys" style="display:none" accept="image/*,image/avif">
`;
document.body.appendChild(uiWrap);
const viewport = document.querySelector('.viewport') || document.querySelector('.editor');
const selTb = document.getElementById('sel-tb');
const plusBtn = document.getElementById('plus-btn');
const insMenu = document.getElementById('ins-m');
const fSys = document.getElementById('f-sys');
let activeRange = null, activeBlock = null;
const autoSave = () => { if(window.performAutoSave) window.performAutoSave(); };
// --- UPLOAD HELPER ---
async function uploadToServer(file) {
const fd = new FormData(); fd.append('file', file);
try {
const r = await fetch(`https://madenp.com/upload.php?action=upload&subdomain=${sub}`, { method: 'POST', body: fd, credentials: 'include' });
const d = await r.json();
return d.success ? `https://madenp.com/media.php?s=${sub}&f=${d.filename}` : null;
} catch (e) { return null; }
}
// --- RESPONSIVE TRANSFORM ENGINE ---
const applyTransform = (el) => {
el.style.position = 'absolute';
el.style.left = el.dataset.x + "%";
el.style.top = el.dataset.y + "%";
// translate(-50%, -50%) ensures the CENTER of the image stays on the % coordinate
el.style.transform = `translate(-50%, -50%) scale(${el.dataset.scale}) rotate(${el.dataset.rotate}deg)`;
el.style.margin = "0";
el.classList.add('is-active');
};
const activateSticker = (sticker) => {
if (sticker.classList.contains('is-active')) return;
// Find the nearest parent section/article to anchor to
const parent = sticker.closest('section, article, .row, .viewport, .editor');
parent.style.position = 'relative';
const pRect = parent.getBoundingClientRect();
const sRect = sticker.getBoundingClientRect();
// Calculate center relative to parent in percentages
const centerX = (sRect.left + sRect.width / 2) - pRect.left;
const centerY = (sRect.top + sRect.height / 2) - pRect.top;
sticker.dataset.x = ((centerX / pRect.width) * 100).toFixed(2);
sticker.dataset.y = ((centerY / pRect.height) * 100).toFixed(2);
sticker.dataset.scale = sticker.dataset.scale || "1";
sticker.dataset.rotate = sticker.dataset.rotate || "0";
applyTransform(sticker);
};
document.addEventListener('mousedown', (e) => {
const sticker = e.target.closest('.gif-sticker');
if (!sticker) return;
activateSticker(sticker);
const parent = sticker.parentElement;
const isResizer = e.target.classList.contains('resizer');
const isRotator = e.target.closest('.rotator');
if (isResizer) {
const onResize = (ev) => {
let scale = parseFloat(sticker.dataset.scale);
scale += ev.movementX * 0.005;
sticker.dataset.scale = Math.max(0.1, scale).toFixed(3);
applyTransform(sticker);
};
document.addEventListener('mousemove', onResize);
document.addEventListener('mouseup', () => { document.removeEventListener('mousemove', onResize); autoSave(); }, {once:true});
}
else if (isRotator) {
const onRotate = (ev) => {
let r = parseFloat(sticker.dataset.rotate);
r += ev.movementX;
sticker.dataset.rotate = r.toFixed(1);
applyTransform(sticker);
};
document.addEventListener('mousemove', onRotate);
document.addEventListener('mouseup', () => { document.removeEventListener('mousemove', onRotate); autoSave(); }, {once:true});
}
else {
const onMove = (ev) => {
const pRect = parent.getBoundingClientRect();
let x = ((ev.clientX - pRect.left) / pRect.width) * 100;
let y = ((ev.clientY - pRect.top) / pRect.height) * 100;
sticker.dataset.x = x.toFixed(2);
sticker.dataset.y = y.toFixed(2);
applyTransform(sticker);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', () => { document.removeEventListener('mousemove', onMove); autoSave(); }, {once:true});
}
e.preventDefault();
});
// --- EDITOR UI LOGIC ---
viewport.addEventListener('mousemove', (e) => {
const block = e.target.closest('.viewport > *, .col > *, h1, h2, h3, h4, p, img, .row, section, article');
if (block && viewport.contains(block) && !block.classList.contains('gif-sticker')) {
activeBlock = block;
const rect = block.getBoundingClientRect();
plusBtn.style.display = 'flex';
plusBtn.style.top = `${rect.bottom + window.scrollY - 15}px`;
plusBtn.style.left = `${rect.left + window.scrollX + (rect.width / 2)}px`;
}
});
plusBtn.onclick = (e) => {
e.stopPropagation();
insMenu.style.display = 'block';
insMenu.style.left = plusBtn.style.left; insMenu.style.top = plusBtn.style.top;
};
const insertAfter = (html) => {
if (activeBlock) activeBlock.insertAdjacentHTML('afterend', html);
else viewport.insertAdjacentHTML('beforeend', html);
insMenu.style.display = 'none'; autoSave();
};
// --- TOOL ACTIONS ---
insMenu.querySelectorAll('[data-b]').forEach(i => {
i.onclick = () => insertAfter(`<${i.dataset.b.toLowerCase()}>New Block</${i.dataset.b.toLowerCase()}>`);
});
document.getElementById('do-ul').onclick = () => insertAfter('<ul><li>List Item</li></ul>');
insMenu.querySelectorAll('[data-g]').forEach(i => {
i.onclick = () => {
const c = parseInt(i.dataset.g);
let h = `<div class="row ${c > 4 ? 'mobile-overflow' : ''}" contenteditable="false">`;
for(let j=0; j<c; j++) h += `<div class="col" contenteditable="true"><p>Column ${j+1}</p></div>`;
h += `</div><p><br></p>`;
insertAfter(h);
};
});
// --- MEDIA ---
document.getElementById('do-img').onclick = () => fSys.click();
fSys.onchange = async function() {
const file = this.files[0]; if(!file) return;
const url = await uploadToServer(file);
if(!url) return;
const isSticker = file.name.match(/\.(gif|avif)$/i) || file.type.match(/(gif|avif)/i);
if(isSticker) {
// STEP 1: Insert as normal block (width is set smaller to start)
insertAfter(`
<div class="gif-sticker" style="position:relative; display:block; margin: 20px auto; width: 180px; cursor: pointer; z-index:10;" contenteditable="false">
<img src="${url}" style="width:100%; display:block; pointer-events:none;">
<div class="gif-handle rotator"><span class="material-icons">cached</span></div>
<div class="gif-handle resizer"></div>
</div>
`);
} else {
insertAfter(`<img src="${url}" class="standard-img">`);
}
this.value = ''; autoSave();
};
document.getElementById('do-vid').onclick = () => {
const url = prompt("YouTube URL:"); if (!url) return;
let id = url.includes('v=') ? url.split('v=')[1].split('&')[0] : url.split('/').pop();
if (id) insertAfter(`<div class="v-w" contenteditable="false"><iframe src="https://www.youtube.com/embed/${id}" allowfullscreen></iframe></div><p><br></p>`);
};
const run = (c, v = null) => {
if (activeRange) { const s = window.getSelection(); s.removeAllRanges(); s.addRange(activeRange); }
document.execCommand(c, false, v); autoSave();
};
document.addEventListener('selectionchange', () => {
const sel = window.getSelection();
if (sel.rangeCount > 0 && !sel.isCollapsed && viewport.contains(sel.anchorNode)) {
activeRange = sel.getRangeAt(0).cloneRange();
const rect = activeRange.getBoundingClientRect();
selTb.classList.add('active');
selTb.style.left = `${rect.left + window.scrollX}px`;
selTb.style.top = `${rect.top + window.scrollY - 50}px`;
} else if (!selTb.matches(':hover')) selTb.classList.remove('active');
});
selTb.querySelectorAll('[data-cmd]').forEach(b => b.onmousedown = (e) => { e.preventDefault(); run(b.dataset.cmd); });
document.getElementById('add-lnk').onmousedown = (e) => { e.preventDefault(); const u = prompt("URL:"); if(u) run('createLink', u); };
document.getElementById('add-id').onmousedown = (e) => { e.preventDefault(); const id = prompt("Block ID:"); if (id && activeBlock) activeBlock.id = id; };
document.getElementById('clr-txt').oninput = (e) => run('foreColor', e.target.value);
document.getElementById('clr-bg').oninput = (e) => run('hiliteColor', e.target.value);
document.addEventListener('mousedown', (e) => { if (!insMenu.contains(e.target) && e.target !== plusBtn) insMenu.style.display = 'none'; });
if(viewport) viewport.contentEditable = "true";
})();
Summary: Building a Responsive “Adaptive Sticker” Engine
In this session, we evolved a standard WYSIWYG editor into a high-end layout builder by solving the biggest problem with “draggable” elements: Responsive Breakdown.
1. The “Two-Step Activation” Workflow (UX)
Standard editors either force an image to be a “block” (static) or a “sticker” (floating). We created a hybrid approach:
- Step 1 (The Ghost Block): When a user clicks the “Plus” button, the GIF is inserted as a standard HTML element in the natural flow of the document (inside the current <section> or <article>). This ensures the content doesn’t “jump” and stays exactly where the user intended.
- Step 2 (The Activation): The element only becomes “Absolute” the moment the user interacts with it (Drag, Resize, or Rotate). This keeps the DOM clean and prevents layout shifts during the initial build.
2. Section-Bound Containment (Clean HTML Structure)
One major challenge was preventing absolute-positioned stickers from flying out of their parent containers.
- The Logic: Upon activation, the engine identifies the nearest parent <section> or <article> and sets it to position: relative.
- The Result: The sticker is now “trapped” within its specific section. If you move a section up or down in your CMS, the sticker travels with its section because its coordinates are local to that parent, not the whole page.
3. Percentage-Based Geometry (100% Responsive)
Pixel-based positioning (top: 200px) is the enemy of mobile design. We moved to a Relative Coordinate System:
- Storage: We store the position as data-x and data-y in percentages (0% to 100%).
- Visual Consistency: Whether the screen is 1920px wide or 375px wide, the sticker stays at the exact same relative spot (e.g., “Top Right of Section 2”).
- The Math: By using translate(-50%, -50%), we moved the “Pivot Point” to the true center of the image. This makes rotation and scaling mathematically perfect regardless of the image aspect ratio.
4. The Transformation Engine
We built a unified applyTransform function that handles three complex states simultaneously:
- Scale: Uses multipliers instead of width/height px to preserve image quality.
- Rotation: Maintains degrees even after the mouse is released.
- Translation: Keeps the image centered on its coordinate.
🧠 The Technical “Aha!” Moment:
Instead of trying to make the editor behave like a Word document, we made it behave like a Design Canvas. By calculating coordinates as (Mouse Position – Parent Offset) / Parent Width, we created a builder that looks the same on a 4K monitor as it does on an iPhone.