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