diff --git a/index.html b/index.html new file mode 100644 index 0000000..1768752 --- /dev/null +++ b/index.html @@ -0,0 +1,95 @@ + + + + + + Vanilla WYSIWYG Editor + + + + +
+
+
+
+
+

Bootstrap 5.3.3 WYSIWYG, No JQuery

+
+
    +
  • +
    + + + + +
    +
  • +
  • +
    +
    + + + +
    +
    + + + +
    +
    + + + + +
    +
    +
  • +
  • +
    +
    + + + +
    +
    + + + +
    +
    + + +
    +
    + + + +
    +
    +
  • +
+
+
+ Start typing here... +
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..6f40e36 --- /dev/null +++ b/main.js @@ -0,0 +1,8 @@ +//import './style.css'; +import { WysiwygEditor } from 'wysiwyg.js'; + +document.addEventListener('DOMContentLoaded', () => { + const editor = new WysiwygEditor('#editor', { + toolbar: '#toolbar' + }); +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..850b3ef --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "vanilla-wysiwyg", + "private": true, + "version": "0.0.0", + "type": "module", + "dependencies": { + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3" + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..c9e19f1 --- /dev/null +++ b/style.css @@ -0,0 +1,94 @@ +#sourceView { + font-family: 'Courier New', Courier, monospace; + font-size: 14px; + white-space: pre-wrap; + resize: none; +} + +/* Font controls */ +#fontFamilySelect, +#fontSizeSelect, +#headingSelect { + min-width: 120px; +} + +#fontFamilySelect option { + font-size: 14px; +} + +/* Emoji and Symbol pickers */ +.emoji-picker, +.color-picker, +.symbol-picker { + position: absolute; + background: var(--bs-body-bg); + border: 1px solid var(--bs-border-color-translucent); + border-radius: var(--bs-border-radius); + padding: 5px; + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 2px; + z-index: 1000; + box-shadow: var(--bs-box-shadow); +} + +.emoji-picker button, +.color-picker button, +.symbol-picker button { + background: none; + border: 1px solid #464646; + padding: 5px; + cursor: pointer; + font-size: 16px; +} + +.emoji-picker button:hover, +.color-picker button:hover, +.symbol-picker button:hover { + background: #575757; +} + +/* Table styles */ +.table { + width: 100%; + margin-bottom: 1rem; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border: 1px solid #dee2e6; + min-width: 100px; +} + +.table th { + font-weight: bold; + background-color: rgba(0, 0, 0, 0.05); +} + +/* Add tooltips for better UX */ +[data-command] { + position: relative; +} + +[data-command]::after { + transition:opacity 1s linear; + opacity:0; +} +[data-command]:hover::after { + content: attr(title); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + padding: 4px 8px; + background-color: rgba(0, 0, 0, 0.8); + color: white; + opacity: 1; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + z-index: 1000; +} \ No newline at end of file diff --git a/wysiwyg.js b/wysiwyg.js new file mode 100644 index 0000000..608fb85 --- /dev/null +++ b/wysiwyg.js @@ -0,0 +1,382 @@ +export class WysiwygEditor { + constructor(editorSelector, options = {}) { + this.editor = document.querySelector(editorSelector); + this.sourceView = document.querySelector('#sourceView'); + this.toolbar = document.querySelector(options.toolbar); + this.fontSizes = ['8px', '9px', '10px', '11px', '12px', '14px', '15px', '16px', '18px', '20px', '24px', '30px', '32px', '36px', '48px']; + this.fontSizeDefault = '12px'; + this.fontFamilies = ['Open Sans', 'Arial', 'Arial Black', 'Courier', 'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 'Lucida Grande', 'Lucida Sans', 'Tahoma', 'Times', 'Times New Roman', 'Verdana']; + this.fontFamilyDefault = 'Open Sans'; + this.colors = [ + '#000000', '#434343', '#666666', '#999999', '#b7b7b7', + '#ffffff', '#f3f3f3', '#cccccc', '#d9d9d9', '#efefef', + '#980000', '#ff0000', '#ff9900', '#ffff00', '#00ff00', + '#ff00ff', '#9900ff', '#0000ff', '#4a86e8', '#00ffff', + '#e6b8af', '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', + '#d0e0e3', '#c9daf8', '#cfe2f3', '#d9d2e9', '#ead1dc' + ]; + this.emojis = ['😀', '😂', '😊', '😍', '🤔', '😎', '😴', '😄', '🥰', '😇', '🤗', '🤭', '😅', '😂', '🤣']; + this.symbols = ['©', '®', '™', '€', '£', '¥', '§', '¶', '†', '‡', '±', '¹', '²', '³', '¼', '½', '¾', '⁄', '¿', '·', '•', '°', '′', '″', '‴']; + this.isSourceView = false; + this.init(); + } + + init() { + if (!this.editor || !this.toolbar) { + console.error('Editor or toolbar not found'); + return; + } + this.initFontControls(); + this.initToolbarEvents(); + this.initEditorEvents(); + this.initClipboardHandlers(); + // Set default font size and family + this.execCommand('fontSize', this.fontSizeDefault); + this.execCommand('fontName', this.fontFamilyDefault); + } + + initFontControls() { + // Update font size select + const fontSizeSelect = this.toolbar.querySelector('#fontSizeSelect'); + if (fontSizeSelect) { + this.fontSizes.forEach(size => { + const option = document.createElement('option'); + option.value = size; + option.textContent = size; + option.selected = size === this.fontSizeDefault; + fontSizeSelect.appendChild(option); + }); + } + // Update font family select + const fontFamilySelect = this.toolbar.querySelector('#fontFamilySelect'); + if (fontFamilySelect) { + this.fontFamilies.forEach(font => { + const option = document.createElement('option'); + option.value = font; + option.textContent = font; + option.style.fontFamily = font; + option.selected = font === this.fontFamilyDefault; + fontFamilySelect.appendChild(option); + }); + } + // Update heading select + const headingSelect = this.toolbar.querySelector('#headingSelect'); + if (headingSelect) { + headingSelect.addEventListener('change', (e) => { + if (e.target.value) { + this.execCommand('formatBlock', e.target.value); + } + }); + } + } + + initToolbarEvents() { + this.toolbar.querySelectorAll('[data-command]').forEach(button => { + button.addEventListener('click', (e) => { + e.preventDefault(); + const command = button.getAttribute('data-command'); + if (['cut', 'copy', 'paste'].includes(command)) { + this.handleClipboardOperation(command); + } else if (command === 'createLink') { + this.createLink(); + } else if (command === 'insertImage') { + this.insertImage(); + } else if (command === 'insertTable') { + this.insertTable(); + } else if (command === 'insertEmoji') { + this.showEmojiPicker(); + } else if (command === 'insertSymbol') { + this.showSymbolPicker(); + } else if (command === 'toggleSource') { + this.toggleSourceView(); + } else if (command === 'insertCircleList') { + this.insertList('circle'); + } else if (command === 'insertSquareList') { + this.insertList('square'); + } else if (command === 'insertColor') { + this.showColorPicker(); + } else { + this.execCommand(command); + } + }); + }); + // Font size change handler + const fontSizeSelect = this.toolbar.querySelector('#fontSizeSelect'); + if (fontSizeSelect) { + fontSizeSelect.addEventListener('change', (e) => { + this.execCommand('fontSize', e.target.value); + }); + } + // Font family change handler + const fontFamilySelect = this.toolbar.querySelector('#fontFamilySelect'); + if (fontFamilySelect) { + fontFamilySelect.addEventListener('change', (e) => { + this.execCommand('fontName', e.target.value); + }); + } + } + + insertList(style) { + this.execCommand('insertUnorderedList'); + const selection = window.getSelection(); + const list = selection?.anchorNode.closest('ul'); + if (list) { + list.style.listStyleType = style; + } + } + + insertTable() { + const rows = prompt('Enter number of rows:', '3'); + const cols = prompt('Enter number of columns:', '3'); + + if (rows && cols) { + let table = ''; + + // Create header row + table += ''; + for (let j = 0; j < cols; j++) { + table += ''; + } + table += ''; + + // Create data rows + for (let i = 0; i < rows - 1; i++) { + table += ''; + for (let j = 0; j < cols; j++) { + table += ''; + } + table += ''; + } + + table += '
Header ' + (j + 1) + '
Cell ' + (i + 1) + ',' + (j + 1) + '

'; + this.execCommand('insertHTML', table); + } + } + + toggleSourceView() { + this.isSourceView = !this.isSourceView; + + if (this.isSourceView) { + this.sourceView.value = this.editor.innerHTML; + this.editor.classList.add('d-none'); + this.sourceView.classList.remove('d-none'); + this.disableToolbar(true); + } else { + this.editor.innerHTML = this.sourceView.value; + this.sourceView.classList.add('d-none'); + this.editor.classList.remove('d-none'); + this.disableToolbar(false); + } + } + + disableToolbar(disabled) { + this.toolbar.querySelectorAll('button, select').forEach(element => { + if (element.closest('[data-command="toggleSource"]')) return; + element.disabled = disabled; + }); + } + + createLink() { + const url = prompt('Enter URL:', 'http://'); + if (url) { + this.execCommand('createLink', url); + } + } + + insertImage() { + const url = prompt('Enter image URL:', 'http://'); + if (url) { + this.execCommand('insertImage', url); + } + } + + useModal(title, callback){ + new iModal({dialogCentered:true}); + } + + showColorPicker() { + const picker = document.createElement('div'); + picker.className = 'color-picker'; + this.colors.forEach(color => { + const btn = document.createElement('button'); + btn.style.backgroundColor = color; + btn.style.width = '20px'; + btn.style.height = '20px'; + btn.style.border = '1px solid #ccc'; + btn.style.margin = '2px'; + btn.addEventListener('click', (e) => { + e.stopPropagation(); // Stop event propagation + this.execCommand('foreColor', color); + picker.remove(); + }); + picker.appendChild(btn); + }); + + const button = this.toolbar.querySelector('[data-command="insertColor"]'); + button.parentNode.appendChild(picker); + + // Close picker when clicking outside + const closePicker = (e) => { + this.closeOpenPickers(e, button); + document.removeEventListener('click', closePicker); + }; + document.addEventListener('click', closePicker); + } + + showEmojiPicker() { + const picker = document.createElement('div'); + picker.className = 'emoji-picker'; + this.emojis.forEach(emoji => { + const btn = document.createElement('button'); + btn.textContent = emoji; + btn.addEventListener('click', (e) => { + e.stopPropagation(); // Stop event propagation + this.execCommand('insertText', emoji); + picker.remove(); + }); + picker.appendChild(btn); + }); + + const button = this.toolbar.querySelector('[data-command="insertEmoji"]'); + button.parentNode.appendChild(picker); + + // Close picker when clicking outside + const closePicker = (e) => { + this.closeOpenPickers(e, button); + document.removeEventListener('click', closePicker); + }; + document.addEventListener('click', closePicker); + } + + showSymbolPicker() { + const picker = document.createElement('div'); + picker.className = 'symbol-picker'; + this.symbols.forEach(symbol => { + const btn = document.createElement('button'); + btn.textContent = symbol; + btn.addEventListener('click', (e) => { + e.stopPropagation(); // Stop event propagation + this.execCommand('insertText', symbol); + picker.remove(); + }); + picker.appendChild(btn); + }); + + const button = this.toolbar.querySelector('[data-command="insertSymbol"]'); + button.parentNode.appendChild(picker); + + // Close picker when clicking outside + const closePicker = (e) => { + this.closeOpenPickers(e, button); + document.removeEventListener('click', closePicker); + }; + document.addEventListener('click', closePicker); + } + + closeOpenPickers(e, targetButton) { + const pickers = document.querySelectorAll('.color-picker, .emoji-picker, .symbol-picker'); + pickers.forEach(picker => { + if (!picker.contains(e.target) && e.target !== targetButton) { + picker.remove(); + } + }); + } + + async handleClipboardOperation(command) { + try { + switch (command) { + case 'cut': + if (document.getSelection().toString().length > 0) { + await navigator.clipboard.writeText(document.getSelection().toString()); + document.execCommand('delete'); + } + break; + case 'copy': + if (document.getSelection().toString().length > 0) { + await navigator.clipboard.writeText(document.getSelection().toString()); + } + break; + case 'paste': + const text = await navigator.clipboard.readText(); + document.execCommand('insertText', false, text); + break; + } + } catch (err) { + console.error('Clipboard operation failed:', err); + document.execCommand(command); + } + } + + initEditorEvents() { + this.editor.addEventListener('paste', (e) => { + e.preventDefault(); + const text = e.clipboardData.getData('text/plain'); + document.execCommand('insertText', false, text); + }); + this.editor.addEventListener('drop', (e) => { + e.preventDefault(); + const text = e.dataTransfer.getData('text/plain'); + document.execCommand('insertText', false, text); + }); + this.editor.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key.toLowerCase()) { + case 'z': + e.preventDefault(); + this.execCommand(e.shiftKey ? 'redo' : 'undo'); + break; + case 'y': + e.preventDefault(); + this.execCommand('redo'); + break; + } + } + + if (e.key === 'Tab') { + e.preventDefault(); + document.execCommand('insertHTML', false, '    '); + } + }); + } + + initClipboardHandlers() { + this.editor.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key.toLowerCase()) { + case 'x': + case 'c': + case 'v': + if (!this.editor.contains(document.activeElement)) return; + break; + } + } + }); + } + + execCommand(command, value = null) { + this.editor.focus(); + document.execCommand(command, false, value); + this.updateToolbarState(); + } + + updateToolbarState() { + this.toolbar.querySelectorAll('[data-command]').forEach(button => { + const command = button.getAttribute('data-command'); + if (['bold', 'italic', 'underline', 'justifyLeft', 'justifyCenter', 'justifyRight'].includes(command)) { + if (document.queryCommandState(command)) { + button.classList.add('active'); + } else { + button.classList.remove('active'); + } + } + }); + } + + getContent() { + return this.editor.innerHTML; + } + + setContent(html) { + this.editor.innerHTML = html; + } +} \ No newline at end of file