1/** 2 * WideArea v0.2.0 3 * https://github.com/usablica/widearea 4 * MIT licensed 5 * 6 * Copyright (C) 2013 usabli.ca - By Afshin Mehrabani (@afshinmeh) 7 */ 8 9(function (root, factory) { 10 if (typeof exports === 'object') { 11 // CommonJS 12 factory(exports); 13 } else if (typeof define === 'function' && define.amd) { 14 // AMD. Register as an anonymous module. 15 define(['exports'], factory); 16 } else { 17 // Browser globals 18 factory(root); 19 } 20} (this, function (exports) { 21 //Default config/variables 22 var VERSION = '0.2.0'; 23 24 /** 25 * WideArea main class 26 * 27 * @class WideArea 28 */ 29 function WideArea(obj) { 30 this._targetElement = obj; 31 32 //starts with 1 33 this._wideAreaId = 1; 34 35 this._options = { 36 wideAreaAttr: 'data-widearea', 37 exitOnEsc: true, 38 defaultColorScheme: 'light', 39 closeIconLabel: 'Close WideArea', 40 changeThemeIconLabel: 'Toggle Color Scheme', 41 fullScreenIconLabel: 'WideArea Mode' 42 }; 43 44 _enable.call(this); 45 } 46 47 /** 48 * Enable the WideArea 49 * 50 * @api private 51 * @method _enable 52 */ 53 function _enable() { 54 var self = this, 55 //select all textareas in the target element 56 textAreaList = this._targetElement.querySelectorAll('textarea[' + this._options.wideAreaAttr + '=\'enable\']'), 57 58 // don't make functions within a loop. 59 fullscreenIconClickHandler = function() { 60 _enableFullScreen.call(self, this); 61 }; 62 63 //to hold all textareas in the page 64 this._textareas = []; 65 66 //then, change all textareas to widearea 67 for (var i = textAreaList.length - 1; i >= 0; i--) { 68 var currentTextArea = textAreaList[i]; 69 //create widearea wrapper element 70 var wideAreaWrapper = document.createElement('div'), 71 wideAreaIcons = document.createElement('div'), 72 fullscreenIcon = document.createElement('a'); 73 74 wideAreaWrapper.className = 'widearea-wrapper'; 75 wideAreaIcons.className = 'widearea-icons'; 76 fullscreenIcon.className = 'widearea-icon fullscreen'; 77 fullscreenIcon.title = this._options.fullScreenIconLabel; 78 79 //hack! 80 fullscreenIcon.href = 'javascript:void(0);'; 81 fullscreenIcon.draggable = false; 82 83 //bind to click event 84 fullscreenIcon.onclick = fullscreenIconClickHandler; 85 86 //add widearea class to textarea 87 currentTextArea.className = (currentTextArea.className + " widearea").replace(/^\s+|\s+$/g, ""); 88 89 //set wideArea id and increase the stepper 90 currentTextArea.setAttribute("data-widearea-id", this._wideAreaId); 91 wideAreaIcons.setAttribute("id", "widearea-" + this._wideAreaId); 92 ++this._wideAreaId; 93 94 //set icons panel position 95 _renewIconsPosition(currentTextArea, wideAreaIcons); 96 97 //append all prepared div(s) 98 wideAreaIcons.appendChild(fullscreenIcon); 99 wideAreaWrapper.appendChild(wideAreaIcons); 100 101 //and append it to the page 102 document.body.appendChild(wideAreaWrapper); 103 104 //add the textarea to internal variable 105 this._textareas.push(currentTextArea); 106 } 107 108 //set a timer to re-calculate the position of textareas, I don't know whether this is a good approach or not 109 this._timer = setInterval(function() { 110 for (var i = self._textareas.length - 1; i >= 0; i--) { 111 var currentTextArea = self._textareas[i]; 112 //get the related icon panel. Using `getElementById` for better performance 113 var wideAreaIcons = document.getElementById("widearea-" + currentTextArea.getAttribute("data-widearea-id")); 114 115 //get old position 116 var oldPosition = _getOffset(wideAreaIcons); 117 118 //get the new element's position 119 var currentTextareaPosition = _getOffset(currentTextArea); 120 121 //only set the new position of old positions changed 122 if((oldPosition.left - currentTextareaPosition.width + 21) != currentTextareaPosition.left || oldPosition.top != currentTextareaPosition.top) { 123 //set icons panel position 124 _renewIconsPosition(currentTextArea, wideAreaIcons, currentTextareaPosition); 125 } 126 }; 127 }, 200); 128 } 129 130 /** 131 * Set new position to icons panel 132 * 133 * @api private 134 * @method _renewIconsPosition 135 * @param {Object} textarea 136 * @param {Object} iconPanel 137 */ 138 function _renewIconsPosition(textarea, iconPanel, textAreaPosition) { 139 var currentTextareaPosition = textAreaPosition || _getOffset(textarea); 140 //set icon panel position 141 iconPanel.style.left = currentTextareaPosition.left + currentTextareaPosition.width - 36 + "px"; 142 iconPanel.style.top = currentTextareaPosition.top + "px"; 143 } 144 145 /** 146 * Get an element position on the page 147 * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966 148 * 149 * @api private 150 * @method _getOffset 151 * @param {Object} element 152 * @returns Element's position info 153 */ 154 function _getOffset(element) { 155 var elementPosition = {}; 156 157 //set width 158 elementPosition.width = element.offsetWidth; 159 160 //set height 161 elementPosition.height = element.offsetHeight; 162 163 //calculate element top and left 164 var _x = 0; 165 var _y = 0; 166 while(element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) { 167 _x += element.offsetLeft; 168 _y += element.offsetTop; 169 element = element.offsetParent; 170 } 171 //set top 172 elementPosition.top = _y; 173 //set left 174 elementPosition.left = _x; 175 176 return elementPosition; 177 } 178 179 /** 180 * Get an element CSS property on the page 181 * Thanks to JavaScript Kit: http://www.javascriptkit.com/dhtmltutors/dhtmlcascade4.shtml 182 * 183 * @api private 184 * @method _getPropValue 185 * @param {Object} element 186 * @param {String} propName 187 * @returns Element's property value 188 */ 189 function _getPropValue (element, propName) { 190 var propValue = ''; 191 if (element.currentStyle) { //IE 192 propValue = element.currentStyle[propName]; 193 } else if (document.defaultView && document.defaultView.getComputedStyle) { //Others 194 propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName); 195 } 196 197 //Prevent exception in IE 198 if(propValue.toLowerCase) { 199 return propValue.toLowerCase(); 200 } else { 201 return propValue; 202 } 203 } 204 205 /** 206 * FullScreen the textarea 207 * 208 * @api private 209 * @method _enableFullScreen 210 * @param {Object} link 211 */ 212 function _enableFullScreen(link) { 213 var self = this; 214 215 //first of all, get the textarea id 216 var wideAreaId = parseInt(link.parentNode.id.replace(/widearea\-/, "")); 217 218 //I don't know whether is this correct or not, but I think it's not a bad way 219 var targetTextarea = document.querySelector("textarea[data-widearea-id='" + wideAreaId + "']"); 220 221 //clone current textarea 222 var currentTextArea = targetTextarea.cloneNode(); 223 224 //add proper css class names 225 currentTextArea.className = ('widearea-fullscreen ' + targetTextarea.className).replace(/^\s+|\s+$/g, ""); 226 targetTextarea.className = ('widearea-fullscreened ' + targetTextarea.className).replace(/^\s+|\s+$/g, ""); 227 228 var controlPanel = document.createElement('div'); 229 controlPanel.className = 'widearea-controlPanel'; 230 231 //create close icon 232 var closeIcon = document.createElement('a'); 233 closeIcon.href = 'javascript:void(0);'; 234 closeIcon.className = 'widearea-icon close'; 235 closeIcon.title = this._options.closeIconLabel; 236 closeIcon.onclick = function(){ 237 _disableFullScreen.call(self); 238 }; 239 240 //disable dragging 241 closeIcon.draggable = false; 242 243 //create close icon 244 var changeThemeIcon = document.createElement('a'); 245 changeThemeIcon.href = 'javascript:void(0);'; 246 changeThemeIcon.className = 'widearea-icon changeTheme'; 247 changeThemeIcon.title = this._options.changeThemeIconLabel; 248 changeThemeIcon.onclick = function() { 249 _toggleColorScheme.call(self); 250 }; 251 252 //disable dragging 253 changeThemeIcon.draggable = false; 254 255 controlPanel.appendChild(closeIcon); 256 controlPanel.appendChild(changeThemeIcon); 257 258 //create overlay layer 259 var overlayLayer = document.createElement('div'); 260 overlayLayer.className = 'widearea-overlayLayer ' + this._options.defaultColorScheme; 261 262 //add controls to overlay layer 263 overlayLayer.appendChild(currentTextArea); 264 overlayLayer.appendChild(controlPanel); 265 266 //finally add it to the body 267 document.body.appendChild(overlayLayer); 268 269 //set the focus to textarea 270 currentTextArea.focus(); 271 272 //set the value of small textarea to fullscreen one 273 currentTextArea.value = targetTextarea.value; 274 275 //bind to keydown event 276 this._onKeyDown = function(e) { 277 if (e.keyCode === 27 && self._options.exitOnEsc) { 278 //escape key pressed 279 _disableFullScreen.call(self); 280 } 281 if (e.keyCode == 9) { 282 // tab key pressed 283 e.preventDefault(); 284 var selectionStart = currentTextArea.selectionStart; 285 currentTextArea.value = currentTextArea.value.substring(0, selectionStart) + "\t" + currentTextArea.value.substring(currentTextArea.selectionEnd); 286 currentTextArea.selectionEnd = selectionStart + 1; 287 } 288 }; 289 if (window.addEventListener) { 290 window.addEventListener('keydown', self._onKeyDown, true); 291 } else if (document.attachEvent) { //IE 292 document.attachEvent('onkeydown', self._onKeyDown); 293 } 294 } 295 296 /** 297 * Change/Toggle color scheme of WideArea 298 * 299 * @api private 300 * @method _toggleColorScheme 301 */ 302 function _toggleColorScheme() { 303 var overlayLayer = document.querySelector(".widearea-overlayLayer"); 304 if(/dark/gi.test(overlayLayer.className)) { 305 overlayLayer.className = overlayLayer.className.replace('dark', 'light'); 306 } else { 307 overlayLayer.className = overlayLayer.className.replace('light', 'dark'); 308 } 309 } 310 311 /** 312 * Close FullScreen 313 * 314 * @api private 315 * @method _disableFullScreen 316 */ 317 function _disableFullScreen() { 318 var smallTextArea = document.querySelector("textarea.widearea-fullscreened"); 319 var overlayLayer = document.querySelector(".widearea-overlayLayer"); 320 var fullscreenTextArea = overlayLayer.querySelector("textarea"); 321 322 //change the focus 323 smallTextArea.focus(); 324 325 //set fullscreen textarea to small one 326 smallTextArea.value = fullscreenTextArea.value; 327 328 //reset class for targeted text 329 smallTextArea.className = smallTextArea.className.replace(/widearea-fullscreened/gi, "").replace(/^\s+|\s+$/g, ""); 330 331 //and then remove the overlay layer 332 overlayLayer.parentNode.removeChild(overlayLayer); 333 334 //clean listeners 335 if (window.removeEventListener) { 336 window.removeEventListener('keydown', this._onKeyDown, true); 337 } else if (document.detachEvent) { //IE 338 document.detachEvent('onkeydown', this._onKeyDown); 339 } 340 } 341 342 /** 343 * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1 344 * 345 * @param obj1 346 * @param obj2 347 * @returns obj3 a new object based on obj1 and obj2 348 */ 349 function _mergeOptions(obj1, obj2) { 350 var obj3 = {}, attrname; 351 for (attrname in obj1) { obj3[attrname] = obj1[attrname]; } 352 for (attrname in obj2) { obj3[attrname] = obj2[attrname]; } 353 return obj3; 354 } 355 356 var wideArea = function (selector) { 357 if (typeof (selector) === 'string') { 358 //select the target element with query selector 359 var targetElement = document.querySelector(selector); 360 361 if (targetElement) { 362 return new WideArea(targetElement); 363 } else { 364 throw new Error('There is no element with given selector.'); 365 } 366 } else { 367 return new WideArea(document.body); 368 } 369 }; 370 371 /** 372 * Current WideArea version 373 * 374 * @property version 375 * @type String 376 */ 377 wideArea.version = VERSION; 378 379 //Prototype 380 wideArea.fn = WideArea.prototype = { 381 clone: function () { 382 return new WideArea(this); 383 }, 384 setOption: function(option, value) { 385 this._options[option] = value; 386 return this; 387 }, 388 setOptions: function(options) { 389 this._options = _mergeOptions(this._options, options); 390 return this; 391 } 392 }; 393 394 exports.wideArea = wideArea; 395 return wideArea; 396})); 397