PluginProbe ʕ •ᴥ•ʔ
SiteOrigin CSS / 1.0.7
SiteOrigin CSS v1.0.7
1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.10 1.5.11 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0
so-css / js / editor.js
so-css / js Last commit date
css.js 10 years ago css.min.js 10 years ago csslint.js 11 years ago csslint.min.js 10 years ago editor.js 9 years ago editor.min.js 9 years ago inspector.js 9 years ago inspector.min.js 9 years ago jquery.sizes.js 11 years ago jquery.sizes.min.js 10 years ago specificity.js 11 years ago specificity.min.js 10 years ago
editor.js
1544 lines
1 /* globals jQuery, _, socssOptions, Backbone, CodeMirror, console, cssjs, wp */
2
3 (function ($, _, socssOptions) {
4
5 var socss = {
6 model: {},
7 collection: {},
8 view: {},
9 fn: {}
10 };
11
12 window.socss = socss;
13
14 /**
15 * The toolbar view
16 */
17 socss.view.toolbar = Backbone.View.extend({
18
19 button: _.template('<li><a href="#" class="toolbar-button socss-button"><%= text %></a></li>'),
20
21 editor: null,
22
23 initialize: function ( attr ) {
24 this.editor = attr.editor;
25
26 var thisView = this;
27 this.$('.editor-expand').click(function (e) {
28 e.preventDefault();
29 $(this).blur();
30 thisView.trigger('click_expand');
31 });
32
33 this.$('.editor-visual').click(function (e) {
34 e.preventDefault();
35 $(this).blur();
36 thisView.trigger('click_visual');
37 });
38 },
39
40 addButton: function (text, action) {
41 var thisView = this;
42 var button = $(this.button({text: text}))
43 .appendTo(this.$('.toolbar-function-buttons .toolbar-buttons'))
44 .click(function (e) {
45 e.preventDefault();
46 $(this).blur();
47 thisView.trigger('click_' + action);
48 });
49
50 return button;
51 }
52 });
53
54 /**
55 * The editor view, which handles codemirror stuff
56 */
57 socss.view.editor = Backbone.View.extend({
58
59 codeMirror: null,
60 snippets: null,
61 toolbar: null,
62 visualProperties: null,
63
64 inspector: null,
65
66 cssSelectors: [],
67
68 initialize: function (args) {
69 this.setupEditor();
70 },
71
72 render: function () {
73 var thisView = this;
74
75 // Setup the toolbar
76 this.toolbar = new socss.view.toolbar({
77 editor: this,
78 el: this.$('.custom-css-toolbar')
79 });
80 this.toolbar.editor = this;
81 this.toolbar.render();
82
83 // Create the visual properties view
84 this.visualProperties = new socss.view.properties({
85 editor: this,
86 el: $('#so-custom-css-properties')
87 });
88 this.visualProperties.render();
89
90 this.toolbar.on('click_expand', function () {
91 thisView.toggleExpand();
92 });
93
94 this.toolbar.on('click_visual', function () {
95 thisView.visualProperties.loadCSS( thisView.codeMirror.getValue().trim() );
96 thisView.visualProperties.show();
97 });
98
99 this.preview = new socss.view.preview({
100 editor: this,
101 el: this.$('.custom-css-preview')
102 });
103 this.preview.render();
104 },
105
106 /**
107 * Do the initial setup of the CodeMirror editor
108 */
109 setupEditor: function () {
110 var thisView = this;
111 this.registerCodeMirrorAutocomplete();
112
113 // Setup the Codemirror instance
114 var $textArea = this.$('textarea.css-editor');
115 var initValue = $textArea.val();
116 // Pad with empty lines so the editor takes up all the white space. To try make sure user gets copy/paste
117 // options in context menu.
118 var newlineMatches = initValue.match(/\n/gm);
119 var lineCount = newlineMatches ? newlineMatches.length+1 : 1;
120 var numPadLines = 15 - lineCount;
121 var paddedValue = initValue;
122 for(var i = 0; i < numPadLines; i++) {
123 paddedValue += '\n';
124 }
125 $textArea.val(paddedValue);
126 this.codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
127 tabSize: 2,
128 lineNumbers: true,
129 mode: 'css',
130 theme: 'neat',
131 inputStyle: 'contenteditable', //necessary to allow context menu (right click) copy/paste etc.
132 gutters: [
133 "CodeMirror-lint-markers"
134 ],
135 lint: true,
136 });
137
138 // Make sure the user doesn't leave without saving
139 this.$el.on('submit', function(){
140 initValue = thisView.codeMirror.getValue().trim();
141 });
142 $(window).bind('beforeunload', function(){
143 var editorValue = thisView.codeMirror.getValue().trim();
144 if( editorValue !== initValue ) {
145 return socssOptions.loc.leave;
146 }
147 });
148
149
150 // Set the container to visible overflow once the editor is setup
151 this.$el.find('.custom-css-container').css('overflow', 'visible');
152 this.scaleEditor();
153
154 // Scale the editor whenever the window is resized
155 $(window).resize(function () {
156 thisView.scaleEditor();
157 });
158
159 // Setup the extensions
160 this.setupCodeMirrorExtensions();
161 },
162
163 /**
164 * Register the autocomplete helper. Based on css-hint.js in the codemirror addon folder.
165 */
166 registerCodeMirrorAutocomplete: function () {
167 var thisView = this;
168
169 var pseudoClasses = {
170 link: 1, visited: 1, active: 1, hover: 1, focus: 1,
171 "first-letter": 1, "first-line": 1, "first-child": 1,
172 before: 1, after: 1, lang: 1
173 };
174
175 CodeMirror.registerHelper("hint", "css", function (cm) {
176 var cur = cm.getCursor(), token = cm.getTokenAt(cur);
177 var inner = CodeMirror.innerMode(cm.getMode(), token.state);
178 if (inner.mode.name !== "css") {
179 return;
180 }
181
182 if (token.type === "keyword" && "!important".indexOf(token.string) === 0) {
183 return {
184 list: ["!important"], from: CodeMirror.Pos(cur.line, token.start),
185 to: CodeMirror.Pos(cur.line, token.end)
186 };
187 }
188
189 var start = token.start, end = cur.ch, word = token.string.slice(0, end - start);
190 if (/[^\w$_-]/.test(word)) {
191 word = "";
192 start = end = cur.ch;
193 }
194
195 var spec = CodeMirror.resolveMode("text/css");
196
197 var result = [];
198
199 function add(keywords) {
200 for (var name in keywords) {
201 if (!word || name.lastIndexOf(word, 0) === 0) {
202 result.push(name);
203 }
204 }
205 }
206
207 var st = inner.state.state;
208
209 if (st === 'top') {
210 // We're going to autocomplete the selector using our own set of rules
211 var line = cm.getLine(cur.line).trim();
212
213 var selectors = thisView.cssSelectors;
214 for (var i = 0; i < selectors.length; i++) {
215 if (selectors[i].selector.indexOf(line) !== -1) {
216 result.push(selectors[i].selector);
217 }
218 }
219
220 if (result.length) {
221 return {
222 list: result,
223 from: CodeMirror.Pos(cur.line, 0),
224 to: CodeMirror.Pos(cur.line, end)
225 };
226 }
227 }
228 else {
229
230 if (st === "pseudo" || token.type === "variable-3") {
231 add(pseudoClasses);
232 }
233 else if (st === "block" || st === "maybeprop") {
234 add(spec.propertyKeywords);
235 }
236 else if (st === "prop" || st === "parens" || st === "at" || st === "params") {
237 add(spec.valueKeywords);
238 add(spec.colorKeywords);
239 }
240 else if (st === "media" || st === "media_parens") {
241 add(spec.mediaTypes);
242 add(spec.mediaFeatures);
243 }
244
245 if (result.length) {
246 return {
247 list: result,
248 from: CodeMirror.Pos(cur.line, start),
249 to: CodeMirror.Pos(cur.line, end)
250 };
251 }
252
253 }
254
255 });
256 },
257
258 setupCodeMirrorExtensions: function () {
259 var thisView = this;
260
261 this.codeMirror.on('cursorActivity', function (cm) {
262 var cur = cm.getCursor(), token = cm.getTokenAt(cur);
263 var inner = CodeMirror.innerMode(cm.getMode(), token.state);
264
265 // If we have a qualifier selected, then highlight that in the preview
266 if (token.type === 'qualifier' || token.type === 'tag' || token.type === 'builtin') {
267 var line = cm.getLine(cur.line);
268 var selector = line.substring(0, token.end);
269
270 thisView.preview.highlight(selector);
271 }
272 else {
273 thisView.preview.clearHighlight();
274 }
275 });
276
277 // This sets up automatic autocompletion at all times
278 this.codeMirror.on('keyup', function (cm, e) {
279 if (
280 ( e.keyCode >= 65 && e.keyCode <= 90 ) ||
281 ( e.keyCode === 189 && !e.shiftKey ) ||
282 ( e.keyCode === 190 && !e.shiftKey ) ||
283 ( e.keyCode === 51 && e.shiftKey ) ||
284 ( e.keyCode === 189 && e.shiftKey )
285 ) {
286 cm.showHint( {
287 completeSingle: false
288 } );
289 }
290 });
291 },
292
293 /**
294 * Scale the size of the editor depending on whether it's expanded or not
295 */
296 scaleEditor: function () {
297 if (this.$el.hasClass('expanded')) {
298 // If we're in the expanded view, then resize the editor
299 this.codeMirror.setSize('100%', $(window).outerHeight() - this.$('.custom-css-toolbar').outerHeight());
300 }
301 else {
302 this.codeMirror.setSize('100%', 'auto');
303 }
304 },
305
306 /**
307 * Check if the editor is in expanded mode
308 * @returns bool
309 */
310 isExpanded: function () {
311 return this.$el.hasClass('expanded');
312 },
313
314 /**
315 * Toggle if this is expanded or not
316 */
317 toggleExpand: function () {
318 this.$el.toggleClass('expanded');
319 this.scaleEditor();
320 },
321
322 /**
323 * Set the expanded state of the editor
324 * @param expanded
325 */
326 setExpand: function (expanded) {
327 if (expanded) {
328 this.$el.addClass('expanded');
329 }
330 else {
331 this.$el.removeClass('expanded');
332 }
333 this.scaleEditor();
334 },
335
336 /**
337 * Set the snippets available to this editor
338 */
339 setSnippets: function (snippets) {
340 if (!_.isEmpty(snippets)) {
341 var thisView = this;
342
343 this.snippets = new socss.view.snippets({
344 snippets: snippets
345 });
346 this.snippets.editor = this;
347
348 this.snippets.render();
349 this.toolbar.addButton('Snippets', 'snippets');
350 this.toolbar.on('click_snippets', function () {
351 thisView.snippets.show();
352 });
353 }
354 },
355
356 /**
357 * Add some CSS to the editor.
358 * @param css
359 */
360 addCode: function (css) {
361 var editor = this.codeMirror;
362
363 var before_css = '';
364 if (editor.doc.lineCount() === 1 && editor.doc.getLine(editor.doc.lastLine()).length === 0) {
365 before_css = "";
366 }
367 else if (editor.doc.getLine(editor.doc.lastLine()).length === 0) {
368 before_css = "\n";
369 }
370 else {
371 before_css = "\n\n";
372 }
373
374 // Now insert the code in the editor
375 editor.doc.setCursor(
376 editor.doc.lastLine(),
377 editor.doc.getLine(editor.doc.lastLine()).length
378 );
379 editor.doc.replaceSelection(before_css + css);
380 },
381
382 addEmptySelector: function (selector) {
383 this.addCode(selector + " {\n \n}");
384 },
385
386 /**
387 * Sets the inspector view that's being used by the editor
388 */
389 setInspector: function (inspector) {
390 var thisView = this;
391 this.inspector = inspector;
392 this.cssSelectors = inspector.pageSelectors;
393
394 // A selector is clicked in the inspector
395 inspector.on('click_selector', function (selector) {
396 if ( thisView.visualProperties.isVisible() ) {
397 thisView.visualProperties.addSelector(selector);
398 }
399 else {
400 thisView.addEmptySelector(selector);
401 }
402 });
403
404 // A property is clicked in the inspector
405 inspector.on('click_property', function (property) {
406 if ( ! thisView.visualProperties.isVisible() ) {
407 thisView.codeMirror.replaceSelection(property + ";\n ");
408 }
409 });
410
411 inspector.on('set_active_element', function(el, selectors){
412 if ( thisView.visualProperties.isVisible() && selectors.length ) {
413 thisView.visualProperties.addSelector( selectors[0].selector );
414 }
415 });
416 }
417
418 });
419
420 /**
421 * The preview.
422 */
423 socss.view.preview = Backbone.View.extend({
424
425 template: _.template('<iframe class="preview-iframe" seamless="seamless"></iframe>'),
426 editor: null,
427
428 initialize: function (attr) {
429 this.editor = attr.editor;
430
431 var thisView = this;
432 this.editor.codeMirror.on('change', function (cm, c) {
433 thisView.updatePreviewCss();
434 });
435 },
436
437 render: function () {
438 var thisView = this;
439
440 this.$el.html(this.template());
441
442 this.$('.preview-iframe')
443 .attr('src', socssOptions.homeURL)
444 .load(function () {
445 var $$ = $(this);
446 $$.contents().find('a').each(function () {
447 var href = $(this).attr('href');
448 if (href === undefined) {
449 return true;
450 }
451
452 var firstSeperator = (href.indexOf('?') === -1 ? '?' : '&');
453 $(this).attr('href', href + firstSeperator + 'so_css_preview=1');
454 });
455
456 thisView.updatePreviewCss();
457 })
458 .mouseleave(function () {
459 thisView.clearHighlight();
460 });
461 },
462
463 /**
464 * Update the preview CSS from the CodeMirror value in the editor
465 */
466 updatePreviewCss: function () {
467 var preview = this.$('.preview-iframe');
468 if (preview.length === 0) {
469 return;
470 }
471
472 var head = preview.contents().find('head');
473 if (head.find('style.siteorigin-custom-css').length === 0) {
474 head.append('<style class="siteorigin-custom-css" type="text/css"></style>');
475 }
476 var style = head.find('style.siteorigin-custom-css');
477
478 // Update the CSS after a short delay
479 var css = this.editor.codeMirror.getValue().trim();
480 style.html(css);
481 },
482
483 /**
484 * Highlight all elements with a given selector
485 */
486 highlight: function (selector) {
487 try {
488 this.editor.inspector.hl.highlight(selector);
489 }
490 catch (err) {
491 console.log('No inspector to highlight with');
492 }
493 },
494
495 /**
496 * Clear the currently highlighted elements in preview
497 */
498 clearHighlight: function () {
499 try {
500 this.editor.inspector.hl.clear();
501 }
502 catch (err) {
503 console.log('No inspector to highlight with');
504 }
505 }
506
507 });
508
509 /**
510 * The dialog for the snippets browser
511 */
512 socss.view.snippets = Backbone.View.extend({
513 template: _.template($('#template-snippet-browser').html()),
514 snippet: _.template('<li class="snippet"><%- name %></li>'),
515 className: 'css-editor-snippet-browser',
516 snippets: null,
517 editor: null,
518
519 events: {
520 'click .close': 'hide',
521 'click .buttons .insert-snippet': 'insertSnippet'
522 },
523
524 currentSnippet: null,
525
526 initialize: function (args) {
527 this.snippets = args.snippets;
528 },
529
530 render: function () {
531 var thisView = this;
532
533
534 var clickSnippet = function (e) {
535 e.preventDefault();
536 var $$ = $(this);
537
538 thisView.$('.snippets li.snippet').removeClass('active');
539 $(this).addClass('active');
540 thisView.viewSnippet({
541 name: $$.html(),
542 description: $$.data('description'),
543 css: $$.data('css')
544 });
545 };
546
547 this.$el.html(this.template());
548 for (var i = 0; i < this.snippets.length; i++) {
549 $(this.snippet({name: this.snippets[i].Name}))
550 .data({
551 'description': this.snippets[i].Description,
552 'css': this.snippets[i].css
553 })
554 .appendTo(this.$('ul.snippets'))
555 .click(clickSnippet);
556 }
557
558 // Click on the first one
559 thisView.$('.snippets li.snippet').eq(0).click();
560
561 this.attach();
562 return this;
563 },
564
565 viewSnippet: function (args) {
566 var w = this.$('.main .snippet-view');
567
568 w.find('.snippet-title').html(args.name);
569 w.find('.snippet-description').html(args.description);
570 w.find('.snippet-code').html(args.css);
571
572 this.currentSnippet = args;
573 },
574
575 insertSnippet: function () {
576 var editor = this.editor.codeMirror;
577 var css = this.currentSnippet.css;
578
579 var before_css = '';
580 if (editor.doc.lineCount() === 1 && editor.doc.getLine(editor.doc.lastLine()).length === 0) {
581 before_css = "";
582 }
583 else if (editor.doc.getLine(editor.doc.lastLine()).length === 0) {
584 before_css = "\n";
585 }
586 else {
587 before_css = "\n\n";
588 }
589
590 // Now insert the code in the editor
591 editor.doc.setCursor(
592 editor.doc.lastLine(),
593 editor.doc.getLine(editor.doc.lastLine()).length
594 );
595 editor.doc.replaceSelection(before_css + css);
596
597 this.hide();
598 },
599
600 attach: function () {
601 this.$el.appendTo('body');
602 },
603
604 show: function () {
605 this.$el.show();
606 },
607
608 hide: function () {
609 this.$el.hide();
610 }
611 });
612
613
614 /**
615 * The visual properties editor
616 */
617 socss.view.properties = Backbone.View.extend({
618
619 model: socss.model.cssRules,
620
621 tabTemplate: _.template('<li data-section="<%- id %>"><span class="fa fa-<%- icon %>"></span> <%- title %></li>'),
622 sectionTemplate: _.template('<div class="section" data-section="<%- id %>"><table class="fields-table"><tbody></tbody></table></div>'),
623 controllerTemplate: _.template('<tr><th scope="row"><%- title %></th><td></td></tr>'),
624
625 /**
626 * The controllers for each of the properties
627 */
628 propertyControllers: [],
629
630 /**
631 * The editor view
632 */
633 editor: null,
634
635 /**
636 * The current, raw CSS
637 */
638 css: '',
639
640 /**
641 * Parsed CSS
642 */
643 parsed: {},
644
645 /**
646 * The current active selector
647 */
648 activeSelector: '',
649
650 /**
651 * Was the editor expanded before we went into the property editor
652 */
653 editorExpandedBefore: false,
654
655 events: {
656 'click .close': 'hide'
657 },
658
659 /**
660 * Initialize the properties editor with a new model
661 */
662 initialize: function ( attr ) {
663 this.parser = new cssjs();
664 this.editor = attr.editor;
665 },
666
667 /**
668 * Render the property editor
669 */
670 render: function () {
671 var thisView = this;
672
673 var controllers = socssOptions.propertyControllers;
674
675 for (var id in controllers) {
676 // Create the tabs
677 var $t = $(this.tabTemplate({
678 id: id,
679 icon: controllers[id].icon,
680 title: controllers[id].title
681 })).appendTo(this.$('.section-tabs'));
682
683 // Create the section wrapper
684 var $s = $(this.sectionTemplate({
685 id: id
686 })).appendTo(this.$('.sections'));
687
688 // Now lets add the controllers
689 if (!_.isEmpty(controllers[id].controllers)) {
690
691 for (var i = 0; i < controllers[id].controllers.length; i++) {
692
693 var $c = $(thisView.controllerTemplate({
694 title: controllers[id].controllers[i].title
695 })).appendTo($s.find('tbody'));
696
697 var controllerAtts = controllers[id].controllers[i];
698 var controller;
699
700 if (typeof socss.view.properties.controllers[controllerAtts.type] === 'undefined') {
701 // Setup a default controller
702 controller = new socss.view.propertyController({
703 el: $c.find('td'),
704 propertiesView: thisView,
705 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
706 });
707 }
708 else {
709 // Setup a specific controller
710 controller = new socss.view.properties.controllers[controllerAtts.type]({
711 el: $c.find('td'),
712 propertiesView: thisView,
713 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
714 });
715 }
716
717 thisView.propertyControllers.push(controller);
718
719 // Setup and render the controller
720 controller.render();
721 controller.initChangeEvents();
722 }
723 }
724 }
725
726 // Setup the tab switching for the property sections
727 this.$('.section-tabs li').click(function () {
728 var $$ = $(this);
729 var show = thisView.$('.sections .section[data-section="' + $$.data('section') + '"]');
730
731 thisView.$('.sections .section').not(show).hide().removeClass('active');
732 show.show().addClass('active');
733
734 thisView.$('.section-tabs li').not($$).removeClass('active');
735 $$.addClass('active');
736 }).eq(0).click();
737
738 this.$('.toolbar select').change(function () {
739 thisView.setActivateSelector($(this).find(':selected').data('selector'));
740 });
741 },
742
743 /**
744 * Sets the rule value for the active selector
745 * @param rule
746 * @param value
747 */
748 setRuleValue: function (rule, value) {
749 if (typeof this.activeSelector === 'undefined' || typeof this.activeSelector.rules === 'undefined') {
750 return;
751 }
752
753 var newRule = true;
754 for (var i = 0; i < this.activeSelector.rules.length; i++) {
755 if (this.activeSelector.rules[i].directive === rule) {
756 this.activeSelector.rules[i].value = value;
757 newRule = false;
758 break;
759 }
760 }
761
762 if (newRule) {
763 this.activeSelector.rules.push({
764 directive: rule,
765 value: value
766 });
767 }
768
769 this.updateMainEditor( false );
770 },
771
772 /**
773 * Get the rule value for the active selector
774 * @param rule
775 */
776 getRuleValue: function (rule) {
777 if (typeof this.activeSelector === 'undefined' || typeof this.activeSelector.rules === 'undefined') {
778 return '';
779 }
780
781 for (var i = 0; i < this.activeSelector.rules.length; i++) {
782 if (this.activeSelector.rules[i].directive === rule) {
783 return this.activeSelector.rules[i].value;
784 }
785 }
786 return '';
787 },
788
789 /**
790 * Update the main editor with the value of the parsed CSS
791 */
792 updateMainEditor: function ( compress ) {
793 var css;
794 if( typeof compress === 'undefined' || compress === true ) {
795 css = this.parser.compressCSS( this.parsed );
796 // Also remove any empty selectors
797 css = css.filter( function(v){
798 return (
799 typeof v.type !== 'undefined' ||
800 v.rules.length > 0
801 );
802 } );
803 }
804 else {
805 css = this.parsed;
806 }
807
808 this.editor.codeMirror.setValue( this.parser.getCSSForEditor( css ).trim() );
809 },
810
811 /**
812 * Show the properties editor
813 */
814 show: function () {
815 this.editorExpandedBefore = this.editor.isExpanded();
816 this.editor.setExpand(true);
817
818 this.$el.show().animate({'left': 0}, 'fast');
819 },
820
821 /**
822 * Hide the properties editor
823 */
824 hide: function () {
825 this.editor.setExpand(this.editorExpandedBefore);
826 this.$el.animate( {'left': -338}, 'fast', function(){
827 $(this).hide();
828 } );
829
830 // Update the main editor with compressed CSS when we close the properties editor
831 this.updateMainEditor( true );
832 },
833
834 /**
835 * @returns boolean
836 */
837 isVisible: function () {
838 return this.$el.is(':visible');
839 },
840
841 /**
842 * Loads a single CSS selector and associated properties into the model
843 * @param css
844 */
845 loadCSS: function (css, activeSelector) {
846 this.css = css;
847
848 // Load the CSS and combine rules
849 this.parsed = this.parser.compressCSS( this.parser.parseCSS(css) );
850
851 // Add the dropdown menu items
852 var dropdown = this.$('.toolbar select').empty();
853 for (var i = 0; i < this.parsed.length; i++) {
854 var rule = this.parsed[i];
855
856 if( typeof rule.subStyles !== 'undefined' ) {
857
858 for (var j = 0; j < rule.subStyles.length; j++) {
859 var subRule = rule.subStyles[j];
860 dropdown.append(
861 $('<option>')
862 .html( rule.selector + ': ' + subRule.selector )
863 .attr( 'val', rule.selector + ': ' + subRule.selector )
864 .data( 'selector', subRule )
865 );
866 }
867
868 }
869 else {
870 dropdown.append(
871 $('<option>')
872 .html( rule.selector )
873 .attr( 'val', rule.selector )
874 .data( 'selector', rule )
875 );
876 }
877 }
878
879 if (typeof activeSelector === 'undefined') {
880 activeSelector = dropdown.find('option').eq(0).attr('val');
881 }
882 dropdown.val(activeSelector).change();
883 },
884
885 /**
886 * Set the selector that we're currently dealing with
887 * @param selector
888 */
889 setActivateSelector: function (selector) {
890 this.activeSelector = selector;
891 for (var i = 0; i < this.propertyControllers.length; i++) {
892 this.propertyControllers[i].refreshFromRule();
893 }
894 },
895
896 /**
897 * Add or select a selector.
898 *
899 * @param selector
900 */
901 addSelector: function(selector) {
902 // Check if this selector already exists
903 var dropdown = this.$('.toolbar select');
904 dropdown.val( selector );
905
906 if( dropdown.val() === selector ) {
907 // Trigger a change event to load the existing selector
908 dropdown.change();
909 }
910 else {
911 // The selector doesn't exist, so add it to the CSS, then reload
912 this.editor.addEmptySelector(selector);
913 this.loadCSS( this.editor.codeMirror.getValue().trim(), selector );
914 }
915
916 dropdown.addClass('highlighted');
917 setTimeout(function(){
918 dropdown.removeClass('highlighted');
919 }, 2000);
920 }
921
922 });
923
924 // The basic property controller
925 socss.view.propertyController = Backbone.View.extend({
926
927 template: _.template('<input type="text" value="" />'),
928 activeRule: null,
929 args: null,
930 propertiesView: null,
931
932 initialize: function (args) {
933
934 this.args = args.args;
935 this.propertiesView = args.propertiesView;
936
937 // By default, update the active rule whenever things change
938 this.on('set_value', this.updateRule, this);
939 this.on('change', this.updateRule, this);
940 },
941
942 /**
943 * Render the property field controller
944 */
945 render: function () {
946 this.$el.append( $(this.template( {} )) );
947 this.field = this.$('input');
948 },
949
950 /**
951 * Initialize the events that constitute a change
952 */
953 initChangeEvents: function () {
954 var thisView = this;
955 this.field.on( 'change keyup', function () {
956 thisView.trigger('change', $(this).val());
957 } );
958 },
959
960
961 /**
962 * Update the value of an active rule
963 */
964 updateRule: function () {
965 this.propertiesView.setRuleValue(
966 this.args.property,
967 this.getValue()
968 );
969 },
970
971 /**
972 * This is called when the selector changes
973 */
974 refreshFromRule: function () {
975 var value = this.propertiesView.getRuleValue(this.args.property);
976 this.setValue(value, {silent: true});
977 },
978
979 /**
980 * Get the current value
981 * @return string
982 */
983 getValue: function () {
984 return this.field.val();
985 },
986
987 /**
988 * Set the current value
989 * @param socss.view.properties val
990 */
991 setValue: function (val, options) {
992 options = _.extend({silent: false}, options);
993
994 this.field.val(val);
995
996 if (!options.silent) {
997 this.trigger('set_value', val);
998 }
999 },
1000
1001 /**
1002 * Reset the current value
1003 */
1004 reset: function (options) {
1005 options = _.extend({silent: false}, options);
1006
1007 this.setValue('', options);
1008 }
1009
1010 });
1011
1012 // All the value controllers
1013 socss.view.properties.controllers = {};
1014
1015 // The color controller
1016 socss.view.properties.controllers.color = socss.view.propertyController.extend({
1017
1018 template: _.template('<input type="text" value="" />'),
1019
1020 render: function () {
1021 var thisView = this;
1022
1023 this.$el.append($(this.template({})));
1024
1025 // Set this up as a color picker
1026 this.field = this.$el.find('input');
1027 this.field.minicolors({});
1028
1029 },
1030
1031 initChangeEvents: function () {
1032 var thisView = this;
1033 this.field.on('change keyup', function () {
1034 thisView.trigger('change', thisView.field.minicolors('value'));
1035 });
1036 },
1037
1038 getValue: function () {
1039 return this.field.minicolors('value');
1040 },
1041
1042 setValue: function (val, options) {
1043 options = _.extend({silent: false}, options);
1044
1045 this.field.minicolors('value', val);
1046
1047 if (!options.silent) {
1048 this.trigger('set_value', val);
1049 }
1050 }
1051
1052 });
1053
1054 // The dropdown select box controller
1055 socss.view.properties.controllers.select = socss.view.propertyController.extend( {
1056 template: _.template('<select></select>'),
1057
1058 render: function(){
1059 var thisView = this;
1060
1061 this.$el.append($(this.template({})));
1062 this.field = this.$el.find('select');
1063
1064 // Add the unchanged option
1065 this.field.append( $('<option value=""></option>').html('') );
1066
1067 // Add all the options to the dropdown
1068 for( var k in this.args.options ) {
1069 this.field.append( $('<option></option>').attr('value', k).html( this.args.options[k] ) );
1070 }
1071
1072 if( typeof this.args.option_icons !== 'undefined' ) {
1073 this.setupVisualSelect();
1074 }
1075 },
1076
1077 setupVisualSelect: function(){
1078 var thisView = this;
1079 this.field.hide();
1080
1081 var $tc = $('<div class="select-tabs"></div>').appendTo( this.$el );
1082
1083 // Add the none value
1084 $('<div class="select-tab" data-value=""><span class="fa fa-circle-o"></span></div>').appendTo($tc);
1085
1086 // Now add one for each of the option icons
1087 for ( var k in this.args.option_icons ) {
1088 $('<div class="select-tab"></div>')
1089 .appendTo($tc)
1090 .append(
1091 $('<span class="fa"></span>')
1092 .addClass('fa-' + this.args.option_icons[k])
1093 )
1094 .attr('data-value', k)
1095 ;
1096 }
1097
1098 $tc.find('.select-tab')
1099 .css('width', 100/( $tc.find('>div').length ) + "%" )
1100 .click( function(){
1101 var $t = $(this);
1102 $tc.find('.select-tab').removeClass('active');
1103 $t.addClass('active');
1104 thisView.field.val( $t.data('value')).change();
1105 } );
1106 },
1107
1108 /**
1109 * Set the current value
1110 * @param socss.view.properties val
1111 */
1112 setValue: function (val, options) {
1113 options = _.extend({silent: false}, options);
1114
1115 this.field.val(val);
1116
1117 this.$('.select-tabs .select-tab').removeClass('active').filter('[data-value="' + val + '"]').addClass('active');
1118
1119 if (!options.silent) {
1120 this.trigger('set_value', val);
1121 }
1122 }
1123
1124 } );
1125
1126 // A field that lets a user upload an image
1127 socss.view.properties.controllers.image = socss.view.propertyController.extend( {
1128 template: _.template('<input type="text" value="" /> <span class="select socss-button"><span class="fa fa-upload"></span></span>'),
1129
1130 render: function(){
1131 var thisView = this;
1132
1133 this.media = wp.media({
1134 // Set the title of the modal.
1135 title: socssOptions.loc.select_image,
1136
1137 // Tell the modal to show only images.
1138 library: {
1139 type: 'image'
1140 },
1141
1142 // Customize the submit button.
1143 button: {
1144 // Set the text of the button.
1145 text: socssOptions.loc.select,
1146 // Tell the button not to close the modal, since we're
1147 // going to refresh the page when the image is selected.
1148 close: false
1149 }
1150 });
1151
1152 this.$el.append( $(this.template({
1153 select: socssOptions.loc.select
1154 })) );
1155
1156 this.field = this.$el.find('input');
1157
1158 this.$('.select').click(function(){
1159 thisView.media.open();
1160 });
1161
1162 this.media.on('select', function(){
1163 // Grab the selected attachment.
1164 var attachment = this.state().get('selection').first().attributes;
1165 var val = thisView.args.value.replace('{{url}}', attachment.url);
1166
1167 // Change the field value and trigger a change event
1168 thisView.field.val( val ).change();
1169
1170 // Close the image selector
1171 thisView.media.close();
1172
1173 }, this.media);
1174 }
1175
1176 } );
1177
1178 // A simple measurement field
1179 socss.view.properties.controllers.measurement = socss.view.propertyController.extend( {
1180
1181 wrapperClass: 'socss-field-measurement',
1182
1183 render: function(){
1184 this.$el.append($(this.template({})));
1185 this.field = this.$('input');
1186 this.setupMeasurementField( this.field, {} );
1187 },
1188
1189 setValue: function (val, options) {
1190 options = _.extend({silent: false}, options);
1191 this.field.val(val).trigger('measurement_refresh');
1192 if (!options.silent) {
1193 this.trigger('set_value', val);
1194 }
1195 },
1196
1197 units : [
1198 'px',
1199 '%',
1200 'em',
1201 'cm',
1202 'mm',
1203 'in',
1204 'pt',
1205 'pc',
1206 'ex',
1207 'ch',
1208 'rem',
1209 'vw',
1210 'vh',
1211 'vmin',
1212 'vmax'
1213 ],
1214
1215 parseUnits: function( value ){
1216 var escapeRegExp = function(str) {
1217 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
1218 };
1219
1220 var regexUnits = this.units.map(escapeRegExp);
1221 var regex = new RegExp('([0-9\\.\\-]+)(' + regexUnits.join('|') + ')?', 'i');
1222 var result = regex.exec( value );
1223
1224 if( result === null ) {
1225 return {
1226 value: '',
1227 unit: ''
1228 };
1229 }
1230 else {
1231 return {
1232 value: result[1],
1233 unit: result[2] === undefined ? '' : result[2]
1234 };
1235 }
1236 },
1237
1238 setupMeasurementField: function( $el, options ){
1239 var thisView = this;
1240 var $p = $el.parent();
1241
1242 options = _.extend( {
1243 defaultUnit: 'px'
1244 }, options );
1245
1246 $el.hide();
1247 $p.addClass( this.wrapperClass ).data('unit', options.defaultUnit);
1248
1249 // Create the fake input field
1250 var $fi = $('<input type="text" class="socss-field-input"/>').appendTo($p);
1251 var $da = $('<span class="dashicons dashicons-arrow-down"></span>').appendTo($p);
1252 var $dd = $('<ul class="dropdown"></ul>').appendTo($p);
1253 var $u = $('<span class="units"></span>').html( options.defaultUnit ).appendTo( $p );
1254
1255 for( var i = 0; i < thisView.units.length; i++ ) {
1256 var $o = $('<li></li>').html( thisView.units[i] ).data('unit', thisView.units[i]);
1257 if( thisView.units[i] === options.defaultUnit ) {
1258 $o.addClass('active');
1259 }
1260 $dd.append( $o );
1261 }
1262
1263 var updateValue = function(){
1264 var value = thisView.parseUnits( $fi.val() );
1265
1266 if( value.unit !== '' && value.unit !== $p.data( 'unit' ) ) {
1267 $fi.val( value.value );
1268 setUnit( value.unit );
1269 }
1270
1271 if( value.value === '' ) {
1272 $el.val( '' );
1273 }
1274 else {
1275 $el.val( value.value + $p.data( 'unit' ) );
1276 }
1277 };
1278
1279 var setUnit = function( unit ){
1280 $u.html( unit );
1281 $p.data( 'unit', unit );
1282 $fi.trigger('keydown');
1283 };
1284
1285 $da.click( function(){
1286 $dd.toggle();
1287 } );
1288
1289 $dd.find('li').click( function(){
1290 $dd.toggle();
1291 setUnit( $(this).data('unit') );
1292 updateValue();
1293 $el.trigger('change');
1294 } );
1295
1296 $fi.on( 'keyup keydown', function(e){
1297 var $$ = $(this);
1298
1299 var char = '';
1300 if( e.type === 'keydown' ) {
1301 if(e.keyCode >= 48 && e.keyCode <= 57 ) {
1302 char = String.fromCharCode(e.keyCode);
1303 }
1304 else if( e.keyCode === 189 ) {
1305 char = '-';
1306 }
1307 else if( e.keyCode === 190 ) {
1308 char = '.';
1309 }
1310 }
1311
1312 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1313 .css( {
1314 'font-size' : '14px'
1315 } )
1316 .html( $fi.val() + char )
1317 .appendTo( 'body' );
1318 var width = $pl.width();
1319 width = Math.min(width, 63);
1320 $pl.remove();
1321
1322 $u.css('left', width + 12);
1323 } );
1324
1325 $fi.on('keyup', function(e){
1326 updateValue();
1327 $el.trigger('change');
1328 } );
1329
1330 $el.on('measurement_refresh', function(){
1331 var value = thisView.parseUnits( $el.val() );
1332 $fi.val( value.value );
1333
1334 var unit = value.unit === '' ? options.defaultUnit : value.unit;
1335 $p.data( 'unit', unit );
1336 $u.html( unit );
1337
1338 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1339 .css({
1340 'font-size' : '14px'
1341 })
1342 .html( value.value )
1343 .appendTo( 'body' );
1344 var width = $pl.width();
1345 width = Math.min(width, 63);
1346 $pl.remove();
1347
1348 $u.css('left', width + 12);
1349 } );
1350
1351 // Now add the increment/decrement buttons
1352 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1353 var $dec = $('<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>').appendTo($diw);
1354 var $inc = $('<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>').appendTo($diw);
1355
1356 // Increment is clicked
1357 $inc.click( function(){
1358 var value = thisView.parseUnits( $el.val() );
1359 if( value.value === '' ) {
1360 return true;
1361 }
1362
1363 var newVal = Math.ceil( value.value * 1.05 );
1364
1365 $fi.val( newVal );
1366 updateValue();
1367 $el.trigger('change').trigger('measurement_refresh');
1368 } );
1369
1370 $dec.click( function(){
1371 var value = thisView.parseUnits( $el.val() );
1372 if( value.value === '' ) {
1373 return true;
1374 }
1375
1376 var newVal = Math.floor( value.value / 1.05 );
1377
1378 $fi.val( newVal );
1379 updateValue();
1380 $el.trigger('change').trigger('measurement_refresh');
1381 } );
1382 }
1383
1384 } );
1385
1386 // A simple measurement field
1387 socss.view.properties.controllers.number = socss.view.propertyController.extend( {
1388
1389 render: function(){
1390 this.$el.append($(this.template({})));
1391 this.field = this.$('input');
1392
1393 // Setup the measurement field
1394 this.setupNumberField(this.field, this.args);
1395 },
1396
1397 /**
1398 * Setup the number field
1399 * @param el
1400 * @param options
1401 */
1402 setupNumberField: function($el, options){
1403 options = _.extend({
1404 change: null,
1405 default: 0,
1406 increment: 1,
1407 decrement: -1,
1408 max: null,
1409 min: null
1410 }, options);
1411
1412 var $p = $el.parent();
1413 $p.addClass('socss-field-number');
1414
1415 // Now add the increment/decrement buttons
1416 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1417 var $dec = $('<div class="dec-button socss-button">-</div>').appendTo($diw);
1418 var $inc = $('<div class="inc-button socss-button">+</div>').appendTo($diw);
1419
1420 // Increment is clicked
1421 $diw.find('> div').click( function(e){
1422 e.preventDefault();
1423
1424 var val = options.default;
1425 if( $el.val() !== '' ) {
1426 val = Number($el.val());
1427 }
1428 val = val + ( $(this).is( $dec ) ? options.decrement : options.increment );
1429
1430 val = Math.round(val*100)/100;
1431
1432 if( options.max !== null ) {
1433 val = Math.min( options.max, val);
1434 }
1435
1436 if( options.min !== null ) {
1437 val = Math.max( options.min, val);
1438 }
1439
1440 $el.val( val );
1441 $el.trigger('change');
1442 } );
1443
1444 return this;
1445 }
1446
1447 } );
1448
1449
1450 socss.view.properties.controllers.sides = socss.view.propertyController.extend( {
1451
1452 template: _.template( $('#template-sides-field').html().trim() ),
1453
1454 controllers: [],
1455
1456 render: function(){
1457 var thisView = this;
1458
1459 this.$el.append( $(this.template({})) );
1460 this.field = this.$el.find('input');
1461
1462 if( !thisView.args.hasAll ) {
1463 this.$('.select-tab').eq(0).remove();
1464 this.$('.select-tab').css('width', '25%');
1465 }
1466
1467 this.$('.select-tab').each( function(){
1468 var dir = $(this).data('direction');
1469
1470 var container = $('<li class="side">')
1471 .appendTo( thisView.$('.sides') )
1472 .hide();
1473
1474 for( var i = 0; i < thisView.args.controllers.length; i++ ) {
1475
1476 var controllerArgs = thisView.args.controllers[i];
1477
1478 if( typeof socss.view.properties.controllers[ controllerArgs.type ] ) {
1479
1480 // Create the measurement view
1481 var property = '';
1482 if( dir === 'all' ) {
1483 property = controllerArgs.args.propertyAll;
1484 }
1485 else {
1486 property = controllerArgs.args.property.replace('{dir}', dir);
1487 }
1488
1489 var theseControllerArgs = _.extend({}, controllerArgs.args, {property: property});
1490
1491 var controller = new socss.view.properties.controllers[ controllerArgs.type ]( {
1492 el: $('<div>').appendTo( container ),
1493 propertiesView: thisView.propertiesView,
1494 args: theseControllerArgs
1495 } );
1496
1497 // Setup and render the measurement controller and register it with the properties view
1498 controller.render();
1499 controller.initChangeEvents();
1500 thisView.propertiesView.propertyControllers.push(controller);
1501
1502 }
1503
1504 }
1505
1506 $(this).on( 'click', function(){
1507 thisView.$('.select-tab').removeClass('active');
1508 $(this).addClass('active');
1509
1510 thisView.$('.sides .side').hide();
1511 container.show();
1512 } );
1513
1514 } );
1515
1516 // Select the first tab by default
1517 this.$('.select-tab').eq(0).click();
1518 }
1519
1520 } );
1521
1522 })(jQuery, _, socssOptions);
1523
1524 // Setup the main editor
1525 jQuery(function ($) {
1526 var socss = window.socss;
1527
1528 // Setup the editor
1529 var editor = new socss.view.editor({
1530 el: $('#so-custom-css-form').get(0)
1531 });
1532 editor.render();
1533 editor.setSnippets(socssOptions.snippets);
1534
1535 window.socss.mainEditor = editor;
1536
1537 // This is for hiding the getting started video
1538 $('#so-custom-css-getting-started a.hide').click( function(e){
1539 e.preventDefault();
1540 $('#so-custom-css-getting-started').slideUp();
1541 $.get( $(this).attr('href') );
1542 } );
1543 });
1544