UNPKG

wise-web-client

Version:

Based on Spine MVC framework

427 lines (363 loc) 13.1 kB
// A customized etchjs for reflen // change save function to adopte 2 step model save // by homer quan (contact@homerquan.com) (function() { 'use strict'; var models = {}, views = {}, collections = {}, etch = {}; // versioning as per semver.org etch.VERSION = '0.6.3'; etch.config = { // selector to specify editable elements selector: '.editable', // Named sets of buttons to be specified on the editable element // in the markup as "data-button-class" buttonClasses: { 'default': ['save'], 'all': ['bold', 'italic', 'underline', 'unordered-list', 'ordered-list', 'link', 'clear-formatting', 'save'], 'title': ['bold', 'italic', 'underline', 'save'] }, // data-field in dom attribute to bind model attribute dataField: 'data-field' }; models.Editor = Backbone.Model; views.Editor = Backbone.View.extend({ initialize: function() { this.$el = $(this.el); // Model attribute event listeners: _.bindAll(this, 'changeButtons', 'changePosition', 'changeEditable', 'insertImage'); this.model.bind('change:buttons', this.changeButtons); this.model.bind('change:position', this.changePosition); this.model.bind('change:editable', this.changeEditable); // Init Routines: this.changeEditable(); }, events: { 'click .etch-bold': 'toggleBold', 'click .etch-italic': 'toggleItalic', 'click .etch-underline': 'toggleUnderline', 'click .etch-heading': 'toggleHeading', 'click .etch-unordered-list': 'toggleUnorderedList', 'click .etch-justify-left': 'justifyLeft', 'click .etch-justify-center': 'justifyCenter', 'click .etch-justify-right': 'justifyRight', 'click .etch-ordered-list': 'toggleOrderedList', 'click .etch-link': 'toggleLink', 'click .etch-image': 'getImage', 'click .etch-save': 'save', 'click .etch-clear-formatting': 'clearFormatting' }, changeEditable: function() { this.setButtonClass(); // Im assuming that Ill add more functionality here }, setButtonClass: function() { // check the button class of the element being edited and set the associated buttons on the model var editorModel = this.model; var buttonClass = editorModel.get('editable').attr('data-button-class') || 'default'; editorModel.set({ buttons: etch.config.buttonClasses[buttonClass] }); }, changeButtons: function() { // render the buttons into the editor-panel this.$el.empty(); var view = this; var buttons = this.model.get('buttons'); // hide editor panel if there are no buttons in it and exit early if (!buttons.length) { $(this.el).hide(); return; } _.each(this.model.get('buttons'), function(button) { var $buttonEl = $('<a href="#" data-bypass="true" class="etch-editor-button etch-' + button + '" title="' + button.replace('-', ' ') + '"><span></span></a>'); view.$el.append($buttonEl); }); $(this.el).show('fast'); }, changePosition: function() { // animate editor-panel to new position var pos = this.model.get('position'); this.$el.animate({ 'top': pos.y, 'left': pos.x }, { queue: false }); }, wrapSelection: function(selectionOrRange, elString, cb) { // wrap current selection with elString tag var range = selectionOrRange === Range ? selectionOrRange : selectionOrRange.getRangeAt(0); var el = document.createElement(elString); range.surroundContents(el); }, clearFormatting: function(e) { e.preventDefault(); document.execCommand('removeFormat', false, null); }, toggleBold: function(e) { e.preventDefault(); document.execCommand('bold', false, null); }, toggleItalic: function(e) { e.preventDefault(); document.execCommand('italic', false, null); }, toggleUnderline: function(e) { e.preventDefault(); document.execCommand('underline', false, null); }, toggleHeading: function(e) { e.preventDefault(); var range = window.getSelection().getRangeAt(0); var wrapper = range.commonAncestorContainer.parentElement if ($(wrapper).is('h3')) { $(wrapper).replaceWith(wrapper.textContent) return; } var h3 = document.createElement('h3'); range.surroundContents(h3); }, urlPrompt: function(callback) { // This uses the default browser UI prompt to get a url. // Override this function if you want to implement a custom UI. var url = prompt('Enter a url', 'http://'); // Ensure a new link URL starts with http:// or https:// // before it's added to the DOM. // // NOTE: This implementation will disallow relative URLs from being added // but will make it easier for users typing external URLs. if (/^((http|https)...)/.test(url)) { callback(url); } else { callback("http://" + url); } }, toggleLink: function(e) { e.preventDefault(); var range = window.getSelection().getRangeAt(0); // are we in an anchor element? if (range.startContainer.parentNode.tagName === 'A' || range.endContainer.parentNode.tagName === 'A') { // unlink anchor document.execCommand('unlink', false, null); } else { // promt for url and create link this.urlPrompt(function(url) { document.execCommand('createLink', false, url); }); } }, toggleUnorderedList: function(e) { e.preventDefault(); document.execCommand('insertUnorderedList', false, null); }, toggleOrderedList: function(e) { e.preventDefault(); document.execCommand('insertOrderedList', false, null); }, justifyLeft: function(e) { e.preventDefault(); document.execCommand('justifyLeft', false, null); }, justifyCenter: function(e) { e.preventDefault(); document.execCommand('justifyCenter', false, null); }, justifyRight: function(e) { e.preventDefault(); document.execCommand('justifyRight', false, null); }, getImage: function(e) { e.preventDefault(); // call startUploader with callback to handle inserting it once it is uploaded/cropped this.startUploader(this.insertImage); }, startUploader: function(cb) { // initialize Image Uploader var model = new models.ImageUploader(); var view = new views.ImageUploader({ model: model }); // stash a reference to the callback to be called after image is uploaded model._imageCallback = function(image) { view.startCropper(image, cb); }; // stash reference to saved range for inserting the image once its this._savedRange = window.getSelection().getRangeAt(0); // insert uploader html into DOM $('body').append(view.render().el); }, insertImage: function(image) { // insert image - passed as a callback to startUploader var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(this._savedRange); var attrs = { 'editable': this.model.get('editable'), 'editableModel': this.model.get('editableModel') }; _.extend(attrs, image); var model = new models.EditableImage(attrs); var view = new views.EditableImage({ model: model }); this._savedRange.insertNode($(view.render().el).addClass('etch-float-left')[0]); }, save: function(e) { e.preventDefault(); var editableModel = this.model.get('editableModel'); var editableElem = this.model.get('editable'); var dataField = this.getField(); // update model attribute editableModel.set(dataField, this.getUpdateValue()); editableModel.trigger('updateField', dataField); }, getField: function() { return this.model.get('editable')[0].getAttribute(etch.config.dataField); }, getUpdateValue: function() { return this.model.get('editable')[0].innerHTML; } }); // tack on models, views, etc... as well as init function _.extend(etch, { models: models, views: views, collections: collections, // This function is to be used as callback to whatever event // you use to initialize editing editableInit: function(e) { e.stopPropagation(); var target = e.target || e.srcElement; var $editable = $(target).etchFindEditable(); $editable.attr('contenteditable', true); if ($(target).hasClass('first')) { $(target).empty(); $(target).removeClass('first'); } // if the editor isn't already built, build it var $editor = $('.etch-editor-panel'); var editorModel = $editor.data('model'); if (!$editor.size()) { $editor = $('<div class="etch-editor-panel">'); var editorAttrs = { editable: $editable, editableModel: this.model }; document.body.appendChild($editor[0]); $editor.etchInstantiate({ classType: 'Editor', attrs: editorAttrs }); editorModel = $editor.data('model'); // check if we are on a new editable } else if ($editable[0] !== editorModel.get('editable')[0]) { // set new editable editorModel.set({ editable: $editable, editableModel: this.model }); } // Firefox seems to be only browser that defaults to `StyleWithCSS == true` // so we turn it off here. Plus a try..catch to avoid an error being thrown in IE8. try { document.execCommand('StyleWithCSS', false, false); } catch (err) { // expecting to just eat IE8 error, but if different error, rethrow if (err.message !== "Invalid argument.") { throw err; } } if (models.EditableImage) { // instantiate any images that may be in the editable var $imgs = $editable.find('img'); if ($imgs.size()) { var attrs = { editable: $editable, editableModel: this.model }; $imgs.each(function() { var $this = $(this); if (!$this.data('editableImageModel')) { var editableImageModel = new models.EditableImage(attrs); var editableImageView = new views.EditableImage({ model: editableImageModel, el: this, tagName: this.tagName }); $this.data('editableImageModel', editableImageModel); } }); } } // listen for mousedowns that are not coming from the editor // and close the editor // unbind first to make sure we aren't doubling up on listeners $('body').unbind('mousedown.editor').bind('mousedown.editor', function(e) { // check to see if the click was in an etch tool var target = e.target || e.srcElement; if ($(target).not('.etch-editor-panel, .etch-editor-panel *, .etch-image-tools, .etch-image-tools *').size()) { // remove editor $editor.remove(); if (models.EditableImage) { // unblind the image-tools if the editor isn't active $editable.find('img').unbind('mouseenter'); // remove any latent image tool model references $(etch.config.selector + ' img').data('editableImageModel', false) } // once the editor is removed, remove the body binding for it $(this).unbind('mousedown.editor'); } }); editorModel.set({ position: { x: $(e.currentTarget).position().left, y: $(e.currentTarget).position().top + 20 } }); } }); // jquery helper functions $.fn.etchInstantiate = function(options, cb) { return this.each(function() { var $el = $(this); options || (options = {}); var settings = { el: this, attrs: {} } _.extend(settings, options); var model = new models[settings.classType](settings.attrs, settings); // initialize a view is there is one if (_.isFunction(views[settings.classType])) { var view = new views[settings.classType]({ model: model, el: this, tagName: this.tagName }); } // stash the model and view on the elements data object $el.data({ model: model }); $el.data({ view: view }); if (_.isFunction(cb)) { cb({ model: model, view: view }); } }); } $.fn.etchFindEditable = function() { // function that looks for the editable selector on itself or its parents // and returns that el when it is found var $el = $(this); return $el.is(etch.config.selector) ? $el : $el.closest(etch.config.selector); } window.etch = etch; })();