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