|
1 /* vim:set ts=2 sw=2 sts=2 et tw=80: |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 "use strict"; |
|
6 |
|
7 const {Cu} = require("chrome"); |
|
8 const Editor = require("devtools/sourceeditor/editor"); |
|
9 Cu.import("resource://gre/modules/Services.jsm"); |
|
10 Cu.import("resource://gre/modules/devtools/event-emitter.js"); |
|
11 |
|
12 exports.HTMLEditor = HTMLEditor; |
|
13 |
|
14 function ctrl(k) { |
|
15 return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k; |
|
16 } |
|
17 function stopPropagation(e) { |
|
18 e.stopPropagation(); |
|
19 } |
|
20 /** |
|
21 * A wrapper around the Editor component, that allows editing of HTML. |
|
22 * |
|
23 * The main functionality this provides around the Editor is the ability |
|
24 * to show/hide/position an editor inplace. It only appends once to the |
|
25 * body, and uses CSS to position the editor. The reason it is done this |
|
26 * way is that the editor is loaded in an iframe, and calling appendChild |
|
27 * causes it to reload. |
|
28 * |
|
29 * Meant to be embedded inside of an HTML page, as in markup-view.xhtml. |
|
30 * |
|
31 * @param HTMLDocument htmlDocument |
|
32 * The document to attach the editor to. Will also use this |
|
33 * document as a basis for listening resize events. |
|
34 */ |
|
35 function HTMLEditor(htmlDocument) |
|
36 { |
|
37 this.doc = htmlDocument; |
|
38 this.container = this.doc.createElement("div"); |
|
39 this.container.className = "html-editor theme-body"; |
|
40 this.container.style.display = "none"; |
|
41 this.editorInner = this.doc.createElement("div"); |
|
42 this.editorInner.className = "html-editor-inner"; |
|
43 this.container.appendChild(this.editorInner); |
|
44 |
|
45 this.doc.body.appendChild(this.container); |
|
46 this.hide = this.hide.bind(this); |
|
47 this.refresh = this.refresh.bind(this); |
|
48 |
|
49 EventEmitter.decorate(this); |
|
50 |
|
51 this.doc.defaultView.addEventListener("resize", |
|
52 this.refresh, true); |
|
53 |
|
54 let config = { |
|
55 mode: Editor.modes.html, |
|
56 lineWrapping: true, |
|
57 styleActiveLine: false, |
|
58 extraKeys: {}, |
|
59 theme: "mozilla markup-view" |
|
60 }; |
|
61 |
|
62 config.extraKeys[ctrl("Enter")] = this.hide; |
|
63 config.extraKeys["F2"] = this.hide; |
|
64 config.extraKeys["Esc"] = this.hide.bind(this, false); |
|
65 |
|
66 this.container.addEventListener("click", this.hide, false); |
|
67 this.editorInner.addEventListener("click", stopPropagation, false); |
|
68 this.editor = new Editor(config); |
|
69 |
|
70 let iframe = this.editorInner.ownerDocument.createElement("iframe"); |
|
71 this.editor.appendTo(this.editorInner, iframe).then(() => { |
|
72 this.hide(false); |
|
73 }).then(null, (err) => console.log(err.message)); |
|
74 } |
|
75 |
|
76 HTMLEditor.prototype = { |
|
77 |
|
78 /** |
|
79 * Need to refresh position by manually setting CSS values, so this will |
|
80 * need to be called on resizes and other sizing changes. |
|
81 */ |
|
82 refresh: function() { |
|
83 let element = this._attachedElement; |
|
84 |
|
85 if (element) { |
|
86 this.container.style.top = element.offsetTop + "px"; |
|
87 this.container.style.left = element.offsetLeft + "px"; |
|
88 this.container.style.width = element.offsetWidth + "px"; |
|
89 this.container.style.height = element.parentNode.offsetHeight + "px"; |
|
90 this.editor.refresh(); |
|
91 } |
|
92 }, |
|
93 |
|
94 /** |
|
95 * Anchor the editor to a particular element. |
|
96 * |
|
97 * @param DOMNode element |
|
98 * The element that the editor will be anchored to. |
|
99 * Should belong to the HTMLDocument passed into the constructor. |
|
100 */ |
|
101 _attach: function(element) |
|
102 { |
|
103 this._detach(); |
|
104 this._attachedElement = element; |
|
105 element.classList.add("html-editor-container"); |
|
106 this.refresh(); |
|
107 }, |
|
108 |
|
109 /** |
|
110 * Unanchor the editor from an element. |
|
111 */ |
|
112 _detach: function() |
|
113 { |
|
114 if (this._attachedElement) { |
|
115 this._attachedElement.classList.remove("html-editor-container"); |
|
116 this._attachedElement = undefined; |
|
117 } |
|
118 }, |
|
119 |
|
120 /** |
|
121 * Anchor the editor to a particular element, and show the editor. |
|
122 * |
|
123 * @param DOMNode element |
|
124 * The element that the editor will be anchored to. |
|
125 * Should belong to the HTMLDocument passed into the constructor. |
|
126 * @param string text |
|
127 * Value to set the contents of the editor to |
|
128 * @param function cb |
|
129 * The function to call when hiding |
|
130 */ |
|
131 show: function(element, text) |
|
132 { |
|
133 if (this._visible) { |
|
134 return; |
|
135 } |
|
136 |
|
137 this._originalValue = text; |
|
138 this.editor.setText(text); |
|
139 this._attach(element); |
|
140 this.container.style.display = "flex"; |
|
141 this._visible = true; |
|
142 |
|
143 this.editor.refresh(); |
|
144 this.editor.focus(); |
|
145 |
|
146 this.emit("popupshown"); |
|
147 }, |
|
148 |
|
149 /** |
|
150 * Hide the editor, optionally committing the changes |
|
151 * |
|
152 * @param bool shouldCommit |
|
153 * A change will be committed by default. If this param |
|
154 * strictly equals false, no change will occur. |
|
155 */ |
|
156 hide: function(shouldCommit) |
|
157 { |
|
158 if (!this._visible) { |
|
159 return; |
|
160 } |
|
161 |
|
162 this.container.style.display = "none"; |
|
163 this._detach(); |
|
164 |
|
165 let newValue = this.editor.getText(); |
|
166 let valueHasChanged = this._originalValue !== newValue; |
|
167 let preventCommit = shouldCommit === false || !valueHasChanged; |
|
168 this._originalValue = undefined; |
|
169 this._visible = undefined; |
|
170 this.emit("popuphidden", !preventCommit, newValue); |
|
171 }, |
|
172 |
|
173 /** |
|
174 * Destroy this object and unbind all event handlers |
|
175 */ |
|
176 destroy: function() |
|
177 { |
|
178 this.doc.defaultView.removeEventListener("resize", |
|
179 this.refresh, true); |
|
180 this.container.removeEventListener("click", this.hide, false); |
|
181 this.editorInner.removeEventListener("click", stopPropagation, false); |
|
182 |
|
183 this.hide(false); |
|
184 this.container.parentNode.removeChild(this.container); |
|
185 } |
|
186 }; |