diff options
Diffstat (limited to 'comm/mail/components/compose/content/dialogs')
43 files changed, 13266 insertions, 0 deletions
diff --git a/comm/mail/components/compose/content/dialogs/EdAEAttributes.js b/comm/mail/components/compose/content/dialogs/EdAEAttributes.js new file mode 100644 index 0000000000..52b7e30fac --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdAEAttributes.js @@ -0,0 +1,973 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// HTML Attributes object for "Name" menulist +var gHTMLAttr = {}; + +// JS Events Attributes object for "Name" menulist +var gJSAttr = {}; + +// Core HTML attribute values // +// This is appended to Name menulist when "_core" is attribute name +var gCoreHTMLAttr = ["^id", "class", "title"]; + +// Core event attribute values // +// This is appended to all JS menulists +// except those elements having "noJSEvents" +// as a value in their gJSAttr array. +var gCoreJSEvents = [ + "onclick", + "ondblclick", + "onmousedown", + "onmouseup", + "onmouseover", + "onmousemove", + "onmouseout", + "-", + "onkeypress", + "onkeydown", + "onkeyup", +]; + +// Following are commonly-used strings + +// Also accept: sRGB: #RRGGBB // +var gHTMLColors = [ + "Aqua", + "Black", + "Blue", + "Fuchsia", + "Gray", + "Green", + "Lime", + "Maroon", + "Navy", + "Olive", + "Purple", + "Red", + "Silver", + "Teal", + "White", + "Yellow", +]; + +var gHAlign = ["left", "center", "right"]; + +var gHAlignJustify = ["left", "center", "right", "justify"]; + +var gHAlignTableContent = ["left", "center", "right", "justify", "char"]; + +var gVAlignTable = ["top", "middle", "bottom", "baseline"]; + +var gTarget = ["_blank", "_self", "_parent", "_top"]; + +// ================ HTML Attributes ================ // +/* For each element, there is an array of attributes, + whose name is the element name, + used to fill the "Attribute Name" menulist. + For each of those attributes, if they have a specific + set of values, those are listed in an array named: + "elementName_attName". + + In each values string, the following characters + are signal to do input filtering: + "#" Allow only integer values + "%" Allow integer values or a number ending in "%" + "+" Allow integer values and allow "+" or "-" as first character + "!" Allow only one character + "^" The first character can be only be A-Z, a-z, hyphen, underscore, colon or period + "$" is an attribute required by HTML DTD +*/ + +/* + Most elements have the "dir" attribute, + so we use this value array + for all elements instead of specifying + separately for each element +*/ +gHTMLAttr.all_dir = ["ltr", "rtl"]; + +gHTMLAttr.a = [ + "charset", + "type", + "name", + "href", + "^hreflang", + "target", + "rel", + "rev", + "!accesskey", + "shape", // with imagemap // + "coords", // with imagemap // + "#tabindex", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.a_target = gTarget; + +gHTMLAttr.a_rel = [ + "alternate", + "stylesheet", + "start", + "next", + "prev", + "contents", + "index", + "glossary", + "copyright", + "chapter", + "section", + "subsection", + "appendix", + "help", + "bookmark", +]; + +gHTMLAttr.a_rev = [ + "alternate", + "stylesheet", + "start", + "next", + "prev", + "contents", + "index", + "glossary", + "copyright", + "chapter", + "section", + "subsection", + "appendix", + "help", + "bookmark", +]; + +gHTMLAttr.a_shape = ["rect", "circle", "poly", "default"]; + +gHTMLAttr.abbr = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.acronym = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.address = ["_core", "-", "^lang", "dir"]; + +// this is deprecated // +gHTMLAttr.applet = [ + "codebase", + "archive", + "code", + "object", + "alt", + "name", + "%$width", + "%$height", + "align", + "#hspace", + "#vspace", + "-", + "_core", +]; + +gHTMLAttr.applet_align = ["top", "middle", "bottom", "left", "right"]; + +gHTMLAttr.area = [ + "shape", + "coords", + "href", + "nohref", + "target", + "$alt", + "#tabindex", + "!accesskey", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.area_target = gTarget; + +gHTMLAttr.area_shape = ["rect", "circle", "poly", "default"]; + +gHTMLAttr.area_nohref = ["nohref"]; + +gHTMLAttr.b = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.base = ["href", "target"]; + +gHTMLAttr.base_target = gTarget; + +// this is deprecated // +gHTMLAttr.basefont = ["^id", "$size", "color", "face"]; + +gHTMLAttr.basefont_color = gHTMLColors; + +gHTMLAttr.bdo = ["_core", "-", "^lang", "$dir"]; + +gHTMLAttr.bdo_dir = ["ltr", "rtl"]; + +gHTMLAttr.big = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.blockquote = ["cite", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.body = [ + "background", + "bgcolor", + "text", + "link", + "vlink", + "alink", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.body_bgcolor = gHTMLColors; + +gHTMLAttr.body_text = gHTMLColors; + +gHTMLAttr.body_link = gHTMLColors; + +gHTMLAttr.body_vlink = gHTMLColors; + +gHTMLAttr.body_alink = gHTMLColors; + +gHTMLAttr.br = ["clear", "-", "_core"]; + +gHTMLAttr.br_clear = ["none", "left", "all", "right"]; + +gHTMLAttr.button = [ + "name", + "value", + "$type", + "disabled", + "#tabindex", + "!accesskey", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.button_type = ["submit", "button", "reset"]; + +gHTMLAttr.button_disabled = ["disabled"]; + +gHTMLAttr.caption = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.caption_align = ["top", "bottom", "left", "right"]; + +// this is deprecated // +gHTMLAttr.center = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.cite = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.code = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.col = [ + "#$span", + "%width", + "align", + "!char", + "#charoff", + "valign", + "char", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.col_span = [ + "1", // default +]; + +gHTMLAttr.col_align = gHAlignTableContent; + +gHTMLAttr.col_valign = ["top", "middle", "bottom", "baseline"]; + +gHTMLAttr.colgroup = [ + "#$span", + "%width", + "align", + "!char", + "#charoff", + "valign", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.colgroup_span = [ + "1", // default +]; + +gHTMLAttr.colgroup_align = gHAlignTableContent; + +gHTMLAttr.colgroup_valign = ["top", "middle", "bottom", "baseline"]; + +gHTMLAttr.dd = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.del = ["cite", "datetime", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.dfn = ["_core", "-", "^lang", "dir"]; + +// this is deprecated // +gHTMLAttr.dir = ["compact", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.dir_compact = ["compact"]; + +gHTMLAttr.div = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.div_align = gHAlignJustify; + +gHTMLAttr.dl = ["compact", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.dl_compact = ["compact"]; + +gHTMLAttr.dt = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.em = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.fieldset = ["_core", "-", "^lang", "dir"]; + +// this is deprecated // +gHTMLAttr.font = ["+size", "color", "face", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.font_color = gHTMLColors; + +gHTMLAttr.form = [ + "$action", + "$method", + "enctype", + "accept", + "name", + "accept-charset", + "target", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.form_method = ["get", "post"]; + +gHTMLAttr.form_enctype = ["application/x-www-form-urlencoded"]; + +gHTMLAttr.form_target = gTarget; + +gHTMLAttr.frame = [ + "longdesc", + "name", + "src", + "#frameborder", + "#marginwidth", + "#marginheight", + "noresize", + "$scrolling", +]; + +gHTMLAttr.frame_frameborder = ["1", "0"]; + +gHTMLAttr.frame_noresize = ["noresize"]; + +gHTMLAttr.frame_scrolling = ["auto", "yes", "no"]; + +gHTMLAttr.frameset = ["rows", "cols", "-", "_core"]; + +gHTMLAttr.h1 = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.h1_align = gHAlignJustify; + +gHTMLAttr.h2 = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.h2_align = gHAlignJustify; + +gHTMLAttr.h3 = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.h3_align = gHAlignJustify; + +gHTMLAttr.h4 = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.h4_align = gHAlignJustify; + +gHTMLAttr.h5 = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.h5_align = gHAlignJustify; + +gHTMLAttr.h6 = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.h6_align = gHAlignJustify; + +gHTMLAttr.head = ["profile", "-", "^lang", "dir"]; + +gHTMLAttr.hr = [ + "align", + "noshade", + "#size", + "%width", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.hr_align = gHAlign; + +gHTMLAttr.hr_noshade = ["noshade"]; + +gHTMLAttr.html = ["version", "-", "^lang", "dir"]; + +gHTMLAttr.i = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.iframe = [ + "longdesc", + "name", + "src", + "$frameborder", + "marginwidth", + "marginheight", + "$scrolling", + "align", + "%height", + "%width", + "-", + "_core", +]; + +gHTMLAttr.iframe_frameborder = ["1", "0"]; + +gHTMLAttr.iframe_scrolling = ["auto", "yes", "no"]; + +gHTMLAttr.iframe_align = ["top", "middle", "bottom", "left", "right"]; + +gHTMLAttr.img = [ + "$src", + "$alt", + "longdesc", + "name", + "%height", + "%width", + "usemap", + "ismap", + "align", + "#border", + "#hspace", + "#vspace", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.img_ismap = ["ismap"]; + +gHTMLAttr.img_align = ["top", "middle", "bottom", "left", "right"]; + +gHTMLAttr.input = [ + "$type", + "name", + "value", + "checked", + "disabled", + "readonly", + "#size", + "#maxlength", + "src", + "alt", + "usemap", + "ismap", + "#tabindex", + "!accesskey", + "accept", + "align", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.input_type = [ + "text", + "password", + "checkbox", + "radio", + "submit", + "reset", + "file", + "hidden", + "image", + "button", +]; + +gHTMLAttr.input_checked = ["checked"]; + +gHTMLAttr.input_disabled = ["disabled"]; + +gHTMLAttr.input_readonly = ["readonly"]; + +gHTMLAttr.input_ismap = ["ismap"]; + +gHTMLAttr.input_align = ["top", "middle", "bottom", "left", "right"]; + +gHTMLAttr.ins = ["cite", "datetime", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.isindex = ["prompt", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.kbd = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.label = ["for", "!accesskey", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.legend = ["!accesskey", "align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.legend_align = ["top", "bottom", "left", "right"]; + +gHTMLAttr.li = ["type", "#value", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.li_type = ["disc", "square", "circle", "-", "1", "a", "A", "i", "I"]; + +gHTMLAttr.link = [ + "charset", + "href", + "^hreflang", + "type", + "rel", + "rev", + "media", + "target", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.link_target = gTarget; + +gHTMLAttr.link_rel = [ + "alternate", + "stylesheet", + "start", + "next", + "prev", + "contents", + "index", + "glossary", + "copyright", + "chapter", + "section", + "subsection", + "appendix", + "help", + "bookmark", +]; + +gHTMLAttr.link_rev = [ + "alternate", + "stylesheet", + "start", + "next", + "prev", + "contents", + "index", + "glossary", + "copyright", + "chapter", + "section", + "subsection", + "appendix", + "help", + "bookmark", +]; + +gHTMLAttr.map = ["$name", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.menu = ["compact", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.menu_compact = ["compact"]; + +gHTMLAttr.meta = [ + "http-equiv", + "name", + "$content", + "scheme", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.noframes = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.noscript = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.object = [ + "declare", + "classid", + "codebase", + "data", + "type", + "codetype", + "archive", + "standby", + "%height", + "%width", + "usemap", + "name", + "#tabindex", + "align", + "#border", + "#hspace", + "#vspace", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.object_declare = ["declare"]; + +gHTMLAttr.object_align = ["top", "middle", "bottom", "left", "right"]; + +gHTMLAttr.ol = ["type", "compact", "#start", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.ol_type = ["1", "a", "A", "i", "I"]; + +gHTMLAttr.ol_compact = ["compact"]; + +gHTMLAttr.optgroup = ["disabled", "$label", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.optgroup_disabled = ["disabled"]; + +gHTMLAttr.option = [ + "selected", + "disabled", + "label", + "value", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.option_selected = ["selected"]; + +gHTMLAttr.option_disabled = ["disabled"]; + +gHTMLAttr.p = ["align", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.p_align = gHAlignJustify; + +gHTMLAttr.param = ["^id", "$name", "value", "$valuetype", "type"]; + +gHTMLAttr.param_valuetype = ["data", "ref", "object"]; + +gHTMLAttr.pre = ["%width", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.q = ["cite", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.s = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.samp = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.script = ["charset", "$type", "language", "src", "defer"]; + +gHTMLAttr.script_defer = ["defer"]; + +gHTMLAttr.select = [ + "name", + "#size", + "multiple", + "disabled", + "#tabindex", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.select_multiple = ["multiple"]; + +gHTMLAttr.select_disabled = ["disabled"]; + +gHTMLAttr.small = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.span = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.strike = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.strong = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.style = ["$type", "media", "title", "-", "^lang", "dir"]; + +gHTMLAttr.sub = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.sup = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.table = [ + "summary", + "%width", + "#border", + "frame", + "rules", + "#cellspacing", + "#cellpadding", + "align", + "bgcolor", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.table_frame = [ + "void", + "above", + "below", + "hsides", + "lhs", + "rhs", + "vsides", + "box", + "border", +]; + +gHTMLAttr.table_rules = ["none", "groups", "rows", "cols", "all"]; + +// Note; This is alignment of the table, +// not table contents, like all other table child elements +gHTMLAttr.table_align = gHAlign; + +gHTMLAttr.table_bgcolor = gHTMLColors; + +gHTMLAttr.tbody = [ + "align", + "!char", + "#charoff", + "valign", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.tbody_align = gHAlignTableContent; + +gHTMLAttr.tbody_valign = gVAlignTable; + +gHTMLAttr.td = [ + "abbr", + "axis", + "headers", + "scope", + "$#rowspan", + "$#colspan", + "align", + "!char", + "#charoff", + "valign", + "nowrap", + "bgcolor", + "%width", + "%height", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.td_scope = ["row", "col", "rowgroup", "colgroup"]; + +gHTMLAttr.td_rowspan = [ + "1", // default +]; + +gHTMLAttr.td_colspan = [ + "1", // default +]; + +gHTMLAttr.td_align = gHAlignTableContent; + +gHTMLAttr.td_valign = gVAlignTable; + +gHTMLAttr.td_nowrap = ["nowrap"]; + +gHTMLAttr.td_bgcolor = gHTMLColors; + +gHTMLAttr.textarea = [ + "name", + "$#rows", + "$#cols", + "disabled", + "readonly", + "#tabindex", + "!accesskey", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.textarea_disabled = ["disabled"]; + +gHTMLAttr.textarea_readonly = ["readonly"]; + +gHTMLAttr.tfoot = [ + "align", + "!char", + "#charoff", + "valign", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.tfoot_align = gHAlignTableContent; + +gHTMLAttr.tfoot_valign = gVAlignTable; + +gHTMLAttr.th = [ + "abbr", + "axis", + "headers", + "scope", + "$#rowspan", + "$#colspan", + "align", + "!char", + "#charoff", + "valign", + "nowrap", + "bgcolor", + "%width", + "%height", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.th_scope = ["row", "col", "rowgroup", "colgroup"]; + +gHTMLAttr.th_rowspan = [ + "1", // default +]; + +gHTMLAttr.th_colspan = [ + "1", // default +]; + +gHTMLAttr.th_align = gHAlignTableContent; + +gHTMLAttr.th_valign = gVAlignTable; + +gHTMLAttr.th_nowrap = ["nowrap"]; + +gHTMLAttr.th_bgcolor = gHTMLColors; + +gHTMLAttr.thead = [ + "align", + "!char", + "#charoff", + "valign", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.thead_align = gHAlignTableContent; + +gHTMLAttr.thead_valign = gVAlignTable; + +gHTMLAttr.title = ["^lang", "dir"]; + +gHTMLAttr.tr = [ + "align", + "!char", + "#charoff", + "valign", + "bgcolor", + "-", + "_core", + "-", + "^lang", + "dir", +]; + +gHTMLAttr.tr_align = gHAlignTableContent; + +gHTMLAttr.tr_valign = gVAlignTable; + +gHTMLAttr.tr_bgcolor = gHTMLColors; + +gHTMLAttr.tt = ["_core", "-", "^lang", "dir"]; + +gHTMLAttr.u = ["_core", "-", "^lang", "dir"]; +gHTMLAttr.ul = ["type", "compact", "-", "_core", "-", "^lang", "dir"]; + +gHTMLAttr.ul_type = ["disc", "square", "circle"]; + +gHTMLAttr.ul_compact = ["compact"]; + +// Prefix with "_" since this is reserved (it's stripped out) +gHTMLAttr._var = ["_core", "-", "^lang", "dir"]; + +// ================ JS Attributes ================ // +// These are element specific even handlers. +/* Most all elements use gCoreJSEvents, so those + are assumed except for those listed here with "noEvents" +*/ + +gJSAttr.a = ["onfocus", "onblur"]; + +gJSAttr.area = ["onfocus", "onblur"]; + +gJSAttr.body = ["onload", "onupload"]; + +gJSAttr.button = ["onfocus", "onblur"]; + +gJSAttr.form = ["onsubmit", "onreset"]; + +gJSAttr.frameset = ["onload", "onunload"]; + +gJSAttr.input = ["onfocus", "onblur", "onselect", "onchange"]; + +gJSAttr.label = ["onfocus", "onblur"]; + +gJSAttr.select = ["onfocus", "onblur", "onchange"]; + +gJSAttr.textarea = ["onfocus", "onblur", "onselect", "onchange"]; + +// Elements that don't have JSEvents: +gJSAttr.font = ["noJSEvents"]; + +gJSAttr.applet = ["noJSEvents"]; + +gJSAttr.isindex = ["noJSEvents"]; + +gJSAttr.iframe = ["noJSEvents"]; diff --git a/comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js b/comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js new file mode 100644 index 0000000000..ca54fa16da --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdAdvancedEdit.js */ +/* import-globals-from EdDialogCommon.js */ + +// build attribute list in tree form from element attributes +function BuildCSSAttributeTable() { + var style = gElement.style; + if (style == undefined) { + dump("Inline styles undefined\n"); + return; + } + + var declLength = style.length; + + if (declLength == undefined || declLength == 0) { + if (declLength == undefined) { + dump("Failed to query the number of inline style declarations\n"); + } + + return; + } + + if (declLength > 0) { + for (var i = 0; i < declLength; ++i) { + var name = style.item(i); + var value = style.getPropertyValue(name); + AddTreeItem(name, value, "CSSAList", CSSAttrs); + } + } + + ClearCSSInputWidgets(); +} + +function onChangeCSSAttribute() { + var name = TrimString(gDialog.AddCSSAttributeNameInput.value); + if (!name) { + return; + } + + var value = TrimString(gDialog.AddCSSAttributeValueInput.value); + + // First try to update existing attribute + // If not found, add new attribute + if (!UpdateExistingAttribute(name, value, "CSSAList") && value) { + AddTreeItem(name, value, "CSSAList", CSSAttrs); + } +} + +function ClearCSSInputWidgets() { + gDialog.AddCSSAttributeTree.view.selection.clearSelection(); + gDialog.AddCSSAttributeNameInput.value = ""; + gDialog.AddCSSAttributeValueInput.value = ""; + SetTextboxFocus(gDialog.AddCSSAttributeNameInput); +} + +function onSelectCSSTreeItem() { + if (!gDoOnSelectTree) { + return; + } + + var tree = gDialog.AddCSSAttributeTree; + if (tree && tree.view.selection.count) { + gDialog.AddCSSAttributeNameInput.value = GetTreeItemAttributeStr( + getSelectedItem(tree) + ); + gDialog.AddCSSAttributeValueInput.value = GetTreeItemValueStr( + getSelectedItem(tree) + ); + } +} + +function onInputCSSAttributeName() { + var attName = TrimString( + gDialog.AddCSSAttributeNameInput.value + ).toLowerCase(); + var newValue = ""; + + var existingValue = GetAndSelectExistingAttributeValue(attName, "CSSAList"); + if (existingValue) { + newValue = existingValue; + } + + gDialog.AddCSSAttributeValueInput.value = newValue; +} + +function editCSSAttributeValue(targetCell) { + if (IsNotTreeHeader(targetCell)) { + gDialog.AddCSSAttributeValueInput.select(); + } +} + +function UpdateCSSAttributes() { + var CSSAList = document.getElementById("CSSAList"); + var styleString = ""; + for (var i = 0; i < CSSAList.children.length; i++) { + var item = CSSAList.children[i]; + var name = GetTreeItemAttributeStr(item); + var value = GetTreeItemValueStr(item); + // this code allows users to be sloppy in typing in values, and enter + // things like "foo: " and "bar;". This will trim off everything after the + // respective character. + if (name.includes(":")) { + name = name.substring(0, name.lastIndexOf(":")); + } + if (value.includes(";")) { + value = value.substring(0, value.lastIndexOf(";")); + } + if (i == CSSAList.children.length - 1) { + // Last property. + styleString += name + ": " + value + ";"; + } else { + styleString += name + ": " + value + "; "; + } + } + if (styleString) { + // Use editor transactions if modifying the element directly in the document + doRemoveAttribute("style"); + doSetAttribute("style", styleString); // NOTE BUG 18894!!! + } else if (gElement.getAttribute("style")) { + doRemoveAttribute("style"); + } +} + +function RemoveCSSAttribute() { + // We only allow 1 selected item + if (gDialog.AddCSSAttributeTree.view.selection.count) { + // Remove the item from the tree + // We always rebuild complete "style" string, + // so no list of "removed" items + getSelectedItem(gDialog.AddCSSAttributeTree).remove(); + + ClearCSSInputWidgets(); + } +} + +function SelectCSSTree(index) { + gDoOnSelectTree = false; + try { + gDialog.AddCSSAttributeTree.selectedIndex = index; + } catch (e) {} + gDoOnSelectTree = true; +} diff --git a/comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js b/comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js new file mode 100644 index 0000000000..127bfb858b --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js @@ -0,0 +1,362 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdAdvancedEdit.js */ +/* import-globals-from EdDialogCommon.js */ + +function BuildHTMLAttributeNameList() { + gDialog.AddHTMLAttributeNameInput.removeAllItems(); + + var elementName = gElement.localName; + var attNames = gHTMLAttr[elementName]; + + if (attNames && attNames.length) { + var menuitem; + + for (var i = 0; i < attNames.length; i++) { + var name = attNames[i]; + + if (name == "_core") { + // Signal to append the common 'core' attributes. + for (var j = 0; j < gCoreHTMLAttr.length; j++) { + name = gCoreHTMLAttr[j]; + + // only filtering rule used for core attributes as of 8-20-01 + // Add more rules if necessary. + if (name.includes("^")) { + name = name.replace(/\^/g, ""); + menuitem = gDialog.AddHTMLAttributeNameInput.appendItem(name, name); + menuitem.setAttribute("limitFirstChar", "true"); + } else { + gDialog.AddHTMLAttributeNameInput.appendItem(name, name); + } + } + } else if (name == "-") { + // Signal for separator + var popup = gDialog.AddHTMLAttributeNameInput.menupopup; + if (popup) { + var sep = document.createXULElement("menuseparator"); + if (sep) { + popup.appendChild(sep); + } + } + } else { + // Get information about value filtering + let forceOneChar = name.includes("!"); + let forceInteger = name.includes("#"); + let forceSignedInteger = name.includes("+"); + let forceIntOrPercent = name.includes("%"); + let limitFirstChar = name.includes("^"); + // let required = name.includes("$"); + + // Strip flag characters + name = name.replace(/[!^#%$+]/g, ""); + + menuitem = gDialog.AddHTMLAttributeNameInput.appendItem(name, name); + if (menuitem) { + // Signify "required" attributes by special style + // TODO: Don't do this until next version, when we add + // explanatory text and an 'Autofill Required Attributes' button + // if (required) + // menuitem.setAttribute("class", "menuitem-highlight-1"); + + // Set flags to filter value input + if (forceOneChar) { + menuitem.setAttribute("forceOneChar", "true"); + } + if (limitFirstChar) { + menuitem.setAttribute("limitFirstChar", "true"); + } + if (forceInteger) { + menuitem.setAttribute("forceInteger", "true"); + } + if (forceSignedInteger) { + menuitem.setAttribute("forceSignedInteger", "true"); + } + if (forceIntOrPercent) { + menuitem.setAttribute("forceIntOrPercent", "true"); + } + } + } + } + } +} + +// build attribute list in tree form from element attributes +function BuildHTMLAttributeTable() { + var nodeMap = gElement.attributes; + var i; + if (nodeMap.length > 0) { + var added = false; + for (i = 0; i < nodeMap.length; i++) { + let name = nodeMap[i].name.trim().toLowerCase(); + if ( + CheckAttributeNameSimilarity(nodeMap[i].nodeName, HTMLAttrs) || + name.startsWith("on") || + name == "style" + ) { + continue; // repeated or non-HTML attribute, ignore this one and go to next + } + if ( + !name.startsWith("_moz") && + AddTreeItem(name, nodeMap[i].value, "HTMLAList", HTMLAttrs) + ) { + added = true; + } + } + + if (added) { + SelectHTMLTree(0); + } + } +} + +function ClearHTMLInputWidgets() { + gDialog.AddHTMLAttributeTree.view.selection.clearSelection(); + gDialog.AddHTMLAttributeNameInput.value = ""; + gDialog.AddHTMLAttributeValueInput.value = ""; + SetTextboxFocus(gDialog.AddHTMLAttributeNameInput); +} + +function onSelectHTMLTreeItem() { + if (!gDoOnSelectTree) { + return; + } + + var tree = gDialog.AddHTMLAttributeTree; + if (tree && tree.view.selection.count) { + var inputName = TrimString( + gDialog.AddHTMLAttributeNameInput.value + ).toLowerCase(); + var selectedItem = getSelectedItem(tree); + var selectedName = + selectedItem.firstElementChild.firstElementChild.getAttribute("label"); + + if (inputName == selectedName) { + // Already editing selected name - just update the value input + gDialog.AddHTMLAttributeValueInput.value = + GetTreeItemValueStr(selectedItem); + } else { + gDialog.AddHTMLAttributeNameInput.value = selectedName; + + // Change value input based on new selected name + onInputHTMLAttributeName(); + } + } +} + +function onInputHTMLAttributeName() { + let attName = gDialog.AddHTMLAttributeNameInput.value.toLowerCase().trim(); + + // Clear value widget, but prevent triggering update in tree + gUpdateTreeValue = false; + gDialog.AddHTMLAttributeValueInput.value = ""; + gUpdateTreeValue = true; + + if (attName) { + // Get value list for current attribute name + var valueListName; + + // Most elements have the "dir" attribute, + // so we have just one array for the allowed values instead + // requiring duplicate entries for each element in EdAEAttributes.js + if (attName == "dir") { + valueListName = "all_dir"; + } else { + valueListName = gElement.localName + "_" + attName; + } + + // Strip off leading "_" we sometimes use (when element name is reserved word) + if (valueListName.startsWith("_")) { + valueListName = valueListName.slice(1); + } + + let useMenulist = false; // Editable menulist vs. input for the value. + var newValue = ""; + if (valueListName in gHTMLAttr) { + var valueList = gHTMLAttr[valueListName]; + + let listLen = valueList.length; + useMenulist = listLen > 1; + if (listLen == 1) { + newValue = valueList[0]; + } + + // Note: For case where "value list" is actually just + // one (default) item, don't use menulist for that + if (useMenulist) { + gDialog.AddHTMLAttributeValueMenulist.removeAllItems(); + + // Rebuild the list + for (var i = 0; i < listLen; i++) { + if (valueList[i] == "-") { + // Signal for separator + var popup = gDialog.AddHTMLAttributeValueInput.menupopup; + if (popup) { + var sep = document.createXULElement("menuseparator"); + if (sep) { + popup.appendChild(sep); + } + } + } else { + gDialog.AddHTMLAttributeValueMenulist.appendItem( + valueList[i], + valueList[i] + ); + } + } + } + } + if (useMenulist) { + // Switch to using editable menulist instead of the input. + gDialog.AddHTMLAttributeValueMenulist.parentElement.collapsed = false; + gDialog.AddHTMLAttributeValueTextbox.parentElement.collapsed = true; + gDialog.AddHTMLAttributeValueInput = + gDialog.AddHTMLAttributeValueMenulist; + } else { + // No list: Use input instead of editable menulist. + gDialog.AddHTMLAttributeValueMenulist.parentElement.collapsed = true; + gDialog.AddHTMLAttributeValueTextbox.parentElement.collapsed = false; + gDialog.AddHTMLAttributeValueInput = gDialog.AddHTMLAttributeValueTextbox; + } + + // If attribute already exists in tree, use associated value, + // else use default found above + var existingValue = GetAndSelectExistingAttributeValue( + attName, + "HTMLAList" + ); + if (existingValue) { + newValue = existingValue; + } + + gDialog.AddHTMLAttributeValueInput.value = newValue; + + if (!existingValue) { + onInputHTMLAttributeValue(); + } + } +} + +function onInputHTMLAttributeValue() { + if (!gUpdateTreeValue) { + return; + } + + var name = TrimString(gDialog.AddHTMLAttributeNameInput.value); + if (!name) { + return; + } + + // Trim spaces only from left since we must allow spaces within the string + // (we always reset the input field's value below) + var value = TrimStringLeft(gDialog.AddHTMLAttributeValueInput.value); + if (value) { + // Do value filtering based on type of attribute + // (Do not use "forceInteger()" to avoid multiple + // resetting of input's value and flickering) + var selectedItem = gDialog.AddHTMLAttributeNameInput.selectedItem; + + if (selectedItem) { + if ( + selectedItem.getAttribute("forceOneChar") == "true" && + value.length > 1 + ) { + value = value.slice(0, 1); + } + + if (selectedItem.getAttribute("forceIntOrPercent") == "true") { + // Allow integer with optional "%" as last character + var percent = TrimStringRight(value).slice(-1); + value = value.replace(/\D+/g, ""); + if (percent == "%") { + value += percent; + } + } else if (selectedItem.getAttribute("forceInteger") == "true") { + value = value.replace(/\D+/g, ""); + } else if (selectedItem.getAttribute("forceSignedInteger") == "true") { + // Allow integer with optional "+" or "-" as first character + var sign = value[0]; + value = value.replace(/\D+/g, ""); + if (sign == "+" || sign == "-") { + value = sign + value; + } + } + + // Special case attributes + if (selectedItem.getAttribute("limitFirstChar") == "true") { + // Limit first character to letter, and all others to + // letters, numbers, and a few others + value = value + .replace(/^[^a-zA-Z\u0080-\uFFFF]/, "") + .replace(/[^a-zA-Z0-9_\.\-\:\u0080-\uFFFF]+/g, ""); + } + + // Update once only if it changed + if (value != gDialog.AddHTMLAttributeValueInput.value) { + gDialog.AddHTMLAttributeValueInput.value = value; + } + } + } + + // Update value in the tree list + // If not found, add new attribute + if (!UpdateExistingAttribute(name, value, "HTMLAList") && value) { + AddTreeItem(name, value, "HTMLAList", HTMLAttrs); + } +} + +function editHTMLAttributeValue(targetCell) { + if (IsNotTreeHeader(targetCell)) { + gDialog.AddHTMLAttributeValueInput.select(); + } +} + +// update the object with added and removed attributes +function UpdateHTMLAttributes() { + var HTMLAList = document.getElementById("HTMLAList"); + var i; + + // remove removed attributes + for (i = 0; i < HTMLRAttrs.length; i++) { + var name = HTMLRAttrs[i]; + + if (gElement.hasAttribute(name)) { + doRemoveAttribute(name); + } + } + + // Set added or changed attributes + for (i = 0; i < HTMLAList.children.length; i++) { + var item = HTMLAList.children[i]; + doSetAttribute(GetTreeItemAttributeStr(item), GetTreeItemValueStr(item)); + } +} + +function RemoveHTMLAttribute() { + // We only allow 1 selected item + if (gDialog.AddHTMLAttributeTree.view.selection.count) { + var item = getSelectedItem(gDialog.AddHTMLAttributeTree); + var attr = GetTreeItemAttributeStr(item); + + // remove the item from the attribute array + HTMLRAttrs[HTMLRAttrs.length] = attr; + RemoveNameFromAttArray(attr, HTMLAttrs); + + // Remove the item from the tree + item.remove(); + + // Clear inputs and selected item in tree + ClearHTMLInputWidgets(); + } +} + +function SelectHTMLTree(index) { + gDoOnSelectTree = false; + try { + gDialog.AddHTMLAttributeTree.selectedIndex = index; + } catch (e) {} + gDoOnSelectTree = true; +} diff --git a/comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js b/comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js new file mode 100644 index 0000000000..8f902b74cd --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdAdvancedEdit.js */ +/* import-globals-from EdDialogCommon.js */ + +function BuildJSEAttributeNameList() { + gDialog.AddJSEAttributeNameList.removeAllItems(); + + // Get events specific to current element + var elementName = gElement.localName; + if (elementName in gJSAttr) { + var attNames = gJSAttr[elementName]; + var i; + var popup; + var sep; + + if (attNames && attNames.length) { + // Since we don't allow user-editable JS events yet (but we will soon) + // simply remove the JS tab to not allow adding JS events + if (attNames[0] == "noJSEvents") { + var tab = document.getElementById("tabJSE"); + if (tab) { + tab.remove(); + } + + return; + } + + for (i = 0; i < attNames.length; i++) { + gDialog.AddJSEAttributeNameList.appendItem(attNames[i], attNames[i]); + } + + popup = gDialog.AddJSEAttributeNameList.firstElementChild; + if (popup) { + sep = document.createXULElement("menuseparator"); + if (sep) { + popup.appendChild(sep); + } + } + } + } + + // Always add core JS events unless we aborted above + for (i = 0; i < gCoreJSEvents.length; i++) { + if (gCoreJSEvents[i] == "-") { + if (!popup) { + popup = gDialog.AddJSEAttributeNameList.firstElementChild; + } + + sep = document.createXULElement("menuseparator"); + + if (popup && sep) { + popup.appendChild(sep); + } + } else { + gDialog.AddJSEAttributeNameList.appendItem( + gCoreJSEvents[i], + gCoreJSEvents[i] + ); + } + } + + gDialog.AddJSEAttributeNameList.selectedIndex = 0; + + // Use current name and value of first tree item if it exists + onSelectJSETreeItem(); +} + +// build attribute list in tree form from element attributes +function BuildJSEAttributeTable() { + var nodeMap = gElement.attributes; + if (nodeMap.length > 0) { + var added = false; + for (var i = 0; i < nodeMap.length; i++) { + let name = nodeMap[i].nodeName.toLowerCase(); + if (CheckAttributeNameSimilarity(nodeMap[i].nodeName, JSEAttrs)) { + // Repeated or non-JS handler, ignore this one and go to next. + continue; + } + if (!name.startsWith("on")) { + // Attribute isn't an event handler. + continue; + } + var value = gElement.getAttribute(nodeMap[i].nodeName); + if (AddTreeItem(name, value, "JSEAList", JSEAttrs)) { + // add item to tree + added = true; + } + } + + // Select first item + if (added) { + gDialog.AddJSEAttributeTree.selectedIndex = 0; + } + } +} + +function onSelectJSEAttribute() { + if (!gDoOnSelectTree) { + return; + } + + gDialog.AddJSEAttributeValueInput.value = GetAndSelectExistingAttributeValue( + gDialog.AddJSEAttributeNameList.label, + "JSEAList" + ); +} + +function onSelectJSETreeItem() { + var tree = gDialog.AddJSEAttributeTree; + if (tree && tree.view.selection.count) { + // Select attribute name in list + gDialog.AddJSEAttributeNameList.value = GetTreeItemAttributeStr( + getSelectedItem(tree) + ); + + // Set value input to that in tree (no need to update this in the tree) + gUpdateTreeValue = false; + gDialog.AddJSEAttributeValueInput.value = GetTreeItemValueStr( + getSelectedItem(tree) + ); + gUpdateTreeValue = true; + } +} + +function onInputJSEAttributeValue() { + if (gUpdateTreeValue) { + var name = TrimString(gDialog.AddJSEAttributeNameList.label); + var value = TrimString(gDialog.AddJSEAttributeValueInput.value); + + // Update value in the tree list + // Since we have a non-editable menulist, + // we MUST automatically add the event attribute if it doesn't exist + if (!UpdateExistingAttribute(name, value, "JSEAList") && value) { + AddTreeItem(name, value, "JSEAList", JSEAttrs); + } + } +} + +function editJSEAttributeValue(targetCell) { + if (IsNotTreeHeader(targetCell)) { + gDialog.AddJSEAttributeValueInput.select(); + } +} + +function UpdateJSEAttributes() { + var JSEAList = document.getElementById("JSEAList"); + var i; + + // remove removed attributes + for (i = 0; i < JSERAttrs.length; i++) { + var name = JSERAttrs[i]; + + if (gElement.hasAttribute(name)) { + doRemoveAttribute(name); + } + } + + // Add events + for (i = 0; i < JSEAList.children.length; i++) { + var item = JSEAList.children[i]; + + // set the event handler + doSetAttribute(GetTreeItemAttributeStr(item), GetTreeItemValueStr(item)); + } +} + +function RemoveJSEAttribute() { + // This differs from HTML and CSS panels: + // We reselect after removing, because there is not + // editable attribute name input, so we can't clear that + // like we do in other panels + var newIndex = gDialog.AddJSEAttributeTree.selectedIndex; + + // We only allow 1 selected item + if (gDialog.AddJSEAttributeTree.view.selection.count) { + var item = getSelectedItem(gDialog.AddJSEAttributeTree); + + // Name is the text of the treecell + var attr = GetTreeItemAttributeStr(item); + + // remove the item from the attribute array + if (newIndex >= JSEAttrs.length - 1) { + newIndex--; + } + + // remove the item from the attribute array + JSERAttrs[JSERAttrs.length] = attr; + RemoveNameFromAttArray(attr, JSEAttrs); + + // Remove the item from the tree + item.remove(); + + // Reselect an item + gDialog.AddJSEAttributeTree.selectedIndex = newIndex; + } +} diff --git a/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js new file mode 100644 index 0000000000..5f2515c2f6 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js @@ -0,0 +1,342 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdAEAttributes.js */ +/* import-globals-from EdAECSSAttributes.js */ +/* import-globals-from EdAEHTMLAttributes.js */ +/* import-globals-from EdAEJSEAttributes.js */ +/* import-globals-from EdDialogCommon.js */ + +/** ************ GLOBALS */ +var gElement = null; // handle to actual element edited + +var HTMLAttrs = []; // html attributes +var CSSAttrs = []; // css attributes +var JSEAttrs = []; // js events + +var HTMLRAttrs = []; // removed html attributes +var JSERAttrs = []; // removed js events + +/* Set false to allow changing selection in tree + without doing "onselect" handler actions +*/ +var gDoOnSelectTree = true; +var gUpdateTreeValue = true; + +/** ************ INITIALISATION && SETUP */ + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +/** + * function : void Startup(); + * parameters : none + * returns : none + * desc. : startup and initialisation, prepares dialog. + */ +function Startup() { + var editor = GetCurrentEditor(); + + // Element to edit is passed in + if (!editor || !window.arguments[1]) { + dump("Advanced Edit: No editor or element to edit not supplied\n"); + window.close(); + return; + } + // This is the return value for the parent, + // who only needs to know if OK was clicked + window.opener.AdvancedEditOK = false; + + // The actual element edited (not a copy!) + gElement = window.arguments[1]; + + // place the tag name in the header + var tagLabel = document.getElementById("tagLabel"); + tagLabel.setAttribute("value", "<" + gElement.localName + ">"); + + // Create dialog object to store controls for easy access + gDialog.AddHTMLAttributeNameInput = document.getElementById( + "AddHTMLAttributeNameInput" + ); + + gDialog.AddHTMLAttributeValueMenulist = document.getElementById( + "AddHTMLAttributeValueMenulist" + ); + gDialog.AddHTMLAttributeValueTextbox = document.getElementById( + "AddHTMLAttributeValueTextbox" + ); + gDialog.AddHTMLAttributeValueInput = gDialog.AddHTMLAttributeValueTextbox; + + gDialog.AddHTMLAttributeTree = document.getElementById("HTMLATree"); + gDialog.AddCSSAttributeNameInput = document.getElementById( + "AddCSSAttributeNameInput" + ); + gDialog.AddCSSAttributeValueInput = document.getElementById( + "AddCSSAttributeValueInput" + ); + gDialog.AddCSSAttributeTree = document.getElementById("CSSATree"); + gDialog.AddJSEAttributeNameList = document.getElementById( + "AddJSEAttributeNameList" + ); + gDialog.AddJSEAttributeValueInput = document.getElementById( + "AddJSEAttributeValueInput" + ); + gDialog.AddJSEAttributeTree = document.getElementById("JSEATree"); + gDialog.okButton = document.querySelector("dialog").getButton("accept"); + + // build the attribute trees + BuildHTMLAttributeTable(); + BuildCSSAttributeTable(); + BuildJSEAttributeTable(); + + // Build attribute name arrays for menulists + BuildJSEAttributeNameList(); + BuildHTMLAttributeNameList(); + // No menulists for CSS panel (yet) + + // Set focus to Name editable menulist in HTML panel + SetTextboxFocus(gDialog.AddHTMLAttributeNameInput); + + // size the dialog properly + window.sizeToContent(); + + SetWindowLocation(); +} + +/** + * function : bool onAccept ( void ); + * parameters : none + * returns : boolean true to close the window + * desc. : event handler for ok button + */ +function onAccept() { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + try { + // Update our gElement attributes + UpdateHTMLAttributes(); + UpdateCSSAttributes(); + UpdateJSEAttributes(); + } catch (ex) { + dump(ex); + } + editor.endTransaction(); + + window.opener.AdvancedEditOK = true; + SaveWindowLocation(); +} + +// Helpers for removing and setting attributes +// Use editor transactions if modifying the element already in the document +// (Temporary element from a property dialog won't have a parent node) +function doRemoveAttribute(attrib) { + try { + var editor = GetCurrentEditor(); + if (gElement.parentNode) { + editor.removeAttribute(gElement, attrib); + } else { + gElement.removeAttribute(attrib); + } + } catch (ex) {} +} + +function doSetAttribute(attrib, value) { + try { + var editor = GetCurrentEditor(); + if (gElement.parentNode) { + editor.setAttribute(gElement, attrib, value); + } else { + gElement.setAttribute(attrib, value); + } + } catch (ex) {} +} + +/** + * function : bool CheckAttributeNameSimilarity ( string attName, array attArray ); + * parameters : attribute to look for, array of current attributes + * returns : true if attribute already exists, false if it does not + * desc. : checks to see if any other attributes by the same name as the arg supplied + * already exist. + */ +function CheckAttributeNameSimilarity(attName, attArray) { + for (var i = 0; i < attArray.length; i++) { + if (attName.toLowerCase() == attArray[i].toLowerCase()) { + return true; + } + } + return false; +} + +/** + * function : bool UpdateExistingAttribute ( string attName, string attValue, string treeChildrenId ); + * parameters : attribute to look for, new value, ID of <treeChildren> node in XUL tree + * returns : true if attribute already exists in tree, false if it does not + * desc. : checks to see if any other attributes by the same name as the arg supplied + * already exist while setting the associated value if different from current value + */ +function UpdateExistingAttribute(attName, attValue, treeChildrenId) { + var treeChildren = document.getElementById(treeChildrenId); + if (!treeChildren) { + return false; + } + + var name; + var i; + attName = TrimString(attName).toLowerCase(); + attValue = TrimString(attValue); + + for (i = 0; i < treeChildren.children.length; i++) { + var item = treeChildren.children[i]; + name = GetTreeItemAttributeStr(item); + if (name.toLowerCase() == attName) { + // Set the text in the "value' column treecell + SetTreeItemValueStr(item, attValue); + + // Select item just changed, + // but don't trigger the tree's onSelect handler + gDoOnSelectTree = false; + try { + selectTreeItem(treeChildren, item); + } catch (e) {} + gDoOnSelectTree = true; + + return true; + } + } + return false; +} + +/** + * function : string GetAndSelectExistingAttributeValue ( string attName, string treeChildrenId ); + * parameters : attribute to look for, ID of <treeChildren> node in XUL tree + * returns : value in from the tree or empty string if name not found + */ +function GetAndSelectExistingAttributeValue(attName, treeChildrenId) { + if (!attName) { + return ""; + } + + var treeChildren = document.getElementById(treeChildrenId); + var name; + var i; + + for (i = 0; i < treeChildren.children.length; i++) { + var item = treeChildren.children[i]; + name = GetTreeItemAttributeStr(item); + if (name.toLowerCase() == attName.toLowerCase()) { + // Select item in the tree + // but don't trigger the tree's onSelect handler + gDoOnSelectTree = false; + try { + selectTreeItem(treeChildren, item); + } catch (e) {} + gDoOnSelectTree = true; + + // Get the text in the "value' column treecell + return GetTreeItemValueStr(item); + } + } + + // Attribute doesn't exist in tree, so remove selection + gDoOnSelectTree = false; + try { + treeChildren.parentNode.view.selection.clearSelection(); + } catch (e) {} + gDoOnSelectTree = true; + + return ""; +} + +/* Tree structure: + <treeItem> + <treeRow> + <treeCell> // Name Cell + <treeCell // Value Cell +*/ +function GetTreeItemAttributeStr(treeItem) { + if (treeItem) { + return TrimString( + treeItem.firstElementChild.firstElementChild.getAttribute("label") + ); + } + + return ""; +} + +function GetTreeItemValueStr(treeItem) { + if (treeItem) { + return TrimString( + treeItem.firstElementChild.lastElementChild.getAttribute("label") + ); + } + + return ""; +} + +function SetTreeItemValueStr(treeItem, value) { + if (treeItem && GetTreeItemValueStr(treeItem) != value) { + treeItem.firstElementChild.lastElementChild.setAttribute("label", value); + } +} + +function IsNotTreeHeader(treeCell) { + if (treeCell) { + return treeCell.parentNode.parentNode.nodeName != "treehead"; + } + + return false; +} + +function RemoveNameFromAttArray(attName, attArray) { + for (var i = 0; i < attArray.length; i++) { + if (attName.toLowerCase() == attArray[i].toLowerCase()) { + // Remove 1 array item + attArray.splice(i, 1); + break; + } + } +} + +// adds a generalised treeitem. +function AddTreeItem(name, value, treeChildrenId, attArray) { + attArray[attArray.length] = name; + var treeChildren = document.getElementById(treeChildrenId); + var treeitem = document.createXULElement("treeitem"); + var treerow = document.createXULElement("treerow"); + + var attrCell = document.createXULElement("treecell"); + attrCell.setAttribute("class", "propertylist"); + attrCell.setAttribute("label", name); + + var valueCell = document.createXULElement("treecell"); + valueCell.setAttribute("class", "propertylist"); + valueCell.setAttribute("label", value); + + treerow.appendChild(attrCell); + treerow.appendChild(valueCell); + treeitem.appendChild(treerow); + treeChildren.appendChild(treeitem); + + // Select item just added, but suppress calling the onSelect handler. + gDoOnSelectTree = false; + try { + selectTreeItem(treeChildren, treeitem); + } catch (e) {} + gDoOnSelectTree = true; + + return treeitem; +} + +function selectTreeItem(treeChildren, item) { + var index = treeChildren.parentNode.view.getIndexOfItem(item); + treeChildren.parentNode.view.selection.select(index); +} + +function getSelectedItem(tree) { + if (tree.view.selection.count == 1) { + return tree.view.getItemAtIndex(tree.currentIndex); + } + return null; +} diff --git a/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml new file mode 100644 index 0000000000..cfeff95b42 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml @@ -0,0 +1,243 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!-- first checkin of the year 2000! --> +<!-- Ben Goodger, 12:50AM, 01/00/00 NZST --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/menulist.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EdAdvancedEdit.dtd"> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + style="min-width: 40em" + title="&WindowTitle.label;" + lightweightthemes="true" + onload="Startup()" +> + <dialog id="advancedEditDlg"> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <!-- element page functions --> + <script src="chrome://messenger/content/messengercompose/EdAEHTMLAttributes.js" /> + <script src="chrome://messenger/content/messengercompose/EdAECSSAttributes.js" /> + <script src="chrome://messenger/content/messengercompose/EdAEJSEAttributes.js" /> + <script src="chrome://messenger/content/messengercompose/EdAEAttributes.js" /> + + <!-- global dialog functions --> + <script src="chrome://messenger/content/messengercompose/EdAdvancedEdit.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <hbox> + <label value="¤tattributesfor.label;" /> + <label class="header" id="tagLabel" /> + </hbox> + + <separator class="thin" /> + + <tabbox flex="1"> + <tabs> + <tab label="&tabHTML.label;" /> + <tab label="&tabCSS.label;" /> + <tab label="&tabJSE.label;" id="tabJSE" /> + </tabs> + <tabpanels flex="1"> + <!-- ============================================================== --> + <!-- HTML Attributes --> + <!-- ============================================================== --> + <vbox> + <tree + id="HTMLATree" + class="AttributesTree" + flex="1" + hidecolumnpicker="true" + seltype="single" + onselect="onSelectHTMLTreeItem();" + onclick="onSelectHTMLTreeItem();" + ondblclick="editHTMLAttributeValue(event.target);" + > + <treecols> + <treecol id="HTMLAttrCol" label="&tree.attributeHeader.label;" /> + <splitter class="tree-splitter" /> + <treecol id="HTMLValCol" label="&tree.valueHeader.label;" /> + </treecols> + <treechildren id="HTMLAList" flex="1" /> + </tree> + <hbox align="center"> + <label value="&editAttribute.label;" /> + <spacer flex="1" /> + <button + label="&removeAttribute.label;" + oncommand="RemoveHTMLAttribute();" + /> + </hbox> + <hbox> + <vbox flex="1"> + <label + control="AddHTMLAttributeNameInput" + value="&AttName.label;" + /> + <menulist + is="menulist-editable" + id="AddHTMLAttributeNameInput" + class="editorAdvancedEditableMenulist" + editable="true" + flex="1" + oninput="onInputHTMLAttributeName();" + oncommand="onInputHTMLAttributeName();" + /> + </vbox> + <vbox flex="1"> + <label + id="AddHTMLAttributeValueLabel" + control="AddHTMLAttributeValueInput" + value="&AttValue.label;" + /> + <vbox flex="1"> + <hbox flex="1" class="input-container"> + <html:input + id="AddHTMLAttributeValueTextbox" + type="text" + class="input-inline" + onchange="onInputHTMLAttributeValue();" + aria-labelledby="AddHTMLAttributeValueLabel" + /> + </hbox> + <hbox flex="1" collapsed="true"> + <menulist + is="menulist-editable" + id="AddHTMLAttributeValueMenulist" + editable="true" + flex="1" + oninput="onInputHTMLAttributeValue();" + oncommand="onInputHTMLAttributeValue();" + /> + </hbox> + </vbox> + </vbox> + </hbox> + </vbox> + <!-- ============================================================== --> + <!-- CSS Attributes --> + <!-- ============================================================== --> + <vbox> + <tree + id="CSSATree" + class="AttributesTree" + flex="1" + hidecolumnpicker="true" + seltype="single" + onselect="onSelectCSSTreeItem();" + onclick="onSelectCSSTreeItem();" + ondblclick="editCSSAttributeValue(event.target);" + > + <treecols> + <treecol id="CSSPropCol" label="&tree.propertyHeader.label;" /> + <splitter class="tree-splitter" /> + <treecol id="CSSValCol" label="&tree.valueHeader.label;" /> + </treecols> + <treechildren id="CSSAList" flex="1" /> + </tree> + <hbox align="center"> + <label value="&editAttribute.label;" /> + <spacer flex="1" /> + <button + label="&removeAttribute.label;" + oncommand="RemoveCSSAttribute();" + /> + </hbox> + <hbox> + <vbox flex="1"> + <label + id="AddCSSAttributeNameLabel" + value="&PropertyName.label;" + /> + <html:input + id="AddCSSAttributeNameInput" + type="text" + class="input-inline" + onchange="onInputCSSAttributeName();" + aria-labelledby="AddCSSAttributeNameLabel" + /> + </vbox> + <vbox flex="1"> + <label id="AddCSSAttributeValueLabel" value="&AttValue.label;" /> + <html:input + id="AddCSSAttributeValueInput" + type="text" + class="input-inline" + onchange="onChangeCSSAttribute();" + aria-labelledby="AddCSSAttributeValueLabel" + /> + </vbox> + </hbox> + </vbox> + <!-- ============================================================== --> + <!-- JavaScript Event Handlers --> + <!-- ============================================================== --> + <vbox> + <tree + id="JSEATree" + class="AttributesTree" + flex="1" + hidecolumnpicker="true" + seltype="single" + onselect="onSelectJSETreeItem();" + onclick="onSelectJSETreeItem();" + ondblclick="editJSEAttributeValue(event.target);" + > + <treecols> + <treecol id="AttrCol" label="&tree.attributeHeader.label;" /> + <splitter class="tree-splitter" /> + <treecol id="HeaderCol" label="&tree.valueHeader.label;" /> + </treecols> + <treechildren id="JSEAList" flex="1" /> + </tree> + <hbox align="center"> + <label value="&editAttribute.label;" /> + <spacer flex="1" /> + <button + label="&removeAttribute.label;" + oncommand="RemoveJSEAttribute()" + /> + </hbox> + <hbox> + <vbox flex="1"> + <label value="&AttName.label;" /> + <menulist + id="AddJSEAttributeNameList" + oncommand="onSelectJSEAttribute();" + /> + </vbox> + <vbox flex="1"> + <label id="AddJSEAttributeValueLabel" value="&AttValue.label;" /> + <hbox flex="1" class="input-container"> + <html:input + id="AddJSEAttributeValueInput" + type="text" + class="input-inline" + onchange="onInputJSEAttributeValue();" + aria-labelledby="AddJSEAttributeValueLabel" + /> + </hbox> + </vbox> + </hbox> + </vbox> + </tabpanels> + </tabbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdColorPicker.js b/comm/mail/components/compose/content/dialogs/EdColorPicker.js new file mode 100644 index 0000000000..ef03a1d10b --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdColorPicker.js @@ -0,0 +1,290 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +// Cancel() is in EdDialogCommon.js + +var insertNew = true; +var tagname = "TAG NAME"; +var gColor = ""; +var LastPickedColor = ""; +var ColorType = "Text"; +var TextType = false; +var HighlightType = false; +var TableOrCell = false; +var LastPickedIsDefault = true; +var NoDefault = false; +var gColorObj; + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancelColor); + +function Startup() { + if (!window.arguments[1]) { + dump("EdColorPicker: Missing color object param\n"); + return; + } + + // window.arguments[1] is object to get initial values and return color data + gColorObj = window.arguments[1]; + gColorObj.Cancel = false; + + gDialog.ColorPicker = document.getElementById("ColorPicker"); + gDialog.ColorInput = document.getElementById("ColorInput"); + gDialog.LastPickedButton = document.getElementById("LastPickedButton"); + gDialog.LastPickedColor = document.getElementById("LastPickedColor"); + gDialog.CellOrTableGroup = document.getElementById("CellOrTableGroup"); + gDialog.TableRadio = document.getElementById("TableRadio"); + gDialog.CellRadio = document.getElementById("CellRadio"); + gDialog.ColorSwatch = document.getElementById("ColorPickerSwatch"); + gDialog.Ok = document.querySelector("dialog").getButton("accept"); + + // The type of color we are setting: + // text: Text, Link, ActiveLink, VisitedLink, + // or background: Page, Table, or Cell + if (gColorObj.Type) { + ColorType = gColorObj.Type; + // Get string for dialog title from passed-in type + // (note constraint on editor.properties string name) + let IsCSSPrefChecked = Services.prefs.getBoolPref("editor.use_css"); + + if (GetCurrentEditor()) { + if (ColorType == "Page" && IsCSSPrefChecked && IsHTMLEditor()) { + document.title = GetString("BlockColor"); + } else { + document.title = GetString(ColorType + "Color"); + } + } + } + + gDialog.ColorInput.value = ""; + var tmpColor; + var haveTableRadio = false; + + switch (ColorType) { + case "Page": + tmpColor = gColorObj.PageColor; + if (tmpColor && tmpColor.toLowerCase() != "window") { + gColor = tmpColor; + } + break; + case "Table": + if (gColorObj.TableColor) { + gColor = gColorObj.TableColor; + } + break; + case "Cell": + if (gColorObj.CellColor) { + gColor = gColorObj.CellColor; + } + break; + case "TableOrCell": + TableOrCell = true; + document.getElementById("TableOrCellGroup").collapsed = false; + haveTableRadio = true; + if (gColorObj.SelectedType == "Cell") { + gColor = gColorObj.CellColor; + gDialog.CellOrTableGroup.selectedItem = gDialog.CellRadio; + gDialog.CellRadio.focus(); + } else { + gColor = gColorObj.TableColor; + gDialog.CellOrTableGroup.selectedItem = gDialog.TableRadio; + gDialog.TableRadio.focus(); + } + break; + case "Highlight": + HighlightType = true; + if (gColorObj.HighlightColor) { + gColor = gColorObj.HighlightColor; + } + break; + default: + // Any other type will change some kind of text, + TextType = true; + tmpColor = gColorObj.TextColor; + if (tmpColor && tmpColor.toLowerCase() != "windowtext") { + gColor = gColorObj.TextColor; + } + break; + } + + // Set initial color in input field and in the colorpicker + SetCurrentColor(gColor); + gDialog.ColorPicker.value = gColor; + + // Use last-picked colors passed in, or those persistent on dialog + if (TextType) { + if (!("LastTextColor" in gColorObj) || !gColorObj.LastTextColor) { + gColorObj.LastTextColor = + gDialog.LastPickedColor.getAttribute("LastTextColor"); + } + LastPickedColor = gColorObj.LastTextColor; + } else if (HighlightType) { + if (!("LastHighlightColor" in gColorObj) || !gColorObj.LastHighlightColor) { + gColorObj.LastHighlightColor = + gDialog.LastPickedColor.getAttribute("LastHighlightColor"); + } + LastPickedColor = gColorObj.LastHighlightColor; + } else { + if ( + !("LastBackgroundColor" in gColorObj) || + !gColorObj.LastBackgroundColor + ) { + gColorObj.LastBackgroundColor = gDialog.LastPickedColor.getAttribute( + "LastBackgroundColor" + ); + } + LastPickedColor = gColorObj.LastBackgroundColor; + } + + // Set method to detect clicking on OK button + // so we don't get fooled by changing "default" behavior + gDialog.Ok.setAttribute("onclick", "SetDefaultToOk()"); + + if (!LastPickedColor) { + // Hide the button, as there is no last color available. + gDialog.LastPickedButton.hidden = true; + } else { + gDialog.LastPickedColor.setAttribute( + "style", + "background-color: " + LastPickedColor + ); + + // Make "Last-picked" the default button, until the user selects a color. + gDialog.Ok.removeAttribute("default"); + gDialog.LastPickedButton.setAttribute("default", "true"); + } + + // Caller can prevent user from submitting an empty, i.e., default color + NoDefault = gColorObj.NoDefault; + if (NoDefault) { + // Hide the "Default button -- user must pick a color + document.getElementById("DefaultColorButton").collapsed = true; + } + + // Set focus to colorpicker if not set to table radio buttons above + if (!haveTableRadio) { + gDialog.ColorPicker.focus(); + } + + SetWindowLocation(); +} + +function SelectColor() { + var color = gDialog.ColorPicker.value; + if (color) { + SetCurrentColor(color); + } +} + +function RemoveColor() { + SetCurrentColor(""); + gDialog.ColorInput.focus(); + SetDefaultToOk(); +} + +function SelectColorByKeypress(aEvent) { + if (aEvent.charCode == aEvent.DOM_VK_SPACE) { + SelectColor(); + SetDefaultToOk(); + } +} + +function SelectLastPickedColor() { + SetCurrentColor(LastPickedColor); + if (onAccept()) { + // window.close(); + return true; + } + + return false; +} + +function SetCurrentColor(color) { + // TODO: Validate color? + if (!color) { + color = ""; + } + gColor = TrimString(color).toLowerCase(); + if (gColor == "mixed") { + gColor = ""; + } + gDialog.ColorInput.value = gColor; + SetColorSwatch(); +} + +function SetColorSwatch() { + gDialog.ColorSwatch.setAttribute( + "style", + `background-color: ${TrimString(gDialog.ColorInput.value) || "inherit"}` + ); +} + +function SetDefaultToOk() { + gDialog.LastPickedButton.removeAttribute("default"); + gDialog.Ok.setAttribute("default", "true"); + LastPickedIsDefault = false; +} + +function ValidateData() { + if (LastPickedIsDefault) { + gColor = LastPickedColor; + } else { + gColor = gDialog.ColorInput.value; + } + + gColor = TrimString(gColor).toLowerCase(); + + // TODO: Validate the color string! + + if (NoDefault && !gColor) { + ShowInputErrorMessage(GetString("NoColorError")); + SetTextboxFocus(gDialog.ColorInput); + return false; + } + return true; +} + +function onAccept(event) { + if (!ValidateData()) { + event.preventDefault(); + return; + } + + // Set return values and save in persistent color attributes + if (TextType) { + gColorObj.TextColor = gColor; + if (gColor.length > 0) { + gDialog.LastPickedColor.setAttribute("LastTextColor", gColor); + gColorObj.LastTextColor = gColor; + } + } else if (HighlightType) { + gColorObj.HighlightColor = gColor; + if (gColor.length > 0) { + gDialog.LastPickedColor.setAttribute("LastHighlightColor", gColor); + gColorObj.LastHighlightColor = gColor; + } + } else { + gColorObj.BackgroundColor = gColor; + if (gColor.length > 0) { + gDialog.LastPickedColor.setAttribute("LastBackgroundColor", gColor); + gColorObj.LastBackgroundColor = gColor; + } + // If table or cell requested, tell caller which element to set on + if (TableOrCell && gDialog.TableRadio.selected) { + gColorObj.Type = "Table"; + } + } + SaveWindowLocation(); +} + +function onCancelColor() { + // Tells caller that user canceled + gColorObj.Cancel = true; + SaveWindowLocation(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml b/comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml new file mode 100644 index 0000000000..8576fc27da --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml @@ -0,0 +1,103 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EdColorPicker.dtd"> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" +> + <dialog> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdColorPicker.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <hbox id="TableOrCellGroup" align="center" collapsed="true"> + <label + control="CellOrTableGroup" + value="&background.label;" + accesskey="&background.accessKey;" + /> + <radiogroup id="CellOrTableGroup" orient="horizontal"> + <radio + id="TableRadio" + label="&table.label;" + accesskey="&table.accessKey;" + /> + <radio + id="CellRadio" + label="&cell.label;" + accesskey="&cell.accessKey;" + /> + </radiogroup> + </hbox> + <hbox align="center"> + <label value="&chooseColor1.label;" /> + <html:input + type="color" + id="ColorPicker" + onclick="SetDefaultToOk();" + ondblclick="if (onAccept()) { window.close(); }" + onkeypress="SelectColorByKeypress(event);" + onchange="SelectColor();" + /> + <spacer flex="1" /> + <button + id="LastPickedButton" + label="&lastPickedColor.label;" + accesskey="&lastPickedColor.accessKey;" + crop="right" + oncommand="SelectLastPickedColor();" + > + <spacer + id="LastPickedColor" + LastTextColor="" + LastBackgroundColor="" + persist="LastTextColor LastBackgroundColor" + /> + </button> + </hbox> + + <spacer class="spacer" /> + <hbox align="center" flex="1"> + <vbox> + <label + class="tip-caption" + value="&chooseColor2.label;" + accesskey="&chooseColor2.accessKey;" + control="ColorInput" + /> + <label class="tip-caption" value="&setColorExample.label;" /> + </vbox> + <html:input + id="ColorInput" + type="text" + style="width: 8em" + oninput="SetColorSwatch(); SetDefaultToOk();" + /> + <label id="ColorPickerSwatch" /> + <spacer flex="1" /> + <button + id="DefaultColorButton" + label="&default.label;" + accesskey="&default.accessKey;" + oncommand="RemoveColor()" + /> + </hbox> + <separator class="groove" /> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdColorProps.js b/comm/mail/components/compose/content/dialogs/EdColorProps.js new file mode 100644 index 0000000000..c2635912d5 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdColorProps.js @@ -0,0 +1,476 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + Behavior notes: + Radio buttons select "UseDefaultColors" vs. "UseCustomColors" modes. + If any color attribute is set in the body, mode is "Custom Colors", + even if 1 or more (but not all) are actually null (= "use default") + When in "Custom Colors" mode, all colors will be set on body tag, + even if they are just default colors, to assure compatible colors in page. + User cannot select "use default" for individual colors +*/ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +// Cancel() is in EdDialogCommon.js + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +var gBodyElement; +var prefs; +var gBackgroundImage; + +// Initialize in case we can't get them from prefs??? +var defaultTextColor = "#000000"; +var defaultLinkColor = "#000099"; +var defaultActiveColor = "#000099"; +var defaultVisitedColor = "#990099"; +var defaultBackgroundColor = "#FFFFFF"; +const styleStr = "style"; +const textStr = "text"; +const linkStr = "link"; +const vlinkStr = "vlink"; +const alinkStr = "alink"; +const bgcolorStr = "bgcolor"; +const backgroundStr = "background"; +const cssColorStr = "color"; +const cssBackgroundColorStr = "background-color"; +const cssBackgroundImageStr = "background-image"; +const colorStyle = cssColorStr + ": "; +const backColorStyle = cssBackgroundColorStr + ": "; +const backImageStyle = "; " + cssBackgroundImageStr + ": url("; + +var customTextColor; +var customLinkColor; +var customActiveColor; +var customVisitedColor; +var customBackgroundColor; +var previewBGColor; + +// dialog initialization code +function Startup() { + var editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + + gDialog.ColorPreview = document.getElementById("ColorPreview"); + gDialog.NormalText = document.getElementById("NormalText"); + gDialog.LinkText = document.getElementById("LinkText"); + gDialog.ActiveLinkText = document.getElementById("ActiveLinkText"); + gDialog.VisitedLinkText = document.getElementById("VisitedLinkText"); + gDialog.PageColorGroup = document.getElementById("PageColorGroup"); + gDialog.DefaultColorsRadio = document.getElementById("DefaultColorsRadio"); + gDialog.CustomColorsRadio = document.getElementById("CustomColorsRadio"); + gDialog.BackgroundImageInput = document.getElementById( + "BackgroundImageInput" + ); + + try { + gBodyElement = editor.rootElement; + } catch (e) {} + + if (!gBodyElement) { + dump("Failed to get BODY element!\n"); + window.close(); + } + + // Set element we will edit + globalElement = gBodyElement.cloneNode(false); + + // Initialize default colors from browser prefs + var browserColors = GetDefaultBrowserColors(); + if (browserColors) { + // Use author's browser pref colors passed into dialog + defaultTextColor = browserColors.TextColor; + defaultLinkColor = browserColors.LinkColor; + defaultActiveColor = browserColors.ActiveLinkColor; + defaultVisitedColor = browserColors.VisitedLinkColor; + defaultBackgroundColor = browserColors.BackgroundColor; + } + + // We only need to test for this once per dialog load + gHaveDocumentUrl = GetDocumentBaseUrl(); + + InitDialog(); + + gDialog.PageColorGroup.focus(); + + SetWindowLocation(); +} + +function InitDialog() { + // Get image from document + gBackgroundImage = GetHTMLOrCSSStyleValue( + globalElement, + backgroundStr, + cssBackgroundImageStr + ); + if (/url\((.*)\)/.test(gBackgroundImage)) { + gBackgroundImage = RegExp.$1; + } + + if (gBackgroundImage) { + // Shorten data URIs for display. + shortenImageData(gBackgroundImage, gDialog.BackgroundImageInput); + gDialog.ColorPreview.setAttribute( + styleStr, + backImageStyle + gBackgroundImage + ");" + ); + } + + SetRelativeCheckbox(); + + customTextColor = GetHTMLOrCSSStyleValue(globalElement, textStr, cssColorStr); + customTextColor = ConvertRGBColorIntoHEXColor(customTextColor); + customLinkColor = globalElement.getAttribute(linkStr); + customActiveColor = globalElement.getAttribute(alinkStr); + customVisitedColor = globalElement.getAttribute(vlinkStr); + customBackgroundColor = GetHTMLOrCSSStyleValue( + globalElement, + bgcolorStr, + cssBackgroundColorStr + ); + customBackgroundColor = ConvertRGBColorIntoHEXColor(customBackgroundColor); + + var haveCustomColor = + customTextColor || + customLinkColor || + customVisitedColor || + customActiveColor || + customBackgroundColor; + + // Set default color explicitly for any that are missing + // PROBLEM: We are using "windowtext" and "window" for the Windows OS + // default color values. This works with CSS in preview window, + // but we should NOT use these as values for HTML attributes! + + if (!customTextColor) { + customTextColor = defaultTextColor; + } + if (!customLinkColor) { + customLinkColor = defaultLinkColor; + } + if (!customActiveColor) { + customActiveColor = defaultActiveColor; + } + if (!customVisitedColor) { + customVisitedColor = defaultVisitedColor; + } + if (!customBackgroundColor) { + customBackgroundColor = defaultBackgroundColor; + } + + if (haveCustomColor) { + // If any colors are set, then check the "Custom" radio button + gDialog.PageColorGroup.selectedItem = gDialog.CustomColorsRadio; + UseCustomColors(); + } else { + gDialog.PageColorGroup.selectedItem = gDialog.DefaultColorsRadio; + UseDefaultColors(); + } +} + +function GetColorAndUpdate(ColorWellID) { + // Only allow selecting when in custom mode + if (!gDialog.CustomColorsRadio.selected) { + return; + } + + var colorWell = document.getElementById(ColorWellID); + if (!colorWell) { + return; + } + + // Don't allow a blank color, i.e., using the "default" + var colorObj = { + NoDefault: true, + Type: "", + TextColor: 0, + PageColor: 0, + Cancel: false, + }; + + switch (ColorWellID) { + case "textCW": + colorObj.Type = "Text"; + colorObj.TextColor = customTextColor; + break; + case "linkCW": + colorObj.Type = "Link"; + colorObj.TextColor = customLinkColor; + break; + case "activeCW": + colorObj.Type = "ActiveLink"; + colorObj.TextColor = customActiveColor; + break; + case "visitedCW": + colorObj.Type = "VisitedLink"; + colorObj.TextColor = customVisitedColor; + break; + case "backgroundCW": + colorObj.Type = "Page"; + colorObj.PageColor = customBackgroundColor; + break; + } + + window.openDialog( + "chrome://messenger/content/messengercompose/EdColorPicker.xhtml", + "_blank", + "chrome,close,titlebar,modal", + "", + colorObj + ); + + // User canceled the dialog + if (colorObj.Cancel) { + return; + } + + var color = ""; + switch (ColorWellID) { + case "textCW": + color = customTextColor = colorObj.TextColor; + break; + case "linkCW": + color = customLinkColor = colorObj.TextColor; + break; + case "activeCW": + color = customActiveColor = colorObj.TextColor; + break; + case "visitedCW": + color = customVisitedColor = colorObj.TextColor; + break; + case "backgroundCW": + color = customBackgroundColor = colorObj.BackgroundColor; + break; + } + + setColorWell(ColorWellID, color); + SetColorPreview(ColorWellID, color); +} + +function SetColorPreview(ColorWellID, color) { + switch (ColorWellID) { + case "textCW": + gDialog.NormalText.setAttribute(styleStr, colorStyle + color); + break; + case "linkCW": + gDialog.LinkText.setAttribute(styleStr, colorStyle + color); + break; + case "activeCW": + gDialog.ActiveLinkText.setAttribute(styleStr, colorStyle + color); + break; + case "visitedCW": + gDialog.VisitedLinkText.setAttribute(styleStr, colorStyle + color); + break; + case "backgroundCW": + // Must combine background color and image style values + var styleValue = backColorStyle + color; + if (gBackgroundImage) { + styleValue += ";" + backImageStyle + gBackgroundImage + ");"; + } + + gDialog.ColorPreview.setAttribute(styleStr, styleValue); + previewBGColor = color; + break; + } +} + +function UseCustomColors() { + SetElementEnabledById("TextButton", true); + SetElementEnabledById("LinkButton", true); + SetElementEnabledById("ActiveLinkButton", true); + SetElementEnabledById("VisitedLinkButton", true); + SetElementEnabledById("BackgroundButton", true); + SetElementEnabledById("Text", true); + SetElementEnabledById("Link", true); + SetElementEnabledById("Active", true); + SetElementEnabledById("Visited", true); + SetElementEnabledById("Background", true); + + SetColorPreview("textCW", customTextColor); + SetColorPreview("linkCW", customLinkColor); + SetColorPreview("activeCW", customActiveColor); + SetColorPreview("visitedCW", customVisitedColor); + SetColorPreview("backgroundCW", customBackgroundColor); + + setColorWell("textCW", customTextColor); + setColorWell("linkCW", customLinkColor); + setColorWell("activeCW", customActiveColor); + setColorWell("visitedCW", customVisitedColor); + setColorWell("backgroundCW", customBackgroundColor); +} + +function UseDefaultColors() { + SetColorPreview("textCW", defaultTextColor); + SetColorPreview("linkCW", defaultLinkColor); + SetColorPreview("activeCW", defaultActiveColor); + SetColorPreview("visitedCW", defaultVisitedColor); + SetColorPreview("backgroundCW", defaultBackgroundColor); + + // Setting to blank color will remove color from buttons, + setColorWell("textCW", ""); + setColorWell("linkCW", ""); + setColorWell("activeCW", ""); + setColorWell("visitedCW", ""); + setColorWell("backgroundCW", ""); + + // Disable color buttons and labels + SetElementEnabledById("TextButton", false); + SetElementEnabledById("LinkButton", false); + SetElementEnabledById("ActiveLinkButton", false); + SetElementEnabledById("VisitedLinkButton", false); + SetElementEnabledById("BackgroundButton", false); + SetElementEnabledById("Text", false); + SetElementEnabledById("Link", false); + SetElementEnabledById("Active", false); + SetElementEnabledById("Visited", false); + SetElementEnabledById("Background", false); +} + +function chooseFile() { + // Get a local image file, converted into URL format + GetLocalFileURL("img").then(fileURL => { + // Always try to relativize local file URLs + if (gHaveDocumentUrl) { + fileURL = MakeRelativeUrl(fileURL); + } + + gDialog.BackgroundImageInput.value = fileURL; + + SetRelativeCheckbox(); + ValidateAndPreviewImage(true); + SetTextboxFocus(gDialog.BackgroundImageInput); + }); +} + +function ChangeBackgroundImage() { + // Don't show error message for image while user is typing + ValidateAndPreviewImage(false); + SetRelativeCheckbox(); +} + +function ValidateAndPreviewImage(ShowErrorMessage) { + // First make a string with just background color + var styleValue = backColorStyle + previewBGColor + ";"; + + var retVal = true; + var image = TrimString(gDialog.BackgroundImageInput.value); + if (image) { + if (isImageDataShortened(image)) { + gBackgroundImage = restoredImageData(gDialog.BackgroundImageInput); + } else { + gBackgroundImage = image; + + // Display must use absolute URL if possible + var displayImage = gHaveDocumentUrl ? MakeAbsoluteUrl(image) : image; + styleValue += backImageStyle + displayImage + ");"; + } + } else { + gBackgroundImage = null; + } + + // Set style on preview (removes image if not valid) + gDialog.ColorPreview.setAttribute(styleStr, styleValue); + + // Note that an "empty" string is valid + return retVal; +} + +function ValidateData() { + var editor = GetCurrentEditor(); + try { + // Colors values are updated as they are picked, no validation necessary + if (gDialog.DefaultColorsRadio.selected) { + editor.removeAttributeOrEquivalent(globalElement, textStr, true); + globalElement.removeAttribute(linkStr); + globalElement.removeAttribute(vlinkStr); + globalElement.removeAttribute(alinkStr); + editor.removeAttributeOrEquivalent(globalElement, bgcolorStr, true); + } else { + // Do NOT accept the CSS "WindowsOS" color strings! + // Problem: We really should try to get the actual color values + // from windows, but I don't know how to do that! + var tmpColor = customTextColor.toLowerCase(); + if (tmpColor != "windowtext") { + editor.setAttributeOrEquivalent( + globalElement, + textStr, + customTextColor, + true + ); + } else { + editor.removeAttributeOrEquivalent(globalElement, textStr, true); + } + + tmpColor = customBackgroundColor.toLowerCase(); + if (tmpColor != "window") { + editor.setAttributeOrEquivalent( + globalElement, + bgcolorStr, + customBackgroundColor, + true + ); + } else { + editor.removeAttributeOrEquivalent(globalElement, bgcolorStr, true); + } + + globalElement.setAttribute(linkStr, customLinkColor); + globalElement.setAttribute(vlinkStr, customVisitedColor); + globalElement.setAttribute(alinkStr, customActiveColor); + } + + if (ValidateAndPreviewImage(true)) { + // A valid image may be null for no image + if (gBackgroundImage) { + globalElement.setAttribute(backgroundStr, gBackgroundImage); + } else { + editor.removeAttributeOrEquivalent(globalElement, backgroundStr, true); + } + + return true; + } + } catch (e) {} + return false; +} + +function onAccept(event) { + // If it's a file, convert to a data URL. + if (gBackgroundImage && /^file:/i.test(gBackgroundImage)) { + let nsFile = Services.io + .newURI(gBackgroundImage) + .QueryInterface(Ci.nsIFileURL).file; + if (nsFile.exists()) { + let reader = new FileReader(); + reader.addEventListener("load", function () { + gBackgroundImage = reader.result; + gDialog.BackgroundImageInput.value = reader.result; + if (onAccept(event)) { + window.close(); + } + }); + File.createFromNsIFile(nsFile).then(file => { + reader.readAsDataURL(file); + }); + event.preventDefault(); // Don't close just yet... + return false; + } + } + if (ValidateData()) { + // Copy attributes to element we are changing + try { + GetCurrentEditor().cloneAttributes(gBodyElement, globalElement); + } catch (e) {} + + SaveWindowLocation(); + return true; // do close the window + } + event.preventDefault(); + return false; +} diff --git a/comm/mail/components/compose/content/dialogs/EdColorProps.xhtml b/comm/mail/components/compose/content/dialogs/EdColorProps.xhtml new file mode 100644 index 0000000000..633b1639d9 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdColorProps.xhtml @@ -0,0 +1,211 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % edColorPropertiesDTD SYSTEM "chrome://messenger/locale/messengercompose/EditorColorProperties.dtd"> +%edColorPropertiesDTD; +<!ENTITY % composeEditorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/mailComposeEditorOverlay.dtd"> +%composeEditorOverlayDTD; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" +> + <dialog> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdColorProps.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <html:fieldset align="start"> + <html:legend>&pageColors.label;</html:legend> + <radiogroup id="PageColorGroup"> + <radio + id="DefaultColorsRadio" + label="&defaultColorsRadio.label;" + oncommand="UseDefaultColors()" + accesskey="&defaultColorsRadio.accessKey;" + tooltiptext="&defaultColorsRadio.tooltip;" + /> + <radio + id="CustomColorsRadio" + label="&customColorsRadio.label;" + oncommand="UseCustomColors()" + accesskey="&customColorsRadio.accessKey;" + tooltiptext="&customColorsRadio.tooltip;" + /> + </radiogroup> + <hbox class="indent"> + <hbox> + <vbox> + <hbox flex="1" align="center"> + <label + id="Text" + control="TextButton" + value="&normalText.label;&colon.character;" + accesskey="&normalText.accessKey;" + /> + </hbox> + <hbox flex="1" align="center"> + <label + id="Link" + flex="1" + control="LinkButton" + value="&linkText.label;&colon.character;" + accesskey="&linkText.accessKey;" + /> + </hbox> + <hbox flex="1" align="center"> + <label + id="Active" + flex="1" + control="ActiveLinkButton" + value="&activeLinkText.label;&colon.character;" + accesskey="&activeLinkText.accessKey;" + /> + </hbox> + <hbox flex="1" align="center"> + <label + id="Visited" + flex="1" + control="VisitedLinkButton" + value="&visitedLinkText.label;&colon.character;" + accesskey="&visitedLinkText.accessKey;" + /> + </hbox> + <hbox flex="1" align="center"> + <label + id="Background" + flex="1" + control="BackgroundButton" + value="&background.label;" + accesskey="&background.accessKey;" + /> + </hbox> + </vbox> + <vbox> + <button + id="TextButton" + class="color-button" + oncommand="GetColorAndUpdate('textCW');" + > + <spacer id="textCW" class="color-well" /> + </button> + <button + id="LinkButton" + class="color-button" + oncommand="GetColorAndUpdate('linkCW');" + > + <spacer id="linkCW" class="color-well" /> + </button> + <button + id="ActiveLinkButton" + class="color-button" + oncommand="GetColorAndUpdate('activeCW');" + > + <spacer id="activeCW" class="color-well" /> + </button> + <button + id="VisitedLinkButton" + class="color-button" + oncommand="GetColorAndUpdate('visitedCW');" + > + <spacer id="visitedCW" class="color-well" /> + </button> + <button + id="BackgroundButton" + class="color-button" + oncommand="GetColorAndUpdate('backgroundCW');" + > + <spacer id="backgroundCW" class="color-well" /> + </button> + </vbox> + </hbox> + <vbox id="ColorPreview"> + <spacer flex="1" /> + <label class="larger" id="NormalText" value="&normalText.label;" /> + <spacer flex="1" /> + <label class="larger" id="LinkText" value="&linkText.label;" /> + <spacer flex="1" /> + <label + class="larger" + id="ActiveLinkText" + value="&activeLinkText.label;" + /> + <spacer flex="1" /> + <label + class="larger" + id="VisitedLinkText" + value="&visitedLinkText.label;" + /> + <spacer flex="1" /> + </vbox> + <spacer flex="1" /> + </hbox> + <spacer class="spacer" /> + </html:fieldset> + <spacer class="spacer" /> + <label + control="BackgroundImageInput" + value="&backgroundImage.label;" + tooltiptext="&backgroundImage.tooltip;" + accesskey="&backgroundImage.accessKey;" + /> + <tooltip id="shortenedDataURI"> + <label value="&backgroundImage.shortenedDataURI;" /> + </tooltip> + <html:input + id="BackgroundImageInput" + type="text" + class="uri-element input-inline" + onchange="ChangeBackgroundImage()" + aria-label="&backgroundImage.tooltip;" + /> + <hbox align="center"> + <checkbox + id="MakeRelativeCheckbox" + for="BackgroundImageInput" + label="&makeUrlRelative.label;" + accesskey="&makeUrlRelative.accessKey;" + oncommand="MakeInputValueRelativeOrAbsolute(this);" + tooltiptext="&makeUrlRelative.tooltip;" + /> + <spacer flex="1" /> + <button + id="ChooseFile" + oncommand="chooseFile()" + label="&chooseFileButton.label;" + accesskey="&chooseFileButton.accessKey;" + /> + </hbox> + <spacer class="smallspacer" /> + <hbox> + <spacer flex="1" /> + <button + id="AdvancedEditButton" + oncommand="onAdvancedEdit();" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <separator class="groove" /> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdConvertToTable.js b/comm/mail/components/compose/content/dialogs/EdConvertToTable.js new file mode 100644 index 0000000000..e7f19cff67 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdConvertToTable.js @@ -0,0 +1,325 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +var gIndex; +var gCommaIndex = "0"; +var gSpaceIndex = "1"; +var gOtherIndex = "2"; + +// dialog initialization code +function Startup() { + if (!GetCurrentEditor()) { + window.close(); + return; + } + + gDialog.sepRadioGroup = document.getElementById("SepRadioGroup"); + gDialog.sepCharacterInput = document.getElementById("SepCharacterInput"); + gDialog.deleteSepCharacter = document.getElementById("DeleteSepCharacter"); + gDialog.collapseSpaces = document.getElementById("CollapseSpaces"); + + // We persist the user's separator character + gDialog.sepCharacterInput.value = + gDialog.sepRadioGroup.getAttribute("character"); + + gIndex = gDialog.sepRadioGroup.getAttribute("index"); + + switch (gIndex) { + case gCommaIndex: + default: + gDialog.sepRadioGroup.selectedItem = document.getElementById("comma"); + break; + case gSpaceIndex: + gDialog.sepRadioGroup.selectedItem = document.getElementById("space"); + break; + case gOtherIndex: + gDialog.sepRadioGroup.selectedItem = document.getElementById("other"); + break; + } + + // Set initial enable state on character input and "collapse" checkbox + SelectCharacter(gIndex); + + SetWindowLocation(); +} + +function InputSepCharacter() { + var str = gDialog.sepCharacterInput.value; + + // Limit input to 1 character + if (str.length > 1) { + str = str.slice(0, 1); + } + + // We can never allow tag or entity delimiters for separator character + if (str == "<" || str == ">" || str == "&" || str == ";" || str == " ") { + str = ""; + } + + gDialog.sepCharacterInput.value = str; +} + +function SelectCharacter(radioGroupIndex) { + gIndex = radioGroupIndex; + SetElementEnabledById("SepCharacterInput", gIndex == gOtherIndex); + SetElementEnabledById("CollapseSpaces", gIndex == gSpaceIndex); +} + +/* eslint-disable complexity */ +function onAccept() { + var sepCharacter = ""; + switch (gIndex) { + case gCommaIndex: + sepCharacter = ","; + break; + case gSpaceIndex: + sepCharacter = " "; + break; + case gOtherIndex: + sepCharacter = gDialog.sepCharacterInput.value.slice(0, 1); + break; + } + + var editor = GetCurrentEditor(); + var str; + try { + str = editor.outputToString( + "text/html", + kOutputLFLineBreak | kOutputSelectionOnly + ); + } catch (e) {} + if (!str) { + SaveWindowLocation(); + return; + } + + // Replace nbsp with spaces: + str = str.replace(/\u00a0/g, " "); + + // Strip out </p> completely + str = str.replace(/\s*<\/p>\s*/g, ""); + + // Trim whitespace adjacent to <p> and <br> tags + // and replace <p> with <br> + // (which will be replaced with </tr> below) + str = str.replace(/\s*<p>\s*|\s*<br>\s*/g, "<br>"); + + // Trim leading <br>s + str = str.replace(/^(<br>)+/, ""); + + // Trim trailing <br>s + str = str.replace(/(<br>)+$/, ""); + + // Reduce multiple internal <br> to just 1 + // TODO: Maybe add a checkbox to let user decide + // str = str.replace(/(<br>)+/g, "<br>"); + + // Trim leading and trailing spaces + str = str.trim(); + + // Remove all tag contents so we don't replace + // separator character within tags + // Also converts lists to something useful + var stack = []; + var start; + var end; + var searchStart = 0; + var listSeparator = ""; + var listItemSeparator = ""; + var endList = false; + + do { + start = str.indexOf("<", searchStart); + + if (start >= 0) { + end = str.indexOf(">", start + 1); + if (end > start) { + let tagContent = str.slice(start + 1, end).trim(); + + if (/^ol|^ul|^dl/.test(tagContent)) { + // Replace list tag with <BR> to start new row + // at beginning of second or greater list tag + str = str.slice(0, start) + listSeparator + str.slice(end + 1); + if (listSeparator == "") { + listSeparator = "<br>"; + } + + // Reset for list item separation into cells + listItemSeparator = ""; + } else if (/^li|^dt|^dd/.test(tagContent)) { + // Start a new row if this is first item after the ending the last list + if (endList) { + listItemSeparator = "<br>"; + } + + // Start new cell at beginning of second or greater list items + str = str.slice(0, start) + listItemSeparator + str.slice(end + 1); + + if (endList || listItemSeparator == "") { + listItemSeparator = sepCharacter; + } + + endList = false; + } else { + // Find end tags + endList = /^\/ol|^\/ul|^\/dl/.test(tagContent); + if (endList || /^\/li|^\/dt|^\/dd/.test(tagContent)) { + // Strip out tag + str = str.slice(0, start) + str.slice(end + 1); + } else { + // Not a list-related tag: Store tag contents in an array + stack.push(tagContent); + + // Keep the "<" and ">" while removing from source string + start++; + str = str.slice(0, start) + str.slice(end); + } + } + } + searchStart = start + 1; + } + } while (start >= 0); + + // Replace separator characters with table cells + var replaceString; + if (gDialog.deleteSepCharacter.checked) { + replaceString = ""; + } else { + // Don't delete separator character, + // so include it at start of string to replace + replaceString = sepCharacter; + } + + replaceString += "<td>"; + + if (sepCharacter.length > 0) { + var tempStr = sepCharacter; + var regExpChars = ".!@#$%^&*-+[]{}()|\\/"; + if (regExpChars.includes(sepCharacter)) { + tempStr = "\\" + sepCharacter; + } + + if (gIndex == gSpaceIndex) { + // If checkbox is checked, + // one or more adjacent spaces are one separator + if (gDialog.collapseSpaces.checked) { + tempStr = "\\s+"; + } else { + tempStr = "\\s"; + } + } + var pattern = new RegExp(tempStr, "g"); + str = str.replace(pattern, replaceString); + } + + // Put back tag contents that we removed above + searchStart = 0; + var stackIndex = 0; + do { + start = str.indexOf("<", searchStart); + end = start + 1; + if (start >= 0 && str.charAt(end) == ">") { + // We really need a FIFO stack! + str = str.slice(0, end) + stack[stackIndex++] + str.slice(end); + } + searchStart = end; + } while (start >= 0); + + // End table row and start another for each br or p + str = str.replace(/\s*<br>\s*/g, "</tr>\n<tr><td>"); + + // Add the table tags and the opening and closing tr/td tags + // Default table attributes should be same as those used in nsHTMLEditor::CreateElementWithDefaults() + // (Default width="100%" is used in EdInsertTable.js) + str = + '<table border="1" width="100%" cellpadding="2" cellspacing="2">\n<tr><td>' + + str + + "</tr>\n</table>\n"; + + editor.beginTransaction(); + + // Delete the selection -- makes it easier to find where table will insert + var nodeBeforeTable = null; + var nodeAfterTable = null; + try { + editor.deleteSelection(editor.eNone, editor.eStrip); + + var anchorNodeBeforeInsert = editor.selection.anchorNode; + var offset = editor.selection.anchorOffset; + if (anchorNodeBeforeInsert.nodeType == Node.TEXT_NODE) { + // Text was split. Table should be right after the first or before + nodeBeforeTable = anchorNodeBeforeInsert.previousSibling; + nodeAfterTable = anchorNodeBeforeInsert; + } else { + // Table should be inserted right after node pointed to by selection + if (offset > 0) { + nodeBeforeTable = anchorNodeBeforeInsert.childNodes.item(offset - 1); + } + + nodeAfterTable = anchorNodeBeforeInsert.childNodes.item(offset); + } + + editor.insertHTML(str); + } catch (e) {} + + var table = null; + if (nodeAfterTable) { + var previous = nodeAfterTable.previousSibling; + if (previous && previous.nodeName.toLowerCase() == "table") { + table = previous; + } + } + if (!table && nodeBeforeTable) { + var next = nodeBeforeTable.nextSibling; + if (next && next.nodeName.toLowerCase() == "table") { + table = next; + } + } + + if (table) { + // Fixup table only if pref is set + var firstRow; + try { + if (Services.prefs.getBoolPref("editor.table.maintain_structure")) { + editor.normalizeTable(table); + } + + firstRow = editor.getFirstRow(table); + } catch (e) {} + + // Put caret in first cell + if (firstRow) { + var node2 = firstRow.firstChild; + do { + if ( + node2.nodeName.toLowerCase() == "td" || + node2.nodeName.toLowerCase() == "th" + ) { + try { + editor.selection.collapse(node2, 0); + } catch (e) {} + break; + } + node2 = node2.nextSibling; + } while (node2); + } + } + + editor.endTransaction(); + + // Save persisted attributes + gDialog.sepRadioGroup.setAttribute("index", gIndex); + if (gIndex == gOtherIndex) { + gDialog.sepRadioGroup.setAttribute("character", sepCharacter); + } + + SaveWindowLocation(); +} +/* eslint-enable complexity */ diff --git a/comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml b/comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml new file mode 100644 index 0000000000..6f2d9ad5b1 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EdConvertToTable.dtd"> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="Startup()" + lightweightthemes="true" + style="min-width: 20em" +> + <dialog> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <!--- Element-specific methods --> + <script src="chrome://messenger/content/messengercompose/EdConvertToTable.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + <description class="wrap" flex="1">&instructions1.label;</description> + <description class="wrap" flex="1">&instructions2.label;</description> + <radiogroup + id="SepRadioGroup" + persist="index character" + index="0" + character="" + > + <radio + id="comma" + label="&commaRadio.label;" + oncommand="SelectCharacter('0');" + /> + <radio + id="space" + label="&spaceRadio.label;" + oncommand="SelectCharacter('1');" + /> + <hbox> + <spacer class="radio-spacer" /> + <checkbox + id="CollapseSpaces" + label="&collapseSpaces.label;" + checked="true" + persist="checked" + tooltiptext="&collapseSpaces.tooltip;" + /> + </hbox> + <hbox align="center"> + <radio + id="other" + label="&otherRadio.label;" + oncommand="SelectCharacter('2');" + /> + <html:input + id="SepCharacterInput" + type="text" + aria-labelledby="other" + class="narrow input-inline" + oninput="InputSepCharacter()" + /> + </hbox> + </radiogroup> + <spacer class="spacer" /> + <checkbox + id="DeleteSepCharacter" + label="&deleteCharCheck.label;" + persist="checked" + /> + <spacer class="spacer" /> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdDialogCommon.js b/comm/mail/components/compose/content/dialogs/EdDialogCommon.js new file mode 100644 index 0000000000..ce377e4bbf --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdDialogCommon.js @@ -0,0 +1,679 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Each editor window must include this file + +/* import-globals-from ../editorUtilities.js */ +/* globals InitDialog, ChangeLinkLocation, ValidateData */ + +// Object to attach commonly-used widgets (all dialogs should use this) +var gDialog = {}; + +var gHaveDocumentUrl = false; +var gValidationError = false; + +// Use for 'defaultIndex' param in InitPixelOrPercentMenulist +const gPixel = 0; +const gPercent = 1; + +const gMaxPixels = 100000; // Used for image size, borders, spacing, and padding +// Gecko code uses 1000 for maximum rowspan, colspan +// Also, editing performance is really bad above this +const gMaxRows = 1000; +const gMaxColumns = 1000; +const gMaxTableSize = 1000000; // Width or height of table or cells + +// A XUL element with id="location" for managing +// dialog location relative to parent window +var gLocation; + +// The element being edited - so AdvancedEdit can have access to it +var globalElement; + +/* Validate contents of an input field + * + * inputWidget The 'input' element for the the attribute's value + * listWidget The 'menulist' XUL element for choosing "pixel" or "percent" + * May be null when no pixel/percent is used. + * minVal minimum allowed for input widget's value + * maxVal maximum allowed for input widget's value + * (when "listWidget" is used, maxVal is used for "pixel" maximum, + * 100% is assumed if "percent" is the user's choice) + * element The DOM element that we set the attribute on. May be null. + * attName Name of the attribute to set. May be null or ignored if "element" is null + * mustHaveValue If true, error dialog is displayed if "value" is empty string + * + * This calls "ValidateNumberRange()", which puts up an error dialog to inform the user. + * If error, we also: + * Shift focus and select contents of the inputWidget, + * Switch to appropriate panel of tabbed dialog if user implements "SwitchToValidate()", + * and/or will expand the dialog to full size if "More / Fewer" feature is implemented + * + * Returns the "value" as a string, or "" if error or input contents are empty + * The global "gValidationError" variable is set true if error was found + */ +function ValidateNumber( + inputWidget, + listWidget, + minVal, + maxVal, + element, + attName, + mustHaveValue +) { + if (!inputWidget) { + gValidationError = true; + return ""; + } + + // Global error return value + gValidationError = false; + var maxLimit = maxVal; + var isPercent = false; + + var numString = TrimString(inputWidget.value); + if (numString || mustHaveValue) { + if (listWidget) { + isPercent = listWidget.selectedIndex == 1; + } + if (isPercent) { + maxLimit = 100; + } + + // This method puts up the error message + numString = ValidateNumberRange(numString, minVal, maxLimit, mustHaveValue); + if (!numString) { + // Switch to appropriate panel for error reporting + SwitchToValidatePanel(); + + // Error - shift to offending input widget + SetTextboxFocus(inputWidget); + gValidationError = true; + } else { + if (isPercent) { + numString += "%"; + } + if (element) { + GetCurrentEditor().setAttributeOrEquivalent( + element, + attName, + numString, + true + ); + } + } + } else if (element) { + GetCurrentEditor().removeAttributeOrEquivalent(element, attName, true); + } + return numString; +} + +/* Validate contents of an input field + * + * value number to validate + * minVal minimum allowed for input widget's value + * maxVal maximum allowed for input widget's value + * (when "listWidget" is used, maxVal is used for "pixel" maximum, + * 100% is assumed if "percent" is the user's choice) + * mustHaveValue If true, error dialog is displayed if "value" is empty string + * + * If inputWidget's value is outside of range, or is empty when "mustHaveValue" = true, + * an error dialog is popuped up to inform the user. The focus is shifted + * to the inputWidget. + * + * Returns the "value" as a string, or "" if error or input contents are empty + * The global "gValidationError" variable is set true if error was found + */ +function ValidateNumberRange(value, minValue, maxValue, mustHaveValue) { + // Initialize global error flag + gValidationError = false; + value = TrimString(String(value)); + + // We don't show error for empty string unless caller wants to + if (!value && !mustHaveValue) { + return ""; + } + + var numberStr = ""; + + if (value.length > 0) { + // Extract just numeric characters + var number = Number(value.replace(/\D+/g, "")); + if (number >= minValue && number <= maxValue) { + // Return string version of the number + return String(number); + } + numberStr = String(number); + } + + var message = ""; + + if (numberStr.length > 0) { + // We have a number from user outside of allowed range + message = GetString("ValidateRangeMsg"); + message = message.replace(/%n%/, numberStr); + message += "\n "; + } + message += GetString("ValidateNumberMsg"); + + // Replace variable placeholders in message with number values + message = message.replace(/%min%/, minValue).replace(/%max%/, maxValue); + ShowInputErrorMessage(message); + + // Return an empty string to indicate error + gValidationError = true; + return ""; +} + +function SetTextboxFocusById(id) { + SetTextboxFocus(document.getElementById(id)); +} + +function SetTextboxFocus(input) { + if (input) { + input.focus(); + } +} + +function ShowInputErrorMessage(message) { + Services.prompt.alert(window, GetString("InputError"), message); + window.focus(); +} + +// Get the text appropriate to parent container +// to determine what a "%" value is referring to. +// elementForAtt is element we are actually setting attributes on +// (a temporary copy of element in the doc to allow canceling), +// but elementInDoc is needed to find parent context in document +function GetAppropriatePercentString(elementForAtt, elementInDoc) { + var editor = GetCurrentEditor(); + try { + var name = elementForAtt.nodeName.toLowerCase(); + if (name == "td" || name == "th") { + return GetString("PercentOfTable"); + } + + // Check if element is within a table cell + if (editor.getElementOrParentByTagName("td", elementInDoc)) { + return GetString("PercentOfCell"); + } + return GetString("PercentOfWindow"); + } catch (e) { + return ""; + } +} + +function ClearListbox(listbox) { + if (listbox) { + listbox.clearSelection(); + while (listbox.hasChildNodes()) { + listbox.lastChild.remove(); + } + } +} + +function forceInteger(elementID) { + var editField = document.getElementById(elementID); + if (!editField) { + return; + } + + var stringIn = editField.value; + if (stringIn && stringIn.length > 0) { + // Strip out all nonnumeric characters + stringIn = stringIn.replace(/\D+/g, ""); + if (!stringIn) { + stringIn = ""; + } + + // Write back only if changed + if (stringIn != editField.value) { + editField.value = stringIn; + } + } +} + +function InitPixelOrPercentMenulist( + elementForAtt, + elementInDoc, + attribute, + menulistID, + defaultIndex +) { + if (!defaultIndex) { + defaultIndex = gPixel; + } + + // var size = elementForAtt.getAttribute(attribute); + var size = GetHTMLOrCSSStyleValue(elementForAtt, attribute, attribute); + var menulist = document.getElementById(menulistID); + var pixelItem; + var percentItem; + + if (!menulist) { + dump("NO MENULIST found for ID=" + menulistID + "\n"); + return size; + } + + menulist.removeAllItems(); + pixelItem = menulist.appendItem(GetString("Pixels")); + + if (!pixelItem) { + return 0; + } + + percentItem = menulist.appendItem( + GetAppropriatePercentString(elementForAtt, elementInDoc) + ); + if (size && size.length > 0) { + // Search for a "%" or "px" + if (size.includes("%")) { + // Strip out the % + size = size.substr(0, size.indexOf("%")); + if (percentItem) { + menulist.selectedItem = percentItem; + } + } else { + if (size.includes("px")) { + // Strip out the px + size = size.substr(0, size.indexOf("px")); + } + menulist.selectedItem = pixelItem; + } + } else { + menulist.selectedIndex = defaultIndex; + } + + return size; +} + +function onAdvancedEdit() { + // First validate data from widgets in the "simpler" property dialog + if (ValidateData()) { + // Set true if OK is clicked in the Advanced Edit dialog + window.AdvancedEditOK = false; + // Open the AdvancedEdit dialog, passing in the element to be edited + // (the copy named "globalElement") + window.openDialog( + "chrome://messenger/content/messengercompose/EdAdvancedEdit.xhtml", + "_blank", + "chrome,close,titlebar,modal,resizable=yes", + "", + globalElement + ); + window.focus(); + if (window.AdvancedEditOK) { + // Copy edited attributes to the dialog widgets: + InitDialog(); + } + } +} + +function getColor(ColorPickerID) { + var colorPicker = document.getElementById(ColorPickerID); + var color; + if (colorPicker) { + // Extract color from colorPicker and assign to colorWell. + color = colorPicker.getAttribute("color"); + if (color && color == "") { + return null; + } + // Clear color so next if it's called again before + // color picker is actually used, we dedect the "don't set color" state + colorPicker.setAttribute("color", ""); + } + + return color; +} + +function setColorWell(ColorWellID, color) { + var colorWell = document.getElementById(ColorWellID); + if (colorWell) { + if (!color || color == "") { + // Don't set color (use default) + // Trigger change to not show color swatch + colorWell.setAttribute("default", "true"); + // Style in CSS sets "background-color", + // but color won't clear unless we do this: + colorWell.removeAttribute("style"); + } else { + colorWell.removeAttribute("default"); + // Use setAttribute so colorwell can be a XUL element, such as button + colorWell.setAttribute("style", "background-color:" + color); + } + } +} + +function SwitchToValidatePanel() { + // no default implementation + // Only EdTableProps.js currently implements this +} + +/** + * @returns {Promise} URL spec of the file chosen, or null + */ +function GetLocalFileURL(filterType) { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + var fileType = "html"; + + if (filterType == "img") { + fp.init(window, GetString("SelectImageFile"), Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterImages); + fileType = "image"; + } else if (filterType.startsWith("html")) { + // Current usage of this is in Link dialog, + // where we always want HTML first + fp.init(window, GetString("OpenHTMLFile"), Ci.nsIFilePicker.modeOpen); + + // When loading into Composer, direct user to prefer HTML files and text files, + // so we call separately to control the order of the filter list + fp.appendFilters(Ci.nsIFilePicker.filterHTML); + fp.appendFilters(Ci.nsIFilePicker.filterText); + + // Link dialog also allows linking to images + if (filterType.includes("img", 1)) { + fp.appendFilters(Ci.nsIFilePicker.filterImages); + } + } + // Default or last filter is "All Files" + fp.appendFilters(Ci.nsIFilePicker.filterAll); + + // set the file picker's current directory to last-opened location saved in prefs + SetFilePickerDirectory(fp, fileType); + + return new Promise(resolve => { + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK || !fp.file) { + resolve(null); + return; + } + SaveFilePickerDirectory(fp, fileType); + resolve(fp.fileURL.spec); + }); + }); +} + +function SetWindowLocation() { + gLocation = document.getElementById("location"); + if (gLocation) { + const screenX = Math.max( + 0, + Math.min( + window.opener.screenX + Number(gLocation.getAttribute("offsetX")), + screen.availWidth - window.outerWidth + ) + ); + const screenY = Math.max( + 0, + Math.min( + window.opener.screenY + Number(gLocation.getAttribute("offsetY")), + screen.availHeight - window.outerHeight + ) + ); + window.moveTo(screenX, screenY); + } +} + +function SaveWindowLocation() { + if (gLocation) { + gLocation.setAttribute("offsetX", window.screenX - window.opener.screenX); + gLocation.setAttribute("offsetY", window.screenY - window.opener.screenY); + } +} + +function onCancel() { + SaveWindowLocation(); +} + +function SetRelativeCheckbox(checkbox) { + if (!checkbox) { + checkbox = document.getElementById("MakeRelativeCheckbox"); + if (!checkbox) { + return; + } + } + + var editor = GetCurrentEditor(); + // Mail never allows relative URLs, so hide the checkbox + if (editor && editor.flags & Ci.nsIEditor.eEditorMailMask) { + checkbox.collapsed = true; + return; + } + + var input = document.getElementById(checkbox.getAttribute("for")); + if (!input) { + return; + } + + var url = TrimString(input.value); + var urlScheme = GetScheme(url); + + // Check it if url is relative (no scheme). + checkbox.checked = url.length > 0 && !urlScheme; + + // Now do checkbox enabling: + var enable = false; + + var docUrl = GetDocumentBaseUrl(); + var docScheme = GetScheme(docUrl); + + if (url && docUrl && docScheme) { + if (urlScheme) { + // Url is absolute + // If we can make a relative URL, then enable must be true! + // (this lets the smarts of MakeRelativeUrl do all the hard work) + enable = GetScheme(MakeRelativeUrl(url)).length == 0; + } else if (url[0] == "#") { + // Url is relative + // Check if url is a named anchor + // but document doesn't have a filename + // (it's probably "index.html" or "index.htm", + // but we don't want to allow a malformed URL) + var docFilename = GetFilename(docUrl); + enable = docFilename.length > 0; + } else { + // Any other url is assumed + // to be ok to try to make absolute + enable = true; + } + } + + SetElementEnabled(checkbox, enable); +} + +// oncommand handler for the Relativize checkbox in EditorOverlay.xhtml +function MakeInputValueRelativeOrAbsolute(checkbox) { + var input = document.getElementById(checkbox.getAttribute("for")); + if (!input) { + return; + } + + var docUrl = GetDocumentBaseUrl(); + if (!docUrl) { + // Checkbox should be disabled if not saved, + // but keep this error message in case we change that + Services.prompt.alert(window, "", GetString("SaveToUseRelativeUrl")); + window.focus(); + } else { + // Note that "checked" is opposite of its last state, + // which determines what we want to do here + if (checkbox.checked) { + input.value = MakeRelativeUrl(input.value); + } else { + input.value = MakeAbsoluteUrl(input.value); + } + + // Reset checkbox to reflect url state + SetRelativeCheckbox(checkbox); + } +} + +var IsBlockParent = [ + "applet", + "blockquote", + "body", + "center", + "dd", + "div", + "form", + "li", + "noscript", + "object", + "td", + "th", +]; + +var NotAnInlineParent = [ + "col", + "colgroup", + "dl", + "dir", + "menu", + "ol", + "table", + "tbody", + "tfoot", + "thead", + "tr", + "ul", +]; + +function FillLinkMenulist(linkMenulist, headingsArray) { + var editor = GetCurrentEditor(); + try { + var treeWalker = editor.document.createTreeWalker( + editor.document, + 1, + null, + true + ); + var headingList = []; + var anchorList = []; // for sorting + var anchorMap = {}; // for weeding out duplicates and making heading anchors unique + var anchor; + var i; + for ( + var element = treeWalker.nextNode(); + element; + element = treeWalker.nextNode() + ) { + // grab headings + // Skip headings that already have a named anchor as their first child + // (this may miss nearby anchors, but at least we don't insert another + // under the same heading) + if ( + HTMLHeadingElement.isInstance(element) && + element.textContent && + !( + HTMLAnchorElement.isInstance(element.firstChild) && + element.firstChild.name + ) + ) { + headingList.push(element); + } + + // grab named anchors + if (HTMLAnchorElement.isInstance(element) && element.name) { + anchor = "#" + element.name; + if (!(anchor in anchorMap)) { + anchorList.push({ anchor, sortkey: anchor.toLowerCase() }); + anchorMap[anchor] = true; + } + } + + // grab IDs + if (element.id) { + anchor = "#" + element.id; + if (!(anchor in anchorMap)) { + anchorList.push({ anchor, sortkey: anchor.toLowerCase() }); + anchorMap[anchor] = true; + } + } + } + // add anchor for headings + for (i = 0; i < headingList.length; i++) { + var heading = headingList[i]; + + // Use just first 40 characters, don't add "...", + // and replace whitespace with "_" and strip non-word characters + anchor = + "#" + + ConvertToCDATAString( + TruncateStringAtWordEnd(heading.textContent, 40, false) + ); + + // Append "_" to any name already in the list + while (anchor in anchorMap) { + anchor += "_"; + } + anchorList.push({ anchor, sortkey: anchor.toLowerCase() }); + anchorMap[anchor] = true; + + // Save nodes in an array so we can create anchor node under it later + headingsArray[anchor] = heading; + } + let menuItems = []; + if (anchorList.length) { + // case insensitive sort + anchorList.sort((a, b) => { + if (a.sortkey < b.sortkey) { + return -1; + } + if (a.sortkey > b.sortkey) { + return 1; + } + return 0; + }); + for (i = 0; i < anchorList.length; i++) { + menuItems.push(createMenuItem(anchorList[i].anchor)); + } + } else { + // Don't bother with named anchors in Mail. + if (editor && editor.flags & Ci.nsIEditor.eEditorMailMask) { + linkMenulist.removeAttribute("enablehistory"); + return; + } + let item = createMenuItem(GetString("NoNamedAnchorsOrHeadings")); + item.setAttribute("disabled", "true"); + menuItems.push(item); + } + window.addEventListener("contextmenu", event => { + if (document.getElementById("datalist-menuseparator")) { + return; + } + let menuseparator = document.createXULElement("menuseparator"); + menuseparator.setAttribute("id", "datalist-menuseparator"); + document.getElementById("textbox-contextmenu").appendChild(menuseparator); + for (let menuitem of menuItems) { + document.getElementById("textbox-contextmenu").appendChild(menuitem); + } + }); + } catch (e) {} +} + +function createMenuItem(label) { + var menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", label); + menuitem.addEventListener("click", event => { + gDialog.hrefInput.value = label; + ChangeLinkLocation(); + }); + return menuitem; +} + +// Shared by Image and Link dialogs for the "Choose" button for links +function chooseLinkFile() { + GetLocalFileURL("html, img").then(fileURL => { + // Always try to relativize local file URLs + if (gHaveDocumentUrl) { + fileURL = MakeRelativeUrl(fileURL); + } + + gDialog.hrefInput.value = fileURL; + + // Do stuff specific to a particular dialog + // (This is defined separately in Image and Link dialogs) + ChangeLinkLocation(); + }); +} diff --git a/comm/mail/components/compose/content/dialogs/EdDictionary.js b/comm/mail/components/compose/content/dialogs/EdDictionary.js new file mode 100644 index 0000000000..a79a01469c --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdDictionary.js @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var gSpellChecker; +var gWordToAdd; + +function Startup() { + if (!GetCurrentEditor()) { + window.close(); + return; + } + // Get the SpellChecker shell + if ("gSpellChecker" in window.opener && window.opener.gSpellChecker) { + gSpellChecker = window.opener.gSpellChecker; + } + + if (!gSpellChecker) { + dump("SpellChecker not found!!!\n"); + window.close(); + return; + } + // The word to add word is passed as the 2nd extra parameter in window.openDialog() + gWordToAdd = window.arguments[1]; + + gDialog.WordInput = document.getElementById("WordInput"); + gDialog.DictionaryList = document.getElementById("DictionaryList"); + + gDialog.WordInput.value = gWordToAdd; + FillDictionaryList(); + + // Select the supplied word if it is already in the list + SelectWordToAddInList(); + SetTextboxFocus(gDialog.WordInput); +} + +function ValidateWordToAdd() { + gWordToAdd = TrimString(gDialog.WordInput.value); + if (gWordToAdd.length > 0) { + return true; + } + return false; +} + +function SelectWordToAddInList() { + for (var i = 0; i < gDialog.DictionaryList.getRowCount(); i++) { + var wordInList = gDialog.DictionaryList.getItemAtIndex(i); + if (wordInList && gWordToAdd == wordInList.label) { + gDialog.DictionaryList.selectedIndex = i; + break; + } + } +} + +function AddWord() { + if (ValidateWordToAdd()) { + try { + gSpellChecker.AddWordToDictionary(gWordToAdd); + } catch (e) { + dump( + "Exception occurred in gSpellChecker.AddWordToDictionary\nWord to add probably already existed\n" + ); + } + + // Rebuild the dialog list + FillDictionaryList(); + + SelectWordToAddInList(); + gDialog.WordInput.value = ""; + } +} + +function RemoveWord() { + var selIndex = gDialog.DictionaryList.selectedIndex; + if (selIndex >= 0) { + var word = gDialog.DictionaryList.selectedItem.label; + + // Remove word from list + gDialog.DictionaryList.selectedItem.remove(); + + // Remove from dictionary + try { + // Not working: BUG 43348 + gSpellChecker.RemoveWordFromDictionary(word); + } catch (e) { + dump("Failed to remove word from dictionary\n"); + } + + ResetSelectedItem(selIndex); + } +} + +function FillDictionaryList() { + var selIndex = gDialog.DictionaryList.selectedIndex; + + // Clear the current contents of the list + ClearListbox(gDialog.DictionaryList); + + // Get the list from the spell checker + gSpellChecker.GetPersonalDictionary(); + + var haveList = false; + + // Get words until an empty string is returned + do { + var word = gSpellChecker.GetPersonalDictionaryWord(); + if (word != "") { + gDialog.DictionaryList.appendItem(word, ""); + haveList = true; + } + } while (word != ""); + + // XXX: BUG 74467: If list is empty, it doesn't layout to full height correctly + // (ignores "rows" attribute) (bug is latered, so we are fixing here for now) + if (!haveList) { + gDialog.DictionaryList.appendItem("", ""); + } + + ResetSelectedItem(selIndex); +} + +function ResetSelectedItem(index) { + var lastIndex = gDialog.DictionaryList.getRowCount() - 1; + if (index > lastIndex) { + index = lastIndex; + } + + // If we didn't have a selected item, + // set it to the first item + if (index == -1 && lastIndex >= 0) { + index = 0; + } + + gDialog.DictionaryList.selectedIndex = index; +} diff --git a/comm/mail/components/compose/content/dialogs/EdDictionary.xhtml b/comm/mail/components/compose/content/dialogs/EdDictionary.xhtml new file mode 100644 index 0000000000..c5c33212a9 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdDictionary.xhtml @@ -0,0 +1,88 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorPersonalDictionary.dtd"> +<window + id="dictionaryDlg" + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + persist="screenX screenY" + lightweightthemes="true" + onload="Startup()" +> + <dialog + buttonlabelaccept="&CloseButton.label;" + buttonaccesskeyaccept="&CloseButton.accessKey;" + buttons="accept" + > + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdDictionary.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <hbox flex="1"> + <div xmlns="http://www.w3.org/1999/xhtml" class="grid-two-column"> + <div class="flex-items-center grid-item-span-row"> + <xul:label + id="WordInputLabel" + value="&wordEditField.label;" + control="WordInput" + accesskey="&wordEditField.accessKey;" + /> + </div> + <div> + <input + id="WordInput" + type="text" + style="width: 14.5em" + aria-labelledby="WordInputLabel" + /> + </div> + <div> + <xul:button + id="AddWord" + oncommand="AddWord()" + label="&AddButton.label;" + accesskey="&AddButton.accessKey;" + /> + </div> + <div class="flex-items-center grid-item-span-row"> + <xul:label + value="&DictionaryList.label;" + control="DictionaryList" + accesskey="&DictionaryList.accessKey;" + /> + </div> + <div> + <xul:richlistbox + id="DictionaryList" + style="width: 15em; height: 10em" + /> + </div> + <div> + <xul:button + id="RemoveWord" + oncommand="RemoveWord()" + label="&RemoveButton.label;" + accesskey="&RemoveButton.accessKey;" + /> + </div> + </div> + </hbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdHLineProps.js b/comm/mail/components/compose/content/dialogs/EdHLineProps.js new file mode 100644 index 0000000000..4a5393d1dc --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdHLineProps.js @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var tagName = "hr"; +var gHLineElement; +var width; +var height; +var align; +var shading; +const gMaxHRSize = 1000; // This is hard-coded in nsHTMLHRElement::StringToAttribute() + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + var editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + try { + // Get the selected horizontal line + gHLineElement = editor.getSelectedElement(tagName); + } catch (e) {} + + if (!gHLineElement) { + // We should never be here if not editing an existing HLine + window.close(); + return; + } + gDialog.heightInput = document.getElementById("height"); + gDialog.widthInput = document.getElementById("width"); + gDialog.leftAlign = document.getElementById("leftAlign"); + gDialog.centerAlign = document.getElementById("centerAlign"); + gDialog.rightAlign = document.getElementById("rightAlign"); + gDialog.alignGroup = gDialog.rightAlign.radioGroup; + gDialog.shading = document.getElementById("3dShading"); + gDialog.pixelOrPercentMenulist = document.getElementById( + "pixelOrPercentMenulist" + ); + + // Make a copy to use for AdvancedEdit and onSaveDefault + globalElement = gHLineElement.cloneNode(false); + + // Initialize control values based on existing attributes + InitDialog(); + + // SET FOCUS TO FIRST CONTROL + SetTextboxFocus(gDialog.widthInput); + + // Resize window + window.sizeToContent(); + + SetWindowLocation(); +} + +// Set dialog widgets with attribute data +// We get them from globalElement copy so this can be used +// by AdvancedEdit(), which is shared by all property dialogs +function InitDialog() { + // Just to be confusing, "size" is used instead of height because it does + // not accept % values, only pixels + var height = GetHTMLOrCSSStyleValue(globalElement, "size", "height"); + if (height.includes("px")) { + height = height.substr(0, height.indexOf("px")); + } + if (!height) { + height = 2; // Default value + } + + // We will use "height" here and in UI + gDialog.heightInput.value = height; + + // Get the width attribute of the element, stripping out "%" + // This sets contents of menulist (adds pixel and percent menuitems elements) + gDialog.widthInput.value = InitPixelOrPercentMenulist( + globalElement, + gHLineElement, + "width", + "pixelOrPercentMenulist" + ); + + var marginLeft = GetHTMLOrCSSStyleValue( + globalElement, + "align", + "margin-left" + ).toLowerCase(); + var marginRight = GetHTMLOrCSSStyleValue( + globalElement, + "align", + "margin-right" + ).toLowerCase(); + align = marginLeft + " " + marginRight; + gDialog.leftAlign.checked = align == "left left" || align == "0px auto"; + gDialog.centerAlign.checked = + align == "center center" || align == "auto auto" || align == " "; + gDialog.rightAlign.checked = align == "right right" || align == "auto 0px"; + + if (gDialog.centerAlign.checked) { + gDialog.alignGroup.selectedItem = gDialog.centerAlign; + } else if (gDialog.rightAlign.checked) { + gDialog.alignGroup.selectedItem = gDialog.rightAlign; + } else { + gDialog.alignGroup.selectedItem = gDialog.leftAlign; + } + + gDialog.shading.checked = !globalElement.hasAttribute("noshade"); +} + +function onSaveDefault() { + // "false" means set attributes on the globalElement, + // not the real element being edited + if (ValidateData()) { + var alignInt; + if (align == "left") { + alignInt = 0; + } else if (align == "right") { + alignInt = 2; + } else { + alignInt = 1; + } + Services.prefs.setIntPref("editor.hrule.align", alignInt); + + var percent; + var widthInt; + var heightInt; + + if (width) { + if (width.includes("%")) { + percent = true; + widthInt = Number(width.substr(0, width.indexOf("%"))); + } else { + percent = false; + widthInt = Number(width); + } + } else { + percent = true; + widthInt = Number(100); + } + + heightInt = height ? Number(height) : 2; + + Services.prefs.setIntPref("editor.hrule.width", widthInt); + Services.prefs.setBoolPref("editor.hrule.width_percent", percent); + Services.prefs.setIntPref("editor.hrule.height", heightInt); + Services.prefs.setBoolPref("editor.hrule.shading", shading); + + // Write the prefs out NOW! + Services.prefs.savePrefFile(null); + } +} + +// Get and validate data from widgets. +// Set attributes on globalElement so they can be accessed by AdvancedEdit() +function ValidateData() { + // Height is always pixels + height = ValidateNumber( + gDialog.heightInput, + null, + 1, + gMaxHRSize, + globalElement, + "size", + false + ); + if (gValidationError) { + return false; + } + + width = ValidateNumber( + gDialog.widthInput, + gDialog.pixelOrPercentMenulist, + 1, + gMaxPixels, + globalElement, + "width", + false + ); + if (gValidationError) { + return false; + } + + align = "left"; + if (gDialog.centerAlign.selected) { + // Don't write out default attribute + align = ""; + } else if (gDialog.rightAlign.selected) { + align = "right"; + } + if (align) { + globalElement.setAttribute("align", align); + } else { + try { + GetCurrentEditor().removeAttributeOrEquivalent( + globalElement, + "align", + true + ); + } catch (e) {} + } + + if (gDialog.shading.checked) { + shading = true; + globalElement.removeAttribute("noshade"); + } else { + shading = false; + globalElement.setAttribute("noshade", "noshade"); + } + return true; +} + +function onAccept(event) { + if (ValidateData()) { + // Copy attributes from the globalElement to the document element + try { + GetCurrentEditor().cloneAttributes(gHLineElement, globalElement); + } catch (e) {} + return; + } + event.preventDefault(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml b/comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml new file mode 100644 index 0000000000..21fa52147c --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml @@ -0,0 +1,131 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % edHLineProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorHLineProperties.dtd"> +%edHLineProperties; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" +> + <dialog> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <!--- Element-specific methods --> + <script src="chrome://messenger/content/messengercompose/EdHLineProps.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <html:fieldset> + <html:legend>&dimensionsBox.label;</html:legend> + <html:table> + <html:tr> + <html:th> + <label + id="widthLabel" + control="width" + value="&widthEditField.label;" + accesskey="&widthEditField.accessKey;" + /> + </html:th> + <html:td> + <html:input + id="width" + type="number" + class="narrow input-inline" + aria-labelledby="widthLabel" + /> + </html:td> + <html:td> + <menulist id="pixelOrPercentMenulist" /> + </html:td> + </html:tr> + <html:tr> + <html:th> + <label + id="heightLabel" + control="height" + value="&heightEditField.label;" + accesskey="&heightEditField.accessKey;" + /> + </html:th> + <html:td> + <html:input + id="height" + type="number" + class="narrow input-inline" + aria-labelledby="heightLabel" + /> + </html:td> + <html:td> + <label value="&pixelsPopup.value;" /> + </html:td> + </html:tr> + </html:table> + <checkbox + id="3dShading" + label="&threeDShading.label;" + accesskey="&threeDShading.accessKey;" + /> + </html:fieldset> + <html:fieldset> + <html:legend>&alignmentBox.label;</html:legend> + <radiogroup id="alignmentGroup" orient="horizontal"> + <spacer class="spacer" /> + <radio + id="leftAlign" + label="&leftRadio.label;" + accesskey="&leftRadio.accessKey;" + /> + <radio + id="centerAlign" + label="¢erRadio.label;" + accesskey="¢erRadio.accessKey;" + /> + <radio + id="rightAlign" + label="&rightRadio.label;" + accesskey="&rightRadio.accessKey;" + /> + </radiogroup> + </html:fieldset> + <spacer class="spacer" /> + <hbox> + <button + id="SaveDefault" + label="&saveSettings.label;" + accesskey="&saveSettings.accessKey;" + oncommand="onSaveDefault()" + tooltiptext="&saveSettings.tooltip;" + /> + <spacer flex="1" /> + <button + id="AdvancedEditButton" + oncommand="onAdvancedEdit();" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <separator class="groove" /> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdImageDialog.js b/comm/mail/components/compose/content/dialogs/EdImageDialog.js new file mode 100644 index 0000000000..91e558cd50 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdImageDialog.js @@ -0,0 +1,639 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + Note: We encourage non-empty alt text for images inserted into a page. + When there's no alt text, we always write 'alt=""' as the attribute, since "alt" is a required attribute. + We allow users to not have alt text by checking a "Don't use alterate text" radio button, + and we don't accept spaces as valid alt text. A space used to be required to avoid the error message + if user didn't enter alt text, but is unnecessary now that we no longer annoy the user + with the error dialog if alt="" is present on an img element. + We trim all spaces at the beginning and end of user's alt text +*/ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var gInsertNewImage = true; +var gDoAltTextError = false; +var gConstrainOn = false; +// Note used in current version, but these are set correctly +// and could be used to reset width and height used for constrain ratio +var gConstrainWidth = 0; +var gConstrainHeight = 0; +var imageElement; +var gImageMap = 0; +var gCanRemoveImageMap = false; +var gRemoveImageMap = false; +var gImageMapDisabled = false; +var gActualWidth = ""; +var gActualHeight = ""; +var gOriginalSrc = ""; +var gTimerID; +var gValidateTab; +var gInsertNewIMap; + +// These must correspond to values in EditorDialog.css for each theme +// (unfortunately, setting "style" attribute here doesn't work!) +var gPreviewImageWidth = 80; +var gPreviewImageHeight = 50; + +// dialog initialization code + +function ImageStartup() { + gDialog.tabBox = document.getElementById("TabBox"); + gDialog.tabLocation = document.getElementById("imageLocationTab"); + gDialog.tabDimensions = document.getElementById("imageDimensionsTab"); + gDialog.tabBorder = document.getElementById("imageBorderTab"); + gDialog.srcInput = document.getElementById("srcInput"); + gDialog.titleInput = document.getElementById("titleInput"); + gDialog.altTextInput = document.getElementById("altTextInput"); + gDialog.altTextRadioGroup = document.getElementById("altTextRadioGroup"); + gDialog.altTextRadio = document.getElementById("altTextRadio"); + gDialog.noAltTextRadio = document.getElementById("noAltTextRadio"); + gDialog.actualSizeRadio = document.getElementById("actualSizeRadio"); + gDialog.constrainCheckbox = document.getElementById("constrainCheckbox"); + gDialog.widthInput = document.getElementById("widthInput"); + gDialog.heightInput = document.getElementById("heightInput"); + gDialog.widthUnitsMenulist = document.getElementById("widthUnitsMenulist"); + gDialog.heightUnitsMenulist = document.getElementById("heightUnitsMenulist"); + gDialog.imagelrInput = document.getElementById("imageleftrightInput"); + gDialog.imagetbInput = document.getElementById("imagetopbottomInput"); + gDialog.border = document.getElementById("border"); + gDialog.alignTypeSelect = document.getElementById("alignTypeSelect"); + gDialog.PreviewWidth = document.getElementById("PreviewWidth"); + gDialog.PreviewHeight = document.getElementById("PreviewHeight"); + gDialog.PreviewImage = document.getElementById("preview-image"); + gDialog.PreviewImage.addEventListener("load", PreviewImageLoaded); + gDialog.OkButton = document.querySelector("dialog").getButton("accept"); +} + +// Set dialog widgets with attribute data +// We get them from globalElement copy so this can be used +// by AdvancedEdit(), which is shared by all property dialogs +function InitImage() { + // Set the controls to the image's attributes + var src = globalElement.getAttribute("src"); + + // For image insertion the 'src' attribute is null. + if (src) { + // Shorten data URIs for display. + shortenImageData(src, gDialog.srcInput); + } + + // Set "Relativize" checkbox according to current URL state + SetRelativeCheckbox(); + + // Force loading of image from its source and show preview image + LoadPreviewImage(); + + gDialog.titleInput.value = globalElement.getAttribute("title"); + + var hasAltText = globalElement.hasAttribute("alt"); + var altText = globalElement.getAttribute("alt"); + gDialog.altTextInput.value = altText; + if (altText || (!hasAltText && globalElement.hasAttribute("src"))) { + gDialog.altTextRadioGroup.selectedItem = gDialog.altTextRadio; + } else if (hasAltText) { + gDialog.altTextRadioGroup.selectedItem = gDialog.noAltTextRadio; + } + SetAltTextDisabled( + gDialog.altTextRadioGroup.selectedItem == gDialog.noAltTextRadio + ); + + // setup the height and width widgets + var width = InitPixelOrPercentMenulist( + globalElement, + gInsertNewImage ? null : imageElement, + "width", + "widthUnitsMenulist", + gPixel + ); + var height = InitPixelOrPercentMenulist( + globalElement, + gInsertNewImage ? null : imageElement, + "height", + "heightUnitsMenulist", + gPixel + ); + + // Set actual radio button if both set values are the same as actual + SetSizeWidgets(width, height); + + gDialog.widthInput.value = gConstrainWidth = width || gActualWidth || ""; + gDialog.heightInput.value = gConstrainHeight = height || gActualHeight || ""; + + // set spacing editfields + gDialog.imagelrInput.value = globalElement.getAttribute("hspace"); + gDialog.imagetbInput.value = globalElement.getAttribute("vspace"); + + // dialog.border.value = globalElement.getAttribute("border"); + var bv = GetHTMLOrCSSStyleValue(globalElement, "border", "border-top-width"); + if (bv.includes("px")) { + // Strip out the px + bv = bv.substr(0, bv.indexOf("px")); + } else if (bv == "thin") { + bv = "1"; + } else if (bv == "medium") { + bv = "3"; + } else if (bv == "thick") { + bv = "5"; + } + gDialog.border.value = bv; + + // Get alignment setting + var align = globalElement.getAttribute("align"); + if (align) { + align = align.toLowerCase(); + } + + switch (align) { + case "top": + case "middle": + case "right": + case "left": + gDialog.alignTypeSelect.value = align; + break; + default: + // Default or "bottom" + gDialog.alignTypeSelect.value = "bottom"; + } + + // Get image map for image + gImageMap = GetImageMap(); + + doOverallEnabling(); + doDimensionEnabling(); +} + +function SetSizeWidgets(width, height) { + if ( + !(width || height) || + (gActualWidth && + gActualHeight && + width == gActualWidth && + height == gActualHeight) + ) { + gDialog.actualSizeRadio.radioGroup.selectedItem = gDialog.actualSizeRadio; + } + + if (!gDialog.actualSizeRadio.selected) { + // Decide if user's sizes are in the same ratio as actual sizes + if (gActualWidth && gActualHeight) { + if (gActualWidth > gActualHeight) { + gDialog.constrainCheckbox.checked = + Math.round((gActualHeight * width) / gActualWidth) == height; + } else { + gDialog.constrainCheckbox.checked = + Math.round((gActualWidth * height) / gActualHeight) == width; + } + } + } +} + +// Disable alt text input when "Don't use alt" radio is checked +function SetAltTextDisabled(disable) { + gDialog.altTextInput.disabled = disable; +} + +function GetImageMap() { + var usemap = globalElement.getAttribute("usemap"); + if (usemap) { + gCanRemoveImageMap = true; + let mapname = usemap.substr(1); + try { + return GetCurrentEditor().document.querySelector( + '[name="' + mapname + '"]' + ); + } catch (e) {} + } else { + gCanRemoveImageMap = false; + } + + return null; +} + +function chooseFile() { + if (gTimerID) { + clearTimeout(gTimerID); + } + + // Put focus into the input field + SetTextboxFocus(gDialog.srcInput); + + GetLocalFileURL("img").then(fileURL => { + // Always try to relativize local file URLs + if (gHaveDocumentUrl) { + fileURL = MakeRelativeUrl(fileURL); + } + + gDialog.srcInput.value = fileURL; + + SetRelativeCheckbox(); + doOverallEnabling(); + LoadPreviewImage(); + }); +} + +function PreviewImageLoaded() { + if (gDialog.PreviewImage) { + // Image loading has completed -- we can get actual width + gActualWidth = gDialog.PreviewImage.naturalWidth; + gActualHeight = gDialog.PreviewImage.naturalHeight; + + if (gActualWidth && gActualHeight) { + // Use actual size or scale to fit preview if either dimension is too large + var width = gActualWidth; + var height = gActualHeight; + if (gActualWidth > gPreviewImageWidth) { + width = gPreviewImageWidth; + height = gActualHeight * (gPreviewImageWidth / gActualWidth); + } + if (height > gPreviewImageHeight) { + height = gPreviewImageHeight; + width = gActualWidth * (gPreviewImageHeight / gActualHeight); + } + gDialog.PreviewImage.width = width; + gDialog.PreviewImage.height = height; + + gDialog.PreviewWidth.setAttribute("value", gActualWidth); + gDialog.PreviewHeight.setAttribute("value", gActualHeight); + + document.getElementById("imagePreview").hidden = false; + + SetSizeWidgets(gDialog.widthInput.value, gDialog.heightInput.value); + } + + if (gDialog.actualSizeRadio.selected) { + SetActualSize(); + } + + window.sizeToContent(); + } +} + +function LoadPreviewImage() { + var imageSrc = gDialog.srcInput.value.trim(); + if (!imageSrc) { + return; + } + if (isImageDataShortened(imageSrc)) { + imageSrc = restoredImageData(gDialog.srcInput); + } + + try { + // Remove the image URL from image cache so it loads fresh + // (if we don't do this, loads after the first will always use image cache + // and we won't see image edit changes or be able to get actual width and height) + + // We must have an absolute URL to preview it or remove it from the cache + imageSrc = MakeAbsoluteUrl(imageSrc); + + if (GetScheme(imageSrc)) { + let uri = Services.io.newURI(imageSrc); + if (uri) { + let imgCache = Cc["@mozilla.org/image/cache;1"].getService( + Ci.imgICache + ); + + // This returns error if image wasn't in the cache; ignore that + imgCache.removeEntry(uri); + } + } + } catch (e) {} + + gDialog.PreviewImage.addEventListener("load", PreviewImageLoaded, true); + gDialog.PreviewImage.src = imageSrc; +} + +function SetActualSize() { + gDialog.widthInput.value = gActualWidth ? gActualWidth : ""; + gDialog.widthUnitsMenulist.selectedIndex = 0; + gDialog.heightInput.value = gActualHeight ? gActualHeight : ""; + gDialog.heightUnitsMenulist.selectedIndex = 0; + doDimensionEnabling(); +} + +function ChangeImageSrc() { + if (gTimerID) { + clearTimeout(gTimerID); + } + + gTimerID = setTimeout(LoadPreviewImage, 800); + + SetRelativeCheckbox(); + doOverallEnabling(); +} + +function doDimensionEnabling() { + // Enabled unless "Actual Size" is selected + var enable = !gDialog.actualSizeRadio.selected; + + // BUG 74145: After input field is disabled, + // setting it enabled causes blinking caret to appear + // even though focus isn't set to it. + SetElementEnabledById("heightInput", enable); + SetElementEnabledById("heightLabel", enable); + SetElementEnabledById("heightUnitsMenulist", enable); + + SetElementEnabledById("widthInput", enable); + SetElementEnabledById("widthLabel", enable); + SetElementEnabledById("widthUnitsMenulist", enable); + + var constrainEnable = + enable && + gDialog.widthUnitsMenulist.selectedIndex == 0 && + gDialog.heightUnitsMenulist.selectedIndex == 0; + + SetElementEnabledById("constrainCheckbox", constrainEnable); +} + +function doOverallEnabling() { + var enabled = TrimString(gDialog.srcInput.value) != ""; + + SetElementEnabled(gDialog.OkButton, enabled); + SetElementEnabledById("AdvancedEditButton1", enabled); + SetElementEnabledById("imagemapLabel", enabled); + SetElementEnabledById("removeImageMap", gCanRemoveImageMap); +} + +function ToggleConstrain() { + // If just turned on, save the current width and height as basis for constrain ratio + // Thus clicking on/off lets user say "Use these values as aspect ration" + if ( + gDialog.constrainCheckbox.checked && + !gDialog.constrainCheckbox.disabled && + gDialog.widthUnitsMenulist.selectedIndex == 0 && + gDialog.heightUnitsMenulist.selectedIndex == 0 + ) { + gConstrainWidth = Number(TrimString(gDialog.widthInput.value)); + gConstrainHeight = Number(TrimString(gDialog.heightInput.value)); + } +} + +function constrainProportions(srcID, destID) { + var srcElement = document.getElementById(srcID); + if (!srcElement) { + return; + } + + var destElement = document.getElementById(destID); + if (!destElement) { + return; + } + + // always force an integer (whether we are constraining or not) + forceInteger(srcID); + + if ( + !gActualWidth || + !gActualHeight || + !(gDialog.constrainCheckbox.checked && !gDialog.constrainCheckbox.disabled) + ) { + return; + } + + // double-check that neither width nor height is in percent mode; bail if so! + if ( + gDialog.widthUnitsMenulist.selectedIndex != 0 || + gDialog.heightUnitsMenulist.selectedIndex != 0 + ) { + return; + } + + // This always uses the actual width and height ratios + // which is kind of funky if you change one number without the constrain + // and then turn constrain on and change a number + // I prefer the old strategy (below) but I can see some merit to this solution + if (srcID == "widthInput") { + destElement.value = Math.round( + (srcElement.value * gActualHeight) / gActualWidth + ); + } else { + destElement.value = Math.round( + (srcElement.value * gActualWidth) / gActualHeight + ); + } + + /* + // With this strategy, the width and height ratio + // can be reset to whatever the user entered. + if (srcID == "widthInput") { + destElement.value = Math.round( srcElement.value * gConstrainHeight / gConstrainWidth ); + } else { + destElement.value = Math.round( srcElement.value * gConstrainWidth / gConstrainHeight ); + } + */ +} + +function removeImageMap() { + gRemoveImageMap = true; + gCanRemoveImageMap = false; + SetElementEnabledById("removeImageMap", false); +} + +function SwitchToValidatePanel() { + if ( + gDialog.tabBox && + gValidateTab && + gDialog.tabBox.selectedTab != gValidateTab + ) { + gDialog.tabBox.selectedTab = gValidateTab; + } +} + +// Get data from widgets, validate, and set for the global element +// accessible to AdvancedEdit() [in EdDialogCommon.js] +function ValidateImage() { + var editor = GetCurrentEditor(); + if (!editor) { + return false; + } + + gValidateTab = gDialog.tabLocation; + if (!gDialog.srcInput.value) { + Services.prompt.alert( + window, + GetString("Alert"), + GetString("MissingImageError") + ); + SwitchToValidatePanel(); + gDialog.srcInput.focus(); + return false; + } + + // We must convert to "file:///" or "http://" format else image doesn't load! + let src = gDialog.srcInput.value.trim(); + + if (isImageDataShortened(src)) { + src = restoredImageData(gDialog.srcInput); + } else { + var checkbox = document.getElementById("MakeRelativeCheckbox"); + try { + if (checkbox && !checkbox.checked) { + src = Services.uriFixup.createFixupURI( + src, + Ci.nsIURIFixup.FIXUP_FLAG_NONE + ).spec; + } + } catch (e) {} + + globalElement.setAttribute("src", src); + } + + let title = gDialog.titleInput.value.trim(); + if (title) { + globalElement.setAttribute("title", title); + } else { + globalElement.removeAttribute("title"); + } + + // Force user to enter Alt text only if "Alternate text" radio is checked + // Don't allow just spaces in alt text + var alt = ""; + var useAlt = gDialog.altTextRadioGroup.selectedItem == gDialog.altTextRadio; + if (useAlt) { + alt = TrimString(gDialog.altTextInput.value); + } + + if (alt || !useAlt) { + globalElement.setAttribute("alt", alt); + } else if (!gDoAltTextError) { + globalElement.removeAttribute("alt"); + } else { + Services.prompt.alert(window, GetString("Alert"), GetString("NoAltText")); + SwitchToValidatePanel(); + gDialog.altTextInput.focus(); + return false; + } + + var width = ""; + var height = ""; + + gValidateTab = gDialog.tabDimensions; + if (!gDialog.actualSizeRadio.selected) { + // Get user values for width and height + width = ValidateNumber( + gDialog.widthInput, + gDialog.widthUnitsMenulist, + 1, + gMaxPixels, + globalElement, + "width", + false, + true + ); + if (gValidationError) { + return false; + } + + height = ValidateNumber( + gDialog.heightInput, + gDialog.heightUnitsMenulist, + 1, + gMaxPixels, + globalElement, + "height", + false, + true + ); + if (gValidationError) { + return false; + } + } + + // We always set the width and height attributes, even if same as actual. + // This speeds up layout of pages since sizes are known before image is loaded + if (!width) { + width = gActualWidth; + } + if (!height) { + height = gActualHeight; + } + + // Remove existing width and height only if source changed + // and we couldn't obtain actual dimensions + var srcChanged = src != gOriginalSrc; + if (width) { + editor.setAttributeOrEquivalent(globalElement, "width", width, true); + } else if (srcChanged) { + editor.removeAttributeOrEquivalent(globalElement, "width", true); + } + + if (height) { + editor.setAttributeOrEquivalent(globalElement, "height", height, true); + } else if (srcChanged) { + editor.removeAttributeOrEquivalent(globalElement, "height", true); + } + + // spacing attributes + gValidateTab = gDialog.tabBorder; + ValidateNumber( + gDialog.imagelrInput, + null, + 0, + gMaxPixels, + globalElement, + "hspace", + false, + true, + true + ); + if (gValidationError) { + return false; + } + + ValidateNumber( + gDialog.imagetbInput, + null, + 0, + gMaxPixels, + globalElement, + "vspace", + false, + true + ); + if (gValidationError) { + return false; + } + + // note this is deprecated and should be converted to stylesheets + ValidateNumber( + gDialog.border, + null, + 0, + gMaxPixels, + globalElement, + "border", + false, + true + ); + if (gValidationError) { + return false; + } + + // Default or setting "bottom" means don't set the attribute + // Note that the attributes "left" and "right" are opposite + // of what we use in the UI, which describes where the TEXT wraps, + // not the image location (which is what the HTML describes) + switch (gDialog.alignTypeSelect.value) { + case "top": + case "middle": + case "right": + case "left": + editor.setAttributeOrEquivalent( + globalElement, + "align", + gDialog.alignTypeSelect.value, + true + ); + break; + default: + try { + editor.removeAttributeOrEquivalent(globalElement, "align", true); + } catch (e) {} + } + + return true; +} diff --git a/comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js b/comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js new file mode 100644 index 0000000000..9c41679c15 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var gMsgCompProcessLink = false; +var gMsgCompInputElement = null; +var gMsgCompPrevInputValue = null; +var gMsgCompPrevMozDoNotSendAttribute; +var gMsgCompAttachSourceElement = null; + +function OnLoadDialog() { + gMsgCompAttachSourceElement = document.getElementById("AttachSourceToMail"); + var editor = GetCurrentEditor(); + if ( + gMsgCompAttachSourceElement && + editor && + editor.flags & Ci.nsIEditor.eEditorMailMask + ) { + SetRelativeCheckbox = function () { + SetAttachCheckbox(); + }; + // initialize the AttachSourceToMail checkbox + gMsgCompAttachSourceElement.hidden = false; + + switch (document.querySelector("dialog").id) { + case "imageDlg": + gMsgCompInputElement = gDialog.srcInput; + gMsgCompProcessLink = false; + break; + case "linkDlg": + gMsgCompInputElement = gDialog.hrefInput; + gMsgCompProcessLink = true; + break; + } + if (gMsgCompInputElement) { + SetAttachCheckbox(); + gMsgCompPrevMozDoNotSendAttribute = + globalElement.getAttribute("moz-do-not-send"); + } + } +} +addEventListener("load", OnLoadDialog, false); + +function OnAcceptDialog() { + // Auto-convert file URLs to data URLs. If we're in the link properties + // dialog convert only when requested - for the image dialog do it always. + if ( + /^file:/i.test(gMsgCompInputElement.value.trim()) && + (gMsgCompAttachSourceElement.checked || !gMsgCompProcessLink) + ) { + var dataURI = GenerateDataURL(gMsgCompInputElement.value.trim()); + gMsgCompInputElement.value = dataURI; + gMsgCompAttachSourceElement.checked = true; + } + DoAttachSourceCheckbox(); +} +document.addEventListener("dialogaccept", OnAcceptDialog, true); + +function SetAttachCheckbox() { + var resetCheckbox = false; + var mozDoNotSend = globalElement.getAttribute("moz-do-not-send"); + + // In case somebody played with the advanced property and changed the moz-do-not-send attribute + if (mozDoNotSend != gMsgCompPrevMozDoNotSendAttribute) { + gMsgCompPrevMozDoNotSendAttribute = mozDoNotSend; + resetCheckbox = true; + } + + // Has the URL changed + if ( + gMsgCompInputElement && + gMsgCompInputElement.value != gMsgCompPrevInputValue + ) { + gMsgCompPrevInputValue = gMsgCompInputElement.value; + resetCheckbox = true; + } + + if (gMsgCompInputElement && resetCheckbox) { + // Here is the rule about how to set the checkbox Attach Source To Message: + // If the attribute "moz-do-not-send" has not been set, we look at the scheme of the URL + // and at some preference to decide what is the best for the user. + // If it is set to "false", the checkbox is checked, otherwise unchecked. + var attach = false; + if (mozDoNotSend == null) { + // We haven't yet set the "moz-do-not-send" attribute. + var inputValue = gMsgCompInputElement.value.trim(); + if (/^(file|data):/i.test(inputValue)) { + // For files or data URLs, default to attach them. + attach = true; + } else if ( + !gMsgCompProcessLink && // Implies image dialogue. + /^https?:/i.test(inputValue) + ) { + // For images loaded via http(s) we default to the preference value. + attach = Services.prefs.getBoolPref("mail.compose.attach_http_images"); + } + } else { + attach = mozDoNotSend == "false"; + } + + gMsgCompAttachSourceElement.checked = attach; + } +} + +function DoAttachSourceCheckbox() { + gMsgCompPrevMozDoNotSendAttribute = + (!gMsgCompAttachSourceElement.checked).toString(); + globalElement.setAttribute( + "moz-do-not-send", + gMsgCompPrevMozDoNotSendAttribute + ); +} + +function GenerateDataURL(url) { + var file = Services.io.newURI(url).QueryInterface(Ci.nsIFileURL).file; + var contentType = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + var inputStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + inputStream.init(file, 0x01, 0o600, 0); + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + stream.setInputStream(inputStream); + let data = ""; + while (stream.available() > 0) { + data += stream.readBytes(stream.available()); + } + let encoded = btoa(data); + stream.close(); + return ( + "data:" + + contentType + + ";filename=" + + encodeURIComponent(file.leafName) + + ";base64," + + encoded + ); +} diff --git a/comm/mail/components/compose/content/dialogs/EdImageProps.js b/comm/mail/components/compose/content/dialogs/EdImageProps.js new file mode 100644 index 0000000000..861d098edc --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdImageProps.js @@ -0,0 +1,293 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ +/* import-globals-from EdImageDialog.js */ + +var gAnchorElement = null; +var gLinkElement = null; +var gOriginalHref = ""; +var gHNodeArray = {}; + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + var editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + + ImageStartup(); + gDialog.hrefInput = document.getElementById("hrefInput"); + gDialog.makeRelativeLink = document.getElementById("MakeRelativeLink"); + gDialog.showLinkBorder = document.getElementById("showLinkBorder"); + gDialog.linkTab = document.getElementById("imageLinkTab"); + gDialog.linkAdvanced = document.getElementById("LinkAdvancedEditButton"); + + // Get a single selected image element + var tagName = "img"; + if ("arguments" in window && window.arguments[0]) { + imageElement = window.arguments[0]; + // We've been called from form field properties, so we can't insert a link + gDialog.linkTab.remove(); + gDialog.linkTab = null; + } else { + // First check for <input type="image"> + try { + imageElement = editor.getSelectedElement("input"); + + if (!imageElement || imageElement.getAttribute("type") != "image") { + // Get a single selected image element + imageElement = editor.getSelectedElement(tagName); + if (imageElement) { + gAnchorElement = editor.getElementOrParentByTagName( + "href", + imageElement + ); + } + } + } catch (e) {} + } + + if (imageElement) { + // We found an element and don't need to insert one + if (imageElement.hasAttribute("src")) { + gInsertNewImage = false; + gActualWidth = imageElement.naturalWidth; + gActualHeight = imageElement.naturalHeight; + } + } else { + gInsertNewImage = true; + + // We don't have an element selected, + // so create one with default attributes + try { + imageElement = editor.createElementWithDefaults(tagName); + } catch (e) {} + + if (!imageElement) { + dump("Failed to get selected element or create a new one!\n"); + window.close(); + return; + } + try { + gAnchorElement = editor.getSelectedElement("href"); + } catch (e) {} + } + + // Make a copy to use for AdvancedEdit + globalElement = imageElement.cloneNode(false); + + // We only need to test for this once per dialog load + gHaveDocumentUrl = GetDocumentBaseUrl(); + + InitDialog(); + if (gAnchorElement) { + gOriginalHref = gAnchorElement.getAttribute("href"); + // Make a copy to use for AdvancedEdit + gLinkElement = gAnchorElement.cloneNode(false); + } else { + gLinkElement = editor.createElementWithDefaults("a"); + } + gDialog.hrefInput.value = gOriginalHref; + + FillLinkMenulist(gDialog.hrefInput, gHNodeArray); + ChangeLinkLocation(); + + // Save initial source URL + gOriginalSrc = gDialog.srcInput.value; + + // By default turn constrain on, but both width and height must be in pixels + gDialog.constrainCheckbox.checked = + gDialog.widthUnitsMenulist.selectedIndex == 0 && + gDialog.heightUnitsMenulist.selectedIndex == 0; + + // Start in "Link" tab if 2nd argument is true + if (gDialog.linkTab && "arguments" in window && window.arguments[1]) { + document.getElementById("TabBox").selectedTab = gDialog.linkTab; + SetTextboxFocus(gDialog.hrefInput); + } else { + SetTextboxFocus(gDialog.srcInput); + } + + SetWindowLocation(); +} + +// Set dialog widgets with attribute data +// We get them from globalElement copy so this can be used +// by AdvancedEdit(), which is shared by all property dialogs +function InitDialog() { + InitImage(); + var border = TrimString(gDialog.border.value); + gDialog.showLinkBorder.checked = border != "" && border > 0; +} + +function ChangeLinkLocation() { + var href = TrimString(gDialog.hrefInput.value); + SetRelativeCheckbox(gDialog.makeRelativeLink); + gDialog.showLinkBorder.disabled = !href; + gDialog.linkAdvanced.disabled = !href; + gLinkElement.setAttribute("href", href); +} + +function ToggleShowLinkBorder() { + if (gDialog.showLinkBorder.checked) { + var border = TrimString(gDialog.border.value); + if (!border || border == "0") { + gDialog.border.value = "2"; + } + } else { + gDialog.border.value = "0"; + } +} + +// Get data from widgets, validate, and set for the global element +// accessible to AdvancedEdit() [in EdDialogCommon.js] +function ValidateData() { + return ValidateImage(); +} + +function onAccept(event) { + // Use this now (default = false) so Advanced Edit button dialog doesn't trigger error message + gDoAltTextError = true; + window.opener.gMsgCompose.allowRemoteContent = true; + if (ValidateData()) { + if ("arguments" in window && window.arguments[0]) { + SaveWindowLocation(); + return; + } + + var editor = GetCurrentEditor(); + + editor.beginTransaction(); + + try { + if (gRemoveImageMap) { + globalElement.removeAttribute("usemap"); + if (gImageMap) { + editor.deleteNode(gImageMap); + gInsertNewIMap = true; + gImageMap = null; + } + } else if (gImageMap) { + // un-comment to see that inserting image maps does not work! + /* + gImageMap = editor.createElementWithDefaults("map"); + gImageMap.setAttribute("name", "testing"); + var testArea = editor.createElementWithDefaults("area"); + testArea.setAttribute("shape", "circle"); + testArea.setAttribute("coords", "86,102,52"); + testArea.setAttribute("href", "test"); + gImageMap.appendChild(testArea); + */ + + // Assign to map if there is one + var mapName = gImageMap.getAttribute("name"); + if (mapName != "") { + globalElement.setAttribute("usemap", "#" + mapName); + if (globalElement.getAttribute("border") == "") { + globalElement.setAttribute("border", 0); + } + } + } + + // Create or remove the link as appropriate + var href = gDialog.hrefInput.value; + if (href != gOriginalHref) { + if (href && !gInsertNewImage) { + EditorSetTextProperty("a", "href", href); + // gAnchorElement is needed for cloning attributes later. + if (!gAnchorElement) { + gAnchorElement = editor.getElementOrParentByTagName( + "href", + imageElement + ); + } + } else { + EditorRemoveTextProperty("href", ""); + } + } + + // If inside a link, always write the 'border' attribute + if (href) { + if (gDialog.showLinkBorder.checked) { + // Use default = 2 if border attribute is empty + if (!globalElement.hasAttribute("border")) { + globalElement.setAttribute("border", "2"); + } + } else { + globalElement.setAttribute("border", "0"); + } + } + + if (gInsertNewImage) { + if (href) { + gLinkElement.appendChild(imageElement); + editor.insertElementAtSelection(gLinkElement, true); + } else { + // 'true' means delete the selection before inserting + editor.insertElementAtSelection(imageElement, true); + } + } + + // Check to see if the link was to a heading + // Do this last because it moves the caret (BAD!) + if (href in gHNodeArray) { + var anchorNode = editor.createElementWithDefaults("a"); + if (anchorNode) { + anchorNode.name = href.substr(1); + // Remember to use editor method so it is undoable! + editor.insertNode(anchorNode, gHNodeArray[href], 0); + } + } + // All values are valid - copy to actual element in doc or + // element we just inserted + editor.cloneAttributes(imageElement, globalElement); + if (gAnchorElement) { + editor.cloneAttributes(gAnchorElement, gLinkElement); + } + + // If document is empty, the map element won't insert, + // so always insert the image first + if (gImageMap && gInsertNewIMap) { + // Insert the ImageMap element at beginning of document + var body = editor.rootElement; + editor.setShouldTxnSetSelection(false); + editor.insertNode(gImageMap, body, 0); + editor.setShouldTxnSetSelection(true); + } + } catch (e) { + dump(e); + } + + editor.endTransaction(); + + SaveWindowLocation(); + return; + } + + gDoAltTextError = false; + + event.preventDefault(); +} + +function onLinkAdvancedEdit() { + window.AdvancedEditOK = false; + window.openDialog( + "chrome://messenger/content/messengercompose/EdAdvancedEdit.xhtml", + "_blank", + "chrome,close,titlebar,modal,resizable=yes", + "", + gLinkElement + ); + window.focus(); + if (window.AdvancedEditOK) { + gDialog.hrefInput.value = gLinkElement.getAttribute("href"); + } +} diff --git a/comm/mail/components/compose/content/dialogs/EdImageProps.xhtml b/comm/mail/components/compose/content/dialogs/EdImageProps.xhtml new file mode 100644 index 0000000000..c894a30175 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdImageProps.xhtml @@ -0,0 +1,454 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % edImageProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorImageProperties.dtd"> +%edImageProperties; +<!ENTITY % composeEditorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/mailComposeEditorOverlay.dtd"> +%composeEditorOverlayDTD; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<!-- dialog containing a control requiring initial setup --> +<window + windowtype="Mail:image" + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + style="min-height: 24em" + lightweightthemes="true" + onload="Startup()" +> + <dialog id="imageDlg" buttons="accept,cancel" style="width: 68ch"> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdImageProps.js" /> + <script src="chrome://messenger/content/messengercompose/EdImageDialog.js" /> + <script src="chrome://messenger/content/messengercompose/EdImageLinkLoader.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <tabbox id="TabBox"> + <tabs flex="1"> + <tab id="imageLocationTab" label="&imageLocationTab.label;" /> + <tab id="imageDimensionsTab" label="&imageDimensionsTab.label;" /> + <tab id="imageAppearanceTab" label="&imageAppearanceTab.label;" /> + <tab id="imageLinkTab" label="&imageLinkTab.label;" /> + </tabs> + <tabpanels> + <vbox id="imageLocation"> + <spacer class="spacer" /> + <label + id="srcLabel" + control="srcInput" + value="&locationEditField.label;" + accesskey="&locationEditField.accessKey;" + tooltiptext="&locationEditField.tooltip;" + /> + <tooltip id="shortenedDataURI"> + <label value="&locationEditField.shortenedDataURI;" /> + </tooltip> + <html:input + id="srcInput" + type="text" + oninput="ChangeImageSrc();" + tabindex="1" + class="uri-element input-inline" + title="&locationEditField.tooltip;" + aria-labelledby="srcLabel" + /> + <hbox id="MakeRelativeHbox"> + <checkbox + id="MakeRelativeCheckbox" + tabindex="2" + label="&makeUrlRelative.label;" + accesskey="&makeUrlRelative.accessKey;" + oncommand="MakeInputValueRelativeOrAbsolute(this);" + tooltiptext="&makeUrlRelative.tooltip;" + /> + <checkbox + id="AttachSourceToMail" + hidden="true" + label="&attachImageSource.label;" + accesskey="&attachImageSource.accesskey;" + oncommand="DoAttachSourceCheckbox()" + /> + <spacer flex="1" /> + <button + id="ChooseFile" + tabindex="3" + oncommand="chooseFile()" + label="&chooseFileButton.label;" + accesskey="&chooseFileButton.accessKey;" + /> + </hbox> + <spacer class="spacer" /> + <radiogroup id="altTextRadioGroup" flex="1"> + <hbox> + <vbox> + <hbox align="center" flex="1"> + <label + id="titleLabel" + style="margin-left: 26px" + control="titleInput" + accesskey="&title.accessKey;" + value="&title.label;" + tooltiptext="&title.tooltip;" + /> + </hbox> + <hbox align="center" flex="1"> + <radio + id="altTextRadio" + value="usealt-yes" + label="&altText.label;" + accesskey="&altText.accessKey;" + tooltiptext="&altTextEditField.tooltip;" + persist="selected" + oncommand="SetAltTextDisabled(false);" + tabindex="5" + /> + </hbox> + </vbox> + <vbox flex="1"> + <html:input + id="titleInput" + type="text" + class="MinWidth20em input-inline" + title="&title.tooltip;" + tabindex="4" + aria-labelledby="titleLabel" + /> + <html:input + id="altTextInput" + type="text" + class="MinWidth20em input-inline" + title="&altTextEditField.tooltip;" + oninput="SetAltTextDisabled(false);" + tabindex="6" + aria-labelledby="altTextRadio" + /> + </vbox> + </hbox> + <radio + id="noAltTextRadio" + value="usealt-no" + label="&noAltText.label;" + accesskey="&noAltText.accessKey;" + persist="selected" + oncommand="SetAltTextDisabled(true);" + /> + </radiogroup> + </vbox> + + <vbox id="imageDimensions" align="start"> + <spacer class="spacer" /> + <hbox> + <radiogroup id="imgSizeGroup"> + <radio + id="actualSizeRadio" + label="&actualSizeRadio.label;" + accesskey="&actualSizeRadio.accessKey;" + tooltiptext="&actualSizeRadio.tooltip;" + oncommand="SetActualSize()" + value="actual" + /> + <radio + id="customSizeRadio" + label="&customSizeRadio.label;" + selected="true" + accesskey="&customSizeRadio.accessKey;" + tooltiptext="&customSizeRadio.tooltip;" + oncommand="doDimensionEnabling();" + value="custom" + /> + </radiogroup> + <spacer flex="1" /> + <vbox> + <spacer flex="1" /> + <checkbox + id="constrainCheckbox" + label="&constrainCheckbox.label;" + accesskey="&constrainCheckbox.accessKey;" + oncommand="ToggleConstrain()" + tooltiptext="&constrainCheckbox.tooltip;" + /> + </vbox> + <spacer flex="1" /> + </hbox> + <spacer class="spacer" /> + <hbox class="indent"> + <html:table> + <html:tr> + <html:th> + <label + id="widthLabel" + control="widthInput" + accesskey="&widthEditField.accessKey;" + value="&widthEditField.label;" + /> + </html:th> + <html:td> + <html:input + id="widthInput" + type="number" + min="0" + class="narrow input-inline" + oninput="constrainProportions(this.id,'heightInput')" + aria-labelledby="widthLabel" + /> + </html:td> + <html:td> + <menulist + id="widthUnitsMenulist" + oncommand="doDimensionEnabling();" + /> + </html:td> + </html:tr> + <html:tr> + <html:th> + <label + id="heightLabel" + control="heightInput" + accesskey="&heightEditField.accessKey;" + value="&heightEditField.label;" + /> + </html:th> + <html:td> + <html:input + id="heightInput" + type="number" + min="0" + class="narrow input-inline" + oninput="constrainProportions(this.id,'widthInput')" + aria-labelledby="heightLabel" + /> + </html:td> + <html:td> + <menulist + id="heightUnitsMenulist" + oncommand="doDimensionEnabling();" + /> + </html:td> + </html:tr> + </html:table> + </hbox> + <spacer flex="1" /> + </vbox> + + <vbox id="imageAppearance"> + <html:legend id="spacingLabel">&spacingBox.label;</html:legend> + <html:table> + <html:tr> + <html:th> + <label + id="leftrightLabel" + class="align-right" + control="imageleftrightInput" + accesskey="&leftRightEditField.accessKey;" + value="&leftRightEditField.label;" + /> + </html:th> + <html:td> + <html:input + id="imageleftrightInput" + type="number" + min="0" + class="narrow input-inline" + aria-labelledby="leftrightLabel" + /> + </html:td> + <html:td id="leftrighttypeLabel"> &pixelsPopup.value; </html:td> + <html:td style="width: 80%"> + <spacer /> + </html:td> + </html:tr> + <html:tr> + <html:th> + <label + id="topbottomLabel" + class="align-right" + control="imagetopbottomInput" + accesskey="&topBottomEditField.accessKey;" + value="&topBottomEditField.label;" + /> + </html:th> + <html:td> + <html:input + id="imagetopbottomInput" + type="number" + min="0" + class="narrow input-inline" + aria-labelledby="topbottomLabel" + /> + </html:td> + <html:td id="topbottomtypeLabel"> &pixelsPopup.value; </html:td> + <html:td> + <spacer /> + </html:td> + </html:tr> + <html:tr> + <html:th> + <label + id="borderLabel" + class="align-right" + control="border" + accesskey="&borderEditField.accessKey;" + value="&borderEditField.label;" + /> + </html:th> + <html:td> + <html:input + id="border" + type="number" + min="0" + class="narrow input-inline" + aria-labelledby="borderLabel" + /> + </html:td> + <html:td id="bordertypeLabel"> &pixelsPopup.value; </html:td> + <html:td> + <spacer /> + </html:td> + </html:tr> + </html:table> + <separator class="thin" /> + <html:legend id="alignLabel">&alignment.label;</html:legend> + <menulist id="alignTypeSelect" class="align-menu"> + <menupopup> + <menuitem + class="align-menu menuitem-iconic" + value="top" + label="&topPopup.value;" + /> + <menuitem + class="align-menu menuitem-iconic" + value="middle" + label="¢erPopup.value;" + /> + <menuitem + class="align-menu menuitem-iconic" + value="bottom" + label="&bottomPopup.value;" + /> + <!-- HTML attribute value is opposite of the button label on purpose --> + <menuitem + class="align-menu menuitem-iconic" + value="right" + label="&wrapLeftPopup.value;" + /> + <menuitem + class="align-menu menuitem-iconic" + value="left" + label="&wrapRightPopup.value;" + /> + </menupopup> + </menulist> + <separator class="thin" /> + <html:legend id="imagemapLabel">&imagemapBox.label;</html:legend> + <html:div class="grid-two-column-equalsize"> + <button + id="removeImageMap" + oncommand="removeImageMap()" + accesskey="&removeImageMapButton.accessKey;" + label="&removeImageMapButton.label;" + /> + <spacer /><!-- remove when we restore Image Map Editor --> + </html:div> + </vbox> + <vbox> + <spacer class="spacer" /> + <vbox id="LinkLocationBox"> + <label + id="hrefLabel" + control="hrefInput" + accesskey="&LinkURLEditField2.accessKey;" + width="1" + >&LinkURLEditField2.label;</label + > + <html:input + id="hrefInput" + type="text" + class="uri-element padded input-inline" + oninput="ChangeLinkLocation();" + aria-labelledby="hrefLabel" + /> + <hbox align="center"> + <checkbox + id="MakeRelativeLink" + for="hrefInput" + label="&makeUrlRelative.label;" + accesskey="&makeUrlRelative.accessKey;" + oncommand="MakeInputValueRelativeOrAbsolute(this);" + tooltiptext="&makeUrlRelative.tooltip;" + /> + <spacer flex="1" /> + <button + label="&chooseFileLinkButton.label;" + accesskey="&chooseFileLinkButton.accessKey;" + oncommand="chooseLinkFile();" + /> + </hbox> + </vbox> + <spacer class="spacer" /> + <hbox> + <checkbox + id="showLinkBorder" + label="&showImageLinkBorder.label;" + accesskey="&showImageLinkBorder.accessKey;" + oncommand="ToggleShowLinkBorder();" + /> + <spacer flex="1" /> + </hbox> + <separator class="thin" /> + <hbox pack="end"> + <button + id="LinkAdvancedEditButton" + label="&LinkAdvancedEditButton.label;" + accesskey="&LinkAdvancedEditButton.accessKey;" + tooltiptext="&LinkAdvancedEditButton.tooltip;" + oncommand="onLinkAdvancedEdit();" + /> + </hbox> + </vbox> + </tabpanels> + </tabbox> + + <spacer flex="1" /> + + <html:fieldset id="imagePreview" hidden="hidden"> + <html:legend>&previewBox.label;</html:legend> + + <html:figure> + <html:img id="preview-image" style="display: inline-block" alt="" /> + <html:figcaption style="float: right"> + <label value="&actualSize.label;" /> + <label id="PreviewWidth" />x<label id="PreviewHeight" /> + </html:figcaption> + </html:figure> + </html:fieldset> + + <hbox pack="end"> + <button + id="AdvancedEditButton1" + oncommand="onAdvancedEdit()" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + + <separator class="groove" /> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdInsSrc.js b/comm/mail/components/compose/content/dialogs/EdInsSrc.js new file mode 100644 index 0000000000..d00f119ed7 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsSrc.js @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Insert Source HTML dialog */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var gFullDataStrings = new Map(); +var gShortDataStrings = new Map(); +var gListenerAttached = false; + +window.addEventListener("load", Startup); + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + let editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + + document + .querySelector("dialog") + .getButton("accept") + .removeAttribute("default"); + + // Create dialog object to store controls for easy access + gDialog.srcInput = document.getElementById("srcInput"); + + // Attach a paste listener so we can detect pasted data URIs we need to shorten. + gDialog.srcInput.addEventListener("paste", onPaste); + + let selection; + try { + selection = editor.outputToString( + "text/html", + kOutputFormatted | kOutputSelectionOnly | kOutputWrap + ); + } catch (e) {} + if (selection) { + selection = selection.replace(/<body[^>]*>/, "").replace(/<\/body>/, ""); + + // Shorten data URIs for display. + selection = replaceDataURIs(selection); + + if (selection) { + gDialog.srcInput.value = selection; + } + } + // Set initial focus + gDialog.srcInput.focus(); + SetWindowLocation(); +} + +function replaceDataURIs(input) { + return input.replace( + /(data:.+;base64,)([^"' >]+)/gi, + function (match, nonDataPart, dataPart) { + if (gShortDataStrings.has(dataPart)) { + // We found the exact same data URI, just return the shortened URI. + return nonDataPart + gShortDataStrings.get(dataPart); + } + + let l = 5; + let key; + // Normally we insert the ellipsis after five characters but if it's not unique + // we include more data. + do { + key = + dataPart.substr(0, l) + "…" + dataPart.substr(dataPart.length - 10); + l++; + } while (gFullDataStrings.has(key) && l < dataPart.length - 10); + gFullDataStrings.set(key, dataPart); + gShortDataStrings.set(dataPart, key); + + // Attach listeners. In case anyone copies/cuts from the HTML window, + // we want to restore the data URI on the clipboard. + if (!gListenerAttached) { + gDialog.srcInput.addEventListener("copy", onCopyOrCut); + gDialog.srcInput.addEventListener("cut", onCopyOrCut); + gListenerAttached = true; + } + + return nonDataPart + key; + } + ); +} + +function onCopyOrCut(event) { + let startPos = gDialog.srcInput.selectionStart; + if (startPos == undefined) { + return; + } + let endPos = gDialog.srcInput.selectionEnd; + let clipboard = gDialog.srcInput.value.substring(startPos, endPos); + + // Add back the original data URIs we stashed away earlier. + clipboard = clipboard.replace( + /(data:.+;base64,)([^"' >]+)/gi, + function (match, nonDataPart, key) { + if (!gFullDataStrings.has(key)) { + // User changed data URI. + return match; + } + return nonDataPart + gFullDataStrings.get(key); + } + ); + event.clipboardData.setData("text/plain", clipboard); + if (event.type == "cut") { + // We have to cut the selection manually. + gDialog.srcInput.value = + gDialog.srcInput.value.substr(0, startPos) + + gDialog.srcInput.value.substr(endPos); + } + event.preventDefault(); +} + +function onPaste(event) { + let startPos = gDialog.srcInput.selectionStart; + if (startPos == undefined) { + return; + } + let endPos = gDialog.srcInput.selectionEnd; + let clipboard = event.clipboardData.getData("text/plain"); + + // We do out own paste by replacing the selection with the pre-processed + // clipboard data. + gDialog.srcInput.value = + gDialog.srcInput.value.substr(0, startPos) + + replaceDataURIs(clipboard) + + gDialog.srcInput.value.substr(endPos); + event.preventDefault(); +} + +function onAccept(event) { + let html = gDialog.srcInput.value; + if (!html) { + event.preventDefault(); + return; + } + + // Add back the original data URIs we stashed away earlier. + html = html.replace( + /(data:.+;base64,)([^"' >]+)/gi, + function (match, nonDataPart, key) { + if (!gFullDataStrings.has(key)) { + // User changed data URI. + return match; + } + return nonDataPart + gFullDataStrings.get(key); + } + ); + + try { + GetCurrentEditor().insertHTML(html); + } catch (e) {} + SaveWindowLocation(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml b/comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml new file mode 100644 index 0000000000..1f35de996d --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE html SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertSource.dtd"> +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + style="min-height: 430px; min-width: 600px" + scrolling="false" +> + <head> + <title>&windowTitle.label;</title> + <link rel="localization" href="branding/brand.ftl" /> + <script + defer="defer" + src="chrome://messenger/content/globalOverlay.js" + ></script> + <script + defer="defer" + src="chrome://global/content/editMenuOverlay.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/dialogShadowDom.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/messengercompose/editorUtilities.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/messengercompose/EdDialogCommon.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/messengercompose/EdInsSrc.js" + ></script> + </head> + <body> + <xul:dialog + buttonlabelaccept="&insertButton.label;" + buttonaccesskeyaccept="&insertButton.accesskey;" + > + <p id="srcMessage">&sourceEditField.label;</p> + <textarea id="srcInput" style="flex: 1" rows="18" cols="70"></textarea> + <p> + &example.label; + <code class="bold"> + &exampleOpenTag.label; + <i>&exampleText.label;</i> &exampleCloseTag.label; + </code> + </p> + <hr /> + </xul:dialog> + </body> +</html> diff --git a/comm/mail/components/compose/content/dialogs/EdInsertChars.js b/comm/mail/components/compose/content/dialogs/EdInsertChars.js new file mode 100644 index 0000000000..b710fb91a0 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertChars.js @@ -0,0 +1,412 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +// ------------------------------------------------------------------ +// From Unicode 3.0 Page 54. 3.11 Conjoining Jamo Behavior +var SBase = 0xac00; +var LBase = 0x1100; +var VBase = 0x1161; +var TBase = 0x11a7; +var LCount = 19; +var VCount = 21; +var TCount = 28; +var NCount = VCount * TCount; +// End of Unicode 3.0 + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onClose); + +// dialog initialization code +function Startup() { + if (!GetCurrentEditor()) { + window.close(); + return; + } + + StartupLatin(); + + // Set a variable on the opener window so we + // can track ownership of close this window with it + window.opener.InsertCharWindow = window; + window.sizeToContent(); + + SetWindowLocation(); +} + +function onAccept(event) { + // Insert the character + try { + GetCurrentEditor().insertText(LatinM.label); + } catch (e) {} + + // Set persistent attributes to save + // which category, letter, and character modifier was used + CategoryGroup.setAttribute("category", category); + CategoryGroup.setAttribute("letter_index", indexL); + CategoryGroup.setAttribute("char_index", indexM); + + // Don't close the dialog + event.preventDefault(); +} + +// Don't allow inserting in HTML Source Mode +function onFocus() { + var enable = true; + if ("gEditorDisplayMode" in window.opener) { + enable = !window.opener.IsInHTMLSourceMode(); + } + + SetElementEnabled( + document.querySelector("dialog").getButton("accept"), + enable + ); +} + +function onClose() { + window.opener.InsertCharWindow = null; + SaveWindowLocation(); +} + +// ------------------------------------------------------------------ +var LatinL; +var LatinM; +var LatinL_Label; +var LatinM_Label; +var indexL = 0; +var indexM = 0; +var indexM_AU = 0; +var indexM_AL = 0; +var indexM_U = 0; +var indexM_L = 0; +var indexM_S = 0; +var LItems = 0; +var category; +var CategoryGroup; +var initialize = true; + +function StartupLatin() { + LatinL = document.getElementById("LatinL"); + LatinM = document.getElementById("LatinM"); + LatinL_Label = document.getElementById("LatinL_Label"); + LatinM_Label = document.getElementById("LatinM_Label"); + + var Symbol = document.getElementById("Symbol"); + var AccentUpper = document.getElementById("AccentUpper"); + var AccentLower = document.getElementById("AccentLower"); + var Upper = document.getElementById("Upper"); + var Lower = document.getElementById("Lower"); + CategoryGroup = document.getElementById("CatGrp"); + + // Initialize which radio button is set from persistent attribute... + var category = CategoryGroup.getAttribute("category"); + + // ...as well as indexes into the letter and character lists + var index = Number(CategoryGroup.getAttribute("letter_index")); + if (index && index >= 0) { + indexL = index; + } + index = Number(CategoryGroup.getAttribute("char_index")); + if (index && index >= 0) { + indexM = index; + } + + switch (category) { + case "AccentUpper": // Uppercase Diacritical + CategoryGroup.selectedItem = AccentUpper; + indexM_AU = indexM; + break; + case "AccentLower": // Lowercase Diacritical + CategoryGroup.selectedItem = AccentLower; + indexM_AL = indexM; + break; + case "Upper": // Uppercase w/o Diacritical + CategoryGroup.selectedItem = Upper; + indexM_U = indexM; + break; + case "Lower": // Lowercase w/o Diacritical + CategoryGroup.selectedItem = Lower; + indexM_L = indexM; + break; + default: + category = "Symbol"; + CategoryGroup.selectedItem = Symbol; + indexM_S = indexM; + break; + } + + ChangeCategory(category); + initialize = false; +} + +function ChangeCategory(newCategory) { + if (category != newCategory || initialize) { + category = newCategory; + // Note: Must do L before M to set LatinL.selectedIndex + UpdateLatinL(); + UpdateLatinM(); + UpdateCharacter(); + } +} + +function SelectLatinLetter() { + if (LatinL.selectedIndex != indexL) { + indexL = LatinL.selectedIndex; + UpdateLatinM(); + UpdateCharacter(); + } +} + +function SelectLatinModifier() { + if (LatinM.selectedIndex != indexM) { + indexM = LatinM.selectedIndex; + UpdateCharacter(); + } +} +function DisableLatinL(disable) { + if (disable) { + LatinL_Label.setAttribute("disabled", "true"); + LatinL.setAttribute("disabled", "true"); + } else { + LatinL_Label.removeAttribute("disabled"); + LatinL.removeAttribute("disabled"); + } +} + +function UpdateLatinL() { + LatinL.removeAllItems(); + if (category == "AccentUpper" || category == "AccentLower") { + DisableLatinL(false); + // No Q or q + var alphabet = + category == "AccentUpper" + ? "ABCDEFGHIJKLMNOPRSTUVWXYZ" + : "abcdefghijklmnoprstuvwxyz"; + for (var letter = 0; letter < alphabet.length; letter++) { + LatinL.appendItem(alphabet.charAt(letter)); + } + + LatinL.selectedIndex = indexL; + } else { + // Other categories don't hinge on a "letter" + DisableLatinL(true); + // Note: don't change the indexL so it can be used next time + } +} + +function UpdateLatinM() { + LatinM.removeAllItems(); + var i, accent; + switch (category) { + case "AccentUpper": // Uppercase Diacritical + accent = upper[indexL]; + for (i = 0; i < accent.length; i++) { + LatinM.appendItem(accent.charAt(i)); + } + + if (indexM_AU < accent.length) { + indexM = indexM_AU; + } else { + indexM = accent.length - 1; + } + indexM_AU = indexM; + break; + + case "AccentLower": // Lowercase Diacritical + accent = lower[indexL]; + for (i = 0; i < accent.length; i++) { + LatinM.appendItem(accent.charAt(i)); + } + + if (indexM_AL < accent.length) { + indexM = indexM_AL; + } else { + indexM = lower[indexL].length - 1; + } + indexM_AL = indexM; + break; + + case "Upper": // Uppercase w/o Diacritical + for (i = 0; i < otherupper.length; i++) { + LatinM.appendItem(otherupper.charAt(i)); + } + + if (indexM_U < otherupper.length) { + indexM = indexM_U; + } else { + indexM = otherupper.length - 1; + } + indexM_U = indexM; + break; + + case "Lower": // Lowercase w/o Diacritical + for (i = 0; i < otherlower.length; i++) { + LatinM.appendItem(otherlower.charAt(i)); + } + + if (indexM_L < otherlower.length) { + indexM = indexM_L; + } else { + indexM = otherlower.length - 1; + } + indexM_L = indexM; + break; + + case "Symbol": // Symbol + for (i = 0; i < symbol.length; i++) { + LatinM.appendItem(symbol.charAt(i)); + } + + if (indexM_S < symbol.length) { + indexM = indexM_S; + } else { + indexM = symbol.length - 1; + } + indexM_S = indexM; + break; + } + LatinM.selectedIndex = indexM; +} + +function UpdateCharacter() { + indexM = LatinM.selectedIndex; + + switch (category) { + case "AccentUpper": // Uppercase Diacritical + indexM_AU = indexM; + break; + case "AccentLower": // Lowercase Diacritical + indexM_AL = indexM; + break; + case "Upper": // Uppercase w/o Diacritical + indexM_U = indexM; + break; + case "Lower": // Lowercase w/o Diacritical + indexM_L = indexM; + break; + case "Symbol": + indexM_S = indexM; + break; + } + // dump("Letter Index="+indexL+", Character Index="+indexM+", Character = "+LatinM.label+"\n"); +} + +const upper = [ + // A + "\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u0100\u0102\u0104\u01cd\u01de\u01de\u01e0\u01fa\u0200\u0202\u0226\u1e00\u1ea0\u1ea2\u1ea4\u1ea6\u1ea8\u1eaa\u1eac\u1eae\u1eb0\u1eb2\u1eb4\u1eb6", + // B + "\u0181\u0182\u0184\u1e02\u1e04\u1e06", + // C + "\u00c7\u0106\u0108\u010a\u010c\u0187\u1e08", + // D + "\u010e\u0110\u0189\u018a\u1e0a\u1e0c\u1e0e\u1e10\u1e12", + // E + "\u00C8\u00C9\u00CA\u00CB\u0112\u0114\u0116\u0118\u011A\u0204\u0206\u0228\u1e14\u1e16\u1e18\u1e1a\u1e1c\u1eb8\u1eba\u1ebc\u1ebe\u1ec0\u1ec2\u1ec4\u1ec6", + // F + "\u1e1e", + // G + "\u011c\u011E\u0120\u0122\u01e4\u01e6\u01f4\u1e20", + // H + "\u0124\u0126\u021e\u1e22\u1e24\u1e26\u1e28\u1e2a", + // I + "\u00CC\u00CD\u00CE\u00CF\u0128\u012a\u012C\u012e\u0130\u0208\u020a\u1e2c\u1e2e\u1ec8\u1eca", + // J + "\u0134\u01f0", + // K + "\u0136\u0198\u01e8\u1e30\u1e32\u1e34", + // L + "\u0139\u013B\u013D\u013F\u0141\u1e36\u1e38\u1e3a\u1e3c", + // M + "\u1e3e\u1e40\u1e42", + // N + "\u00D1\u0143\u0145\u0147\u014A\u01F8\u1e44\u1e46\u1e48\u1e4a", + // O + "\u00D2\u00D3\u00D4\u00D5\u00D6\u014C\u014E\u0150\u01ea\u01ec\u020c\u020e\u022A\u022C\u022E\u0230\u1e4c\u1e4e\u1e50\u1e52\u1ecc\u1ece\u1ed0\u1ed2\u1ed4\u1ed6\u1ed8\u1eda\u1edc\u1ede\u1ee0\u1ee2", + // P + "\u1e54\u1e56", + // No Q + // R + "\u0154\u0156\u0158\u0210\u0212\u1e58\u1e5a\u1e5c\u1e5e", + // S + "\u015A\u015C\u015E\u0160\u0218\u1e60\u1e62\u1e64\u1e66\u1e68", + // T + "\u0162\u0164\u0166\u021A\u1e6a\u1e6c\u1e6e\u1e70", + // U + "\u00D9\u00DA\u00DB\u00DC\u0168\u016A\u016C\u016E\u0170\u0172\u0214\u0216\u1e72\u1e74\u1e76\u1e78\u1e7a\u1ee4\u1ee6\u1ee8\u1eea\u1eec\u1eee\u1ef0", + // V + "\u1e7c\u1e7e", + // W + "\u0174\u1e80\u1e82\u1e84\u1e86\u1e88", + // X + "\u1e8a\u1e8c", + // Y + "\u00DD\u0176\u0178\u0232\u1e8e\u1ef2\u1ef4\u1ef6\u1ef8", + // Z + "\u0179\u017B\u017D\u0224\u1e90\u1e92\u1e94", +]; + +const lower = [ + // a + "\u00e0\u00e1\u00e2\u00e3\u00e4\u00e5\u0101\u0103\u0105\u01ce\u01df\u01e1\u01fb\u0201\u0203\u0227\u1e01\u1e9a\u1ea1\u1ea3\u1ea5\u1ea7\u1ea9\u1eab\u1ead\u1eaf\u1eb1\u1eb3\u1eb5\u1eb7", + // b + "\u0180\u0183\u0185\u1e03\u1e05\u1e07", + // c + "\u00e7\u0107\u0109\u010b\u010d\u0188\u1e09", + // d + "\u010f\u0111\u1e0b\u1e0d\u1e0f\u1e11\u1e13", + // e + "\u00e8\u00e9\u00ea\u00eb\u0113\u0115\u0117\u0119\u011b\u0205\u0207\u0229\u1e15\u1e17\u1e19\u1e1b\u1e1d\u1eb9\u1ebb\u1ebd\u1ebf\u1ec1\u1ec3\u1ec5\u1ec7", + // f + "\u1e1f", + // g + "\u011d\u011f\u0121\u0123\u01e5\u01e7\u01f5\u1e21", + // h + "\u0125\u0127\u021f\u1e23\u1e25\u1e27\u1e29\u1e2b\u1e96", + // i + "\u00ec\u00ed\u00ee\u00ef\u0129\u012b\u012d\u012f\u0131\u01d0\u0209\u020b\u1e2d\u1e2f\u1ec9\u1ecb", + // j + "\u0135", + // k + "\u0137\u0138\u01e9\u1e31\u1e33\u1e35", + // l + "\u013a\u013c\u013e\u0140\u0142\u1e37\u1e39\u1e3b\u1e3d", + // m + "\u1e3f\u1e41\u1e43", + // n + "\u00f1\u0144\u0146\u0148\u0149\u014b\u01f9\u1e45\u1e47\u1e49\u1e4b", + // o + "\u00f2\u00f3\u00f4\u00f5\u00f6\u014d\u014f\u0151\u01d2\u01eb\u01ed\u020d\u020e\u022b\u022d\u022f\u0231\u1e4d\u1e4f\u1e51\u1e53\u1ecd\u1ecf\u1ed1\u1ed3\u1ed5\u1ed7\u1ed9\u1edb\u1edd\u1edf\u1ee1\u1ee3", + // p + "\u1e55\u1e57", + // No q + // r + "\u0155\u0157\u0159\u0211\u0213\u1e59\u1e5b\u1e5d\u1e5f", + // s + "\u015b\u015d\u015f\u0161\u0219\u1e61\u1e63\u1e65\u1e67\u1e69", + // t + "\u0162\u0163\u0165\u0167\u021b\u1e6b\u1e6d\u1e6f\u1e71\u1e97", + // u + "\u00f9\u00fa\u00fb\u00fc\u0169\u016b\u016d\u016f\u0171\u0173\u01d4\u01d6\u01d8\u01da\u01dc\u0215\u0217\u1e73\u1e75\u1e77\u1e79\u1e7b\u1ee5\u1ee7\u1ee9\u1eeb\u1eed\u1eef\u1ef1", + // v + "\u1e7d\u1e7f", + // w + "\u0175\u1e81\u1e83\u1e85\u1e87\u1e89\u1e98", + // x + "\u1e8b\u1e8d", + // y + "\u00fd\u00ff\u0177\u0233\u1e8f\u1e99\u1ef3\u1ef5\u1ef7\u1ef9", + // z + "\u017a\u017c\u017e\u0225\u1e91\u1e93\u1e95", +]; + +const symbol = + "\u00a1\u00a2\u00a3\u00a4\u00a5\u20ac\u00a6\u00a7\u00a8\u00a9\u00aa\u00ab\u00ac\u00ae\u00af\u00b0\u00b1\u00b2\u00b3\u00b4\u00b5\u00b6\u00b7\u00b8\u00b9\u00ba\u00bb\u00bc\u00bd\u00be\u00bf\u00d7\u00f7"; + +const otherupper = + "\u00c6\u00d0\u00d8\u00de\u0132\u0152\u0186\u01c4\u01c5\u01c7\u01c8\u01ca\u01cb\u01F1\u01f2"; + +const otherlower = + "\u00e6\u00f0\u00f8\u00fe\u00df\u0133\u0153\u01c6\u01c9\u01cc\u01f3"; diff --git a/comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml new file mode 100644 index 0000000000..c610abdd88 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml @@ -0,0 +1,92 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/EdInsertChars.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertChars.dtd"> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="Startup()" + onfocus="onFocus()" + lightweightthemes="true" + style="min-width: 20em" +> + <dialog + id="insertCharsDlg" + buttonlabelaccept="&insertButton.label;" + buttonlabelcancel="&closeButton.label;" + > + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdInsertChars.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <html:fieldset> + <html:legend>&category.label;</html:legend> + <radiogroup id="CatGrp" persist="category letter_index char_index"> + <radio + id="AccentUpper" + label="&accentUpper.label;" + oncommand="ChangeCategory(this.id)" + /> + <radio + id="AccentLower" + label="&accentLower.label;" + oncommand="ChangeCategory(this.id)" + /> + <radio + id="Upper" + label="&otherUpper.label;" + oncommand="ChangeCategory(this.id)" + /> + <radio + id="Lower" + label="&otherLower.label;" + oncommand="ChangeCategory(this.id)" + /> + <radio + id="Symbol" + label="&commonSymbols.label;" + oncommand="ChangeCategory(this.id)" + /> + </radiogroup> + <spacer class="spacer" /> + </html:fieldset> + <html:div class="grid-two-column-equalsize"> + <!-- value is set in JS from editor.properties strings --> + <label + id="LatinL_Label" + control="LatinL" + value="&letter.label;" + accesskey="&letter.accessKey;" + /> + <menulist id="LatinL" oncommand="SelectLatinLetter()"> + <menupopup /> + </menulist> + <label + id="LatinM_Label" + control="LatinM" + value="&character.label;" + accesskey="&character.accessKey;" + /> + <menulist id="LatinM" oncommand="SelectLatinModifier()"> + <menupopup /> + </menulist> + </html:div> + <separator class="groove" /> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdInsertMath.js b/comm/mail/components/compose/content/dialogs/EdInsertMath.js new file mode 100644 index 0000000000..a60a3affcc --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertMath.js @@ -0,0 +1,317 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Insert MathML dialog */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + var editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + + // Create dialog object for easy access + gDialog.accept = document.querySelector("dialog").getButton("accept"); + gDialog.mode = document.getElementById("optionMode"); + gDialog.direction = document.getElementById("optionDirection"); + gDialog.input = document.getElementById("input"); + gDialog.output = document.getElementById("output"); + gDialog.tabbox = document.getElementById("tabboxInsertLaTeXCommand"); + + // Set initial focus + gDialog.input.focus(); + + // Load TeXZilla + // TeXZilla.js contains non-ASCII characters and explicitly sets + // window.TeXZilla, so we have to specify the charset parameter but don't + // need to worry about the targetObj parameter. + /* globals TeXZilla */ + Services.scriptloader.loadSubScript( + "chrome://messenger/content/messengercompose/TeXZilla.js", + {}, + "UTF-8" + ); + + // Verify if the selection is on a <math> and initialize the dialog. + gDialog.oldMath = editor.getElementOrParentByTagName("math", null); + if (gDialog.oldMath) { + // When these attributes are absent or invalid, they default to "inline" and "ltr" respectively. + gDialog.mode.selectedIndex = + gDialog.oldMath.getAttribute("display") == "block" ? 1 : 0; + gDialog.direction.selectedIndex = + gDialog.oldMath.getAttribute("dir") == "rtl" ? 1 : 0; + gDialog.input.value = TeXZilla.getTeXSource(gDialog.oldMath); + } + + // Create the tabbox with LaTeX commands. + createCommandPanel({ + "√⅗²": [ + "{⋯}^{⋯}", + "{⋯}_{⋯}", + "{⋯}_{⋯}^{⋯}", + "\\underset{⋯}{⋯}", + "\\overset{⋯}{⋯}", + "\\underoverset{⋯}{⋯}{⋯}", + "\\left(⋯\\right)", + "\\left[⋯\\right]", + "\\frac{⋯}{⋯}", + "\\binom{⋯}{⋯}", + "\\sqrt{⋯}", + "\\sqrt[⋯]{⋯}", + "\\cos\\left({⋯}\\right)", + "\\sin\\left({⋯}\\right)", + "\\tan\\left({⋯}\\right)", + "\\exp\\left({⋯}\\right)", + "\\ln\\left({⋯}\\right)", + "\\underbrace{⋯}", + "\\underline{⋯}", + "\\overbrace{⋯}", + "\\widevec{⋯}", + "\\widetilde{⋯}", + "\\widehat{⋯}", + "\\widecheck{⋯}", + "\\widebar{⋯}", + "\\dot{⋯}", + "\\ddot{⋯}", + "\\boxed{⋯}", + "\\slash{⋯}", + ], + "(▦)": [ + "\\begin{matrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{matrix}", + "\\begin{pmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{pmatrix}", + "\\begin{bmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{bmatrix}", + "\\begin{Bmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{Bmatrix}", + "\\begin{vmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{vmatrix}", + "\\begin{Vmatrix} ⋯ & ⋯ \\\\ ⋯ & ⋯ \\end{Vmatrix}", + "\\begin{cases} ⋯ \\\\ ⋯ \\end{cases}", + "\\begin{aligned} ⋯ &= ⋯ \\\\ ⋯ &= ⋯ \\end{aligned}", + ], + }); + createSymbolPanels([ + "∏∐∑∫∬∭⨌∮⊎⊕⊖⊗⊘⊙⋀⋁⋂⋃⌈⌉⌊⌋⎰⎱⟨⟩⟪⟫∥⫼⨀⨁⨂⨄⨅⨆ðıȷℏℑℓ℘ℜℵℶ", + "∀∃∄∅∉∊∋∌⊂⊃⊄⊅⊆⊇⊈⊈⊉⊊⊊⊋⊋⊏⊐⊑⊒⊓⊔⊥⋐⋑⋔⫅⫆⫋⫋⫌⫌…⋮⋯⋰⋱♭♮♯∂∇", + "±×÷†‡•∓∔∗∘∝∠∡∢∧∨∴∵∼∽≁≃≅≇≈≈≊≍≎≏≐≑≒≓≖≗≜≡≢≬⊚⊛⊞⊡⊢⊣⊤⊥", + "⊨⊩⊪⊫⊬⊭⊯⊲⊲⊳⊴⊵⊸⊻⋄⋅⋇⋈⋉⋊⋋⋌⋍⋎⋏⋒⋓⌅⌆⌣△▴▵▸▹▽▾▿◂◃◊○★♠♡♢♣⧫", + "≦≧≨≩≩≪≫≮≯≰≱≲≳≶≷≺≻≼≽≾≿⊀⊁⋖⋗⋘⋙⋚⋛⋞⋟⋦⋧⋨⋩⩽⩾⪅⪆⪇⪈⪉⪊⪋⪌⪕⪯⪰⪷⪸⪹⪺", + "←↑→↓↔↕↖↗↘↙↜↝↞↠↢↣↦↩↪↫↬↭↭↰↱↼↽↾↿⇀⇁⇂⇃⇄⇆⇇⇈⇉⇊⇋⇌⇐⇑⇒⇓⇕⇖⇗⇘⇙⟺", + "αβγδϵ϶εζηθϑικϰλμνξℴπϖρϱσςτυϕφχψωΓΔΘΛΞΠΣϒΦΨΩϝ℧", + "𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ", + "𝒶𝒷𝒸𝒹ℯ𝒻ℊ𝒽𝒾𝒿𝓀𝓁𝓂𝓃ℴ𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏𝒜ℬ𝒞𝒟ℰℱ𝒢ℋℐ𝒥𝒦ℒℳ𝒩𝒪𝒫𝒬ℛ𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵", + "𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ", + ]); + gDialog.tabbox.selectedIndex = 0; + + updateMath(); + + SetWindowLocation(); +} + +function insertLaTeXCommand(aButton) { + gDialog.input.focus(); + + // For a single math symbol, just use the insertText command. + if (aButton.label) { + gDialog.input.editor.insertText(aButton.label); + return; + } + + // Otherwise, it's a LaTeX command with at least one argument... + var latex = TeXZilla.getTeXSource(aButton.firstElementChild); + var selectionStart = gDialog.input.selectionStart; + var selectionEnd = gDialog.input.selectionEnd; + + // If the selection is not empty, we replace the first argument of the LaTeX + // command with the current selection. + var selection = gDialog.input.value.substring(selectionStart, selectionEnd); + if (selection != "") { + latex = latex.replace("⋯", selection); + } + + // Try and move to the next position. + var latexNewStart = latex.indexOf("⋯"), + latexNewEnd; + if (latexNewStart == -1) { + // This is a unary function and the selection was used as an argument above. + // We select the expression again so that one can choose to apply further + // command to it or just move the caret after that text. + latexNewStart = 0; + latexNewEnd = latex.length; + } else { + // Otherwise, select the dots representing the next argument. + latexNewEnd = latexNewStart + 1; + } + + // Update the input text and selection. + gDialog.input.editor.insertText(latex); + gDialog.input.setSelectionRange( + selectionStart + latexNewStart, + selectionStart + latexNewEnd + ); + + updateMath(); +} + +function createCommandPanel(aCommandPanelList) { + const columnCount = 10; + + for (var label in aCommandPanelList) { + var commands = aCommandPanelList[label]; + + // Create the <table> element with the <tr>. + var table = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "table" + ); + + var i = 0, + row; + for (var command of commands) { + if (i % columnCount == 0) { + // Create a new row. + row = document.createElementNS("http://www.w3.org/1999/xhtml", "tr"); + table.appendChild(row); + } + + // Create a new button to insert the symbol. + var button = document.createXULElement("toolbarbutton"); + var td = document.createElementNS("http://www.w3.org/1999/xhtml", "td"); + button.setAttribute("class", "tabbable"); + button.appendChild(TeXZilla.toMathML(command)); + td.append(button); + row.appendChild(td); + + i++; + } + + // Create a new <tab> element. + var tab = document.createXULElement("tab"); + tab.setAttribute("label", label); + gDialog.tabbox.tabs.appendChild(tab); + + // Append the new tab panel. + gDialog.tabbox.tabpanels.appendChild(table); + } +} + +function createSymbolPanels(aSymbolPanelList) { + const columnCount = 13, + tabLabelLength = 3; + + for (var symbols of aSymbolPanelList) { + // Create the <table> element with the <tr>. + var table = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "table" + ); + var i = 0, + tabLabel = "", + row; + for (var symbol of symbols) { + if (i % columnCount == 0) { + // Create a new row. + row = document.createElementNS("http://www.w3.org/1999/xhtml", "tr"); + table.appendChild(row); + } + + // Build the tab label from the first symbols of this tab. + if (i < tabLabelLength) { + tabLabel += symbol; + } + + // Create a new button to insert the symbol. + var button = document.createXULElement("toolbarbutton"); + var td = document.createElementNS("http://www.w3.org/1999/xhtml", "td"); + button.setAttribute("label", symbol); + button.setAttribute("class", "tabbable"); + td.append(button); + row.appendChild(td); + + i++; + } + + // Create a new <tab> element with the label determined above. + var tab = document.createXULElement("tab"); + tab.setAttribute("label", tabLabel); + gDialog.tabbox.tabs.appendChild(tab); + + // Append the new tab panel. + gDialog.tabbox.tabpanels.appendChild(table); + } +} + +function onAccept(event) { + if (gDialog.output.firstElementChild) { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + + try { + var newMath = editor.document.importNode( + gDialog.output.firstElementChild, + true + ); + if (gDialog.oldMath) { + // Replace the old <math> element with the new one. + editor.selectElement(gDialog.oldMath); + editor.insertElementAtSelection(newMath, true); + } else { + // Insert the new <math> element. + editor.insertElementAtSelection(newMath, false); + } + } catch (e) {} + + editor.endTransaction(); + } else { + dump("Null value -- not inserting in MathML Source dialog\n"); + event.preventDefault(); + } + SaveWindowLocation(); +} + +function updateMath() { + // Remove the preview, if any. + if (gDialog.output.firstElementChild) { + gDialog.output.firstElementChild.remove(); + } + + // Try to convert the LaTeX source into MathML using TeXZilla. + // We use the placeholder text if no input is provided. + try { + var input = gDialog.input.value || gDialog.input.placeholder; + var newMath = TeXZilla.toMathML( + input, + gDialog.mode.selectedIndex, + gDialog.direction.selectedIndex, + true + ); + gDialog.output.appendChild(document.importNode(newMath, true)); + gDialog.output.style.opacity = gDialog.input.value ? 1 : 0.5; + } catch (e) {} + // Disable the accept button if parsing fails or when the placeholder is used. + gDialog.accept.disabled = + !gDialog.input.value || !gDialog.output.firstElementChild; +} + +function updateMode() { + if (gDialog.output.firstElementChild) { + gDialog.output.firstElementChild.setAttribute( + "display", + gDialog.mode.selectedIndex ? "block" : "inline" + ); + } +} + +function updateDirection() { + if (gDialog.output.firstElementChild) { + gDialog.output.firstElementChild.setAttribute( + "dir", + gDialog.direction.selectedIndex ? "rtl" : "ltr" + ); + } +} diff --git a/comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml new file mode 100644 index 0000000000..d76a518b0a --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public +- License, v. 2.0. If a copy of the MPL was not distributed with this +- file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertMath.dtd"> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup();" +> + <dialog + buttonlabelaccept="&insertButton.label;" + buttonaccesskeyaccept="&insertButton.accesskey;" + > + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdInsertMath.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <label id="srcMessage" value="&sourceEditField.label;" /> + <html:textarea + id="input" + rows="5" + oninput="updateMath();" + placeholder="\sqrt{x_1} + \frac{π^3}{2}" + /> + <vbox flex="1" style="overflow: auto; width: 30em; height: 5em"> + <description id="output" /> + </vbox> + <tabbox id="tabboxInsertLaTeXCommand"> + <tabs /> + <tabpanels oncommand="insertLaTeXCommand(event.target);" /> + </tabbox> + <spacer class="spacer" /> + <html:fieldset> + <html:legend>&options.label;</html:legend> + <hbox> + <radiogroup id="optionMode" oncommand="updateMode();"> + <radio + label="&optionInline.label;" + accesskey="&optionInline.accesskey;" + /> + <radio + label="&optionDisplay.label;" + accesskey="&optionDisplay.accesskey;" + /> + </radiogroup> + <radiogroup id="optionDirection" oncommand="updateDirection();"> + <radio label="&optionLTR.label;" accesskey="&optionLTR.accesskey;" /> + <radio label="&optionRTL.label;" accesskey="&optionRTL.accesskey;" /> + </radiogroup> + </hbox> + </html:fieldset> + <spacer class="spacer" /> + <separator class="groove" /> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTOC.js b/comm/mail/components/compose/content/dialogs/EdInsertTOC.js new file mode 100644 index 0000000000..45d0972f3b --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertTOC.js @@ -0,0 +1,378 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +// tocHeadersArray is the array containing the pairs tag/class +// defining TOC entries +var tocHeadersArray = new Array(6); + +// a global used when building the TOC +var currentHeaderLevel = 0; + +// a global set to true if the TOC is to be readonly +var readonly = false; + +// a global set to true if user wants indexes in the TOC +var orderedList = true; + +// constants +const kMozToc = "mozToc"; +const kMozTocLength = 6; +const kMozTocIdPrefix = "mozTocId"; +const kMozTocIdPrefixLength = 8; +const kMozTocClassPrefix = "mozToc"; +const kMozTocClassPrefixLength = 6; + +document.addEventListener("dialogaccept", () => BuildTOC(true)); + +// Startup() is called when EdInsertTOC.xhtml is opened +function Startup() { + // early way out if if we have no editor + if (!GetCurrentEditor()) { + window.close(); + return; + } + + var i; + // clean the table of tag/class pairs we look for + for (i = 0; i < 6; ++i) { + tocHeadersArray[i] = ["", ""]; + } + + // reset all settings + for (i = 1; i < 7; ++i) { + var menulist = document.getElementById("header" + i + "Menulist"); + var menuitem = document.getElementById("header" + i + "none"); + var textbox = document.getElementById("header" + i + "Class"); + menulist.selectedItem = menuitem; + textbox.setAttribute("disabled", "true"); + } + + var theDocument = GetCurrentEditor().document; + + // do we already have a TOC in the document ? It should have "mozToc" ID + var toc = theDocument.getElementById(kMozToc); + + // default TOC definition, use h1-h6 for TOC entry levels 1-6 + var headers = "h1 1 h2 2 h3 3 h4 4 h5 5 h6 6"; + + var orderedListCheckbox = document.getElementById("orderedListCheckbox"); + orderedListCheckbox.checked = true; + + if (toc) { + // man, there is already a TOC here + + if (toc.getAttribute("class") == "readonly") { + // and it's readonly + var checkbox = document.getElementById("readOnlyCheckbox"); + checkbox.checked = true; + readonly = true; + } + + // let's see if it's an OL or an UL + orderedList = toc.nodeName.toLowerCase() == "ol"; + orderedListCheckbox.checked = orderedList; + + var nodeList = toc.childNodes; + // let's look at the children of the TOC ; if we find a comment beginning + // with "mozToc", it contains the TOC definition + for (i = 0; i < nodeList.length; ++i) { + if ( + nodeList.item(i).nodeType == Node.COMMENT_NODE && + nodeList.item(i).data.startsWith(kMozToc) + ) { + // yep, there is already a definition here; parse it ! + headers = nodeList + .item(i) + .data.substr( + kMozTocLength + 1, + nodeList.item(i).length - kMozTocLength - 1 + ); + break; + } + } + } + + // let's get an array filled with the (tag.class, index level) pairs + var headersArray = headers.split(" "); + + for (i = 0; i < headersArray.length; i += 2) { + var tag = headersArray[i], + className = ""; + var index = headersArray[i + 1]; + menulist = document.getElementById("header" + index + "Menulist"); + if (menulist) { + var sep = tag.indexOf("."); + if (sep != -1) { + // the tag variable contains in fact "tag.className", let's parse + // the class and get the real tag name + var tmp = tag.substr(0, sep); + className = tag.substr(sep + 1, tag.length - sep - 1); + tag = tmp; + } + + // update the dialog + menuitem = document.getElementById("header" + index + tag.toUpperCase()); + textbox = document.getElementById("header" + index + "Class"); + menulist.selectedItem = menuitem; + if (tag != "") { + textbox.removeAttribute("disabled"); + } + if (className != "") { + textbox.value = className; + } + tocHeadersArray[index - 1] = [tag, className]; + } + } +} + +function BuildTOC(update) { + // controlClass() is a node filter that accepts a node if + // (a) we don't look for a class (b) we look for a class and + // node has it + function controlClass(node, index) { + currentHeaderLevel = index + 1; + if (tocHeadersArray[index][1] == "") { + // we are not looking for a specific class, this node is ok + return NodeFilter.FILTER_ACCEPT; + } + if (node.getAttribute("class")) { + // yep, we look for a class, let's look at all the classes + // the node has + var classArray = node.getAttribute("class").split(" "); + for (var j = 0; j < classArray.length; j++) { + if (classArray[j] == tocHeadersArray[index][1]) { + // hehe, we found it... + return NodeFilter.FILTER_ACCEPT; + } + } + } + return NodeFilter.FILTER_SKIP; + } + + // the main node filter for our node iterator + // it selects the tag names as specified in the dialog + // then calls the controlClass filter above + function acceptNode(node) { + switch (node.nodeName.toLowerCase()) { + case tocHeadersArray[0][0]: + return controlClass(node, 0); + case tocHeadersArray[1][0]: + return controlClass(node, 1); + case tocHeadersArray[2][0]: + return controlClass(node, 2); + case tocHeadersArray[3][0]: + return controlClass(node, 3); + case tocHeadersArray[4][0]: + return controlClass(node, 4); + case tocHeadersArray[5][0]: + return controlClass(node, 5); + default: + return NodeFilter.FILTER_SKIP; + } + } + + var editor = GetCurrentEditor(); + var theDocument = editor.document; + // let's create a TreeWalker to look for our nodes + var treeWalker = theDocument.createTreeWalker( + theDocument.documentElement, + NodeFilter.SHOW_ELEMENT, + acceptNode, + true + ); + // we need an array to store all TOC entries we find in the document + var tocArray = []; + if (treeWalker) { + var tocSourceNode = treeWalker.nextNode(); + while (tocSourceNode) { + var headerIndex = currentHeaderLevel; + + // we have a node, we need to get all its textual contents + var textTreeWalker = theDocument.createTreeWalker( + tocSourceNode, + NodeFilter.SHOW_TEXT, + null, + true + ); + var textNode = textTreeWalker.nextNode(), + headerText = ""; + while (textNode) { + headerText += textNode.data; + textNode = textTreeWalker.nextNode(); + } + + var anchor = tocSourceNode.firstChild, + id; + // do we have a named anchor as 1st child of our node ? + if ( + anchor.nodeName.toLowerCase() == "a" && + anchor.hasAttribute("name") && + anchor.getAttribute("name").startsWith(kMozTocIdPrefix) + ) { + // yep, get its name + id = anchor.getAttribute("name"); + } else { + // no we don't and we need to create one + anchor = theDocument.createElement("a"); + tocSourceNode.insertBefore(anchor, tocSourceNode.firstChild); + // let's give it a random ID + var c = 1000000 * Math.random(); + id = kMozTocIdPrefix + Math.round(c); + anchor.setAttribute("name", id); + anchor.setAttribute( + "class", + kMozTocClassPrefix + tocSourceNode.nodeName.toUpperCase() + ); + } + // and store that new entry in our array + tocArray.push(headerIndex, headerText, id); + tocSourceNode = treeWalker.nextNode(); + } + } + + /* generate the TOC itself */ + headerIndex = 0; + var item, toc; + for (var i = 0; i < tocArray.length; i += 3) { + if (!headerIndex) { + // do we need to create an ol/ul container for the first entry ? + ++headerIndex; + toc = theDocument.getElementById(kMozToc); + if (!toc || !update) { + // we need to create a list container for the table of contents + toc = GetCurrentEditor().createElementWithDefaults( + orderedList ? "ol" : "ul" + ); + // grrr, we need to create a LI inside the list otherwise + // Composer will refuse an empty list and will remove it ! + var pit = theDocument.createElement("li"); + toc.appendChild(pit); + GetCurrentEditor().insertElementAtSelection(toc, true); + // ah, now it's inserted so let's remove the useless list item... + toc.removeChild(pit); + // we need to recognize later that this list is our TOC + toc.setAttribute("id", kMozToc); + } else if (orderedList != (toc.nodeName.toLowerCase() == "ol")) { + // we have to update an existing TOC, is the existing TOC of the + // desired type (ordered or not) ? + + // nope, we have to recreate the list + var newToc = GetCurrentEditor().createElementWithDefaults( + orderedList ? "ol" : "ul" + ); + toc.parentNode.insertBefore(newToc, toc); + // and remove the old one + toc.remove(); + toc = newToc; + toc.setAttribute("id", kMozToc); + } else { + // we can keep the list itself but let's get rid of the TOC entries + while (toc.hasChildNodes()) { + toc.lastChild.remove(); + } + } + + var commentText = "mozToc "; + for (var j = 0; j < 6; j++) { + if (tocHeadersArray[j][0] != "") { + commentText += tocHeadersArray[j][0]; + if (tocHeadersArray[j][1] != "") { + commentText += "." + tocHeadersArray[j][1]; + } + commentText += " " + (j + 1) + " "; + } + } + // important, we have to remove trailing spaces + commentText = TrimStringRight(commentText); + + // forge a comment we'll insert in the TOC ; that comment will hold + // the TOC definition for us + var ct = theDocument.createComment(commentText); + toc.appendChild(ct); + + // assign a special class to the TOC top element if the TOC is readonly + // the definition of this class is in EditorOverride.css + if (readonly) { + toc.setAttribute("class", "readonly"); + } else { + toc.removeAttribute("class"); + } + + // We need a new variable to hold the local ul/ol container + // The toplevel TOC element is not the parent element of a + // TOC entry if its depth is > 1... + var tocList = toc; + // create a list item + var tocItem = theDocument.createElement("li"); + // and an anchor in this list item + var tocAnchor = theDocument.createElement("a"); + // make it target the source of the TOC entry + tocAnchor.setAttribute("href", "#" + tocArray[i + 2]); + // and put the textual contents of the TOC entry in that anchor + var tocEntry = theDocument.createTextNode(tocArray[i + 1]); + // now, insert everything where it has to be inserted + tocAnchor.appendChild(tocEntry); + tocItem.appendChild(tocAnchor); + tocList.appendChild(tocItem); + item = tocList; + } else { + if (tocArray[i] < headerIndex) { + // if the depth of the new TOC entry is less than the depth of the + // last entry we created, find the good ul/ol ancestor + for (j = headerIndex - tocArray[i]; j > 0; --j) { + if (item != toc) { + item = item.parentNode.parentNode; + } + } + tocItem = theDocument.createElement("li"); + } else if (tocArray[i] > headerIndex) { + // to the contrary, it's deeper than the last one + // we need to create sub ul/ol's and li's + for (j = tocArray[i] - headerIndex; j > 0; --j) { + tocList = theDocument.createElement(orderedList ? "ol" : "ul"); + item.lastChild.appendChild(tocList); + tocItem = theDocument.createElement("li"); + tocList.appendChild(tocItem); + item = tocList; + } + } else { + tocItem = theDocument.createElement("li"); + } + tocAnchor = theDocument.createElement("a"); + tocAnchor.setAttribute("href", "#" + tocArray[i + 2]); + tocEntry = theDocument.createTextNode(tocArray[i + 1]); + tocAnchor.appendChild(tocEntry); + tocItem.appendChild(tocAnchor); + item.appendChild(tocItem); + headerIndex = tocArray[i]; + } + } + SaveWindowLocation(); +} + +function selectHeader(elt, index) { + var tag = elt.value; + tocHeadersArray[index - 1][0] = tag; + var textbox = document.getElementById("header" + index + "Class"); + if (tag == "") { + textbox.setAttribute("disabled", "true"); + } else { + textbox.removeAttribute("disabled"); + } +} + +function changeClass(elt, index) { + tocHeadersArray[index - 1][1] = elt.value; +} + +function ToggleReadOnlyToc(elt) { + readonly = elt.checked; +} + +function ToggleOrderedList(elt) { + orderedList = elt.checked; +} diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml new file mode 100644 index 0000000000..38c85c764d --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml @@ -0,0 +1,505 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertTOC.dtd"> + +<window + title="&Window.title;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="Startup();" + lightweightthemes="true" + oncancel="window.close(); return true;" +> + <dialog> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdInsertTOC.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + <spacer id="dummy" style="display: none" /> + <vbox flex="1"> + <html:fieldset> + <html:legend>&buildToc.label;</html:legend> + <html:table> + <html:tr> + <html:th></html:th> + <html:th>&tag.label;</html:th> + <html:th>&class.label;</html:th> + </html:tr> + <html:tr> + <html:th id="header1Label">&header1.label;</html:th> + <html:td> + <menulist id="header1Menulist"> + <menupopup> + <menuitem + id="header1none" + label="--" + value="" + oncommand="selectHeader(this, 1)" + /> + <menuseparator /> + <menuitem + id="header1H1" + label="h1" + value="h1" + oncommand="selectHeader(this, 1)" + /> + <menuitem + id="header1H2" + label="h2" + value="h2" + oncommand="selectHeader(this, 1)" + /> + <menuitem + id="header1H3" + label="h3" + value="h3" + oncommand="selectHeader(this, 1)" + /> + <menuitem + id="header1H4" + label="h4" + value="h4" + oncommand="selectHeader(this, 1)" + /> + <menuitem + id="header1H5" + label="h5" + value="h5" + oncommand="selectHeader(this, 1)" + /> + <menuitem + id="header1H6" + label="h6" + value="h6" + oncommand="selectHeader(this, 1)" + /> + <menuitem + id="header1DIV" + label="div" + value="div" + oncommand="selectHeader(this, 1)" + /> + <menuitem + id="header1P" + label="p" + value="p" + oncommand="selectHeader(this, 1)" + /> + </menupopup> + </menulist> + </html:td> + <html:td> + <html:input + id="header1Class" + type="text" + class="input-inline" + size="10" + onchange="changeClass(this, 1)" + aria-labelledby="header1Label" + /> + </html:td> + </html:tr> + <html:tr> + <html:th id="header2Label">&header2.label;</html:th> + <html:td> + <menulist id="header2Menulist"> + <menupopup> + <menuitem + id="header2none" + label="--" + value="" + oncommand="selectHeader(this, 2)" + /> + <menuseparator /> + <menuitem + id="header2H1" + label="h1" + value="h1" + oncommand="selectHeader(this, 2)" + /> + <menuitem + id="header2H2" + label="h2" + value="h2" + oncommand="selectHeader(this, 2)" + /> + <menuitem + id="header2H3" + label="h3" + value="h3" + oncommand="selectHeader(this, 2)" + /> + <menuitem + id="header2H4" + label="h4" + value="h4" + oncommand="selectHeader(this, 2)" + /> + <menuitem + id="header2H5" + label="h5" + value="h5" + oncommand="selectHeader(this, 2)" + /> + <menuitem + id="header2H6" + label="h6" + value="h6" + oncommand="selectHeader(this, 2)" + /> + <menuitem + id="header2DIV" + label="div" + value="div" + oncommand="selectHeader(this, 2)" + /> + <menuitem + id="header2P" + label="p" + value="p" + oncommand="selectHeader(this, 2)" + /> + </menupopup> + </menulist> + </html:td> + <html:td> + <html:input + id="header2Class" + type="text" + class="input-inline" + size="10" + onchange="changeClass(this, 2)" + aria-labelledby="header2Label" + /> + </html:td> + </html:tr> + <html:tr> + <html:th id="header3Label">&header3.label;</html:th> + <html:td> + <menulist id="header3Menulist"> + <menupopup> + <menuitem + id="header3none" + label="--" + value="" + oncommand="selectHeader(this, 3)" + /> + <menuseparator /> + <menuitem + id="header3H1" + label="h1" + value="h1" + oncommand="selectHeader(this, 3)" + /> + <menuitem + id="header3H2" + label="h2" + value="h2" + oncommand="selectHeader(this, 3)" + /> + <menuitem + id="header3H3" + label="h3" + value="h3" + oncommand="selectHeader(this, 3)" + /> + <menuitem + id="header3H4" + label="h4" + value="h4" + oncommand="selectHeader(this, 3)" + /> + <menuitem + id="header3H5" + label="h5" + value="h5" + oncommand="selectHeader(this, 3)" + /> + <menuitem + id="header3H6" + label="h6" + value="h6" + oncommand="selectHeader(this, 3)" + /> + <menuitem + id="header3DIV" + label="div" + value="div" + oncommand="selectHeader(this, 3)" + /> + <menuitem + id="header3P" + label="p" + value="p" + oncommand="selectHeader(this, 3)" + /> + </menupopup> + </menulist> + </html:td> + <html:td> + <html:input + id="header3Class" + type="text" + class="input-inline" + size="10" + onchange="changeClass(this, 3)" + aria-labelledby="header3Label" + /> + </html:td> + </html:tr> + <html:tr> + <html:th id="header4Label">&header4.label;</html:th> + <html:td> + <menulist id="header4Menulist"> + <menupopup> + <menuitem + id="header4none" + label="--" + value="" + oncommand="selectHeader(this, 4)" + /> + <menuseparator /> + <menuitem + id="header4H1" + label="h1" + value="h1" + oncommand="selectHeader(this, 4)" + /> + <menuitem + id="header4H2" + label="h2" + value="h2" + oncommand="selectHeader(this, 4)" + /> + <menuitem + id="header4H3" + label="h3" + value="h3" + oncommand="selectHeader(this, 4)" + /> + <menuitem + id="header4H4" + label="h4" + value="h4" + oncommand="selectHeader(this, 4)" + /> + <menuitem + id="header4H5" + label="h5" + value="h5" + oncommand="selectHeader(this, 4)" + /> + <menuitem + id="header4H6" + label="h6" + value="h6" + oncommand="selectHeader(this, 4)" + /> + <menuitem + id="header4DIV" + label="div" + value="div" + oncommand="selectHeader(this, 4)" + /> + <menuitem + id="header4P" + label="p" + value="p" + oncommand="selectHeader(this, 4)" + /> + </menupopup> + </menulist> + </html:td> + <html:td> + <html:input + id="header4Class" + type="text" + class="input-inline" + size="10" + onchange="changeClass(this, 4)" + aria-labelledby="header4Label" + /> + </html:td> + </html:tr> + <html:tr> + <html:th id="header5Label">&header5.label;</html:th> + <html:td> + <menulist id="header5Menulist"> + <menupopup> + <menuitem + id="header5none" + label="--" + value="" + oncommand="selectHeader(this, 5)" + /> + <menuseparator /> + <menuitem + id="header5H1" + label="h1" + value="h1" + oncommand="selectHeader(this, 5)" + /> + <menuitem + id="header5H2" + label="h2" + value="h2" + oncommand="selectHeader(this, 5)" + /> + <menuitem + id="header5H3" + label="h3" + value="h3" + oncommand="selectHeader(this, 5)" + /> + <menuitem + id="header5H4" + label="h4" + value="h4" + oncommand="selectHeader(this, 5)" + /> + <menuitem + id="header5H5" + label="h5" + value="h5" + oncommand="selectHeader(this, 5)" + /> + <menuitem + id="header5H6" + label="h6" + value="h6" + oncommand="selectHeader(this, 5)" + /> + <menuitem + id="header5DIV" + label="div" + value="div" + oncommand="selectHeader(this, 5)" + /> + <menuitem + id="header5P" + label="p" + value="p" + oncommand="selectHeader(this, 5)" + /> + </menupopup> + </menulist> + </html:td> + <html:td> + <html:input + id="header5Class" + type="text" + class="input-inline" + size="10" + onchange="changeClass(this, 5)" + aria-labelledby="header5Label" + /> + </html:td> + </html:tr> + <html:tr> + <html:th id="header6Label">&header6.label;</html:th> + <html:td> + <menulist id="header6Menulist"> + <menupopup> + <menuitem + id="header6none" + label="--" + value="" + oncommand="selectHeader(this, 6)" + /> + <menuseparator /> + <menuitem + id="header6H1" + label="h1" + value="h1" + oncommand="selectHeader(this, 6)" + /> + <menuitem + id="header6H2" + label="h2" + value="h2" + oncommand="selectHeader(this, 6)" + /> + <menuitem + id="header6H3" + label="h3" + value="h3" + oncommand="selectHeader(this, 6)" + /> + <menuitem + id="header6H4" + label="h4" + value="h4" + oncommand="selectHeader(this, 6)" + /> + <menuitem + id="header6H5" + label="h5" + value="h5" + oncommand="selectHeader(this, 6)" + /> + <menuitem + id="header6H6" + label="h6" + value="h6" + oncommand="selectHeader(this, 6)" + /> + <menuitem + id="header6DIV" + label="div" + value="div" + oncommand="selectHeader(this, 6)" + /> + <menuitem + id="header6P" + label="p" + value="p" + oncommand="selectHeader(this, 6)" + /> + </menupopup> + </menulist> + </html:td> + <html:td> + <html:input + id="header6Class" + type="text" + class="input-inline" + size="10" + onchange="changeClass(this, 6)" + aria-labelledby="header6Label" + /> + </html:td> + </html:tr> + </html:table> + </html:fieldset> + <vbox> + <checkbox + id="orderedListCheckbox" + label="&orderedList.label;" + oncommand="ToggleOrderedList(this)" + /> + <checkbox + id="readOnlyCheckbox" + label="&makeReadOnly.label;" + oncommand="ToggleReadOnlyToc(this)" + /> + </vbox> + <separator class="groove" /> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTable.js b/comm/mail/components/compose/content/dialogs/EdInsertTable.js new file mode 100644 index 0000000000..5da0da46d3 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertTable.js @@ -0,0 +1,258 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +// Cancel() is in EdDialogCommon.js + +var gTableElement = null; +var gRows; +var gColumns; +var gActiveEditor; + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + gActiveEditor = GetCurrentTableEditor(); + if (!gActiveEditor) { + dump("Failed to get active editor!\n"); + window.close(); + return; + } + + try { + gTableElement = gActiveEditor.createElementWithDefaults("table"); + } catch (e) {} + + if (!gTableElement) { + dump("Failed to create a new table!\n"); + window.close(); + return; + } + gDialog.rowsInput = document.getElementById("rowsInput"); + gDialog.columnsInput = document.getElementById("columnsInput"); + gDialog.widthInput = document.getElementById("widthInput"); + gDialog.borderInput = document.getElementById("borderInput"); + gDialog.widthPixelOrPercentMenulist = document.getElementById( + "widthPixelOrPercentMenulist" + ); + gDialog.OkButton = document.querySelector("dialog").getButton("accept"); + + // Make a copy to use for AdvancedEdit + globalElement = gTableElement.cloneNode(false); + try { + if ( + Services.prefs.getBoolPref("editor.use_css") && + IsHTMLEditor() && + !(gActiveEditor.flags & Ci.nsIEditor.eEditorMailMask) + ) { + // only for Composer and not for htmlmail + globalElement.setAttribute("style", "text-align: left;"); + } + } catch (e) {} + + // Initialize all widgets with image attributes + InitDialog(); + + // Set initial number to 2 rows, 2 columns: + // Note, these are not attributes on the table, + // so don't put them in InitDialog(), + // else the user's values will be trashed when they use + // the Advanced Edit dialog + gDialog.rowsInput.value = 2; + gDialog.columnsInput.value = 2; + + // If no default value on the width, set to 100% + if (gDialog.widthInput.value.length == 0) { + gDialog.widthInput.value = "100"; + gDialog.widthPixelOrPercentMenulist.selectedIndex = 1; + } + + SetTextboxFocusById("rowsInput"); + + SetWindowLocation(); +} + +// Set dialog widgets with attribute data +// We get them from globalElement copy so this can be used +// by AdvancedEdit(), which is shared by all property dialogs +function InitDialog() { + // Get default attributes set on the created table: + // Get the width attribute of the element, stripping out "%" + // This sets contents of menu combobox list + // 2nd param = null: Use current selection to find if parent is table cell or window + gDialog.widthInput.value = InitPixelOrPercentMenulist( + globalElement, + null, + "width", + "widthPixelOrPercentMenulist", + gPercent + ); + gDialog.borderInput.value = globalElement.getAttribute("border"); +} + +function ChangeRowOrColumn(id) { + // Allow only integers + forceInteger(id); + + // Enable OK only if both rows and columns have a value > 0 + var enable = + gDialog.rowsInput.value.length > 0 && + gDialog.rowsInput.value > 0 && + gDialog.columnsInput.value.length > 0 && + gDialog.columnsInput.value > 0; + + SetElementEnabled(gDialog.OkButton, enable); + SetElementEnabledById("AdvancedEditButton1", enable); +} + +// Get and validate data from widgets. +// Set attributes on globalElement so they can be accessed by AdvancedEdit() +function ValidateData() { + gRows = ValidateNumber( + gDialog.rowsInput, + null, + 1, + gMaxRows, + null, + null, + true + ); + if (gValidationError) { + return false; + } + + gColumns = ValidateNumber( + gDialog.columnsInput, + null, + 1, + gMaxColumns, + null, + null, + true + ); + if (gValidationError) { + return false; + } + + // Set attributes: NOTE: These may be empty strings (last param = false) + ValidateNumber( + gDialog.borderInput, + null, + 0, + gMaxPixels, + globalElement, + "border", + false + ); + // TODO: Deal with "BORDER" without value issue + if (gValidationError) { + return false; + } + + ValidateNumber( + gDialog.widthInput, + gDialog.widthPixelOrPercentMenulist, + 1, + gMaxTableSize, + globalElement, + "width", + false + ); + if (gValidationError) { + return false; + } + + return true; +} + +function onAccept(event) { + if (ValidateData()) { + gActiveEditor.beginTransaction(); + try { + gActiveEditor.cloneAttributes(gTableElement, globalElement); + + // Create necessary rows and cells for the table + var tableBody = gActiveEditor.createElementWithDefaults("tbody"); + if (tableBody) { + gTableElement.appendChild(tableBody); + + // Create necessary rows and cells for the table + for (var i = 0; i < gRows; i++) { + var newRow = gActiveEditor.createElementWithDefaults("tr"); + if (newRow) { + tableBody.appendChild(newRow); + for (var j = 0; j < gColumns; j++) { + var newCell = gActiveEditor.createElementWithDefaults("td"); + if (newCell) { + newRow.appendChild(newCell); + } + } + } + } + } + // Detect when entire cells are selected: + // Get number of cells selected + var tagNameObj = { value: "" }; + var countObj = { value: 0 }; + var element = gActiveEditor.getSelectedOrParentTableElement( + tagNameObj, + countObj + ); + var deletePlaceholder = false; + + if (tagNameObj.value == "table") { + // Replace entire selected table with new table, so delete the table + gActiveEditor.deleteTable(); + } else if (tagNameObj.value == "td") { + if (countObj.value >= 1) { + if (countObj.value > 1) { + // Assume user wants to replace a block of + // contiguous cells with a table, so + // join the selected cells + gActiveEditor.joinTableCells(false); + + // Get the cell everything was merged into + element = gActiveEditor.getSelectedCells()[0]; + + // Collapse selection into just that cell + gActiveEditor.selection.collapse(element, 0); + } + + if (element) { + // Empty just the contents of the cell + gActiveEditor.deleteTableCellContents(); + + // Collapse selection to start of empty cell... + gActiveEditor.selection.collapse(element, 0); + // ...but it will contain a <br> placeholder + deletePlaceholder = true; + } + } + } + + // true means delete selection when inserting + gActiveEditor.insertElementAtSelection(gTableElement, true); + + if ( + deletePlaceholder && + gTableElement && + gTableElement.nextElementSibling + ) { + // Delete the placeholder <br> + gActiveEditor.deleteNode(gTableElement.nextElementSibling); + } + } catch (e) {} + + gActiveEditor.endTransaction(); + + SaveWindowLocation(); + return; + } + event.preventDefault(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml b/comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml new file mode 100644 index 0000000000..b114e09d44 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml @@ -0,0 +1,126 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % edInsertTable SYSTEM "chrome://messenger/locale/messengercompose/EditorInsertTable.dtd"> +%edInsertTable; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" +> + <dialog> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdInsertTable.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + <html:table> + <html:tr> + <html:th> + <label + control="rowsInput" + value="&numRowsEditField.label;" + accesskey="&numRowsEditField.accessKey;" + /> + </html:th> + <html:td> + <html:input + id="rowsInput" + type="number" + class="narrow input-inline" + oninput="ChangeRowOrColumn(this.id)" + /> + </html:td> + </html:tr> + <html:tr> + <html:th> + <label + control="columnsInput" + value="&numColumnsEditField.label;" + accesskey="&numColumnsEditField.accessKey;" + /> + </html:th> + <html:td> + <html:input + id="columnsInput" + type="number" + class="narrow input-inline" + oninput="ChangeRowOrColumn(this.id)" + /> + </html:td> + </html:tr> + <html:tr> + <html:th> + <label + control="widthInput" + value="&widthEditField.label;" + accesskey="&widthEditField.accessKey;" + /> + </html:th> + <html:td> + <html:input + id="widthInput" + type="number" + class="narrow input-inline" + oninput="forceInteger(this.id)" + /> + </html:td> + <html:td> + <menulist id="widthPixelOrPercentMenulist" class="menulist-narrow" /> + </html:td> + </html:tr> + <html:tr> + <html:th> + <label + control="borderInput" + value="&borderEditField.label;" + accesskey="&borderEditField.accessKey;" + tooltiptext="&borderEditField.tooltip;" + /> + </html:th> + <html:td> + <html:input + id="borderInput" + type="number" + class="narrow input-inline" + oninput="forceInteger(this.id)" + /> + </html:td> + <html:td> + <label value="&pixels.label;" /> + </html:td> + </html:tr> + </html:table> + <vbox id="AdvancedEdit"> + <hbox flex="1" style="margin-top: 0.2em" align="center"> + <!-- This will right-align the button --> + <spacer flex="1" /> + <button + id="AdvancedEditButton1" + oncommand="onAdvancedEdit()" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <separator id="advancedSeparator" class="groove" /> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdLinkProps.js b/comm/mail/components/compose/content/dialogs/EdLinkProps.js new file mode 100644 index 0000000000..903a4d3099 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdLinkProps.js @@ -0,0 +1,323 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var gActiveEditor; +var anchorElement = null; +var imageElement = null; +var insertNew = false; +var replaceExistingLink = false; +var insertLinkAtCaret; +var needLinkText = false; +var href; +var newLinkText; +var gHNodeArray = {}; +var gHaveNamedAnchors = false; +var gHaveHeadings = false; +var gCanChangeHeadingSelected = true; +var gCanChangeAnchorSelected = true; + +// NOTE: Use "href" instead of "a" to distinguish from Named Anchor +// The returned node is has an "a" tagName +var tagName = "href"; + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + gActiveEditor = GetCurrentEditor(); + if (!gActiveEditor) { + dump("Failed to get active editor!\n"); + window.close(); + return; + } + // Message was wrapped in a <label> or <div>, so actual text is a child text node + gDialog.linkTextCaption = document.getElementById("linkTextCaption"); + gDialog.linkTextMessage = document.getElementById("linkTextMessage"); + gDialog.linkTextInput = document.getElementById("linkTextInput"); + gDialog.hrefInput = document.getElementById("hrefInput"); + gDialog.makeRelativeLink = document.getElementById("MakeRelativeLink"); + gDialog.AdvancedEditSection = document.getElementById("AdvancedEdit"); + + // See if we have a single selected image + imageElement = gActiveEditor.getSelectedElement("img"); + + if (imageElement) { + // Get the parent link if it exists -- more efficient than GetSelectedElement() + anchorElement = gActiveEditor.getElementOrParentByTagName( + "href", + imageElement + ); + if (anchorElement) { + if (anchorElement.children.length > 1) { + // If there are other children, then we want to break + // this image away by inserting a new link around it, + // so make a new node and copy existing attributes + anchorElement = anchorElement.cloneNode(false); + // insertNew = true; + replaceExistingLink = true; + } + } + } else { + // Get an anchor element if caret or + // entire selection is within the link. + anchorElement = gActiveEditor.getSelectedElement(tagName); + + if (anchorElement) { + // Select the entire link + gActiveEditor.selectElement(anchorElement); + } else { + // If selection starts in a link, but extends beyond it, + // the user probably wants to extend existing link to new selection, + // so check if either end of selection is within a link + // POTENTIAL PROBLEM: This prevents user from selecting text in an existing + // link and making 2 links. + // Note that this isn't a problem with images, handled above + + anchorElement = gActiveEditor.getElementOrParentByTagName( + "href", + gActiveEditor.selection.anchorNode + ); + if (!anchorElement) { + anchorElement = gActiveEditor.getElementOrParentByTagName( + "href", + gActiveEditor.selection.focusNode + ); + } + + if (anchorElement) { + // But clone it for reinserting/merging around existing + // link that only partially overlaps the selection + anchorElement = anchorElement.cloneNode(false); + // insertNew = true; + replaceExistingLink = true; + } + } + } + + if (!anchorElement) { + // No existing link -- create a new one + anchorElement = gActiveEditor.createElementWithDefaults(tagName); + insertNew = true; + // Hide message about removing existing link + // document.getElementById("RemoveLinkMsg").hidden = true; + } + if (!anchorElement) { + dump("Failed to get selected element or create a new one!\n"); + window.close(); + return; + } + + // We insert at caret only when nothing is selected + insertLinkAtCaret = gActiveEditor.selection.isCollapsed; + + var selectedText; + if (insertLinkAtCaret) { + // Groupbox caption: + gDialog.linkTextCaption.setAttribute("label", GetString("LinkText")); + + // Message above input field: + gDialog.linkTextMessage.setAttribute("value", GetString("EnterLinkText")); + gDialog.linkTextMessage.setAttribute( + "accesskey", + GetString("EnterLinkTextAccessKey") + ); + } else { + if (!imageElement) { + // We get here if selection is exactly around a link node + // Check if selection has some text - use that first + selectedText = GetSelectionAsText(); + if (!selectedText) { + // No text, look for first image in the selection + imageElement = anchorElement.querySelector("img"); + } + } + // Set "caption" for link source and the source text or image URL + if (imageElement) { + gDialog.linkTextCaption.setAttribute("label", GetString("LinkImage")); + // Link source string is the source URL of image + // TODO: THIS DOESN'T HANDLE MULTIPLE SELECTED IMAGES! + gDialog.linkTextMessage.setAttribute("value", imageElement.src); + } else { + gDialog.linkTextCaption.setAttribute("label", GetString("LinkText")); + if (selectedText) { + // Use just the first 60 characters and add "..." + gDialog.linkTextMessage.setAttribute( + "value", + TruncateStringAtWordEnd( + ReplaceWhitespace(selectedText, " "), + 60, + true + ) + ); + } else { + gDialog.linkTextMessage.setAttribute( + "value", + GetString("MixedSelection") + ); + } + } + } + + // Make a copy to use for AdvancedEdit and onSaveDefault + globalElement = anchorElement.cloneNode(false); + + // Get the list of existing named anchors and headings + FillLinkMenulist(gDialog.hrefInput, gHNodeArray); + + // We only need to test for this once per dialog load + gHaveDocumentUrl = GetDocumentBaseUrl(); + + // Set data for the dialog controls + InitDialog(); + + // Search for a URI pattern in the selected text + // as candidate href + selectedText = TrimString(selectedText); + if (!gDialog.hrefInput.value && TextIsURI(selectedText)) { + gDialog.hrefInput.value = selectedText; + } + + // Set initial focus + if (insertLinkAtCaret) { + // We will be using the HREF inputbox, so text message + gDialog.linkTextInput.focus(); + } else { + gDialog.hrefInput.select(); + gDialog.hrefInput.focus(); + + // We will not insert a new link at caret, so remove link text input field + gDialog.linkTextInput.hidden = true; + gDialog.linkTextInput = null; + } + + // This sets enable state on OK button + doEnabling(); + + SetWindowLocation(); +} + +// Set dialog widgets with attribute data +// We get them from globalElement copy so this can be used +// by AdvancedEdit(), which is shared by all property dialogs +function InitDialog() { + // Must use getAttribute, not "globalElement.href", + // or foreign chars aren't converted correctly! + gDialog.hrefInput.value = globalElement.getAttribute("href"); + + // Set "Relativize" checkbox according to current URL state + SetRelativeCheckbox(gDialog.makeRelativeLink); +} + +function doEnabling() { + // We disable Ok button when there's no href text only if inserting a new link + var enable = insertNew + ? TrimString(gDialog.hrefInput.value).length > 0 + : true; + + // anon. content, so can't use SetElementEnabledById here + var dialogNode = document.getElementById("linkDlg"); + dialogNode.getButton("accept").disabled = !enable; + + SetElementEnabledById("AdvancedEditButton1", enable); +} + +function ChangeLinkLocation() { + SetRelativeCheckbox(gDialog.makeRelativeLink); + // Set OK button enable state + doEnabling(); +} + +// Get and validate data from widgets. +// Set attributes on globalElement so they can be accessed by AdvancedEdit() +function ValidateData() { + href = TrimString(gDialog.hrefInput.value); + if (href) { + // Set the HREF directly on the editor document's anchor node + // or on the newly-created node if insertNew is true + globalElement.setAttribute("href", href); + } else if (insertNew) { + // We must have a URL to insert a new link + // NOTE: We accept an empty HREF on existing link to indicate removing the link + ShowInputErrorMessage(GetString("EmptyHREFError")); + return false; + } + if (gDialog.linkTextInput) { + // The text we will insert isn't really an attribute, + // but it makes sense to validate it + newLinkText = TrimString(gDialog.linkTextInput.value); + if (!newLinkText) { + if (href) { + newLinkText = href; + } else { + ShowInputErrorMessage(GetString("EmptyLinkTextError")); + SetTextboxFocus(gDialog.linkTextInput); + return false; + } + } + } + return true; +} + +function onAccept(event) { + if (ValidateData()) { + if (href.length > 0) { + // Copy attributes to element we are changing or inserting + gActiveEditor.cloneAttributes(anchorElement, globalElement); + + // Coalesce into one undo transaction + gActiveEditor.beginTransaction(); + + // Get text to use for a new link + if (insertLinkAtCaret) { + // Append the link text as the last child node + // of the anchor node + var textNode = gActiveEditor.document.createTextNode(newLinkText); + if (textNode) { + anchorElement.appendChild(textNode); + } + try { + gActiveEditor.insertElementAtSelection(anchorElement, false); + } catch (e) { + dump("Exception occurred in InsertElementAtSelection\n"); + return; + } + } else if (insertNew || replaceExistingLink) { + // Link source was supplied by the selection, + // so insert a link node as parent of this + // (may be text, image, or other inline content) + try { + gActiveEditor.insertLinkAroundSelection(anchorElement); + } catch (e) { + dump("Exception occurred in InsertElementAtSelection\n"); + return; + } + } + // Check if the link was to a heading + if (href in gHNodeArray) { + var anchorNode = gActiveEditor.createElementWithDefaults("a"); + if (anchorNode) { + anchorNode.name = href.substr(1); + + // Insert the anchor into the document, + // but don't let the transaction change the selection + gActiveEditor.setShouldTxnSetSelection(false); + gActiveEditor.insertNode(anchorNode, gHNodeArray[href], 0); + gActiveEditor.setShouldTxnSetSelection(true); + } + } + gActiveEditor.endTransaction(); + } else if (!insertNew) { + // We already had a link, but empty HREF means remove it + EditorRemoveTextProperty("href", ""); + } + SaveWindowLocation(); + return; + } + event.preventDefault(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml b/comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml new file mode 100644 index 0000000000..7c550a7a45 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml @@ -0,0 +1,112 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % linkPropertiesDTD SYSTEM "chrome://messenger/locale/messengercompose/EditorLinkProperties.dtd"> +%linkPropertiesDTD; +<!ENTITY % composeEditorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/mailComposeEditorOverlay.dtd"> +%composeEditorOverlayDTD; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" + style="min-height: 26em" +> + <dialog id="linkDlg" style="width: 50ch"> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdLinkProps.js" /> + <script src="chrome://messenger/content/messengercompose/EdImageLinkLoader.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <vbox> + <html:fieldset> + <html:legend><label id="linkTextCaption" /></html:legend> + <vbox> + <label id="linkTextMessage" control="linkTextInput" /> + <html:input + id="linkTextInput" + type="text" + class="input-inline" + aria-labelledby="linkTextMessage" + /> + </vbox> + </html:fieldset> + + <html:fieldset id="LinkURLBox"> + <html:legend>&LinkURLBox.label;</html:legend> + <vbox id="LinkLocationBox"> + <label + id="hrefLabel" + control="hrefInput" + accesskey="&LinkURLEditField2.accessKey;" + width="1" + >&LinkURLEditField2.label;</label + > + <html:input + id="hrefInput" + type="text" + class="input-inline uri-element padded" + oninput="ChangeLinkLocation();" + aria-labelledby="hrefLabel" + /> + <hbox align="center"> + <checkbox + id="MakeRelativeLink" + for="hrefInput" + label="&makeUrlRelative.label;" + accesskey="&makeUrlRelative.accessKey;" + oncommand="MakeInputValueRelativeOrAbsolute(this);" + tooltiptext="&makeUrlRelative.tooltip;" + /> + <spacer flex="1" /> + <button + label="&chooseFileLinkButton.label;" + accesskey="&chooseFileLinkButton.accessKey;" + oncommand="chooseLinkFile();" + /> + </hbox> + </vbox> + <checkbox + id="AttachSourceToMail" + hidden="true" + label="&attachLinkSource.label;" + accesskey="&attachLinkSource.accesskey;" + oncommand="DoAttachSourceCheckbox()" + /> + </html:fieldset> + </vbox> + <vbox id="AdvancedEdit"> + <hbox flex="1" style="margin-top: 0.2em" align="center"> + <!-- This will right-align the button --> + <spacer flex="1" /> + <button + id="AdvancedEditButton1" + oncommand="onAdvancedEdit()" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <separator id="advancedSeparator" class="groove" /> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdListProps.js b/comm/mail/components/compose/content/dialogs/EdListProps.js new file mode 100644 index 0000000000..c33efc9bb1 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdListProps.js @@ -0,0 +1,455 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +// Cancel() is in EdDialogCommon.js +var gBulletStyleType = ""; +var gNumberStyleType = ""; +var gListElement; +var gOriginalListType = ""; +var gListType = ""; +var gMixedListSelection = false; +var gStyleType = ""; +var gOriginalStyleType = ""; +const gOnesArray = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"]; +const gTensArray = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"]; +const gHundredsArray = [ + "", + "C", + "CC", + "CCC", + "CD", + "D", + "DC", + "DCC", + "DCCC", + "CM", +]; +const gThousandsArray = [ + "", + "M", + "MM", + "MMM", + "MMMM", + "MMMMM", + "MMMMMM", + "MMMMMMM", + "MMMMMMMM", + "MMMMMMMMM", +]; +const gRomanDigits = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }; +const A = "A".charCodeAt(0); +const gArabic = "1"; +const gUpperRoman = "I"; +const gLowerRoman = "i"; +const gUpperLetters = "A"; +const gLowerLetters = "a"; +const gDecimalCSS = "decimal"; +const gUpperRomanCSS = "upper-roman"; +const gLowerRomanCSS = "lower-roman"; +const gUpperAlphaCSS = "upper-alpha"; +const gLowerAlphaCSS = "lower-alpha"; + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + var editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + gDialog.ListTypeList = document.getElementById("ListType"); + gDialog.BulletStyleList = document.getElementById("BulletStyle"); + gDialog.BulletStyleLabel = document.getElementById("BulletStyleLabel"); + gDialog.StartingNumberInput = document.getElementById("StartingNumber"); + gDialog.StartingNumberLabel = document.getElementById("StartingNumberLabel"); + gDialog.AdvancedEditButton = document.getElementById("AdvancedEditButton1"); + gDialog.RadioGroup = document.getElementById("RadioGroup"); + gDialog.ChangeAllRadio = document.getElementById("ChangeAll"); + gDialog.ChangeSelectedRadio = document.getElementById("ChangeSelected"); + + // Try to get an existing list(s) + var mixedObj = { value: null }; + try { + gListType = editor.getListState(mixedObj, {}, {}, {}); + + // We may have mixed list and non-list, or > 1 list type in selection + gMixedListSelection = mixedObj.value; + + // Get the list element at the anchor node + gListElement = editor.getElementOrParentByTagName("list", null); + } catch (e) {} + + // The copy to use in AdvancedEdit + if (gListElement) { + globalElement = gListElement.cloneNode(false); + } + + // Show extra options for changing entire list if we have one already. + gDialog.RadioGroup.collapsed = !gListElement; + if (gListElement) { + // Radio button index is persistent + if (gDialog.RadioGroup.getAttribute("index") == "1") { + gDialog.RadioGroup.selectedItem = gDialog.ChangeSelectedRadio; + } else { + gDialog.RadioGroup.selectedItem = gDialog.ChangeAllRadio; + } + } + + InitDialog(); + + gOriginalListType = gListType; + + gDialog.ListTypeList.focus(); + + SetWindowLocation(); +} + +function InitDialog() { + // Note that if mixed, we we pay attention + // only to the anchor node's list type + // (i.e., don't confuse user with "mixed" designation) + if (gListElement) { + gListType = gListElement.nodeName.toLowerCase(); + } else { + gListType = ""; + } + + gDialog.ListTypeList.value = gListType; + gDialog.StartingNumberInput.value = ""; + + // Last param = true means attribute value is case-sensitive + var type = globalElement + ? GetHTMLOrCSSStyleValue(globalElement, "type", "list-style-type") + : null; + + if (gListType == "ul") { + if (type) { + type = type.toLowerCase(); + gBulletStyleType = type; + gOriginalStyleType = type; + } + } else if (gListType == "ol") { + // Translate CSS property strings + switch (type.toLowerCase()) { + case gDecimalCSS: + type = gArabic; + break; + case gUpperRomanCSS: + type = gUpperRoman; + break; + case gLowerRomanCSS: + type = gLowerRoman; + break; + case gUpperAlphaCSS: + type = gUpperLetters; + break; + case gLowerAlphaCSS: + type = gLowerLetters; + break; + } + if (type) { + gNumberStyleType = type; + gOriginalStyleType = type; + } + + // Convert attribute number to appropriate letter or roman numeral + gDialog.StartingNumberInput.value = ConvertStartAttrToUserString( + globalElement.getAttribute("start"), + type + ); + } + BuildBulletStyleList(); +} + +// Convert attribute number to appropriate letter or roman numeral +function ConvertStartAttrToUserString(startAttr, type) { + switch (type) { + case gUpperRoman: + startAttr = ConvertArabicToRoman(startAttr); + break; + case gLowerRoman: + startAttr = ConvertArabicToRoman(startAttr).toLowerCase(); + break; + case gUpperLetters: + startAttr = ConvertArabicToLetters(startAttr); + break; + case gLowerLetters: + startAttr = ConvertArabicToLetters(startAttr).toLowerCase(); + break; + } + return startAttr; +} + +function BuildBulletStyleList() { + gDialog.BulletStyleList.removeAllItems(); + var label; + + if (gListType == "ul") { + gDialog.BulletStyleList.removeAttribute("disabled"); + gDialog.BulletStyleLabel.removeAttribute("disabled"); + gDialog.StartingNumberInput.setAttribute("disabled", "true"); + gDialog.StartingNumberLabel.setAttribute("disabled", "true"); + + label = GetString("BulletStyle"); + + gDialog.BulletStyleList.appendItem(GetString("Automatic"), ""); + gDialog.BulletStyleList.appendItem(GetString("SolidCircle"), "disc"); + gDialog.BulletStyleList.appendItem(GetString("OpenCircle"), "circle"); + gDialog.BulletStyleList.appendItem(GetString("SolidSquare"), "square"); + + gDialog.BulletStyleList.value = gBulletStyleType; + } else if (gListType == "ol") { + gDialog.BulletStyleList.removeAttribute("disabled"); + gDialog.BulletStyleLabel.removeAttribute("disabled"); + gDialog.StartingNumberInput.removeAttribute("disabled"); + gDialog.StartingNumberLabel.removeAttribute("disabled"); + label = GetString("NumberStyle"); + + gDialog.BulletStyleList.appendItem(GetString("Automatic"), ""); + gDialog.BulletStyleList.appendItem(GetString("Style_1"), gArabic); + gDialog.BulletStyleList.appendItem(GetString("Style_I"), gUpperRoman); + gDialog.BulletStyleList.appendItem(GetString("Style_i"), gLowerRoman); + gDialog.BulletStyleList.appendItem(GetString("Style_A"), gUpperLetters); + gDialog.BulletStyleList.appendItem(GetString("Style_a"), gLowerLetters); + + gDialog.BulletStyleList.value = gNumberStyleType; + } else { + gDialog.BulletStyleList.setAttribute("disabled", "true"); + gDialog.BulletStyleLabel.setAttribute("disabled", "true"); + gDialog.StartingNumberInput.setAttribute("disabled", "true"); + gDialog.StartingNumberLabel.setAttribute("disabled", "true"); + } + + // Disable advanced edit button if changing to "normal" + if (gListType) { + gDialog.AdvancedEditButton.removeAttribute("disabled"); + } else { + gDialog.AdvancedEditButton.setAttribute("disabled", "true"); + } + + if (label) { + gDialog.BulletStyleLabel.textContent = label; + } +} + +function SelectListType() { + // Each list type is stored in the "value" of each menuitem + var NewType = gDialog.ListTypeList.value; + + if (NewType == "ol") { + SetTextboxFocus(gDialog.StartingNumberInput); + } + + if (gListType != NewType) { + gListType = NewType; + + // Create a newlist object for Advanced Editing + try { + if (gListType) { + globalElement = GetCurrentEditor().createElementWithDefaults(gListType); + } + } catch (e) {} + + BuildBulletStyleList(); + } +} + +function SelectBulletStyle() { + // Save the selected index so when user changes + // list style, restore index to associated list + // Each bullet or number type is stored in the "value" of each menuitem + if (gListType == "ul") { + gBulletStyleType = gDialog.BulletStyleList.value; + } else if (gListType == "ol") { + var type = gDialog.BulletStyleList.value; + if (gNumberStyleType != type) { + // Convert existing input value to attr number first, + // then convert to the appropriate format for the newly-selected + gDialog.StartingNumberInput.value = ConvertStartAttrToUserString( + ConvertUserStringToStartAttr(gNumberStyleType), + type + ); + + gNumberStyleType = type; + SetTextboxFocus(gDialog.StartingNumberInput); + } + } +} + +function ValidateData() { + gBulletStyleType = gDialog.BulletStyleList.value; + // globalElement should already be of the correct type + + if (globalElement) { + var editor = GetCurrentEditor(); + if (gListType == "ul") { + if (gBulletStyleType && gDialog.ChangeAllRadio.selected) { + globalElement.setAttribute("type", gBulletStyleType); + } else { + try { + editor.removeAttributeOrEquivalent(globalElement, "type", true); + } catch (e) {} + } + } else if (gListType == "ol") { + if (gBulletStyleType) { + globalElement.setAttribute("type", gBulletStyleType); + } else { + try { + editor.removeAttributeOrEquivalent(globalElement, "type", true); + } catch (e) {} + } + + var startingNumber = ConvertUserStringToStartAttr(gBulletStyleType); + if (startingNumber) { + globalElement.setAttribute("start", startingNumber); + } else { + globalElement.removeAttribute("start"); + } + } + } + return true; +} + +function ConvertUserStringToStartAttr(type) { + var startingNumber = TrimString(gDialog.StartingNumberInput.value); + + switch (type) { + case gUpperRoman: + case gLowerRoman: + // If the input isn't an integer, assume it's a roman numeral. Convert it. + if (!Number(startingNumber)) { + startingNumber = ConvertRomanToArabic(startingNumber); + } + break; + case gUpperLetters: + case gLowerLetters: + // Get the number equivalent of the letters + if (!Number(startingNumber)) { + startingNumber = ConvertLettersToArabic(startingNumber); + } + break; + } + return startingNumber; +} + +function ConvertRomanToArabic(num) { + num = num.toUpperCase(); + if (num && !/[^MDCLXVI]/i.test(num)) { + var Arabic = 0; + var last_digit = 1000; + for (var i = 0; i < num.length; i++) { + var digit = gRomanDigits[num.charAt(i)]; + if (last_digit < digit) { + Arabic -= 2 * last_digit; + } + + last_digit = digit; + Arabic += last_digit; + } + return Arabic; + } + + return ""; +} + +function ConvertArabicToRoman(num) { + if (/^\d{1,4}$/.test(num)) { + var digits = ("000" + num).substr(-4); + return ( + gThousandsArray[digits.charAt(0)] + + gHundredsArray[digits.charAt(1)] + + gTensArray[digits.charAt(2)] + + gOnesArray[digits.charAt(3)] + ); + } + return ""; +} + +function ConvertLettersToArabic(letters) { + letters = letters.toUpperCase(); + if (!letters || /[^A-Z]/.test(letters)) { + return ""; + } + + var num = 0; + for (var i = 0; i < letters.length; i++) { + num = num * 26 + letters.charCodeAt(i) - A + 1; + } + return num; +} + +function ConvertArabicToLetters(num) { + var letters = ""; + while (num) { + num--; + letters = String.fromCharCode(A + (num % 26)) + letters; + num = Math.floor(num / 26); + } + return letters; +} + +function onAccept(event) { + if (ValidateData()) { + // Coalesce into one undo transaction + var editor = GetCurrentEditor(); + + editor.beginTransaction(); + + var changeEntireList = + gDialog.RadioGroup.selectedItem == gDialog.ChangeAllRadio; + + // Remember which radio button was selected + if (gListElement) { + gDialog.RadioGroup.setAttribute("index", changeEntireList ? "0" : "1"); + } + + var changeList; + if (gListElement && gDialog.ChangeAllRadio.selected) { + changeList = true; + } else { + changeList = + gMixedListSelection || + gListType != gOriginalListType || + gBulletStyleType != gOriginalStyleType; + } + if (changeList) { + try { + if (gListType) { + editor.makeOrChangeList( + gListType, + changeEntireList, + gBulletStyleType != gOriginalStyleType ? gBulletStyleType : null + ); + + // Get the new list created: + gListElement = editor.getElementOrParentByTagName(gListType, null); + + editor.cloneAttributes(gListElement, globalElement); + } else { + // Remove all existing lists + if (gListElement && changeEntireList) { + editor.selectElement(gListElement); + } + + editor.removeList("ol"); + editor.removeList("ul"); + editor.removeList("dl"); + } + } catch (e) {} + } + + editor.endTransaction(); + + SaveWindowLocation(); + + return; + } + event.preventDefault(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdListProps.xhtml b/comm/mail/components/compose/content/dialogs/EdListProps.xhtml new file mode 100644 index 0000000000..b8d7c40cb2 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdListProps.xhtml @@ -0,0 +1,101 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % edListProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorListProperties.dtd"> +%edListProperties; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" +> + <dialog> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdListProps.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <html:fieldset> + <html:legend>&ListType.label;</html:legend> + <menulist id="ListType" oncommand="SelectListType()"> + <menupopup> + <menuitem label="&none.value;" /> + <menuitem value="ul" label="&bulletList.value;" /> + <menuitem value="ol" label="&numberList.value;" /> + <menuitem value="dl" label="&definitionList.value;" /> + </menupopup> + </menulist> + </html:fieldset> + <spacer class="spacer" /> + + <!-- message text and list items are set in JS + text value should be identical to string with id=BulletStyle in editor.properties + --> + <html:fieldset> + <html:legend id="BulletStyleLabel">&bulletStyle.label;</html:legend> + <menulist id="BulletStyle" oncommand="SelectBulletStyle()"> + <menupopup /> + </menulist> + <spacer class="spacer" /> + <hbox align="center"> + <label + id="StartingNumberLabel" + control="StartingNumber" + value="&startingNumber.label;" + accesskey="&startingNumber.accessKey;" + /> + <html:input + id="StartingNumber" + type="number" + class="narrow input-inline" + aria-labelledby="StartingNumberLabel" + /> + <spacer /> + </hbox> + </html:fieldset> + <radiogroup id="RadioGroup" index="0" persist="index"> + <radio + id="ChangeAll" + label="&changeEntireListRadio.label;" + accesskey="&changeEntireListRadio.accessKey;" + /> + <radio + id="ChangeSelected" + label="&changeSelectedRadio.label;" + accesskey="&changeSelectedRadio.accessKey;" + /> + </radiogroup> + <vbox id="AdvancedEdit"> + <hbox flex="1" style="margin-top: 0.2em" align="center"> + <!-- This will right-align the button --> + <spacer flex="1" /> + <button + id="AdvancedEditButton1" + oncommand="onAdvancedEdit()" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <separator id="advancedSeparator" class="groove" /> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js new file mode 100644 index 0000000000..c943cc2833 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var gInsertNew = true; +var gAnchorElement = null; +var gOriginalName = ""; +const kTagName = "anchor"; + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + var editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + + gDialog.OkButton = document.querySelector("dialog").getButton("accept"); + gDialog.NameInput = document.getElementById("nameInput"); + + // Get a single selected element of the desired type + gAnchorElement = editor.getSelectedElement(kTagName); + + if (gAnchorElement) { + // We found an element and don't need to insert one + gInsertNew = false; + + // Make a copy to use for AdvancedEdit + globalElement = gAnchorElement.cloneNode(false); + gOriginalName = ConvertToCDATAString(gAnchorElement.name); + } else { + gInsertNew = true; + // We don't have an element selected, + // so create one with default attributes + gAnchorElement = editor.createElementWithDefaults(kTagName); + if (gAnchorElement) { + // Use the current selection as suggested name + var name = GetSelectionAsText(); + // Get 40 characters of the selected text and don't add "...", + // replace whitespace with "_" and strip non-word characters + name = ConvertToCDATAString(TruncateStringAtWordEnd(name, 40, false)); + // Be sure the name is unique to the document + if (AnchorNameExists(name)) { + name += "_"; + } + + // Make a copy to use for AdvancedEdit + globalElement = gAnchorElement.cloneNode(false); + globalElement.setAttribute("name", name); + } + } + if (!gAnchorElement) { + dump("Failed to get selected element or create a new one!\n"); + window.close(); + return; + } + + InitDialog(); + + DoEnabling(); + SetTextboxFocus(gDialog.NameInput); + SetWindowLocation(); +} + +function InitDialog() { + gDialog.NameInput.value = globalElement.getAttribute("name"); +} + +function ChangeName() { + if (gDialog.NameInput.value.length > 0) { + // Replace spaces with "_" and strip other non-URL characters + // Note: we could use ConvertAndEscape, but then we'd + // have to UnEscapeAndConvert beforehand - too messy! + gDialog.NameInput.value = ConvertToCDATAString(gDialog.NameInput.value); + } + DoEnabling(); +} + +function DoEnabling() { + var enable = gDialog.NameInput.value.length > 0; + SetElementEnabled(gDialog.OkButton, enable); + SetElementEnabledById("AdvancedEditButton1", enable); +} + +function AnchorNameExists(name) { + var anchorList; + try { + anchorList = GetCurrentEditor().document.anchors; + } catch (e) {} + + if (anchorList) { + for (var i = 0; i < anchorList.length; i++) { + if (anchorList[i].name == name) { + return true; + } + } + } + return false; +} + +// Get and validate data from widgets. +// Set attributes on globalElement so they can be accessed by AdvancedEdit() +function ValidateData() { + var name = TrimString(gDialog.NameInput.value); + if (!name) { + ShowInputErrorMessage(GetString("MissingAnchorNameError")); + SetTextboxFocus(gDialog.NameInput); + return false; + } + // Replace spaces with "_" and strip other characters + // Note: we could use ConvertAndEscape, but then we'd + // have to UnConverAndEscape beforehand - too messy! + name = ConvertToCDATAString(name); + + if (gOriginalName != name && AnchorNameExists(name)) { + ShowInputErrorMessage( + GetString("DuplicateAnchorNameError").replace(/%name%/, name) + ); + SetTextboxFocus(gDialog.NameInput); + return false; + } + globalElement.name = name; + + return true; +} + +function onAccept(event) { + if (ValidateData()) { + if (gOriginalName != globalElement.name) { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + + try { + // "false" = don't delete selected text when inserting + if (gInsertNew) { + // We must insert element before copying CSS style attribute, + // but we must set the name else it won't insert at all + gAnchorElement.name = globalElement.name; + editor.insertElementAtSelection(gAnchorElement, false); + } + + // Copy attributes to element we are changing or inserting + editor.cloneAttributes(gAnchorElement, globalElement); + } catch (e) {} + + editor.endTransaction(); + } + SaveWindowLocation(); + return; + } + event.preventDefault(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml new file mode 100644 index 0000000000..d26f4d73b4 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml @@ -0,0 +1,67 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % edNamedAnchorProperties SYSTEM "chrome://messenger/locale/messengercompose/EdNamedAnchorProperties.dtd"> +%edNamedAnchorProperties; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<window + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" +> + <dialog> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdNamedAnchorProps.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <label + id="nameLabel" + control="nameInput" + value="&anchorNameEditField.label;" + accesskey="&anchorNameEditField.accessKey;" + /> + <html:input + id="nameInput" + type="text" + class="MinWidth20em input-inline" + oninput="ChangeName()" + title="&nameInput.tooltip;" + aria-labelledby="nameLabel" + /> + <spacer class="spacer" /> + <vbox id="AdvancedEdit"> + <hbox flex="1" style="margin-top: 0.2em" align="center"> + <!-- This will right-align the button --> + <spacer flex="1" /> + <button + id="AdvancedEditButton1" + oncommand="onAdvancedEdit()" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <separator id="advancedSeparator" class="groove" /> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdReplace.js b/comm/mail/components/compose/content/dialogs/EdReplace.js new file mode 100644 index 0000000000..c937702416 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdReplace.js @@ -0,0 +1,380 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var gReplaceDialog; // Quick access to document/form elements. +var gFindInst; // nsIWebBrowserFind that we're going to use +var gFindService; // Global service which remembers find params +var gEditor; // the editor we're using + +document.addEventListener("dialogaccept", event => { + onFindNext(); + event.preventDefault(); +}); + +function initDialogObject() { + // Create gReplaceDialog object and initialize. + gReplaceDialog = {}; + gReplaceDialog.findInput = document.getElementById("dialog.findInput"); + gReplaceDialog.replaceInput = document.getElementById("dialog.replaceInput"); + gReplaceDialog.caseSensitive = document.getElementById( + "dialog.caseSensitive" + ); + gReplaceDialog.wrap = document.getElementById("dialog.wrap"); + gReplaceDialog.searchBackwards = document.getElementById( + "dialog.searchBackwards" + ); + gReplaceDialog.findNext = document.getElementById("findNext"); + gReplaceDialog.replace = document.getElementById("replace"); + gReplaceDialog.replaceAndFind = document.getElementById("replaceAndFind"); + gReplaceDialog.replaceAll = document.getElementById("replaceAll"); +} + +function loadDialog() { + // Set initial dialog field contents. + // Set initial dialog field contents. Use the gFindInst attributes first, + // this is necessary for window.find() + gReplaceDialog.findInput.value = gFindInst.searchString + ? gFindInst.searchString + : gFindService.searchString; + gReplaceDialog.replaceInput.value = gFindService.replaceString; + gReplaceDialog.caseSensitive.checked = gFindInst.matchCase + ? gFindInst.matchCase + : gFindService.matchCase; + gReplaceDialog.wrap.checked = gFindInst.wrapFind + ? gFindInst.wrapFind + : gFindService.wrapFind; + gReplaceDialog.searchBackwards.checked = gFindInst.findBackwards + ? gFindInst.findBackwards + : gFindService.findBackwards; + + doEnabling(); +} + +function onLoad() { + // Get the xul <editor> element: + var editorElement = window.arguments[0]; + + // If we don't get the editor, then we won't allow replacing. + gEditor = editorElement.getEditor(editorElement.contentWindow); + if (!gEditor) { + window.close(); + return; + } + + // Get the nsIWebBrowserFind service: + gFindInst = editorElement.webBrowserFind; + + try { + // get the find service, which stores global find state + gFindService = Cc["@mozilla.org/find/find_service;1"].getService( + Ci.nsIFindService + ); + } catch (e) { + dump("No find service!\n"); + gFindService = 0; + } + + // Init gReplaceDialog. + initDialogObject(); + + // Change "OK" to "Find". + // dialog.find.label = document.getElementById("fBLT").getAttribute("label"); + + // Fill dialog. + loadDialog(); + + if (gReplaceDialog.findInput.value) { + gReplaceDialog.findInput.select(); + } else { + gReplaceDialog.findInput.focus(); + } +} + +function saveFindData() { + // Set data attributes per user input. + if (gFindService) { + gFindService.searchString = gReplaceDialog.findInput.value; + gFindService.matchCase = gReplaceDialog.caseSensitive.checked; + gFindService.wrapFind = gReplaceDialog.wrap.checked; + gFindService.findBackwards = gReplaceDialog.searchBackwards.checked; + } +} + +function setUpFindInst() { + gFindInst.searchString = gReplaceDialog.findInput.value; + gFindInst.matchCase = gReplaceDialog.caseSensitive.checked; + gFindInst.wrapFind = gReplaceDialog.wrap.checked; + gFindInst.findBackwards = gReplaceDialog.searchBackwards.checked; +} + +function onFindNext() { + // Transfer dialog contents to the find service. + saveFindData(); + // set up the find instance + setUpFindInst(); + + // Search. + var result = gFindInst.findNext(); + + if (!result) { + var bundle = document.getElementById("findBundle"); + Services.prompt.alert( + window, + GetString("Alert"), + bundle.getString("notFoundWarning") + ); + SetTextboxFocus(gReplaceDialog.findInput); + gReplaceDialog.findInput.select(); + gReplaceDialog.findInput.focus(); + return false; + } + return true; +} + +function onReplace() { + if (!gEditor) { + return false; + } + + // Does the current selection match the find string? + var selection = gEditor.selection; + + var selStr = selection.toString(); + var specStr = gReplaceDialog.findInput.value; + if (!gReplaceDialog.caseSensitive.checked) { + selStr = selStr.toLowerCase(); + specStr = specStr.toLowerCase(); + } + // Unfortunately, because of whitespace we can't just check + // whether (selStr == specStr), but have to loop ourselves. + // N chars of whitespace in specStr can match any M >= N in selStr. + var matches = true; + var specLen = specStr.length; + var selLen = selStr.length; + if (selLen < specLen) { + matches = false; + } else { + var specArray = specStr.match(/\S+|\s+/g); + var selArray = selStr.match(/\S+|\s+/g); + if (specArray.length != selArray.length) { + matches = false; + } else { + for (var i = 0; i < selArray.length; i++) { + if (selArray[i] != specArray[i]) { + if (/\S/.test(selArray[i][0]) || /\S/.test(specArray[i][0])) { + // not a space chunk -- match fails + matches = false; + break; + } else if (selArray[i].length < specArray[i].length) { + // if it's a space chunk then we only care that sel be + // at least as long as spec + matches = false; + break; + } + } + } + } + } + + // If the current selection doesn't match the pattern, + // then we want to find the next match, but not do the replace. + // That's what most other apps seem to do. + // So here, just return. + if (!matches) { + return false; + } + + // Transfer dialog contents to the find service. + saveFindData(); + + // For reverse finds, need to remember the caret position + // before current selection + var newRange; + if (gReplaceDialog.searchBackwards.checked && selection.rangeCount > 0) { + newRange = selection.getRangeAt(0).cloneRange(); + newRange.collapse(true); + } + + // nsPlaintextEditor::InsertText fails if the string is empty, + // so make that a special case: + var replStr = gReplaceDialog.replaceInput.value; + if (replStr == "") { + gEditor.deleteSelection(gEditor.eNone, gEditor.eStrip); + } else { + gEditor.insertText(replStr); + } + + // For reverse finds, need to move caret just before the replaced text + if (gReplaceDialog.searchBackwards.checked && newRange) { + gEditor.selection.removeAllRanges(); + gEditor.selection.addRange(newRange); + } + + return true; +} + +function onReplaceAll() { + if (!gEditor) { + return; + } + + var findStr = gReplaceDialog.findInput.value; + var repStr = gReplaceDialog.replaceInput.value; + + // Transfer dialog contents to the find service. + saveFindData(); + + var finder = Cc["@mozilla.org/embedcomp/rangefind;1"] + .createInstance() + .QueryInterface(Ci.nsIFind); + + finder.caseSensitive = gReplaceDialog.caseSensitive.checked; + finder.findBackwards = gReplaceDialog.searchBackwards.checked; + + // We want the whole operation to be undoable in one swell foop, + // so start a transaction: + gEditor.beginTransaction(); + + // and to make sure we close the transaction, guard against exceptions: + try { + // Make a range containing the current selection, + // so we don't go past it when we wrap. + var selection = gEditor.selection; + var selecRange; + if (selection.rangeCount > 0) { + selecRange = selection.getRangeAt(0); + } + var origRange = selecRange.cloneRange(); + + // We'll need a range for the whole document: + var wholeDocRange = gEditor.document.createRange(); + var rootNode = gEditor.rootElement; + wholeDocRange.selectNodeContents(rootNode); + + // And start and end points: + var endPt = gEditor.document.createRange(); + + if (gReplaceDialog.searchBackwards.checked) { + endPt.setStart(wholeDocRange.startContainer, wholeDocRange.startOffset); + endPt.setEnd(wholeDocRange.startContainer, wholeDocRange.startOffset); + } else { + endPt.setStart(wholeDocRange.endContainer, wholeDocRange.endOffset); + endPt.setEnd(wholeDocRange.endContainer, wholeDocRange.endOffset); + } + + // Find and replace from here to end (start) of document: + var foundRange; + var searchRange = wholeDocRange.cloneRange(); + while ( + (foundRange = finder.Find(findStr, searchRange, selecRange, endPt)) != + null + ) { + gEditor.selection.removeAllRanges(); + gEditor.selection.addRange(foundRange); + + // The editor will leave the caret at the end of the replaced text. + // For reverse finds, we need it at the beginning, + // so save the next position now. + if (gReplaceDialog.searchBackwards.checked) { + selecRange = foundRange.cloneRange(); + selecRange.setEnd(selecRange.startContainer, selecRange.startOffset); + } + + // nsPlaintextEditor::InsertText fails if the string is empty, + // so make that a special case: + if (repStr == "") { + gEditor.deleteSelection(gEditor.eNone, gEditor.eStrip); + } else { + gEditor.insertText(repStr); + } + + // If we're going forward, we didn't save selecRange before, so do it now: + if (!gReplaceDialog.searchBackwards.checked) { + selection = gEditor.selection; + if (selection.rangeCount <= 0) { + gEditor.endTransaction(); + return; + } + selecRange = selection.getRangeAt(0).cloneRange(); + } + } + + // If no wrapping, then we're done + if (!gReplaceDialog.wrap.checked) { + gEditor.endTransaction(); + return; + } + + // If wrapping, find from start/end of document back to start point. + if (gReplaceDialog.searchBackwards.checked) { + // Collapse origRange to end + origRange.setStart(origRange.endContainer, origRange.endOffset); + // Set current position to document end + selecRange.setEnd(wholeDocRange.endContainer, wholeDocRange.endOffset); + selecRange.setStart(wholeDocRange.endContainer, wholeDocRange.endOffset); + } else { + // Collapse origRange to start + origRange.setEnd(origRange.startContainer, origRange.startOffset); + // Set current position to document start + selecRange.setStart( + wholeDocRange.startContainer, + wholeDocRange.startOffset + ); + selecRange.setEnd( + wholeDocRange.startContainer, + wholeDocRange.startOffset + ); + } + + while ( + (foundRange = finder.Find( + findStr, + wholeDocRange, + selecRange, + origRange + )) != null + ) { + gEditor.selection.removeAllRanges(); + gEditor.selection.addRange(foundRange); + + // Save insert point for backward case + if (gReplaceDialog.searchBackwards.checked) { + selecRange = foundRange.cloneRange(); + selecRange.setEnd(selecRange.startContainer, selecRange.startOffset); + } + + // nsPlaintextEditor::InsertText fails if the string is empty, + // so make that a special case: + if (repStr == "") { + gEditor.deleteSelection(gEditor.eNone, gEditor.eStrip); + } else { + gEditor.insertText(repStr); + } + + // Get insert point for forward case + if (!gReplaceDialog.searchBackwards.checked) { + selection = gEditor.selection; + if (selection.rangeCount <= 0) { + gEditor.endTransaction(); + return; + } + selecRange = selection.getRangeAt(0); + } + } + } catch (e) {} + + gEditor.endTransaction(); +} + +function doEnabling() { + var findStr = gReplaceDialog.findInput.value; + gReplaceDialog.enabled = findStr; + gReplaceDialog.findNext.disabled = !findStr; + gReplaceDialog.replace.disabled = !findStr; + gReplaceDialog.replaceAndFind.disabled = !findStr; + gReplaceDialog.replaceAll.disabled = !findStr; +} diff --git a/comm/mail/components/compose/content/dialogs/EdReplace.xhtml b/comm/mail/components/compose/content/dialogs/EdReplace.xhtml new file mode 100644 index 0000000000..62ce5a67e2 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdReplace.xhtml @@ -0,0 +1,126 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorReplace.dtd"> + +<window + id="replaceDlg" + title="&replaceDialog.title;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + persist="screenX screenY" + lightweightthemes="true" + onload="onLoad()" +> + <dialog buttons="cancel"> + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdReplace.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <stringbundle + id="findBundle" + src="chrome://global/locale/finddialog.properties" + /> + + <hbox> + <vbox> + <spacer class="spacer" /> + <html:div class="grid-two-column"> + <html:div class="flex-items-center"> + <label + value="&findField.label;" + accesskey="&findField.accesskey;" + control="dialog.findInput" + /> + </html:div> + <html:div> + <html:input + id="dialog.findInput" + class="input-inline" + oninput="doEnabling();" + /> + </html:div> + <html:div class="flex-items-center"> + <label + value="&replaceField.label;" + accesskey="&replaceField.accesskey;" + control="dialog.replaceInput" + /> + </html:div> + <html:div> + <html:input + id="dialog.replaceInput" + class="input-inline" + oninput="doEnabling();" + /> + </html:div> + <html:div class="grid-item-col2"> + <vbox align="start"> + <checkbox + id="dialog.caseSensitive" + label="&caseSensitiveCheckbox.label;" + accesskey="&caseSensitiveCheckbox.accesskey;" + /> + <checkbox + id="dialog.wrap" + label="&wrapCheckbox.label;" + accesskey="&wrapCheckbox.accesskey;" + /> + <checkbox + id="dialog.searchBackwards" + label="&backwardsCheckbox.label;" + accesskey="&backwardsCheckbox.accesskey;" + /> + </vbox> + </html:div> + </html:div> + </vbox> + <spacer class="spacer" /> + <vbox> + <button + id="findNext" + label="&findNextButton.label;" + accesskey="&findNextButton.accesskey;" + oncommand="onFindNext();" + default="true" + /> + <button + id="replace" + label="&replaceButton.label;" + accesskey="&replaceButton.accesskey;" + oncommand="onReplace();" + /> + <button + id="replaceAndFind" + label="&replaceAndFindButton.label;" + accesskey="&replaceAndFindButton.accesskey;" + oncommand="onReplace(); onFindNext();" + /> + <button + id="replaceAll" + label="&replaceAllButton.label;" + accesskey="&replaceAllButton.accesskey;" + oncommand="onReplaceAll();" + /> + <button + dlgtype="cancel" + label="&closeButton.label;" + accesskey="&closeButton.accesskey;" + /> + </vbox> + </hbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdSpellCheck.js b/comm/mail/components/compose/content/dialogs/EdSpellCheck.js new file mode 100644 index 0000000000..5b54205bc3 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdSpellCheck.js @@ -0,0 +1,496 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../../base/content/utilityOverlay.js */ +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +var { InlineSpellChecker } = ChromeUtils.importESModule( + "resource://gre/modules/InlineSpellChecker.sys.mjs" +); + +var gMisspelledWord; +var gSpellChecker = null; +var gAllowSelectWord = true; +var gPreviousReplaceWord = ""; +var gFirstTime = true; +var gDictCount = 0; + +document.addEventListener("dialogaccept", doDefault); +document.addEventListener("dialogcancel", CancelSpellCheck); + +function Startup() { + var editor = GetCurrentEditor(); + if (!editor) { + window.close(); + return; + } + + // Get the spellChecker shell + gSpellChecker = Cu.createSpellChecker(); + if (!gSpellChecker) { + dump("SpellChecker not found!!!\n"); + window.close(); + return; + } + + // Start the spell checker module. + try { + var skipBlockQuotes = window.arguments[1]; + var enableSelectionChecking = window.arguments[2]; + + gSpellChecker.setFilterType( + skipBlockQuotes + ? Ci.nsIEditorSpellCheck.FILTERTYPE_MAIL + : Ci.nsIEditorSpellCheck.FILTERTYPE_NORMAL + ); + gSpellChecker.InitSpellChecker( + editor, + enableSelectionChecking, + spellCheckStarted + ); + } catch (ex) { + dump("*** Exception error: InitSpellChecker\n"); + window.close(); + } +} + +function spellCheckStarted() { + gDialog.MisspelledWordLabel = document.getElementById("MisspelledWordLabel"); + gDialog.MisspelledWord = document.getElementById("MisspelledWord"); + gDialog.ReplaceButton = document.getElementById("Replace"); + gDialog.IgnoreButton = document.getElementById("Ignore"); + gDialog.StopButton = document.getElementById("Stop"); + gDialog.CloseButton = document.getElementById("Close"); + gDialog.ReplaceWordInput = document.getElementById("ReplaceWordInput"); + gDialog.SuggestedList = document.getElementById("SuggestedList"); + gDialog.LanguageMenulist = document.getElementById("LanguageMenulist"); + + // Fill in the language menulist and sync it up + // with the spellchecker's current language. + + var curLangs; + + try { + curLangs = new Set(gSpellChecker.getCurrentDictionaries()); + } catch (ex) { + curLangs = new Set(); + } + + InitLanguageMenu(curLangs); + + // Get the first misspelled word and setup all UI + NextWord(); + + // When startup param is true, setup different UI when spell checking + // just before sending mail message + if (window.arguments[0]) { + // If no misspelled words found, simply close dialog and send message + if (!gMisspelledWord) { + onClose(); + return; + } + + // Hide "Close" button and use "Send" instead + gDialog.CloseButton.hidden = true; + gDialog.CloseButton = document.getElementById("Send"); + gDialog.CloseButton.hidden = false; + } else { + // Normal spell checking - hide the "Stop" button + // (Note that this button is the "Cancel" button for + // Esc keybinding and related window close actions) + gDialog.StopButton.hidden = true; + } + + // Clear flag that determines message when + // no misspelled word is found + // (different message when used for the first time) + gFirstTime = false; + + window.sizeToContent(); +} + +/** + * Populate the dictionary language selector menu. + * + * @param {Set<string>} activeDictionaries - Currently active dictionaries. + */ +function InitLanguageMenu(activeDictionaries) { + // Get the list of dictionaries from + // the spellchecker. + + var dictList; + try { + dictList = gSpellChecker.GetDictionaryList(); + } catch (ex) { + dump("Failed to get DictionaryList!\n"); + return; + } + + // If we're not just starting up and dictionary count + // hasn't changed then no need to update the menu. + if (gDictCount == dictList.length) { + return; + } + + // Store current dictionary count. + gDictCount = dictList.length; + + var inlineSpellChecker = new InlineSpellChecker(); + var sortedList = inlineSpellChecker.sortDictionaryList(dictList); + + // Remove any languages from the list. + let list = document.getElementById("dictionary-list"); + let template = document.getElementById("language-item"); + + list.replaceChildren( + ...sortedList.map(({ displayName, localeCode }) => { + let item = template.content.cloneNode(true); + item.querySelector(".checkbox-label").textContent = displayName; + let input = item.querySelector("input"); + input.addEventListener("input", () => { + SelectLanguage(localeCode); + }); + input.checked = activeDictionaries.has(localeCode); + return item; + }) + ); +} + +function DoEnabling() { + if (!gMisspelledWord) { + // No more misspelled words + gDialog.MisspelledWord.setAttribute( + "value", + GetString(gFirstTime ? "NoMisspelledWord" : "CheckSpellingDone") + ); + + gDialog.ReplaceButton.removeAttribute("default"); + gDialog.IgnoreButton.removeAttribute("default"); + + gDialog.CloseButton.setAttribute("default", "true"); + // Shouldn't have to do this if "default" is true? + gDialog.CloseButton.focus(); + + SetElementEnabledById("MisspelledWordLabel", false); + SetElementEnabledById("ReplaceWordLabel", false); + SetElementEnabledById("ReplaceWordInput", false); + SetElementEnabledById("CheckWord", false); + SetElementEnabledById("SuggestedListLabel", false); + SetElementEnabledById("SuggestedList", false); + SetElementEnabledById("Ignore", false); + SetElementEnabledById("IgnoreAll", false); + SetElementEnabledById("Replace", false); + SetElementEnabledById("ReplaceAll", false); + SetElementEnabledById("AddToDictionary", false); + } else { + SetElementEnabledById("MisspelledWordLabel", true); + SetElementEnabledById("ReplaceWordLabel", true); + SetElementEnabledById("ReplaceWordInput", true); + SetElementEnabledById("CheckWord", true); + SetElementEnabledById("SuggestedListLabel", true); + SetElementEnabledById("SuggestedList", true); + SetElementEnabledById("Ignore", true); + SetElementEnabledById("IgnoreAll", true); + SetElementEnabledById("AddToDictionary", true); + + gDialog.CloseButton.removeAttribute("default"); + SetReplaceEnable(); + } +} + +function NextWord() { + gMisspelledWord = gSpellChecker.GetNextMisspelledWord(); + SetWidgetsForMisspelledWord(); +} + +function SetWidgetsForMisspelledWord() { + gDialog.MisspelledWord.setAttribute("value", gMisspelledWord); + + // Initial replace word is misspelled word + gDialog.ReplaceWordInput.value = gMisspelledWord; + gPreviousReplaceWord = gMisspelledWord; + + // This sets gDialog.ReplaceWordInput to first suggested word in list + FillSuggestedList(gMisspelledWord); + + DoEnabling(); + + if (gMisspelledWord) { + SetTextboxFocus(gDialog.ReplaceWordInput); + } +} + +function CheckWord() { + var word = gDialog.ReplaceWordInput.value; + if (word) { + if (gSpellChecker.CheckCurrentWord(word)) { + FillSuggestedList(word); + SetReplaceEnable(); + } else { + ClearListbox(gDialog.SuggestedList); + var item = gDialog.SuggestedList.appendItem( + GetString("CorrectSpelling"), + "" + ); + if (item) { + item.setAttribute("disabled", "true"); + } + // Suppress being able to select the message text + gAllowSelectWord = false; + } + } +} + +function SelectSuggestedWord() { + if (gAllowSelectWord) { + if (gDialog.SuggestedList.selectedItem) { + var selValue = gDialog.SuggestedList.selectedItem.label; + gDialog.ReplaceWordInput.value = selValue; + gPreviousReplaceWord = selValue; + } else { + gDialog.ReplaceWordInput.value = gPreviousReplaceWord; + } + SetReplaceEnable(); + } +} + +function ChangeReplaceWord() { + // Calling this triggers SelectSuggestedWord(), + // so temporarily suppress the effect of that + var saveAllow = gAllowSelectWord; + gAllowSelectWord = false; + + // Select matching word in list + var newSelectedItem; + var replaceWord = TrimString(gDialog.ReplaceWordInput.value); + if (replaceWord) { + for (var i = 0; i < gDialog.SuggestedList.getRowCount(); i++) { + var item = gDialog.SuggestedList.getItemAtIndex(i); + if (item.label == replaceWord) { + newSelectedItem = item; + break; + } + } + } + gDialog.SuggestedList.selectedItem = newSelectedItem; + + gAllowSelectWord = saveAllow; + + // Remember the new word + gPreviousReplaceWord = gDialog.ReplaceWordInput.value; + + SetReplaceEnable(); +} + +function Ignore() { + NextWord(); +} + +function IgnoreAll() { + if (gMisspelledWord) { + gSpellChecker.IgnoreWordAllOccurrences(gMisspelledWord); + } + NextWord(); +} + +function Replace(newWord) { + if (!newWord) { + return; + } + + if (gMisspelledWord && gMisspelledWord != newWord) { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + try { + gSpellChecker.ReplaceWord(gMisspelledWord, newWord, false); + } catch (e) {} + editor.endTransaction(); + } + NextWord(); +} + +function ReplaceAll() { + var newWord = gDialog.ReplaceWordInput.value; + if (gMisspelledWord && gMisspelledWord != newWord) { + var editor = GetCurrentEditor(); + editor.beginTransaction(); + try { + gSpellChecker.ReplaceWord(gMisspelledWord, newWord, true); + } catch (e) {} + editor.endTransaction(); + } + NextWord(); +} + +function AddToDictionary() { + if (gMisspelledWord) { + gSpellChecker.AddWordToDictionary(gMisspelledWord); + } + NextWord(); +} + +function EditDictionary() { + window.openDialog( + "chrome://messenger/content/messengercompose/EdDictionary.xhtml", + "_blank", + "chrome,close,titlebar,modal", + "", + gMisspelledWord + ); +} + +/** + * Change the selection state of the given dictionary language. + * + * @param {string} language + */ +function SelectLanguage(language) { + let activeDictionaries = new Set(gSpellChecker.getCurrentDictionaries()); + if (activeDictionaries.has(language)) { + activeDictionaries.delete(language); + } else { + activeDictionaries.add(language); + } + let activeDictionariesArray = Array.from(activeDictionaries); + gSpellChecker.setCurrentDictionaries(activeDictionariesArray); + // For compose windows we need to set the "lang" attribute so the + // core editor uses the correct dictionary for the inline spell check. + if (window.arguments[1]) { + if ("ComposeChangeLanguage" in window.opener) { + // We came here from a compose window. + window.opener.ComposeChangeLanguage(activeDictionariesArray); + } else if (activeDictionaries.size === 1) { + window.opener.document.documentElement.setAttribute( + "lang", + activeDictionariesArray[0] + ); + } else { + window.opener.document.documentElement.setAttribute("lang", ""); + } + } +} + +function Recheck() { + var recheckLanguages; + + function finishRecheck() { + gSpellChecker.setCurrentDictionaries(recheckLanguages); + gMisspelledWord = gSpellChecker.GetNextMisspelledWord(); + SetWidgetsForMisspelledWord(); + } + + // TODO: Should we bother to add a "Recheck" method to interface? + try { + recheckLanguages = gSpellChecker.getCurrentDictionaries(); + gSpellChecker.UninitSpellChecker(); + // Clear the ignore all list. + Cc["@mozilla.org/spellchecker/personaldictionary;1"] + .getService(Ci.mozIPersonalDictionary) + .endSession(); + gSpellChecker.InitSpellChecker(GetCurrentEditor(), false, finishRecheck); + } catch (ex) { + console.error(ex); + } +} + +function FillSuggestedList(misspelledWord) { + var list = gDialog.SuggestedList; + + // Clear the current contents of the list + gAllowSelectWord = false; + ClearListbox(list); + var item; + + if (misspelledWord.length > 0) { + // Get suggested words until an empty string is returned + var count = 0; + do { + var word = gSpellChecker.GetSuggestedWord(); + if (word.length > 0) { + list.appendItem(word, ""); + count++; + } + } while (word.length > 0); + + if (count == 0) { + // No suggestions - show a message but don't let user select it + item = list.appendItem(GetString("NoSuggestedWords")); + if (item) { + item.setAttribute("disabled", "true"); + } + gAllowSelectWord = false; + } else { + gAllowSelectWord = true; + // Initialize with first suggested list by selecting it + gDialog.SuggestedList.selectedIndex = 0; + } + } else { + item = list.appendItem("", ""); + if (item) { + item.setAttribute("disabled", "true"); + } + } +} + +function SetReplaceEnable() { + // Enable "Change..." buttons only if new word is different than misspelled + var newWord = gDialog.ReplaceWordInput.value; + var enable = newWord.length > 0 && newWord != gMisspelledWord; + SetElementEnabledById("Replace", enable); + SetElementEnabledById("ReplaceAll", enable); + if (enable) { + gDialog.ReplaceButton.setAttribute("default", "true"); + gDialog.IgnoreButton.removeAttribute("default"); + } else { + gDialog.IgnoreButton.setAttribute("default", "true"); + gDialog.ReplaceButton.removeAttribute("default"); + } +} + +function doDefault(event) { + if (gDialog.ReplaceButton.getAttribute("default") == "true") { + Replace(gDialog.ReplaceWordInput.value); + } else if (gDialog.IgnoreButton.getAttribute("default") == "true") { + Ignore(); + } else if (gDialog.CloseButton.getAttribute("default") == "true") { + onClose(); + } + + event.preventDefault(); +} + +function ExitSpellChecker() { + if (gSpellChecker) { + try { + gSpellChecker.UninitSpellChecker(); + // now check the document over again with the new dictionary + // if we have an inline spellchecker + if ( + "InlineSpellCheckerUI" in window.opener && + window.opener.InlineSpellCheckerUI.enabled + ) { + window.opener.InlineSpellCheckerUI.mInlineSpellChecker.spellCheckRange( + null + ); + } + } finally { + gSpellChecker = null; + } + } +} + +function CancelSpellCheck() { + ExitSpellChecker(); + + // Signal to calling window that we canceled + window.opener.cancelSendMessage = true; +} + +function onClose() { + ExitSpellChecker(); + + window.opener.cancelSendMessage = false; + window.close(); +} diff --git a/comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml b/comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml new file mode 100644 index 0000000000..fcff0e1703 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml @@ -0,0 +1,209 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://messenger/locale/messengercompose/EditorSpellCheck.dtd"> + +<!-- dialog containing a control requiring initial setup --> +<window + id="spellCheckDlg" + title="&windowTitle.label;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + persist="screenX screenY" + lightweightthemes="true" + onload="Startup()" +> + <dialog buttons="cancel"> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://communicator/content/utilityOverlay.js" /> + <script src="chrome://messenger/content/messengercompose/EdSpellCheck.js" /> + <script src="chrome://global/content/contentAreaUtils.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <stringbundle + id="languageBundle" + src="chrome://global/locale/languageNames.properties" + /> + <stringbundle + id="regionBundle" + src="chrome://global/locale/regionNames.properties" + /> + + <html:div class="grid-three-column-auto-x-auto"> + <html:div class="flex-items-center"> + <label id="MisspelledWordLabel" value="&misspelledWord.label;" /> + </html:div> + <html:div class="flex-items-center"> + <label id="MisspelledWord" class="bold" crop="end" /> + </html:div> + <html:div class="flex-items-center"> + <button + class="spell-check" + label="&recheckButton2.label;" + oncommand="Recheck();" + accesskey="&recheckButton2.accessKey;" + /> + </html:div> + <html:div class="flex-items-center"> + <label + id="ReplaceWordLabel" + value="&wordEditField.label;" + control="ReplaceWordInput" + accesskey="&wordEditField.accessKey;" + /> + </html:div> + <html:div> + <hbox flex="1" class="input-container"> + <html:input + id="ReplaceWordInput" + type="text" + class="input-inline" + onchange="ChangeReplaceWord()" + aria-labelledby="ReplaceWordLabel" + /> + </hbox> + </html:div> + <html:div class="flex-items-center"> + <button + id="CheckWord" + class="spell-check" + oncommand="CheckWord()" + label="&checkwordButton.label;" + accesskey="&checkwordButton.accessKey;" + /> + </html:div> + </html:div> + <label + id="SuggestedListLabel" + value="&suggestions.label;" + control="SuggestedList" + accesskey="&suggestions.accessKey;" + /> + <hbox flex="1" class="display-flex"> + <html:div class="grid-two-column-x-auto flex-1"> + <html:div class="display-flex"> + <richlistbox + id="SuggestedList" + class="display-flex flex-1" + onselect="SelectSuggestedWord()" + ondblclick="if (gAllowSelectWord) { Replace(event.target.value); }" + /> + </html:div> + <html:div> + <vbox> + <html:div class="grid-two-column-equalsize"> + <button + id="Replace" + class="spell-check" + label="&replaceButton.label;" + oncommand="Replace(gDialog.ReplaceWordInput.value);" + accesskey="&replaceButton.accessKey;" + /> + <button + id="Ignore" + class="spell-check" + oncommand="Ignore();" + label="&ignoreButton.label;" + accesskey="&ignoreButton.accessKey;" + /> + <button + id="ReplaceAll" + class="spell-check" + oncommand="ReplaceAll();" + label="&replaceAllButton.label;" + accesskey="&replaceAllButton.accessKey;" + /> + <button + id="IgnoreAll" + class="spell-check" + oncommand="IgnoreAll();" + label="&ignoreAllButton.label;" + accesskey="&ignoreAllButton.accessKey;" + /> + </html:div> + <separator /> + <label value="&userDictionary.label;" /> + <hbox align="start"> + <button + id="AddToDictionary" + class="spell-check" + oncommand="AddToDictionary()" + label="&addToUserDictionaryButton.label;" + accesskey="&addToUserDictionaryButton.accessKey;" + /> + <button + id="EditDictionary" + class="spell-check" + oncommand="EditDictionary()" + label="&editUserDictionaryButton.label;" + accesskey="&editUserDictionaryButton.accessKey;" + /> + </hbox> + </vbox> + </html:div> + <html:div class="grid-item-span-row"> + <label + value="&languagePopup.label;" + control="LanguageMenulist" + accesskey="&languagePopup.accessKey;" + /> + </html:div> + <html:div> + <html:ul id="dictionary-list"> </html:ul> + <html:template id="language-item" + ><html:li> + <html:label + ><html:input type="checkbox"></html:input> + <html:span class="checkbox-label"></html:span + ></html:label> </html:li + ></html:template> + <html:a onclick="openDictionaryList()" href="" + >&moreDictionaries.label;</html:a + > + </html:div> + <html:div> + <hbox class="display-flex"> + <button + id="Stop" + class="spell-check" + dlgtype="cancel" + label="&stopButton.label;" + oncommand="CancelSpellCheck();" + accesskey="&stopButton.accessKey;" + /> + <spacer class="flex-1" /> + <button + id="Close" + class="spell-check" + label="&closeButton.label;" + oncommand="onClose();" + accesskey="&closeButton.accessKey;" + /> + <button + id="Send" + class="spell-check" + label="&sendButton.label;" + oncommand="onClose();" + accesskey="&sendButton.accessKey;" + hidden="true" + /> + </hbox> + </html:div> + </html:div> + </hbox> + </dialog> +</window> diff --git a/comm/mail/components/compose/content/dialogs/EdTableProps.js b/comm/mail/components/compose/content/dialogs/EdTableProps.js new file mode 100644 index 0000000000..fd4ab40f3a --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdTableProps.js @@ -0,0 +1,1426 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../editorUtilities.js */ +/* import-globals-from EdDialogCommon.js */ + +// Cancel() is in EdDialogCommon.js + +var gTableElement; +var gCellElement; +var gTableCaptionElement; +var globalCellElement; +var globalTableElement; +var gValidateTab; +const defHAlign = "left"; +const centerStr = "center"; // Index=1 +const rightStr = "right"; // 2 +const justifyStr = "justify"; // 3 +const charStr = "char"; // 4 +const defVAlign = "middle"; +const topStr = "top"; +const bottomStr = "bottom"; +const bgcolor = "bgcolor"; +var gTableColor; +var gCellColor; + +const cssBackgroundColorStr = "background-color"; + +var gRowCount = 1; +var gColCount = 1; +var gLastRowIndex; +var gLastColIndex; +var gNewRowCount; +var gNewColCount; +var gCurRowIndex; +var gCurColIndex; +var gCurColSpan; +var gSelectedCellsType = 1; +const SELECT_CELL = 1; +const SELECT_ROW = 2; +const SELECT_COLUMN = 3; +const RESET_SELECTION = 0; +var gCellData = { + value: null, + startRowIndex: 0, + startColIndex: 0, + rowSpan: 0, + colSpan: 0, + actualRowSpan: 0, + actualColSpan: 0, + isSelected: false, +}; +var gAdvancedEditUsed; +var gAlignWasChar = false; + +/* +From C++: + 0 TABLESELECTION_TABLE + 1 TABLESELECTION_CELL There are 1 or more cells selected + but complete rows or columns are not selected + 2 TABLESELECTION_ROW All cells are in 1 or more rows + and in each row, all cells selected + Note: This is the value if all rows (thus all cells) are selected + 3 TABLESELECTION_COLUMN All cells are in 1 or more columns +*/ + +var gSelectedCellCount = 0; +var gApplyUsed = false; +var gSelection; +var gCellDataChanged = false; +var gCanDelete = false; +var gUseCSS = true; +var gActiveEditor; + +// dialog initialization code + +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogextra1", Apply); +document.addEventListener("dialogcancel", onCancel); + +function Startup() { + gActiveEditor = GetCurrentTableEditor(); + if (!gActiveEditor) { + window.close(); + return; + } + + try { + gSelection = gActiveEditor.selection; + } catch (e) {} + if (!gSelection) { + return; + } + + // Get dialog widgets - Table Panel + gDialog.TableRowsInput = document.getElementById("TableRowsInput"); + gDialog.TableColumnsInput = document.getElementById("TableColumnsInput"); + gDialog.TableWidthInput = document.getElementById("TableWidthInput"); + gDialog.TableWidthUnits = document.getElementById("TableWidthUnits"); + gDialog.TableHeightInput = document.getElementById("TableHeightInput"); + gDialog.TableHeightUnits = document.getElementById("TableHeightUnits"); + try { + if ( + !Services.prefs.getBoolPref("editor.use_css") || + gActiveEditor.flags & 1 + ) { + gUseCSS = false; + var tableHeightLabel = document.getElementById("TableHeightLabel"); + tableHeightLabel.remove(); + gDialog.TableHeightInput.remove(); + gDialog.TableHeightUnits.remove(); + } + } catch (e) {} + gDialog.BorderWidthInput = document.getElementById("BorderWidthInput"); + gDialog.SpacingInput = document.getElementById("SpacingInput"); + gDialog.PaddingInput = document.getElementById("PaddingInput"); + gDialog.TableAlignList = document.getElementById("TableAlignList"); + gDialog.TableCaptionList = document.getElementById("TableCaptionList"); + gDialog.TableInheritColor = document.getElementById("TableInheritColor"); + gDialog.TabBox = document.getElementById("TabBox"); + + // Cell Panel + gDialog.SelectionList = document.getElementById("SelectionList"); + gDialog.PreviousButton = document.getElementById("PreviousButton"); + gDialog.NextButton = document.getElementById("NextButton"); + // Currently, we always apply changes and load new attributes when changing selection + // (Let's keep this for possible future use) + // gDialog.ApplyBeforeMove = document.getElementById("ApplyBeforeMove"); + // gDialog.KeepCurrentData = document.getElementById("KeepCurrentData"); + + gDialog.CellHeightInput = document.getElementById("CellHeightInput"); + gDialog.CellHeightUnits = document.getElementById("CellHeightUnits"); + gDialog.CellWidthInput = document.getElementById("CellWidthInput"); + gDialog.CellWidthUnits = document.getElementById("CellWidthUnits"); + gDialog.CellHAlignList = document.getElementById("CellHAlignList"); + gDialog.CellVAlignList = document.getElementById("CellVAlignList"); + gDialog.CellInheritColor = document.getElementById("CellInheritColor"); + gDialog.CellStyleList = document.getElementById("CellStyleList"); + gDialog.TextWrapList = document.getElementById("TextWrapList"); + + // In cell panel, user must tell us which attributes to apply via checkboxes, + // else we would apply values from one cell to ALL in selection + // and that's probably not what they expect! + gDialog.CellHeightCheckbox = document.getElementById("CellHeightCheckbox"); + gDialog.CellWidthCheckbox = document.getElementById("CellWidthCheckbox"); + gDialog.CellHAlignCheckbox = document.getElementById("CellHAlignCheckbox"); + gDialog.CellVAlignCheckbox = document.getElementById("CellVAlignCheckbox"); + gDialog.CellStyleCheckbox = document.getElementById("CellStyleCheckbox"); + gDialog.TextWrapCheckbox = document.getElementById("TextWrapCheckbox"); + gDialog.CellColorCheckbox = document.getElementById("CellColorCheckbox"); + gDialog.TableTab = document.getElementById("TableTab"); + gDialog.CellTab = document.getElementById("CellTab"); + gDialog.AdvancedEditCell = document.getElementById("AdvancedEditButton2"); + // Save "normal" tooltip message for Advanced Edit button + gDialog.AdvancedEditCellToolTipText = + gDialog.AdvancedEditCell.getAttribute("tooltiptext"); + + try { + gTableElement = gActiveEditor.getElementOrParentByTagName("table", null); + } catch (e) {} + if (!gTableElement) { + dump("Failed to get table element!\n"); + window.close(); + return; + } + globalTableElement = gTableElement.cloneNode(false); + + var tagNameObj = { value: "" }; + var countObj = { value: 0 }; + var tableOrCellElement; + try { + tableOrCellElement = gActiveEditor.getSelectedOrParentTableElement( + tagNameObj, + countObj + ); + } catch (e) {} + + if (tagNameObj.value == "td") { + // We are in a cell + gSelectedCellCount = countObj.value; + gCellElement = tableOrCellElement; + globalCellElement = gCellElement.cloneNode(false); + + // Tells us whether cell, row, or column is selected + try { + gSelectedCellsType = gActiveEditor.getSelectedCellsType(gTableElement); + } catch (e) {} + + // Ignore types except Cell, Row, and Column + if ( + gSelectedCellsType < SELECT_CELL || + gSelectedCellsType > SELECT_COLUMN + ) { + gSelectedCellsType = SELECT_CELL; + } + + // Be sure at least 1 cell is selected. + // (If the count is 0, then we were inside the cell.) + if (gSelectedCellCount == 0) { + DoCellSelection(); + } + + // Get location in the cell map + var rowIndexObj = { value: 0 }; + var colIndexObj = { value: 0 }; + try { + gActiveEditor.getCellIndexes(gCellElement, rowIndexObj, colIndexObj); + } catch (e) {} + gCurRowIndex = rowIndexObj.value; + gCurColIndex = colIndexObj.value; + + // We save the current colspan to quickly + // move selection from from cell to cell + if (GetCellData(gCurRowIndex, gCurColIndex)) { + gCurColSpan = gCellData.colSpan; + } + + // Starting TabPanel name is passed in + if (window.arguments[1] == "CellPanel") { + gDialog.TabBox.selectedTab = gDialog.CellTab; + } + } + + if (gDialog.TabBox.selectedTab == gDialog.TableTab) { + // We may call this with table selected, but no cell, + // so disable the Cell Properties tab + if (!gCellElement) { + // XXX: Disabling of tabs is currently broken, so for + // now we'll just remove the tab completely. + // gDialog.CellTab.disabled = true; + gDialog.CellTab.remove(); + } + } + + // Note: we must use gTableElement, not globalTableElement for these, + // thus we should not put this in InitDialog. + // Instead, monitor desired counts with separate globals + var rowCountObj = { value: 0 }; + var colCountObj = { value: 0 }; + try { + gActiveEditor.getTableSize(gTableElement, rowCountObj, colCountObj); + } catch (e) {} + + gRowCount = rowCountObj.value; + gLastRowIndex = gRowCount - 1; + gColCount = colCountObj.value; + gLastColIndex = gColCount - 1; + + // Set appropriate icons and enable state for the Previous/Next buttons + SetSelectionButtons(); + + // If only one cell in table, disable change-selection widgets + if (gRowCount == 1 && gColCount == 1) { + gDialog.SelectionList.setAttribute("disabled", "true"); + } + + // User can change these via textboxes + gNewRowCount = gRowCount; + gNewColCount = gColCount; + + // This flag is used to control whether set check state + // on "set attribute" checkboxes + // (Advanced Edit dialog use calls InitDialog when done) + gAdvancedEditUsed = false; + InitDialog(); + gAdvancedEditUsed = true; + + // If first initializing, we really aren't changing anything + gCellDataChanged = false; + + SetWindowLocation(); +} + +function InitDialog() { + // Get Table attributes + gDialog.TableRowsInput.value = gRowCount; + gDialog.TableColumnsInput.value = gColCount; + gDialog.TableWidthInput.value = InitPixelOrPercentMenulist( + globalTableElement, + gTableElement, + "width", + "TableWidthUnits", + gPercent + ); + if (gUseCSS) { + gDialog.TableHeightInput.value = InitPixelOrPercentMenulist( + globalTableElement, + gTableElement, + "height", + "TableHeightUnits", + gPercent + ); + } + gDialog.BorderWidthInput.value = globalTableElement.border; + gDialog.SpacingInput.value = globalTableElement.cellSpacing; + gDialog.PaddingInput.value = globalTableElement.cellPadding; + + var marginLeft = GetHTMLOrCSSStyleValue( + globalTableElement, + "align", + "margin-left" + ); + var marginRight = GetHTMLOrCSSStyleValue( + globalTableElement, + "align", + "margin-right" + ); + var halign = marginLeft.toLowerCase() + " " + marginRight.toLowerCase(); + if (halign == "center center" || halign == "auto auto") { + gDialog.TableAlignList.value = "center"; + } else if (halign == "right right" || halign == "auto 0px") { + gDialog.TableAlignList.value = "right"; + } else { + // Default is left. + gDialog.TableAlignList.value = "left"; + } + + // Be sure to get caption from table in doc, not the copied "globalTableElement" + gTableCaptionElement = gTableElement.caption; + if (gTableCaptionElement) { + var align = GetHTMLOrCSSStyleValue( + gTableCaptionElement, + "align", + "caption-side" + ); + if (align != "bottom" && align != "left" && align != "right") { + align = "top"; + } + gDialog.TableCaptionList.value = align; + } + + gTableColor = GetHTMLOrCSSStyleValue( + globalTableElement, + bgcolor, + cssBackgroundColorStr + ); + gTableColor = ConvertRGBColorIntoHEXColor(gTableColor); + SetColor("tableBackgroundCW", gTableColor); + + InitCellPanel(); +} + +function InitCellPanel() { + // Get cell attributes + if (globalCellElement) { + // This assumes order of items is Cell, Row, Column + gDialog.SelectionList.value = gSelectedCellsType; + + var previousValue = gDialog.CellHeightInput.value; + gDialog.CellHeightInput.value = InitPixelOrPercentMenulist( + globalCellElement, + gCellElement, + "height", + "CellHeightUnits", + gPixel + ); + gDialog.CellHeightCheckbox.checked = + gAdvancedEditUsed && previousValue != gDialog.CellHeightInput.value; + + previousValue = gDialog.CellWidthInput.value; + gDialog.CellWidthInput.value = InitPixelOrPercentMenulist( + globalCellElement, + gCellElement, + "width", + "CellWidthUnits", + gPixel + ); + gDialog.CellWidthCheckbox.checked = + gAdvancedEditUsed && previousValue != gDialog.CellWidthInput.value; + + var previousIndex = gDialog.CellVAlignList.selectedIndex; + var valign = GetHTMLOrCSSStyleValue( + globalCellElement, + "valign", + "vertical-align" + ).toLowerCase(); + if (valign == topStr || valign == bottomStr) { + gDialog.CellVAlignList.value = valign; + } else { + // Default is middle. + gDialog.CellVAlignList.value = defVAlign; + } + + gDialog.CellVAlignCheckbox.checked = + gAdvancedEditUsed && + previousIndex != gDialog.CellVAlignList.selectedIndex; + + previousIndex = gDialog.CellHAlignList.selectedIndex; + + gAlignWasChar = false; + + var halign = GetHTMLOrCSSStyleValue( + globalCellElement, + "align", + "text-align" + ).toLowerCase(); + switch (halign) { + case centerStr: + case rightStr: + case justifyStr: + gDialog.CellHAlignList.value = halign; + break; + case charStr: + // We don't support UI for this because layout doesn't work: bug 2212. + // Remember that's what they had so we don't change it + // unless they change the alignment by using the menulist + gAlignWasChar = true; + // Fall through to use show default alignment in menu + default: + // Default depends on cell type (TH is "center", TD is "left") + gDialog.CellHAlignList.value = + globalCellElement.nodeName.toLowerCase() == "th" ? "center" : "left"; + break; + } + + gDialog.CellHAlignCheckbox.checked = + gAdvancedEditUsed && + previousIndex != gDialog.CellHAlignList.selectedIndex; + + previousIndex = gDialog.CellStyleList.selectedIndex; + gDialog.CellStyleList.value = globalCellElement.nodeName.toLowerCase(); + gDialog.CellStyleCheckbox.checked = + gAdvancedEditUsed && previousIndex != gDialog.CellStyleList.selectedIndex; + + previousIndex = gDialog.TextWrapList.selectedIndex; + if ( + GetHTMLOrCSSStyleValue(globalCellElement, "nowrap", "white-space") == + "nowrap" + ) { + gDialog.TextWrapList.value = "nowrap"; + } else { + gDialog.TextWrapList.value = "wrap"; + } + gDialog.TextWrapCheckbox.checked = + gAdvancedEditUsed && previousIndex != gDialog.TextWrapList.selectedIndex; + + previousValue = gCellColor; + gCellColor = GetHTMLOrCSSStyleValue( + globalCellElement, + bgcolor, + cssBackgroundColorStr + ); + gCellColor = ConvertRGBColorIntoHEXColor(gCellColor); + SetColor("cellBackgroundCW", gCellColor); + gDialog.CellColorCheckbox.checked = + gAdvancedEditUsed && previousValue != gCellColor; + + // We want to set this true in case changes came + // from Advanced Edit dialog session (must assume something changed) + gCellDataChanged = true; + } +} + +function GetCellData(rowIndex, colIndex) { + // Get actual rowspan and colspan + var startRowIndexObj = { value: 0 }; + var startColIndexObj = { value: 0 }; + var rowSpanObj = { value: 0 }; + var colSpanObj = { value: 0 }; + var actualRowSpanObj = { value: 0 }; + var actualColSpanObj = { value: 0 }; + var isSelectedObj = { value: false }; + + try { + gActiveEditor.getCellDataAt( + gTableElement, + rowIndex, + colIndex, + gCellData, + startRowIndexObj, + startColIndexObj, + rowSpanObj, + colSpanObj, + actualRowSpanObj, + actualColSpanObj, + isSelectedObj + ); + // We didn't find a cell + if (!gCellData.value) { + return false; + } + } catch (ex) { + return false; + } + + gCellData.startRowIndex = startRowIndexObj.value; + gCellData.startColIndex = startColIndexObj.value; + gCellData.rowSpan = rowSpanObj.value; + gCellData.colSpan = colSpanObj.value; + gCellData.actualRowSpan = actualRowSpanObj.value; + gCellData.actualColSpan = actualColSpanObj.value; + gCellData.isSelected = isSelectedObj.value; + return true; +} + +function SelectCellHAlign() { + SetCheckbox("CellHAlignCheckbox"); + // Once user changes the alignment, + // we lose their original "CharAt" alignment" + gAlignWasChar = false; +} + +function GetColorAndUpdate(ColorWellID) { + var colorWell = document.getElementById(ColorWellID); + if (!colorWell) { + return; + } + + var colorObj = { + Type: "", + TableColor: 0, + CellColor: 0, + NoDefault: false, + Cancel: false, + BackgroundColor: 0, + }; + + switch (ColorWellID) { + case "tableBackgroundCW": + colorObj.Type = "Table"; + colorObj.TableColor = gTableColor; + break; + case "cellBackgroundCW": + colorObj.Type = "Cell"; + colorObj.CellColor = gCellColor; + break; + } + window.openDialog( + "chrome://messenger/content/messengercompose/EdColorPicker.xhtml", + "_blank", + "chrome,close,titlebar,modal", + "", + colorObj + ); + + // User canceled the dialog + if (colorObj.Cancel) { + return; + } + + switch (ColorWellID) { + case "tableBackgroundCW": + gTableColor = colorObj.BackgroundColor; + SetColor(ColorWellID, gTableColor); + break; + case "cellBackgroundCW": + gCellColor = colorObj.BackgroundColor; + SetColor(ColorWellID, gCellColor); + SetCheckbox("CellColorCheckbox"); + break; + } +} + +function SetColor(ColorWellID, color) { + // Save the color + if (ColorWellID == "cellBackgroundCW") { + if (color) { + try { + gActiveEditor.setAttributeOrEquivalent( + globalCellElement, + bgcolor, + color, + true + ); + } catch (e) {} + gDialog.CellInheritColor.collapsed = true; + } else { + try { + gActiveEditor.removeAttributeOrEquivalent( + globalCellElement, + bgcolor, + true + ); + } catch (e) {} + // Reveal addition message explaining "default" color + gDialog.CellInheritColor.collapsed = false; + } + } else { + if (color) { + try { + gActiveEditor.setAttributeOrEquivalent( + globalTableElement, + bgcolor, + color, + true + ); + } catch (e) {} + gDialog.TableInheritColor.collapsed = true; + } else { + try { + gActiveEditor.removeAttributeOrEquivalent( + globalTableElement, + bgcolor, + true + ); + } catch (e) {} + gDialog.TableInheritColor.collapsed = false; + } + SetCheckbox("CellColorCheckbox"); + } + + setColorWell(ColorWellID, color); +} + +function ChangeSelectionToFirstCell() { + if (!GetCellData(0, 0)) { + dump("Can't find first cell in table!\n"); + return; + } + gCellElement = gCellData.value; + globalCellElement = gCellElement; + + gCurRowIndex = 0; + gCurColIndex = 0; + ChangeSelection(RESET_SELECTION); +} + +function ChangeSelection(newType) { + newType = Number(newType); + + if (gSelectedCellsType == newType) { + return; + } + + if (newType == RESET_SELECTION) { + // Restore selection to existing focus cell + gSelection.collapse(gCellElement, 0); + } else { + gSelectedCellsType = newType; + } + + // Keep the same focus gCellElement, just change the type + DoCellSelection(); + SetSelectionButtons(); + + // Note: globalCellElement should still be a clone of gCellElement +} + +function MoveSelection(forward) { + var newRowIndex = gCurRowIndex; + var newColIndex = gCurColIndex; + var inRow = false; + + if (gSelectedCellsType == SELECT_ROW) { + newRowIndex += forward ? 1 : -1; + + // Wrap around if before first or after last row + if (newRowIndex < 0) { + newRowIndex = gLastRowIndex; + } else if (newRowIndex > gLastRowIndex) { + newRowIndex = 0; + } + inRow = true; + + // Use first cell in row for focus cell + newColIndex = 0; + } else { + // Cell or column: + if (!forward) { + newColIndex--; + } + + if (gSelectedCellsType == SELECT_CELL) { + // Skip to next cell + if (forward) { + newColIndex += gCurColSpan; + } + } else { + // SELECT_COLUMN + // Use first cell in column for focus cell + newRowIndex = 0; + + // Don't skip by colspan, + // but find first cell in next cellmap column + if (forward) { + newColIndex++; + } + } + + if (newColIndex < 0) { + // Request is before the first cell in column + + // Wrap to last cell in column + newColIndex = gLastColIndex; + + if (gSelectedCellsType == SELECT_CELL) { + // If moving by cell, also wrap to previous... + if (newRowIndex > 0) { + newRowIndex -= 1; + } else { + // ...or the last row. + newRowIndex = gLastRowIndex; + } + + inRow = true; + } + } else if (newColIndex > gLastColIndex) { + // Request is after the last cell in column + + // Wrap to first cell in column + newColIndex = 0; + + if (gSelectedCellsType == SELECT_CELL) { + // If moving by cell, also wrap to next... + if (newRowIndex < gLastRowIndex) { + newRowIndex++; + } else { + // ...or the first row. + newRowIndex = 0; + } + + inRow = true; + } + } + } + + // Get the cell at the new location + do { + if (!GetCellData(newRowIndex, newColIndex)) { + dump("MoveSelection: CELL NOT FOUND\n"); + return; + } + if (inRow) { + if (gCellData.startRowIndex == newRowIndex) { + break; + } else { + // Cell spans from a row above, look for the next cell in row. + newRowIndex += gCellData.actualRowSpan; + } + } else if (gCellData.startColIndex == newColIndex) { + break; + } else { + // Cell spans from a Col above, look for the next cell in column + newColIndex += gCellData.actualColSpan; + } + } while (true); + + // Save data for current selection before changing + if (gCellDataChanged) { + // && gDialog.ApplyBeforeMove.checked) + if (!ValidateCellData()) { + return; + } + + gActiveEditor.beginTransaction(); + // Apply changes to all selected cells + ApplyCellAttributes(); + gActiveEditor.endTransaction(); + + SetCloseButton(); + } + + // Set cell and other data for new selection + gCellElement = gCellData.value; + + // Save globals for new current cell + gCurRowIndex = gCellData.startRowIndex; + gCurColIndex = gCellData.startColIndex; + gCurColSpan = gCellData.actualColSpan; + + // Copy for new global cell + globalCellElement = gCellElement.cloneNode(false); + + // Change the selection + DoCellSelection(); + + // Scroll page so new selection is visible + // Using SELECTION_ANCHOR_REGION makes the upper-left corner of first selected cell + // the point to bring into view. + try { + var selectionController = gActiveEditor.selectionController; + selectionController.scrollSelectionIntoView( + selectionController.SELECTION_NORMAL, + selectionController.SELECTION_ANCHOR_REGION, + true + ); + } catch (e) {} + + // Reinitialize dialog using new cell + // if (!gDialog.KeepCurrentData.checked) + // Setting this false unchecks all "set attributes" checkboxes + gAdvancedEditUsed = false; + InitCellPanel(); + gAdvancedEditUsed = true; +} + +function DoCellSelection() { + // Collapse selection into to the focus cell + // so editor uses that as start cell + gSelection.collapse(gCellElement, 0); + + var tagNameObj = { value: "" }; + var countObj = { value: 0 }; + try { + switch (gSelectedCellsType) { + case SELECT_CELL: + gActiveEditor.selectTableCell(); + break; + case SELECT_ROW: + gActiveEditor.selectTableRow(); + break; + default: + gActiveEditor.selectTableColumn(); + break; + } + // Get number of cells selected + gActiveEditor.getSelectedOrParentTableElement(tagNameObj, countObj); + } catch (e) {} + + if (tagNameObj.value == "td") { + gSelectedCellCount = countObj.value; + } else { + gSelectedCellCount = 0; + } + + // Currently, we can only allow advanced editing on ONE cell element at a time + // else we ignore CSS, JS, and HTML attributes not already in dialog + SetElementEnabled(gDialog.AdvancedEditCell, gSelectedCellCount == 1); + + gDialog.AdvancedEditCell.setAttribute( + "tooltiptext", + gSelectedCellCount > 1 + ? GetString("AdvancedEditForCellMsg") + : gDialog.AdvancedEditCellToolTipText + ); +} + +function SetSelectionButtons() { + if (gSelectedCellsType == SELECT_ROW) { + // Trigger CSS to set images of up and down arrows + gDialog.PreviousButton.setAttribute("type", "row"); + gDialog.NextButton.setAttribute("type", "row"); + } else { + // or images of left and right arrows + gDialog.PreviousButton.setAttribute("type", "col"); + gDialog.NextButton.setAttribute("type", "col"); + } + DisableSelectionButtons( + (gSelectedCellsType == SELECT_ROW && gRowCount == 1) || + (gSelectedCellsType == SELECT_COLUMN && gColCount == 1) || + (gRowCount == 1 && gColCount == 1) + ); +} + +function DisableSelectionButtons(disable) { + gDialog.PreviousButton.setAttribute("disabled", disable ? "true" : "false"); + gDialog.NextButton.setAttribute("disabled", disable ? "true" : "false"); +} + +function SwitchToValidatePanel() { + if (gDialog.TabBox.selectedTab != gValidateTab) { + gDialog.TabBox.selectedTab = gValidateTab; + } +} + +function SetAlign(listID, defaultValue, element, attName) { + var value = document.getElementById(listID).value; + if (value == defaultValue) { + try { + gActiveEditor.removeAttributeOrEquivalent(element, attName, true); + } catch (e) {} + } else { + try { + gActiveEditor.setAttributeOrEquivalent(element, attName, value, true); + } catch (e) {} + } +} + +function ValidateTableData() { + gValidateTab = gDialog.TableTab; + gNewRowCount = Number( + ValidateNumber(gDialog.TableRowsInput, null, 1, gMaxRows, null, true, true) + ); + if (gValidationError) { + return false; + } + + gNewColCount = Number( + ValidateNumber( + gDialog.TableColumnsInput, + null, + 1, + gMaxColumns, + null, + true, + true + ) + ); + if (gValidationError) { + return false; + } + + // If user is deleting any cells, get confirmation + // (This is a global to the dialog and we ask only once per dialog session) + if (!gCanDelete && (gNewRowCount < gRowCount || gNewColCount < gColCount)) { + if ( + ConfirmWithTitle( + GetString("DeleteTableTitle"), + GetString("DeleteTableMsg"), + GetString("DeleteCells") + ) + ) { + gCanDelete = true; + } else { + SetTextboxFocus( + gNewRowCount < gRowCount + ? gDialog.TableRowsInput + : gDialog.TableColumnsInput + ); + return false; + } + } + + ValidateNumber( + gDialog.TableWidthInput, + gDialog.TableWidthUnits, + 1, + gMaxTableSize, + globalTableElement, + "width" + ); + if (gValidationError) { + return false; + } + + if (gUseCSS) { + ValidateNumber( + gDialog.TableHeightInput, + gDialog.TableHeightUnits, + 1, + gMaxTableSize, + globalTableElement, + "height" + ); + if (gValidationError) { + return false; + } + } + + ValidateNumber( + gDialog.BorderWidthInput, + null, + 0, + gMaxPixels, + globalTableElement, + "border" + ); + // TODO: Deal with "BORDER" without value issue + if (gValidationError) { + return false; + } + + ValidateNumber( + gDialog.SpacingInput, + null, + 0, + gMaxPixels, + globalTableElement, + "cellspacing" + ); + if (gValidationError) { + return false; + } + + ValidateNumber( + gDialog.PaddingInput, + null, + 0, + gMaxPixels, + globalTableElement, + "cellpadding" + ); + if (gValidationError) { + return false; + } + + SetAlign("TableAlignList", defHAlign, globalTableElement, "align"); + + // Color is set on globalCellElement immediately + return true; +} + +function ValidateCellData() { + gValidateTab = gDialog.CellTab; + + if (gDialog.CellHeightCheckbox.checked) { + ValidateNumber( + gDialog.CellHeightInput, + gDialog.CellHeightUnits, + 1, + gMaxTableSize, + globalCellElement, + "height" + ); + if (gValidationError) { + return false; + } + } + + if (gDialog.CellWidthCheckbox.checked) { + ValidateNumber( + gDialog.CellWidthInput, + gDialog.CellWidthUnits, + 1, + gMaxTableSize, + globalCellElement, + "width" + ); + if (gValidationError) { + return false; + } + } + + if (gDialog.CellHAlignCheckbox.checked) { + var hAlign = gDialog.CellHAlignList.value; + + // Horizontal alignment is complicated by "char" type + // We don't change current values if user didn't edit alignment + if (!gAlignWasChar) { + globalCellElement.removeAttribute(charStr); + + // Always set "align" attribute, + // so the default "left" is effective in a cell + // when parent row has align set. + globalCellElement.setAttribute("align", hAlign); + } + } + + if (gDialog.CellVAlignCheckbox.checked) { + // Always set valign (no default in 2nd param) so + // the default "middle" is effective in a cell + // when parent row has valign set. + SetAlign("CellVAlignList", "", globalCellElement, "valign"); + } + + if (gDialog.TextWrapCheckbox.checked) { + if (gDialog.TextWrapList.value == "nowrap") { + try { + gActiveEditor.setAttributeOrEquivalent( + globalCellElement, + "nowrap", + "nowrap", + true + ); + } catch (e) {} + } else { + try { + gActiveEditor.removeAttributeOrEquivalent( + globalCellElement, + "nowrap", + true + ); + } catch (e) {} + } + } + + return true; +} + +function ValidateData() { + var result; + + // Validate current panel first + if (gDialog.TabBox.selectedTab == gDialog.TableTab) { + result = ValidateTableData(); + if (result) { + result = ValidateCellData(); + } + } else { + result = ValidateCellData(); + if (result) { + result = ValidateTableData(); + } + } + if (!result) { + return false; + } + + // Set global element for AdvancedEdit + if (gDialog.TabBox.selectedTab == gDialog.TableTab) { + globalElement = globalTableElement; + } else { + globalElement = globalCellElement; + } + + return true; +} + +function ChangeCellTextbox(textboxID) { + // Filter input for just integers + forceInteger(textboxID); + + if (gDialog.TabBox.selectedTab == gDialog.CellTab) { + gCellDataChanged = true; + } +} + +// Call this when a textbox or menulist is changed +// so the checkbox is automatically set +function SetCheckbox(checkboxID) { + if (checkboxID && checkboxID.length > 0) { + // Set associated checkbox + document.getElementById(checkboxID).checked = true; + } + gCellDataChanged = true; +} + +function ChangeIntTextbox(checkboxID) { + // Set associated checkbox + SetCheckbox(checkboxID); +} + +function CloneAttribute(destElement, srcElement, attr) { + var value = srcElement.getAttribute(attr); + // Use editor methods since we are always + // modifying a table in the document and + // we need transaction system for undo + try { + if (!value || value.length == 0) { + gActiveEditor.removeAttributeOrEquivalent(destElement, attr, false); + } else { + gActiveEditor.setAttributeOrEquivalent(destElement, attr, value, false); + } + } catch (e) {} +} + +/* eslint-disable complexity */ +function ApplyTableAttributes() { + var newAlign = gDialog.TableCaptionList.value; + if (!newAlign) { + newAlign = ""; + } + + if (gTableCaptionElement) { + // Get current alignment + var align = GetHTMLOrCSSStyleValue( + gTableCaptionElement, + "align", + "caption-side" + ).toLowerCase(); + // This is the default + if (!align) { + align = "top"; + } + + if (newAlign == "") { + // Remove existing caption + try { + gActiveEditor.deleteNode(gTableCaptionElement); + } catch (e) {} + gTableCaptionElement = null; + } else if (newAlign != align) { + try { + if (newAlign == "top") { + // This is default, so don't explicitly set it + gActiveEditor.removeAttributeOrEquivalent( + gTableCaptionElement, + "align", + false + ); + } else { + gActiveEditor.setAttributeOrEquivalent( + gTableCaptionElement, + "align", + newAlign, + false + ); + } + } catch (e) {} + } + } else if (newAlign != "") { + // Create and insert a caption: + try { + gTableCaptionElement = gActiveEditor.createElementWithDefaults("caption"); + } catch (e) {} + if (gTableCaptionElement) { + if (newAlign != "top") { + gTableCaptionElement.setAttribute("align", newAlign); + } + + // Insert it into the table - caption is always inserted as first child + try { + gActiveEditor.insertNode(gTableCaptionElement, gTableElement, 0); + } catch (e) {} + + // Put selection back where it was + ChangeSelection(RESET_SELECTION); + } + } + + var countDelta; + var foundCell; + var i; + + if (gNewRowCount != gRowCount) { + countDelta = gNewRowCount - gRowCount; + if (gNewRowCount > gRowCount) { + // Append new rows + // Find first cell in last row + if (GetCellData(gLastRowIndex, 0)) { + try { + // Move selection to the last cell + gSelection.collapse(gCellData.value, 0); + // Insert new rows after it + gActiveEditor.insertTableRow(countDelta, true); + gRowCount = gNewRowCount; + gLastRowIndex = gRowCount - 1; + // Put selection back where it was + ChangeSelection(RESET_SELECTION); + } catch (ex) { + dump("FAILED TO FIND FIRST CELL IN LAST ROW\n"); + } + } + } else if (gCanDelete) { + // Delete rows + // Find first cell starting in first row we delete + var firstDeleteRow = gRowCount + countDelta; + foundCell = false; + for (i = 0; i <= gLastColIndex; i++) { + if (!GetCellData(firstDeleteRow, i)) { + // We failed to find a cell. + break; + } + + if (gCellData.startRowIndex == firstDeleteRow) { + foundCell = true; + break; + } + } + if (foundCell) { + try { + // Move selection to the cell we found + gSelection.collapse(gCellData.value, 0); + gActiveEditor.deleteTableRow(-countDelta); + gRowCount = gNewRowCount; + gLastRowIndex = gRowCount - 1; + if (gCurRowIndex > gLastRowIndex) { + // We are deleting our selection + // move it to start of table + ChangeSelectionToFirstCell(); + } else { + // Put selection back where it was. + ChangeSelection(RESET_SELECTION); + } + } catch (ex) { + dump("FAILED TO FIND FIRST CELL IN LAST ROW\n"); + } + } + } + } + + if (gNewColCount != gColCount) { + countDelta = gNewColCount - gColCount; + + if (gNewColCount > gColCount) { + // Append new columns + // Find last cell in first column + if (GetCellData(0, gLastColIndex)) { + try { + // Move selection to the last cell + gSelection.collapse(gCellData.value, 0); + gActiveEditor.insertTableColumn(countDelta, true); + gColCount = gNewColCount; + gLastColIndex = gColCount - 1; + // Restore selection + ChangeSelection(RESET_SELECTION); + } catch (ex) { + dump("FAILED TO FIND FIRST CELL IN LAST COLUMN\n"); + } + } + } else if (gCanDelete) { + // Delete columns + var firstDeleteCol = gColCount + countDelta; + foundCell = false; + for (i = 0; i <= gLastRowIndex; i++) { + // Find first cell starting in first column we delete + if (!GetCellData(i, firstDeleteCol)) { + // We failed to find a cell. + break; + } + + if (gCellData.startColIndex == firstDeleteCol) { + foundCell = true; + break; + } + } + if (foundCell) { + try { + // Move selection to the cell we found + gSelection.collapse(gCellData.value, 0); + gActiveEditor.deleteTableColumn(-countDelta); + gColCount = gNewColCount; + gLastColIndex = gColCount - 1; + if (gCurColIndex > gLastColIndex) { + ChangeSelectionToFirstCell(); + } else { + ChangeSelection(RESET_SELECTION); + } + } catch (ex) { + dump("FAILED TO FIND FIRST CELL IN LAST ROW\n"); + } + } + } + } + + // Clone all remaining attributes to pick up + // anything changed by Advanced Edit Dialog + try { + gActiveEditor.cloneAttributes(gTableElement, globalTableElement); + } catch (e) {} +} +/* eslint-enable complexity */ + +function ApplyCellAttributes() { + let selectedCells = gActiveEditor.getSelectedCells(); + if (selectedCells.length == 0) { + return; + } + + if (selectedCells.length == 1) { + let cell = selectedCells[0]; + // When only one cell is selected, simply clone entire element, + // thus CSS and JS from Advanced edit is copied + + gActiveEditor.cloneAttributes(cell, globalCellElement); + + if (gDialog.CellStyleCheckbox.checked) { + let currentStyleIndex = cell.nodeName.toLowerCase() == "th" ? 1 : 0; + if (gDialog.CellStyleList.selectedIndex != currentStyleIndex) { + // Switch cell types + // (replaces with new cell and copies attributes and contents) + gActiveEditor.switchTableCellHeaderType(cell); + } + } + } else { + // Apply changes to all selected cells + // XXX THIS DOESN'T COPY ADVANCED EDIT CHANGES! + for (let cell of selectedCells) { + ApplyAttributesToOneCell(cell); + } + } + gCellDataChanged = false; +} + +function ApplyAttributesToOneCell(destElement) { + if (gDialog.CellHeightCheckbox.checked) { + CloneAttribute(destElement, globalCellElement, "height"); + } + + if (gDialog.CellWidthCheckbox.checked) { + CloneAttribute(destElement, globalCellElement, "width"); + } + + if (gDialog.CellHAlignCheckbox.checked) { + CloneAttribute(destElement, globalCellElement, "align"); + CloneAttribute(destElement, globalCellElement, charStr); + } + + if (gDialog.CellVAlignCheckbox.checked) { + CloneAttribute(destElement, globalCellElement, "valign"); + } + + if (gDialog.TextWrapCheckbox.checked) { + CloneAttribute(destElement, globalCellElement, "nowrap"); + } + + if (gDialog.CellStyleCheckbox.checked) { + var newStyleIndex = gDialog.CellStyleList.selectedIndex; + var currentStyleIndex = destElement.nodeName.toLowerCase() == "th" ? 1 : 0; + + if (newStyleIndex != currentStyleIndex) { + // Switch cell types + // (replaces with new cell and copies attributes and contents) + try { + destElement = gActiveEditor.switchTableCellHeaderType(destElement); + } catch (e) {} + } + } + + if (gDialog.CellColorCheckbox.checked) { + CloneAttribute(destElement, globalCellElement, "bgcolor"); + } +} + +function SetCloseButton() { + // Change text on "Cancel" button after Apply is used + if (!gApplyUsed) { + document + .querySelector("dialog") + .setAttribute( + "buttonlabelcancel", + document.querySelector("dialog").getAttribute("buttonlabelclose") + ); + gApplyUsed = true; + } +} + +function Apply() { + if (ValidateData()) { + gActiveEditor.beginTransaction(); + + ApplyTableAttributes(); + + // We may have just a table, so check for cell element + if (globalCellElement) { + ApplyCellAttributes(); + } + + gActiveEditor.endTransaction(); + + SetCloseButton(); + return true; + } + return false; +} + +function onAccept(event) { + // Do same as Apply and close window if ValidateData succeeded + var retVal = Apply(); + if (retVal) { + SaveWindowLocation(); + } else { + event.preventDefault(); + } +} diff --git a/comm/mail/components/compose/content/dialogs/EdTableProps.xhtml b/comm/mail/components/compose/content/dialogs/EdTableProps.xhtml new file mode 100644 index 0000000000..a82d5e18c5 --- /dev/null +++ b/comm/mail/components/compose/content/dialogs/EdTableProps.xhtml @@ -0,0 +1,472 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/EditorDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE window [ <!ENTITY % edTableProperties SYSTEM "chrome://messenger/locale/messengercompose/EditorTableProperties.dtd"> +%edTableProperties; +<!ENTITY % edDialogOverlay SYSTEM "chrome://messenger/locale/messengercompose/EdDialogOverlay.dtd"> +%edDialogOverlay; ]> + +<window + title="&tableWindow.title;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + onload="Startup()" +> + <dialog + id="tableDlg" + buttons="accept,extra1,cancel" + buttonlabelclose="&closeButton.label;" + buttonlabelextra1="&applyButton.label;" + buttonaccesskeyextra1="&applyButton.accesskey;" + > + <!-- Methods common to all editor dialogs --> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://messenger/content/messengercompose/editorUtilities.js" /> + <script src="chrome://messenger/content/messengercompose/EdDialogCommon.js" /> + <script src="chrome://messenger/content/messengercompose/EdTableProps.js" /> + <script src="chrome://messenger/content/dialogShadowDom.js" /> + + <spacer id="location" offsetY="50" persist="offsetX offsetY" /> + + <tabbox id="TabBox"> + <tabs flex="1"> + <tab id="TableTab" label="&tableTab.label;" /> + <tab id="CellTab" label="&cellTab.label;" /> + </tabs> + <tabpanels> + <!-- TABLE PANEL --> + <vbox> + <html:fieldset orient="horizontal"> + <html:legend>&size.label;</html:legend> + <hbox> + <vbox> + <hbox> + <vbox> + <hbox align="center" flex="1"> + <label + id="TableRowsLabel" + value="&tableRows.label;" + accesskey="&tableRows.accessKey;" + control="TableRowsInput" + /> + </hbox> + <hbox align="center" flex="1"> + <label + id="TableColumnsLabel" + value="&tableColumns.label;" + accesskey="&tableColumns.accessKey;" + control="TableColumnsInput" + /> + </hbox> + </vbox> + <vbox> + <html:input + id="TableRowsInput" + type="number" + class="narrow input-inline" + aria-labelledby="TableRowsLabel" + /> + <html:input + id="TableColumnsInput" + type="number" + class="narrow input-inline" + aria-labelledby="TableColumnsLabel" + /> + </vbox> + </hbox> + </vbox> + <vbox> + <html:div class="grid-three-column"> + <html:div class="flex-items-center"> + <label + id="TableHeightLabel" + value="&tableHeight.label;" + accesskey="&tableHeight.accessKey;" + control="TableHeightInput" + /> + </html:div> + <html:div> + <html:input + id="TableHeightInput" + type="number" + class="narrow input-inline" + aria-labelledby="TableHeightLabel" + /> + </html:div> + <html:div class="flex-items-center"> + <menulist id="TableHeightUnits" /> + </html:div> + <html:div class="flex-items-center"> + <label + id="TableWidthLabel" + value="&tableWidth.label;" + accesskey="&tableWidth.accessKey;" + control="TableWidthInput" + /> + </html:div> + <html:div class="flex-items-center"> + <html:input + id="TableWidthInput" + type="number" + class="narrow input-inline" + aria-labelledby="TableWidthLabel" + /> + </html:div> + <html:div class="flex-items-center"> + <menulist id="TableWidthUnits" /> + </html:div> + </html:div> + </vbox> + </hbox> + </html:fieldset> + <html:fieldset> + <html:legend>&tableBorderSpacing.label;</html:legend> + <hbox> + <vbox> + <hbox flex="1" align="center"> + <label + id="BorderWidthLabel" + control="BorderWidthInput" + value="&tableBorderWidth.label;" + accesskey="&tableBorderWidth.accessKey;" + /> + </hbox> + <hbox flex="1" align="center"> + <label + id="SpacingLabel" + control="SpacingInput" + value="&tableSpacing.label;" + accesskey="&tableSpacing.accessKey;" + /> + </hbox> + <hbox flex="1" align="center"> + <label + id="PaddingLabel" + control="PaddingInput" + value="&tablePadding.label;" + accesskey="&tablePadding.accessKey;" + /> + </hbox> + </vbox> + <vbox> + <html:input + id="BorderWidthInput" + type="number" + class="narrow input-inline" + aria-labelledby="BorderWidthLabel" + /> + <html:input + id="SpacingInput" + type="number" + class="narrow input-inline" + aria-labelledby="SpacingLabel" + /> + <html:input + id="PaddingInput" + type="number" + class="narrow input-inline" + aria-labelledby="PaddingLabel" + /> + </vbox> + <vbox> + <hbox flex="1" align="center"> + <label align="start" value="&pixels.label;" /> + </hbox> + <hbox flex="1" align="center"> + <label value="&tablePxBetwCells.label;" /> + </hbox> + <hbox flex="1" align="center"> + <label value="&tablePxBetwBrdrCellContent.label;" /> + </hbox> + </vbox> + </hbox> + </html:fieldset> + <!-- Table Alignment and Caption --> + <hbox flex="1" align="center"> + <label + control="TableAlignList" + value="&tableAlignment.label;" + accesskey="&tableAlignment.accessKey;" + /> + <menulist id="TableAlignList"> + <menupopup> + <menuitem label="&AlignLeft.label;" value="left" /> + <menuitem label="&AlignCenter.label;" value="center" /> + <menuitem label="&AlignRight.label;" value="right" /> + </menupopup> + </menulist> + <spacer class="spacer" /> + <label + control="TableCaptionList" + value="&tableCaption.label;" + accesskey="&tableCaption.accessKey;" + /> + <menulist id="TableCaptionList"> + <menupopup> + <menuitem label="&tableCaptionNone.label;" value="" /> + <menuitem label="&tableCaptionAbove.label;" value="top" /> + <menuitem label="&tableCaptionBelow.label;" value="bottom" /> + <menuitem label="&tableCaptionLeft.label;" value="left" /> + <menuitem label="&tableCaptionRight.label;" value="right" /> + </menupopup> + </menulist> + </hbox> + <separator class="groove" /> + <hbox align="center"> + <label value="&backgroundColor.label;" /> + <button + id="tableBackground" + class="color-button" + oncommand="GetColorAndUpdate('tableBackgroundCW');" + > + <spacer id="tableBackgroundCW" class="color-well" /> + </button> + <spacer class="spacer" /> + <label + id="TableInheritColor" + value="&tableInheritColor.label;" + collapsed="true" + /> + </hbox> + <separator class="groove" /> + <hbox flex="1" align="center"> + <spacer flex="1" /> + <button + id="AdvancedEditButton" + oncommand="onAdvancedEdit();" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <spacer flex="1" /> </vbox + ><!-- Table Panel --> + + <!-- CELL PANEL --> + <vbox> + <html:fieldset> + <html:legend>&cellSelection.label;</html:legend> + <vbox> + <menulist + id="SelectionList" + oncommand="ChangeSelection(event.target.value)" + > + <menupopup> + <!-- JS code assumes order is Cell, Row, Column --> + <menuitem label="&cellSelectCell.label;" value="1" /> + <menuitem label="&cellSelectRow.label;" value="2" /> + <menuitem label="&cellSelectColumn.label;" value="3" /> + </menupopup> + </menulist> + <hbox> + <button + id="PreviousButton" + label="&cellSelectPrevious.label;" + accesskey="&cellSelectPrevious.accessKey;" + oncommand="MoveSelection(0)" + /> + <button + id="NextButton" + label="&cellSelectNext.label;" + accesskey="&cellSelectNext.accessKey;" + oncommand="MoveSelection(1)" + /> + </hbox> + <hbox flex="1"> &applyBeforeChange.label; </hbox> + </vbox> + </html:fieldset> + + <separator class="groove" /> + + <hbox align="center"> + <html:fieldset> + <html:legend>&size.label;</html:legend> + <hbox> + <vbox> + <hbox flex="1" align="center"> + <checkbox + id="CellHeightCheckbox" + label="&tableHeight.label;" + accesskey="&tableHeight.accessKey;" + /> + </hbox> + <hbox flex="1" align="center"> + <checkbox + id="CellWidthCheckbox" + label="&tableWidth.label;" + accesskey="&tableWidth.accessKey;" + /> + </hbox> + </vbox> + <vbox flex="1"> + <hbox flex="1" align="center"> + <html:input + id="CellHeightInput" + type="number" + class="narrow input-inline" + onchange="ChangeIntTextbox('CellHeightCheckbox');" + aria-labelledby="CellHeightCheckbox" + /> + </hbox> + <hbox flex="1" align="center"> + <html:input + id="CellWidthInput" + type="number" + class="narrow input-inline" + onchange="ChangeIntTextbox('CellWidthCheckbox');" + aria-labelledby="CellWidthCheckbox" + /> + </hbox> + </vbox> + <vbox> + <hbox flex="1" align="center"> + <menulist + id="CellHeightUnits" + oncommand="SetCheckbox('CellHeightCheckbox');" + /> + </hbox> + <hbox flex="1" align="center"> + <menulist + id="CellWidthUnits" + oncommand="SetCheckbox('CellWidthCheckbox');" + /> + </hbox> + </vbox> + </hbox> + </html:fieldset> + <html:fieldset> + <html:legend>&cellContentAlignment.label;</html:legend> + <hbox> + <vbox> + <hbox align="center" flex="1"> + <checkbox + id="CellVAlignCheckbox" + label="&cellVertical.label;" + accesskey="&cellVertical.accessKey;" + /> + </hbox> + <hbox align="center" flex="1"> + <checkbox + id="CellHAlignCheckbox" + label="&cellHorizontal.label;" + accesskey="&cellHorizontal.accessKey;" + /> + </hbox> + </vbox> + <vbox flex="1"> + <menulist + id="CellVAlignList" + oncommand="SetCheckbox('CellVAlignCheckbox');" + > + <menupopup> + <menuitem label="&cellAlignTop.label;" value="top" /> + <menuitem + label="&cellAlignMiddle.label;" + value="middle" + /> + <menuitem + label="&cellAlignBottom.label;" + value="bottom" + /> + </menupopup> + </menulist> + <menulist id="CellHAlignList" oncommand="SelectCellHAlign()"> + <menupopup> + <menuitem label="&AlignLeft.label;" value="left" /> + <menuitem label="&AlignCenter.label;" value="center" /> + <menuitem label="&AlignRight.label;" value="right" /> + <menuitem + label="&cellAlignJustify.label;" + value="justify" + /> + </menupopup> + </menulist> + </vbox> + </hbox> + </html:fieldset> + </hbox> + <spacer class="spacer" /> + <hbox align="center"> + <checkbox + id="CellStyleCheckbox" + label="&cellStyle.label;" + accesskey="&cellStyle.accessKey;" + /> + <menulist + id="CellStyleList" + oncommand="SetCheckbox('CellStyleCheckbox');" + > + <menupopup> + <menuitem label="&cellNormal.label;" value="td" /> + <menuitem label="&cellHeader.label;" value="th" /> + </menupopup> + </menulist> + <spacer flex="1" /> + <checkbox + id="TextWrapCheckbox" + label="&cellTextWrap.label;" + accesskey="&cellTextWrap.accessKey;" + /> + <menulist + id="TextWrapList" + oncommand="SetCheckbox('TextWrapCheckbox');" + > + <menupopup> + <menuitem label="&cellWrap.label;" value="wrap" /> + <menuitem label="&cellNoWrap.label;" value="nowrap" /> + </menupopup> + </menulist> + </hbox> + <separator class="groove" /> + <hbox align="center"> + <checkbox + id="CellColorCheckbox" + label="&backgroundColor.label;" + accesskey="&backgroundColor.accessKey;" + /> + <button + class="color-button" + oncommand="GetColorAndUpdate('cellBackgroundCW');" + > + <spacer id="cellBackgroundCW" class="color-well" /> + </button> + <spacer class="spacer" /> + <label + id="CellInheritColor" + value="&cellInheritColor.label;" + collapsed="true" + /> + </hbox> + <separator class="groove" /> + <hbox align="center"> + <description class="wrap" flex="1" style="width: 1em" + >&cellUseCheckboxHelp.label;</description + > + <button + id="AdvancedEditButton2" + oncommand="onAdvancedEdit()" + label="&AdvancedEditButton.label;" + accesskey="&AdvancedEditButton.accessKey;" + tooltiptext="&AdvancedEditButton.tooltip;" + /> + </hbox> + <spacer flex="1" /> </vbox + ><!-- Cell Panel --> + </tabpanels> + </tabbox> + <spacer class="spacer" /> + </dialog> +</window> |