PluginProbe ʕ •ᴥ•ʔ
SiteOrigin CSS / 1.1.1
SiteOrigin CSS v1.1.1
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 11 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
1673 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 .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 dropdown.append(
988 $('<option>')
989 .html( mediaRule + ': ' + subRule.selectors.join(',') )
990 .attr( 'val', mediaRule + ': ' + subRule.selectors.join(',') )
991 .data( 'selector', subRule )
992 );
993 }
994
995 }
996 else {
997 dropdown.append(
998 $('<option>')
999 .html( rule.selectors.join(',') )
1000 .attr( 'val', rule.selectors.join(',') )
1001 .data( 'selector', rule )
1002 );
1003 }
1004 }
1005
1006 if (typeof activeSelector === 'undefined') {
1007 activeSelector = dropdown.find('option').eq(0).attr('val');
1008 }
1009 if(!_.isEmpty(activeSelector)) {
1010 dropdown.val(activeSelector).change();
1011 }
1012 },
1013
1014 /**
1015 * Set the selector that we're currently dealing with
1016 * @param selector
1017 */
1018 setActivateSelector: function (selector) {
1019 this.activeSelector = selector;
1020 for (var i = 0; i < this.propertyControllers.length; i++) {
1021 this.propertyControllers[i].refreshFromRule();
1022 }
1023 },
1024
1025 /**
1026 * Add or select a selector.
1027 *
1028 * @param selector
1029 */
1030 addSelector: function(selector) {
1031 // Check if this selector already exists
1032 var dropdown = this.$('.toolbar select');
1033 dropdown.val( selector );
1034
1035 if( dropdown.val() === selector ) {
1036 // Trigger a change event to load the existing selector
1037 dropdown.change();
1038 }
1039 else {
1040 // The selector doesn't exist, so add it to the CSS, then reload
1041 this.editor.addEmptySelector(selector);
1042 this.loadCSS( this.editor.codeMirror.getValue().trim(), selector );
1043 }
1044
1045 dropdown.addClass('highlighted');
1046 setTimeout(function(){
1047 dropdown.removeClass('highlighted');
1048 }, 2000);
1049 }
1050
1051 });
1052
1053 // The basic property controller
1054 socss.view.propertyController = Backbone.View.extend({
1055
1056 template: _.template('<input type="text" value="" />'),
1057 activeRule: null,
1058 args: null,
1059 propertiesView: null,
1060
1061 initialize: function (args) {
1062
1063 this.args = args.args;
1064 this.propertiesView = args.propertiesView;
1065
1066 // By default, update the active rule whenever things change
1067 this.on('set_value', this.updateRule, this);
1068 this.on('change', this.updateRule, this);
1069 },
1070
1071 /**
1072 * Render the property field controller
1073 */
1074 render: function () {
1075 this.$el.append( $(this.template( {} )) );
1076 this.field = this.$('input');
1077 },
1078
1079 /**
1080 * Initialize the events that constitute a change
1081 */
1082 initChangeEvents: function () {
1083 var thisView = this;
1084 this.field.on( 'change keyup', function () {
1085 thisView.trigger('change', $(this).val());
1086 } );
1087 },
1088
1089
1090 /**
1091 * Update the value of an active rule
1092 */
1093 updateRule: function () {
1094 this.propertiesView.setRuleValue(
1095 this.args.property,
1096 this.getValue()
1097 );
1098 },
1099
1100 /**
1101 * This is called when the selector changes
1102 */
1103 refreshFromRule: function () {
1104 var value = this.propertiesView.getRuleValue(this.args.property);
1105 this.setValue(value, {silent: true});
1106 },
1107
1108 /**
1109 * Get the current value
1110 * @return string
1111 */
1112 getValue: function () {
1113 return this.field.val();
1114 },
1115
1116 /**
1117 * Set the current value
1118 * @param socss.view.properties val
1119 */
1120 setValue: function (val, options) {
1121 options = _.extend({silent: false}, options);
1122
1123 this.field.val(val);
1124
1125 if (!options.silent) {
1126 this.trigger('set_value', val);
1127 }
1128 },
1129
1130 /**
1131 * Reset the current value
1132 */
1133 reset: function (options) {
1134 options = _.extend({silent: false}, options);
1135
1136 this.setValue('', options);
1137 }
1138
1139 });
1140
1141 // All the value controllers
1142 socss.view.properties.controllers = {};
1143
1144 // The color controller
1145 socss.view.properties.controllers.color = socss.view.propertyController.extend({
1146
1147 template: _.template('<input type="text" value="" />'),
1148
1149 render: function () {
1150 var thisView = this;
1151
1152 this.$el.append($(this.template({})));
1153
1154 // Set this up as a color picker
1155 this.field = this.$el.find('input');
1156 this.field.minicolors({});
1157
1158 },
1159
1160 initChangeEvents: function () {
1161 var thisView = this;
1162 this.field.on('change keyup', function () {
1163 thisView.trigger('change', thisView.field.minicolors('value'));
1164 });
1165 },
1166
1167 getValue: function () {
1168 return this.field.minicolors('value').trim();
1169 },
1170
1171 setValue: function (val, options) {
1172 options = _.extend({silent: false}, options);
1173
1174 this.field.minicolors('value', val);
1175
1176 if (!options.silent) {
1177 this.trigger('set_value', val);
1178 }
1179 }
1180
1181 });
1182
1183 // The dropdown select box controller
1184 socss.view.properties.controllers.select = socss.view.propertyController.extend( {
1185 template: _.template('<select></select>'),
1186
1187 render: function(){
1188 var thisView = this;
1189
1190 this.$el.append($(this.template({})));
1191 this.field = this.$el.find('select');
1192
1193 // Add the unchanged option
1194 this.field.append( $('<option value=""></option>').html('') );
1195
1196 // Add all the options to the dropdown
1197 for( var k in this.args.options ) {
1198 this.field.append( $('<option></option>').attr('value', k).html( this.args.options[k] ) );
1199 }
1200
1201 if( typeof this.args.option_icons !== 'undefined' ) {
1202 this.setupVisualSelect();
1203 }
1204 },
1205
1206 setupVisualSelect: function(){
1207 var thisView = this;
1208 this.field.hide();
1209
1210 var $tc = $('<div class="select-tabs"></div>').appendTo( this.$el );
1211
1212 // Add the none value
1213 $('<div class="select-tab" data-value=""><span class="fa fa-circle-o"></span></div>').appendTo($tc);
1214
1215 // Now add one for each of the option icons
1216 for ( var k in this.args.option_icons ) {
1217 $('<div class="select-tab"></div>')
1218 .appendTo($tc)
1219 .append(
1220 $('<span class="fa"></span>')
1221 .addClass('fa-' + this.args.option_icons[k])
1222 )
1223 .attr('data-value', k)
1224 ;
1225 }
1226
1227 $tc.find('.select-tab')
1228 .css('width', 100/( $tc.find('>div').length ) + "%" )
1229 .click( function(){
1230 var $t = $(this);
1231 $tc.find('.select-tab').removeClass('active');
1232 $t.addClass('active');
1233 thisView.field.val( $t.data('value')).change();
1234 } );
1235 },
1236
1237 /**
1238 * Set the current value
1239 * @param socss.view.properties val
1240 */
1241 setValue: function (val, options) {
1242 options = _.extend({silent: false}, options);
1243
1244 this.field.val(val);
1245
1246 this.$('.select-tabs .select-tab').removeClass('active').filter('[data-value="' + val + '"]').addClass('active');
1247
1248 if (!options.silent) {
1249 this.trigger('set_value', val);
1250 }
1251 }
1252
1253 } );
1254
1255 // A field that lets a user upload an image
1256 socss.view.properties.controllers.image = socss.view.propertyController.extend( {
1257 template: _.template('<input type="text" value="" /> <span class="select socss-button"><span class="fa fa-upload"></span></span>'),
1258
1259 render: function(){
1260 var thisView = this;
1261
1262 this.media = wp.media({
1263 // Set the title of the modal.
1264 title: socssOptions.loc.select_image,
1265
1266 // Tell the modal to show only images.
1267 library: {
1268 type: 'image'
1269 },
1270
1271 // Customize the submit button.
1272 button: {
1273 // Set the text of the button.
1274 text: socssOptions.loc.select,
1275 // Tell the button not to close the modal, since we're
1276 // going to refresh the page when the image is selected.
1277 close: false
1278 }
1279 });
1280
1281 this.$el.append( $(this.template({
1282 select: socssOptions.loc.select
1283 })) );
1284
1285 this.field = this.$el.find('input');
1286
1287 this.$('.select').click(function(){
1288 thisView.media.open();
1289 });
1290
1291 this.media.on('select', function(){
1292 // Grab the selected attachment.
1293 var attachment = this.state().get('selection').first().attributes;
1294 var val = thisView.args.value.replace('{{url}}', attachment.url);
1295
1296 // Change the field value and trigger a change event
1297 thisView.field.val( val ).change();
1298
1299 // Close the image selector
1300 thisView.media.close();
1301
1302 }, this.media);
1303 }
1304
1305 } );
1306
1307 // A simple measurement field
1308 socss.view.properties.controllers.measurement = socss.view.propertyController.extend( {
1309
1310 wrapperClass: 'socss-field-measurement',
1311
1312 render: function(){
1313 this.$el.append($(this.template({})));
1314 this.field = this.$('input');
1315 this.setupMeasurementField( this.field, {} );
1316 },
1317
1318 setValue: function (val, options) {
1319 options = _.extend({silent: false}, options);
1320 this.field.val(val).trigger('measurement_refresh');
1321 if (!options.silent) {
1322 this.trigger('set_value', val);
1323 }
1324 },
1325
1326 units : [
1327 'px',
1328 '%',
1329 'em',
1330 'cm',
1331 'mm',
1332 'in',
1333 'pt',
1334 'pc',
1335 'ex',
1336 'ch',
1337 'rem',
1338 'vw',
1339 'vh',
1340 'vmin',
1341 'vmax'
1342 ],
1343
1344 parseUnits: function( value ){
1345 var escapeRegExp = function(str) {
1346 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
1347 };
1348
1349 var regexUnits = this.units.map(escapeRegExp);
1350 var regex = new RegExp('([0-9\\.\\-]+)(' + regexUnits.join('|') + ')?', 'i');
1351 var result = regex.exec( value );
1352
1353 if( result === null ) {
1354 return {
1355 value: '',
1356 unit: ''
1357 };
1358 }
1359 else {
1360 return {
1361 value: result[1],
1362 unit: result[2] === undefined ? '' : result[2]
1363 };
1364 }
1365 },
1366
1367 setupMeasurementField: function( $el, options ){
1368 var thisView = this;
1369 var $p = $el.parent();
1370
1371 options = _.extend( {
1372 defaultUnit: 'px'
1373 }, options );
1374
1375 $el.hide();
1376 $p.addClass( this.wrapperClass ).data('unit', options.defaultUnit);
1377
1378 // Create the fake input field
1379 var $fi = $('<input type="text" class="socss-field-input"/>').appendTo($p);
1380 var $da = $('<span class="dashicons dashicons-arrow-down"></span>').appendTo($p);
1381 var $dd = $('<ul class="dropdown"></ul>').appendTo($p);
1382 var $u = $('<span class="units"></span>').html( options.defaultUnit ).appendTo( $p );
1383
1384 for( var i = 0; i < thisView.units.length; i++ ) {
1385 var $o = $('<li></li>').html( thisView.units[i] ).data('unit', thisView.units[i]);
1386 if( thisView.units[i] === options.defaultUnit ) {
1387 $o.addClass('active');
1388 }
1389 $dd.append( $o );
1390 }
1391
1392 var updateValue = function(){
1393 var value = thisView.parseUnits( $fi.val() );
1394
1395 if( value.unit !== '' && value.unit !== $p.data( 'unit' ) ) {
1396 $fi.val( value.value );
1397 setUnit( value.unit );
1398 }
1399
1400 if( value.value === '' ) {
1401 $el.val( '' );
1402 }
1403 else {
1404 $el.val( value.value + $p.data( 'unit' ) );
1405 }
1406 };
1407
1408 var setUnit = function( unit ){
1409 $u.html( unit );
1410 $p.data( 'unit', unit );
1411 $fi.trigger('keydown');
1412 };
1413
1414 $da.click( function(){
1415 $dd.toggle();
1416 } );
1417
1418 $dd.find('li').click( function(){
1419 $dd.toggle();
1420 setUnit( $(this).data('unit') );
1421 updateValue();
1422 $el.trigger('change');
1423 } );
1424
1425 $fi.on( 'keyup keydown', function(e){
1426 var $$ = $(this);
1427
1428 var char = '';
1429 if( e.type === 'keydown' ) {
1430 if(e.keyCode >= 48 && e.keyCode <= 57 ) {
1431 char = String.fromCharCode(e.keyCode);
1432 }
1433 else if( e.keyCode === 189 ) {
1434 char = '-';
1435 }
1436 else if( e.keyCode === 190 ) {
1437 char = '.';
1438 }
1439 }
1440
1441 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1442 .css( {
1443 'font-size' : '14px'
1444 } )
1445 .html( $fi.val() + char )
1446 .appendTo( 'body' );
1447 var width = $pl.width();
1448 width = Math.min(width, 63);
1449 $pl.remove();
1450
1451 $u.css('left', width + 12);
1452 } );
1453
1454 $fi.on('keyup', function(e){
1455 updateValue();
1456 $el.trigger('change');
1457 } );
1458
1459 $el.on('measurement_refresh', function(){
1460 var value = thisView.parseUnits( $el.val() );
1461 $fi.val( value.value );
1462
1463 var unit = value.unit === '' ? options.defaultUnit : value.unit;
1464 $p.data( 'unit', unit );
1465 $u.html( unit );
1466
1467 var $pl = $('<span class="socss-hidden-placeholder"></span>')
1468 .css({
1469 'font-size' : '14px'
1470 })
1471 .html( value.value )
1472 .appendTo( 'body' );
1473 var width = $pl.width();
1474 width = Math.min(width, 63);
1475 $pl.remove();
1476
1477 $u.css('left', width + 12);
1478 } );
1479
1480 // Now add the increment/decrement buttons
1481 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1482 var $dec = $('<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>').appendTo($diw);
1483 var $inc = $('<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>').appendTo($diw);
1484
1485 // Increment is clicked
1486 $inc.click( function(){
1487 var value = thisView.parseUnits( $el.val() );
1488 if( value.value === '' ) {
1489 return true;
1490 }
1491
1492 var newVal = Math.ceil( value.value * 1.05 );
1493
1494 $fi.val( newVal );
1495 updateValue();
1496 $el.trigger('change').trigger('measurement_refresh');
1497 } );
1498
1499 $dec.click( function(){
1500 var value = thisView.parseUnits( $el.val() );
1501 if( value.value === '' ) {
1502 return true;
1503 }
1504
1505 var newVal = Math.floor( value.value / 1.05 );
1506
1507 $fi.val( newVal );
1508 updateValue();
1509 $el.trigger('change').trigger('measurement_refresh');
1510 } );
1511 }
1512
1513 } );
1514
1515 // A simple measurement field
1516 socss.view.properties.controllers.number = socss.view.propertyController.extend( {
1517
1518 render: function(){
1519 this.$el.append($(this.template({})));
1520 this.field = this.$('input');
1521
1522 // Setup the measurement field
1523 this.setupNumberField(this.field, this.args);
1524 },
1525
1526 /**
1527 * Setup the number field
1528 * @param el
1529 * @param options
1530 */
1531 setupNumberField: function($el, options){
1532 options = _.extend({
1533 change: null,
1534 default: 0,
1535 increment: 1,
1536 decrement: -1,
1537 max: null,
1538 min: null
1539 }, options);
1540
1541 var $p = $el.parent();
1542 $p.addClass('socss-field-number');
1543
1544 // Now add the increment/decrement buttons
1545 var $diw = $('<div class="socss-diw"></div>').appendTo($p);
1546 var $dec = $('<div class="dec-button socss-button">-</div>').appendTo($diw);
1547 var $inc = $('<div class="inc-button socss-button">+</div>').appendTo($diw);
1548
1549 // Increment is clicked
1550 $diw.find('> div').click( function(e){
1551 e.preventDefault();
1552
1553 var val = options.default;
1554 if( $el.val() !== '' ) {
1555 val = Number($el.val());
1556 }
1557 val = val + ( $(this).is( $dec ) ? options.decrement : options.increment );
1558
1559 val = Math.round(val*100)/100;
1560
1561 if( options.max !== null ) {
1562 val = Math.min( options.max, val);
1563 }
1564
1565 if( options.min !== null ) {
1566 val = Math.max( options.min, val);
1567 }
1568
1569 $el.val( val );
1570 $el.trigger('change');
1571 } );
1572
1573 return this;
1574 }
1575
1576 } );
1577
1578
1579 socss.view.properties.controllers.sides = socss.view.propertyController.extend( {
1580
1581 template: _.template( $('#template-sides-field').html().trim() ),
1582
1583 controllers: [],
1584
1585 render: function(){
1586 var thisView = this;
1587
1588 this.$el.append( $(this.template({})) );
1589 this.field = this.$el.find('input');
1590
1591 if( !thisView.args.hasAll ) {
1592 this.$('.select-tab').eq(0).remove();
1593 this.$('.select-tab').css('width', '25%');
1594 }
1595
1596 this.$('.select-tab').each( function(){
1597 var dir = $(this).data('direction');
1598
1599 var container = $('<li class="side">')
1600 .appendTo( thisView.$('.sides') )
1601 .hide();
1602
1603 for( var i = 0; i < thisView.args.controllers.length; i++ ) {
1604
1605 var controllerArgs = thisView.args.controllers[i];
1606
1607 if( typeof socss.view.properties.controllers[ controllerArgs.type ] ) {
1608
1609 // Create the measurement view
1610 var property = '';
1611 if( dir === 'all' ) {
1612 property = controllerArgs.args.propertyAll;
1613 }
1614 else {
1615 property = controllerArgs.args.property.replace('{dir}', dir);
1616 }
1617
1618 var theseControllerArgs = _.extend({}, controllerArgs.args, {property: property});
1619
1620 var controller = new socss.view.properties.controllers[ controllerArgs.type ]( {
1621 el: $('<div>').appendTo( container ),
1622 propertiesView: thisView.propertiesView,
1623 args: theseControllerArgs
1624 } );
1625
1626 // Setup and render the measurement controller and register it with the properties view
1627 controller.render();
1628 controller.initChangeEvents();
1629 thisView.propertiesView.propertyControllers.push(controller);
1630
1631 }
1632
1633 }
1634
1635 $(this).on( 'click', function(){
1636 thisView.$('.select-tab').removeClass('active');
1637 $(this).addClass('active');
1638
1639 thisView.$('.sides .side').hide();
1640 container.show();
1641 } );
1642
1643 } );
1644
1645 // Select the first tab by default
1646 this.$('.select-tab').eq(0).click();
1647 }
1648
1649 } );
1650
1651 })(jQuery, _, socssOptions);
1652
1653 // Setup the main editor
1654 jQuery(function ($) {
1655 var socss = window.socss;
1656
1657 // Setup the editor
1658 var editor = new socss.view.editor({
1659 el: $('#so-custom-css-form').get(0)
1660 });
1661 editor.render();
1662 editor.setSnippets(socssOptions.snippets);
1663
1664 window.socss.mainEditor = editor;
1665
1666 // This is for hiding the getting started video
1667 $('#so-custom-css-getting-started a.hide').click( function(e){
1668 e.preventDefault();
1669 $('#so-custom-css-getting-started').slideUp();
1670 $.get( $(this).attr('href') );
1671 } );
1672 });
1673