PluginProbe ʕ •ᴥ•ʔ
SiteOrigin CSS / 1.1.3
SiteOrigin CSS v1.1.3
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
URI.js 9 years ago URI.min.js 9 years ago css.js 9 years ago css.min.js 9 years ago csslint.js 9 years ago csslint.min.js 9 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 9 years ago specificity.js 11 years ago specificity.min.js 9 years ago
editor.js
1676 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( $('#template-preview-window').html() ),
426 editor: null,
427 originalUri: null,
428 currentUri: null,
429
430 initialize: function (attr) {
431 this.editor = attr.editor;
432
433 var thisView = this;
434 this.editor.codeMirror.on('change', function (cm, c) {
435 thisView.updatePreviewCss();
436 });
437 },
438
439 render: function () {
440 var thisView = this;
441
442 this.$el.html( this.template() );
443
444 this.$( '#preview-iframe' )
445 .attr( 'src', socssOptions.homeURL )
446 .on( 'load', function () {
447 var $$ = $(this);
448
449 // Update the current URI with the iframe URI
450 thisView.currentUri = new URI( $$.contents().get(0).location.href );
451 thisView.currentUri.removeQuery( 'so_css_preview' );
452 thisView.$( '#preview-navigator input' ).val( thisView.currentUri.toString() );
453 thisView.currentUri.addQuery( 'so_css_preview', 1 );
454
455 $$.contents().find('a').each(function () {
456 var href = $(this).attr('href');
457 if (href === undefined) {
458 return true;
459 }
460
461 var firstSeperator = (href.indexOf('?') === -1 ? '?' : '&');
462 $(this).attr('href', href + firstSeperator + 'so_css_preview=1');
463 });
464
465 thisView.updatePreviewCss();
466 })
467 .mouseleave(function () {
468 thisView.clearHighlight();
469 });
470
471 this.$( '#preview-navigator input' ).keydown( function( e ){
472 var $$ = $(this);
473
474 if( e.keyCode == 13 ) {
475 e.preventDefault();
476
477 var newUri = new URI( $$.val() );
478
479 // Validate the URI
480 if(
481 thisView.originalUri.host() !== newUri.host() ||
482 thisView.originalUri.protocol() !== newUri.protocol()
483 ) {
484 $$.blur();
485 alert( $$.data( 'invalid-uri' ) );
486 $$.focus();
487 }
488 else {
489 newUri.addQuery( 'so_css_preview', 1 );
490 thisView.$( '#preview-iframe' ).attr( 'src', newUri.toString() );
491 }
492 }
493 } );
494
495 this.originalUri = new URI( socssOptions.homeURL );
496 this.currentUri = new URI( socssOptions.homeURL );
497
498 this.currentUri.removeQuery( 'so_css_preview' );
499 this.$('#preview-navigator input').val( this.currentUri.toString() );
500 this.currentUri.addQuery( 'so_css_preview', 1 );
501 },
502
503 /**
504 * Update the preview CSS from the CodeMirror value in the editor
505 */
506 updatePreviewCss: function () {
507 var preview = this.$('#preview-iframe');
508 if (preview.length === 0) {
509 return;
510 }
511
512 var head = preview.contents().find('head');
513 if (head.find('style.siteorigin-custom-css').length === 0) {
514 head.append('<style class="siteorigin-custom-css" type="text/css"></style>');
515 }
516 var style = head.find('style.siteorigin-custom-css');
517
518 // Update the CSS after a short delay
519 var css = this.editor.codeMirror.getValue().trim();
520 style.html(css);
521 },
522
523 /**
524 * Highlight all elements with a given selector
525 */
526 highlight: function (selector) {
527 try {
528 this.editor.inspector.hl.highlight(selector);
529 }
530 catch (err) {
531 console.log('No inspector to highlight with');
532 }
533 },
534
535 /**
536 * Clear the currently highlighted elements in preview
537 */
538 clearHighlight: function () {
539 try {
540 this.editor.inspector.hl.clear();
541 }
542 catch (err) {
543 console.log('No inspector to highlight with');
544 }
545 }
546
547 });
548
549 /**
550 * The dialog for the snippets browser
551 */
552 socss.view.snippets = Backbone.View.extend({
553 template: _.template($('#template-snippet-browser').html()),
554 snippet: _.template('<li class="snippet"><%- name %></li>'),
555 className: 'css-editor-snippet-browser',
556 snippets: null,
557 editor: null,
558
559 events: {
560 'click .close': 'hide',
561 'click .buttons .insert-snippet': 'insertSnippet'
562 },
563
564 currentSnippet: null,
565
566 initialize: function (args) {
567 this.snippets = args.snippets;
568 },
569
570 render: function () {
571 var thisView = this;
572
573
574 var clickSnippet = function (e) {
575 e.preventDefault();
576 var $$ = $(this);
577
578 thisView.$('.snippets li.snippet').removeClass('active');
579 $(this).addClass('active');
580 thisView.viewSnippet({
581 name: $$.html(),
582 description: $$.data('description'),
583 css: $$.data('css')
584 });
585 };
586
587 this.$el.html(this.template());
588 for (var i = 0; i < this.snippets.length; i++) {
589 $(this.snippet({name: this.snippets[i].Name}))
590 .data({
591 'description': this.snippets[i].Description,
592 'css': this.snippets[i].css
593 })
594 .appendTo(this.$('ul.snippets'))
595 .click(clickSnippet);
596 }
597
598 // Click on the first one
599 thisView.$('.snippets li.snippet').eq(0).click();
600
601 this.attach();
602 return this;
603 },
604
605 viewSnippet: function (args) {
606 var w = this.$('.main .snippet-view');
607
608 w.find('.snippet-title').html(args.name);
609 w.find('.snippet-description').html(args.description);
610 w.find('.snippet-code').html(args.css);
611
612 this.currentSnippet = args;
613 },
614
615 insertSnippet: function () {
616 var editor = this.editor.codeMirror;
617 var css = this.currentSnippet.css;
618
619 var before_css = '';
620 if (editor.doc.lineCount() === 1 && editor.doc.getLine(editor.doc.lastLine()).length === 0) {
621 before_css = "";
622 }
623 else if (editor.doc.getLine(editor.doc.lastLine()).length === 0) {
624 before_css = "\n";
625 }
626 else {
627 before_css = "\n\n";
628 }
629
630 // Now insert the code in the editor
631 editor.doc.setCursor(
632 editor.doc.lastLine(),
633 editor.doc.getLine(editor.doc.lastLine()).length
634 );
635 editor.doc.replaceSelection(before_css + css);
636
637 this.hide();
638 },
639
640 attach: function () {
641 this.$el.appendTo('body');
642 },
643
644 show: function () {
645 this.$el.show();
646 },
647
648 hide: function () {
649 this.$el.hide();
650 }
651 });
652
653
654 /**
655 * The visual properties editor
656 */
657 socss.view.properties = Backbone.View.extend({
658
659 model: socss.model.cssRules,
660
661 tabTemplate: _.template('<li data-section="<%- id %>"><span class="fa fa-<%- icon %>"></span> <%- title %></li>'),
662 sectionTemplate: _.template('<div class="section" data-section="<%- id %>"><table class="fields-table"><tbody></tbody></table></div>'),
663 controllerTemplate: _.template('<tr><th scope="row"><%- title %></th><td></td></tr>'),
664
665 /**
666 * The controllers for each of the properties
667 */
668 propertyControllers: [],
669
670 /**
671 * The editor view
672 */
673 editor: null,
674
675 /**
676 * The current, raw CSS
677 */
678 css: '',
679
680 /**
681 * Parsed CSS
682 */
683 parsed: {},
684
685 /**
686 * The current active selector
687 */
688 activeSelector: '',
689
690 /**
691 * Was the editor expanded before we went into the property editor
692 */
693 editorExpandedBefore: false,
694
695 events: {
696 'click .close': 'hide'
697 },
698
699 /**
700 * Initialize the properties editor with a new model
701 */
702 initialize: function ( attr ) {
703 this.parser = window.css;
704 this.editor = attr.editor;
705 },
706
707 /**
708 * Render the property editor
709 */
710 render: function () {
711 var thisView = this;
712
713 // Clean up for potential re-renders
714 this.$('.section-tabs').empty();
715 this.$('.sections').empty();
716 this.$('.toolbar select').off();
717 thisView.propertyControllers = [];
718
719 var controllers = socssOptions.propertyControllers;
720
721 for (var id in controllers) {
722 // Create the tabs
723 var $t = $(this.tabTemplate({
724 id: id,
725 icon: controllers[id].icon,
726 title: controllers[id].title
727 })).appendTo(this.$('.section-tabs'));
728
729 // Create the section wrapper
730 var $s = $(this.sectionTemplate({
731 id: id
732 })).appendTo(this.$('.sections'));
733
734 // Now lets add the controllers
735 if (!_.isEmpty(controllers[id].controllers)) {
736
737 for (var i = 0; i < controllers[id].controllers.length; i++) {
738
739 var $c = $(thisView.controllerTemplate({
740 title: controllers[id].controllers[i].title
741 })).appendTo($s.find('tbody'));
742
743 var controllerAtts = controllers[id].controllers[i];
744 var controller;
745
746 if (typeof socss.view.properties.controllers[controllerAtts.type] === 'undefined') {
747 // Setup a default controller
748 controller = new socss.view.propertyController({
749 el: $c.find('td'),
750 propertiesView: thisView,
751 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
752 });
753 }
754 else {
755 // Setup a specific controller
756 controller = new socss.view.properties.controllers[controllerAtts.type]({
757 el: $c.find('td'),
758 propertiesView: thisView,
759 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
760 });
761 }
762
763 thisView.propertyControllers.push(controller);
764
765 // Setup and render the controller
766 controller.render();
767 controller.initChangeEvents();
768 }
769 }
770 }
771
772 // Setup the tab switching for the property sections
773 this.$('.section-tabs li').click(function () {
774 var $$ = $(this);
775 var show = thisView.$('.sections .section[data-section="' + $$.data('section') + '"]');
776
777 thisView.$('.sections .section').not(show).hide().removeClass('active');
778 show.show().addClass('active');
779
780 thisView.$('.section-tabs li').not($$).removeClass('active');
781 $$.addClass('active');
782 }).eq(0).click();
783
784 this.$('.toolbar select').change(function () {
785 thisView.setActivateSelector($(this).find(':selected').data('selector'));
786 });
787 },
788
789 /**
790 * Sets the rule value for the active selector
791 * @param rule
792 * @param value
793 */
794 setRuleValue: function (rule, value) {
795 if (
796 typeof this.activeSelector === 'undefined' ||
797 typeof this.activeSelector.declarations === 'undefined'
798 ) {
799 return;
800 }
801
802 var declarations = this.activeSelector.declarations;
803 var newRule = true;
804 var valueChanged = false;
805 for (var i = 0; i < declarations.length; i++) {
806 if (declarations[i].property === rule) {
807 newRule = false;
808 var declaration = declarations[i];
809 if ( declaration.value !== value ) {
810 declaration.value = value;
811 valueChanged = true;
812 }
813
814 // Remove empty declarations
815 if ( _.isEmpty( declaration.value ) ) {
816 declarations.splice( declarations.indexOf( declaration ) );
817 }
818 break;
819 }
820 }
821
822 if ( newRule && !_.isEmpty( value ) ) {
823 declarations.push({
824 property: rule,
825 value: value,
826 type: 'declaration',
827 });
828 valueChanged = true;
829 }
830
831 if ( valueChanged ) {
832 this.updateMainEditor(false);
833 }
834 },
835
836 /**
837 * Adds the @import rule value if it doesn't already exist.
838 *
839 * @param newRule
840 *
841 */
842 addImport: function (newRule) {
843
844 // get @import rules
845 // check if any have the same value
846 // if not, then add the new @ rule
847
848 var importRules = _.filter( this.parsed.stylesheet.rules, function ( rule) {
849 return rule.type === 'import';
850 } );
851 var exists = _.any( importRules, function ( rule ) {
852 return rule.import === newRule.import;
853 } );
854
855 if ( !exists ) {
856 // Add it to the top!
857 // @import statements must precede other rule types.
858 this.parsed.stylesheet.rules.unshift( newRule );
859 this.updateMainEditor( false );
860 }
861
862 },
863
864 /**
865 * Find @import which completely or partially contains the specified value.
866 *
867 * @param value
868 */
869 findImport: function(value) {
870 return _.find( this.parsed.stylesheet.rules, function ( rule ) {
871 return rule.type === 'import' && rule.import.indexOf(value) > -1;
872 } );
873 },
874
875 /**
876 * Find @import which completely or partially contains the identifier value and update it's import property.
877 *
878 * @param identifier
879 * @param value
880 */
881 updateImport: function(identifier, value) {
882 var importRule = this.findImport(identifier);
883 if ( importRule.import !== value.import ) {
884 importRule.import = value.import;
885 this.updateMainEditor(false);
886 }
887 },
888
889 /**
890 * Find @import which completely or partially contains the identifier value and remove it.
891 *
892 * @param identifier
893 */
894 removeImport: function(identifier) {
895 var importIndex = _.findIndex( this.parsed.stylesheet.rules, function ( rule ) {
896 return rule.type === 'import' && rule.import.indexOf(identifier) > -1;
897 } );
898 if ( importIndex > -1 ) {
899 this.parsed.stylesheet.rules.splice(importIndex, 1);
900 }
901 },
902
903 /**
904 * Get the rule value for the active selector
905 * @param rule
906 */
907 getRuleValue: function (rule) {
908 if (typeof this.activeSelector === 'undefined' || typeof this.activeSelector.declarations === 'undefined') {
909 return '';
910 }
911
912 var declarations = this.activeSelector.declarations;
913 for (var i = 0; i < declarations.length; i++) {
914 if (declarations[i].property === rule) {
915 return declarations[i].value;
916 }
917 }
918 return '';
919 },
920
921 /**
922 * Update the main editor with the value of the parsed CSS
923 */
924 updateMainEditor: function ( compress ) {
925 //TODO: add back compress option to remove/merge duplicated CSS selectors.
926 this.editor.codeMirror.setValue( this.parser.stringify( this.parsed ) );
927 },
928
929 /**
930 * Show the properties editor
931 */
932 show: function () {
933 this.editorExpandedBefore = this.editor.isExpanded();
934 this.editor.setExpand(true);
935
936 this.$el.show().animate({'left': 0}, 'fast');
937 },
938
939 /**
940 * Hide the properties editor
941 */
942 hide: function () {
943 this.editor.setExpand(this.editorExpandedBefore);
944 this.$el.animate( {'left': -338}, 'fast', function(){
945 $(this).hide();
946 } );
947
948 // Update the main editor with compressed CSS when we close the properties editor
949 this.updateMainEditor( true );
950 },
951
952 /**
953 * @returns boolean
954 */
955 isVisible: function () {
956 return this.$el.is(':visible');
957 },
958
959 /**
960 * Loads a single CSS selector and associated properties into the model
961 * @param css
962 */
963 loadCSS: function (css, activeSelector) {
964 this.css = css;
965
966 // Load the CSS
967 this.parsed = this.parser.parse(css, {
968 silent:true
969 } );
970 var rules = this.parsed.stylesheet.rules;
971
972 // Add the dropdown menu items
973 var dropdown = this.$('.toolbar select').empty();
974 for (var i = 0; i < rules.length; i++) {
975 var rule = rules[i];
976
977 // Exclude @import statements
978 if ( ! _.contains( [ 'rule', 'media' ], rule.type) ) {
979 continue;
980 }
981
982 if( rule.type === 'media' ) {
983
984 for (var j = 0; j < rule.rules.length; j++) {
985 var mediaRule = '@media ' + rule.media;
986 var subRule = rule.rules[j];
987 if(subRule.type != 'rule') {
988 continue;
989 }
990 dropdown.append(
991 $('<option>')
992 .html( mediaRule + ': ' + subRule.selectors.join(',') )
993 .attr( 'val', mediaRule + ': ' + subRule.selectors.join(',') )
994 .data( 'selector', subRule )
995 );
996 }
997
998 }
999 else {
1000 dropdown.append(
1001 $('<option>')
1002 .html( rule.selectors.join(',') )
1003 .attr( 'val', rule.selectors.join(',') )
1004 .data( 'selector', rule )
1005 );
1006 }
1007 }
1008
1009 if (typeof activeSelector === 'undefined') {
1010 activeSelector = dropdown.find('option').eq(0).attr('val');
1011 }
1012 if(!_.isEmpty(activeSelector)) {
1013 dropdown.val(activeSelector).change();
1014 }
1015 },
1016
1017 /**
1018 * Set the selector that we're currently dealing with
1019 * @param selector
1020 */
1021 setActivateSelector: function (selector) {
1022 this.activeSelector = selector;
1023 for (var i = 0; i < this.propertyControllers.length; i++) {
1024 this.propertyControllers[i].refreshFromRule();
1025 }
1026 },
1027
1028 /**
1029 * Add or select a selector.
1030 *
1031 * @param selector
1032 */
1033 addSelector: function(selector) {
1034 // Check if this selector already exists
1035 var dropdown = this.$('.toolbar select');
1036 dropdown.val( selector );
1037
1038 if( dropdown.val() === selector ) {
1039 // Trigger a change event to load the existing selector
1040 dropdown.change();
1041 }
1042 else {
1043 // The selector doesn't exist, so add it to the CSS, then reload
1044 this.editor.addEmptySelector(selector);
1045 this.loadCSS( this.editor.codeMirror.getValue().trim(), selector );
1046 }
1047
1048 dropdown.addClass('highlighted');
1049 setTimeout(function(){
1050 dropdown.removeClass('highlighted');
1051 }, 2000);
1052 }
1053
1054 });
1055
1056 // The basic property controller
1057 socss.view.propertyController = Backbone.View.extend({
1058
1059 template: _.template('<input type="text" value="" />'),
1060 activeRule: null,
1061 args: null,
1062 propertiesView: null,
1063
1064 initialize: function (args) {
1065
1066 this.args = args.args;
1067 this.propertiesView = args.propertiesView;
1068
1069 // By default, update the active rule whenever things change
1070 this.on('set_value', this.updateRule, this);
1071 this.on('change', this.updateRule, this);
1072 },
1073
1074 /**
1075 * Render the property field controller
1076 */
1077 render: function () {
1078 this.$el.append( $(this.template( {} )) );
1079 this.field = this.$('input');
1080 },
1081
1082 /**
1083 * Initialize the events that constitute a change
1084 */
1085 initChangeEvents: function () {
1086 var thisView = this;
1087 this.field.on( 'change keyup', function () {
1088 thisView.trigger('change', $(this).val());
1089 } );
1090 },
1091
1092
1093 /**
1094 * Update the value of an active rule
1095 */
1096 updateRule: function () {
1097 this.propertiesView.setRuleValue(
1098 this.args.property,
1099 this.getValue()
1100 );
1101 },
1102
1103 /**
1104 * This is called when the selector changes
1105 */
1106 refreshFromRule: function () {
1107 var value = this.propertiesView.getRuleValue(this.args.property);
1108 this.setValue(value, {silent: true});
1109 },
1110
1111 /**
1112 * Get the current value
1113 * @return string
1114 */
1115 getValue: function () {
1116 return this.field.val();
1117 },
1118
1119 /**
1120 * Set the current value
1121 * @param socss.view.properties val
1122 */
1123 setValue: function (val, options) {
1124 options = _.extend({silent: false}, options);
1125
1126 this.field.val(val);
1127
1128 if (!options.silent) {
1129 this.trigger('set_value', val);
1130 }
1131 },
1132
1133 /**
1134 * Reset the current value
1135 */
1136 reset: function (options) {
1137 options = _.extend({silent: false}, options);
1138
1139 this.setValue('', options);
1140 }
1141
1142 });
1143
1144 // All the value controllers
1145 socss.view.properties.controllers = {};
1146
1147 // The color controller
1148 socss.view.properties.controllers.color = socss.view.propertyController.extend({
1149
1150 template: _.template('<input type="text" value="" />'),
1151
1152 render: function () {
1153 var thisView = this;
1154
1155 this.$el.append($(this.template({})));
1156
1157 // Set this up as a color picker
1158 this.field = this.$el.find('input');
1159 this.field.minicolors({});
1160
1161 },
1162
1163 initChangeEvents: function () {
1164 var thisView = this;
1165 this.field.on('change keyup', function () {
1166 thisView.trigger('change', thisView.field.minicolors('value'));
1167 });
1168 },
1169
1170 getValue: function () {
1171 return this.field.minicolors('value').trim();
1172 },
1173
1174 setValue: function (val, options) {
1175 options = _.extend({silent: false}, options);
1176
1177 this.field.minicolors('value', val);
1178
1179 if (!options.silent) {
1180 this.trigger('set_value', val);
1181 }
1182 }
1183
1184 });
1185
1186 // The dropdown select box controller
1187 socss.view.properties.controllers.select = socss.view.propertyController.extend( {
1188 template: _.template('<select></select>'),
1189
1190 render: function(){
1191 var thisView = this;
1192
1193 this.$el.append($(this.template({})));
1194 this.field = this.$el.find('select');
1195
1196 // Add the unchanged option
1197 this.field.append( $('<option value=""></option>').html('') );
1198
1199 // Add all the options to the dropdown
1200 for( var k in this.args.options ) {
1201 this.field.append( $('<option></option>').attr('value', k).html( this.args.options[k] ) );
1202 }
1203
1204 if( typeof this.args.option_icons !== 'undefined' ) {
1205 this.setupVisualSelect();
1206 }
1207 },
1208
1209 setupVisualSelect: function(){
1210 var thisView = this;
1211 this.field.hide();
1212
1213 var $tc = $('<div class="select-tabs"></div>').appendTo( this.$el );
1214
1215 // Add the none value
1216 $('<div class="select-tab" data-value=""><span class="fa fa-circle-o"></span></div>').appendTo($tc);
1217
1218 // Now add one for each of the option icons
1219 for ( var k in this.args.option_icons ) {
1220 $('<div class="select-tab"></div>')
1221 .appendTo($tc)
1222 .append(
1223 $('<span class="fa"></span>')
1224 .addClass('fa-' + this.args.option_icons[k])
1225 )
1226 .attr('data-value', k)
1227 ;
1228 }
1229
1230 $tc.find('.select-tab')
1231 .css('width', 100/( $tc.find('>div').length ) + "%" )
1232 .click( function(){
1233 var $t = $(this);
1234 $tc.find('.select-tab').removeClass('active');
1235 $t.addClass('active');
1236 thisView.field.val( $t.data('value')).change();
1237 } );
1238 },
1239
1240 /**
1241 * Set the current value
1242 * @param socss.view.properties val
1243 */
1244 setValue: function (val, options) {
1245 options = _.extend({silent: false}, options);
1246
1247 this.field.val(val);
1248
1249 this.$('.select-tabs .select-tab').removeClass('active').filter('[data-value="' + val + '"]').addClass('active');
1250
1251 if (!options.silent) {
1252 this.trigger('set_value', val);
1253 }
1254 }
1255
1256 } );
1257
1258 // A field that lets a user upload an image
1259 socss.view.properties.controllers.image = socss.view.propertyController.extend( {
1260 template: _.template('<input type="text" value="" /> <span class="select socss-button"><span class="fa fa-upload"></span></span>'),
1261
1262 render: function(){
1263 var thisView = this;
1264
1265 this.media = wp.media({
1266 // Set the title of the modal.
1267 title: socssOptions.loc.select_image,
1268
1269 // Tell the modal to show only images.
1270 library: {
1271 type: 'image'
1272 },
1273
1274 // Customize the submit button.
1275 button: {
1276 // Set the text of the button.
1277 text: socssOptions.loc.select,
1278 // Tell the button not to close the modal, since we're
1279 // going to refresh the page when the image is selected.
1280 close: false
1281 }
1282 });
1283
1284 this.$el.append( $(this.template({
1285 select: socssOptions.loc.select
1286 })) );
1287
1288 this.field = this.$el.find('input');
1289
1290 this.$('.select').click(function(){
1291 thisView.media.open();
1292 });
1293
1294 this.media.on('select', function(){
1295 // Grab the selected attachment.
1296 var attachment = this.state().get('selection').first().attributes;
1297 var val = thisView.args.value.replace('{{url}}', attachment.url);
1298
1299 // Change the field value and trigger a change event
1300 thisView.field.val( val ).change();
1301
1302 // Close the image selector
1303 thisView.media.close();
1304
1305 }, this.media);
1306 }
1307
1308 } );
1309
1310 // A simple measurement field
1311 socss.view.properties.controllers.measurement = socss.view.propertyController.extend( {
1312
1313 wrapperClass: 'socss-field-measurement',
1314
1315 render: function(){
1316 this.$el.append($(this.template({})));
1317 this.field = this.$('input');
1318 this.setupMeasurementField( this.field, {} );
1319 },
1320
1321 setValue: function (val, options) {
1322 options = _.extend({silent: false}, options);
1323 this.field.val(val).trigger('measurement_refresh');
1324 if (!options.silent) {
1325 this.trigger('set_value', val);
1326 }
1327 },
1328
1329 units : [
1330 'px',
1331 '%',
1332 'em',
1333 'cm',
1334 'mm',
1335 'in',
1336 'pt',
1337 'pc',
1338 'ex',
1339 'ch',
1340 'rem',
1341 'vw',
1342 'vh',
1343 'vmin',
1344 'vmax'
1345 ],
1346
1347 parseUnits: function( value ){
1348 var escapeRegExp = function(str) {
1349 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
1350 };
1351
1352 var regexUnits = this.units.map(escapeRegExp);
1353 var regex = new RegExp('([0-9\\.\\-]+)(' + regexUnits.join('|') + ')?', 'i');
1354 var result = regex.exec( value );
1355
1356 if( result === null ) {
1357 return {
1358 value: '',
1359 unit: ''
1360 };
1361 }
1362 else {
1363 return {
1364 value: result[1],
1365 unit: result[2] === undefined ? '' : result[2]
1366 };
1367 }
1368 },
1369
1370 setupMeasurementField: function( $el, options ){
1371 var thisView = this;
1372 var $p = $el.parent();
1373
1374 options = _.extend( {
1375 defaultUnit: 'px'
1376 }, options );
1377
1378 $el.hide();
1379 $p.addClass( this.wrapperClass ).data('unit', options.defaultUnit);
1380
1381 // Create the fake input field
1382 var $fi = $('<input type="text" class="socss-field-input"/>').appendTo($p);
1383 var $da = $('<span class="dashicons dashicons-arrow-down"></span>').appendTo($p);
1384 var $dd = $('<ul class="dropdown"></ul>').appendTo($p);
1385 var $u = $('<span class="units"></span>').html( options.defaultUnit ).appendTo( $p );
1386
1387 for( var i = 0; i < thisView.units.length; i++ ) {
1388 var $o = $('<li></li>').html( thisView.units[i] ).data('unit', thisView.units[i]);
1389 if( thisView.units[i] === options.defaultUnit ) {
1390 $o.addClass('active');
1391 }
1392 $dd.append( $o );
1393 }
1394
1395 var updateValue = function(){
1396 var value = thisView.parseUnits( $fi.val() );
1397
1398 if( value.unit !== '' && value.unit !== $p.data( 'unit' ) ) {
1399 $fi.val( value.value );
1400 setUnit( value.unit );
1401 }
1402
1403 if( value.value === '' ) {
1404 $el.val( '' );
1405 }
1406 else {
1407 $el.val( value.value + $p.data( 'unit' ) );
1408 }
1409 };
1410
1411 var setUnit = function( unit ){
1412 $u.html( unit );
1413 $p.data( 'unit', unit );
1414 $fi.trigger('keydown');
1415 };
1416
1417 $da.click( function(){
1418 $dd.toggle();
1419 } );
1420
1421 $dd.find('li').click( function(){
1422 $dd.toggle();
1423 setUnit( $(this).data('unit') );
1424 updateValue();
1425 $el.trigger('change');
1426 } );
1427
1428 $fi.on( 'keyup keydown', function(e){
1429 var $$ = $(this);
1430
1431 var char = '';
1432 if( e.type === 'keydown' ) {
1433 if(e.keyCode >= 48 && e.keyCode <= 57 ) {
1434 char = String.fromCharCode(e.keyCode);
1435 }
1436 else if( e.keyCode === 189 ) {
1437 char = '-';
1438 }
1439 else if( e.keyCode === 190 ) {
1440 char = '.';
1441 }
1442 }
1443
1444 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1445 .css( {
1446 'font-size' : '14px'
1447 } )
1448 .html( $fi.val() + char )
1449 .appendTo( 'body' );
1450 var width = $pl.width();
1451 width = Math.min(width, 63);
1452 $pl.remove();
1453
1454 $u.css('left', width + 12);
1455 } );
1456
1457 $fi.on('keyup', function(e){
1458 updateValue();
1459 $el.trigger('change');
1460 } );
1461
1462 $el.on('measurement_refresh', function(){
1463 var value = thisView.parseUnits( $el.val() );
1464 $fi.val( value.value );
1465
1466 var unit = value.unit === '' ? options.defaultUnit : value.unit;
1467 $p.data( 'unit', unit );
1468 $u.html( unit );
1469
1470 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1471 .css({
1472 'font-size' : '14px'
1473 })
1474 .html( value.value )
1475 .appendTo( 'body' );
1476 var width = $pl.width();
1477 width = Math.min(width, 63);
1478 $pl.remove();
1479
1480 $u.css('left', width + 12);
1481 } );
1482
1483 // Now add the increment/decrement buttons
1484 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1485 var $dec = $('<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>').appendTo($diw);
1486 var $inc = $('<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>').appendTo($diw);
1487
1488 // Increment is clicked
1489 $inc.click( function(){
1490 var value = thisView.parseUnits( $el.val() );
1491 if( value.value === '' ) {
1492 return true;
1493 }
1494
1495 var newVal = Math.ceil( value.value * 1.05 );
1496
1497 $fi.val( newVal );
1498 updateValue();
1499 $el.trigger('change').trigger('measurement_refresh');
1500 } );
1501
1502 $dec.click( function(){
1503 var value = thisView.parseUnits( $el.val() );
1504 if( value.value === '' ) {
1505 return true;
1506 }
1507
1508 var newVal = Math.floor( value.value / 1.05 );
1509
1510 $fi.val( newVal );
1511 updateValue();
1512 $el.trigger('change').trigger('measurement_refresh');
1513 } );
1514 }
1515
1516 } );
1517
1518 // A simple measurement field
1519 socss.view.properties.controllers.number = socss.view.propertyController.extend( {
1520
1521 render: function(){
1522 this.$el.append($(this.template({})));
1523 this.field = this.$('input');
1524
1525 // Setup the measurement field
1526 this.setupNumberField(this.field, this.args);
1527 },
1528
1529 /**
1530 * Setup the number field
1531 * @param el
1532 * @param options
1533 */
1534 setupNumberField: function($el, options){
1535 options = _.extend({
1536 change: null,
1537 default: 0,
1538 increment: 1,
1539 decrement: -1,
1540 max: null,
1541 min: null
1542 }, options);
1543
1544 var $p = $el.parent();
1545 $p.addClass('socss-field-number');
1546
1547 // Now add the increment/decrement buttons
1548 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1549 var $dec = $('<div class="dec-button socss-button">-</div>').appendTo($diw);
1550 var $inc = $('<div class="inc-button socss-button">+</div>').appendTo($diw);
1551
1552 // Increment is clicked
1553 $diw.find('> div').click( function(e){
1554 e.preventDefault();
1555
1556 var val = options.default;
1557 if( $el.val() !== '' ) {
1558 val = Number($el.val());
1559 }
1560 val = val + ( $(this).is( $dec ) ? options.decrement : options.increment );
1561
1562 val = Math.round(val*100)/100;
1563
1564 if( options.max !== null ) {
1565 val = Math.min( options.max, val);
1566 }
1567
1568 if( options.min !== null ) {
1569 val = Math.max( options.min, val);
1570 }
1571
1572 $el.val( val );
1573 $el.trigger('change');
1574 } );
1575
1576 return this;
1577 }
1578
1579 } );
1580
1581
1582 socss.view.properties.controllers.sides = socss.view.propertyController.extend( {
1583
1584 template: _.template( $('#template-sides-field').html().trim() ),
1585
1586 controllers: [],
1587
1588 render: function(){
1589 var thisView = this;
1590
1591 this.$el.append( $(this.template({})) );
1592 this.field = this.$el.find('input');
1593
1594 if( !thisView.args.hasAll ) {
1595 this.$('.select-tab').eq(0).remove();
1596 this.$('.select-tab').css('width', '25%');
1597 }
1598
1599 this.$('.select-tab').each( function(){
1600 var dir = $(this).data('direction');
1601
1602 var container = $('<li class="side">')
1603 .appendTo( thisView.$('.sides') )
1604 .hide();
1605
1606 for( var i = 0; i < thisView.args.controllers.length; i++ ) {
1607
1608 var controllerArgs = thisView.args.controllers[i];
1609
1610 if( typeof socss.view.properties.controllers[ controllerArgs.type ] ) {
1611
1612 // Create the measurement view
1613 var property = '';
1614 if( dir === 'all' ) {
1615 property = controllerArgs.args.propertyAll;
1616 }
1617 else {
1618 property = controllerArgs.args.property.replace('{dir}', dir);
1619 }
1620
1621 var theseControllerArgs = _.extend({}, controllerArgs.args, {property: property});
1622
1623 var controller = new socss.view.properties.controllers[ controllerArgs.type ]( {
1624 el: $('<div>').appendTo( container ),
1625 propertiesView: thisView.propertiesView,
1626 args: theseControllerArgs
1627 } );
1628
1629 // Setup and render the measurement controller and register it with the properties view
1630 controller.render();
1631 controller.initChangeEvents();
1632 thisView.propertiesView.propertyControllers.push(controller);
1633
1634 }
1635
1636 }
1637
1638 $(this).on( 'click', function(){
1639 thisView.$('.select-tab').removeClass('active');
1640 $(this).addClass('active');
1641
1642 thisView.$('.sides .side').hide();
1643 container.show();
1644 } );
1645
1646 } );
1647
1648 // Select the first tab by default
1649 this.$('.select-tab').eq(0).click();
1650 }
1651
1652 } );
1653
1654 })(jQuery, _, socssOptions);
1655
1656 // Setup the main editor
1657 jQuery(function ($) {
1658 var socss = window.socss;
1659
1660 // Setup the editor
1661 var editor = new socss.view.editor({
1662 el: $('#so-custom-css-form').get(0)
1663 });
1664 editor.render();
1665 editor.setSnippets(socssOptions.snippets);
1666
1667 window.socss.mainEditor = editor;
1668
1669 // This is for hiding the getting started video
1670 $('#so-custom-css-getting-started a.hide').click( function(e){
1671 e.preventDefault();
1672 $('#so-custom-css-getting-started').slideUp();
1673 $.get( $(this).attr('href') );
1674 } );
1675 });
1676