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