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