forked from textAngular/textAngular
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtextAngular.js
417 lines (396 loc) · 19.6 KB
/
textAngular.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
// Generated by CoffeeScript 1.6.3
/*
textAngular
Author : Austin Anderson
License : 2013 MIT
Version 1.1.0
Requirements: Angular 1.2.0, Angular ngSanitize module
Optional Requirements: Bootstrap 3.0.0 and font-awesome for styling if you are using the default classes and icons.
How to Use:
1. Include textAngular.js in your project, alternatively grab all this code and throw it in your directives.js module file.
2. In your HTML instantiate textAngular as an attribute or element, the only required attribute is the ng-model which is the variable to bind the content of the editor to, this is the standard ng-model allowing you to use validating filters on it.
3. I reccommend using the following CSS in your stylesheet or a variant of to display the text box nicely:
.ta-editor{
min-height: 300px;
height: auto;
overflow: auto;
font-family: inherit;
font-size: 100%;
}
4. Have fun!
Setting Options:
Several options can be set through attributes on the HTML tag, these are;
- ta-toolbar: this should evaluate to an array of arrays. Each element is the name of one of the toolbar tools. The default is: [['h1', 'h2', 'h3', 'p', 'pre', 'bold', 'italics', 'ul', 'ol', 'redo', 'undo', 'clear'],['html', 'insertImage', 'insertLink']]
- ta-toolbar-class: this is the class to apply to the overall div of the toolbar, defaults to "btn-toolbar". Note that the class "ta-toolbar" is also added to the toolbar.
- ta-toolbar-group-class: this is the class to apply to the nested groups in the toolbar, a div with this class is created for each nested array in the ta-toolbar array and then the tool buttons are nested inside the group, defaults to "btn-group".
- ta-toolbar-button-class: this is the class to apply to each tool button in the toolbar, defaults to: "btn btn-default"
- ta-toolbar-active-button-class: this is the class to apply to each tool button in the toolbar if it's activeState function returns true ie when a tool function is applied to the selected text, defaults to: "active".
- ta-text-editor-class: this is the class to apply to the text editor <pre>, defaults to "form-control". Note that the classes: ta-editor and ta-text are also added.
- ta-html-editor-class: this is the class to apply to the html editor <div>, defaults to "form-control". Note that the classes: ta-editor and ta-html are also added.
The defaults can be changed by altering/overwriting the variable: $rootScope.textAngularOpts which acts like global defaults for the classes and toolbar.
The default value for this is:
$rootScope.textAngularOpts = {
toolbar: [['h1', 'h2', 'h3', 'p', 'pre', 'bold', 'italics', 'ul', 'ol', 'redo', 'undo', 'clear'],['html', 'insertImage', 'insertLink']],
classes: {
toolbar: "btn-toolbar",
toolbarGroup: "btn-group",
toolbarButton: "btn btn-default",
toolbarButtonActive: "active",
textEditor: 'form-control',
htmlEditor: 'form-control'
}
}
The toolbar buttons are defined in the object variable $rootScope.textAngularTools.
The following is an example of how to add a button to make the selected text red:
`
$rootScope.textAngularTools.colourRed = {
display: "<button ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-square' style='color: red;'></i></button>",
action: function(){
this.$parent.wrapSelection('formatBlock', '<span style="color: red">');
},
activeState: function(){return false;} //This isn't required, and currently doesn't work reliably except for the html tag that doesn't rely on the cursor position.
};
//the following adds it to the toolbar to be displayed and used.
$rootScope.textAngularOpts.toolbar = [['h1', 'h2', 'h3', 'p', 'pre', 'bold', 'colourRed', 'italics', 'ul', 'ol', 'redo', 'undo', 'clear'],['html', 'insertImage', 'insertLink']];
`
To explain how this works, when we create a button we create an isolated child scope of the textAngular scope and extend it with the values in the tools object, we then compile the HTML in the display value with the newly created scope.
Note that the way any functions are called in the plugins the 'this' variable will allways point to the scope of the button ensuring that this.$parent will allways
Here's the code we run for every tool:
`
toolElement = angular.element($rootScope.textAngularTools[tool].display);
toolElement.addClass(scope.classes.toolbarButton);
groupElement.append($compile(toolElement)(angular.extend scope.$new(true), $rootScope.textAngularTools[tool]));
`
*/
var textAngular = angular.module("textAngular", ['ngSanitize']); //This makes ngSanitize required
textAngular.directive("textAngular", function($compile, $sce, $window, $document, $rootScope, $timeout) {
console.log("Thank you for using textAngular! http://www.textangular.com");
// Here we set up the global display defaults, make sure we don't overwrite any that the user may have already set.
$rootScope.textAngularOpts = angular.extend({
toolbar: [['h1', 'h2', 'h3', 'p', 'pre', 'bold', 'italics', 'ul', 'ol', 'redo', 'undo', 'clear'], ['html', 'insertImage', 'insertLink']],
classes: {
toolbar: "btn-toolbar",
toolbarGroup: "btn-group",
toolbarButton: "btn btn-default",
toolbarButtonActive: "active",
textEditor: 'form-control',
htmlEditor: 'form-control'
}
}, ($rootScope.textAngularOpts != null)? $rootScope.textAngularOpts : {});
// deepExtend instead of angular.extend in order to allow easy customization of "display" for default buttons
// snatched from: http://stackoverflow.com/a/15311794/2966847
function deepExtend(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
}
// Setup the default toolbar tools, this way allows the user to add new tools like plugins
$rootScope.textAngularTools = deepExtend({
html: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'>Toggle HTML</button>",
action: function() {
// this variable in an action function referrs to the angular scope of the tool
var ht, _this = this;
this.$parent.showHtml = !this.$parent.showHtml;
if (this.$parent.showHtml) { //Show the HTML view
ht = this.$parent.displayElements.text.html();
$timeout((function() { //defer until the element is visible
return _this.$parent.displayElements.html[0].focus(); //dereference the DOM object from the angular.element
}), 100);
} else { //Show the WYSIWYG view
ht = this.$parent.displayElements.html.html();
$timeout((function() { //defer until the element is visible
return _this.$parent.displayElements.text[0].focus(); //dereference the DOM object from the angular.element
}), 100);
}
// don't need to wrap in $apply as this is called within the ng-click which is in an $apply anyway
this.$parent.compileHtml(ht);
this.active = this.$parent.showHtml;
}
},
h1: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'>H1</button>",
action: function() {
return this.$parent.wrapSelection("formatBlock", "<H1>");
}
},
h2: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'>H2</button>",
action: function() {
return this.$parent.wrapSelection("formatBlock", "<H2>");
}
},
h3: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'>H3</button>",
action: function() {
return this.$parent.wrapSelection("formatBlock", "<H3>");
}
},
p: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'>P</button>",
action: function() {
return this.$parent.wrapSelection("formatBlock", "<P>");
}
},
pre: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'>pre</button>",
action: function() {
return this.$parent.wrapSelection("formatBlock", "<PRE>");
}
},
ul: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-list-ul'></i></button>",
action: function() {
return this.$parent.wrapSelection("insertUnorderedList", null);
}
},
ol: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-list-ol'></i></button>",
action: function() {
return this.$parent.wrapSelection("insertOrderedList", null);
}
},
quote: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-quote-right'></i></button>",
action: function() {
return this.$parent.wrapSelection("formatBlock", "<BLOCKQUOTE>");
}
},
undo: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-undo'></i></button>",
action: function() {
return this.$parent.wrapSelection("undo", null);
}
},
redo: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-repeat'></i></button>",
action: function() {
return this.$parent.wrapSelection("redo", null);
}
},
bold: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-bold'></i></button>",
action: function() {
return this.$parent.wrapSelection("bold", null);
},
activeState: function() {
return $document[0].queryCommandState('bold');
}
},
justifyLeft: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-align-left'></i></button>",
action: function() {
return this.$parent.wrapSelection("justifyLeft", null);
},
activeState: function() {
return $document[0].queryCommandState('justifyLeft');
}
},
justifyRight: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-align-right'></i></button>",
action: function() {
return this.$parent.wrapSelection("justifyRight", null);
},
activeState: function() {
return $document[0].queryCommandState('justifyRight');
}
},
justifyCenter: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-align-center'></i></button>",
action: function() {
return this.$parent.wrapSelection("justifyCenter", null);
},
activeState: function() {
return $document[0].queryCommandState('justifyCenter');
}
},
italics: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-italic'></i></button>",
action: function() {
return this.$parent.wrapSelection("italic", null);
},
activeState: function() {
return $document[0].queryCommandState('italic');
}
},
clear: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-ban'></i></button>",
action: function() {
return this.$parent.wrapSelection("FormatBlock", "<div>");
}
},
insertImage: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-picture-o'></i></button>",
action: function() {
var imageLink;
imageLink = prompt("Please enter an image URL to insert", 'http://');
if (imageLink !== '') {
return this.$parent.wrapSelection('insertImage', imageLink);
}
}
},
insertLink: {
display: "<button type='button' ng-click='action()' ng-class='displayActiveToolClass(active)'><i class='fa fa-chain'></i></button>",
action: function() {
var urlLink;
urlLink = prompt("Please enter an URL to insert", 'http://');
if (urlLink !== '') {
return this.$parent.wrapSelection('createLink', urlLink);
}
}
}
}, ($rootScope.textAngularTools != null)? $rootScope.textAngularTools : {});
// ngSanitize is a requirement for the module so this shouldn't cause any trouble
var sanitizationWrapper = function(html) {
return $sce.trustAsHtml(html);
};
return {
require: 'ngModel',
scope: {},
restrict: "EA",
link: function(scope, element, attrs, ngModel) {
var group, groupElement, keydown, keyup, tool, toolElement; //all these vars should not be accessable outside this directive
// get the settings from the defaults and add our specific functions that need to be on the scope
angular.extend(scope, $rootScope.textAngularOpts, {
// This must be called within a $apply or the ngModel value will not be updated correctly
compileHtml: function(html) {
// this refers to the scope
var compHtml = angular.element("<div>").append(html).html().replace(/(class="(.*?)")|(class='(.*?)')/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/style=("|')(.*?)("|')/g, "");
if (scope.showHtml === "load") {
scope.text = sanitizationWrapper(compHtml);
scope.html = sanitizationWrapper(compHtml.replace(/</g, "<"));
scope.showHtml = scope.showHtmlDefault || false;
} else if (scope.showHtml) { // update the raw HTML view
scope.text = sanitizationWrapper(compHtml);
} else { // update the WYSIWYG view
scope.html = sanitizationWrapper(compHtml.replace(/</g, "<"));
}
ngModel.$setViewValue(compHtml);
},
// wraps the selection in the provided tag / execCommand function.
wrapSelection: function(command, opt, updateDisplay) {
// the default value for updateDisplay is true
if (updateDisplay == null) updateDisplay = true;
document.execCommand(command, false, opt);
// refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box. You still have focus on the text/html input it just doesn't show up
if (scope.showHtml)
scope.displayElements.text[0].focus();
else
scope.displayElements.html[0].focus();
// note that wrapSelection is called via ng-click in the tool plugins so we are already within a $apply
if (updateDisplay) scope.updateDisplay();
},
// due to restritions on compileHtml this must also be called in a scope.$apply - this is really a convenience function for compileHtml anyway.
updateDisplay: function() {
scope.compileHtml((!scope.showHtml)? scope.displayElements.html.html() : scope.displayElements.text.html());
},
showHtml: false
});
// setup the options from the optional attributes
if (!!attrs.taToolbar) scope.toolbar = scope.$eval(attrs.taToolbar);
if (!!attrs.taToolbarClass) scope.classes.toolbar = attrs.taToolbarClass;
if (!!attrs.taToolbarGroupClass) scope.classes.toolbarGroup = attrs.taToolbarGroupClass;
if (!!attrs.taToolbarButtonClass) scope.classes.toolbarButton = attrs.taToolbarButtonClass;
if (!!attrs.taToolbarActiveButtonClass) scope.classes.toolbarButtonActive = attrs.taToolbarActiveButtonClass;
if (!!attrs.taTextEditorClass) scope.classes.textEditor = attrs.taTextEditorClass;
if (!!attrs.taHtmlEditorClass) scope.classes.htmlEditor = attrs.taHtmlEditorClass;
// Setup the HTML elements as variable references for use later
scope.displayElements = {
toolbar: angular.element("<div></div>"),
text: angular.element("<pre contentEditable='true' ng-show='showHtml' ng-bind-html='html' ></pre>"),
html: angular.element("<div contentEditable='true' ng-hide='showHtml' ng-bind-html='text' ></div>")
};
// add the main elements to the origional element
element.append(scope.displayElements.toolbar);
element.append(scope.displayElements.text);
element.append(scope.displayElements.html);
// compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
$compile(scope.displayElements.text)(scope);
$compile(scope.displayElements.html)(scope);
// add the classes manually last
element.addClass("ta-root");
scope.displayElements.toolbar.addClass("ta-toolbar " + scope.classes.toolbar);
scope.displayElements.text.addClass("ta-text ta-editor " + scope.classes.textEditor);
scope.displayElements.html.addClass("ta-html ta-editor " + scope.classes.textEditor);
scope.tools = {}; // Keep a reference for updating the active states later
// create the tools in the toolbar
for (var _i = 0; _i < scope.toolbar.length; _i++) {
// setup the toolbar group
group = scope.toolbar[_i];
groupElement = angular.element("<div></div>");
groupElement.addClass(scope.classes.toolbarGroup);
for (var _j = 0; _j < group.length; _j++) {
// init and add the tools to the group
tool = group[_j]; // a tool name (key name from textAngularTools struct)
toolElement = angular.element($rootScope.textAngularTools[tool].display);
toolElement.addClass(scope.classes.toolbarButton);
toolElement.attr('unselectable', 'on'); // important to not take focus from the main text/html entry
var childScope = angular.extend(scope.$new(true), $rootScope.textAngularTools[tool], { // add the tool specific functions
displayActiveToolClass: function(active){
return (active)? this.$parent.classes.toolbarButtonActive : '';
}
}); //creates a child scope of the main angularText scope and then extends the childScope with the functions of this particular tool
scope.tools[tool] = childScope; // reference to the scope kept
groupElement.append($compile(toolElement)(childScope)); // append the tool compiled with the childScope to the group element
}
scope.displayElements.toolbar.append(groupElement); // append the group to the toolbar
}
// changes to the model variable from outside the html/text inputs
ngModel.$render = function() {
if(ngModel.$viewValue === undefined) return;
// if the editors aren't focused they need to be updated, otherwise they are doing the updating
if (!($document[0].activeElement === scope.displayElements.html[0]) && !($document[0].activeElement === scope.displayElements.text[0])) {
var val = ngModel.$viewValue || ''; // in case model is null
scope.text = sanitizationWrapper(val);
scope.html = sanitizationWrapper(val.replace(/</g, "<"));
}
};
// the following is for applying the active states to the tools that support it
scope.bUpdateSelectedStyles = false;
// loop through all the tools polling their activeState function if it exists
scope.updateSelectedStyles = function() {
for (var _k = 0; _k < scope.toolbar.length; _k++) {
var groups = scope.toolbar[_k];
for (var _l = 0; _l < groups.length; _l++) {
tool = groups[_l];
if (scope.tools[tool].activeState != null) {
scope.tools[tool].active = scope.tools[tool].activeState.apply(scope);
}
}
}
if (this.bUpdateSelectedStyles) $timeout(this.updateSelectedStyles, 200); // used to update the active state when a key is held down, ie the left arrow
};
// start updating on keydown
keydown = function(e) {
scope.bUpdateSelectedStyles = true;
scope.$apply(function() {
scope.updateSelectedStyles();
});
};
scope.displayElements.html.on('keydown', keydown);
scope.displayElements.text.on('keydown', keydown);
// stop updating on key up and update the display/model
keyup = function(e) {
scope.bUpdateSelectedStyles = false;
scope.$apply(scope.updateDisplay);
};
scope.displayElements.html.on('keyup', keyup);
scope.displayElements.text.on('keyup', keyup);
// update the toolbar active states when we click somewhere in the text/html boxed
mouseup = function(e) {
scope.$apply(function() {
scope.updateSelectedStyles();
});
};
scope.displayElements.html.on('mouseup', mouseup);
scope.displayElements.text.on('mouseup', mouseup);
}
};
});