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