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