├── .gitignore ├── smart.json ├── package.js ├── medium-editor-insert-plugin.css ├── medium-editor.css ├── medium-editor-insert-plugin.all.min.js ├── README.md └── medium-editor.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medium-editor-updated", 3 | "description": "The medium editor clone created by daviferreira", 4 | "homepage": "https://github.com/donflopez/meteor-medium-editor", 5 | "author": "Francisco López (http://about.me/donflopez)", 6 | "version": "1.2.0", 7 | "git": "https://github.com/donflopez/meteor-medium-editor.git", 8 | "packages": {} 9 | } 10 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "donflopez:medium-editor", 3 | summary: "Add medium-editor and medium insert clone to Meteor client side", 4 | version: "1.2.0" 5 | }); 6 | 7 | Package.on_use(function (api) { 8 | api.add_files(['medium-editor.css','medium-editor.js', 'medium-editor-insert-plugin.all.min.js','medium-editor-insert-plugin.css'], 'client'); 9 | 10 | if(api.export) 11 | api.export('MediumEditor', 'client'); 12 | }); 13 | -------------------------------------------------------------------------------- /medium-editor-insert-plugin.css: -------------------------------------------------------------------------------- 1 | .clearfix:before,[data-medium-element="true"]:before,.clearfix:after,[data-medium-element="true"]:after{content:" ";display:table}.clearfix:after,[data-medium-element="true"]:after{clear:both}img{max-width:100%}q,blockquote{display:block;margin-top:1em;margin-bottom:1em;border-left:5px solid #efefef;padding-left:20px;margin-left:-25px}[draggable]{-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;user-select:none;-khtml-user-drag:element;-webkit-user-drag:element}[contenteditable]{outline:0px solid transparent}[contenteditable]:focus{outline:0px solid transparent}.medium-editor-placeholder{padding-bottom:0 !important;min-height:58px}.medium-editor-placeholder:after{content:attr(data-placeholder) !important;top:1em}.hide{display:none !important}.mediumInsert-buttonsShow{opacity:0;-moz-transform:scale(0);-ms-transform:scale(0);-o-transform:scale(0);-webkit-transform:scale(0);transform:scale(0);-moz-transition:all 0.08s cubic-bezier(0.2, 0.3, 0.25, 0.9);-o-transition:all 0.08s cubic-bezier(0.2, 0.3, 0.25, 0.9);-webkit-transition:all 0.08s cubic-bezier(0.2, 0.3, 0.25, 0.9);transition:all 0.08s cubic-bezier(0.2, 0.3, 0.25, 0.9);display:block;width:18px;height:18px;margin-top:-5px;border-radius:10px;border:2px solid;font-size:18px;line-height:18px;text-align:center;text-decoration:none !important}.mediumInsert-buttonsShow:after{left:auto;right:100%;top:50%;margin-top:-4px}.mediumInsert{position:relative;margin:-1em 0 -1em -40px;min-height:18px}.mediumInsert .mediumInsert-buttons{position:absolute;width:40px;top:0;left:0;color:#ddd;font-size:0.9em}.mediumInsert .mediumInsert-buttons a{text-decoration:underline;cursor:pointer}.mediumInsert .mediumInsert-buttons a.active{font-weight:bold}.mediumInsert .mediumInsert-buttons ul.mediumInsert-buttonsOptions{margin:0;padding:0;list-style:none;display:none;position:absolute;z-index:2;left:40px;top:-10px;border-radius:5px}.mediumInsert .mediumInsert-buttons ul.mediumInsert-buttonsOptions button{min-height:auto;height:auto;padding:5px;border-left:none;float:none}.mediumInsert .mediumInsert-buttons ul.mediumInsert-buttonsOptions button .fa{font-size:20px}.mediumInsert .mediumInsert-placeholder{position:relative;margin-left:40px;text-align:center}.mediumInsert .mediumInsert-placeholder .mediumInsert-images img{margin-top:1em;margin-bottom:10px;vertical-align:top}.mediumInsert .mediumInsert-placeholder .mediumInsert-images a{position:absolute;top:1em;width:30px;height:30px;background-color:#3b3b3b;background-repeat:no-repeat;background-position:center center;cursor:pointer}.mediumInsert .mediumInsert-placeholder .mediumInsert-images a.mediumInsert-imageRemove{right:0;background-image:url(../images/remove.png)}.mediumInsert .mediumInsert-placeholder .mediumInsert-images a.mediumInsert-imageResizeSmaller,.mediumInsert .mediumInsert-placeholder .mediumInsert-images a.mediumInsert-imageResizeBigger{right:31px;background-image:url(../images/resize-smaller.png)}.mediumInsert .mediumInsert-placeholder .mediumInsert-images a.mediumInsert-imageResizeBigger{background-image:url(../images/resize-bigger.png)}.mediumInsert .mediumInsert-placeholder .mediumInsert-images:first-child:after{content:"\a";white-space:pre}.mediumInsert .mediumInsert-placeholder .mediumInsert-images:not(:first-child){margin-right:10px}.mediumInsert .mediumInsert-placeholder .mediumInsert-images:not(:first-child) img{width:20%}.mediumInsert .mediumInsert-placeholder .mediumInsert-maps{padding:10px;background:#ccc}.mediumInsert.hover .mediumInsert-placeholder{background:#f0f0f0}.mediumInsert:hover .mediumInsert-buttonsShow{-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);-webkit-transform:scale(1);transform:scale(1);opacity:1}.mediumInsert.small .mediumInsert-placeholder{width:33.33%;float:left;margin-right:30px}.hover .mediumInsert-placeholder{min-height:14px;border:1px dashed #ddd;margin-top:-1px;margin-bottom:-1px} 2 | -------------------------------------------------------------------------------- /medium-editor.css: -------------------------------------------------------------------------------- 1 | .clearfix:after{display:block;visibility:hidden;clear:both;height:0;content:" ";font-size:0;}@-webkit-keyframes pop-upwards{0%{-webkit-transform:matrix(0.97, 0, 0, 1, 0, 12);transform:matrix(0.97, 0, 0, 1, 0, 12);opacity:0;}20%{-webkit-transform:matrix(0.99, 0, 0, 1, 0, 2);transform:matrix(0.99, 0, 0, 1, 0, 2);opacity:0.7;}40%{-webkit-transform:matrix(1, 0, 0, 1, 0, -1);transform:matrix(1, 0, 0, 1, 0, -1);opacity:1;}70%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}100%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}}@keyframes pop-upwards{0%{-webkit-transform:matrix(0.97, 0, 0, 1, 0, 12);transform:matrix(0.97, 0, 0, 1, 0, 12);opacity:0;}20%{-webkit-transform:matrix(0.99, 0, 0, 1, 0, 2);transform:matrix(0.99, 0, 0, 1, 0, 2);opacity:0.7;}40%{-webkit-transform:matrix(1, 0, 0, 1, 0, -1);transform:matrix(1, 0, 0, 1, 0, -1);opacity:1;}70%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}100%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}}.medium-toolbar-arrow-under:after,.medium-toolbar-arrow-over:before{position:absolute;left:50%;display:block;margin-left:-8px;width:0;height:0;border-style:solid;content:"";}.medium-toolbar-arrow-under:after{border-width:8px 8px 0 8px;}.medium-toolbar-arrow-over:before{top:-8px;border-width:0 8px 8px 8px;}.medium-editor-toolbar,.medium-editor-anchor-preview{position:absolute;top:0;left:0;z-index:2000;visibility:hidden;font-size:16px;font-family:HelveticaNeue,Helvetica,Arial,sans-serif;}.medium-editor-toolbar ul,.medium-editor-anchor-preview ul{margin:0;padding:0;}.medium-editor-toolbar li,.medium-editor-anchor-preview li{float:left;margin:0;padding:0;list-style:none;}.medium-editor-toolbar li button,.medium-editor-anchor-preview li button{display:block;margin:0;padding:15px;cursor:pointer;font-size:14px;line-height:1.33;text-decoration:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}.medium-editor-toolbar li .medium-editor-action-underline,.medium-editor-anchor-preview li .medium-editor-action-underline{text-decoration:underline;}.medium-editor-toolbar li .medium-editor-action-pre,.medium-editor-anchor-preview li .medium-editor-action-pre{padding:15px 0;font-weight:100;font-size:12px;font-family:'Menlo',monospace;}.medium-editor-anchor-preview i{display:inline-block;margin:5px 5px 5px 10px;text-decoration:underline;font-style:normal;cursor:pointer;}.medium-editor-toolbar-active,.medium-editor-anchor-preview-active{visibility:visible;-webkit-animation:pop-upwards 160ms forwards linear;-ms-animation:pop-upwards 160ms forwards linear;animation:pop-upwards 160ms forwards linear;-webkit-transition:top 0.075s ease-out,left 0.075s ease-out;transition:top 0.075s ease-out,left 0.075s ease-out;}.medium-editor-action-bold{font-weight:bolder;}.medium-editor-action-italic{font-style:italic;}.medium-editor-toolbar-form-anchor{display:none;}.medium-editor-toolbar-form-anchor input,.medium-editor-toolbar-form-anchor a{font-family:HelveticaNeue,Helvetica,Arial,sans-serif;}.medium-editor-toolbar-form-anchor input{margin:0;padding:6px;width:316px;border:none;font-size:14px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}.medium-editor-toolbar-form-anchor input:focus{outline:0;border:none;-webkit-box-shadow:none;box-shadow:none;-webkit-appearance:none;-moz-appearance:none;}.medium-editor-toolbar-form-anchor a{display:inline-block;margin:0 10px;text-decoration:none;font-weight:bolder;font-size:24px;}.medium-editor-placeholder{position:relative;}.medium-editor-placeholder:after{position:absolute;top:0;left:0;content:attr(data-placeholder);font-style:italic;} 2 | .clearfix:after{display:block;visibility:hidden;clear:both;height:0;content:" ";font-size:0;}@-webkit-keyframes pop-upwards{0%{-webkit-transform:matrix(0.97, 0, 0, 1, 0, 12);transform:matrix(0.97, 0, 0, 1, 0, 12);opacity:0;}20%{-webkit-transform:matrix(0.99, 0, 0, 1, 0, 2);transform:matrix(0.99, 0, 0, 1, 0, 2);opacity:0.7;}40%{-webkit-transform:matrix(1, 0, 0, 1, 0, -1);transform:matrix(1, 0, 0, 1, 0, -1);opacity:1;}70%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}100%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}}@keyframes pop-upwards{0%{-webkit-transform:matrix(0.97, 0, 0, 1, 0, 12);-ms-transform:matrix(0.97, 0, 0, 1, 0, 12);transform:matrix(0.97, 0, 0, 1, 0, 12);opacity:0;}20%{-webkit-transform:matrix(0.99, 0, 0, 1, 0, 2);-ms-transform:matrix(0.99, 0, 0, 1, 0, 2);transform:matrix(0.99, 0, 0, 1, 0, 2);opacity:0.7;}40%{-webkit-transform:matrix(1, 0, 0, 1, 0, -1);-ms-transform:matrix(1, 0, 0, 1, 0, -1);transform:matrix(1, 0, 0, 1, 0, -1);opacity:1;}70%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);-ms-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}100%{-webkit-transform:matrix(1, 0, 0, 1, 0, 0);-ms-transform:matrix(1, 0, 0, 1, 0, 0);transform:matrix(1, 0, 0, 1, 0, 0);opacity:1;}}.medium-toolbar-arrow-under:after{top:50px;border-color:#242424 transparent transparent transparent;}.medium-toolbar-arrow-over:before{top:-8px;border-color:transparent transparent #242424 transparent;}.medium-editor-toolbar{border:1px solid #000;background-color:#242424;background:-webkit-gradient(linear, left bottom, left top, from(#242424), to(rgba(36, 36, 36, 0.75)));background:-webkit-linear-gradient(bottom, #242424, rgba(36, 36, 36, 0.75));background:linear-gradient(bottom, #242424, rgba(36, 36, 36, 0.75));border-radius:5px;-webkit-box-shadow:0 0 3px #000;box-shadow:0 0 3px #000;-webkit-transition:top 0.075s ease-out,left 0.075s ease-out;transition:top 0.075s ease-out,left 0.075s ease-out;}.medium-editor-toolbar li button{min-width:50px;height:50px;border:0;border-right:1px solid #000;border-left:1px solid #333;border-left:1px solid rgba(255, 255, 255, 0.1);background-color:#242424;color:#fff;background:-webkit-gradient(linear, left bottom, left top, from(#242424), to(rgba(36, 36, 36, 0.89)));background:-webkit-linear-gradient(bottom, #242424, rgba(36, 36, 36, 0.89));background:linear-gradient(bottom, #242424, rgba(36, 36, 36, 0.89));-webkit-box-shadow:0 2px 2px rgba(0, 0, 0, 0.3);box-shadow:0 2px 2px rgba(0, 0, 0, 0.3);-webkit-transition:background-color 0.2s ease-in;transition:background-color 0.2s ease-in;}.medium-editor-toolbar li button:hover{background-color:#000;color:yellow;}.medium-editor-toolbar li .medium-editor-button-first{border-top-left-radius:5px;border-bottom-left-radius:5px;}.medium-editor-toolbar li .medium-editor-button-last{border-top-right-radius:5px;border-bottom-right-radius:5px;}.medium-editor-toolbar li .medium-editor-button-active{background-color:#000;color:#fff;background:-webkit-gradient(linear, left bottom, left top, from(#242424), to(rgba(0, 0, 0, 0.89)));background:-webkit-linear-gradient(bottom, #242424, rgba(0, 0, 0, 0.89));background:linear-gradient(bottom, #242424, rgba(0, 0, 0, 0.89));}.medium-editor-toolbar-form-anchor{background:#242424;color:#999;border-radius:5px;}.medium-editor-toolbar-form-anchor input{height:50px;background:#242424;color:#ccc;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}.medium-editor-toolbar-form-anchor a{color:#fff;}.medium-editor-toolbar-anchor-preview{background:#242424;color:#fff;border-radius:5px;}.medium-editor-placeholder:after{color:#b3b3b1;} 3 | -------------------------------------------------------------------------------- /medium-editor-insert-plugin.all.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * medium-editor-insert-plugin v0.2.1 - jQuery insert plugin for MediumEditor 3 | * 4 | * https://github.com/orthes/medium-editor-images-plugin 5 | * 6 | * Copyright (c) 2014 Pavel Linkesch (http://linkesch.sk) 7 | * Released under the MIT license 8 | */ 9 | 10 | !function(a){var b={};MediumEditor.prototype.serialize=function(){var b,c,d,e,f,g,h,i,j={};for(b=0;bb&&(b=c)}),b},setPlaceholders:function(){var c=this,d=a.fn.mediumInsert.insert.$el,e=a.fn.mediumInsert.settings.editor,f=e&&e.options?e.options.buttonLabels:"",g='",g='
+'+g+'
',d.is(":empty")&&d.html("


"),d.keyup(function(){var b,e=d.children(":last");e.hasClass("mediumInsert")&&e.find(".mediumInsert-placeholder").children().length>0&&d.append("


"),navigator.userAgent.match(/firefox/i)&&a(".mediumInsert .mediumInsert-placeholder:empty",d).each(function(){a(this).parent().remove()}),b=c.getMaxId()+1,d.children("p").each(function(){a(this).next().hasClass("mediumInsert")===!1&&(a(this).after(g),a(this).next(".mediumInsert").attr("id","mediumInsert-"+b)),b++})}).keyup()},setEvents:function(){var c=this,d=a.fn.mediumInsert.insert.$el;d.on("selectstart",".mediumInsert",function(a){return a.preventDefault(),!1}),d.on("blur",function(){var b,c=a(this).clone();c.find(".mediumInsert").remove(),b=c.html().replace(/^\s+|\s+$/g,""),(""===b||"


"===b)&&a(this).addClass("medium-editor-placeholder")}),d.on("click",".mediumInsert-buttons a.mediumInsert-buttonsShow",function(){var b=a(this).siblings(".mediumInsert-buttonsOptions"),d=a(this).parent().siblings(".mediumInsert-placeholder");a(this).hasClass("active")?(a(this).removeClass("active"),b.hide(),a("a",b).show()):(a(this).addClass("active"),b.show(),a("a",b).each(function(){var c=a(this).attr("class").split("action-")[1],e=c.split("-")[0];a(".mediumInsert-"+e,d).length>0&&a("a:not(.action-"+c+")",b).hide()})),c.deselect()}),d.on("mouseleave",".mediumInsert",function(){a("a.mediumInsert-buttonsShow",this).removeClass("active"),a(".mediumInsert-buttonsOptions",this).hide()}),d.on("click",".mediumInsert-buttons .mediumInsert-action",function(){var c=a(this).data("addon"),d=a(this).data("action"),e=a(this).parents(".mediumInsert-buttons").siblings(".mediumInsert-placeholder");b[c]&&b[c][d]&&b[c][d](e),a(this).parents(".mediumInsert").mouseleave()})}}}(jQuery),function(a){a.fn.mediumInsert.registerAddon("images",{init:function(b){b&&b.$el&&(this.$el=b.$el),this.options=a.extend(this.default,b),this.setImageEvents(),this.setDragAndDropEvents(),this.preparePreviousImages()},insertButton:function(a){var b="Img";return"fontawesome"==a&&(b=''),'"},"default":{imagesUploadScript:"upload.php",formatData:function(a){var b=new FormData;return b.append("file",a),b}},preparePreviousImages:function(){this.$el.find(".mediumInsert-images").each(function(){var b=a(this).parent();b.html('
'+b.html()+"
")})},add:function(b){var c,d,e=this;return c=a('').click(),c.change(function(){d=this.files,e.uploadFiles(b,d)}),a.fn.mediumInsert.insert.deselect(),c},updateProgressBar:function(b){var c,d=a(".progress:first",this.$el);b.lengthComputable&&(c=b.loaded/b.total*100|0,d.attr("value",c),d.html(c))},uploadCompleted:function(b){var c,d=a(".progress:first",this.$el);d.attr("value",100),d.html(100),d.before('
'),c=d.siblings("img"),d.remove(),c.load(function(){c.parent().mouseleave().mouseenter()}),a.fn.mediumInsert.insert.$el.keyup()},uploadFiles:function(b,c){for(var d={"image/png":!0,"image/jpeg":!0,"image/gif":!0},e=this,f=function(){var a=new XMLHttpRequest;return a.upload.onprogress=e.updateProgressBar,a},g=0;g0'),a.ajax({type:"post",url:this.options.imagesUploadScript,xhr:f,cache:!1,contentType:!1,complete:this.uploadCompleted,processData:!1,data:this.options.formatData(h)}))}},setImageEvents:function(){this.$el.on("mouseenter",".mediumInsert-images",function(){var b,c,d=a("img",this);a.fn.mediumInsert.settings.enabled!==!1&&d.length>0&&(a(this).append(''),a(this).append(a(this).parent().parent().hasClass("small")?'':''),b=d.position().top+parseInt(d.css("margin-top"),10),c=d.position().left+d.width()-30,a(".mediumInsert-imageRemove",this).css({right:"auto",top:b,left:c}),a(".mediumInsert-imageResizeBigger, .mediumInsert-imageResizeSmaller",this).css({right:"auto",top:b,left:c-31}))}),this.$el.on("mouseleave",".mediumInsert-images",function(){a(".mediumInsert-imageRemove, .mediumInsert-imageResizeSmaller, .mediumInsert-imageResizeBigger",this).remove()}),this.$el.on("click",".mediumInsert-imageResizeSmaller",function(){a(this).parent().parent().parent().addClass("small"),a(this).parent().mouseleave().mouseleave(),a.fn.mediumInsert.insert.deselect()}),this.$el.on("click",".mediumInsert-imageResizeBigger",function(){a(this).parent().parent().parent().removeClass("small"),a(this).parent().mouseleave().mouseleave(),a.fn.mediumInsert.insert.deselect()}),this.$el.on("click",".mediumInsert-imageRemove",function(){0===a(this).parent().siblings().length&&a(this).parent().parent().parent().removeClass("small"),a(this).parent().remove(),a.fn.mediumInsert.insert.deselect()})},setDragAndDropEvents:function(){var b,c,d=this,e=!1,f=!1;a(document).on("dragover","body",function(){a.fn.mediumInsert.settings.enabled!==!1&&a(this).addClass("hover")}),a(document).on("dragend","body",function(){a.fn.mediumInsert.settings.enabled!==!1&&a(this).removeClass("hover")}),this.$el.on("dragover",".mediumInsert",function(){a.fn.mediumInsert.settings.enabled!==!1&&(a(this).addClass("hover"),a(this).attr("contenteditable",!0))}),this.$el.on("dragleave",".mediumInsert",function(){a.fn.mediumInsert.settings.enabled!==!1&&(a(this).removeClass("hover"),a(this).attr("contenteditable",!1))}),this.$el.on("dragstart",".mediumInsert .mediumInsert-images img",function(){a.fn.mediumInsert.settings.enabled!==!1&&(b=a(this).parent().index(),c=a(this).parent().parent().parent().attr("id"))}),this.$el.on("dragend",".mediumInsert .mediumInsert-images img",function(b){a.fn.mediumInsert.settings.enabled!==!1&&e===!0&&(0===a(b.originalEvent.target.parentNode).siblings().length&&a(b.originalEvent.target.parentNode).parent().parent().removeClass("small"),a(b.originalEvent.target.parentNode).mouseleave(),a(b.originalEvent.target.parentNode).remove(),e=!1,f=!1)}),this.$el.on("dragover",".mediumInsert .mediumInsert-images img",function(b){a.fn.mediumInsert.settings.enabled!==!1&&b.preventDefault()}),this.$el.on("drop",".mediumInsert .mediumInsert-images img",function(){var d,e,g;if(a.fn.mediumInsert.settings.enabled!==!1){if(c!==a(this).parent().parent().parent().attr("id"))return f=!1,void(b=c=null);d=parseInt(b,10),e=a(this).parent().parent().find(".mediumInsert-images:nth-child("+(d+1)+")"),g=a(this).parent().index(),g>d?e.insertAfter(a(this).parent()):d>g&&e.insertBefore(a(this).parent()),e.mouseleave(),f=!0,b=null}}),this.$el.on("drop",".mediumInsert",function(b){var c;b.preventDefault(),a.fn.mediumInsert.settings.enabled!==!1&&(a(this).removeClass("hover"),a("body").removeClass("hover"),a(this).attr("contenteditable",!1),c=b.originalEvent.dataTransfer.files,c.length>0?d.uploadFiles(a(".mediumInsert-placeholder",this),c):f===!0?f=!1:(a(".mediumInsert-placeholder",this).append('
'+b.originalEvent.dataTransfer.getData("text/html")+"
"),a("meta",this).remove(),e=!0))})}})}(jQuery),function(a){a.fn.mediumInsert.registerAddon("maps",{init:function(){this.$el=a.fn.mediumInsert.insert.$el},insertButton:function(a){var b="Map";return"fontawesome"==a&&(b=''),'"},add:function(b){a.fn.mediumInsert.insert.deselect(),b.append('
Map - Coming soon...
')}})}(jQuery); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediumEditor 2 | 3 | This is a clone of [medium.com](https://medium.com) inline editor toolbar. 4 | 5 | **This has been packaged and adapted for Meteorjs with the insert plugin and default theme** 6 | 7 | Since I always had problems with bloated editors and I loved the simplicity of medium.com solution I've tried to implement their WYSIWYG approach with this script. 8 | 9 | MediumEditor has been written using vanilla JavaScript, no additional frameworks required. 10 | 11 | Tested on Google Chrome, Firefox and IE9+. 12 | 13 | [![NPM info](https://nodei.co/npm/medium-editor.png?downloads=true)](https://nodei.co/npm/medium-editor.png?downloads=true) 14 | 15 | [![Travis build status](https://travis-ci.org/daviferreira/medium-editor.png?branch=master)](https://travis-ci.org/daviferreira/medium-editor) 16 | [![dependencies](https://david-dm.org/daviferreira/medium-editor.png)](https://david-dm.org/daviferreira/medium-editor) 17 | [![devDependency Status](https://david-dm.org/daviferreira/medium-editor/dev-status.png)](https://david-dm.org/daviferreira/medium-editor#info=devDependencies) 18 | 19 | # Basic usage 20 | 21 | ![screenshot](https://raw.github.com/daviferreira/medium-editor/master/demo/img/medium-editor.jpg) 22 | 23 | __demo__: [http://daviferreira.github.io/medium-editor/](http://daviferreira.github.io/medium-editor/) 24 | 25 | First, you need to attach medium editor's stylesheet to your page: 26 | 27 | ```html 28 | 29 | 30 | ``` 31 | 32 | The next step is to reference the editor's script and instantiate a new MediumEditor object: 33 | 34 | ```html 35 | 36 | 37 | ``` 38 | 39 | The above code will transform all the elements with the .editable class into HTML5 editable contents and add the medium editor toolbar to them. 40 | 41 | You can also pass a list of HTML elements: 42 | 43 | ```javascript 44 | var elements = document.querySelectorAll('.editable'), 45 | editor = new MediumEditor(elements); 46 | ``` 47 | 48 | ## IE9 49 | 50 | If you want to support IE9, you will need to use a classList pollyfill, like Eli Gray's, available at [https://github.com/eligrey/classList.js](https://github.com/eligrey/classList.js). 51 | 52 | ## Initialization options 53 | 54 | * __allowMultiParagraphSelection__: enables the toolbar when selecting multiple paragraphs/block elements. Default: true 55 | * __anchorInputPlaceholder__: text to be shown as placeholder of the anchor input. Default: _Paste or type a link_ 56 | * __anchorPreviewHideDelay__: time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag. Default: 500 57 | * __buttons__: the set of buttons to display on the toolbar. Default: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'] 58 | * __buttonLabels__: type of labels on the buttons. Values: 'fontawesome', `{'bold': 'b', 'italic': 'i'}`. Default: false 59 | * __checkLinkFormat__: enables/disables check for http on anchor links. Default: 60 | false 61 | * __cleanPastedHTML__: cleans pasted content from different sources, like google docs etc. Default: false 62 | * __delay__: time in milliseconds to show the toolbar or anchor tag preview. Default: 0 63 | * __diffLeft__: value in pixels to be added to the X axis positioning of the toolbar. Default: 0 64 | * __diffTop__: value in pixels to be added to the Y axis positioning of the toolbar. Default: -10 65 | * __disableReturn__: enables/disables the use of the return-key. You can also set specific element behavior by using setting a data-disable-return attribute. Default: false 66 | * __disableDoubleReturn__: allows/disallows two (or more) empty new lines. You can also set specific element behavior by using setting a data-disable-double-return attribute. Default: false 67 | * __disableToolbar__: enables/disables the toolbar, adding only the contenteditable behavior. You can also set specific element behavior by using setting a data-disable-toolbar attribute. Default: false 68 | * __firstHeader__: HTML tag to be used as first header. Default: h3 69 | * __forcePlainText__: Forces pasting as plain text. Default: true 70 | * __placeholder__: Defines the default placeholder for empty contenteditables. You can overwrite it by setting a data-placeholder attribute on your elements. Default: 'Type your text' 71 | * __secondHeader__: HTML tag to be used as second header. Default: h4 72 | * __targetBlank__: enables/disables target="\_blank" for anchor tags. Default: false 73 | * __extensions__: extension to use (see _Extensions_) for more. Default: {} 74 | 75 | Example: 76 | 77 | ```javascript 78 | var editor = new MediumEditor('.editable', { 79 | anchorInputPlaceholder: 'Type a link', 80 | buttons: ['bold', 'italic', 'quote'], 81 | diffLeft: 25, 82 | diffTop: 10, 83 | firstHeader: 'h1', 84 | secondHeader: 'h2', 85 | delay: 1000, 86 | targetBlank: true 87 | }); 88 | ``` 89 | 90 | ## Extra buttons 91 | 92 | Medium Editor, by default, will show only the buttons listed above to avoid a huge toolbar. There are a couple of extra buttons you can use: 93 | 94 | * __superscript__ 95 | * __subscript__ 96 | * __strikethrough__ 97 | * __unorderedlist__ 98 | * __orderedlist__ 99 | * __pre__ 100 | * __image__ (this simply converts selected text to an image tag) 101 | * __indent__ (moves the selected text up one level) 102 | * __outdent__ (moves the selected text down one level) 103 | 104 | 105 | ## Themes 106 | 107 | Check out the Wiki page for a list of available themes: [https://github.com/daviferreira/medium-editor/wiki/Themes](https://github.com/daviferreira/medium-editor/wiki/Themes) 108 | 109 | ## API 110 | 111 | * __.deactivate()__: disables the editor 112 | * __.activate()__: re-activates the editor 113 | * __.serialize()__: returns a JSON object with elements contents 114 | 115 | 116 | ## Capturing DOM changes 117 | 118 | For observing any changes on contentEditable 119 | 120 | ```js 121 | $('.editable').on('input', function() { 122 | // Do some work 123 | }); 124 | ``` 125 | 126 | This is handy when you need to capture modifications other thats outside of `key up`'s scope like clicking on toolbar buttons. 127 | 128 | `input` is supported by Chrome, Firefox, IE9 and other modern browsers. If you want to read more or support older browsers, check [Listening to events of a contenteditable HTML element](http://stackoverflow.com/questions/7802784/listening-to-events-of-a-contenteditable-html-element/7804973#7804973) and [Detect changes in the DOM](http://stackoverflow.com/questions/3219758/detect-changes-in-the-dom) 129 | 130 | ## Extensions 131 | 132 | To add additional additional functions that are not supported by the native [browser API](https://developer.mozilla.org/de/docs/Rich-Text_Editing_in_Mozilla) you can 133 | write extensions that are then integrated into the toolbar. The Extension API is currently unstable and very minimal. 134 | 135 | An extension is an object that has essentially two functions `getButton` and `checkState`. 136 | 137 | * `getButton` is called when the editor is initialized and should return a element that is integrated into the toolbar. 138 | Usually this will be a `', 293 | 'italic': '', 294 | 'underline': '', 295 | 'strikethrough': '', 296 | 'superscript': '', 297 | 'subscript': '', 298 | 'anchor': '', 299 | 'image': '', 300 | 'header1': '', 301 | 'header2': '', 302 | 'quote': '', 303 | 'orderedlist': '', 304 | 'unorderedlist': '', 305 | 'pre': '', 306 | 'indent': '', 307 | 'outdent': '' 308 | }; 309 | return buttonTemplates[btnType] || false; 310 | }, 311 | 312 | // TODO: break method 313 | getButtonLabels: function (buttonLabelType) { 314 | var customButtonLabels, 315 | attrname, 316 | buttonLabels = { 317 | 'bold': 'B', 318 | 'italic': 'I', 319 | 'underline': 'U', 320 | 'superscript': 'x1', 321 | 'subscript': 'x1', 322 | 'anchor': '#', 323 | 'image': 'image', 324 | 'header1': 'H1', 325 | 'header2': 'H2', 326 | 'quote': '', 327 | 'orderedlist': '1.', 328 | 'unorderedlist': '', 329 | 'pre': '0101', 330 | 'indent': '', 331 | 'outdent': '' 332 | }; 333 | if (buttonLabelType === 'fontawesome') { 334 | customButtonLabels = { 335 | 'bold': '', 336 | 'italic': '', 337 | 'underline': '', 338 | 'superscript': '', 339 | 'subscript': '', 340 | 'anchor': '', 341 | 'image': '', 342 | 'quote': '', 343 | 'orderedlist': '', 344 | 'unorderedlist': '', 345 | 'pre': '', 346 | 'indent': '', 347 | 'outdent': '' 348 | }; 349 | } else if (typeof buttonLabelType === 'object') { 350 | customButtonLabels = buttonLabelType; 351 | } 352 | if (typeof customButtonLabels === 'object') { 353 | for (attrname in customButtonLabels) { 354 | if (customButtonLabels.hasOwnProperty(attrname)) { 355 | buttonLabels[attrname] = customButtonLabels[attrname]; 356 | } 357 | } 358 | } 359 | return buttonLabels; 360 | }, 361 | 362 | initToolbar: function () { 363 | if (this.toolbar) { 364 | return this; 365 | } 366 | this.toolbar = this.createToolbar(); 367 | this.keepToolbarAlive = false; 368 | this.anchorForm = this.toolbar.querySelector('.medium-editor-toolbar-form-anchor'); 369 | this.anchorInput = this.anchorForm.querySelector('input'); 370 | this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions'); 371 | this.anchorPreview = this.createAnchorPreview(); 372 | 373 | return this; 374 | }, 375 | 376 | createToolbar: function () { 377 | var toolbar = document.createElement('div'); 378 | toolbar.id = 'medium-editor-toolbar-' + this.id; 379 | toolbar.className = 'medium-editor-toolbar'; 380 | toolbar.appendChild(this.toolbarButtons()); 381 | toolbar.appendChild(this.toolbarFormAnchor()); 382 | document.body.appendChild(toolbar); 383 | return toolbar; 384 | }, 385 | 386 | //TODO: actionTemplate 387 | toolbarButtons: function () { 388 | var btns = this.options.buttons, 389 | ul = document.createElement('ul'), 390 | li, 391 | i, 392 | btn, 393 | ext; 394 | 395 | ul.id = 'medium-editor-toolbar-actions'; 396 | ul.className = 'medium-editor-toolbar-actions clearfix'; 397 | 398 | for (i = 0; i < btns.length; i += 1) { 399 | if (this.options.extensions.hasOwnProperty(btns[i])) { 400 | ext = this.options.extensions[btns[i]]; 401 | btn = ext.getButton !== undefined ? ext.getButton() : null; 402 | } else { 403 | btn = this.buttonTemplate(btns[i]); 404 | } 405 | 406 | if (btn) { 407 | li = document.createElement('li'); 408 | if (isElement(btn)) { 409 | li.appendChild(btn); 410 | } else { 411 | li.innerHTML = btn; 412 | } 413 | ul.appendChild(li); 414 | } 415 | } 416 | 417 | return ul; 418 | }, 419 | 420 | toolbarFormAnchor: function () { 421 | var anchor = document.createElement('div'), 422 | input = document.createElement('input'), 423 | a = document.createElement('a'); 424 | 425 | a.setAttribute('href', '#'); 426 | a.innerHTML = '×'; 427 | 428 | input.setAttribute('type', 'text'); 429 | input.setAttribute('placeholder', this.options.anchorInputPlaceholder); 430 | 431 | anchor.className = 'medium-editor-toolbar-form-anchor'; 432 | anchor.id = 'medium-editor-toolbar-form-anchor'; 433 | anchor.appendChild(input); 434 | anchor.appendChild(a); 435 | 436 | return anchor; 437 | }, 438 | 439 | bindSelect: function () { 440 | var self = this, 441 | timer = '', 442 | i; 443 | 444 | this.checkSelectionWrapper = function (e) { 445 | 446 | // Do not close the toolbar when bluring the editable area and clicking into the anchor form 447 | if (e && self.clickingIntoArchorForm(e)) { 448 | return false; 449 | } 450 | 451 | clearTimeout(timer); 452 | timer = setTimeout(function () { 453 | self.checkSelection(); 454 | }, self.options.delay); 455 | }; 456 | 457 | document.documentElement.addEventListener('mouseup', this.checkSelectionWrapper); 458 | 459 | for (i = 0; i < this.elements.length; i += 1) { 460 | this.elements[i].addEventListener('keyup', this.checkSelectionWrapper); 461 | this.elements[i].addEventListener('blur', this.checkSelectionWrapper); 462 | } 463 | return this; 464 | }, 465 | 466 | checkSelection: function () { 467 | var newSelection, 468 | selectionElement; 469 | 470 | if (this.keepToolbarAlive !== true && !this.options.disableToolbar) { 471 | newSelection = window.getSelection(); 472 | if (newSelection.toString().trim() === '' || 473 | (this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs())) { 474 | this.hideToolbarActions(); 475 | } else { 476 | selectionElement = this.getSelectionElement(); 477 | if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) { 478 | this.hideToolbarActions(); 479 | } else { 480 | this.checkSelectionElement(newSelection, selectionElement); 481 | } 482 | } 483 | } 484 | return this; 485 | }, 486 | 487 | clickingIntoArchorForm: function (e) { 488 | var self = this; 489 | if (e.type && e.type.toLowerCase() === 'blur' && e.relatedTarget && e.relatedTarget === self.anchorInput) { 490 | return true; 491 | } 492 | return false; 493 | }, 494 | 495 | hasMultiParagraphs: function () { 496 | var selectionHtml = getSelectionHtml().replace(/<[\S]+><\/[\S]+>/gim, ''), 497 | hasMultiParagraphs = selectionHtml.match(/<(p|h[0-6]|blockquote)>([\s\S]*?)<\/(p|h[0-6]|blockquote)>/g); 498 | 499 | return (hasMultiParagraphs ? hasMultiParagraphs.length : 0); 500 | }, 501 | 502 | checkSelectionElement: function (newSelection, selectionElement) { 503 | var i; 504 | this.selection = newSelection; 505 | this.selectionRange = this.selection.getRangeAt(0); 506 | for (i = 0; i < this.elements.length; i += 1) { 507 | if (this.elements[i] === selectionElement) { 508 | this.setToolbarButtonStates() 509 | .setToolbarPosition() 510 | .showToolbarActions(); 511 | return; 512 | } 513 | } 514 | this.hideToolbarActions(); 515 | }, 516 | 517 | getSelectionElement: function () { 518 | var selection = window.getSelection(), 519 | range, current, parent, 520 | result, 521 | getMediumElement = function (e) { 522 | var localParent = e; 523 | try { 524 | while (!localParent.getAttribute('data-medium-element')) { 525 | localParent = localParent.parentNode; 526 | } 527 | } catch (errb) { 528 | return false; 529 | } 530 | return localParent; 531 | }; 532 | // First try on current node 533 | try { 534 | range = selection.getRangeAt(0); 535 | current = range.commonAncestorContainer; 536 | parent = current.parentNode; 537 | 538 | if (current.getAttribute('data-medium-element')) { 539 | result = current; 540 | } else { 541 | result = getMediumElement(parent); 542 | } 543 | // If not search in the parent nodes. 544 | } catch (err) { 545 | result = getMediumElement(parent); 546 | } 547 | return result; 548 | }, 549 | 550 | setToolbarPosition: function () { 551 | var buttonHeight = 50, 552 | selection = window.getSelection(), 553 | range = selection.getRangeAt(0), 554 | boundary = range.getBoundingClientRect(), 555 | defaultLeft = (this.options.diffLeft) - (this.toolbar.offsetWidth / 2), 556 | middleBoundary = (boundary.left + boundary.right) / 2, 557 | halfOffsetWidth = this.toolbar.offsetWidth / 2; 558 | if (boundary.top < buttonHeight) { 559 | this.toolbar.classList.add('medium-toolbar-arrow-over'); 560 | this.toolbar.classList.remove('medium-toolbar-arrow-under'); 561 | this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + window.pageYOffset - this.toolbar.offsetHeight + 'px'; 562 | } else { 563 | this.toolbar.classList.add('medium-toolbar-arrow-under'); 564 | this.toolbar.classList.remove('medium-toolbar-arrow-over'); 565 | this.toolbar.style.top = boundary.top + this.options.diffTop + window.pageYOffset - this.toolbar.offsetHeight + 'px'; 566 | } 567 | if (middleBoundary < halfOffsetWidth) { 568 | this.toolbar.style.left = defaultLeft + halfOffsetWidth + 'px'; 569 | } else if ((window.innerWidth - middleBoundary) < halfOffsetWidth) { 570 | this.toolbar.style.left = window.innerWidth + defaultLeft - halfOffsetWidth + 'px'; 571 | } else { 572 | this.toolbar.style.left = defaultLeft + middleBoundary + 'px'; 573 | } 574 | 575 | this.hideAnchorPreview(); 576 | 577 | return this; 578 | }, 579 | 580 | setToolbarButtonStates: function () { 581 | var buttons = this.toolbarActions.querySelectorAll('button'), 582 | i; 583 | for (i = 0; i < buttons.length; i += 1) { 584 | buttons[i].classList.remove('medium-editor-button-active'); 585 | } 586 | this.checkActiveButtons(); 587 | return this; 588 | }, 589 | 590 | checkActiveButtons: function () { 591 | var elements = Array.prototype.slice.call(this.elements), 592 | parentNode = this.selection.anchorNode; 593 | if (!parentNode.tagName) { 594 | parentNode = this.selection.anchorNode.parentNode; 595 | } 596 | while (parentNode.tagName !== undefined && this.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) { 597 | this.activateButton(parentNode.tagName.toLowerCase()); 598 | this.callExtensions('checkState', parentNode); 599 | 600 | // we can abort the search upwards if we leave the contentEditable element 601 | if (elements.indexOf(parentNode) !== -1) { 602 | break; 603 | } 604 | parentNode = parentNode.parentNode; 605 | } 606 | }, 607 | 608 | activateButton: function (tag) { 609 | var el = this.toolbar.querySelector('[data-element="' + tag + '"]'); 610 | if (el !== null && el.className.indexOf('medium-editor-button-active') === -1) { 611 | el.className += ' medium-editor-button-active'; 612 | } 613 | }, 614 | 615 | bindButtons: function () { 616 | var buttons = this.toolbar.querySelectorAll('button'), 617 | i, 618 | self = this, 619 | triggerAction = function (e) { 620 | e.preventDefault(); 621 | e.stopPropagation(); 622 | if (self.selection === undefined) { 623 | self.checkSelection(); 624 | } 625 | if (this.className.indexOf('medium-editor-button-active') > -1) { 626 | this.classList.remove('medium-editor-button-active'); 627 | } else { 628 | this.className += ' medium-editor-button-active'; 629 | } 630 | if (this.hasAttribute('data-action')) { 631 | self.execAction(this.getAttribute('data-action'), e); 632 | } 633 | }; 634 | for (i = 0; i < buttons.length; i += 1) { 635 | buttons[i].addEventListener('click', triggerAction); 636 | } 637 | this.setFirstAndLastItems(buttons); 638 | return this; 639 | }, 640 | 641 | setFirstAndLastItems: function (buttons) { 642 | if (buttons.length > 0) { 643 | buttons[0].className += ' medium-editor-button-first'; 644 | buttons[buttons.length - 1].className += ' medium-editor-button-last'; 645 | } 646 | return this; 647 | }, 648 | 649 | execAction: function (action, e) { 650 | if (action.indexOf('append-') > -1) { 651 | this.execFormatBlock(action.replace('append-', '')); 652 | this.setToolbarPosition(); 653 | this.setToolbarButtonStates(); 654 | } else if (action === 'anchor') { 655 | this.triggerAnchorAction(e); 656 | } else if (action === 'image') { 657 | document.execCommand('insertImage', false, window.getSelection()); 658 | } else { 659 | document.execCommand(action, false, null); 660 | this.setToolbarPosition(); 661 | } 662 | }, 663 | 664 | triggerAnchorAction: function () { 665 | if (this.selection.anchorNode.parentNode.tagName.toLowerCase() === 'a') { 666 | document.execCommand('unlink', false, null); 667 | } else { 668 | if (this.anchorForm.style.display === 'block') { 669 | this.showToolbarActions(); 670 | } else { 671 | this.showAnchorForm(); 672 | } 673 | } 674 | return this; 675 | }, 676 | 677 | execFormatBlock: function (el) { 678 | var selectionData = this.getSelectionData(this.selection.anchorNode); 679 | // FF handles blockquote differently on formatBlock 680 | // allowing nesting, we need to use outdent 681 | // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla 682 | if (el === 'blockquote' && selectionData.el && 683 | selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') { 684 | return document.execCommand('outdent', false, null); 685 | } 686 | if (selectionData.tagName === el) { 687 | el = 'p'; 688 | } 689 | // When IE we need to add <> to heading elements and 690 | // blockquote needs to be called as indent 691 | // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie 692 | // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777 693 | if (this.isIE) { 694 | if (el === 'blockquote') { 695 | return document.execCommand('indent', false, el); 696 | } 697 | el = '<' + el + '>'; 698 | } 699 | return document.execCommand('formatBlock', false, el); 700 | }, 701 | 702 | getSelectionData: function (el) { 703 | var tagName; 704 | 705 | if (el && el.tagName) { 706 | tagName = el.tagName.toLowerCase(); 707 | } 708 | 709 | while (el && this.parentElements.indexOf(tagName) === -1) { 710 | el = el.parentNode; 711 | if (el && el.tagName) { 712 | tagName = el.tagName.toLowerCase(); 713 | } 714 | } 715 | 716 | return { 717 | el: el, 718 | tagName: tagName 719 | }; 720 | }, 721 | 722 | getFirstChild: function (el) { 723 | var firstChild = el.firstChild; 724 | while (firstChild !== null && firstChild.nodeType !== 1) { 725 | firstChild = firstChild.nextSibling; 726 | } 727 | return firstChild; 728 | }, 729 | 730 | hideToolbarActions: function () { 731 | this.keepToolbarAlive = false; 732 | if (this.toolbar !== undefined) { 733 | this.toolbar.classList.remove('medium-editor-toolbar-active'); 734 | } 735 | }, 736 | 737 | showToolbarActions: function () { 738 | var self = this, 739 | timer; 740 | this.anchorForm.style.display = 'none'; 741 | this.toolbarActions.style.display = 'block'; 742 | this.keepToolbarAlive = false; 743 | clearTimeout(timer); 744 | timer = setTimeout(function () { 745 | if (self.toolbar && !self.toolbar.classList.contains('medium-editor-toolbar-active')) { 746 | self.toolbar.classList.add('medium-editor-toolbar-active'); 747 | } 748 | }, 100); 749 | }, 750 | 751 | showAnchorForm: function (link_value) { 752 | this.toolbarActions.style.display = 'none'; 753 | this.savedSelection = saveSelection(); 754 | this.anchorForm.style.display = 'block'; 755 | this.keepToolbarAlive = true; 756 | this.anchorInput.focus(); 757 | this.anchorInput.value = link_value || ''; 758 | }, 759 | 760 | bindAnchorForm: function () { 761 | var linkCancel = this.anchorForm.querySelector('a'), 762 | self = this; 763 | this.anchorForm.addEventListener('click', function (e) { 764 | e.stopPropagation(); 765 | }); 766 | this.anchorInput.addEventListener('keyup', function (e) { 767 | if (e.keyCode === 13) { 768 | e.preventDefault(); 769 | self.createLink(this); 770 | } 771 | }); 772 | this.anchorInput.addEventListener('click', function (e) { 773 | // make sure not to hide form when cliking into the input 774 | e.stopPropagation(); 775 | self.keepToolbarAlive = true; 776 | }); 777 | this.anchorInput.addEventListener('blur', function () { 778 | self.keepToolbarAlive = false; 779 | self.checkSelection(); 780 | }); 781 | linkCancel.addEventListener('click', function (e) { 782 | e.preventDefault(); 783 | self.showToolbarActions(); 784 | restoreSelection(self.savedSelection); 785 | }); 786 | return this; 787 | }, 788 | 789 | 790 | hideAnchorPreview: function () { 791 | this.anchorPreview.classList.remove('medium-editor-anchor-preview-active'); 792 | }, 793 | 794 | // TODO: break method 795 | showAnchorPreview: function (anchorEl) { 796 | if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) { 797 | return true; 798 | } 799 | 800 | var self = this, 801 | buttonHeight = 40, 802 | boundary = anchorEl.getBoundingClientRect(), 803 | middleBoundary = (boundary.left + boundary.right) / 2, 804 | halfOffsetWidth, 805 | defaultLeft, 806 | timer; 807 | 808 | self.anchorPreview.querySelector('i').textContent = anchorEl.href; 809 | halfOffsetWidth = self.anchorPreview.offsetWidth / 2; 810 | defaultLeft = self.options.diffLeft - halfOffsetWidth; 811 | 812 | clearTimeout(timer); 813 | timer = setTimeout(function () { 814 | if (self.anchorPreview && !self.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) { 815 | self.anchorPreview.classList.add('medium-editor-anchor-preview-active'); 816 | } 817 | }, 100); 818 | 819 | self.observeAnchorPreview(anchorEl); 820 | 821 | self.anchorPreview.classList.add('medium-toolbar-arrow-over'); 822 | self.anchorPreview.classList.remove('medium-toolbar-arrow-under'); 823 | self.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - self.options.diffTop + window.pageYOffset - self.anchorPreview.offsetHeight) + 'px'; 824 | if (middleBoundary < halfOffsetWidth) { 825 | self.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px'; 826 | } else if ((window.innerWidth - middleBoundary) < halfOffsetWidth) { 827 | self.anchorPreview.style.left = window.innerWidth + defaultLeft - halfOffsetWidth + 'px'; 828 | } else { 829 | self.anchorPreview.style.left = defaultLeft + middleBoundary + 'px'; 830 | } 831 | 832 | return this; 833 | }, 834 | 835 | // TODO: break method 836 | observeAnchorPreview: function (anchorEl) { 837 | var self = this, 838 | lastOver = (new Date()).getTime(), 839 | over = true, 840 | stamp = function () { 841 | lastOver = (new Date()).getTime(); 842 | over = true; 843 | }, 844 | unstamp = function (e) { 845 | if (!e.relatedTarget || !/anchor-preview/.test(e.relatedTarget.className)) { 846 | over = false; 847 | } 848 | }, 849 | interval_timer = setInterval(function () { 850 | if (over) { 851 | return true; 852 | } 853 | var durr = (new Date()).getTime() - lastOver; 854 | if (durr > self.options.anchorPreviewHideDelay) { 855 | // hide the preview 1/2 second after mouse leaves the link 856 | self.hideAnchorPreview(); 857 | 858 | // cleanup 859 | clearInterval(interval_timer); 860 | self.anchorPreview.removeEventListener('mouseover', stamp); 861 | self.anchorPreview.removeEventListener('mouseout', unstamp); 862 | anchorEl.removeEventListener('mouseover', stamp); 863 | anchorEl.removeEventListener('mouseout', unstamp); 864 | 865 | } 866 | }, 200); 867 | 868 | self.anchorPreview.addEventListener('mouseover', stamp); 869 | self.anchorPreview.addEventListener('mouseout', unstamp); 870 | anchorEl.addEventListener('mouseover', stamp); 871 | anchorEl.addEventListener('mouseout', unstamp); 872 | }, 873 | 874 | createAnchorPreview: function () { 875 | var self = this, 876 | anchorPreview = document.createElement('div'); 877 | 878 | anchorPreview.id = 'medium-editor-anchor-preview-' + this.id; 879 | anchorPreview.className = 'medium-editor-anchor-preview'; 880 | anchorPreview.innerHTML = this.anchorPreviewTemplate(); 881 | document.body.appendChild(anchorPreview); 882 | 883 | anchorPreview.addEventListener('click', function () { 884 | self.anchorPreviewClickHandler(); 885 | }); 886 | 887 | return anchorPreview; 888 | }, 889 | 890 | anchorPreviewTemplate: function () { 891 | return '
' + 892 | ' ' + 893 | '
'; 894 | }, 895 | 896 | anchorPreviewClickHandler: function (e) { 897 | if (this.activeAnchor) { 898 | 899 | var self = this, 900 | range = document.createRange(), 901 | sel = window.getSelection(); 902 | 903 | range.selectNodeContents(self.activeAnchor); 904 | sel.removeAllRanges(); 905 | sel.addRange(range); 906 | setTimeout(function () { 907 | if (self.activeAnchor) { 908 | self.showAnchorForm(self.activeAnchor.href); 909 | } 910 | self.keepToolbarAlive = false; 911 | }, 100 + self.options.delay); 912 | 913 | } 914 | 915 | this.hideAnchorPreview(); 916 | }, 917 | 918 | editorAnchorObserver: function (e) { 919 | var self = this, 920 | overAnchor = true, 921 | leaveAnchor = function () { 922 | // mark the anchor as no longer hovered, and stop listening 923 | overAnchor = false; 924 | self.activeAnchor.removeEventListener('mouseout', leaveAnchor); 925 | }; 926 | 927 | if (e.target && e.target.tagName.toLowerCase() === 'a') { 928 | 929 | // Detect empty href attributes 930 | // The browser will make href="" or href="#top" 931 | // into absolute urls when accessed as e.targed.href, so check the html 932 | if (!/href=["']\S+["']/.test(e.target.outerHTML) || /href=["']#\S+["']/.test(e.target.outerHTML)) { 933 | return true; 934 | } 935 | 936 | // only show when hovering on anchors 937 | if (this.toolbar.classList.contains('medium-editor-toolbar-active')) { 938 | // only show when toolbar is not present 939 | return true; 940 | } 941 | this.activeAnchor = e.target; 942 | this.activeAnchor.addEventListener('mouseout', leaveAnchor); 943 | // show the anchor preview according to the configured delay 944 | // if the mouse has not left the anchor tag in that time 945 | setTimeout(function () { 946 | if (overAnchor) { 947 | self.showAnchorPreview(e.target); 948 | } 949 | }, self.options.delay); 950 | 951 | 952 | } 953 | }, 954 | 955 | bindAnchorPreview: function (index) { 956 | var i, self = this; 957 | this.editorAnchorObserverWrapper = function (e) { 958 | self.editorAnchorObserver(e); 959 | }; 960 | for (i = 0; i < this.elements.length; i += 1) { 961 | this.elements[i].addEventListener('mouseover', this.editorAnchorObserverWrapper); 962 | } 963 | return this; 964 | }, 965 | 966 | checkLinkFormat: function (value) { 967 | var re = /^https?:\/\//; 968 | if (value.match(re)) { 969 | return value; 970 | } 971 | return "http://" + value; 972 | }, 973 | 974 | setTargetBlank: function () { 975 | var el = getSelectionStart(), 976 | i; 977 | if (el.tagName.toLowerCase() === 'a') { 978 | el.target = '_blank'; 979 | } else { 980 | el = el.getElementsByTagName('a'); 981 | for (i = 0; i < el.length; i += 1) { 982 | el[i].target = '_blank'; 983 | } 984 | } 985 | }, 986 | 987 | createLink: function (input) { 988 | restoreSelection(this.savedSelection); 989 | if (this.options.checkLinkFormat) { 990 | input.value = this.checkLinkFormat(input.value); 991 | } 992 | document.execCommand('createLink', false, input.value); 993 | if (this.options.targetBlank) { 994 | this.setTargetBlank(); 995 | } 996 | this.showToolbarActions(); 997 | input.value = ''; 998 | }, 999 | 1000 | bindWindowActions: function () { 1001 | var timerResize, 1002 | self = this; 1003 | this.windowResizeHandler = function () { 1004 | clearTimeout(timerResize); 1005 | timerResize = setTimeout(function () { 1006 | if (self.toolbar && self.toolbar.classList.contains('medium-editor-toolbar-active')) { 1007 | self.setToolbarPosition(); 1008 | } 1009 | }, 100); 1010 | }; 1011 | window.addEventListener('resize', this.windowResizeHandler); 1012 | return this; 1013 | }, 1014 | 1015 | activate: function () { 1016 | if (this.isActive) { 1017 | return; 1018 | } 1019 | 1020 | this.setup(); 1021 | }, 1022 | 1023 | // TODO: break method 1024 | deactivate: function () { 1025 | var i; 1026 | if (!this.isActive) { 1027 | return; 1028 | } 1029 | this.isActive = false; 1030 | 1031 | if (this.toolbar !== undefined) { 1032 | document.body.removeChild(this.anchorPreview); 1033 | document.body.removeChild(this.toolbar); 1034 | delete this.toolbar; 1035 | delete this.anchorPreview; 1036 | } 1037 | 1038 | document.documentElement.removeEventListener('mouseup', this.checkSelectionWrapper); 1039 | window.removeEventListener('resize', this.windowResizeHandler); 1040 | 1041 | for (i = 0; i < this.elements.length; i += 1) { 1042 | this.elements[i].removeEventListener('mouseover', this.editorAnchorObserverWrapper); 1043 | this.elements[i].removeEventListener('keyup', this.checkSelectionWrapper); 1044 | this.elements[i].removeEventListener('blur', this.checkSelectionWrapper); 1045 | this.elements[i].removeEventListener('paste', this.pasteWrapper); 1046 | this.elements[i].removeAttribute('contentEditable'); 1047 | this.elements[i].removeAttribute('data-medium-element'); 1048 | } 1049 | 1050 | }, 1051 | 1052 | htmlEntities: function (str) { 1053 | // converts special characters (like <) into their escaped/encoded values (like <). 1054 | // This allows you to show to display the string without the browser reading it as HTML. 1055 | return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); 1056 | }, 1057 | 1058 | bindPaste: function () { 1059 | var i, self = this; 1060 | this.pasteWrapper = function (e) { 1061 | var paragraphs, 1062 | html = '', 1063 | p; 1064 | 1065 | this.classList.remove('medium-editor-placeholder'); 1066 | if (!self.options.forcePlainText && !self.options.cleanPastedHTML) { 1067 | return this; 1068 | } 1069 | 1070 | if (e.clipboardData && e.clipboardData.getData && !e.defaultPrevented) { 1071 | e.preventDefault(); 1072 | 1073 | if (self.options.cleanPastedHTML && e.clipboardData.getData('text/html')) { 1074 | return self.cleanPaste(e.clipboardData.getData('text/html')); 1075 | } 1076 | if (!self.options.disableReturn) { 1077 | paragraphs = e.clipboardData.getData('text/plain').split(/[\r\n]/g); 1078 | for (p = 0; p < paragraphs.length; p += 1) { 1079 | if (paragraphs[p] !== '') { 1080 | if (navigator.userAgent.match(/firefox/i) && p === 0) { 1081 | html += self.htmlEntities(paragraphs[p]); 1082 | } else { 1083 | html += '

' + self.htmlEntities(paragraphs[p]) + '

'; 1084 | } 1085 | } 1086 | } 1087 | document.execCommand('insertHTML', false, html); 1088 | } else { 1089 | document.execCommand('insertHTML', false, e.clipboardData.getData('text/plain')); 1090 | } 1091 | } 1092 | }; 1093 | for (i = 0; i < this.elements.length; i += 1) { 1094 | this.elements[i].addEventListener('paste', this.pasteWrapper); 1095 | } 1096 | return this; 1097 | }, 1098 | 1099 | setPlaceholders: function () { 1100 | var i, 1101 | activatePlaceholder = function (el) { 1102 | if (el.textContent.replace(/^\s+|\s+$/g, '') === '') { 1103 | el.classList.add('medium-editor-placeholder'); 1104 | } 1105 | }, 1106 | placeholderWrapper = function (e) { 1107 | this.classList.remove('medium-editor-placeholder'); 1108 | if (e.type !== 'keypress') { 1109 | activatePlaceholder(this); 1110 | } 1111 | }; 1112 | for (i = 0; i < this.elements.length; i += 1) { 1113 | activatePlaceholder(this.elements[i]); 1114 | this.elements[i].addEventListener('blur', placeholderWrapper); 1115 | this.elements[i].addEventListener('keypress', placeholderWrapper); 1116 | } 1117 | return this; 1118 | }, 1119 | 1120 | cleanPaste: function (text) { 1121 | 1122 | /*jslint regexp: true*/ 1123 | /* 1124 | jslint does not allow character negation, because the negation 1125 | will not match any unicode characters. In the regexes in this 1126 | block, negation is used specifically to match the end of an html 1127 | tag, and in fact unicode characters *should* be allowed. 1128 | */ 1129 | var i, elList, workEl, 1130 | el = this.getSelectionElement(), 1131 | multiline = /]*docs-internal-guid[^>]*>/gi), ""], 1136 | [new RegExp(/<\/b>(]*>)?$/gi), ""], 1137 | 1138 | // un-html spaces and newlines inserted by OS X 1139 | [new RegExp(/\s+<\/span>/g), ' '], 1140 | [new RegExp(/
/g), '
'], 1141 | 1142 | // replace google docs italics+bold with a span to be replaced once the html is inserted 1143 | [new RegExp(/]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), ''], 1144 | 1145 | // replace google docs italics with a span to be replaced once the html is inserted 1146 | [new RegExp(/]*font-style:italic[^>]*>/gi), ''], 1147 | 1148 | //[replace google docs bolds with a span to be replaced once the html is inserted 1149 | [new RegExp(/]*font-weight:bold[^>]*>/gi), ''], 1150 | 1151 | // replace manually entered b/i/a tags with real ones 1152 | [new RegExp(/<(\/?)(i|b|a)>/gi), '<$1$2>'], 1153 | 1154 | // replace manually a tags with real ones, converting smart-quotes from google docs 1155 | [new RegExp(/<a\s+href=("|”|“|“|”)([^&]+)("|”|“|“|”)>/gi), ''] 1156 | 1157 | ]; 1158 | /*jslint regexp: false*/ 1159 | 1160 | for (i = 0; i < replacements.length; i += 1) { 1161 | text = text.replace(replacements[i][0], replacements[i][1]); 1162 | } 1163 | 1164 | if (multiline) { 1165 | 1166 | // double br's aren't converted to p tags, but we want paragraphs. 1167 | elList = text.split('

'); 1168 | 1169 | this.pasteHTML('

' + elList.join('

') + '

'); 1170 | document.execCommand('insertText', false, "\n"); 1171 | 1172 | // block element cleanup 1173 | elList = el.querySelectorAll('p,div,br'); 1174 | for (i = 0; i < elList.length; i += 1) { 1175 | 1176 | workEl = elList[i]; 1177 | 1178 | switch (workEl.tagName.toLowerCase()) { 1179 | case 'p': 1180 | case 'div': 1181 | this.filterCommonBlocks(workEl); 1182 | break; 1183 | case 'br': 1184 | this.filterLineBreak(workEl); 1185 | break; 1186 | } 1187 | 1188 | } 1189 | 1190 | 1191 | } else { 1192 | 1193 | this.pasteHTML(text); 1194 | 1195 | } 1196 | 1197 | }, 1198 | 1199 | pasteHTML: function (html) { 1200 | var elList, workEl, i, fragmentBody, pasteBlock = document.createDocumentFragment(); 1201 | 1202 | pasteBlock.appendChild(document.createElement('body')); 1203 | 1204 | fragmentBody = pasteBlock.querySelector('body'); 1205 | fragmentBody.innerHTML = html; 1206 | 1207 | this.cleanupSpans(fragmentBody); 1208 | 1209 | elList = fragmentBody.querySelectorAll('*'); 1210 | for (i = 0; i < elList.length; i += 1) { 1211 | 1212 | workEl = elList[i]; 1213 | 1214 | // delete ugly attributes 1215 | workEl.removeAttribute('class'); 1216 | workEl.removeAttribute('style'); 1217 | workEl.removeAttribute('dir'); 1218 | 1219 | if (workEl.tagName.toLowerCase() === 'meta') { 1220 | workEl.parentNode.removeChild(workEl); 1221 | } 1222 | 1223 | } 1224 | document.execCommand('insertHTML', false, fragmentBody.innerHTML.replace(/ /g, ' ')); 1225 | }, 1226 | isCommonBlock: function (el) { 1227 | return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div')); 1228 | }, 1229 | filterCommonBlocks: function (el) { 1230 | if (/^\s*$/.test(el.innerText)) { 1231 | el.parentNode.removeChild(el); 1232 | } 1233 | }, 1234 | filterLineBreak: function (el) { 1235 | if (this.isCommonBlock(el.previousElementSibling)) { 1236 | 1237 | // remove stray br's following common block elements 1238 | el.parentNode.removeChild(el); 1239 | 1240 | } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) { 1241 | 1242 | // remove br's just inside open or close tags of a div/p 1243 | el.parentNode.removeChild(el); 1244 | 1245 | } else if (el.parentNode.childElementCount === 1) { 1246 | 1247 | // and br's that are the only child of a div/p 1248 | this.removeWithParent(el); 1249 | 1250 | } 1251 | 1252 | }, 1253 | 1254 | // remove an element, including its parent, if it is the only element within its parent 1255 | removeWithParent: function (el) { 1256 | if (el && el.parentNode) { 1257 | if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) { 1258 | el.parentNode.parentNode.removeChild(el.parentNode); 1259 | } else { 1260 | el.parentNode.removeChild(el.parentNode); 1261 | } 1262 | } 1263 | }, 1264 | 1265 | cleanupSpans: function (container_el) { 1266 | 1267 | var i, 1268 | el, 1269 | new_el, 1270 | spans = container_el.querySelectorAll('.replace-with'); 1271 | 1272 | for (i = 0; i < spans.length; i += 1) { 1273 | 1274 | el = spans[i]; 1275 | new_el = document.createElement(el.classList.contains('bold') ? 'b' : 'i'); 1276 | 1277 | if (el.classList.contains('bold') && el.classList.contains('italic')) { 1278 | 1279 | // add an i tag as well if this has both italics and bold 1280 | new_el.innerHTML = '' + el.innerHTML + ''; 1281 | 1282 | } else { 1283 | 1284 | new_el.innerHTML = el.innerHTML; 1285 | 1286 | } 1287 | el.parentNode.replaceChild(new_el, el); 1288 | 1289 | } 1290 | 1291 | spans = container_el.querySelectorAll('span'); 1292 | for (i = 0; i < spans.length; i += 1) { 1293 | 1294 | el = spans[i]; 1295 | 1296 | // remove empty spans, replace others with their contents 1297 | if (/^\s*$/.test()) { 1298 | el.parentNode.removeChild(el); 1299 | } else { 1300 | el.parentNode.replaceChild(document.createTextNode(el.innerText), el); 1301 | } 1302 | 1303 | } 1304 | 1305 | } 1306 | 1307 | }; 1308 | 1309 | }(window, document)); 1310 | --------------------------------------------------------------------------------