source: trunk/plugins/Stylist/Stylist.js @ 1332

Last change on this file since 1332 was 1332, checked in by gogo, 8 months ago

Pushing some old updates up from my current production system...

  • Add some iPhone/iPad fixes (no idea if these are still valid, I don't have one to test with).
  • Add new plugin event onBeforeSubmitTextarea, fired after the textarea get's its value set by outwardHtml, so you can do any last minute modifications.
  • Increase stylist delay to 500ms for reliability
  • Property svn:eol-style set to native
  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
File size: 18.8 KB
Line 
1/**
2 * Add an empty css_style to Config object's prototype
3 *  the format is { '.className' : 'Description' }
4 */
5
6Xinha.Config.prototype.css_style = { };
7
8/**
9 * This method loads an external stylesheet and uses it in the stylist
10 *
11 * @param string URL to the stylesheet
12 * @param hash Alternate descriptive names for your classes
13 *              { '.fooclass': 'Foo Description' }
14 * @param bool If set true then @import rules in the stylesheet are skipped,
15 *   otherwise they will be incorporated if possible.
16 */
17 
18Xinha.Config.prototype.stylistLoadStylesheet = function(url, altnames, skip_imports)
19{
20  if(!altnames) altnames = { };
21  var newStyles = Xinha.ripStylesFromCSSFile(url, skip_imports);
22  for(var i in newStyles)
23  {
24    if(altnames[i])
25    {
26      this.css_style[i] = altnames[i];
27    }
28    else
29    {
30      this.css_style[i] = newStyles[i];
31    }
32  }
33 
34  for(var x = 0; x < this.pageStyleSheets.length; x++)
35  {
36    if(this.pageStyleSheets[x] == url) return;
37  }
38  this.pageStyleSheets[this.pageStyleSheets.length] = url;
39};
40
41/**
42 * This method takes raw style definitions and uses them in the stylist
43 *
44 * @param string CSS
45 *
46 * @param hash Alternate descriptive names for your classes
47 *              { '.fooclass': 'Foo Description' }
48 *
49 * @param bool If set true then @import rules in the stylesheet are skipped,
50 *   otherwise they will be incorporated if possible.
51 *
52 * @param string If skip_imports is false, this string should contain
53 *   the "URL" of the stylesheet these styles came from (doesn't matter
54 *   if it exists or not), it is used when resolving relative URLs etc. 
55 *   If not provided, it defaults to Xinha.css in the Xinha root.
56 */
57 
58Xinha.Config.prototype.stylistLoadStyles = function(styles, altnames, skip_imports, imports_relative_to)
59{
60  if(!altnames) altnames = { };
61  var newStyles = Xinha.ripStylesFromCSSString(styles, skip_imports);
62  for(var i in newStyles)
63  {
64    if(altnames[i])
65    {
66      this.css_style[i] = altnames[i];
67    }
68    else
69    {
70      this.css_style[i] = newStyles[i];
71    }
72  }
73  this.pageStyle += styles;
74};
75
76
77
78/**
79 * Fill the stylist panel with styles that may be applied to the current selection.  Styles
80 * are supplied in the css_style property of the Xinha.Config object, which is in the format
81 * { '.className' : 'Description' }
82 * classes that are defined on a specific tag (eg 'a.email_link') are only shown in the panel
83 *    when an element of that type is selected.
84 * classes that are defined with selectors/psuedoclasses (eg 'a.email_link:hover') are never
85 *    shown (if you have an 'a.email_link' without the pseudoclass it will be shown of course)
86 * multiple classes (eg 'a.email_link.staff_member') are shown as a single class, and applied
87 *    to the element as multiple classes (class="email_link staff_member")
88 * you may click a class name in the stylist panel to add it, and click again to remove it
89 * you may add multiple classes to any element
90 * spans will be added where no single _and_entire_ element is selected
91 */
92Xinha.prototype._fillStylist = function()
93{
94  if(!this.plugins.Stylist.instance.dialog) return false;
95  var main = this.plugins.Stylist.instance.dialog.main;
96  main.innerHTML = '';
97
98  var may_apply = true;
99  var sel       = this._getSelection();
100
101  // What is applied
102  // var applied = this._getAncestorsClassNames(this._getSelection());
103
104  // Get an active element
105  var active_elem = this._activeElement(sel);
106
107  for(var x in this.config.css_style)
108  {
109    var tag   = null;
110    var className = x.trim();
111    var applicable = true;
112    var apply_to   = active_elem;
113
114    if(applicable && /[^a-zA-Z0-9_.-]/.test(className))
115    {
116      applicable = false; // Only basic classes are accepted, no selectors, etc.. presumed
117                          // that if you have a.foo:visited you'll also have a.foo
118      // alert('complex');
119    }
120
121    if(className.indexOf('.') < 0)
122    {
123      // No class name, just redefines a tag
124      applicable = false;
125    }
126
127    if(applicable && (className.indexOf('.') > 0))
128    {
129      // requires specific html tag
130      tag = className.substring(0, className.indexOf('.')).toLowerCase();
131      className = className.substring(className.indexOf('.'), className.length);
132
133      // To apply we must have an ancestor tag that is the right type
134      if(active_elem != null && active_elem.tagName.toLowerCase() == tag)
135      {
136        applicable = true;
137        apply_to = active_elem;
138      }
139      else
140      {
141        if(this._getFirstAncestor(this._getSelection(), [tag]) != null)
142        {
143          applicable = true;
144          apply_to = this._getFirstAncestor(this._getSelection(), [tag]);
145        }
146        else
147        {
148          // alert (this._getFirstAncestor(this._getSelection(), tag));
149          // If we don't have an ancestor, but it's a div/span/p/hx stle, we can make one
150          if(( tag == 'div' || tag == 'span' || tag == 'p'
151              || (tag.substr(0,1) == 'h' && tag.length == 2 && tag != 'hr')))
152          {
153            if(!this._selectionEmpty(this._getSelection()))
154            {
155              applicable = true;
156              apply_to = 'new';
157            }
158            else
159            {
160              // See if we can get a paragraph or header that can be converted
161              apply_to = this._getFirstAncestor(sel, ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7']);
162              if(apply_to != null)
163              {
164                applicable = true;
165              }
166              else
167              {
168                applicable = false;
169              }
170            }
171          }
172          else
173          {
174            applicable = false;
175          }
176        }
177      }
178    }
179
180    if(applicable)
181    {
182      // Remove the first .
183      className = className.substring(className.indexOf('.'), className.length);
184
185      // Replace any futher ones with spaces (for multiple class definitions)
186      className = className.replace('.', ' ');
187
188      if(apply_to == null)
189      {
190        if(this._selectionEmpty(this._getSelection()))
191        {
192          // Get the previous element and apply to that
193          apply_to = this._getFirstAncestor(this._getSelection(), null);
194        }
195        else
196        {
197          apply_to = 'new';
198          tag      = 'span';
199        }
200      }
201    }
202
203    var applied    = (this._ancestorsWithClasses(sel, tag, className).length > 0 ? true : false);
204    var applied_to = this._ancestorsWithClasses(sel, tag, className);
205
206    if(applicable)
207    {
208      var anch = document.createElement('a');
209      anch.onfocus = function () { this.blur() } // prevent dotted line around link that causes horizontal scrollbar
210      anch._stylist_className = className.trim();
211      anch._stylist_applied   = applied;
212      anch._stylist_appliedTo = applied_to;
213      anch._stylist_applyTo = apply_to;
214      anch._stylist_applyTag = tag;
215
216      anch.innerHTML = this.config.css_style[x];
217      anch.href = 'javascript:void(0)';
218      var editor = this;
219      anch.onclick = function()
220      {
221        if(this._stylist_applied == true)
222        {
223          editor._stylistRemoveClasses(this._stylist_className, this._stylist_appliedTo);
224        }
225        else
226        {
227          editor._stylistAddClasses(this._stylist_applyTo, this._stylist_applyTag, this._stylist_className);
228        }
229        return false;
230      }
231
232      anch.style.display = 'block';
233      anch.style.paddingLeft = '3px';
234      anch.style.paddingTop = '1px';
235      anch.style.paddingBottom = '1px';
236      anch.style.textDecoration = 'none';
237
238      if(applied)
239      {
240        anch.style.background = 'Highlight';
241        anch.style.color = 'HighlightText';
242      }
243      anch.style.position = 'relative';
244      main.appendChild(anch);
245    }
246  }
247};
248
249
250/**
251 * Add the given classes (space seperated list) to the currently selected element
252 * (will add a span if none selected)
253 */
254Xinha.prototype._stylistAddClasses = function(el, tag, classes)
255  {
256    if(el == 'new')
257    {
258      this.insertHTML('<' + tag + ' class="' + classes + '">' + this.getSelectedHTML() + '</' + tag + '>');
259    }
260    else
261    {
262      if(tag != null && el.tagName.toLowerCase() != tag)
263      {
264        // Have to change the tag!
265        var new_el = this.switchElementTag(el, tag);
266
267        if(typeof el._stylist_usedToBe != 'undefined')
268        {
269          new_el._stylist_usedToBe = el._stylist_usedToBe;
270          new_el._stylist_usedToBe[new_el._stylist_usedToBe.length] = {'tagName' : el.tagName, 'className' : el.getAttribute('class')};
271        }
272        else
273        {
274          new_el._stylist_usedToBe = [{'tagName' : el.tagName, 'className' : el.getAttribute('class')}];
275        }
276
277        Xinha.addClasses(new_el, classes);
278      }
279      else
280      {
281        Xinha._addClasses(el, classes);
282      }
283    }
284    this.focusEditor();
285    this.updateToolbar();
286  };
287
288/**
289 * Remove the given classes (space seperated list) from the given elements (array of elements)
290 */
291Xinha.prototype._stylistRemoveClasses = function(classes, from)
292  {
293    for(var x = 0; x < from.length; x++)
294    {
295      this._stylistRemoveClassesFull(from[x], classes);
296    }
297    this.focusEditor();
298    this.updateToolbar();
299  };
300
301Xinha.prototype._stylistRemoveClassesFull = function(el, classes)
302{
303  if(el != null)
304  {
305    var thiers = el.className.trim().split(' ');
306    var new_thiers = [ ];
307    var ours   = classes.split(' ');
308    for(var x = 0; x < thiers.length; x++)
309    {
310      var exists = false;
311      for(var i = 0; exists == false && i < ours.length; i++)
312      {
313        if(ours[i] == thiers[x])
314        {
315          exists = true;
316        }
317      }
318      if(exists == false)
319      {
320        new_thiers[new_thiers.length] = thiers[x];
321      }
322    }
323
324    if(new_thiers.length == 0 && el._stylist_usedToBe && el._stylist_usedToBe.length > 0 && el._stylist_usedToBe[el._stylist_usedToBe.length - 1].className != null)
325    {
326      // Revert back to what we were IF the classes are identical
327      var last_el = el._stylist_usedToBe[el._stylist_usedToBe.length - 1];
328      var last_classes = Xinha.arrayFilter(last_el.className.trim().split(' '), function(c) { if (c == null || c.trim() == '') { return false;} return true; });
329
330      if(
331        (new_thiers.length == 0)
332        ||
333        (
334        Xinha.arrayContainsArray(new_thiers, last_classes)
335        && Xinha.arrayContainsArray(last_classes, new_thiers)
336        )
337      )
338      {
339        el = this.switchElementTag(el, last_el.tagName);
340        new_thiers = last_classes;
341      }
342      else
343      {
344        // We can't rely on the remembered tags any more
345        el._stylist_usedToBe = [ ];
346      }
347    }
348
349    if(     new_thiers.length > 0
350        ||  el.tagName.toLowerCase() != 'span'
351        || (el.id && el.id != '')
352      )
353    {
354      el.className = new_thiers.join(' ').trim();
355    }
356    else
357    {
358      // Must be a span with no classes and no id, so we can splice it out
359      var prnt = el.parentNode;
360      var tmp;
361      while (el.hasChildNodes())
362      {
363        if (el.firstChild.nodeType == 1)
364        {
365          // if el.firstChild is an element, we've got to recurse to make sure classes are
366          // removed from it and and any of its children.
367          this._stylistRemoveClassesFull(el.firstChild, classes);
368        }
369        tmp = el.removeChild(el.firstChild);
370        prnt.insertBefore(tmp, el);
371      }
372      prnt.removeChild(el);
373    }
374  }
375};
376
377/**
378 * Change the tag of an element
379 */
380Xinha.prototype.switchElementTag = function(el, tag)
381{
382  var prnt = el.parentNode;
383  var new_el = this._doc.createElement(tag);
384
385  if(Xinha.is_ie || el.hasAttribute('id'))    new_el.setAttribute('id', el.getAttribute('id'));
386  if(Xinha.is_ie || el.hasAttribute('style')) new_el.setAttribute('style', el.getAttribute('style'));
387
388  var childs = el.childNodes;
389  for(var x = 0; x < childs.length; x++)
390  {
391    new_el.appendChild(childs[x].cloneNode(true));
392  }
393
394  prnt.insertBefore(new_el, el);
395  new_el._stylist_usedToBe = [el.tagName];
396  prnt.removeChild(el);
397  this.selectNodeContents(new_el);
398  return new_el;
399};
400
401Xinha.prototype._getAncestorsClassNames = function(sel)
402{
403  // Scan upwards to find a block level element that we can change or apply to
404  var prnt = this._activeElement(sel);
405  if(prnt == null)
406  {
407    prnt = (Xinha.is_ie ? this._createRange(sel).parentElement() : this._createRange(sel).commonAncestorContainer);
408  }
409
410  var classNames = [ ];
411  while(prnt)
412  {
413    if(prnt.nodeType == 1)
414    {
415      var classes = prnt.className.trim().split(' ');
416      for(var x = 0; x < classes.length; x++)
417      {
418        classNames[classNames.length] = classes[x];
419      }
420
421      if(prnt.tagName.toLowerCase() == 'body') break;
422      if(prnt.tagName.toLowerCase() == 'table'  ) break;
423    }
424      prnt = prnt.parentNode;
425  }
426
427  return classNames;
428};
429
430Xinha.prototype._ancestorsWithClasses = function(sel, tag, classes)
431{
432  var ancestors = [ ];
433  var prnt = this._activeElement(sel);
434  if(prnt == null)
435  {
436    try
437    {
438      prnt = (Xinha.is_ie ? this._createRange(sel).parentElement() : this._createRange(sel).commonAncestorContainer);
439    }
440    catch(e)
441    {
442      return ancestors;
443    }
444  }
445  var search_classes = classes.trim().split(' ');
446
447  while(prnt)
448  {
449    if(prnt.nodeType == 1 && prnt.className)
450    {
451      if(tag == null || prnt.tagName.toLowerCase() == tag)
452      {
453        var classes = prnt.className.trim().split(' ');
454        var found_all = true;
455        for(var i = 0; i < search_classes.length; i++)
456        {
457          var found_class = false;
458          for(var x = 0; x < classes.length; x++)
459          {
460            if(search_classes[i] == classes[x])
461            {
462              found_class = true;
463              break;
464            }
465          }
466
467          if(!found_class)
468          {
469            found_all = false;
470            break;
471          }
472        }
473
474        if(found_all) ancestors[ancestors.length] = prnt;
475      }
476      if(prnt.tagName.toLowerCase() == 'body')    break;
477      if(prnt.tagName.toLowerCase() == 'table'  ) break;
478    }
479    prnt = prnt.parentNode;
480  }
481
482  return ancestors;
483};
484
485
486Xinha.ripStylesFromCSSFile = function(URL, skip_imports)
487{
488  var css = Xinha._geturlcontent(URL);
489 
490  return Xinha.ripStylesFromCSSString(css, skip_imports, URL);
491};
492
493Xinha.ripStylesFromCSSString = function(css, skip_imports, imports_relative_to)
494{
495  if(!skip_imports)
496  {   
497    if(!imports_relative_to)
498    {
499      imports_relative_to = _editor_url + 'Xinha.css'
500    }
501   
502    var seen = { };
503   
504    function resolve_imports(css, url)
505    {
506      seen[url] = true; // protects against infinite recursion
507     
508      var RE_atimport = '@import\\s*(url\\()?["\'](.*)["\'].*';
509      var imports = css.match(new RegExp(RE_atimport,'ig'));
510      var m, file, re = new RegExp(RE_atimport,'i');
511
512      if (imports)
513      {
514        var path = url.replace(/\?.*$/,'').split("/");
515        path.pop();
516        path = path.join('/');
517        for (var i=0;i<imports.length;i++)
518        {
519          m = imports[i].match(re);
520          file = m[2];
521          if (!file.match(/^([^:]+\:)?\//))
522          {
523            file = Xinha._resolveRelativeUrl(path,file);
524          }
525                   
526          if(seen[file]) continue;
527         
528          css += resolve_imports(Xinha._geturlcontent(file), file);
529        }
530      }
531     
532      return css;
533    }
534   
535    css = resolve_imports(css, imports_relative_to);
536  }
537
538  // We are only interested in the selectors, the rules are not important
539  //  so we'll drop out all coments and rules
540  var RE_comment = /\/\*(.|\r|\n)*?\*\//g;
541  var RE_rule    = /\{(.|\r|\n)*?\}/g;
542  var RE_special = /\@[a-zA-Z]+[^;]*;/g;
543
544  css = css.replace(RE_comment, '');
545  css = css.replace(RE_special, ',');
546  css = css.replace(RE_rule, ',');
547
548  // And split on commas
549  css = css.split(',');
550
551  // And add those into our structure
552  var selectors = { };
553  for(var x = 0; x < css.length; x++)
554  {
555    if(css[x].trim())
556    {
557      selectors[css[x].trim()] = css[x].trim();
558    }
559  }
560
561
562  return selectors;
563};
564
565// Make our right side panel and insert appropriatly
566function Stylist(editor, args)
567{
568  this.editor = editor;
569 
570  var stylist = this;
571
572}
573
574Stylist._pluginInfo =
575{
576  name     : "Stylist",
577  version  : "1.0",
578  developer: "James Sleeman",
579  developer_url: "http://www.gogo.co.nz/",
580  c_owner      : "Gogo Internet Services",
581  license      : "HTMLArea",
582  sponsor      : "Gogo Internet Services",
583  sponsor_url  : "http://www.gogo.co.nz/"
584};
585
586Stylist.prototype.onGenerateOnce = function()
587{
588  var cfg = this.editor.config;
589  if(typeof cfg.css_style != 'undefined' && Xinha.objectProperties(cfg.css_style).length != 0)
590  {
591    this._prepareDialog();
592  }
593
594};
595Stylist.prototype._prepareDialog = function()
596{
597  var editor = this.editor;
598  var stylist = this;
599
600  var html = '<h1><l10n>Styles</l10n></h1>';
601 
602  this.dialog = new Xinha.Dialog(editor, html, 'Stylist',{width:200},{modal:false,closable:false});
603        Xinha._addClass( this.dialog.rootElem, 'Stylist' );
604        this.dialog.attachToPanel('right');
605  this.dialog.show();
606 
607        var dialog = this.dialog;
608        var main = this.dialog.main;
609        var caption = this.dialog.captionBar;
610       
611  main.style.overflow = "auto";
612  main.style.height = this.editor._framework.ed_cell.offsetHeight - caption.offsetHeight + 'px';
613
614  editor.notifyOn('modechange',
615  function(e,args)
616  {
617    if (!dialog.attached)
618    {
619      return;
620    }
621    switch(args.mode)
622    {
623      case 'text':
624      {
625        dialog.hide();
626        break;
627      }
628      case 'wysiwyg':
629      {
630        dialog.show();
631        break;
632      }
633    }
634  }
635  );
636  editor.notifyOn('panel_change',
637  function(e,args)
638  {
639    if (!dialog.attached)
640    {
641      return;
642    }
643    switch (args.action)
644    {
645      case 'show':
646      var newHeight = main.offsetHeight - args.panel.offsetHeight;
647      main.style.height = ((newHeight > 0) ?  main.offsetHeight - args.panel.offsetHeight : 0) + 'px';
648      dialog.rootElem.style.height = caption.offsetHeight + "px";
649      editor.sizeEditor();
650      break;
651      case 'hide':
652      stylist.resize();
653      break;
654    }
655  }
656  );
657  editor.notifyOn('before_resize',
658  function()
659  {
660    if (!dialog.attached)
661    {
662      return;
663    }
664    dialog.rootElem.style.height = caption.offsetHeight + "px";
665  }
666  );
667  editor.notifyOn('resize',
668  function()
669  {
670    if (!dialog.attached)
671    {
672      return;
673    }
674    stylist.resize();
675  }
676  );
677}
678Stylist.prototype.resize = function()
679{
680  var editor = this.editor;
681  var rootElem = this.dialog.rootElem;
682 
683  if (rootElem.style.display == 'none') return;
684 
685  var panelContainer = rootElem.parentNode;
686
687  var newSize = panelContainer.offsetHeight;
688  for (var i=0; i < panelContainer.childNodes.length;++i)
689  {
690    if (panelContainer.childNodes[i] == rootElem || !panelContainer.childNodes[i].offsetHeight)
691    {
692      continue;
693    }
694    newSize -= panelContainer.childNodes[i].offsetHeight;
695  }
696  rootElem.style.height = newSize-5 + 'px';
697  this.dialog.main.style.height = newSize - this.dialog.captionBar.offsetHeight -5 + 'px';
698}
699
700Stylist.prototype.onUpdateToolbar = function()
701{
702  if(this.dialog)
703  {
704    if(this._timeoutID)
705    {
706      window.clearTimeout(this._timeoutID);
707    }
708
709    var e = this.editor;
710    this._timeoutID = window.setTimeout(function() { e._fillStylist(); }, 500);
711  }
712};
Note: See TracBrowser for help on using the repository browser.