/*! Inline Editor - v0.1.0 - * 2015-10-06 * * https://github.com/alsofronie/inline-editor * Copyright (c) 2015 Alex Sofronie; * Licensed MIT */ ;(function(w,d,undefined) { "use strict"; // ================ INTERNAL VARIABLES ================= // PRIVATE VAR: Default options var _defaults = { lang: { text: 'Add your text here', title: 'Add your title here', }, toolboxes: { selection: [ 'bold','italic','underline','strikethrough','|','h','p','|','align'], insert: ['plus', 'image','video','embed','section'], image: ['pos','|','sec','|', 'del'] } }; // PRIVATE VAR: Default toolboxes definition // The widget functions will be called with runContext false to get the toolbox element // and with runContext = true (this will point to the plugin instance) var _toolboxes = { selection: { current: null, name: 'selection', id: 'seltbx-pane', position: 'top', widgets: { bold: function(runContext) { if(!runContext) { return { icon: 'bold' }; } else { document.execCommand('bold',false,true); } }, italic: function(runContext) { if(!runContext) { return { icon: 'italic' }; } else { document.execCommand('italic',false,true); } }, underline: function(runContext) { if(!runContext) { return { icon: 'underline' }; } else { document.execCommand('underline', false, true); } }, strikethrough: function(runContext) { if(!runContext) { return { icon:'strike' }; } else { document.execCommand('strikeThrough',false,true); } }, align: function(runContext) { if(!runContext) { return { icon: 'align' }; } else { if(_node.hasClass(this.sel,'text-center')) { _node.removeClass(this.sel,'text-center'); _node.addClass(this.sel,'text-right'); } else if(_node.hasClass(this.sel,'text-right')) { _node.removeClass(this.sel,'text-right'); _node.addClass(this.sel,'text-justify'); } else if(_node.hasClass(this.sel,'text-justify')) { _node.removeClass(this.sel,'text-justify'); // css defaults to text-left so no need to add that class here. } else { _node.addClass(this.sel,'text-center'); } _node.normalize(this.sel); _node.select(this.sel); } }, p: function(runContext) { if(!runContext) { return { icon: 'para' }; } else { if(!_node.is(this.sel,'p')) { var newEl = _node.change(this.sel, 'p', false); _node.normalize(newEl); _node.select(newEl); } } }, h: function(runContext) { if(!runContext) { return { icon: 'heading' }; } else { console.info('We need to change the current parent elment to a heading', this.sel); var destName = 'h1'; if(_node.is(this.sel,'h1')) { destName = 'h2'; } else if(_node.is(this.sel,'h2')) { destName = 'h3'; } else if(_node.is(this.sel,'h3')) { destName = 'h4'; } else if(_node.is(this.sel,'h4')) { destName = 'h5'; } else if(_node.is(this.sel,'h5')) { destName = 'h6'; } var newEl = _node.change(this.sel, destName, true); _node.normalize(newEl); _node.select(newEl); } } } }, insert: { current: null, name: 'insert', id: 'instbx-pane', position: 'left', widgets: { plus: function(runContext) { if(!runContext) { return { icon: 'close' }; } else { var t = document.getElementById('instbx-pane'); _node.toggleClass(t, 'expanded'); } }, image: function(runContext) { if(!runContext) { return { icon: 'image' }; } else { if(!w.FileReader) { alert('Your browser does not support FileReader so you cannot upload files this way. Please use a modern browser such as Google Chrome or Mozilla Firefox, updated to their latest versions!'); return false; } // this.hideToolbox() var that = this; var inp = document.createElement('input'); inp.type = 'file'; inp.name = 'userFile'; inp.multiple = "true"; inp.style.position = 'absolute'; inp.style.left = '-9999px'; inp.style.top = '-9999px'; inp.onchange = function() { var files,imgs,fr,i,filesToLoad,filesLoaded; files = this.files; imgs = []; console.info('Uploading files ', this.files); filesToLoad = files.length; filesLoaded = 0; for (i = 0; i < filesToLoad; i++) { // jshint loopfunc: true console.info('File index is', i); fr = new FileReader(); fr.token = i; fr.fname = files[i].name; fr.onloadend = function() { var img = document.createElement('img'); img.src = this.result; img.dataset.width = img.width; img.dataset.height = img.height; img.dataset.name = this.fname; // img.setAttribute('style','max-width: ' + img.width + 'px;max-height: ' + img.height + 'px'); imgs[this.token] = img; filesLoaded += 1; }; fr.readAsDataURL(files[i]); } var f = function() { if(filesToLoad === filesLoaded) { that.insertFigure(imgs); } else { setTimeout(f,1); } }; f(); }; document.body.appendChild(inp); inp.focus(); inp.click(); } }, video: function(runContext) { if(!runContext) { return { icon: 'video' }; } else { console.info('Clicked on INSERT VIDEO'); } }, embed: function(runContext) { if(!runContext) { return { icon: 'embed' }; } else { console.info('Clicked on INSERT EMBED'); } }, section: function(runContext) { if(!runContext) { return { icon: 'hr' }; } else { // var oldSel = this.sel; _node.change(this.sel,'hr'); this.createNewSection(); this.showToolbox(runContext); } } } }, image: { current: null, name: 'image', id: 'imgtbx-pane', position: 'top', widgets: { pos: function(runContext) { var p; if(!runContext) { return { icon: 'image-size' }; } else { var s = this.sec; console.info('Running through image sizes with section', s); if(_node.hasClass(s,'wide')) { _node.removeClass(s,'wide'); _node.addClass(s,'full'); } else if(_node.hasClass(s,'full')) { _node.removeClass(s,'full'); _node.addClass(s,'inline-text'); // add an empty text besides the image p = _node.create('p'); p.dataset.text = 'Add your text here'; _node.attr(p,'contenteditable','true'); var oldp = this.getData(s,'oldp'); if(oldp) { p.innerHTML = oldp; } s.appendChild(p); } else if(_node.hasClass(s,'inline-text')) { _node.removeClass(s,'inline-text'); _node.addClass(s,'inline-text-expand'); // this should always have the paragraph there, because it will only come from inline-text // which has the paragraph } else if(_node.hasClass(s, 'inline-text-expand')) { _node.removeClass(s, 'inline-text-expand'); // undo the paragraph thing p = s.getElementsByTagName('p'); if(p.length > 0) { p = p[0]; _node.normalize(p); this.setData(s, 'oldp', p.innerHTML); } _node.removeAllChildren(s,'p'); } else { _node.addClass(s, 'wide'); } } }, sec: function(runContext) { if(!runContext) { return { icon: 'image-big' }; } else { console.info('Must make the image very big'); } }, del: function(runContext) { if(!runContext) { return { icon: 'close' }; } else { var selImg = document.getElementsByClassName('img-active'); var imgsel; if(selImg.length === 1 && selImg[0] === this.img) { // test if the image is the only pretty thing in the section imgsel = selImg[0]; var fig = imgsel.parentElement; var childCount = fig.children.length; // remember, we have the fig caption also _node.destroy(imgsel); if(childCount === 2) { // === DELETE === var secToDel = this.sec; this.createNewSection(); _node.destroy(secToDel); } this.updateFigureAppearance(); this.hideToolbox(_toolboxes.image); } else { console.info('No image to delete...'); } } } } } }; // ============== END INTERNAL VARIABLES =============== // ============== INTERNAL FUNCTIONS =================== // ============== END INTERNAL FUNCTIONS =============== // ================ MAIN PLUGIN -- PUBLIC ============== // =============== END MAIN PLUGIN ===================== w.InlineEditor = function() { // global element references this.src = null; // the source element editor this.sel = null; // the active selection element (paragraph, h1, ... ,h6, figure etc) this.sec = null; // the active section (the parent) this.img = null; // the active image this.settings = null; // the settings this.objData = {}; // the object data, to store things in it related to a node // ============= INIT VARIABLES ================ // Create options by extending defaults with the passed in arugments if(arguments.length === 0) { throw 'Init error: we need the editor element!'; } else if(arguments.length === 1) { // this is only the editor this.src = arguments[0]; this.settings = _defaults; } else if(arguments.length === 2 && typeof arguments[1] === 'object') { this.src = arguments[0]; this.settings = _extend(_defaults, arguments[1]); } // =============== END INIT VARIABLES ================== // prepare the editor // add the class this.src.classList.add('ied-article'); // put content editable _node.attr(this.src,'contenteditable','true'); // private internal functions var plugin = this; // all the following functions will use the plugin variable. // Events // event for the selection // this will fire only on document, not on the actual editor function eventSelectionChange(event) { event = event ? event : w.event; plugin.setSelection(); plugin.normalize(); var sel = plugin.getSelection(); if(sel.rangeCount === 0) { return false; } var pe = sel.getRangeAt(0).commonAncestorContainer; while(pe !== plugin.src) { if(_node.is(pe,'body') || _node.is(pe,'html')) { plugin.hideToolbox(); return true; } pe = pe.parentElement; } if(sel.isCollapsed) { plugin.hideToolbox(_toolboxes.selection); if(_node.is(plugin.sel,'p') && !plugin.sel.hasChildNodes()) { plugin.showToolbox(_toolboxes.insert); } else { plugin.hideToolbox(_toolboxes.insert); } } else { // There is some selection // show the selection toolbox plugin.showToolbox(_toolboxes.selection); // hide the insert toolbox plugin.hideToolbox(_toolboxes.insert); // hide the image toolbox plugin.hideToolbox(_toolboxes.image); } } document.addEventListener('selectionchange', eventSelectionChange); // event for the Enter/Return key (creating a new section) // keydown (verify for enter) function eventKeyDown(event) { event = event ? event : w.event; plugin.setSelection(); if(event.keyCode === 13) { _stop(event, true); if(event.shiftKey === true) { // insert element at cursor var sel, range, br, spc; if(window.getSelection) { sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { range = sel.getRangeAt(0); range.deleteContents(); range.collapse(false); br = _node.create('br'); range.insertNode(br); range = range.cloneRange(); range.setStartAfter(br); range.setEndAfter(br); spc = _node.createText('...'); range.insertNode(spc); range.selectNode(spc); sel.removeAllRanges(); sel.addRange(range); if(spc.nextSibling.nodeValue !== '') { sel.deleteFromDocument(); } } } else if(document.selection && document.selection.createRange) { document.selection.createRange().text = '
'; } } else { console.info('creating new section: ', event); plugin.createNewSection(); } } else if(event.keyCode === 8 || event.keyCode === 46) { var selimgs = document.getElementsByClassName('img-active'); if(selimgs.length > 0 && selimgs[0] === plugin.img) { _stop(event, true); _toolboxes.image.widgets.del.apply(plugin,[true]); return false; } } else { console.info('need to hide toolboxes'); plugin.hideToolbox(_toolboxes.insert); } } this.src.addEventListener('keydown', eventKeyDown); // keyup event (verify for delete) // event for the Delete key (test if element is empty) function eventKeyUp(event) { event = event ? event : w.event; plugin.setSelection(); if(event.keyCode === 8 || event.keyCode === 46) { plugin.verifyEmptyElement(); } } this.src.addEventListener('keyup', eventKeyUp); // click event (used to select non-editable elements) function eventClick(event) { event = event ? event: w.event; var el = event.target; // plugin.unselectImages(); if(_node.is(el,'img') && _node.is(el.parentElement, 'figure')) { plugin.selectNone(); plugin.hideToolbox(_toolboxes.insert); plugin.hideToolbox(_toolboxes.selection); plugin.img = el; plugin.sec = _node.hasParent(el,'section'); _node.addClass(el,'img-active'); plugin.showToolbox(_toolboxes.image); } else { plugin.hideToolbox(_toolboxes.image); } } this.src.addEventListener('click', eventClick); function eventPaste(event) { event.preventDefault(); var data = event.clipboardData.getData('text/plain'); console.info('pasted data is ', data); // the target var trg = event.target; if(_node.is(trg,'p') && _node.is(trg.parentElement,'section')) { trg.innerHTML = data; } return false; } this.src.addEventListener('paste', eventPaste); // =============== PUBLIC FUNCIONS ===================== this.setup = function(options) { if(typeof options === 'object') { this.settings = _extend(this.settings, options); } }; this.getSelection = function() { if(window.getSelection) { return window.getSelection(); } else { var sel = document.selection; if(sel && sel.type !== 'Control') { return sel; } } return null; }; this.createNewSection = function() { var section = document.createElement('section'); section.className = 'col'; var p = document.createElement('p'); // p.className = 'p-nor'; p.dataset.text = this.settings.lang.text; section.appendChild(p); if(this.sec.nextSibling) { this.sec.parentNode.insertBefore(section, this.sec.nextSibling); } else { this.sec.appendNode(section); } var range,selection; range = document.createRange(); //Create a range (a range is a like the selection but invisible) range.selectNodeContents(p); //Select the entire contents of the element with the range range.collapse(true); //collapse the range to the end point. false means collapse to end rather than the start selection = window.getSelection(); //get the selection object (allows you to change selection) selection.removeAllRanges(); //remove any selections already made selection.addRange(range); //make the range you have just created the visible selection this.setSelection(); // that.getInsertToolbox(that,p); return false; }; this.showToolbox = function(tbx) { if(tbx.current === null) { var pane = _node.create('div'); pane.id = tbx.id; var ul = _node.create('ul'); pane.appendChild(ul); var widgets = tbx.widgets; var settings = this.settings.toolboxes[tbx.name]; for( var btnIndex in settings ) { // jshint loopfunc: true var bName = settings[btnIndex]; if(bName === '|') { var divider = _node.create('li','divider'); ul.appendChild(divider); continue; } if(!widgets[bName]) { // we do not know this one console.error('We do not know how to handle ', bName); continue; } var appearance = widgets[bName](false); var li = _node.create('li'); var btn = _node.create('button'); btn.dataset.act = bName; if(appearance.icon !== undefined) { var icn = _node.create('i', 'icon icon-' + appearance.icon); btn.appendChild(icn); } else if(appearance.text !== undefined) { btn.appendChild(document.createTextNode(appearance.text)); } var p = this; btn.addEventListener('click', function(event) { // _stop(event,true); var el = ( event.srcElement || event.target ); while(!_node.is(el,'button')) { el = el.parentNode; } console.info('event target is now', el); var b = el.dataset.act; widgets[b].call(p,tbx); }); li.appendChild(btn); ul.appendChild(li); } document.body.appendChild(pane); tbx.current = pane; } tbx.current.className = ''; // just to be sure it's not 'hidden' var sel,range,rect,dims; sel = this.getSelection(); if(sel.rangeCount > 0) { range = sel.getRangeAt(0).cloneRange(); rect = range.getClientRects()[0]; rect = _node.adjustRect(rect); } else if(this.img !== null) { // there is an image selected rect = this.img.getBoundingClientRect(); rect = _node.adjustRect(rect); } if(tbx.position === 'top') { dims = { width: tbx.current.offsetWidth, height: tbx.current.offsetHeight }; tbx.current.style.left = ( rect.left - Math.floor(dims.width / 2) + Math.floor(rect.width / 2)) + 'px'; tbx.current.style.top = ( rect.top - 3 - dims.height) + 'px'; } else if(tbx.position === 'left') { tbx.current.style.left = ( rect.left - 80 ) + 'px'; tbx.current.style.top = ( rect.top ) + 'px'; } return tbx.current; }; this.hideToolbox = function(tbx) { if(tbx === null || tbx === undefined) { this.hideToolbox(_toolboxes.image); this.hideToolbox(_toolboxes.selection); this.hideToolbox(_toolboxes.insert); this.unselectImages(); } else { if(tbx && tbx.current && tbx.current !== null && tbx.current !== undefined) { tbx.current.className = 'hidden'; if(tbx.name === 'image') { this.unselectImages(); } return true; } return false; } }; this.unselectImages = function() { var a = document.getElementsByClassName('img-active'); for(var b in a) { if(a[b] && a[b].classList && _node.is(a[b],'img')) { _node.removeClass(a[b],'img-active'); } } }; this.setSelection = function() { var sel = this.getSelection(); if(sel.rangeCount === 0) { return false; } var pe = sel.getRangeAt(0).commonAncestorContainer; while(pe) { if(_node.is(pe,'body') || _node.is(pe,'html')) { // we've gone too far, the user clicked outside the editor // this.sec = null; // this.sel = null; return false; } if(pe.nodeType === 1 && _node.is(pe,'section')) { break; } pe = pe.parentNode; } if(pe) { this.sec = pe; pe = pe.firstChild; while(pe && pe.nodeType !== 1 || _node.is(pe,'figure')) { pe = pe.nextSibling; } this.sel = pe; return true; } else { // this.sel = null; // this.sec = null; return false; } }; this.normalize = function() { if(this.sel !== null) { _node.normalize(this.sel); } }; this.verifyEmptyElement = function() { // the pe is always the section var pe = this.sel; console.info('Verify empty on parent', pe); // the node could be a paragraph or a figure var node = pe; console.info('We need to check the node ', node); if(!node) { return true; } if(_node.is(node,'figure')) { // we need to check the figcaption not the figure // maybe we have more than one image? // the figcaption is always the last child node = node.lastChild; } var htm = node.innerHTML.trim().toLowerCase(); console.info('html is ', htm); if(htm === '
' || htm === '
' || htm === '
' || htm === ' ' || htm === '') { node.innerHTML = null; node.className = ''; // this.showToolbox(_toolboxes.insert); } }; this.insertFigure = function(images) { var fig,img,i,cap; fig = document.createElement('figure'); fig.setAttribute('contenteditable', false); for(i=0;i 1) { if(imgs.length % 2 === 0) { _node.addClass(this.sec,'multiple-even'); } else { _node.addClass(this.sec,'multiple-odd'); } _node.addClass(this.sec, 'wide'); /* We must make images two by two like in medium Step 0: group the images by two with regard of the last one (eliminate if odd). All the following steps are applied for each pair of two images in a row Step 1: Determine the lowest height and make the highest image the same height as the other Step 2: Measure the width of the two images together with separator: 10px Step 3: What is the ratio we need to apply to the block in order to make the block the same width as the width of the container Step 4: Calculate the width of each of the two images so they will fill the container with height:auto. */ var i,img1,img2; var sectionWidth = _node.getWidth(this.sec); var imageGroups = Math.floor(imgs.length / 2); var SEPARATOR = 10; for(i=0;i 0) { var lastImg = imgs[imgs.length-1]; console.info('We have an odd image: ', lastImg); // _node.attr(lastImg, 'style','max-width:'+lastImg.dataset.width+'px;max-height:'+lastImg.dataset.height+'px'); } }; this.selectNone = function() { var sel = window.getSelection(); sel.removeAllRanges(); this.unselectImages(); }; this.getData = function(node, name) { var ref = node.dataset.iedoc; if(ref && this.objData[ref] && this.objData[ref][name]) { return this.objData[ref][name]; } else { return null; } }; this.setData = function(node, name, anything) { var ref = node.dataset.iedoc; if(!ref) { ref = 'ied' + Math.floor(100000 * Math.random()); node.dataset.iedoc = ref; } if(!this.objData[ref]) { this.objData[ref] = {}; } this.objData[ref][name] = anything; return true; }; }; // ================ UTILITY PRIVATE FUNCTIONS ======================= // Extends the source with properties of addition. // Will goo deep :) function _extend(source, addition) { var prop; for(prop in addition) { if(source.hasOwnProperty(prop)) { if(typeof addition[prop] === 'object') { source[prop] = _extend(source[prop],addition[prop]); } else { source[prop] = addition[prop]; } } } return source; } // stop the propagation of the event and // optionally prevents the default function _stop(event, preventDefault) { event = event || w.event; if(event.stopPropagation) { event.stopPropagation(); } // retarded IE event.cancelBubble = true; if(preventDefault === true) { event.preventDefault(); } return false; } // just some node operations (DOM) var _node = { create: function(name, className) { var node = document.createElement(name); if(className !== undefined) { node.className = className; } return node; }, createText: function(txt) { var node = document.createTextNode(txt); return node; }, destroy: function(el) { el.parentElement.removeChild(el); }, attr: function(node, name, value) { node.setAttribute(name, value); }, is: function(node, name) { // console.info('Match ' + name + ' against node ', node); if(!node || node === null || node === undefined) { return false; } // test only tag elements if(node.nodeType && node.nodeType !== 1) { return false; } name = name.toLowerCase(); return (name === node.tagName.toLowerCase()); }, change: function(src, destName, preserveClasses) { console.info('Changing to ' + destName + ' node ', src); var newClassName = ''; if(preserveClasses) { newClassName = src.className; } var newEl = _node.create(destName); newEl.className = newClassName; while(src.firstChild) { newEl.appendChild(src.firstChild); } src.parentNode.insertBefore(newEl, src); src.parentNode.removeChild(src); return newEl; }, select: function(node) { var newRange = document.createRange(); newRange.selectNodeContents(node); // TODO: call a common function var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(newRange); }, hasClass: function(node, className) { return node.classList.contains(className); }, addClass: function(node, newClass) { node.classList.add(newClass); }, removeClass: function(node, className) { node.classList.remove(className); }, toggleClass: function(node, className) { node.classList.toggle(className); }, normalize: function(node) { node.normalize(); }, hasParent: function(node, match) { while(true) { // console.info('walking on parent of ', node); if(_node.is(node,match)) { return node; } if(!node) { break; } else if(_node.is(node,'html')) { break; } node = node.parentElement; } return false; }, removeAllChildren: function(node, tag) { while(node.getElementsByTagName(tag).length > 0) { var e = node.getElementsByTagName(tag)[0]; _node.destroy(e); } }, replaceWith:function(oldElement, newElement) { oldElement.parentNode.insertBefore(newElement,oldElement); _node.destroy(oldElement); }, adjustRect: function(rect) { return { top: rect.top + window.pageYOffset - document.documentElement.clientTop, left: rect.left + window.pageXOffset - document.documentElement.clientLeft, width: rect.width, height: rect.height }; }, getWidth: function(el) { return el.offsetWidth; }, getHeight: function(el) { return el.offsetHeight; }, lastChildOf: function(el,nodeType) { if(el.children.length > 0) { var ret = el.children[0]; for(var i=0;i